/**
 * Board fairness (ARCHITECTURE §2.1).
 *
 * A round's base layout is one row of a cyclic 9×9 Latin square; each player sees that layout
 * transformed by one of the 8 symmetries of the square (dihedral group D4). Symmetries map rows,
 * columns and diagonals onto rows, columns and diagonals, so every board in a round has exactly
 * the same line structure: identical answers produce identical lines and identical scores.
 */
import { hashString } from './rng';
import { BOARD_SIZE, CATEGORY_COUNT, LINES, type BoardState, type CategoryDef, type Position, type TileState } from './types';

export type SymmetryKey =
  | 'identity'
  | 'rot90'
  | 'rot180'
  | 'rot270'
  | 'mirror-lr'
  | 'mirror-tb'
  | 'transpose'
  | 'anti-transpose';

export interface Symmetry {
  key: SymmetryKey;
  name: string;
  /** permutation[p] = the board cell that base-layout cell p moves to. */
  permutation: readonly Position[];
}

export type PositionClass = 'corner' | 'edge' | 'center';

type CellMap = (r: number, c: number) => readonly [number, number];

function permutationOf(map: CellMap): readonly Position[] {
  return Object.freeze(
    Array.from({ length: BOARD_SIZE }, (_, p) => {
      const [r, c] = map(Math.floor(p / 3), p % 3);
      return r * 3 + c;
    }),
  );
}

/** Index = symmetry id (0..7). Keys and order are shared with the db BoardTemplate seed. */
export const SYMMETRIES: readonly Symmetry[] = Object.freeze([
  { key: 'identity', name: 'Identity', permutation: permutationOf((r, c) => [r, c]) },
  { key: 'rot90', name: 'Rotate 90°', permutation: permutationOf((r, c) => [c, 2 - r]) },
  { key: 'rot180', name: 'Rotate 180°', permutation: permutationOf((r, c) => [2 - r, 2 - c]) },
  { key: 'rot270', name: 'Rotate 270°', permutation: permutationOf((r, c) => [2 - c, r]) },
  { key: 'mirror-lr', name: 'Mirror left–right', permutation: permutationOf((r, c) => [r, 2 - c]) },
  { key: 'mirror-tb', name: 'Mirror top–bottom', permutation: permutationOf((r, c) => [2 - r, c]) },
  { key: 'transpose', name: 'Transpose', permutation: permutationOf((r, c) => [c, r]) },
  { key: 'anti-transpose', name: 'Anti-transpose', permutation: permutationOf((r, c) => [2 - c, 2 - r]) },
] satisfies Symmetry[]);

const mod = (n: number, m: number): number => ((n % m) + m) % m;

function categoryKeys(categories: readonly (string | CategoryDef)[]): string[] {
  if (categories.length !== CATEGORY_COUNT) {
    throw new Error(`A board needs exactly ${CATEGORY_COUNT} categories, got ${categories.length}`);
  }
  const keys = categories.map((c) => (typeof c === 'string' ? c : c.key));
  if (new Set(keys).size !== keys.length) throw new Error('Board categories must be distinct');
  return keys;
}

/** Row `row` of the cyclic Latin square: base[cell] = categories[(cell + row) mod 9]. */
export function baseLayout(row: number, categories: readonly (string | CategoryDef)[]): string[] {
  const keys = categoryKeys(categories);
  return Array.from({ length: BOARD_SIZE }, (_, cell) => keys[mod(cell + row, CATEGORY_COUNT)] as string);
}

/** A fresh player board: board[perm[cell]] = base[cell]. */
export function dealBoard(categories: readonly (string | CategoryDef)[], row: number, symmetry: number): BoardState {
  const sym = SYMMETRIES[mod(symmetry, SYMMETRIES.length)] as Symmetry;
  const base = baseLayout(row, categories);
  const placed: string[] = new Array<string>(BOARD_SIZE);
  base.forEach((category, cell) => {
    placed[sym.permutation[cell] as Position] = category;
  });
  return {
    symmetry: mod(symmetry, SYMMETRIES.length),
    baseLayoutRow: mod(row, CATEGORY_COUNT),
    tiles: placed.map((category, position) => ({ position, category, activated: false, source: null, questionId: null })),
    lines: [],
    fullBoard: false,
  };
}

/** Symmetry for a player in a round: rotates through all 8 as players join and rounds advance. */
export function symmetryFor(joinIndex: number, roundIndex: number): number {
  return mod(joinIndex + roundIndex, SYMMETRIES.length);
}

/** Base layout row for a round: consecutive rounds walk the Latin square. */
export function baseLayoutRowFor(sessionId: string, roundIndex: number): number {
  return mod(hashString(sessionId) + roundIndex, CATEGORY_COUNT);
}

type Activatable = Pick<TileState, 'activated'>;

/** Indices into LINES whose three tiles are all activated. */
export function completedLines(tiles: readonly Activatable[]): number[] {
  const out: number[] = [];
  LINES.forEach((line, i) => {
    if (line.every((p) => tiles[p]?.activated === true)) out.push(i);
  });
  return out;
}

export function isFullBoard(tiles: readonly Activatable[]): boolean {
  return tiles.length === BOARD_SIZE && tiles.every((t) => t.activated);
}

/**
 * Wildcard auto-pick: the unactivated tile completing the most new lines, ties → lowest index.
 * The maximum is symmetry-invariant, so auto-picks never break cross-board fairness.
 * Returns null when the board is already full.
 */
export function bestWildcardPosition(tiles: readonly Activatable[]): Position | null {
  let best: Position | null = null;
  let bestCount = -1;
  for (let p = 0; p < BOARD_SIZE; p++) {
    if (tiles[p]?.activated !== false) continue;
    let count = 0;
    for (const line of LINES) {
      if (line.includes(p) && line.every((q) => q === p || tiles[q]?.activated === true)) count++;
    }
    if (count > bestCount) {
      best = p;
      bestCount = count;
    }
  }
  return best;
}

/** Category Swap: exchange the contents of two tiles (positions stay put). Returns new tiles. */
export function swapTiles(tiles: readonly TileState[], a: Position, b: Position): TileState[] {
  const out = tiles.map((t) => ({ ...t }));
  const ta = out[a];
  const tb = out[b];
  if (!ta || !tb) throw new Error(`swapTiles: invalid positions ${a}, ${b}`);
  out[a] = { ...tb, position: a };
  out[b] = { ...ta, position: b };
  return out;
}

export function positionClass(p: Position): PositionClass {
  if (p === 4) return 'center';
  return p % 2 === 0 ? 'corner' : 'edge';
}

export function isValidPosition(p: unknown): p is Position {
  return typeof p === 'number' && Number.isInteger(p) && p >= 0 && p < BOARD_SIZE;
}
