import type { MatchState } from "./match";
import { N_CARDS, type GameState, type PlaceType } from "./types";

/**
 * v2 because a saved game now holds a match, not a round. The key is bumped
 * rather than migrated: a v1 save has no carry, no deals for rounds two and
 * three, and no coin flips, so there is nothing honest to migrate it into. An
 * unreadable save just deals a fresh game, which is the right failure.
 */
export const SAVED_GAME_KEY = "lost-cities-jax-ppo.game.v2";

export interface SavedFrame {
  state: MatchState;
  selection: { handSlot: number | null; placeType: PlaceType | null };
}

export interface SavedGame {
  version: 2;
  seed: string;
  /** 1 for a one-off deal, 3 for the classic match. */
  mode: number;
  frames: SavedFrame[];
  cursor: number;
  resultOpen: boolean;
}

function isNumberArray(value: unknown, length?: number): value is number[] {
  return Array.isArray(value) && (length === undefined || value.length === length) &&
    value.every((item) => typeof item === "number" && Number.isFinite(item));
}

function isBooleanArray(value: unknown, length: number): value is boolean[] {
  return Array.isArray(value) && value.length === length &&
    value.every((item) => typeof item === "boolean");
}

function isMatrix(value: unknown, rows: number, columns: number): value is number[][] {
  return Array.isArray(value) && value.length === rows &&
    value.every((row) => isNumberArray(row, columns));
}

function isDeckOrder(value: unknown): value is number[] {
  return isNumberArray(value, N_CARDS) && new Set(value).size === N_CARDS;
}

function isGameState(value: unknown): value is GameState {
  if (typeof value !== "object" || value === null) return false;
  const state = value as Partial<GameState>;
  return isDeckOrder(state.deckOrder) &&
    isNumberArray(state.cardLoc, N_CARDS) && isBooleanArray(state.handPublic, N_CARDS) &&
    isMatrix(state.colTop, 2, 5) && isMatrix(state.colHandshakes, 2, 5) &&
    isMatrix(state.colLength, 2, 5) && Array.isArray(state.piles) && state.piles.length === 5 &&
    state.piles.every((pile) => isNumberArray(pile)) &&
    typeof state.drawPtr === "number" && typeof state.stepCount === "number" &&
    (state.toMove === 0 || state.toMove === 1) && typeof state.done === "boolean";
}

function isMatchState(value: unknown): value is MatchState {
  if (typeof value !== "object" || value === null) return false;
  const match = value as Partial<MatchState>;
  return isGameState(match.round) &&
    Array.isArray(match.deckOrders) && match.deckOrders.length === 3 &&
    match.deckOrders.every(isDeckOrder) &&
    isNumberArray(match.coinFlips, 3) &&
    Number.isInteger(match.roundIdx) && match.roundIdx! >= 0 && match.roundIdx! < 3 &&
    isNumberArray(match.carry, 2) &&
    (match.totalRounds === 1 || match.totalRounds === 3) &&
    typeof match.roundComplete === "boolean" &&
    Array.isArray(match.roundHistory) &&
    match.roundHistory.every((entry) => isNumberArray(entry, 2)) &&
    typeof match.done === "boolean";
}

function isSavedGame(value: unknown): value is SavedGame {
  if (typeof value !== "object" || value === null) return false;
  const saved = value as Partial<SavedGame>;
  if (saved.version !== 2 || typeof saved.seed !== "string" ||
      (saved.mode !== 1 && saved.mode !== 3) ||
      !Array.isArray(saved.frames) || saved.frames.length === 0 ||
      !Number.isInteger(saved.cursor) || saved.cursor! < 0 || saved.cursor! >= saved.frames.length ||
      typeof saved.resultOpen !== "boolean") return false;
  return saved.frames.every((frame) => {
    if (typeof frame !== "object" || frame === null || !isMatchState(frame.state)) return false;
    const selection = frame.selection;
    return typeof selection === "object" && selection !== null &&
      (selection.handSlot === null || Number.isInteger(selection.handSlot)) &&
      (selection.placeType === null || selection.placeType === 0 || selection.placeType === 1);
  });
}

export function parseSavedGame(raw: string | null): SavedGame | null {
  if (raw === null) return null;
  try {
    const value: unknown = JSON.parse(raw);
    return isSavedGame(value) ? value : null;
  } catch {
    return null;
  }
}
