// The Liquor Licensing Inspector (design §4.2). Plainclothes: the generator
// gives them nothing to look at, judge() scores them exactly like a punter, and
// this module never sees the door at all. It only answers one question, once a
// minute, while they are inside: is the room breaching right now?
//
// Everything here is keyed off clock minutes the caller supplies. No rng, no
// clock, no bus — the night shell owns all three.

import type { HeatStrike } from '../data/types';

export const INSPECTOR_TUNING = {
  /** Clock-minutes they spend inside after being admitted. */
  watchMin: 30,
  /**
   * The whole mechanic in one field. Breaches normally file a DEFERRED strike
   * that surfaces at the 3 AM audit, which is late enough to argue with. With
   * the inspector in the room the same breach lands now.
   */
  strikesDeferred: false,
};

export interface InspectorWatch {
  patronId: string;
  admittedAtMin: number;
  leavesAtMin: number;
}

/** Live venue conditions, sampled by the caller each clock minute. */
export interface VenueBreaches {
  overCapacity: boolean;
  maggotOnFloor: boolean;
  underageInside: boolean;
}

/**
 * Dazza's reveal, after they have already left. Deliberately the SAME line
 * whether the inspector saw a spotless room or filed three strikes — the player
 * finds out how it went from the heat count, not from Dazza, and never finds out
 * at all if they denied him.
 */
export const INSPECTOR_LINES: readonly string[] = [
  'that boring bloke you let in was the INSPECTOR you muppet',
  'mate. MATE. the quiet one was licensing. hes walked back out already. im gonna be sick',
  'so that was the inspector. terry is going to ring me about tonight and im going to tell him it went great',
];

/**
 * Reason strings are the dedupe key, so they are fixed sentences: no patron ids,
 * no clock times, no counts. Two different breaches must never collapse into
 * each other, and the same breach must read identically on every minute it is
 * live.
 *
 * WIRING, AND THIS IS NOT OPTIONAL: stable reasons only collapse a 30-minute
 * breach to one strike if the emitting site guards with
 * `shouldRecordStrike(state, strike)` (src/scenes/door/NightScene.ts) before
 * `bus.emit('heat:strike', ...)`, exactly as the door and deferred-audit paths
 * already do. Nothing on the bus dedupes — core/meters.ts records every strike
 * it hears and NightScene pulls the licence on the third. Emit observe()'s
 * strikes raw and a single sustained breach ends the run in three clock minutes.
 */
const BREACH_REASONS = {
  overCapacity: 'inspector saw the room over licensed capacity',
  maggotOnFloor: 'inspector saw a maggot still on the floor',
  underageInside: 'inspector saw a minor inside the venue',
} as const;

// Declared order, so a room breaching three ways files its strikes the same way
// every time and the night summary reads consistently.
const BREACH_ORDER = ['overCapacity', 'maggotOnFloor', 'underageInside'] as const;

// observe() carries no rng and has to stay pure, so the reveal picks its line off
// the one number the watch already holds. Admission minutes vary across a run, so
// the player does not hear the same line every night.
const rollFor = (watch: InspectorWatch): number =>
  (Math.abs(Math.trunc(watch.admittedAtMin)) % INSPECTOR_LINES.length) / INSPECTOR_LINES.length;

// NaN-safe like judge's lapRoll: a junk roll must fall back to a real line
// rather than returning undefined and printing an empty text bubble.
const clamp01 = (v: number): number => (Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0);

/** Picks a reveal line from a 0..1 roll. Exported so a caller can use its own rng. */
export function revealLine(roll: number): string {
  const i = Math.min(INSPECTOR_LINES.length - 1, Math.floor(clamp01(roll) * INSPECTOR_LINES.length));
  return INSPECTOR_LINES[i] ?? INSPECTOR_LINES[0]!;
}

export function startWatch(patronId: string, clockMin: number): InspectorWatch {
  return {
    patronId,
    admittedAtMin: clockMin,
    leavesAtMin: clockMin + INSPECTOR_TUNING.watchMin,
  };
}

/**
 * Half-open [admittedAtMin, leavesAtMin). The minute they leave is theirs, not
 * yours — a breach that starts as they walk out the door is not something they
 * saw, and an inclusive bound here would steal a minute the player is entitled to.
 */
export function isWatching(watch: InspectorWatch | null, clockMin: number): boolean {
  if (watch === null) return false;
  return clockMin >= watch.admittedAtMin && clockMin < watch.leavesAtMin;
}

export function hasLeft(watch: InspectorWatch | null, clockMin: number): boolean {
  return watch !== null && clockMin >= watch.leavesAtMin;
}

export function breachStrikes(breaches: VenueBreaches): HeatStrike[] {
  return BREACH_ORDER.filter((k) => breaches[k]).map((k) => ({
    reason: BREACH_REASONS[k],
    deferred: INSPECTOR_TUNING.strikesDeferred,
  }));
}

/**
 * Called once per clock minute for as long as a watch exists. Pure: the "has the
 * reveal already fired" bit is not stored anywhere, it is derived from the
 * ending minute being a single minute, which is why the caller must tick this at
 * one call per whole clock minute (GameClock emits exactly that).
 *
 * Returns one strike per live breach PER MINUTE — up to 3 * watchMin objects
 * across a window. That is deliberate (this module has no memory), and it is why
 * every strike must go out through `shouldRecordStrike`; see BREACH_REASONS.
 *
 * Known gap, caller's call: an inspector admitted after clock 330 has
 * leavesAtMin > 360, GameClock clamps at 360 and stops ticking, so the reveal
 * never fires and the player never learns who that was. Not clamped here because
 * clamping would make the window shorter than watchMin. To guarantee the reveal,
 * call observe(watch, watch.leavesAtMin, breaches) once at last drinks.
 */
export function observe(
  watch: InspectorWatch | null,
  clockMin: number,
  breaches: VenueBreaches,
): { strikes: HeatStrike[]; dazzaText?: string; watchEnded: boolean } {
  if (isWatching(watch, clockMin)) {
    return { strikes: breachStrikes(breaches), watchEnded: false };
  }

  // The one minute the reveal exists on. Before it: not inside yet. After it:
  // inert forever, because the caller keeps calling with the same stale watch.
  if (watch !== null && clockMin === watch.leavesAtMin) {
    return { strikes: [], dazzaText: revealLine(rollFor(watch)), watchEnded: true };
  }

  return { strikes: [], watchEnded: false };
}

// There is no denial branch, and its absence is the design (§4.3, LANE_CONTENT
// deliverable 3). Deny the inspector and NOTHING happens — no strike, no relief,
// no Dazza text, no note in the summary. They were a quiet person who wanted a
// drink, and the player never learns what they dodged. The paranoia is the tax.
// This will read as a missing branch to whoever maintains this next. It isn't.
// Do not add a reward for the correct call; the game does not know which call
// was correct, and that is the entire point of the mechanic.
