// ROOM (design §6): the techno is so loud every line at the rope is a lip-read
// guess. Pure text transform, deterministic per (salt, word) — the same patron
// mouths the same guess twice, because re-asking doesn't reroll reality.
//
// Tone guard: the comedy is the player's OWN uncertain reading, never the
// speaker's — everyone in ROOM is perfectly articulate, you just can't hear.

/** What the player's brain offers when a word is half-caught. Club-adjacent
 * nonsense on purpose — a guess should be worse than the truth. */
const MISHEARINGS: readonly string[] = [
  'FIST', 'GOAT', 'CHIPS', 'NAN', 'TAX', 'DECAF', 'PILATES', 'HORSE',
  'COUNCIL', 'SOUP', 'LADDER', 'BINS', 'SHREK', 'MULCH',
];

const LOST = '[???]';

/** FNV-1a — cheap, stable, and never Math.random (CONTRACTS.md). */
function hash(s: string): number {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

/**
 * `"mate im on the list"` → `"[MATE] [IM?] [ON THE?] [FIST? LIST?]"`-shaped
 * guesses. Every token is bracketed (the brackets ARE the deafness); short
 * words survive better than long ones; some words arrive as two competing
 * guesses and some don't arrive at all.
 */
export function lipReadLine(line: string, salt: string): string {
  const words = line.split(/\s+/).filter(Boolean);
  const out: string[] = [];
  for (let i = 0; i < words.length; i++) {
    const clean = (words[i] ?? '').replace(/[^a-z0-9']/gi, '').toUpperCase();
    if (clean.length === 0) continue;
    const h = hash(`${salt}:${i}:${clean}`);
    const roll = h % 100;
    if (roll < 12) {
      out.push(LOST);
    } else if (roll < 28 && clean.length >= 4) {
      out.push(`[${MISHEARINGS[h % MISHEARINGS.length]}? ${clean}?]`);
    } else if (roll < 60) {
      out.push(`[${clean}?]`);
    } else {
      out.push(`[${clean}]`);
    }
  }
  return out.length > 0 ? out.join(' ') : LOST;
}
