import type { CategoryDef, Command, DomainEvent, EventMode, GameState, QuestionInput, RoundPlanItem, Timings } from '@pintrush/engine';

export interface VenueRecord {
  id: string;
  slug: string;
  name: string;
  joinCode: string;
  timezone: string;
  settings: {
    alwaysOn: { enabled: boolean; packKey: string; schedule: { day: number; open: string; close: string }[] };
    mergeSolos: boolean;
  };
}

export interface RoomRecord {
  id: string;
  venueId: string;
  name: string;
  slug: string;
  tvTokenVersion: number;
  status: 'ACTIVE' | 'ARCHIVED';
}

export interface EventRecord {
  id: string;
  venueId: string;
  roomId: string;
  kind: EventMode;
  status: 'SCHEDULED' | 'LIVE' | 'PAUSED' | 'COMPLETED' | 'CANCELLED';
  name: string;
  scheduledAt: number | null;
  businessDate: string | null;
  config: { plan: RoundPlanItem[]; mergeSolos: boolean; timings?: Partial<Timings> };
}

export interface PackRecord {
  id: string;
  key: string;
  categories: CategoryDef[];
  questions: QuestionInput[];
}

export interface SessionRecord {
  id: string;
  venueId: string;
  roomId: string;
  eventId: string;
  status: 'WAITING' | 'RUNNING' | 'PAUSED' | 'COMPLETED';
  seq: number;
  /** Optimistic-concurrency version of the stored snapshot. */
  stateVersion: number;
  state: GameState | null;
}

export interface PlayerRecord {
  id: string;
  nickname: string;
  deviceHash: string | null;
  tokenVersion: number;
  status: 'ACTIVE' | 'KICKED' | 'LEFT';
}

/** Read side: venues, rooms, events, question packs. Cached by the caller where useful. */
export interface Catalog {
  findVenueByJoinCode(joinCode: string): Promise<VenueRecord | null>;
  findVenue(venueId: string): Promise<VenueRecord | null>;
  listRooms(venueId: string): Promise<RoomRecord[]>;
  findRoom(roomId: string): Promise<RoomRecord | null>;
  findEvent(eventId: string): Promise<EventRecord | null>;
  listAlwaysOnVenues(): Promise<VenueRecord[]>;
  /** Championships in SCHEDULED state whose start is at or before `untilMs`. */
  championshipsToOpen(untilMs: number): Promise<EventRecord[]>;
  nextChampionship(roomId: string, afterMs: number): Promise<{ eventId: string; name: string; scheduledAt: number } | null>;
  loadPack(packKey: string): Promise<PackRecord>;
  /**
   * May this host token holder still control the venue? The venue user must exist in that venue (or be
   * a platform admin acting for it) and the venue must not be cancelled.
   */
  hostAllowed(userId: string, venueId: string): Promise<boolean>;
}

export type ActorRef = { type: 'SYSTEM' | 'GUEST' | 'HOST' | 'ADMIN' | 'TV'; id: string | null };

export interface SessionContext {
  venueId: string;
  roomId: string;
  gameSessionId: string;
  eventId: string;
  eventKind: EventMode;
  businessDate: string | null;
}

/** One accepted command and everything it caused, persisted in order. */
export interface PersistBatch {
  ctx: SessionContext;
  command: Command;
  actor: ActorRef;
  events: DomainEvent[];
  /** Engine state after the command (immutable: reduce always returns a fresh object). */
  state: GameState;
  seq: number;
}

/** Write side for live sessions. The demo room uses the in-memory implementation. */
export interface SessionStore {
  readonly durable: boolean;
  ensureAlwaysOnEvent(room: RoomRecord, venue: VenueRecord, businessDate: string): Promise<EventRecord>;
  findSessionByEvent(eventId: string): Promise<SessionRecord | null>;
  createGameSession(event: EventRecord, state: GameState): Promise<SessionRecord>;
  markEventLive(eventId: string): Promise<void>;
  /** A championship lobby that was never started. */
  markEventCancelled(eventId: string): Promise<void>;
  /** Sessions that were live when the process stopped. */
  loadLiveSessions(): Promise<{ session: SessionRecord; event: EventRecord }[]>;
  listPlayers(gameSessionId: string): Promise<PlayerRecord[]>;
  usedQuestionIds(gameSessionId: string): Promise<string[]>;
  createPlayerSession(input: {
    venueId: string;
    roomId: string;
    eventId: string;
    gameSessionId: string;
    nickname: string;
    nicknameKey: string;
    deviceHash: string | null;
  }): Promise<{ id: string } | 'NICKNAME_TAKEN'>;
  bumpTokenVersion(playerSessionId: string): Promise<number>;
  touchPlayer(playerSessionId: string, at: number): Promise<void>;
  /**
   * Apply batches in order (one transaction) and save the latest snapshot, but only if the stored
   * snapshot is still at `expectedVersion`. Returns the new version; throws StaleSessionError when
   * another process has written the session since (e.g. two instances during a rolling deploy).
   */
  persist(batches: PersistBatch[], expectedVersion: number): Promise<number>;
  purgeGuestData(olderThanMs: number): Promise<{ players: number; audits: number }>;
}

/** The stored snapshot moved on without us: another process owns this session now. */
export class StaleSessionError extends Error {
  constructor(readonly gameSessionId: string) {
    super(`Game session ${gameSessionId} was written by another process`);
    this.name = "StaleSessionError";
  }
}
