import path from 'node:path';
import { existsSync } from 'node:fs';
import Fastify from 'fastify';
import type { FastifyRequest } from 'fastify';
import fastifyWebsocket from '@fastify/websocket';
import fastifyStatic from '@fastify/static';
import type { WebSocket } from 'ws';
import { TournamentState } from './tournament';
import { startScheduler } from './ingest/scheduler';
import { ModelEngine } from './model';
import { buildPreview, buildTeamProfile } from './preview';
import { db, healthAll } from './db/db';
import { canonicalTeam } from '../../src/lib/teams';
import type { MatchStatus, ServerMessage } from '../../src/lib/types';

const PORT = Number(process.env.PORT ?? 8787);
const STATIC_DIR = path.resolve(process.env.STATIC_DIR ?? path.join(process.cwd(), 'dist'));
const IS_PROD = process.env.NODE_ENV === 'production';
const ALLOWED_ORIGINS = (process.env.ALLOWED_ORIGINS ?? '').split(',').map((s) => s.trim()).filter(Boolean);
const MAX_WS = Number(process.env.MAX_WS) || 200;
const HEARTBEAT_MS = 30_000;

export function buildServer() {
  const app = Fastify({ trustProxy: true, bodyLimit: 256 * 1024 });
  // Tolerate body-less requests that still set content-type: application/json.
  app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
    const s = (body as string).trim();
    if (!s) return done(null, undefined);
    try { done(null, JSON.parse(s)); }
    catch { const err = new Error('Invalid JSON') as Error & { statusCode?: number }; err.statusCode = 400; done(err, undefined); }
  });

  db(); // open + migrate SQLite before anything reads it
  const state = new TournamentState(STATIC_DIR);
  const model = new ModelEngine(STATIC_DIR);
  model.recompute(state.allFixtures());
  const clients = new Set<WebSocket>();

  const snapshotMessage = (): string =>
    JSON.stringify({ t: 'snapshot', snapshot: state.snapshot() } satisfies ServerMessage);
  const predictionsMessage = (): string =>
    JSON.stringify({ t: 'predictions', model: model.current() } satisfies ServerMessage);

  const sendAll = (msg: string): void => {
    for (const ws of clients) {
      try { ws.send(msg); } catch { /* closed */ }
    }
  };

  /** Re-push the snapshot, and predictions too if the finished-result set moved. */
  const broadcast = (): void => {
    sendAll(snapshotMessage());
    if (model.recompute(state.allFixtures())) sendAll(predictionsMessage());
  };

  // ---- REST: snapshot + predictions (initial load + a no-WS fallback) ----
  app.get('/api/snapshot', async () => state.snapshot());
  app.get('/api/predictions', async () => model.current());
  app.get('/api/sources/health', async () => ({ sources: healthAll() }));
  app.get('/api/preview/:num', async (req, reply) => {
    const num = Number((req.params as { num: string }).num);
    const preview = Number.isFinite(num) ? buildPreview(num, state, model) : null;
    return preview ?? reply.code(404).send({ error: 'no such fixture' });
  });
  app.get('/api/team/:name', async (req, reply) => {
    const name = canonicalTeam(decodeURIComponent((req.params as { name: string }).name));
    const profile = buildTeamProfile(name, state, model);
    return profile ?? reply.code(404).send({ error: 'no such team' });
  });
  app.get('/healthz', async () => ({ ok: true }));

  // ---- dev-only: inject a score to exercise the live → standings → WS loop ----
  if (!IS_PROD) {
    app.post('/api/dev/score', async (req, reply) => {
      const b = (req.body ?? {}) as {
        num?: number; homeScore?: number; awayScore?: number; status?: MatchStatus; minute?: number;
      };
      if (typeof b.num !== 'number') return reply.code(400).send({ error: 'num required' });
      const status: MatchStatus = b.status ?? 'finished';
      const f = state.applyScore(
        b.num,
        b.homeScore ?? null,
        b.awayScore ?? null,
        status,
        b.minute ?? null,
        'football-data',
      );
      if (!f) return reply.code(404).send({ error: 'no such fixture' });
      broadcast();
      return { ok: true, fixture: f };
    });
  }

  // ---- WebSocket: push the snapshot on connect and on every change ----
  void app.register(fastifyWebsocket);
  void app.register(async (instance) => {
    instance.get('/ws', { websocket: true }, (socket: WebSocket, req: FastifyRequest) => {
      const origin = req.headers.origin;
      if (ALLOWED_ORIGINS.length && origin && !ALLOWED_ORIGINS.includes(origin)) {
        socket.close(1008, 'origin');
        return;
      }
      if (clients.size >= MAX_WS) { socket.close(1013, 'busy'); return; }
      clients.add(socket);
      try { socket.send(snapshotMessage()); socket.send(predictionsMessage()); } catch { /* closed */ }

      let alive = true;
      socket.on('pong', () => { alive = true; });
      const heartbeat = setInterval(() => {
        if (!alive) { try { socket.terminate(); } catch { /* gone */ } return; }
        alive = false;
        try { socket.ping(); } catch { /* closed */ }
      }, HEARTBEAT_MS);

      socket.on('close', () => { clearInterval(heartbeat); clients.delete(socket); });
      socket.on('error', () => { clearInterval(heartbeat); clients.delete(socket); });
    });
  });

  // ---- ingestion scheduler (ESPN primary; football-data / SofaScore fallback) ----
  const stopIngest = startScheduler(state, () => broadcast());
  app.addHook('onClose', async () => stopIngest());

  // ---- static SPA (prod) with history fallback ----
  if (existsSync(STATIC_DIR)) {
    void app.register(fastifyStatic, { root: STATIC_DIR, wildcard: false });
    app.setNotFoundHandler((req, reply) => {
      if (req.raw.url?.startsWith('/ws')) return reply.code(426).send('Upgrade Required');
      if (req.raw.url?.startsWith('/api')) return reply.code(404).send({ error: 'not found' });
      return reply.sendFile('index.html');
    });
  }

  return app;
}

if (process.env.NODE_ENV !== 'test') {
  const app = buildServer();
  app.listen({ port: PORT, host: '0.0.0.0' })
    .then((addr) => console.log(`cup26 server on ${addr} (static: ${existsSync(STATIC_DIR) ? STATIC_DIR : 'dev — vite serves UI'})`))
    .catch((e) => { console.error(e); process.exit(1); });
}
