/**
 * Component 18 — Balance simulation harness.
 *
 * Dev-side only. Runs many seeded games and reports the numbers that the provisional values in
 * docs/rules/card-reference.md were guessed at. See architecture/components.md §2 D.18.
 *
 * The point is narrow and worth stating: this can tell you whether the ARITHMETIC of the game
 * works — whether targets are reachable, whether Crew Trays bottleneck, whether collisions fire at
 * a sane rate. It cannot tell you whether the game is any fun. Only a table can do that.
 *
 * Run with:  node src/sim/harness.ts [games] [length]
 */

import { pump } from '../engine/advance.ts';
import { collectiveRevenueFloor, lengthProfile } from '../engine/content.ts';
import { createGame } from '../engine/setup.ts';
import type { GameLength } from '../engine/content.ts';
import type { GameConfig, GameMode } from '../engine/state.ts';
import type { BotPolicy, PlayOutcome } from './bot.ts';
import { developerBot, playGame, randomBot } from './bot.ts';
import type { GameStats } from './stats.ts';
import { formatAggregate, summarize } from './stats.ts';

export type SimOptions = {
  games: number;
  length: GameLength;
  mode: GameMode;
  players: string[];
  policy: BotPolicy;
};

export type SimReport = {
  policy: string;
  games: number;
  finished: number;
  wins: number;
  revenuePerPlayer: Stats;
  revenuePerPlayerPerDay: Stats;
  collisionsPerGame: Stats;
  trainsScheduled: Stats;
  cardsPlayed: Stats;
  daysPlayed: Stats;
  outcomeReasons: Record<string, number>;
  /** Per-game detail, for end-of-game statistics and anomaly detection. */
  perGame: GameStats[];
};

export type Stats = { min: number; max: number; mean: number; median: number };

function statsOf(xs: number[]): Stats {
  if (xs.length === 0) return { min: 0, max: 0, mean: 0, median: 0 };
  const sorted = [...xs].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return {
    min: sorted[0]!,
    max: sorted[sorted.length - 1]!,
    mean: xs.reduce((a, b) => a + b, 0) / xs.length,
    median:
      sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!,
  };
}

function configFor(mode: GameMode, length: GameLength): GameConfig {
  return {
    mode,
    victory: 'highestAfterDays',
    length,
    optionalRules: {
      reducedVisibility: false,
      sisterTrains: false,
      employeeRotation: false,
      emergencyToolbox: false,
    },
  };
}

export function simulate(opts: SimOptions): SimReport {
  const results: PlayOutcome[] = [];
  const stats: GameStats[] = [];

  for (let i = 0; i < opts.games; i++) {
    const seed = 1000 + i * 7919; // a prime stride, so seeds do not correlate
    const s = createGame({
      id: `sim-${i}`,
      seed,
      config: configFor(opts.mode, opts.length),
      playerNames: opts.players,
    });
    const r = playGame(s, opts.policy, pump);
    results.push(r);
    stats.push(summarize(seed, r.events, r.intents, s));
  }

  const finished = results.filter((r) => r.finished);
  const perPlayer = finished.flatMap((r) => r.revenue);
  const days = finished.map((r) => Math.max(1, r.days - 1));
  const perDay = finished.flatMap((r) =>
    r.revenue.map((v) => v / Math.max(1, r.days - 1)),
  );

  const reasons: Record<string, number> = {};
  for (const r of finished) {
    const key = r.outcome ? `${r.outcome.result}/${r.outcome.reason}` : 'none';
    reasons[key] = (reasons[key] ?? 0) + 1;
  }

  return {
    policy: opts.policy.name,
    games: opts.games,
    finished: finished.length,
    wins: finished.filter((r) => r.outcome?.result === 'win').length,
    revenuePerPlayer: statsOf(perPlayer),
    revenuePerPlayerPerDay: statsOf(perDay),
    collisionsPerGame: statsOf(finished.map((r) => r.collisions)),
    trainsScheduled: statsOf(finished.map((r) => r.trainsScheduled)),
    cardsPlayed: statsOf(finished.map((r) => r.cardsPlayed)),
    daysPlayed: statsOf(days),
    outcomeReasons: reasons,
    perGame: stats,
  };
}

// ---------------------------------------------------------------------------
// Reporting
// ---------------------------------------------------------------------------

const f = (n: number): string => n.toFixed(1).padStart(6);

function line(label: string, s: Stats): string {
  return `  ${label.padEnd(26)} ${f(s.mean)} ${f(s.median)} ${f(s.min)} ${f(s.max)}`;
}

export function formatReport(r: SimReport, length: GameLength, players: number): string {
  const profile = lengthProfile(length);
  const out: string[] = [];

  out.push(`\n=== ${r.policy} bot · ${length} · ${players}p · ${r.games} games ===`);
  out.push(`  finished ${r.finished}/${r.games}   wins ${r.wins}/${r.finished}`);
  out.push(`  ${''.padEnd(26)}   mean median    min    max`);
  out.push(line('revenue per player', r.revenuePerPlayer));
  out.push(line('revenue per player per Day', r.revenuePerPlayerPerDay));
  out.push(line('collisions per game', r.collisionsPerGame));
  out.push(line('trains scheduled', r.trainsScheduled));
  out.push(line('cards played', r.cardsPlayed));
  out.push(line('days played', r.daysPlayed));

  out.push('\n  outcomes:');
  for (const [k, v] of Object.entries(r.outcomeReasons).sort((a, b) => b[1] - a[1])) {
    out.push(`    ${k.padEnd(28)} ${v}`);
  }

  // The provisional numbers this exists to test.
  out.push('\n  against the provisional targets:');
  out.push(`    target ${profile.target} over ${profile.days} Days`);
  out.push(
    `    predicted ~5-6 revenue/player/Day steady state; observed mean ` +
      `${r.revenuePerPlayerPerDay.mean.toFixed(1)}`,
  );
  out.push(
    `    collective floor (competitive) ${collectiveRevenueFloor(players, profile.days)}` +
      `; observed total mean ${(r.revenuePerPlayer.mean * players).toFixed(1)}`,
  );

  return out.join('\n');
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------

const isMain = process.argv[1]?.endsWith('harness.ts') ?? false;

if (isMain) {
  const games = Number(process.argv[2] ?? 200);
  const length = (process.argv[3] ?? 'standard') as GameLength;

  for (const policy of [developerBot, randomBot(12345)]) {
    const report = simulate({
      games,
      length,
      mode: 'solitaire',
      players: ['bot'],
      policy,
    });
    console.log(formatReport(report, length, 1));
    if (policy === developerBot) console.log(formatAggregate(report.perGame));
  }
}
