import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
import type { AddressInfo } from 'node:net';
import { createDb, type Db } from '@pintrush/db';
import { DEFAULT_TIMINGS, type Timings } from '@pintrush/engine';
import type { Server } from 'socket.io';
import { TokenService } from './auth';
import { realClock, type Clock } from './clock';
import { loadConfig, type ServerConfig } from './config';
import { createIo } from './io';
import { log } from './logger';
import { setOutgoingValidation } from './outgoing';
import { RoomManager } from './rooms/RoomManager';
import { MemorySessionStore } from './store/memorySessionStore';
import { PrismaCatalog } from './store/prismaCatalog';
import { PrismaSessionStore } from './store/prismaSessionStore';
import type { Catalog, SessionStore } from './store/types';

export interface GameServerOptions {
  config?: Partial<ServerConfig>;
  clock?: Clock;
  db?: Db;
  catalog?: Catalog;
  durableStore?: SessionStore;
  demoStore?: SessionStore;
  timingsOverride?: Partial<Timings>;
  resultsWindowMs?: number;
}

export interface GameServer {
  config: ServerConfig;
  io: Server;
  rooms: RoomManager;
  tokens: TokenService;
  db: Db;
  listen(port?: number): Promise<number>;
  stop(): Promise<void>;
}

export async function createGameServer(opts: GameServerOptions = {}): Promise<GameServer> {
  const config = loadConfig(opts.config);
  setOutgoingValidation(config.validateOutgoing);
  const clock = opts.clock ?? realClock;
  const db = opts.db ?? createDb(config.databaseUrl);
  const tokens = new TokenService(config.authSecret);
  const catalog = opts.catalog ?? new PrismaCatalog(db, () => clock.now());
  const timingsOverride = opts.timingsOverride ?? scaledTimings(process.env.GAME_TIMINGS_SCALE);
  const rooms = new RoomManager({
    clock,
    catalog,
    tokens,
    config,
    durableStore: opts.durableStore ?? new PrismaSessionStore(db),
    demoStore: opts.demoStore ?? new MemorySessionStore(() => clock.now()),
    ...(timingsOverride ? { timingsOverride } : {}),
    ...(opts.resultsWindowMs !== undefined ? { resultsWindowMs: opts.resultsWindowMs } : {}),
  });

  const httpServer = createServer((req, res) => void handleHttp(req, res));
  const io = createIo(httpServer, { config, tokens, rooms });

  if (config.redisUrl) {
    // Shares broadcasts across processes. Game state itself still lives in one process per room
    // (see ARCHITECTURE "single-process limit"), so this only helps with fan-out.
    const [{ createAdapter }, { Redis }] = await Promise.all([import('@socket.io/redis-adapter'), import('ioredis')]);
    const pub = new Redis(config.redisUrl);
    const sub = pub.duplicate();
    io.adapter(createAdapter(pub, sub));
    log.info('redis adapter enabled');
  }

  async function handleHttp(req: IncomingMessage, res: ServerResponse) {
    const origin = req.headers.origin;
    if (origin && config.webOrigins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin);
      res.setHeader('Vary', 'Origin');
    }
    res.setHeader('Cache-Control', 'no-store');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    const url = new URL(req.url ?? '/', 'http://localhost');
    if (req.method === 'GET' && url.pathname === '/health') {
      return sendJson(res, 200, { ok: true, uptimeSec: Math.round(process.uptime()), rooms: rooms.loadedActors().length });
    }
    const m = /^\/rooms\/([0-9a-f-]{36})\/public$/.exec(url.pathname);
    if (req.method === 'GET' && m) {
      const actor = await rooms.getActor(m[1]!).catch(() => null);
      if (!actor) return sendJson(res, 404, { error: 'NOT_FOUND' });
      return sendJson(res, 200, actor.publicInfo(clock.now()));
    }
    sendJson(res, 404, { error: 'NOT_FOUND' });
  }

  return {
    config,
    io,
    rooms,
    tokens,
    db,
    async listen(port = config.port) {
      const recovered = await rooms.recover();
      if (recovered) log.info('recovered live sessions', { count: recovered });
      rooms.start();
      await new Promise<void>((resolve) => httpServer.listen(port, resolve));
      return (httpServer.address() as AddressInfo).port;
    },
    async stop() {
      // Order matters: refuse new commands, disconnect every socket, then drain the write-behind log,
      // so nothing is ever acked to a phone after the final flush.
      rooms.beginShutdown();
      await new Promise<void>((resolve) => io.close(() => resolve()));
      await rooms.stop();
      if (!opts.db) await db.$disconnect();
    },
  };
}

function sendJson(res: ServerResponse, status: number, body: unknown) {
  res.statusCode = status;
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify(body));
}

/**
 * GAME_TIMINGS_SCALE (end-to-end tests and quick local demos only): multiplies every phase length,
 * e.g. 0.25 turns a 4.5-minute round into about a minute. Grace stays proportional. Unset = real timings.
 */
function scaledTimings(raw: string | undefined): Partial<Timings> | undefined {
  const scale = Number(raw);
  if (!raw || !Number.isFinite(scale) || scale <= 0 || scale === 1) return undefined;
  const out: Partial<Timings> = {};
  for (const [k, v] of Object.entries(DEFAULT_TIMINGS) as [keyof Timings, number][]) out[k] = Math.max(50, Math.round(v * scale));
  log.warn('GAME_TIMINGS_SCALE is set: game phases run at a non-standard speed', { scale });
  return out;
}
