/**
 * LANE-UI — the house voice.
 *
 * Register: OSHA-poster deadpan (lore §8). Everything horrifying is labelled like
 * routine industrial equipment. Nothing here apologises, explains itself, or uses an
 * exclamation mark. The player is an employee who has already read the placard.
 *
 * All copy in the UI comes from this file so the tone stays in one place.
 */
import type { MachineDef, SimEvent } from '../contracts';

/**
 * Fallback flavor, keyed by MachineDef.id.
 *
 * `MachineDef.flavor` (contracts v2) is the real source and wins whenever LANE-DATA has
 * filled it. These are the seven I hand-transcribed from the codex in round 1, kept as
 * the fallback per orders. Unknown ids get `GENERIC_FLAVOR` — a new machine never
 * crashes the panel, it just sounds like a filing cabinet until its line lands.
 */
const MACHINE_FLAVOR: Record<string, string> = {
  'seam-extractor':
    'Chews the payload seam and spits raw ore. Do not place hands in the seam. ' +
    'The seam has no policy regarding hands.',
  belt:
    'Moves things. Moving things is the entire job. Back-pressure is a normal ' +
    'operating condition and is not a grievance.',
  demuxer:
    'Cleaves raw ore into luma and chroma. One intake, four chutes, no apologies. ' +
    'Contents were never yours.',
  quantizer:
    'Rounds coefficients to zero. The dial goes to CRIME. Material leaking from the ' +
    'seams is HF DUST and is not glitter. Do not lick the machine.',
  'mosh-reactor':
    'A decoder sealed in a vessel and starved of anchors until it hallucinates. ' +
    'The hallucination is the product. Certified vessel. Uncertified hallucination.',
  'decode-asic':
    'Handles standard profiles only. Cool, efficient, incurious. Generates bandwidth ' +
    'by declining to be interesting.',
  shipper:
    'Composites shipped freight onto THE SCREEN. All deliveries final. ' +
    'All sins permanent. Congratulations.',
};

const GENERIC_FLAVOR =
  'Industrial equipment. Operates within tolerances. Tolerances not disclosed.';

export function machineFlavor(def: MachineDef | undefined, id: string): string {
  return def?.flavor || MACHINE_FLAVOR[id] || GENERIC_FLAVOR;
}

/**
 * Jam reasons, in the house voice. The sim owns the reason strings; we translate the
 * ones we know and pass anything else through uppercased, so a new stall reason shows
 * up honestly rather than being swallowed.
 */
const JAM_TEXT: Record<string, string> = {
  'output full': 'OUTPUT BUFFER AT CAPACITY. THIS IS A YOU PROBLEM.',
  starved: 'INTAKE STARVED. NOTHING IS ARRIVING.',
  'no recipe': 'NO PROCESS ASSIGNED. UNIT AWAITS INSTRUCTION.',
  'no power': 'INSUFFICIENT BANDWIDTH. THE MATHEMATICS ARE UNMOVED.',
  'no seam': 'THERE IS NOTHING DOWN THERE. MOVE.',
};

export function jamText(reason: string): string {
  return JAM_TEXT[reason] ?? reason.toUpperCase();
}

/** Default item-id → display: dashes to spaces, uppercased. LANE-DATA's names win. */
const defaultItemName = (id: string) => id.replace(/-/g, ' ').toUpperCase();

/** Toast copy for events the player should notice without reading a manual. */
export function eventToast(
  ev: SimEvent,
  machineName: (id: number) => string,
  itemName: (id: string) => string = defaultItemName,
): string | null {
  switch (ev.kind) {
    case 'jammed':
      return `${machineName(ev.entity)}: ${jamText(ev.reason)}`;
    case 'brownout':
      return ev.on
        ? 'BUFFER UNDERRUN. THIS SECTOR IS NOW A STILL LIFE.'
        : 'BUFFER RESTORED. RESUME SINNING.';
    case 'scram':
      return ev.on
        ? `THERMAL SCRAM. ${machineName(ev.entity)} HAS EXCUSED ITSELF.`
        : `${machineName(ev.entity)} IS COOL ENOUGH TO CONTINUE. REGRETTABLY.`;
    case 'commissionDone':
      return 'FAX SENT. THE COLLECTOR DOES NOT SAY THANK YOU.';
    case 'removed':
      return 'UNIT RECLAIMED. THE FLOOR REMEMBERS.';
    case 'researched':
      return 'STANDARD RATIFIED. NEW CRIMES AVAILABLE.';
    case 'wildlife':
      return wildlifeToast(ev.wildlife, ev.on);
    case 'relicFound':
      return COPY.relicDiscovery;
    case 'repaired':
      return repairedToast(itemName(ev.item));
    default:
      return null; // shipped/crafted/placed are too frequent to toast
  }
}

/**
 * A parity mite has "improved" one of your artifacts (contracts v8 `repaired`). The
 * Correction is unfailingly courteous about vandalising your work — the receipt is the
 * insult. `IT LEFT A RECEIPT.` is the line the round-6 orders asked for by name.
 */
export function repairedToast(itemName: string): string {
  return `THE CORRECTION IMPROVED YOUR ${itemName}. IT LEFT A RECEIPT.`;
}

/**
 * Infestation copy. A dust pile spawning is a hatch warning (index.ts dedups it to one
 * per pile — see the notices logic there); a swarm spawning is the bite already landing.
 */
export function wildlifeToast(
  kind: 'dust-pile' | 'mosquito-swarm' | 'parity-mite',
  on: 'spawned' | 'cleared',
): string | null {
  if (kind === 'dust-pile') {
    return on === 'spawned'
      ? 'HF DUST IS POOLING. SOMETHING WILL HATCH IN IT.'
      : null; // clearing a pile is housekeeping, not news
  }
  if (kind === 'parity-mite') {
    // The Correction cannot perceive you, only deviation — which it repairs (lore §5).
    // A mite on the belt is your melt being edited into something that passes review.
    return on === 'spawned'
      ? 'A PARITY MITE IS EDITING YOUR MELT.'
      : 'MITE SWATTED. IT FORGIVES YOU. IT ALWAYS FORGIVES YOU.';
  }
  // mosquito-swarm
  return on === 'spawned'
    ? 'MOSQUITO SWARM. QUANTIZATION NOISE HAS LEGS NOW.'
    : 'SWARM DISPERSED. THE EDGES ARE QUIET AGAIN.';
}

export const COPY = {
  bandwidth: 'BANDWIDTH',
  brownout: 'BROWNOUT',
  /** Deficit covered by the tanks. Not a fault — the machine is doing its job. */
  onReserve: 'ON RESERVE',
  shipped: 'SHIPPED',
  paused: 'HALTED',
  running: 'RUNNING',
  tick: 'TICK',

  inspectorEmptyRecipe: 'NO PROCESS ASSIGNED. UNIT IS BEING PAID TO EXIST.',
  inspectorInput: 'INTAKE',
  inspectorOutput: 'OUTPUT',
  inspectorProgress: 'PROCESS',
  inspectorRecipe: 'PROCESS SELECT',
  inspectorNoRecipeOption: 'IDLE',
  inspectorClose: 'X',
  inspectorHeat: 'HEAT',
  heatThrottling: 'THROTTLING',
  heatScram: 'SCRAM — UNIT OFFLINE',

  faxHeader: 'INCOMING FAX',
  faxSubheader: 'GLITCH ACQUISITIONS — UNSOLICITED',
  faxNoCommission: 'NO ACTIVE COMMISSION. THE FAX MACHINE IS RESTING.',
  faxStamp: 'FAX SENT',
  faxStanding: 'STANDING ORDER',
  faxNext: 'NEXT IN TRAY',
  /** Shown against shipments The Correction quietly buys back (M3 fiction). */
  sanitising: 'SANITISING',

  // THE CORRECTION — parity-mite surfacing (round 6). The register is deliberately
  // gracious: the enemy filing a receipt for wrecking your work is the whole grievance.
  /** Top-strip ticker: how many artifacts the mites have "improved" this session. */
  correctedLabel: 'CORRECTED',
  /** Inspector section header — the unit's own log of what got edited on it. */
  firewallHeader: 'FIREWALL LOG',
  correctionCatches: (n: number) =>
    `THE CORRECTION HAS IMPROVED THIS UNIT ${n} TIME${n === 1 ? '' : 'S'}. YOU'RE WELCOME.`,

  techHeader: 'STANDARDS COMMITTEE',
  techSubheader: 'MEDIA ARCHAEOLOGY — RESEARCH DOWN, DERIVE UP',
  techActive: 'IN COMMITTEE',
  techDone: 'RATIFIED',
  /** Shown when the sim reports no research state at all. */
  researchOffline: 'RESEARCH OFFLINE. THE COMMITTEE HAS NOT CONVENED.',
  requires: 'REQUIRES:',

  placingHint: 'LMB PLACE · R ROTATE · ESC CANCEL',
  idleHint:
    '1-9 SELECT · [ ] PAGE · X REMOVE · T RESEARCH · SPACE HALT · CLICK A UNIT' +
    ' · MMB/WASD PAN · WHEEL ZOOM',

  removeLabel: 'REMOVE',
  removeKey: 'X',
  removeHint: 'REMOVE ARMED · LMB DEMOLISH · ESC STAND DOWN',

  /** The one hint the opening needs — fires once, on the first brownout ever. */
  firstBrownoutMercy: 'NOTHING IS GENERATING. THE COMMITTEE SUGGESTS: POWER.',

  /** Inspector line when a swarm is degrading the open machine. */
  mosquitoExposure: 'MOSQUITO EXPOSURE',
  mosquitoPenalty: (pct: number) => `PRECISION DOWN ${pct}%. SWARM ON THE EDGES.`,

  /** relicFound ceremony. */
  relicDiscovery: 'AUDIO BUS RESTORED. THE BITSTREAM HAS A VOICE.',

  // ABOUT / CREDITS (round 6) — reachable from the tech-panel footer. Deadpan colophon;
  // the voice slot is a standing reservation for the synthetic-voice credit that lands
  // when the voice-cloning plan ships, so there's a home for it already.
  creditsButton: 'ABOUT',
  creditsTitle: 'FKTRY',
  creditsTagline: 'A GLITCH-PRODUCTION FACTORY INSIDE A DYING VIDEO FILE.',
  creditsRepoLabel: 'REPO',
  creditsRepo: 'gitea.partly.party/monster/glytch',
  creditsCodexLabel: 'CODEX',
  creditsCodex: 'docs/FKTRY_LORE.md',
  creditsVoiceLabel: 'VOICES',
  creditsVoice: 'SYNTHESIZED ON SITE. NO PERFORMERS WERE CONSULTED.',
  /** Standing disclosure slot — replaced with the real credit when a synthetic voice ships. */
  creditsVoiceSlot: 'SYNTHETIC VOICE CREDIT: NONE ON FILE.',

  // THE TUNER DOCK
  tunerHeader: 'AUDIO BUS',
  tunerBandLabel: 'BAND',
  tunerVolLabel: 'VOL',
  tunerMute: 'MUTE',
  tunerMuted: 'MUTED',
  tunerLockedSuffix: 'SIGNAL LOCKED',
  tunerStatic: 'STATIC — TUNE FOR A STATION',
  tunerHint: '◄ ► TUNE · B BAND · M MUTE',
} as const;

export type Band = 'AM' | 'FM' | 'SW';

/** "88.1 FKTRY FM — SIGNAL LOCKED" / "104.6 FM — STATIC …". Station names are flavor. */
export function stationReadout(freqMHz: number, band: Band, station: string | null): string {
  const f = freqMHz.toFixed(1);
  if (station) return `${f} ${station} ${band} — ${COPY.tunerLockedSuffix}`;
  return `${f} ${band} — ${COPY.tunerStatic}`;
}

/** Direction names for the rotation readout. Dir 0..3 = N,E,S,W clockwise. */
export const DIR_NAME = ['N', 'E', 'S', 'W'] as const;
