// Pure board reconstruction. The wire carries no board (StateView is summary + rack
// only), so the live grid is rebuilt by replaying the decoded move journal — exactly
// the dictionary-independent history invariant (ARCHITECTURE §9.1): apply each play's
// placed tiles onto an empty grid.

import type { MoveRecord } from './model';
import { BOARD_SIZE } from './premiums';

export interface BoardCell {
  letter: string;
  blank: boolean;
}

export type Board = (BoardCell | null)[][];

export function emptyBoard(): Board {
  return Array.from({ length: BOARD_SIZE }, () =>
    Array.from({ length: BOARD_SIZE }, () => null as BoardCell | null),
  );
}

function inBounds(r: number, c: number): boolean {
  return r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE;
}

/** replay folds every play move's tiles onto an empty board (pass/exchange/resign
 * change no squares). */
export function replay(moves: MoveRecord[]): Board {
  const b = emptyBoard();
  for (const m of moves) {
    if (m.action !== 'play') continue;
    for (const t of m.tiles) {
      if (inBounds(t.row, t.col)) b[t.row][t.col] = { letter: t.letter, blank: t.blank };
    }
  }
  return b;
}

/** lastMoveCells returns the cells of the last move's tiles (as "row,col" keys), but only
 * when that move placed tiles (a play). A trailing pass/exchange/resign/timeout highlights
 * nothing — the recent-move highlight tracks the last move overall, not the last word. */
export function lastMoveCells(moves: MoveRecord[]): Set<string> {
  const last = moves.length ? moves[moves.length - 1] : null;
  if (!last || last.action !== 'play') return new Set();
  return new Set(last.tiles.map((t) => `${t.row},${t.col}`));
}
