// Serialising a local game to a durable record and reconstructing it by replay — the offline
// counterpart of the server's game rehydration (backend replay: engine.New(seed) + replayMove per
// journal row). A record carries the seed, the rules, the seat metadata and the move journal; the
// board, bag and racks are NOT stored — they are rebuilt by replaying the journal on a fresh engine
// seeded the same way (the bag is deterministic from the seed and the sequence of operations).
//
// The journal is in alphabet-index space, which is dictionary-independent: a variant's alphabet
// (Latin 26 / embedded Russian 33) is fixed, so the same journal replays against any dictionary
// version of that variant (only the word list changes, not the indices). The dictionary version is
// pinned in the record, and replay applies each recorded move against that dawg.

import { LocalGame, type LocalMove, type DropoutTiles, type LocalGameOptions } from './engine';
import type { Dawg } from '../dict/dawg';
import type { Variant } from '../model';
import type { PinLock } from '../pin';

/** Seat describes one player of a local game (forward-compatible with a 2-4 hotseat). */
export interface Seat {
  kind: 'human' | 'robot';
  name: string;
  /** The player's real account id for a human seat, so the client identifies "you" and the correct
   *  turn exactly as in an online game; absent for the robot (a synthetic id is derived). */
  accountId?: string;
  /** Offline hotseat only: the seat's optional PIN lock. When set, the seat's rack stays hidden
   *  until the PIN is entered (a social lock for a shared device — see lib/pin). */
  pin?: PinLock;
}

/** LocalGameRecord is the durable form of one local game — everything needed to reconstruct it. */
export interface LocalGameRecord {
  id: string;
  variant: Variant;
  dictVersion: string;
  /** The bag seed, serialised as a decimal string (it may exceed JS safe integers). */
  seed: string;
  players: number;
  multipleWords: boolean;
  /** Forbids laying a word that is already on the board (the Erudit rule). Pinned at creation from
   *  the variant, so a game started before the rule existed (where it is absent, read as false)
   *  replays and scores exactly as it was played. */
  noRepeatWords?: boolean;
  dropoutTiles: DropoutTiles;
  seats: Seat[];
  /** The dictionary-independent, alphabet-index-space move journal. */
  journal: LocalMove[];
  status: 'active' | 'finished';
  /** Offline pass-and-play (hotseat): 2-4 humans share one device. Absent on vs_ai records (and on
   *  older records predating the feature), which the source reads as false. */
  hotseat?: boolean;
  /** Offline hotseat only: the mandatory master (host/referee) PIN lock, needed to skip/resign a
   *  seat, exclude a player or terminate the game (see lib/pin). Absent on vs_ai records. */
  hostPin?: PinLock;
  createdAtUnix: number;
  updatedAtUnix: number;
  /** Wall-clock ms at which the vs_ai idle hint unlocks (the robot's last move + the window), or 0
   *  while open (no robot move yet). Persisted so the wait survives leaving and reopening the app;
   *  read through a sanitiser (cap at now + window) so a device clock set back cannot freeze it —
   *  see lib/hints. */
  hintUnlockAtMs: number;
  /** The seated player's in-progress composition (rack order + pending board tiles) as the draft
   *  JSON the game screen serialises, or absent when there is none. A local game keeps it here —
   *  there is no server to hold it — so a reload of the game state restores the arrangement instead
   *  of dropping the tiles back into the rack. It belongs to the turn: the source clears it wherever
   *  the turn advances (a committed move, a resignation, a host skip), so a hotseat seat never
   *  inherits the previous player's arrangement. */
  draft?: string;
}

/** The record fields the caller owns (the engine supplies the rest). */
export interface RecordMeta {
  id: string;
  seats: Seat[];
  hotseat?: boolean;
  hostPin?: PinLock;
  createdAtUnix: number;
  updatedAtUnix: number;
  hintUnlockAtMs: number;
  draft?: string;
}

/**
 * serializeGame builds the durable record for game, taking the caller-owned metadata (id, seats,
 * timestamps) and deriving the rest — the seed, rules, journal and status — from the game itself.
 */
export function serializeGame(game: LocalGame, meta: RecordMeta): LocalGameRecord {
  const cfg = game.config;
  return {
    id: meta.id,
    variant: game.variant,
    dictVersion: game.version,
    seed: game.seed.toString(),
    players: game.playerCount,
    multipleWords: cfg.multipleWords,
    noRepeatWords: cfg.noRepeatWords || undefined,
    dropoutTiles: cfg.dropoutTiles,
    seats: meta.seats,
    journal: game.history,
    status: game.isOver ? 'finished' : 'active',
    hotseat: meta.hotseat,
    hostPin: meta.hostPin,
    createdAtUnix: meta.createdAtUnix,
    updatedAtUnix: meta.updatedAtUnix,
    hintUnlockAtMs: meta.hintUnlockAtMs,
    draft: meta.draft,
  };
}

/**
 * replayGame reconstructs the live LocalGame from a record and the loaded dictionary, by seeding a
 * fresh engine identically and replaying every journal move in order. The recorded plays stay legal
 * against the pinned dictionary, so replay reproduces the exact board, racks, bag and scores.
 */
export function replayGame(record: LocalGameRecord, dawg: Dawg): LocalGame {
  const opts: LocalGameOptions = {
    variant: record.variant,
    version: record.dictVersion,
    seed: BigInt(record.seed),
    players: record.players,
    dawg,
    multipleWords: record.multipleWords,
    noRepeatWords: record.noRepeatWords,
    dropoutTiles: record.dropoutTiles,
  };
  const game = new LocalGame(opts);
  for (const m of record.journal) {
    switch (m.action) {
      case 'play':
        game.play(m.dir!, m.tiles!);
        break;
      case 'pass':
        game.pass();
        break;
      case 'exchange':
        game.exchange(m.exchanged!);
        break;
      case 'resign':
        game.resignSeat(m.player);
        break;
    }
  }
  return game;
}
