import { boardScore, legalActionMask, resetFromOrder, step } from "./engine";
import type { GameState } from "./types";

export const N_ROUNDS = 3;

/**
 * Classic Lost Cities: three rounds, scores summed, highest total wins.
 *
 * The single-round engine is left alone -- it is the rules oracle the Python
 * engine is differential-tested against. Only two rules live up here, both from
 * the Kosmos rulebook:
 *
 *   "If after three games you have the highest overall score, you win."
 *   "The player who has more points begins" the next game -- not alternating.
 *
 * The rulebook says nothing about an exact tie, so the starter falls back to a
 * coin flip, drawn up front with the deals. This mirrors src/lost_cities_jax/match.py;
 * the two are checked against each other in match.test.ts.
 */
export interface MatchState {
  round: GameState;
  /** Every deal of the match, shuffled up front. */
  deckOrders: number[][];
  /** Tie-breaking starters, used only when the scores are level. */
  coinFlips: number[];
  roundIdx: number;
  /** Points banked by each player in the rounds already finished. */
  carry: [number, number];
  /**
   * 1 for a one-off deal, 3 for the classic match.
   *
   * The observation always reports the round index out of three regardless --
   * that is the space the policy was trained on, and a one-off deal is simply its
   * round one, played at a carry of zero.
   */
  totalRounds: number;
  /**
   * The round's deck is exhausted and its board is being shown, but the match has
   * not rolled into the next round yet. The engine advances rounds atomically; the
   * client holds here so the finished board and its per-colour scores are visible
   * before `matchAdvanceRound` deals the next one. Never set on the last round --
   * that sets `done` instead.
   */
  roundComplete: boolean;
  /** Each finished round's [you, rival] total, in order, for the final summary. */
  roundHistory: [number, number][];
  done: boolean;
}

/**
 * Whoever has banked more points leads; level scores fall back to the coin.
 *
 * Round one needs no special case: carry is (0, 0) there, so the tie branch
 * already picks the coin flip, which is exactly the rulebook's arbitrary
 * "oldest player begins".
 */
export function startingPlayer(
  carry: readonly [number, number],
  roundIdx: number,
  coinFlips: readonly number[],
): number {
  const lead = carry[0] - carry[1];
  if (lead > 0) return 0;
  if (lead < 0) return 1;
  return coinFlips[roundIdx];
}

export function matchFromOrders(
  deckOrders: number[][],
  coinFlips: number[],
  totalRounds: number = N_ROUNDS,
): MatchState {
  const carry: [number, number] = [0, 0];
  return {
    round: resetFromOrder(deckOrders[0], startingPlayer(carry, 0, coinFlips)),
    deckOrders,
    coinFlips,
    roundIdx: 0,
    carry,
    totalRounds,
    roundComplete: false,
    roundHistory: [],
    done: false,
  };
}

/** Running totals: rounds already banked plus the board in play. */
export function matchScore(state: MatchState): [number, number] {
  const board = boardScore(state.round);
  return [state.carry[0] + board[0], state.carry[1] + board[1]];
}

export function matchLegalActionMask(state: MatchState): boolean[] {
  const mask = legalActionMask(state.round);
  // No moves while the match is over or a finished round is being shown.
  return state.done || state.roundComplete ? mask.map(() => false) : mask;
}

/** Score of the round currently on the board, by player. */
export function roundScore(state: MatchState): [number, number] {
  return boardScore(state.round);
}

/**
 * Play one ply. When the deck runs out this holds on the finished board
 * (`roundComplete`) rather than rolling straight into the next round, so the
 * client can show the round's scores; `matchAdvanceRound` continues. The last
 * round sets `done` instead. Rounds one and two only bank into `carry` -- the
 * summed total decides the match.
 */
export function matchStep(state: MatchState, action: number): MatchState {
  const played = step(state.round, action);
  if (!played.done || state.done || state.roundComplete) {
    return { ...state, round: played };
  }

  const board = boardScore(played);
  if (state.roundIdx >= state.totalRounds - 1) {
    return {
      ...state,
      round: played,
      roundHistory: [...state.roundHistory, board],
      done: true,
    };
  }
  return { ...state, round: played, roundComplete: true };
}

/** Bank the finished round and deal the next one. No-op unless roundComplete. */
export function matchAdvanceRound(state: MatchState): MatchState {
  if (!state.roundComplete) return state;
  const board = boardScore(state.round);
  const banked: [number, number] = [state.carry[0] + board[0], state.carry[1] + board[1]];
  const roundIdx = state.roundIdx + 1;
  return {
    ...state,
    round: resetFromOrder(
      state.deckOrders[roundIdx],
      startingPlayer(banked, roundIdx, state.coinFlips),
    ),
    roundIdx,
    carry: banked,
    roundHistory: [...state.roundHistory, board],
    roundComplete: false,
  };
}
