/**
 * The seam between the board and the engine.
 *
 * Everything the demo knows about `./engine` passes through here: the adapter
 * replays a recording, the controls take the visitor's next reply, `verify`
 * re-scores, and all three get a `TriageState` snapshot back. The engine is a
 * mutable object with a `step()`; the shell wants immutable snapshots it can
 * scrub through. So a snapshot carries the replies that produced it, and the
 * next step is "rebuild from the seed, replay them all, step once more". That
 * is a few hundred RNG draws and at most eight steps — cheap enough to do on
 * every click, and it keeps `play()` pure, which is what makes the Play tab
 * and the replay agree by construction rather than by care.
 *
 * `worldForSeed` runs the three shipped analysts to find the reference, so a
 * world is memoised per seed. The engine never mutates its world.
 */

import { Engine, MAX_TURNS, worldForSeed, type World } from './engine';
import { DOC_MINUTES, LOOKUP_MINUTES } from './score';
import type { Disposition, Lookup, LookupKind, Reveal, TriageState } from './types';

export { MAX_TURNS };

/** What each lookup adds to the hour meter, in minutes. The generator's config. */
export const PRICES: Record<LookupKind, number> = {
  history: LOOKUP_MINUTES,
  counterparty: LOOKUP_MINUTES,
  prior_alerts: LOOKUP_MINUTES,
  documents: DOC_MINUTES,
};

const worlds = new Map<number, World>();

function worldFor(seed: number): World {
  let world = worlds.get(seed);
  if (!world) {
    world = worldForSeed(seed);
    worlds.set(seed, world);
  }
  return world;
}

/**
 * The engine's lookup records, narrowed onto the board's discriminated union.
 * The engine validated `what`, so the result shape follows from it.
 */
function lookupsOf(engine: Engine): Lookup[] {
  return engine.lookups.map((record): Lookup => {
    switch (record.what) {
      case 'history':
        return { what: 'history', key: String(record.key), result: record.result as Extract<Lookup, { what: 'history' }>['result'] };
      case 'counterparty':
        return { what: 'counterparty', key: String(record.key), result: record.result as Extract<Lookup, { what: 'counterparty' }>['result'] };
      case 'prior_alerts':
        return { what: 'prior_alerts', key: null, result: record.result as Extract<Lookup, { what: 'prior_alerts' }>['result'] };
      case 'documents':
        return { what: 'documents', key: null, result: record.result as Extract<Lookup, { what: 'documents' }>['result'] };
    }
  });
}

function snapshot(
  engine: Engine,
  seed: number,
  replies: (string | null)[],
  referenceMinutes: number | null | undefined,
): TriageState {
  const d = engine.disposition;
  const status: TriageState['status'] =
    d !== null ? (d.action === 'close' ? 'closed' : 'escalated') : engine.aborted ? 'aborted' : 'playing';

  // The answer key stays off the board until the episode is over.
  const reveal: Reveal | null = engine.done
    ? {
        label: engine.world.label,
        typology: engine.world.typology ?? null,
        planted: engine.world.planted,
        template: engine.world.template,
        overlay: engine.world.overlay,
        tier: engine.world.tier ?? null,
        // A recording's own metric wins over the world's, so the reveal shows
        // the number the fixture was scored against; a fresh board uses what
        // `worldForSeed` computed.
        referenceMinutes: referenceMinutes !== undefined ? referenceMinutes : engine.world.reference_minutes ?? null,
      }
    : null;

  return {
    seed,
    screen: engine.screen(),
    lookups: lookupsOf(engine),
    turnsSpent: engine.turns,
    rejected: engine.rejected,
    minutesSpent: engine.minutes,
    lastRejection: engine.lastRejection,
    // The engine checked `reason`/`typology` against the enums, so the
    // widening from `string` is a fact about the engine, not a hope.
    disposition: d as Disposition | null,
    status,
    outcome: engine.outcome,
    shown: [...engine.shown],
    replies,
    reveal,
  };
}

/**
 * One snapshot per reply, in order. A reply after the episode is over
 * produces the same board again — the engine ignores it, as the Python does.
 */
export function replay(
  seed: number,
  replies: readonly (string | null)[],
  options: { referenceMinutes?: number | null } = {},
): TriageState[] {
  const engine = new Engine(worldFor(seed));
  const played: (string | null)[] = [];
  const out: TriageState[] = [];
  for (const reply of replies) {
    played.push(reply);
    if (!engine.done) engine.step(reply);
    out.push(snapshot(engine, seed, [...played], options.referenceMinutes));
  }
  return out;
}

/** A fresh board for a seed. Deterministic in the seed — the shell relies on it. */
export function initState(seed: number): TriageState {
  return snapshot(new Engine(worldFor(seed)), seed, [], undefined);
}

/** Play one reply, returning the next board. Pure — never mutates its input. */
export function play(state: TriageState, reply: string | null): TriageState {
  if (state.status !== 'playing') return state;
  const snapshots = replay(state.seed, [...state.replies, reply]);
  return snapshots[snapshots.length - 1] ?? state;
}
