/**
 * LANE-UI — HUD root. Everything DOM: build bar, top strip, inspector, fax, toasts.
 *
 * Rules of the house (CONTRACTS + LANE-UI.md):
 *  - `#ui` is pointer-events:none; every interactive child re-enables it, so clicks fall
 *    through to the world everywhere we haven't drawn a panel.
 *  - We never mutate the snapshot and never hold a reference to it across frames.
 *  - All mutation goes out as Commands on the bus.
 */
import type { GameData, ItemDef, MachineDef, SimEvent, SimSnapshot, UI, UIBus } from '../contracts';
import { createBuildBar } from './buildbar';
import { createCorrections } from './corrections';
import { createFax } from './fax';
import { createHotkeys } from './hotkeys';
import { createInspector } from './inspector';
import { createRoster, entityAt, syncRoster } from './pick';
import { createScreenView } from './screenview';
import { createSelection } from './selection';
import { injectStyle } from './style';
import { createTechPanel } from './techpanel';
import { createTopStrip } from './topstrip';
import { createToasts } from './toasts';
import { createTuner } from './tuner';
import { COPY } from './voice';

export function createUI(): UI {
  let machines = new Map<string, MachineDef>();
  let itemName: (id: string) => string = (id) => id.replace(/-/g, ' ').toUpperCase();
  let top: ReturnType<typeof createTopStrip>;
  let build: ReturnType<typeof createBuildBar>;
  let inspector: ReturnType<typeof createInspector>;
  let fax: ReturnType<typeof createFax>;
  let toasts: ReturnType<typeof createToasts>;
  let tech: ReturnType<typeof createTechPanel>;
  let tuner: ReturnType<typeof createTuner>;
  let screenView: ReturnType<typeof createScreenView>;

  /** Our own copy of the entity list; safe to read between frames. See pick.ts. */
  const roster = createRoster();
  /** Session tally of The Correction's edits — feeds the ticker + firewall log (round 6). */
  const corrections = createCorrections();
  let paused = false;

  // ---- onboarding + ceremony state (per session; none of it is sim truth) ----
  /** The opening's one hint. Fires on the first brownout ever, then never again. */
  let mercyShown = false;
  /** Dust piles we've already warned about, so a pile isn't a recurring nag. */
  const warnedPiles = new Set<number>();

  function revealTuner() {
    tuner.reveal();
    // The dock lands in the toasts' corner; move them up so it can't print over the dial.
    toasts.setStandoff(true);
  }

  function machineName(entityId: number): string {
    for (let i = 0; i < roster.length; i++) {
      const r = roster.entries[i];
      if (r.id === entityId) return machines.get(r.def)?.name ?? `UNIT ${entityId}`;
    }
    return `UNIT ${entityId}`;
  }

  return {
    init(root: HTMLElement, data: GameData, bus: UIBus) {
      injectStyle();
      machines = new Map(data.machines.map((m) => [m.id, m]));
      const items = new Map<string, ItemDef>(data.items.map((i) => [i.id, i]));
      itemName = (id) => items.get(id)?.name ?? id.replace(/-/g, ' ').toUpperCase();

      const sel = createSelection(bus);
      top = createTopStrip(data);
      build = createBuildBar(data, sel);
      // v5: closing the inspector — by X, by Esc, or because its unit was demolished —
      // must stop the renderer highlighting it.
      inspector = createInspector(data, bus, () => sel.clearInspect());
      fax = createFax(data);
      toasts = createToasts();
      tech = createTechPanel(data, bus);
      tuner = createTuner();
      screenView = createScreenView(document.getElementById('screen'));

      // A held tool and an open inspector are mutually exclusive: picking a machine or
      // arming remove supersedes the inspect selection, so shut the panel to match.
      sel.onChange(() => {
        if (sel.isArmed() && inspector.isOpen()) inspector.close();
      });

      root.append(
        top.el, build.el, inspector.el, fax.el, tech.el,
        tuner.el, screenView.el, toasts.el,
      );

      // ------------------------------------------------------------------ keyboard
      const keys = createHotkeys();
      for (let n = 1; n <= 9; n++) keys.bind(String(n), () => build.selectSlot(n));
      keys.bind('[', () => build.pageBy(-1));
      keys.bind(']', () => build.pageBy(1));
      keys.bind('r', () => sel.rotate());
      keys.bind('x', () => sel.toggleRemove());
      keys.bind('t', () => tech.toggle());
      // Tuner keys are inert until the dock is revealed, so they don't shadow anything
      // during the pre-discovery game.
      for (const k of ['arrowleft', 'arrowright', 'b', 'm']) keys.bind(k, () => tuner.onKey(k));
      keys.bind('escape', () => {
        // One key, several jobs, in the order the player expects: close the thing most
        // recently in front first, then put the tool down, then shut panels.
        if (screenView.isOpen()) screenView.close();
        else if (sel.isArmed()) sel.clear();
        else if (tech.isOpen()) tech.close();
        else inspector.close();
      });
      keys.bind(' ', () => bus.dispatch({ kind: 'setPaused', paused: !paused }));
      keys.attach();

      // ------------------------------------------------------------------- pointer
      // main.ts owns click-to-place on #game. We handle only the two cases it doesn't:
      // right-click cancels the pending build, and a plain left click with nothing
      // selected inspects whatever unit is under the cursor.
      const game = document.getElementById('game');
      const inWorld = (t: EventTarget | null) => t instanceof Node && !!game?.contains(t);

      addEventListener('contextmenu', (e) => {
        if (!inWorld(e.target)) return;
        e.preventDefault();
        sel.clear();
      });

      addEventListener('pointerdown', (e) => {
        if (e.button !== 0 || !inWorld(e.target)) return;
        // Anything held — a machine or the wrecking ball — makes this main.ts's click:
        // it places, and as of v4 it dispatches the removal too. We must not also
        // dispatch, or every demolition fires twice.
        if (sel.isArmed()) return;

        const tile = bus.pickTile(e.clientX, e.clientY);
        if (!tile) return;

        const hit = entityAt(roster, machines, tile);
        if (hit === null) {
          inspector.close();
        } else {
          inspector.open(hit);
          sel.inspect(hit); // v5: renderer highlights the unit we're showing
        }
      });

      // ?uidemo — build SIM's reference factory on boot. main.ts defines __fktryDemo
      // after ui.init(), and only in DEV, so ask on the next task rather than now.
      if (new URLSearchParams(location.search).has('uidemo')) {
        setTimeout(() => {
          const demo = (globalThis as any).__fktryDemo;
          if (typeof demo === 'function') demo();
          else console.info('[ui] ?uidemo: no __fktryDemo on window (production build?)');
        }, 0);
      }
    },

    update(snap: SimSnapshot, events: SimEvent[]) {
      paused = snap.paused;
      syncRoster(roster, snap);

      top.update(snap, corrections);
      build.update(snap); // padlocks track research
      inspector.update(snap, corrections);
      fax.update(snap);
      tech.update(snap);

      // The fossil may already be dug up in a loaded save, with no event to announce it.
      if (!tuner.isRevealed() && snap.relics?.some((r) => r.found)) revealTuner();

      for (const ev of events) {
        if (ev.kind === 'commissionDone') fax.stamp();

        // v8: The Correction edited an artifact — tally it for the ticker + firewall log.
        if (ev.kind === 'repaired') corrections.record(ev);

        if (ev.kind === 'relicFound') revealTuner(); // ceremony toast comes from voice.ts

        // One hatch warning per pile. `spawned` is edge-triggered so this is usually
        // already true; the guard keeps a re-emitting sim from turning it into a nag.
        if (ev.kind === 'wildlife' && ev.wildlife === 'dust-pile') {
          if (ev.on === 'cleared') warnedPiles.delete(ev.id);
          else if (warnedPiles.has(ev.id)) continue;
          else warnedPiles.add(ev.id);
        }

        toasts.push(ev, machineName, itemName);

        // The alarm alone teaches nothing. Once per session, on the very first brownout,
        // say what to do about it.
        if (ev.kind === 'brownout' && ev.on && !mercyShown) {
          mercyShown = true;
          toasts.pushMessage(COPY.firstBrownoutMercy, 'mercy');
        }
      }
      toasts.update(performance.now());
    },
  };
}
