import { randomUUID } from 'node:crypto';
import type { GameState } from '@pintrush/engine';
import { StaleSessionError, type EventRecord, type PersistBatch, type PlayerRecord, type RoomRecord, type SessionRecord, type SessionStore, type VenueRecord } from './types';

/**
 * Non-durable session store. Used for the public demo room (bots play around the clock; nothing worth
 * keeping) and for fast unit tests. Same contract as the Prisma store, minus durability.
 */
export class MemorySessionStore implements SessionStore {
  readonly durable = false;
  private readonly events = new Map<string, EventRecord>();
  private readonly sessions = new Map<string, SessionRecord>();
  private readonly players = new Map<string, PlayerRecord & { gameSessionId: string; nicknameKey: string; joinedAt: number }>();
  /** Every batch that reached persist(), for tests. */
  readonly persisted: PersistBatch[] = [];

  constructor(private readonly now: () => number = Date.now) {}

  addEvent(event: EventRecord): void {
    this.events.set(event.id, event);
  }

  async ensureAlwaysOnEvent(room: RoomRecord, venue: VenueRecord, businessDate: string): Promise<EventRecord> {
    for (const e of this.events.values()) {
      if (e.roomId === room.id && e.kind === 'ALWAYS_ON' && e.businessDate === businessDate) return e;
    }
    const event: EventRecord = {
      id: randomUUID(),
      venueId: venue.id,
      roomId: room.id,
      kind: 'ALWAYS_ON',
      status: 'SCHEDULED',
      name: `Always-on · ${businessDate}`,
      scheduledAt: null,
      businessDate,
      config: { plan: [], mergeSolos: venue.settings.mergeSolos },
    };
    this.events.set(event.id, event);
    return event;
  }

  async findSessionByEvent(eventId: string): Promise<SessionRecord | null> {
    for (const s of this.sessions.values()) if (s.eventId === eventId) return s;
    return null;
  }

  async createGameSession(event: EventRecord, state: GameState): Promise<SessionRecord> {
    const rec: SessionRecord = { id: state.sessionId, venueId: event.venueId, roomId: event.roomId, eventId: event.id, status: state.status, seq: 0, stateVersion: 0, state };
    this.sessions.set(rec.id, rec);
    return rec;
  }

  async markEventCancelled(eventId: string): Promise<void> {
    const e = this.events.get(eventId);
    if (e) e.status = 'CANCELLED';
  }

  async markEventLive(eventId: string): Promise<void> {
    const e = this.events.get(eventId);
    if (e) e.status = 'LIVE';
  }

  async loadLiveSessions() {
    return [];
  }

  async listPlayers(gameSessionId: string): Promise<PlayerRecord[]> {
    return [...this.players.values()].filter((p) => p.gameSessionId === gameSessionId).map(({ id, nickname, deviceHash, tokenVersion, status }) => ({ id, nickname, deviceHash, tokenVersion, status }));
  }

  async usedQuestionIds(): Promise<string[]> {
    return [];
  }

  async createPlayerSession(input: Parameters<SessionStore['createPlayerSession']>[0]) {
    for (const p of this.players.values()) {
      if (p.gameSessionId === input.gameSessionId && p.nicknameKey === input.nicknameKey) return 'NICKNAME_TAKEN' as const;
    }
    const id = randomUUID();
    this.players.set(id, {
      id,
      nickname: input.nickname,
      nicknameKey: input.nicknameKey,
      deviceHash: input.deviceHash,
      tokenVersion: 1,
      status: 'ACTIVE',
      gameSessionId: input.gameSessionId,
      joinedAt: this.now(),
    });
    return { id };
  }

  async bumpTokenVersion(playerSessionId: string): Promise<number> {
    const p = this.players.get(playerSessionId);
    if (!p) return 1;
    p.tokenVersion += 1;
    return p.tokenVersion;
  }

  async touchPlayer(): Promise<void> {}

  async persist(batches: PersistBatch[], expectedVersion: number): Promise<number> {
    const target = batches[0] ? this.sessions.get(batches[0].ctx.gameSessionId) : undefined;
    if (target && target.stateVersion !== expectedVersion) throw new StaleSessionError(target.id);
    this.persisted.push(...batches);
    // Keep memory bounded for a room that runs forever.
    if (this.persisted.length > 2_000) this.persisted.splice(0, this.persisted.length - 1_000);
    const last = batches[batches.length - 1];
    if (!last) return expectedVersion;
    for (const b of batches) {
      for (const ev of b.events) {
        if (ev.type === 'PlayerKicked') {
          const p = this.players.get(ev.playerId);
          if (p) {
            p.status = 'KICKED';
            p.tokenVersion += 1000;
          }
        }
      }
    }
    const s = this.sessions.get(last.ctx.gameSessionId);
    if (s) {
      s.state = last.state;
      s.seq = last.seq;
      s.status = last.state.status;
      s.stateVersion = expectedVersion + 1;
    }
    return expectedVersion + 1;
  }

  async purgeGuestData(olderThanMs: number) {
    // Finished sessions and their events go too, so a room that runs forever stays bounded.
    for (const [id, sess] of this.sessions) {
      if (sess.status === 'COMPLETED') {
        this.sessions.delete(id);
        this.events.delete(sess.eventId);
      }
    }
    let players = 0;
    for (const [id, p] of this.players) {
      if (p.joinedAt < olderThanMs) {
        this.players.delete(id);
        players++;
      }
    }
    return { players, audits: 0 };
  }
}
