import { randomUUID } from 'node:crypto';
import { createDb, type Db } from '@pintrush/db';
import type { PrivateState, PublicState, Timings } from '@pintrush/engine';
import { io as ioClient, type Socket } from 'socket.io-client';
import { createGameServer, type GameServer } from '../src/server';
import { TEST_DB_URL } from './global-setup';

export const SECRET = 'test-secret-0123456789-0123456789-abcdef';

/** Fast but realistic timings: a full 10-question round takes a few seconds when everyone answers. */
export const FAST: Partial<Timings> = {
  introMs: 80,
  pendingMs: 80,
  windowMs: 900,
  difficultWindowMs: 900,
  lockedMs: 40,
  revealMs: 120,
  graceMs: 100,
  roundOverMs: 120,
  breakMs: 400,
};

export const CATEGORIES = [
  'alpha',
  'bravo',
  'charlie',
  'delta',
  'echo',
  'foxtrot',
  'golf',
  'hotel',
  'india',
].map((key) => ({ key, label: key[0]!.toUpperCase() + key.slice(1) }));

export interface Fixture {
  db: Db;
  venueA: { id: string; joinCode: string };
  roomA: { id: string };
  venueB: { id: string; joinCode: string };
  roomB: { id: string };
  hostA: { id: string };
  hostB: { id: string };
  champA: { id: string };
  champB: { id: string };
  packKey: string;
  /** questionId → correct option id, for test players that "know" the answers. */
  answers: Map<string, string>;
}

let counter = 0;

/** Each test file gets its own venues (unique join codes) inside the shared test database. */
export async function seedFixture(): Promise<Fixture> {
  const db = createDb(TEST_DB_URL);
  const tag = `${process.pid}${Date.now().toString(36)}${counter++}`.slice(-8).toUpperCase();
  const packKey = `test-pack-${tag.toLowerCase()}`;
  const answers = new Map<string, string>();

  const pack = await db.questionPack.create({
    data: { key: packKey, name: 'Test pack', kind: 'GENERAL', categories: CATEGORIES, status: 'PUBLISHED' },
  });
  // 6 questions per category, 4 options each, correct option rotates; a third are wildcard-eligible.
  const questions = CATEGORIES.flatMap((c, ci) =>
    Array.from({ length: 6 }, (_, qi) => {
      const correct = ['a', 'b', 'c', 'd'][(ci + qi) % 4]!;
      return {
        packId: pack.id,
        contentKey: `${packKey}/${c.key}-${qi}`,
        category: c.key,
        type: 'MULTIPLE_CHOICE' as const,
        difficulty: qi === 5 ? ('DIFFICULT' as const) : ('STANDARD' as const),
        prompt: `${c.label} question ${qi}?`,
        options: ['a', 'b', 'c', 'd'].map((id) => ({ id, text: `${c.label} ${qi} option ${id}` })),
        correctOptionId: correct,
        wildcardEligible: qi % 3 === 0,
      };
    }),
  );
  await db.question.createMany({ data: questions });
  for (const q of await db.question.findMany({ where: { packId: pack.id } })) answers.set(q.id, q.correctOptionId);

  const settings = { alwaysOn: { enabled: true, packKey, schedule: [0, 1, 2, 3, 4, 5, 6].map((day) => ({ day, open: '00:00', close: '24:00' })) }, mergeSolos: false };
  const venueA = await db.venue.create({ data: { slug: `a-${tag}`.toLowerCase(), name: 'Test Tavern', joinCode: `A${tag}`.slice(0, 10), settings } });
  const venueB = await db.venue.create({ data: { slug: `b-${tag}`.toLowerCase(), name: 'Other Pub', joinCode: `B${tag}`.slice(0, 10), settings } });
  const roomA = await db.room.create({ data: { venueId: venueA.id, name: 'Main Bar', slug: 'main' } });
  const roomB = await db.room.create({ data: { venueId: venueB.id, name: 'Taproom', slug: 'taproom' } });
  const hostA = await db.venueUser.create({ data: { venueId: venueA.id, email: `host-a-${tag}@test.local`, name: 'Host A', passwordHash: 'x', role: 'OWNER' } });
  const hostB = await db.venueUser.create({ data: { venueId: venueB.id, email: `host-b-${tag}@test.local`, name: 'Host B', passwordHash: 'x', role: 'OWNER' } });
  const plan = [
    { kind: 'RUSH', packKey, label: 'The Rush', segment: 0 },
    { kind: 'TABLE_WARS', packKey, label: 'Table Wars', segment: 1 },
  ];
  const champA = await db.event.create({
    data: { venueId: venueA.id, roomId: roomA.id, kind: 'CHAMPIONSHIP', name: 'Test Championship', scheduledAt: new Date(Date.now() + 86_400_000), config: { plan, mergeSolos: false } },
  });
  const champB = await db.event.create({
    data: { venueId: venueB.id, roomId: roomB.id, kind: 'CHAMPIONSHIP', name: 'Other Championship', scheduledAt: new Date(Date.now() + 86_400_000), config: { plan, mergeSolos: false } },
  });
  return { db, venueA, roomA, venueB, roomB, hostA, hostB, champA, champB, packKey, answers };
}

export async function startServer(fx: Fixture, extra: Parameters<typeof createGameServer>[0] = {}) {
  const { config: extraConfig, ...rest } = extra;
  const server = await createGameServer({
    db: fx.db,
    timingsOverride: FAST,
    resultsWindowMs: 300,
    ...rest,
    config: { authSecret: SECRET, enableScheduler: false, webOrigins: ['http://localhost:3000'], validateOutgoing: true, ...(extraConfig ?? {}) },
  });
  const port = await server.listen(0);
  return { server, url: `http://127.0.0.1:${port}` };
}

export type Envelope = { type: string; seq: number; serverNow: number; public?: PublicState | null; me?: PrivateState | null; snapshot?: any; host?: any; data?: any };
export type Ack<T = any> = { ok: true; data?: T } | { ok: false; error: { code: string; message: string } };

export class TestClient {
  readonly socket: Socket;
  readonly received: Envelope[] = [];
  private waiters: { pred: (e: Envelope) => boolean; resolve: (e: Envelope) => void }[] = [];
  public: PublicState | null = null;
  me: PrivateState | null = null;
  connectError: Error | null = null;

  constructor(url: string, auth: Record<string, unknown> = {}) {
    this.socket = ioClient(url, { auth, transports: ['websocket'], reconnection: false, forceNew: true });
    this.socket.onAny((type: string, env: Envelope) => {
      const e = { ...env, type };
      this.received.push(e);
      if (e.snapshot) {
        this.public = e.snapshot.public ?? null;
        this.me = e.snapshot.me ?? null;
      } else {
        if (e.public !== undefined) this.public = e.public ?? null;
        if (e.me !== undefined) this.me = e.me ?? null;
      }
      this.waiters = this.waiters.filter((w) => {
        if (w.pred(e)) {
          w.resolve(e);
          return false;
        }
        return true;
      });
    });
    this.socket.on('connect_error', (err) => {
      this.connectError = err;
    });
  }

  connected(): Promise<void> {
    if (this.socket.connected) return Promise.resolve();
    return new Promise((resolve, reject) => {
      this.socket.once('connect', () => resolve());
      this.socket.once('connect_error', (err) => reject(err));
    });
  }

  emit<T = any>(event: string, payload: unknown, timeoutMs = 5_000): Promise<Ack<T>> {
    return this.socket.timeout(timeoutMs).emitWithAck(event, payload) as Promise<Ack<T>>;
  }

  waitFor(pred: (e: Envelope) => boolean, timeoutMs = 15_000, label = 'message'): Promise<Envelope> {
    const hit = this.received.find(pred);
    if (hit) return Promise.resolve(hit);
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), timeoutMs);
      this.waiters.push({
        pred,
        resolve: (e) => {
          clearTimeout(timer);
          resolve(e);
        },
      });
    });
  }

  /** Wait for a NEW message (ignores history). */
  next(pred: (e: Envelope) => boolean, timeoutMs = 15_000, label = 'message'): Promise<Envelope> {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), timeoutMs);
      this.waiters.push({
        pred,
        resolve: (e) => {
          clearTimeout(timer);
          resolve(e);
        },
      });
    });
  }

  close() {
    this.socket.close();
  }
}

export async function joinAs(url: string, venueCode: string, nickname: string, opts: { teamName?: string; deviceId?: string } = {}) {
  const c = new TestClient(url);
  await c.connected();
  const res = await c.emit<{ token: string; playerSessionId: string; gameSessionId: string; roomId: string; nickname: string }>('room.join', {
    venueCode,
    nickname,
    teamName: opts.teamName ?? null,
    deviceId: opts.deviceId ?? randomUUID(),
    idempotencyKey: randomUUID(),
  });
  return { client: c, res };
}

export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
