// The dress code Dazza texts through the night. Rules evaluate outfit DATA; the
// player only ever sees pixels (design §4.1) — the gap is the game.
//
// `text` and `activeFrom` are NOT written here. They are read out of
// data/strings/dazza.ts by ruleId, because a rule whose card disagrees with the
// text that announced it is unplayable, and two hand-kept copies always drift.

import './doorTypes';
import { DAZZA_TEXTS } from '../data/strings/dazza';
import type { DressCodeRule, OutfitLayer, OutfitSlot, Patron } from '../data/types';

/** Contract rule + a card label short enough for the on-screen dress-code board. */
export interface DoorDressCodeRule extends DressCodeRule {
  short: string;
}

// The vocab spells "not wearing one" as type 'none' (data/outfits.ts), so every
// lookup has to treat that as absent or `outer.type === 'blazer'` style checks
// start reasoning about a garment nobody is wearing.
const worn = (p: Patron, slot: OutfitSlot): OutfitLayer | undefined => {
  const layer = p.outfit.find((l) => l.slot === slot);
  return layer && layer.type !== 'none' ? layer : undefined;
};

const isType = (p: Patron, slot: OutfitSlot, type: string): boolean => worn(p, slot)?.type === type;

// Slots whose logo dollPlan.ts actually paints (src/patrons/dollPlan.ts:104 draws
// the white logo rect for the TOP slot and nothing else). The generator hands out
// logos on five slots, so enforcing all of them denies roughly a quarter of the
// queue over pixels that were never on screen — and Dazza's own text says
// "no VISIBLE logos". Widen this set the moment the renderer draws the rest; see
// CCR-3 in LANEHANDOVER.md.
const LOGO_VISIBLE_SLOTS: readonly OutfitSlot[] = ['top'];

const hasVisibleLogo = (p: Patron): boolean =>
  p.outfit.some(
    (l) => l.type !== 'none' && l.logo === true && LOGO_VISIBLE_SLOTS.includes(l.slot),
  );

// Signals of the bloke who will be at the DJ booth by 1:15 asking for one song,
// just one. No single item convicts — a jersey is a jersey — but two of these at
// once is a man with a request already typed into his notes app.
//
// `renders` marks the tells dollPlan.ts actually DRAWS today. A jersey torso and
// trackie legs are plain coloured rects, indistinguishable from a tee and jeans,
// so convicting on those two alone denies a man over pixels that were never on
// screen — the one thing design §4.1 forbids. Conviction therefore needs two
// tells of which at least one is visible. When the art pass gives jersey and
// trackies their own geometry, flip `renders` and this guard dissolves.
interface Tell {
  test: (p: Patron) => boolean;
  renders: boolean;
}

const SONG_REQUEST_TELLS: readonly Tell[] = [
  { test: (p) => isType(p, 'top', 'jersey'), renders: false },
  { test: (p) => isType(p, 'hair', 'mullet'), renders: true },
  { test: (p) => isType(p, 'legs', 'trackies'), renders: false },
  {
    test: (p) => {
      const acc = worn(p, 'accessory')?.type;
      return acc === 'cap' || acc === 'chain' || acc === 'bumbag';
    },
    renders: true,
  },
];

function looksLikeASongRequest(p: Patron): boolean {
  const hits = SONG_REQUEST_TELLS.filter((t) => t.test(p));
  return hits.length >= 2 && hits.some((t) => t.renders);
}

interface RuleSpec {
  id: string;
  short: string;
  violates: (p: Patron) => boolean;
}

// Declared in DAZZA_TEXTS order so the two files read side by side. That order is
// NOT chronological (Dazza is not an organised man), so the sort below is
// load-bearing — announcement order comes from activeFrom, never from this array.
const SPECS: readonly RuleSpec[] = [
  {
    id: 'noThongsSinglets',
    short: 'NO THONGS / NO SINGLETS',
    violates: (p) => isType(p, 'shoes', 'thong') || isType(p, 'top', 'singlet'),
  },
  {
    id: 'noLogos',
    short: 'NO VISIBLE LOGOS',
    violates: hasVisibleLogo,
  },
  {
    id: 'noWhiteSneakers',
    short: 'NO WHITE SNEAKERS, VINTAGE OK',
    violates: (p) => {
      const shoes = worn(p, 'shoes');
      return shoes?.type === 'sneaker' && shoes.colour === 'white' && shoes.vintage !== true;
    },
  },
  {
    id: 'noSongRequesters',
    short: 'NO SONG REQUESTERS',
    violates: looksLikeASongRequest,
  },
  {
    id: 'noBucketHats',
    short: 'NO BUCKET HATS',
    violates: (p) => isType(p, 'accessory', 'bucketHat'),
  },
  {
    id: 'noSunnies',
    short: 'NO SUNNIES INSIDE',
    violates: (p) => isType(p, 'accessory', 'sunnies'),
  },
  {
    id: 'noBlazers',
    short: 'NO BLAZERS',
    violates: (p) => isType(p, 'outer', 'blazer'),
  },
  {
    id: 'noHandHolders',
    short: 'NO HAND HOLDING',
    // QueueManager pairs arrivals; the linked-hands pixel is what the player sees.
    violates: (p) => p.flags.handHolding === true,
  },
];

const DAZZA_BY_RULE = new Map(
  DAZZA_TEXTS.filter((l): l is typeof l & { ruleId: string } => l.ruleId !== undefined).map(
    (l) => [l.ruleId, l] as const,
  ),
);

function build(spec: RuleSpec): DoorDressCodeRule {
  const line = DAZZA_BY_RULE.get(spec.id);
  // Shipping a rule with an empty card is worse than not booting: the player
  // would be denying people over a rule nobody announced.
  if (!line) throw new Error(`dressCode: no Dazza text announces rule '${spec.id}'`);
  return {
    id: spec.id,
    text: line.text,
    activeFrom: line.fromMin,
    short: spec.short,
    violates: spec.violates,
  };
}

for (const ruleId of DAZZA_BY_RULE.keys()) {
  if (!SPECS.some((s) => s.id === ruleId)) {
    throw new Error(`dressCode: Dazza announces rule '${ruleId}' but nothing enforces it`);
  }
}

// Sorted once at load so activeRules/violations preserve announcement order for free.
export const DRESS_CODE_RULES: readonly DoorDressCodeRule[] = SPECS.map(build).sort(
  (a, b) => a.activeFrom - b.activeFrom,
);

/** Rules in force at `clockMin`. Live the moment the text lands, hence inclusive. */
/**
 * Dazza's attention span. Only his most recent rules are LIVE — when a fifth
 * lands, the oldest is quietly rescinded (the card greys it out and he sends a
 * retraction text). Tuning pass 2026-07-19: with all eight rules cumulative,
 * ~55% of the late crowd was deniable and a rule-following door emptied the
 * room. Four keeps the card readable and the check fast, and Dazza forgetting
 * his own rules is exactly in character.
 */
export const ACTIVE_RULE_CAP = 4;

/** Every rule Dazza has EVER texted tonight, live or rescinded. */
export function announcedRules(clockMin: number): DoorDressCodeRule[] {
  return DRESS_CODE_RULES.filter((r) => r.activeFrom <= clockMin);
}

export function activeRules(clockMin: number): DoorDressCodeRule[] {
  return announcedRules(clockMin).slice(-ACTIVE_RULE_CAP);
}

export function violations(patron: Patron, clockMin: number): DoorDressCodeRule[] {
  return activeRules(clockMin).filter((r) => r.violates(patron));
}

export function ruleById(id: string): DoorDressCodeRule | undefined {
  return DRESS_CODE_RULES.find((r) => r.id === id);
}
