// NOT TONIGHT — shared contract types. Source of truth: docs/CONTRACTS.md.
// Post-Phase-0 changes require a CONTRACT CHANGE REQUEST in LANEHANDOVER.md.

export type Archetype =
  | 'fresh18' | 'almost18' | 'financeBro' | 'influencer'
  | 'regular' | 'ownersMate' | 'quietMessy' | 'inspector' | 'punter';

export type DrunkStage = 'sober' | 'tipsy' | 'loose' | 'messy' | 'maggot';

export type OutfitSlot = 'shoes' | 'legs' | 'top' | 'outer' | 'hair' | 'accessory';

export interface OutfitLayer {
  slot: OutfitSlot;
  type: string;
  colour: string;
  logo?: boolean;
  vintage?: boolean;
}

export interface IdFakeTells {
  peelingLaminate?: boolean;
  wrongHologram?: boolean;
  jokeName?: boolean;
  photoMismatch?: boolean;
}

export interface IdCard {
  name: string;
  dob: string;    // ISO date
  expiry: string; // ISO date
  photoSeed: number;
  fake?: IdFakeTells;
}

export interface PatronFlags {
  contraband?: string[];
  onGuestList?: boolean;
  claimsGuestList?: boolean;
  knowsOwner?: boolean;
  uvStamped?: boolean;
  timesDeniedBefore?: number;
  disguised?: boolean;
  /** Set on couples by door/QueueManager; predicate for noHandHolders (CCR-1). */
  handHolding?: boolean;
  /** Stable cast identity for run-scoped regulars (cross-night memory, §4.1). */
  regularId?: string;
}

export interface Patron {
  id: string;
  dollSeed: number;
  archetype: Archetype;
  age: number;
  idCard: IdCard;
  outfit: OutfitLayer[];
  intoxication: number; // 0..1
  tolerance: number;    // 0..1
  flags: PatronFlags;
}

// ---- rules ----

export interface DressCodeRule {
  id: string;
  text: string;
  /** Card label for the on-screen dress-code board (CCR-4). */
  short?: string;
  activeFrom: number; // clock minutes since 21:00
  violates: (p: Patron) => boolean;
}

export type Verdict = 'admit' | 'deny' | 'sobrietyTest' | 'patDown' | 'wait';

export interface HeatStrike {
  reason: string;
  deferred?: boolean;
}

export interface VerdictOutcome {
  vibeDelta: number;
  aggroDelta: number;
  /** Queue-theatre payoff channel — the 'wait' verdict's reward (CCR-2). */
  hypeDelta?: number;
  heatStrike?: HeatStrike;
  dazzaText?: string;
}

// ---- night / run state ----

export interface IncidentRecord {
  clockMin: number;
  kind: string;
  patronId?: string;
  detail: string;
}

export interface NightState {
  venueId: string;
  nightIndex: number;
  vibe: number;   // 0..100
  aggro: number;  // 0..100
  heatStrikes: HeatStrike[];
  hype: number;
  capacity: { inside: number; licensed: number; clickerShown: number };
  clockMin: number; // minutes since 21:00; night ends at 360
  location: 'door' | 'floor';
  incidents: IncidentRecord[];
}

export interface RegularMemory {
  patronId: string;
  timesDenied: number;
  lastSeenNight: number;
  grudge: number; // 0..1
}

export interface GameState {
  v: 1;
  seed: number;
  venueIndex: number;
  nightIndex: number;
  heatStrikes: HeatStrike[];
  regulars: Record<string, RegularMemory>;
  pastReports: IncidentRecord[][];
  /** Scripted-encounter ids the run has already met (repeat gating). */
  seenEncounters?: string[];
}

// ---- event bus ----

export interface EventMap {
  'clock:tick': { clockMin: number };
  'night:phaseChange': { location: 'door' | 'floor' };
  'radio:call': { urgency: 1 | 2 | 3; line: string };

  'door:patronUp': { patron: Patron };
  'door:verdict': { patron: Patron; verdict: Verdict; outcome: VerdictOutcome };
  'door:clicker': { direction: 'in' | 'out' };
  'dazza:text': { text: string; rule?: DressCodeRule };

  'floor:infractionSpotted': { patronId: string; kind: 'drunk' | 'stall' | 'contraband' | 'noStamp' | 'fight' };
  'floor:ejection': { patronId: string; style: 'clean' | 'messy' };
  /** A patron went home on their own — capacity frees up (natural churn). */
  'floor:leave': { patronId: string };

  'beat:tick': { beatIndex: number; audioTimeMs: number };
  'audio:location': { location: 'door' | 'floor' };

  'meters:delta': { vibe?: number; aggro?: number; hype?: number };
  'meters:changed': { vibe: number; aggro: number; hype: number };
  'heat:strike': HeatStrike;

  // ---- folded in at Phase-1 review (see LANEHANDOVER.md REVIEW 2026-07-19) ----
  /** Single-writer incident trail: core/meters.ts appends to NightState.incidents. */
  'incident:log': IncidentRecord;
  /** Player fiddled with their phone while a patron waits — pure theatre. */
  'door:phoneTheatre': { durationMs: number };
  /** AudioContext resumed after the first real user gesture. */
  'audio:unlocked': { atMs: number };
  /** Bar line boundary (every 4th beat) — for anything that wants downbeats. */
  'beat:bar': { barIndex: number; beatIndex: number; audioTimeMs: number };
}
