/**
 * Station Master — solitaire, playable in a browser.
 *
 * The whole game runs client-side. There is no server and no network call at any point: the engine
 * is pure, imports nothing outside itself, and never touches `Math.random`, `Date` or `crypto`, so
 * a static host is all this needs. (Proven, not assumed — a test runs full games with every Node
 * global replaced by a throwing stub.)
 *
 * WHAT THIS MODULE IS. Everything here is presentation and input. It builds no rules of its own:
 *
 *   - what you may do  -> `legalActions(state, actor)`
 *   - what it means    -> `describeIntent()`, shared with the replay
 *   - what the board looks like -> `snapshot()`, shared with the replay
 *   - what just happened -> `narrate()`, shared with the replay
 *
 * That is the same discipline the engine holds itself to. A second opinion about which moves are
 * legal would eventually disagree with `check`, and the failure mode is a UI that offers an illegal
 * move or refuses a legal one.
 *
 * SAVING. The event log is the game (`state = fold(events)`), and the RNG is seeded, so a save is
 * the seed plus the list of intents submitted. Replaying them reconstructs the position exactly,
 * which is far smaller and far more robust than serialising the state graph.
 */

import { pump } from '../engine/advance.ts';
import { applyIntent } from '../engine/apply.ts';
import type { GameEvent } from '../engine/events.ts';
import type { Intent } from '../engine/intents.ts';
import { legalActions } from '../engine/legal.ts';
import { createGame } from '../engine/setup.ts';
import type { GameConfig, GameState, PlayerIndex } from '../engine/state.ts';
import { narrate } from '../sim/narrate.ts';
// Import from the view module, NOT replay.ts — replay.ts writes files and reads process.argv,
// which would pull node:fs into a browser bundle.
import {
  cardName,
  describeIntent,
  geometryLabel,
  snapshot,
  trainName,
  variantLabel,
} from '../sim/view.ts';
import type { TrackGeometry } from '../engine/content.ts';
import { variantsFor } from '../engine/track.ts';
import type { Frame } from '../sim/view.ts';

export const SOLO_CONFIG: GameConfig = {
  mode: 'solitaire',
  victory: 'highestAfterDays',
  length: 'standard',
  optionalRules: {
    reducedVisibility: false,
    sisterTrains: false,
    employeeRotation: false,
    emergencyToolbox: false,
  },
};

/** A group of legal actions of one kind, ready to put on screen. */
export type ActionGroup = {
  kind: string;
  title: string;
  actions: { index: number; label: string }[];
};

export type Game = {
  state: GameState;
  seed: number;
  /** Every intent submitted, in order — the save file. */
  history: Intent[];
  /** Narrated lines, newest last. */
  log: { text: string; tone: string }[];
};

/** How each intent kind is introduced in the action list, in the order they should appear. */
const GROUP_ORDER: readonly { prefix: string; title: string }[] = [
  { prefix: 'mainline.clearance', title: 'Superintendent — rule on this train' },
  { prefix: 'localOps.choose', title: 'Local Operations — choose ONE' },
  { prefix: 'switch.', title: 'Switching' },
  { prefix: 'draw.', title: 'Draw' },
  { prefix: 'card.', title: 'Cards' },
  { prefix: 'track.lay', title: 'Lay track from your supply' },
  { prefix: 'mainline.modify', title: 'Mainline modifiers' },
  // American spelling throughout, to match MANEUVER_CARDS and the source deck.
  { prefix: 'maneuver.', title: 'Maneuvers' },
  { prefix: 'freightAgent.', title: 'Freight Agent' },
  { prefix: 'newTrain.', title: 'Making up the train' },
  { prefix: 'porter.', title: 'Porters' },
  { prefix: 'laborer.', title: 'Laborers' },
  { prefix: 'loadUnload.', title: 'Finish' },
  { prefix: 'redFlag.', title: 'Red flag' },
];

export function newGame(seed: number, config: GameConfig = SOLO_CONFIG): Game {
  const state = createGame({ id: `web-${seed}`, seed, config, playerNames: ['You'] });
  const game: Game = { state, seed, history: [], log: [] };
  drain(game);
  return game;
}

/**
 * Run the engine forward until it needs a decision.
 *
 * Most of a Stage is automatic — the Mainline Phase moves trains, the clock turns over — so the
 * player is only ever asked when `advance` genuinely stops.
 */
export function drain(game: Game): void {
  record(game, pump(game.state));
}

/** Whose turn it is, or null if the game is over or waiting on nothing. */
export function currentActor(game: Game): PlayerIndex | null {
  if (game.state.status !== 'active') return null;
  return game.state.clock.pendingDecision !== null
    ? game.state.clock.superintendent
    : game.state.clock.currentActor;
}

/** Every legal action right now, grouped for display. Empty when there is nothing to decide. */
export function actionGroups(game: Game): { options: Intent[]; groups: ActionGroup[] } {
  const actor = currentActor(game);
  if (actor === null) return { options: [], groups: [] };

  const options = legalActions(game.state, actor);
  const byKind = new Map<string, { index: number; label: string }[]>();
  options.forEach((intent, index) => {
    const label = describeIntent(game.state, intent);
    const list = byKind.get(intent.type) ?? [];
    // Orientation variants and duplicate copies describe identically; showing one is enough, and a
    // list of forty identical rows hides the real choice rather than presenting it.
    if (!list.some((a) => a.label === label)) list.push({ index, label });
    byKind.set(intent.type, list);
  });

  const groups: ActionGroup[] = [];
  const used = new Set<string>();
  for (const { prefix, title } of GROUP_ORDER) {
    const kinds = [...byKind.keys()].filter((k) => k.startsWith(prefix) && !used.has(k));
    const actions = kinds.flatMap((k) => {
      used.add(k);
      return byKind.get(k) ?? [];
    });
    if (actions.length > 0) groups.push({ kind: prefix, title, actions });
  }
  // Anything the table above does not name still has to be offered — silently dropping a legal
  // action would make the game unplayable in a way that is very hard to notice.
  const leftovers = [...byKind.entries()].filter(([k]) => !used.has(k));
  for (const [kind, actions] of leftovers) groups.push({ kind, title: kind, actions });

  return { options, groups };
}

/**
 * A thing you might do, and — if it goes on the board — where it could go.
 *
 * Placeable actions are presented as SUBJECT then LOCATION rather than as one flat list of every
 * (card x square x rotation) combination. A single turn offered 29 track buttons and 7 card buttons
 * with no way to tell which square each referred to; picking the card first and the square second is
 * how the choice is actually made at the table.
 */
export type Placeable = {
  /** Groups every option that plays the same card or lays the same piece. */
  subjectKey: string;
  subject: string;
  /**
   * Where it may go. `coord` drives board highlighting; several spots can share one square when the
   * piece has more than one legal rotation there, which is why the label carries the rotation too.
   */
  spots: { label: string; index: number; coord: { row: number; col: number } }[];
};

export type Menu = {
  options: Intent[];
  /** Actions with no further choice to make. */
  direct: ActionGroup[];
  /** Actions needing a location, grouped under their card or track piece. */
  placeable: { title: string; items: Placeable[] }[];
};

/** The action list as the page shows it: direct actions, plus subject-then-location for the rest. */
export function actionMenu(game: Game): Menu {
  const { options, groups } = actionGroups(game);
  const direct: ActionGroup[] = [];
  const placeableByTitle = new Map<string, Map<string, Placeable>>();

  for (const g of groups) {
    const plain: ActionGroup['actions'] = [];
    for (const a of g.actions) {
      const intent = options[a.index]!;
      const key = subjectOf(game, intent);
      if (key === null) {
        plain.push(a);
        continue;
      }
      const bucket = placeableByTitle.get(g.title) ?? new Map<string, Placeable>();
      const entry = bucket.get(key.subjectKey) ?? {
        subjectKey: key.subjectKey,
        subject: key.subject,
        spots: [],
      };
      if (!entry.spots.some((sp) => sp.label === key.spot)) {
        entry.spots.push({ label: key.spot, index: a.index, coord: key.coord });
      }
      bucket.set(key.subjectKey, entry);
      placeableByTitle.set(g.title, bucket);
    }
    if (plain.length > 0) direct.push({ ...g, actions: plain });
  }

  const placeable = [...placeableByTitle.entries()].map(([title, m]) => ({
    title,
    items: [...m.values()],
  }));
  return { options, direct, placeable };
}

/** Split an intent into "what" and "where", or null if it needs no location. */
function subjectOf(
  game: Game,
  i: Intent,
): { subjectKey: string; subject: string; spot: string; coord: { row: number; col: number } } | null {
  const at = (c: { row: number; col: number }): string => `(${c.row}, ${c.col})`;
  if (i.type === 'card.play' && i.placement) {
    return {
      subjectKey: `card:${i.cardId}`,
      subject: cardName(game.state, i.cardId),
      spot: `${at(i.placement)}${rotationNote(null, i.variant)}`,
      coord: i.placement,
    };
  }
  if (i.type === 'track.lay') {
    const hand = i.hand === 'none' ? '' : `${i.hand}-hand `;
    return {
      subjectKey: `track:${i.geometry}:${i.hand}`,
      subject: `${hand}${geometryLabel(i.geometry)}`,
      spot: `${at(i.placement)}${rotationNote(i.geometry, i.variant)}`,
      coord: i.placement,
    };
  }
  return null;
}

/**
 * Rotations share a square, so the square alone does not identify the choice — and "rotation 2"
 * does not tell a player which way the rail will run, which for a curve or turnout is the entire
 * decision. Only shown when there is more than one way to lay the piece.
 */
function rotationNote(geometry: TrackGeometry | null, variant: number | undefined): string {
  if (geometry === null) return variant === undefined || variant === 0 ? '' : ` — option ${variant + 1}`;
  return variantsFor(geometry).length > 1 ? variantLabel(geometry, variant) : '';
}

/** Submit an action. Returns false and changes nothing if the engine rejects it. */
export function submit(game: Game, intent: Intent): boolean {
  const actor = currentActor(game);
  if (actor === null) return false;

  const result = applyIntent(game.state, actor, intent);
  if (!result.ok) {
    game.log.push({ text: `That is not allowed: ${result.code}`, tone: 'bad' });
    return false;
  }
  game.history.push(intent);
  record(game, result.events);
  drain(game);
  return true;
}

/**
 * Which cards in hand can be played RIGHT NOW, in hand order.
 *
 * A hand where some cards are simply unplayable — a Station upgrade before the Office is a Depot, a
 * Modifier with no Facility to sit beside — looks identical to one where everything is available.
 * Derived from `legalActions`, so it cannot disagree with what the buttons offer.
 */
export function handPlayable(game: Game): boolean[] {
  const hand = game.state.decks.hands.get(0) ?? [];
  const actor = currentActor(game);
  if (actor === null) return hand.map(() => false);
  const playable = new Set(
    legalActions(game.state, actor)
      .filter((i) => i.type === 'card.play')
      .map((i) => (i as Extract<Intent, { type: 'card.play' }>).cardId),
  );
  return hand.map((id) => playable.has(id));
}

/** The board as the replay draws it, so the live game and the replay agree. */
export function view(game: Game): Frame {
  return snapshot(game.state, [], null);
}

function record(game: Game, events: GameEvent[]): void {
  for (const e of events) {
    // The same filter the replay uses: actor changes and phase bookkeeping are noise on screen.
    if (e.type === 'actorChanged') continue;
    const n = narrate(e, {
      cardName: (id) => cardName(game.state, id),
      trainName: (id) => trainName(game.state, id),
    });
    game.log.push({ text: n.text, tone: n.tone });
  }
  // Keep the log bounded; the full history lives in `history` and can be replayed.
  if (game.log.length > 400) game.log.splice(0, game.log.length - 400);
}

// ---------------------------------------------------------------------------
// Saving — seed plus intents, replayed
// ---------------------------------------------------------------------------

export type Save = { seed: number; history: Intent[] };

export function toSave(game: Game): Save {
  return { seed: game.seed, history: game.history };
}

/**
 * Rebuild a game from a save.
 *
 * Replays the intents through the real engine rather than restoring a serialised state, so a save
 * can never describe a position the rules could not have produced. An intent that no longer applies
 * stops the replay rather than being forced — better a short game than a corrupt one.
 */
export function fromSave(save: Save, config: GameConfig = SOLO_CONFIG): Game {
  const game = newGame(save.seed, config);
  for (const intent of save.history) {
    const actor = currentActor(game);
    if (actor === null) break;
    const result = applyIntent(game.state, actor, intent);
    if (!result.ok) break;
    game.history.push(intent);
    record(game, result.events);
    drain(game);
  }
  return game;
}
