import type { RngStream, SeededRNG } from '../core/SeededRNG';
import { ageOn } from '../core/GameClock';
import { ARCHETYPES, ARCHETYPE_BY_KEY } from '../data/archetypes';
import { CONTRABAND } from '../data/contraband';
import { OUTFIT_VOCAB } from '../data/outfits';
import type { Archetype, IdCard, OutfitLayer, OutfitSlot, Patron } from '../data/types';

const FIRST = ['Shazza', 'Dazza', 'Bazza', 'Kylie', 'Jai', 'Tahlia', 'Lachlan', 'Brooke', 'Cooper', 'Mia', 'Ngaio', 'Con', 'Duc', 'Sofia', 'Marco', 'Aroha', 'Blake', 'Chantelle', 'Rhys', 'Imogen'] as const;
const LAST = ['Nguyen', 'Smith', 'Papadopoulos', 'Chen', 'OBrien', 'Kowalski', 'Singh', 'Taufa', 'Romano', 'Petersen', 'Doyle', 'Vella', 'Karim', 'Marsh', 'Duffy'] as const;
const JOKE_NAMES = ['M. Lovin', 'B. Drinkwater', 'Rusty Shackleford', 'A. Nonymous', 'J. Kebab'] as const;

let serial = 0;
/** Test hook — resets the patron id counter so runs are comparable. */
export function _resetPatronSerial(): void {
  serial = 0;
}

function isoDaysFrom(base: Date, days: number): string {
  const d = new Date(base.getTime());
  d.setDate(d.getDate() + days);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

/** DOB that makes the patron `years` old, offset by `extraDays`, on nightDate. */
function dobForAge(nightDate: Date, years: number, extraDays: number): string {
  const d = new Date(nightDate.getTime());
  d.setFullYear(d.getFullYear() - years);
  return isoDaysFrom(d, -extraDays);
}

function rollOutfit(rng: RngStream): OutfitLayer[] {
  const layers: OutfitLayer[] = [];
  for (const slot of Object.keys(OUTFIT_VOCAB) as OutfitSlot[]) {
    const def = rng.weighted(OUTFIT_VOCAB[slot].map((d) => [d, d.weight] as const));
    const layer: OutfitLayer = { slot, type: def.type, colour: rng.pick(def.colours) };
    // 0.18 (was 0.35, tuning 2026-07-19): with 8 rules live by 1 AM, a third of
    // the crowd wearing logos made most patrons deniable and emptied the room.
    if (def.canLogo && rng.chance(0.18)) layer.logo = true;
    if (def.canVintage && rng.chance(0.2)) layer.vintage = true;
    layers.push(layer);
  }
  return layers;
}

function rollId(rng: RngStream, nightDate: Date, trueAge: number, dollSeed: number, wantFake: boolean): IdCard {
  const name = `${rng.pick(FIRST)} ${rng.pick(LAST)}`;
  if (!wantFake) {
    // Real ID. DOBs cluster near the 18 boundary so the birthday math stays live.
    const extraDays = trueAge === 18 ? rng.int(0, 90) : rng.int(0, 364);
    return {
      name,
      dob: dobForAge(nightDate, trueAge, extraDays),
      expiry: isoDaysFrom(nightDate, rng.int(30, 365 * 4)),
      photoSeed: dollSeed,
    };
  }
  // Fake: claims 18-19ish, carries 1-2 visible tells. Occasionally the "fake" is
  // just an expired real card — expiry IS the tell then.
  const claimedAge = 18 + rng.int(0, 1);
  const tellPool = ['peelingLaminate', 'wrongHologram', 'jokeName', 'photoMismatch', 'expired'] as const;
  const first = rng.pick(tellPool);
  const second = rng.chance(0.4) ? rng.pick(tellPool) : undefined;
  const tells = new Set([first, second].filter((t): t is (typeof tellPool)[number] => t !== undefined));

  const card: IdCard = {
    name: tells.has('jokeName') ? rng.pick(JOKE_NAMES) : name,
    dob: dobForAge(nightDate, claimedAge, rng.int(10, 200)),
    expiry: tells.has('expired') ? isoDaysFrom(nightDate, -rng.int(5, 400)) : isoDaysFrom(nightDate, rng.int(30, 365 * 3)),
    photoSeed: tells.has('photoMismatch') ? dollSeed + 7777 : dollSeed,
    fake: {},
  };
  if (tells.has('peelingLaminate')) card.fake!.peelingLaminate = true;
  if (tells.has('wrongHologram')) card.fake!.wrongHologram = true;
  if (tells.has('jokeName')) card.fake!.jokeName = true;
  if (tells.has('photoMismatch')) card.fake!.photoMismatch = true;
  return card;
}

export interface GeneratorContext {
  rng: SeededRNG;
  nightDate: Date;
  /** Venue crowd identity: spawn-weight overrides merged over ARCHETYPES. */
  archetypeWeights?: Partial<Record<Archetype, number>>;
}

export function generatePatron(ctx: GeneratorContext, clockMin: number, archetypeOverride?: Archetype): Patron {
  const rng = ctx.rng.stream('patrons');
  const def = archetypeOverride
    ? ARCHETYPE_BY_KEY.get(archetypeOverride)!
    : rng.weighted(
        ARCHETYPES.map((a) => [a, ctx.archetypeWeights?.[a.key] ?? a.weight] as const),
      );

  const dollSeed = rng.int(0, 2 ** 31 - 1);
  const age = rng.int(def.ageRange[0], def.ageRange[1]);
  const wantFake = rng.chance(def.chances.fakeId ?? 0);
  const idCard = rollId(rng, ctx.nightDate, age, dollSeed, wantFake);

  // Crowd arrives progressively drunker as the night wears on.
  const lateDrift = Math.min(0.35, (clockMin / 360) * 0.45);
  const [lo, hi] = def.startDrunkRange;
  const intoxication = Math.min(1, lo + rng.next() * (hi - lo) + lateDrift * rng.next());

  const flags: Patron['flags'] = {};
  if (rng.chance(def.chances.contraband ?? 0)) {
    const item = rng.weighted(CONTRABAND.map((c) => [c, c.weight] as const));
    flags.contraband = [item.id];
    if (rng.chance(0.2)) {
      const extra = rng.weighted(CONTRABAND.map((c) => [c, c.weight] as const));
      if (extra.id !== item.id) flags.contraband.push(extra.id);
    }
  }
  if (rng.chance(def.chances.onGuestList ?? 0)) flags.onGuestList = true;
  if (flags.onGuestList || rng.chance(def.chances.claimsGuestList ?? 0)) flags.claimsGuestList = true;
  if (rng.chance(def.chances.knowsOwner ?? 0)) flags.knowsOwner = true;

  return {
    id: `p${serial++}`,
    dollSeed,
    archetype: def.key,
    age,
    idCard,
    outfit: rollOutfit(rng),
    intoxication,
    tolerance: def.toleranceRange[0] + rng.next() * (def.toleranceRange[1] - def.toleranceRange[0]),
    flags,
  };
}

/** True iff the ID itself (not the face in front of you) reads as valid tonight. */
export function idAge(patron: Patron, nightDate: Date): number {
  return ageOn(patron.idCard.dob, nightDate);
}
