// THE_YARD — main app
const { useState, useEffect, useRef, useCallback, useMemo } = React;

function canUseWebGL() {
  if (!window.WebGLRenderingContext) return false;
  const canvas = document.createElement("canvas");
  const context =
    canvas.getContext("webgl2") ||
    canvas.getContext("webgl") ||
    canvas.getContext("experimental-webgl");
  return !!context;
}

function signatureDismissedAtBoot() {
  try { return localStorage.getItem("yard.signature.dismissed") === "1"; } catch (_) { return false; }
}
function completionDismissedAtBoot() {
  try { return localStorage.getItem("yard.completion.dismissed") === "1"; } catch (_) { return false; }
}

function App() {
  const [booted, setBooted] = useState(false);
  const [nearest, setNearest] = useState(0);
  const [openIndex, setOpenIndex] = useState(-1);
  const [mode, setMode] = useState("overview");
  const [termOpen, setTermOpen] = useState(false);
  const [termLines, setTermLines] = useState([]);
  const [debugSolved, setDebugSolved] = useState(false);
  const [hidden, setHidden] = useState(false);
  const [flash, setFlash] = useState(false);
  const [filter, setFilter] = useState("all");
  const [diveTick, setDiveTick] = useState(0);
  const [startupError, setStartupError] = useState(null);
  const [briefingOpen, setBriefingOpen] = useState(false);
  const [signatureOpen, setSignatureOpen] = useState(false);
  const [tourOpen, setTourOpen] = useState(false);
  // Bumped on every tour start; used as TourMode's `key` so each start is a
  // guaranteed-clean remount (step 0, no stale closing/timer state).
  const [tourSessionId, setTourSessionId] = useState(0);
  const [dossierOpen, setDossierOpen] = useState(false);
  const [tourStepId, setTourStepId] = useState(null);

  // Cinematic mode — when on, the host fades non-essential UI chrome
  // (QuickActions, FilterBar, MiniMap, HoverTooltip) so the walkthrough beat
  // is the only thing competing for attention. Toggling a body class lets
  // CSS handle the dimming consistently across all chrome without each
  // component knowing about the tour.
  const cinematic = tourOpen;
  useEffect(() => {
    if (cinematic) document.body.classList.add("is-cinematic");
    else document.body.classList.remove("is-cinematic");
    return () => document.body.classList.remove("is-cinematic");
  }, [cinematic]);
  // Persisted across sessions — the world "remembers the visitor" (S23). The
  // store is the user's localStorage; we keep the in-memory Set as the
  // authoritative source for renders and hydrate it once on mount.
  const [viewedProjects, setViewedProjects] = useState(() => {
    try {
      const raw = localStorage.getItem("yard.viewed.v1");
      if (!raw) return new Set();
      const arr = JSON.parse(raw);
      return Array.isArray(arr) ? new Set(arr) : new Set();
    } catch (_) { return new Set(); }
  });
  useEffect(() => {
    try {
      localStorage.setItem("yard.viewed.v1", JSON.stringify([...viewedProjects]));
    } catch (_) { /* localStorage unavailable — silently degrade */ }
    // Push to the 3D scene so the topPlate emissive picks up the persistent
    // warmth on visited bays. Scene exposes a setter; we keep the Set
    // upstream so the React tree stays authoritative.
    if (sceneRef.current && sceneRef.current.setVisited) {
      sceneRef.current.setVisited(viewedProjects);
    }
  }, [viewedProjects]);
  const interactedRef = useRef(false);

  // All container ids whose type is "project" — used to drive the conversion
  // CTA (after 3 viewed) and the completion moment (after every one viewed).
  const projectIds = useMemo(() => (
    window.YARD_DATA.containers.filter(c => c.type === "project").map(c => c.id)
  ), []);
  // Count only project bays so the conversion/completion CTA still gates on
  // "every project seen" rather than incidental chapter views.
  const projectsViewedCount = projectIds.reduce(
    (n, id) => n + (viewedProjects.has(id) ? 1 : 0), 0,
  );
  const allProjectsViewed = projectsViewedCount >= projectIds.length && projectIds.length > 0;
  const ctaMode = allProjectsViewed ? "completion" : "conversion";

  // Recommended route — the operator-curated tour through the yard. When
  // every stop has been visited *during this session*, we fire a one-time
  // "Route Complete" beat (full-screen celebratory toast with a DISPATCH cue).
  // The beat must only land for an actual completion event, not for returning
  // visitors whose viewedProjects set was hydrated from localStorage — that
  // produced a "you've walked the yard" state on entry.
  const RECOMMENDED_ROUTE = useMemo(() => ["intro", "mygui", "cortex", "potentia", "pods", "contact"], []);
  const routeCompleted = RECOMMENDED_ROUTE.every(id => viewedProjects.has(id));
  // Snapshot completion state at mount. If the visitor was already complete
  // before booting, treat the beat as already-handled — they're a returning
  // operator, not a fresh-completion event.
  const routeCompletedAtMountRef = useRef(routeCompleted);
  const [routeBeatShown, setRouteBeatShown] = useState(false);
  useEffect(() => {
    if (!routeCompleted) return;
    if (routeCompletedAtMountRef.current) return; // already complete on entry — suppress
    let dismissed = false;
    try { dismissed = localStorage.getItem("yard.route.complete.dismissed") === "1"; } catch (_) {}
    if (dismissed) return;
    if (routeBeatShown) return;
    // Slight delay so the beat lands after the bay panel closes, not during it.
    const t = setTimeout(() => setRouteBeatShown(true), 900);
    return () => clearTimeout(t);
  }, [routeCompleted, routeBeatShown]);
  const dismissRouteBeat = useCallback(() => {
    try { localStorage.setItem("yard.route.complete.dismissed", "1"); } catch (_) {}
    setRouteBeatShown(false);
  }, []);

  // Push filter to scene when it changes
  useEffect(() => {
    if (sceneRef.current && sceneRef.current.setFilter) {
      sceneRef.current.setFilter(filter);
    }
  }, [filter, booted]);

  // Track which bays have been opened. Counts on focus enter so a visitor who
  // opens 3 panels in a row (regardless of close timing) crosses the
  // conversion threshold immediately. Records every bay type (not just
  // projects) so the minimap/bay-grid visited badge applies to chapters too.
  useEffect(() => {
    if (openIndex < 0) return;
    const c = window.YARD_DATA.containers[openIndex];
    if (!c) return;
    setViewedProjects(prev => {
      if (prev.has(c.id)) return prev;
      const next = new Set(prev);
      next.add(c.id);
      return next;
    });
  }, [openIndex]);
  const sceneRef = useRef(null);
  const mountRef = useRef(null);
  const termInputRef = useRef(null);
  const konamiRef = useRef([]);
  const dragRef = useRef({ active: false, x: 0, y: 0 });
  const KONAMI = ["ArrowUp","ArrowUp","ArrowDown","ArrowDown","ArrowLeft","ArrowRight","ArrowLeft","ArrowRight","b","a"];

  useEffect(() => {
    if (!booted || sceneRef.current || startupError) return;
    const scene = window.YARD_SCENE;
    try {
      if (!canUseWebGL()) throw new Error("WebGL unavailable");
      scene.init(mountRef.current);
    } catch {
      setStartupError("webgl");
      return;
    }
    sceneRef.current = scene;
    // Push the localStorage-restored visited set into the scene the moment
    // it's available so returning visitors land on already-warm bays — the
    // viewedProjects effect won't have fired into the scene before this point.
    if (scene.setVisited) scene.setVisited(viewedProjects);
    scene.onChange((p) => {
      if (typeof p.nearest === "number") setNearest(p.nearest);
      if (p.dive) setDiveTick(p.dive.t);
      if (typeof p.openIndex === "number") setOpenIndex(p.openIndex);
      if (p.mode) setMode(p.mode);
    });
    setTermLines(window.YARD_DATA.terminal.boot.map(t => ({ text: t, kind: "ok" })));
  }, [booted, startupError]);

  // Onboarding briefing — fires ~1.3s after boot on EVERY fresh load, so a
  // first-time visitor always gets oriented. Dismissal is session-only:
  // closing it just sets briefingOpen=false in memory, so a page refresh
  // always re-shows it (no localStorage suppression). Kept in its own effect
  // so a WebGL retry or re-render can never drop the schedule.
  useEffect(() => {
    if (!booted || startupError) return;
    const t = setTimeout(() => setBriefingOpen(true), 1300);
    return () => clearTimeout(t);
  }, [booted, startupError]);

  useEffect(() => {
    if (!booted || !sceneRef.current) return;
    const scene = sceneRef.current;
    const onWheel = (e) => {
      if (document.activeElement && document.activeElement.tagName === "INPUT") return;
      if (e.target.closest(".hud, .terminal-bar, .panel, .boot, .minimap, .filter-bar")) return;
      e.preventDefault();
      const ndc = {
        x: (e.clientX / window.innerWidth) * 2 - 1,
        y: -(e.clientY / window.innerHeight) * 2 + 1,
      };
      if (e.ctrlKey) {
        scene.zoomBy(e.deltaY * 4, ndc);
        return;
      }
      scene.zoomBy(e.deltaY, ndc);
    };
    const onKey = (e) => {
      konamiRef.current = [...konamiRef.current, e.key].slice(-KONAMI.length);
      if (konamiRef.current.join(",").toLowerCase() === KONAMI.join(",").toLowerCase()) {
        setFlash(true); setHidden(true);
        setTimeout(() => setFlash(false), 1000);
        konamiRef.current = [];
      }
      if (document.activeElement && (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA")) {
        if (e.key === "Escape") document.activeElement.blur();
        return;
      }
      if (e.key === "Escape") {
        if (scene.getMode() === "focus") scene.closeBay();
        if (hidden) setHidden(false);
        if (briefingOpen) setBriefingOpen(false);
      }
      else if (e.key === "`" || e.key === "~") setTermOpen(t => !t);
      else if (e.key === "r" || e.key === "R") scene.resetView();
      // In focus mode, ←/→ navigate between bays. In overview they pan.
      else if (e.key === "ArrowLeft") {
        if (scene.getMode() === "focus") {
          const idx = scene.getOpenIndex();
          const total = window.YARD_DATA.containers.length;
          if (idx >= 0) scene.focusBay((idx - 1 + total) % total);
        } else scene.panBy(-40, 0);
      }
      else if (e.key === "ArrowRight") {
        if (scene.getMode() === "focus") {
          const idx = scene.getOpenIndex();
          const total = window.YARD_DATA.containers.length;
          if (idx >= 0) scene.focusBay((idx + 1) % total);
        } else scene.panBy(40, 0);
      }
      else if (e.key === "a" || e.key === "A") scene.panBy(-40, 0);
      else if (e.key === "d" || e.key === "D") scene.panBy(40, 0);
      else if (e.key === "ArrowUp" || e.key === "w" || e.key === "W") scene.panBy(0, -40);
      else if (e.key === "ArrowDown" || e.key === "s" || e.key === "S") scene.panBy(0, 40);
      else if (e.key === "q" || e.key === "Q") scene.orbitBy(-0.15, 0);
      else if (e.key === "e" || e.key === "E") scene.orbitBy(0.15, 0);
    };

    // pointer drag — left = pan, right/shift+left = orbit
    const onDown = (e) => {
      if (e.target.closest(".hud, .terminal-bar, .panel, .boot")) return;
      const isOrbit = e.button === 2 || e.shiftKey;
      dragRef.current = { active: true, x: e.clientX, y: e.clientY, moved: false, orbit: isOrbit, startX: e.clientX, startY: e.clientY };
    };
    const onMove = (e) => {
      if (!dragRef.current.active) return;
      const dx = e.clientX - dragRef.current.x;
      const dy = e.clientY - dragRef.current.y;
      const total = Math.abs(e.clientX - dragRef.current.startX) + Math.abs(e.clientY - dragRef.current.startY);
      if (total > 5) dragRef.current.moved = true;
      if (dragRef.current.orbit) {
        scene.orbitBy(-dx * 0.005, -dy * 0.005);
      } else {
        scene.panBy(-dx, -dy);
      }
      dragRef.current.x = e.clientX;
      dragRef.current.y = e.clientY;
    };
    const onUp = (e) => {
      const wasMoved = dragRef.current.moved;
      dragRef.current.active = false;
      // click only fires (selection) if drag didn't really move
      if (!wasMoved && !e.target.closest(".hud, .terminal-bar, .panel, .boot")) {
        scene.tryClickPick();
      }
    };
    const onContextMenu = (e) => e.preventDefault();

    // touch — single = pan, double = pinch zoom + 2-finger orbit
    let touch = null;
    const onTouchStart = (e) => {
      if (e.touches.length === 1) {
        touch = { x: e.touches[0].clientX, y: e.touches[0].clientY, mode: "pan", moved: false, sx: e.touches[0].clientX, sy: e.touches[0].clientY };
      } else if (e.touches.length === 2) {
        const t1 = e.touches[0], t2 = e.touches[1];
        const d = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
        touch = { dist: d, cx: (t1.clientX + t2.clientX) / 2, cy: (t1.clientY + t2.clientY) / 2, mode: "pinch" };
      }
    };
    const onTouchMove = (e) => {
      if (!touch) return;
      e.preventDefault();
      if (touch.mode === "pan" && e.touches.length === 1) {
        const t1 = e.touches[0];
        const dx = t1.clientX - touch.x, dy = t1.clientY - touch.y;
        if (Math.abs(t1.clientX - touch.sx) + Math.abs(t1.clientY - touch.sy) > 5) touch.moved = true;
        scene.panBy(-dx, -dy);
        touch.x = t1.clientX; touch.y = t1.clientY;
      } else if (touch.mode === "pinch" && e.touches.length === 2) {
        const t1 = e.touches[0], t2 = e.touches[1];
        const d = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY);
        scene.zoomBy((touch.dist - d) * 4);
        touch.dist = d;
        // 2-finger drag = orbit
        const cx = (t1.clientX + t2.clientX) / 2, cy = (t1.clientY + t2.clientY) / 2;
        scene.orbitBy(-(cx - touch.cx) * 0.005, -(cy - touch.cy) * 0.005);
        touch.cx = cx; touch.cy = cy;
      }
    };
    const onTouchEnd = (e) => {
      if (touch && touch.mode === "pan" && !touch.moved && e.target && !e.target.closest(".hud, .terminal-bar, .panel, .boot")) {
        scene.tryClickPick();
      }
      touch = null;
    };

    window.addEventListener("wheel", onWheel, { passive: false });
    window.addEventListener("keydown", onKey);
    window.addEventListener("pointerdown", onDown);
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    window.addEventListener("contextmenu", onContextMenu);
    window.addEventListener("touchstart", onTouchStart, { passive: false });
    window.addEventListener("touchmove", onTouchMove, { passive: false });
    window.addEventListener("touchend", onTouchEnd);
    return () => {
      window.removeEventListener("wheel", onWheel);
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("pointerdown", onDown);
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
      window.removeEventListener("contextmenu", onContextMenu);
      window.removeEventListener("touchstart", onTouchStart);
      window.removeEventListener("touchmove", onTouchMove);
      window.removeEventListener("touchend", onTouchEnd);
    };
  }, [booted, hidden, briefingOpen]);

  const runCommand = useCallback((raw) => {
    const cmd = raw.trim();
    const data = window.YARD_DATA;
    const containers = data.containers;
    const push = (arr) => setTermLines(prev => [...prev, { text: raw, kind: "user" }, ...arr]);
    const out = (text, kind="out") => ({ text, kind });
    const lc = cmd.toLowerCase();

    if (!cmd) { setTermLines(p=>[...p, { text: raw, kind:"user" }]); return; }
    if (lc === "help" || lc === "?" || lc === "h") return push(data.terminal.help.map(t => out(t)));
    if (lc === "clear" || lc === "cls") return setTermLines([]);
    if (lc === "reset" || lc === "reset onboarding" || lc === "first run") {
      // Dev / replay aid — clears the yard's per-visitor localStorage flags
      // (briefing, CTAs, route progress, visited bays) and reloads, so the
      // first-time onboarding flow can be re-tested without hand-editing
      // localStorage. Opt-in only: real visitors never trigger this.
      try {
        ["yard.signature.dismissed", "yard.completion.dismissed", "yard.route.complete.dismissed", "yard.tour.seen", "yard.viewed.v1"]
          .forEach(k => localStorage.removeItem(k));
      } catch (_) { /* localStorage unavailable — nothing to clear */ }
      window.setTimeout(() => window.location.reload(), 800);
      return push([out("operator state cleared — reloading the yard as a first-time visitor...", "ok")]);
    }
    if (lc === "manifest" || lc === "dossier") {
      setDossierOpen(true);
      return push([out("opening operator dossier...", "ok")]);
    }
    if (lc === "route") {
      const routeIds = ["intro", "mygui", "cortex", "potentia", "pods", "contact"];
      const visited = viewedProjects;
      const done = routeIds.filter(id => visited.has(id)).length;
      const lines = [out(`// recommended route · ${done}/${routeIds.length} cleared`, "head")];
      routeIds.forEach((id, i) => {
        const c = containers.find(cc => cc.id === id);
        if (!c) return;
        const stamp = visited.has(id) ? "✓" : (i === done ? "▸" : "·");
        const kind = visited.has(id) ? "ok" : (i === done ? "head" : "dim");
        lines.push(out(`  ${stamp}  ${String(i+1).padStart(2," ")}. ${c.label}  ${c.title}`, kind));
      });
      return push(lines);
    }
    if (lc === "ls" || lc === "ls -la") {
      return push([
        out("BAY  ID         TYPE       TITLE", "head"),
        ...containers.map((c,i)=>out(`${c.label}  ${c.id.padEnd(10)} ${(c.type+"        ").slice(0,10)} ${c.title}`))
      ]);
    }
    if (lc === "whoami") return push([
      out(data.identity.name + " · " + data.identity.role, "ok"),
      out(data.identity.headline),
      out("loc: " + data.identity.location, "dim"),
    ]);
    if (lc === "cat resume") return push([
      out("// resume.short", "head"),
      out("CS @ Guelph · Co-founder @ MyGUI · President @ SEC.Guelph"),
      out("Incoming IT Intern @ PODS · Summer 2026"),
      out("Built: Cortex, YUTH (Hack Canada finalist), Potentia (App Store), ScholarScope, AlgoViz"),
      out("Stack: TypeScript, React, Three, Postgres, LLMs, Swift", "dim"),
    ]);
    if (lc === "stack") return push([
      out("frontend  React · Three · WebGL · Tailwind", "dim"),
      out("backend   Node · TS · Postgres · Redis", "dim"),
      out("ai        OpenAI · Anthropic · vector dbs · embeddings", "dim"),
      out("infra     Vercel · Fly · Docker · GitHub Actions", "dim"),
    ]);
    if (lc === "tour" || lc === "walkthrough") {
      // Same single start path as the onboarding modal and the top-right
      // button — never a separate inline sequence.
      handleTourStart();
      return push([out("starting guided walkthrough... use ←/→ to step.", "ok")]);
    }
    if (lc === "resume" || lc === "cv") {
      const resume = data.identity && data.identity.routes && data.identity.routes.resume;
      if (resume && resume.href) {
        window.open(resume.href, "_blank", "noopener,noreferrer");
        return push([out(`opening ${resume.value || resume.href}...`, "ok")]);
      }
      return push([out("resume link not configured.", "err")]);
    }
    if (lc === "contact" || lc === "dispatch") {
      const idx = containers.findIndex(c=>c.id==="contact");
      sceneRef.current.focusBay(idx);
      return push([out("dispatching → BAY-11 · contact channel...", "ok")]);
    }
    if (lc === "sudo hire") return push([
      out("[sudo] password for hr: ************", "dim"),
      out("ACCESS GRANTED. ship a message at BAY-11 · DISPATCH.", "ok"),
    ]);
    if (lc === "exit" || lc === "quit" || lc === "q") {
      return push([out("can't leave the yard. you're already home.", "err")]);
    }
    if (lc.startsWith("open ")) {
      const id = lc.slice(5).trim();
      const idx = containers.findIndex(c => c.id === id || c.label.toLowerCase() === id);
      if (idx === -1) return push([out(`no container '${id}'. try \`ls\`.`, "err")]);
      sceneRef.current.focusBay(idx);
      return push([out(`opening ${containers[idx].label} · ${containers[idx].title}...`, "ok")]);
    }
    push([out(`command not found: ${cmd}. type \`help\`.`, "err")]);
  }, []);

  // Signature CTA gating — view-count driven instead of timer-based.
  // Conversion fires after 3 projects viewed; completion fires after every
  // project has been seen. Each variant has its own dismissal flag so the
  // completion moment still lands even if the visitor dismissed the earlier
  // conversion prompt. Both CTAs wait for overview mode so they never
  // overlap an open manifest.
  useEffect(() => {
    if (!booted || startupError) return;
    if (signatureOpen) return;
    if (briefingOpen) return;
    if (mode !== "overview") return;
    if (projectsViewedCount < 3) return;
    if (allProjectsViewed) {
      if (completionDismissedAtBoot()) return;
      setSignatureOpen(true);
      return;
    }
    if (signatureDismissedAtBoot()) return;
    setSignatureOpen(true);
  }, [booted, startupError, briefingOpen, mode, signatureOpen, projectsViewedCount, allProjectsViewed]);

  const openContainer = openIndex >= 0 ? window.YARD_DATA.containers[openIndex] : null;
  const isDebug = openContainer && openContainer.type === "debug";
  const isContact = openContainer && openContainer.type === "contact";
  // Body class so the rest of the world chrome dims while a dossier is open.
  // Lifts after a single frame on close so the world un-dims smoothly.
  const dossierMounted = !!openContainer && !isDebug && !isContact;
  useEffect(() => {
    if (dossierMounted) document.body.classList.add("is-dossier-open");
    else document.body.classList.remove("is-dossier-open");
    return () => document.body.classList.remove("is-dossier-open");
  }, [dossierMounted]);
  const total = window.YARD_DATA.containers.length;
  const prevIdx = openIndex >= 0 ? (openIndex - 1 + total) % total : -1;
  const nextIdx = openIndex >= 0 ? (openIndex + 1) % total : -1;
  const goPrev = () => sceneRef.current && prevIdx >= 0 && sceneRef.current.focusBay(prevIdx);
  const goNext = () => sceneRef.current && nextIdx >= 0 && sceneRef.current.focusBay(nextIdx);
  const prevC = prevIdx >= 0 ? window.YARD_DATA.containers[prevIdx] : null;
  const nextC = nextIdx >= 0 ? window.YARD_DATA.containers[nextIdx] : null;

  const markInteracted = useCallback(() => { interactedRef.current = true; }, []);
  const handleProjects = useCallback(() => {
    markInteracted();
    const scene = sceneRef.current;
    // "Moment" sequencing: close any open bay, play the camera-dip + lift
    // beat, then drop the filter so sorting lands on a primed yard. The
    // moment is purely visual — if the scene isn't ready yet, fall back to
    // an immediate filter so the click never feels lost.
    if (scene) {
      if (scene.getMode() === "focus") scene.closeBay();
      if (scene.highlightZone) scene.highlightZone("build", 3500);
      const moment = scene.playShowcaseMoment ? scene.playShowcaseMoment() : Promise.resolve();
      moment.then(() => setFilter("zone:build"));
      return;
    }
    setFilter("zone:build");
  }, [markInteracted]);
  const handleDispatch = useCallback(() => {
    markInteracted();
    if (sceneRef.current) sceneRef.current.openContact();
  }, [markInteracted]);
  const handleResume = useCallback(() => {
    markInteracted();
    const resume = window.YARD_DATA.identity && window.YARD_DATA.identity.routes && window.YARD_DATA.identity.routes.resume;
    if (resume && resume.href) {
      window.open(resume.href, "_blank", "noopener,noreferrer");
    } else if (sceneRef.current) {
      // No PDF yet — route to the manifest where the resume status is shown.
      sceneRef.current.openContact();
    }
  }, [markInteracted]);
  // ── THE single guided-walkthrough start path ───────────────────────────
  // Every launcher — onboarding modal "Start Guided Walkthrough", top-right
  // "Start Tour", QuickAccessDock, terminal `tour` — calls THIS and nothing
  // else, so the walkthrough always begins from an identical clean state.
  // Bumping tourSessionId remounts TourMode (via its `key`), which hard-resets
  // step→0 and discards any stale closing/fade state or leftover step timers
  // from a prior run. Do not add a second tour-start path — route new
  // launchers through here.
  const handleTourStart = useCallback(() => {
    markInteracted();
    setBriefingOpen(false);
    setSignatureOpen(false);
    setDossierOpen(false);
    // Begin from a clean overview frame — close any open bay so no previous
    // focus/dive transition leaks into the tour's first (overview) step.
    if (sceneRef.current && sceneRef.current.getMode && sceneRef.current.getMode() === "focus") {
      sceneRef.current.closeBay();
    }
    setTourSessionId(n => n + 1);
    setTourOpen(true);
  }, [markInteracted]);
  const handleDossier = useCallback(() => {
    markInteracted();
    setBriefingOpen(false);
    setSignatureOpen(false);
    setDossierOpen(true);
  }, [markInteracted]);
  const handleExplore = useCallback(() => {
    markInteracted();
    setBriefingOpen(false);
    // Soft starting-point cue so a freely-exploring visitor still has a
    // gravitational pull toward the BUILD floor.
    if (sceneRef.current && sceneRef.current.highlightZone) {
      sceneRef.current.highlightZone("build", 3500);
    }
  }, [markInteracted]);
  const handleProjectFromDossier = useCallback((id) => {
    const idx = window.YARD_DATA.containers.findIndex(c => c.id === id);
    if (idx >= 0 && sceneRef.current) {
      setDossierOpen(false);
      sceneRef.current.focusBay(idx);
    }
  }, []);

  return (
    <>
      {!booted && <BootSequence onDone={() => setBooted(true)} />}
      <div id="scene-root" ref={mountRef} />
      {booted && startupError === "webgl" && <WebGLFallback />}
      {booted && !startupError && (
        <>
          <HUD
            nearest={nearest}
            openIndex={openIndex}
            mode={mode}
            walkTo={(i) => sceneRef.current && sceneRef.current.focusBay(i)}
            onCloseFocus={() => sceneRef.current && sceneRef.current.closeBay()}
            viewedIds={viewedProjects}
          />
          <QuickActions
            mode={mode}
            onResume={handleResume}
            onTour={handleTourStart}
            onContact={handleDispatch}
          />
          {mode === "overview" && (
            <>
              <FilterBar filter={filter} setFilter={setFilter} scene={sceneRef.current} />
              <MiniMap
                scene={sceneRef.current}
                onFocusBay={(i) => sceneRef.current && sceneRef.current.focusBay(i)}
                nearest={nearest}
                openIndex={openIndex}
                filter={filter}
                viewedIds={viewedProjects}
              />
              {!signatureOpen && (
                <BotPrompt
                  scene={sceneRef.current}
                  onOpen={() => sceneRef.current && sceneRef.current.openContact()}
                />
              )}
            </>
          )}
          <FilterToast filter={filter} scene={sceneRef.current} />
          <Terminal
            expanded={termOpen}
            setExpanded={setTermOpen}
            onCommand={runCommand}
            lines={termLines}
            inputRef={termInputRef}
          />
          <DiveFlash keyTick={diveTick} />
          {openContainer && !isDebug && !isContact && (
            <ContainerPanel
              data={openContainer}
              onClose={() => sceneRef.current.closeBay()}
              onPrev={goPrev}
              onNext={goNext}
              prevLabel={prevC ? `${prevC.label} · ${prevC.title}` : null}
              nextLabel={nextC ? `${nextC.label} · ${nextC.title}` : null}
              position={openIndex + 1}
              total={total}
            />
          )}
          {briefingOpen && (
            <MissionBriefing
              identity={window.YARD_DATA.identity}
              onTour={handleTourStart}
              onExplore={handleExplore}
              onResume={handleResume}
              onDossier={handleDossier}
            />
          )}
          {/* Guided walkthrough. key={tourSessionId} forces a clean remount on
              every start (step 0, no stale closing/timer state). onClose is
              deliberately minimal — it closes ONLY the overlay; it never calls
              closeBay / resetView / clears openIndex, so the final step's open
              BAY-11 Dispatch panel and camera are left exactly in place. */}
          {tourOpen && (
            <TourMode
              key={tourSessionId}
              scene={sceneRef.current}
              onResume={handleResume}
              onDossier={handleDossier}
              onClose={() => { setTourOpen(false); setTourStepId(null); }}
              onStepChange={(id) => setTourStepId(id)}
            />
          )}
          <BuildRevealToast active={tourOpen && tourStepId === "build"} />
          <FinalMomentVignette active={tourOpen && tourStepId === "contact"} />
          {dossierOpen && (
            <OperatorDossier
              onClose={() => setDossierOpen(false)}
              onResume={handleResume}
              onProject={handleProjectFromDossier}
              onContact={handleDispatch}
            />
          )}
          {mode === "overview" && !briefingOpen && (
            <ZoneChip scene={sceneRef.current} nearest={nearest} />
          )}
          <QuickAccessDock
            mode={mode}
            onTour={handleTourStart}
            onResume={handleResume}
            onDossier={handleDossier}
            onContact={handleDispatch}
          />
          {mode === "overview" && !tourOpen && !briefingOpen && (
            <HoverTooltip
              scene={sceneRef.current}
              nearest={nearest}
              mode={mode}
              openIndex={openIndex}
            />
          )}
          {signatureOpen && mode === "overview" && (
            <SignatureCTA
              variant={ctaMode}
              onResume={handleResume}
              onDispatch={handleDispatch}
              onClose={() => setSignatureOpen(false)}
            />
          )}
          {routeBeatShown && mode === "overview" && (
            <RouteCompleteBeat
              onDispatch={() => { dismissRouteBeat(); handleDispatch(); }}
              onClose={dismissRouteBeat}
            />
          )}
          {isDebug && (
            <DebugScene
              solved={debugSolved}
              onSolved={() => setDebugSolved(true)}
              onClose={() => sceneRef.current.closeBay()}
            />
          )}
          {isContact && (
            <ContactPanel unlocked={true} debugBonus={debugSolved} onClose={() => sceneRef.current.closeBay()} />
          )}
          {hidden && <HiddenBay onClose={() => setHidden(false)} />}
          {flash && <div className="hidden-flash" />}
          <Cursor />
        </>
      )}
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
