/**
 * Game setup — rules §4.
 *
 * Builds the initial state deterministically from a seed. Part of component 2; the D12 rolls
 * come from component 7.
 */

import {
  ACTION_CARDS,
  ENHANCEMENT_CARDS,
  EXTRA_TRAINS,
  FREIGHT_PROFILES,
  MAINLINE_MODIFIER_CARDS,
  MANEUVER_CARDS,
  SPACE_USE_CARDS,
  MOVES_PER_LOCAL_OPS,
  MODIFIER_PROFILES,
  OFFICE_PROFILES,
  MAINLINE_PROFILES,
  mainlineProfile,
  TRACK_SUPPLY,
  ROLLING_STOCK_SUPPLY,
  STAGES_PER_DAY,
  TIMETABLED_TRAINS,
  crewTrayCount,
  mainlineCardCount,
  officeProfile,
} from './content.ts';
import type { Rng } from './rng.ts';
import { createRng } from './rng.ts';
import type {
  Card,
  CardId,
  DivisionNode,
  GameConfig,
  GameState,
  OfficeArea,
  PlayerIndex,
  RollingStock,
  TrackCard,
  TrayId,
} from './state.ts';
import { coordKey, freshTurn } from './state.ts';

export type SetupOptions = {
  id: string;
  seed: number;
  config: GameConfig;
  playerNames: string[];
};

/** Builds the 52-card Home Office deck (§12.1). Unshuffled; caller shuffles with the seeded RNG. */
export function buildDeck(mode: GameConfig['mode'] = 'competitive'): Card[] {
  // Q6 — the 22 opponent-directed cards have no legal target in a one-player game, so a solitaire
  // deck omits them entirely rather than leaving 19% of draws dead.
  const solitaire = mode === 'solitaire';
  const cards: Card[] = [];
  let n = 0;
  const push = (kind: Card['kind']): void => {
    cards.push({ id: `c${n++}`, kind });
  };

  for (const t of TIMETABLED_TRAINS) push({ kind: 'timetabledTrain', number: t.number });
  for (const t of EXTRA_TRAINS) push({ kind: 'extraTrain', number: t.number });
  for (const o of OFFICE_PROFILES) {
    for (let i = 0; i < o.copiesInDeck; i++) push({ kind: 'office', tier: o.tier });
  }
  for (const f of FREIGHT_PROFILES) {
    for (let i = 0; i < f.copies; i++) push({ kind: 'freightFacility', facility: f.kind });
  }
  for (const m of MODIFIER_PROFILES) {
    for (let i = 0; i < m.copies; i++) push({ kind: 'modifier', modifier: m.kind });
  }
  if (!solitaire) {
    for (const c of SPACE_USE_CARDS) {
      for (let i = 0; i < c.copies; i++) push({ kind: 'spaceUse', key: c.key });
    }
  }
  for (const c of ENHANCEMENT_CARDS) {
    for (let i = 0; i < c.copies; i++) push({ kind: 'enhancement', key: c.key });
  }
  for (const c of MAINLINE_MODIFIER_CARDS) {
    for (let i = 0; i < c.copies; i++) push({ kind: 'mainlineModifier', key: c.key });
  }
  for (const c of MANEUVER_CARDS) {
    for (let i = 0; i < c.copies; i++) push({ kind: 'maneuver', key: c.key });
  }
  if (!solitaire) {
    for (const c of ACTION_CARDS) {
      for (let i = 0; i < c.copies; i++) push({ kind: 'action', key: c.key });
    }
  }
  // Track is NOT in the deck any more — it is a per-player supply (content.ts TRACK_SUPPLY).

  return cards;
}

/** The Division Yard starts with every rolling stock piece (§4.9). */
export function buildRollingStock(): RollingStock[] {
  const out: RollingStock[] = [];
  for (const s of ROLLING_STOCK_SUPPLY) {
    for (let i = 0; i < s.loaded; i++) out.push({ type: s.type, loaded: true });
    for (let i = 0; i < s.empty; i++) out.push({ type: s.type, loaded: false });
  }
  return out;
}

/**
 * A player's opening three cards: Limits, Whistle Post, Limits, in one horizontal row (§4.2).
 *
 * Gap 8 — the Office card carries a plain junction stub above and below, so Secondary Track can
 * branch from the opening Stage. The stubs are NOT turnouts: §A.1's directional rule governs
 * drawn turnout cards only.
 */
function buildOfficeArea(owner: PlayerIndex): OfficeArea {
  const row = 0;
  const officeCoord = { row, col: 0 };
  const limitsWest = { row, col: -1 };
  const limitsEast = { row, col: 1 };

  const officeCard: TrackCard = {
    geometry: { kind: 'office' },
    baseOperationalRail: true,
    standing: [],
    facility: buildPassengerFacility('whistlePost'),
    modifiers: [],
    enhancements: [],
  };

  const limitsCard = (): TrackCard => ({
    geometry: { kind: 'limits' },
    baseOperationalRail: true,
    standing: [],
    facility: null,
    modifiers: [],
    enhancements: [],
  });

  const grid = new Map<string, TrackCard>();
  grid.set(coordKey(officeCoord), officeCard);
  grid.set(coordKey(limitsWest), limitsCard());
  grid.set(coordKey(limitsEast), limitsCard());

  return {
    owner,
    tier: 'whistlePost',
    grid,
    officeCoord,
    runningRow: row,
    limitsWest,
    limitsEast,
    adOccupancy: [],
    heldAtLimits: [],
    dispatchUsedToday: [],
    trackSupply: new Map(TRACK_SUPPLY.map((t) => [`${t.geometry}:${t.hand}`, t.perPlayer])),
  };
}

/**
 * A Whistle Post is not a Passenger Facility (§9), so it has no Porters and no slots — but the
 * Office card still carries a facility record so an upgrade is a property change rather than a
 * card swap (game-state.md constraint 9).
 */
function buildPassengerFacility(tier: Parameters<typeof officeProfile>[0]): NonNullable<TrackCard['facility']> {
  const p = officeProfile(tier);
  return {
    kind: 'passenger',
    subtype: 'office',
    allows: { outbound: p.isPassengerFacility, inbound: p.isPassengerFacility },
    outboundBox: [],
    inboundBox: [],
    capacity: { outbound: p.passengerOut, inbound: p.passengerIn },
    menAtWork: [null, null, null],
    industryTrack: { length: 0, cars: [] },
    laborers: 0,
    porters: p.porters,
    usedThisStage: { laborers: 0, porters: 0 },
  };
}

/**
 * §4.3-4.4 — the west-to-east chain, with a Division Point beyond each end.
 *
 * Each Mainline card is now a TYPE drawn at random (§4.2, "a Mainline card is drawn at random and
 * placed between each player"), which is what gives the Division its terrain and therefore its
 * crossing times.
 */
function buildDivision(players: number, rng: Rng): DivisionNode[] {
  const nodes: DivisionNode[] = [];
  const kinds = MAINLINE_PROFILES.map((m) => m.kind);
  const mainline = (): DivisionNode => {
    const card = kinds[rng.nextInt(kinds.length)]!;
    const node: DivisionNode = { kind: 'mainline', card, transits: [] };
    // The Heavy Grade card says "Player sets orientation", but setup has no decision point yet —
    // createGame is synchronous and returns a ready state. Rolled for now so the orientation is at
    // least deterministic and varies between games; it should become a real player choice when
    // setup gains an interactive phase. See implications.md §10 Q11.
    if (mainlineProfile(card).speed.kind === 'grade') {
      node.gradeUp = rng.nextInt(2) === 0 ? 'east' : 'west';
    }
    return node;
  };

  nodes.push({ kind: 'divisionPoint', side: 'west', holding: [] });
  for (let p = 0; p < players; p++) {
    nodes.push(mainline());
    nodes.push({ kind: 'office', owner: p });
  }
  nodes.push(mainline());
  nodes.push({ kind: 'divisionPoint', side: 'east', holding: [] });

  return nodes;
}

export function createGame(opts: SetupOptions): GameState {
  const { id, seed, config, playerNames } = opts;
  if (playerNames.length < 1) throw new Error('a game needs at least one player');
  if (config.mode === 'solitaire' && playerNames.length !== 1) {
    throw new Error('solitaire is a one-player mode');
  }

  const rng = createRng(seed);
  const playerCount = playerNames.length;

  const players = playerNames.map((name, index) => ({ index, name, revenue: 0 }));

  const officeAreas = new Map<PlayerIndex, OfficeArea>();
  for (let p = 0; p < playerCount; p++) officeAreas.set(p, buildOfficeArea(p));

  // §4.4 - highest D12 takes the Eastern Division Point; §4.5 - highest begins as Superintendent.
  // Both rolls are drawn even in solitaire so the RNG stream stays identical across player counts.
  const divisionRolls = players.map(() => rng.d12());
  const superRolls = players.map(() => rng.d12());
  const superintendent = argmax(superRolls);
  void divisionRolls; // seating is fixed by array order; the roll is recorded for the event log

  // §4.6-4.7 - shuffle, deal three each, then turn three face-up as the Department slots.
  const deck = buildDeck(config.mode);
  const cards = new Map<CardId, Card>();
  for (const c of deck) cards.set(c.id, c);

  const shuffled = rng.shuffle(deck.map((c) => c.id));
  const hands = new Map<PlayerIndex, CardId[]>();
  let cursor = 0;
  for (let i = 0; i < playerCount; i++) {
    const p = (superintendent + i) % playerCount;
    hands.set(p, shuffled.slice(cursor, cursor + 3));
    cursor += 3;
  }
  const departments: (CardId | null)[] = [
    shuffled[cursor++] ?? null,
    shuffled[cursor++] ?? null,
    shuffled[cursor++] ?? null,
  ];
  const homeOffice = shuffled.slice(cursor);

  const redFlags = new Map<PlayerIndex, boolean>();
  for (let p = 0; p < playerCount; p++) {
    redFlags.set(p, config.optionalRules.emergencyToolbox);
  }

  // §4.9 - Crew Trays near the turn sheet. Scarcity is an explicit mechanic (§7).
  const freeTrays: TrayId[] = [];
  for (let i = 0; i < crewTrayCount(playerCount); i++) freeTrays.push(`tray${i}`);

  const timetable: (number | null)[] = new Array(STAGES_PER_DAY).fill(null);

  return {
    id,
    config,
    seed,
    rngState: rng.getState(),
    players,
    division: { nodes: buildDivision(playerCount, rng) },
    officeAreas,
    trays: new Map(),
    freeTrays,
    cards,
    decks: { homeOffice, departments, salvageYard: [], hands, redFlags },
    yards: { divisionYard: buildRollingStock(), classificationYard: [] },
    timetable,
    pendingExtras: [],
    pendingSecondSections: [],
    clock: {
      day: 1,
      stage: 1,
      phase: 'localOps',
      currentActor: superintendent,
      pendingDecision: null,
      clearanceRuling: null,
      superintendent,
      actorOffset: 0,
    },
    turn: freshTurn(MOVES_PER_LOCAL_OPS),
    movedThisPhase: new Set(),
    collisionsToday: 0,
    status: 'active',
    outcome: null,
  };
}

function argmax(values: number[]): number {
  let best = 0;
  for (let i = 1; i < values.length; i++) {
    if (values[i]! > values[best]!) best = i;
  }
  return best;
}

export { mainlineCardCount };
