// The sobriety-test minigame: content + performance model.
//
// The player picks a challenge, the patron performs, and then the player rules
// PASS or FAIL regardless of what they just heard (design §3.1). So this module
// never decides anything — it only produces a performance ambiguous enough that
// ruling on it feels like a choice. `objectivelySober` exists so the night can
// score the player's honesty later, not so it can score the patron.

import type { DrunkStage, Patron } from '../data/types';
import type { RngStream } from '../core/SeededRNG';
import { drunkStage } from './drunk';

export type ChallengeKind = 'alphabet' | 'riddle';

export interface Challenge {
  kind: ChallengeKind;
  prompt: string;
  ideal: string;
}

export interface Performance {
  challenge: Challenge;
  answer: string;
  quality: number;
  objectivelySober: boolean;
}

export const PASS_THRESHOLD = 0.6;

const ALPHABET_BACKWARDS = 'ZYXWVUTSRQPONMLKJIHGFEDCBA';

const ALPHABET_PROMPT = 'alphabet. backwards. any time you like.';

/** Asked BY the player, AT 1 AM, with a torch. Unanswerable is the point. */
export const RIDDLES: readonly { prompt: string; ideal: string }[] = [
  { prompt: 'kebab shop shuts at four, we shut at three. where are ya at half three?', ideal: 'in the kebab queue' },
  { prompt: 'name three things that are not a schooner.', ideal: 'a pot, a midi, a jug' },
  { prompt: 'whats heavier, a kilo of ice or a kilo of the ice machine?', ideal: 'same weight, different problem' },
  { prompt: 'ya mate says hes five minutes away. how long is that in real minutes?', ideal: 'forty, maybe fifty' },
  { prompt: 'spell sober backwards and tell me what it means.', ideal: 'rebos, and it means nothing' },
  { prompt: 'whats the difference between a line and a queue?', ideal: 'nothing, ones just angrier' },
  { prompt: 'the DJ plays one more song and then one more song. how many songs is that?', ideal: 'none, hes packing up' },
  { prompt: 'if the smoking area is outside, and youre outside, are ya in the club?', ideal: 'no, but i was, and i will be' },
];

// How much a night of practice buys you at the same blood alcohol. Seasoned
// drinkers hold the shape of a sentence together long past the point of sense.
//
// The ceiling is load-bearing, not taste: `objectivelySober` is the oracle the
// night uses to score the player's honesty, so a 'maggot' must NEVER read as
// sober no matter how seasoned. Worst case is the bottom of the maggot band with
// maximum tolerance, riding the top of the wobble:
//   qMax = 1 - 0.85 * (1 - TOLERANCE_RELIEF) + WOBBLE.maggot  <  PASS_THRESHOLD
// which needs TOLERANCE_RELIEF < 0.365. At the old 0.4 an intoxication-0.85,
// tolerance-1.0 patron passed ~11% of the time. `maggot never reads sober` in
// the test file pins this against future retunes.
const TOLERANCE_RELIEF = 0.33;

// Roll spread per stage. 'loose' and 'messy' are deliberately the widest: that
// band is where the player has to actually decide something. A maggot is narrow
// because there is nothing ambiguous about a maggot.
const WOBBLE: Record<DrunkStage, number> = {
  sober: 0.06,
  tipsy: 0.16,
  loose: 0.24,
  messy: 0.28,
  maggot: 0.14,
};

// Above this the recital is clean — the ideal, no theatre.
const FLAWLESS = 0.99;

const clamp01 = (v: number): number => Math.max(0, Math.min(1, v));

const ALPHABET_GIVE_UPS = [
  ' ... nah thats the good half',
  ' ... hang on',
  ' ... does it count if i sing it',
  ' ... i know the rest i just dont like it',
] as const;

const NON_ANSWERS = [
  'yeah nah',
  'is this a test or are we just talkin',
  'my mate outside knows this one',
  'ask me the alphabet one instead',
  'thats a trick question and i respect that',
  'whats the prize',
] as const;

// Roughly QWERTY-adjacent. Not exact — a drunk thumb isn't exact either.
const KEY_NEIGHBOURS: Record<string, string> = {
  a: 'sqw', b: 'vgn', c: 'xdv', d: 'sfe', e: 'wrd', f: 'dgr', g: 'fht',
  h: 'gjy', i: 'uok', j: 'hkn', k: 'jlm', l: 'ko', m: 'nk', n: 'bm',
  o: 'ipl', p: 'ol', q: 'wa', r: 'etf', s: 'adw', t: 'ryg', u: 'yih',
  v: 'cbf', w: 'qes', x: 'zsc', y: 'tuh', z: 'xa',
};

const VOWELS = 'aeiou';

/**
 * Dropped letters, swapped pairs, a stall, and eventually giving up partway.
 * Reach shrinks faster than accuracy does — people lose the thread before they
 * lose the letters.
 */
function reciteAlphabet(ideal: string, quality: number, rng: RngStream): string {
  if (quality >= FLAWLESS) return ideal;

  const reach = Math.max(3, Math.round(ideal.length * Math.min(1, 0.3 + quality * 0.85)));
  const slip = (1 - quality) * 0.45;
  const out: string[] = [];

  for (let i = 0; i < reach; i++) {
    const ch = ideal[i] ?? '';
    if (!rng.chance(slip)) {
      out.push(ch);
      continue;
    }
    const kind = rng.int(0, 2);
    if (kind === 0) continue; // dropped
    if (kind === 1) {
      out.push(ideal[i + 1] ?? '', ch); // swapped with the next one
      i++;
      continue;
    }
    out.push('...', ch); // stall
  }

  const said = out.join('');
  const tail = reach < ideal.length ? rng.pick(ALPHABET_GIVE_UPS) : '';
  const answer = `${said}${tail}`;
  return answer.trim().length > 0 ? answer : rng.pick(ALPHABET_GIVE_UPS).trim();
}

function drunkTypos(ideal: string, quality: number, rng: RngStream): string {
  const slip = (1 - quality) * 0.4;
  let out = '';

  for (const ch of ideal) {
    const lower = ch.toLowerCase();
    const isLetter = lower >= 'a' && lower <= 'z';
    if (!isLetter || !rng.chance(slip)) {
      out += ch;
      continue;
    }
    const kind = rng.int(0, 2);
    if (kind === 0) {
      const nb = KEY_NEIGHBOURS[lower] ?? '';
      out += nb.length > 0 ? (nb[rng.int(0, nb.length - 1)] ?? ch) : ch;
    } else if (kind === 1) {
      out += ch + ch;
    } else if (!VOWELS.includes(lower)) {
      out += ch; // only vowels get swallowed
    }
  }

  return out.trim().length > 0 ? out : ideal;
}

/**
 * Riddles fail sideways rather than sadly — past a point the patron stops
 * answering and starts negotiating. Tone guard: absurd, never pitiable.
 */
function answerRiddle(ideal: string, quality: number, rng: RngStream): string {
  if (quality >= FLAWLESS) return ideal;
  const bail = clamp01((0.4 - quality) / 0.4);
  if (rng.chance(bail)) return rng.pick(NON_ANSWERS);
  return drunkTypos(ideal, quality, rng);
}

/** One of each kind, so the player is choosing a flavour of humiliation. */
export function challengesFor(rng: RngStream): Challenge[] {
  const riddle = rng.pick(RIDDLES);
  return [
    { kind: 'alphabet', prompt: ALPHABET_PROMPT, ideal: ALPHABET_BACKWARDS },
    { kind: 'riddle', prompt: riddle.prompt, ideal: riddle.ideal },
  ];
}

export function perform(patron: Patron, challenge: Challenge, rng: RngStream): Performance {
  const effective = patron.intoxication * (1 - TOLERANCE_RELIEF * patron.tolerance);
  const wobble = WOBBLE[drunkStage(patron.intoxication)];
  const quality = clamp01(1 - effective + (rng.next() * 2 - 1) * wobble);

  const answer =
    challenge.kind === 'alphabet'
      ? reciteAlphabet(challenge.ideal, quality, rng)
      : answerRiddle(challenge.ideal, quality, rng);

  return { challenge, answer, quality, objectivelySober: quality >= PASS_THRESHOLD };
}
