/**
 * LANE-UI — inspector panel. Shows one entity: what it is, what the codex says about
 * it, what it's chewing, how far along, how hot, and why it has stopped.
 *
 * Holds an entity *id*, never an entity object — CONTRACTS says the sim may reuse
 * snapshot buffers between frames, so we re-look-up from the live snapshot each update
 * and close ourselves if the unit is gone.
 */
import type {
  EntityState, GameData, ItemDef, MachineDef, RecipeDef, SimSnapshot, UIBus,
} from '../contracts';
import { createChipRow, type ChipRow } from './chips';
import { intakeChips, outputChips } from './chipspec';
import type { Corrections } from './corrections';
import { attrs, cls, el, panel, style, text } from './dom';
import { footprintOf } from './pick';
import { COPY, jamText, machineFlavor } from './voice';

export interface Inspector {
  el: HTMLElement;
  open(entityId: number): void;
  close(): void;
  isOpen(): boolean;
  /** `corrections` (round 6) drives the FIREWALL LOG — how many edits landed on this unit. */
  update(snap: SimSnapshot, corrections?: Corrections): void;
  /** Entity currently shown, for the renderer-side selection highlight later. */
  openId(): number | null;
}

/** Heat above this throttles (CONTRACTS: "throttles >0.7, scrams at 1"). */
const THROTTLE_AT = 0.7;

export function createInspector(data: GameData, bus: UIBus, onClose?: () => void): Inspector {
  const machines = new Map<string, MachineDef>(data.machines.map((m) => [m.id, m]));
  const recipes = new Map<string, RecipeDef>(data.recipes.map((r) => [r.id, r]));
  const items = new Map<string, ItemDef>(data.items.map((i) => [i.id, i]));

  const root = panel();
  root.id = 'fk-inspect';

  const name = el('div', 'fk-ins-name');
  const closeBtn = el('button', 'fk-ins-x', [COPY.inspectorClose]);
  attrs(closeBtn, { type: 'button', title: 'CLOSE (ESC)' });
  const head = el('div', 'fk-ins-head', [name, closeBtn]);

  const flavor = el('div', 'fk-ins-flavor');

  // ------------------------------------------------------------- recipe picker
  const recHead = el('div', 'fk-h', [COPY.inspectorRecipe]);
  const recSelect = el('select', 'fk-ins-recipe');
  attrs(recSelect, { title: 'PROCESS SELECT' });
  recSelect.addEventListener('change', () => {
    if (openId === null) return;
    const v = recSelect.value;
    bus.dispatch({ kind: 'setRecipe', entity: openId, recipe: v === '' ? null : v });
  });
  /** The def whose options are currently in the <select>, to avoid rebuilding per frame. */
  let optionsFor: string | null = null;

  const noRecipe = el('div', 'fk-ins-empty', [COPY.inspectorEmptyRecipe]);

  const inHead = el('div', 'fk-h', [COPY.inspectorInput]);
  const inChips: ChipRow = createChipRow(items);
  const outHead = el('div', 'fk-h', [COPY.inspectorOutput]);
  const outChips: ChipRow = createChipRow(items);

  const progHead = el('div', 'fk-h', [COPY.inspectorProgress]);
  const progFill = el('div', 'fk-bar-f');
  const progBar = el('div', 'fk-bar', [progFill]);

  const heatHead = el('div', 'fk-h', [COPY.inspectorHeat]);
  const heatFill = el('div', 'fk-bar-f');
  const heatBar = el('div', 'fk-bar fk-heat', [heatFill]);
  const heatState = el('div', 'fk-ins-heat-state');

  const jam = el('div', 'fk-ins-jam');

  // MOSQUITO EXPOSURE (v5): a swarm degrading this machine, surfaced where the player
  // is already looking to understand why it's slow.
  const exposure = el('div', 'fk-ins-exposure');
  const exposureLine = el('div', 'fk-ins-exposure-pct');
  exposure.append(el('div', 'fk-h', [COPY.mosquitoExposure]), exposureLine);

  // FIREWALL LOG (v8): the count of edits The Correction has made to this unit's output,
  // resolved from `repaired` events that landed on this footprint. Hidden at zero — a
  // unit the mites haven't touched shouldn't advertise the enemy.
  const firewall = el('div', 'fk-ins-firewall');
  const firewallLine = el('div', 'fk-ins-firewall-line');
  firewall.append(el('div', 'fk-h', [COPY.firewallHeader]), firewallLine);

  const body = el('div', undefined, [
    recHead, recSelect, noRecipe,
    inHead, inChips.el, outHead, outChips.el,
    progHead, progBar,
    heatHead, heatBar, heatState,
  ]);

  root.append(head, flavor, el('div', 'fk-rule'), body, exposure, firewall, jam);

  let openId: number | null = null;

  function close() {
    if (openId === null) return; // already shut — don't re-notify selection
    openId = null;
    optionsFor = null;
    cls(root, 'is-open', false);
    onClose?.();
  }
  closeBtn.addEventListener('click', close);

  /** Rebuild the <option> list when the inspected machine changes. */
  function syncOptions(def: MachineDef | undefined) {
    if (optionsFor === (def?.id ?? null)) return;
    optionsFor = def?.id ?? null;
    recSelect.replaceChildren();
    const idle = el('option', undefined, [COPY.inspectorNoRecipeOption]);
    idle.value = '';
    recSelect.append(idle);
    for (const rid of def?.recipes ?? []) {
      const r = recipes.get(rid);
      const opt = el('option', undefined, [recipeLabel(r, rid)]);
      opt.value = rid;
      recSelect.append(opt);
    }
  }

  /** "2 MDAT ORE -> 1 LUMA BARS + 1 CHROMA SLURRY" in item display names. */
  function recipeLabel(r: RecipeDef | undefined, fallback: string): string {
    if (!r) return fallback.toUpperCase();
    const side = (m: Record<string, number>) =>
      Object.entries(m)
        .map(([id, n]) => `${n} ${items.get(id)?.name ?? id.toUpperCase()}`)
        .join(' + ');
    const ins = side(r.inputs);
    const outs = side(r.outputs);
    return ins ? `${ins} → ${outs}` : outs;
  }

  function paint(e: EntityState, corrections?: Corrections) {
    const def = machines.get(e.def);
    text(name, def?.name ?? e.def.toUpperCase());
    text(flavor, machineFlavor(def, e.def));

    // FIREWALL LOG: edits that landed on this unit's footprint. Off entirely at zero.
    const catches = def && corrections
      ? corrections.catchesOn(e.pos, footprintOf(def, e.dir))
      : 0;
    cls(firewall, 'is-on', catches > 0);
    if (catches > 0) text(firewallLine, COPY.correctionCatches(catches));

    // Only machines with something to choose get a picker.
    const choices = def?.recipes ?? [];
    const canChoose = choices.length > 0;
    syncOptions(def);
    for (const n of [recHead, recSelect]) style(n, 'display', canChoose ? '' : 'none');
    if (canChoose && document.activeElement !== recSelect) {
      const want = e.recipe ?? '';
      if (recSelect.value !== want) recSelect.value = want;
    }

    const recipe = e.recipe ? recipes.get(e.recipe) : null;
    const hasProcess = !!recipe;

    // "No process" only reads as a fault on a machine that could have one.
    style(noRecipe, 'display', !hasProcess && canChoose ? 'block' : 'none');
    for (const n of [outHead, outChips.el, progHead, progBar]) {
      style(n, 'display', hasProcess ? '' : 'none');
    }

    // Extractors take nothing from the world but the world itself — no INTAKE header
    // for a machine that has no intake.
    const hasInputs = !!recipe && Object.keys(recipe.inputs).length > 0;
    for (const n of [inHead, inChips.el]) style(n, 'display', hasInputs ? '' : 'none');

    if (recipe) {
      if (hasInputs) inChips.update(intakeChips(recipe, e));
      outChips.update(outputChips(recipe, e));
      style(progFill, 'width', `${Math.max(0, Math.min(1, e.progress)) * 100}%`);
    }

    // Heat: a bar rather than a number, because it's a thing you watch climb.
    // A scrammed unit stays on screen while it cools, so show the bar for either.
    const scram = e.scrammed === true;
    const hot = e.heat > 0.01 || scram;
    for (const n of [heatHead, heatBar, heatState]) style(n, 'display', hot ? '' : 'none');
    if (hot) {
      // `scrammed` is the latch (contracts v3). Heat alone can't show it: a scrammed
      // machine cools back below 1 while still being offline, and inferring the state
      // from a threshold here would mean re-implementing SIM's hysteresis and silently
      // desyncing the moment they retune it.
      const throttling = !scram && e.heat > THROTTLE_AT;
      style(heatFill, 'width', `${Math.min(1, e.heat) * 100}%`);
      cls(heatBar, 'is-throttling', throttling);
      cls(heatBar, 'is-scram', scram);
      text(heatState, scram ? COPY.heatScram : throttling ? COPY.heatThrottling : '');
      cls(heatState, 'is-scram', scram);
    }

    const jammed = !!e.jammed;
    cls(jam, 'is-on', jammed);
    if (jammed) text(jam, jamText(e.jammed!));
  }

  return {
    el: root,
    open(entityId) {
      openId = entityId;
      cls(root, 'is-open', true);
    },
    close,
    isOpen: () => openId !== null,
    openId: () => openId,
    update(snap, corrections) {
      if (openId === null) return;
      const e = snap.entities.find((x) => x.id === openId);
      if (!e) return close(); // unit was removed out from under us
      paint(e, corrections);

      // A swarm degrading THIS machine, if any. size is severity 0..1; we render it as
      // the precision hit. TEMPORARY: the sim owns the true throughput penalty and
      // doesn't expose a number, so this is the swarm's severity, not a confirmed
      // slowdown — see NOTES. Absent snap.wildlife (sim hasn't shipped it), no row.
      const swarm = snap.wildlife?.find(
        (w) => w.kind === 'mosquito-swarm' && w.target === openId,
      );
      cls(exposure, 'is-on', !!swarm);
      if (swarm) text(exposureLine, COPY.mosquitoPenalty(Math.round(swarm.size * 100)));
    },
  };
}
