/**
 * The entropy solver, in the browser.
 *
 * This is what makes the page feel alive rather than pre-baked: it answers on
 * ANY word the visitor picks, including words no recording covers. It mirrors
 * `envs/wordle_five/wordle_five/solver.py` in behaviour — same opener, same
 * greedy rule, same tie-break — but not in implementation: Python precomputes
 * a 21 MB pattern matrix, which is not a thing to ship to a phone.
 *
 * Run this in a Web Worker. The opening scan is ~4,600 x 4,600 pattern
 * computations and will visibly jank the board on the main thread.
 */

import { scoreGuess, type Pattern } from './engine';

/**
 * The opening guess, precomputed.
 *
 * It never depends on the game state, and computing it in the browser would
 * cost the full 21M-pair scan on first paint for an answer that is always the
 * same. Regenerate with `uv run python -c "from wordle_five.solver import
 * _best_opener; from wordle_five.engine import answers; print(answers()[_best_opener()])"`
 * — and if the word list changes, this changes with it.
 */
export const OPENER = 'tares';

/** Expected bits of information from playing `guess` against a candidate set. */
export function entropyOf(guess: string, candidates: readonly string[]): number {
  const counts = new Map<Pattern, number>();
  for (const candidate of candidates) {
    const p = scoreGuess(guess, candidate);
    counts.set(p, (counts.get(p) ?? 0) + 1);
  }
  const total = candidates.length;
  let bits = 0;
  for (const n of counts.values()) {
    const probability = n / total;
    bits -= probability * Math.log2(probability);
  }
  return bits;
}

/** Every answer still viable given the feedback so far. */
export function filterCandidates(
  pool: readonly string[],
  history: readonly { guess: string; pattern: Pattern }[],
): string[] {
  let alive = pool as string[];
  for (const { guess, pattern } of history) {
    alive = alive.filter((word) => scoreGuess(guess, word) === pattern);
  }
  return alive;
}

export interface Suggestion {
  guess: string;
  bits: number;
  /** True if this guess could itself be the answer. */
  viable: boolean;
  candidatesBefore: number;
}

/**
 * The solver's move.
 *
 * Ties break toward a guess that could actually win — free expected value, and
 * it costs nothing in `consistency`. When it does NOT break that way, the
 * solver is buying information with a word that cannot win, which is exactly
 * the trade the reward's counterweight prices. The `viable` flag is surfaced
 * so the UI can show the moment it happens.
 */
export function suggest(
  pool: readonly string[],
  history: readonly { guess: string; pattern: Pattern }[],
  /** Cap the guesses considered, for responsiveness on a phone. */
  budget = 1500,
): Suggestion {
  const candidates = filterCandidates(pool, history);

  if (history.length === 0) {
    return { guess: OPENER, bits: entropyOf(OPENER, pool), viable: pool.includes(OPENER), candidatesBefore: pool.length };
  }
  if (candidates.length <= 2) {
    const guess = candidates[0] ?? OPENER;
    return { guess, bits: candidates.length > 1 ? 1 : 0, viable: true, candidatesBefore: candidates.length };
  }

  // Score every remaining candidate, plus a slice of the wider pool — a
  // non-candidate probe is often the better play, and considering only
  // candidates would quietly turn this into the candidate-only policy.
  const considered = new Set<string>(candidates);
  for (const word of pool) {
    if (considered.size >= budget) break;
    considered.add(word);
  }

  let best: Suggestion = { guess: candidates[0]!, bits: -1, viable: true, candidatesBefore: candidates.length };
  for (const guess of considered) {
    const bits = entropyOf(guess, candidates);
    const viable = candidates.includes(guess);
    if (bits > best.bits + 1e-12 || (Math.abs(bits - best.bits) <= 1e-12 && viable && !best.viable)) {
      best = { guess, bits, viable, candidatesBefore: candidates.length };
    }
  }
  return best;
}

/** Play a whole game against a known answer. Used for the reference depth. */
export function solve(pool: readonly string[], answer: string, maxGuesses = 6): string[] {
  const history: { guess: string; pattern: Pattern }[] = [];
  const played: string[] = [];
  for (let turn = 0; turn < maxGuesses; turn += 1) {
    const { guess } = suggest(pool, history);
    played.push(guess);
    const pattern = scoreGuess(guess, answer);
    if (pattern === 'GGGGG') return played;
    history.push({ guess, pattern });
  }
  return played;
}
