// FORGE — main app
// Wires the inventory, graph, recipe card, ticker, tooltip, and resolve state.

const { useState, useEffect, useMemo, useCallback, useRef } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "showTicker": true,
  "showSparks": true,
  "sound": true,
  "edgeIntensity": "balanced",
  "demoMode": false,
  "preset": "blank"
}/*EDITMODE-END*/;

function App() {
  const nodes = window.FORGE_NODES;
  const edges = window.FORGE_EDGES;

  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);

  const [selected, setSelected] = useState([]);
  const [hover, setHover] = useState(null); // { node, edge, status, reasons, pos }

  // Recompute resolved status whenever selection changes.
  const resolved = useMemo(() => window.forgeResolve(selected), [selected]);
  const recipe   = useMemo(() => window.forgeRecipe(selected), [selected]);

  const toggleSelect = useCallback((canon) => {
    setSelected(prev => {
      // Don't allow selecting blocked nodes — give feedback by briefly shaking
      const r = window.forgeResolve(prev);
      const cur = r[canon];
      if (cur && cur.status === "blocked" && !prev.includes(canon)) {
        flashBlocked(canon);
        window.forgeSound && window.forgeSound.sizzle();
        return prev;
      }
      if (prev.includes(canon)) {
        window.forgeSound && window.forgeSound.thud();
        return prev.filter(x => x !== canon);
      }
      window.forgeSound && window.forgeSound.clang();
      return [...prev, canon];
    });
  }, []);

  const addOnly = useCallback((canon) => {
    setSelected(prev => {
      if (prev.includes(canon)) return prev;
      window.forgeSound && window.forgeSound.chime();
      return [...prev, canon];
    });
  }, []);

  const removeOnly = useCallback((canon) => {
    setSelected(prev => {
      if (!prev.includes(canon)) return prev;
      window.forgeSound && window.forgeSound.thud();
      return prev.filter(x => x !== canon);
    });
  }, []);

  // ── Hover handlers ──────────────────────────────────────────────────────
  const onHoverNode = useCallback((node, ev) => {
    if (!node) { setHover(null); return; }
    const r = resolved[node.canonical] || { status: "available", reasons: [] };
    setHover({
      node,
      status: selected.includes(node.canonical) ? "selected" : r.status,
      reasons: r.reasons || [],
      pos: { x: ev?.clientX ?? 100, y: ev?.clientY ?? 100 },
    });
  }, [resolved, selected]);

  const onHoverEdge = useCallback((edge, ev) => {
    if (!edge) { setHover(null); return; }
    setHover({ edge, pos: { x: ev?.clientX ?? 100, y: ev?.clientY ?? 100 } });
  }, []);

  // Track cursor for tooltip following
  useEffect(() => {
    const move = (ev) => {
      setHover(h => h ? { ...h, pos: { x: ev.clientX, y: ev.clientY } } : h);
    };
    window.addEventListener("mousemove", move);
    return () => window.removeEventListener("mousemove", move);
  }, []);

  // ── Demo path: scripted Muon → QLoRA → bnb_4bit → DLRS ───────────────────
  const demoTimer = useRef(null);
  useEffect(() => {
    if (!t.demoMode) {
      if (demoTimer.current) { clearTimeout(demoTimer.current); demoTimer.current = null; }
      return;
    }
    const steps = [
      { at: 800,   action: () => setSelected(["muon"]) },
      { at: 3200,  action: () => setSelected(["muon", "qlora"]) },
      { at: 5800,  action: () => setSelected(["muon", "qlora", "bnb_4bit"]) },
      { at: 8400,  action: () => setSelected(["muon", "qlora", "bnb_4bit", "lora", "llama3", "dlrs"]) },
      { at: 14000, action: () => setSelected([]) },
    ];
    setSelected([]);
    let cancelled = false;
    let i = 0;
    const tick = () => {
      if (cancelled || i >= steps.length) return;
      steps[i].action();
      i++;
      if (i < steps.length) {
        demoTimer.current = setTimeout(tick, steps[i].at - (steps[i-1]?.at || 0));
      } else {
        // loop
        demoTimer.current = setTimeout(() => { i = 0; tick(); }, 2000);
      }
    };
    demoTimer.current = setTimeout(tick, steps[0].at);
    return () => { cancelled = true; if (demoTimer.current) clearTimeout(demoTimer.current); };
  }, [t.demoMode]);

  // Sync sound mute state with the tweak
  useEffect(() => {
    if (window.forgeSound) window.forgeSound.setMuted(!t.sound);
  }, [t.sound]);

  // Apply presets from Tweaks
  useEffect(() => {
    if (t.preset === "blank") return;
    const presets = {
      "qlora-llama":   ["qlora", "lora", "bnb_4bit", "llama3", "dlrs"],
      "muon-collide":  ["muon", "adamw_8bit"],
      "rick-tier1":    ["per_layer_lr_rotation", "adamw_8bit"],
      "vllm-ckpt":     ["grad_ckpt", "vllm", "qlora"],
      "chaos":         ["chaos_inject", "jacobian_reg"],
    };
    if (presets[t.preset]) setSelected(presets[t.preset]);
    // Reset the tweak so user can re-trigger
    setTimeout(() => setTweak("preset", "blank"), 100);
  }, [t.preset]);

  return (
    <div className="app">
      {t.showSparks && <Sparks />}

      <Topbar />

      <div className="main">
        <window.Inventory
          nodes={nodes}
          resolved={resolved}
          selected={selected}
          onToggleSelect={toggleSelect}
          onHoverNode={onHoverNode}
        />

        <window.ForgeGraph
          nodes={nodes}
          edges={edges}
          resolved={resolved}
          selected={selected}
          onToggleSelect={toggleSelect}
          onHoverNode={onHoverNode}
          onHoverEdge={onHoverEdge}
        />

        <window.RecipeCard
          selected={selected}
          nodes={nodes}
          recipe={recipe}
          onRemove={removeOnly}
          onSelect={addOnly}
          onHoverNode={onHoverNode}
        />
      </div>

      {t.showTicker && <window.DiscoveriesTicker />}

      <window.EvidenceTip payload={hover} />

      <window.TweaksPanel title="Tweaks">
        <window.TweakSection label="Demo">
          <window.TweakToggle label="Auto-demo path" value={t.demoMode}
                              onChange={v => setTweak("demoMode", v)} />
          <window.TweakSelect
            label="Preset build"
            value={t.preset}
            options={[
              { value: "blank", label: "— pick —" },
              { value: "qlora-llama",  label: "QLoRA · Llama-3 · DLRS" },
              { value: "muon-collide", label: "Muon × AdamW 8-bit (breaks)" },
              { value: "rick-tier1",   label: "Per-Layer LR × AdamW 8-bit (T1)" },
              { value: "vllm-ckpt",    label: "vLLM × Grad Ckpt (breaks)" },
              { value: "chaos",        label: "Chaos × Jacobian (breaks)" },
            ]}
            onChange={v => setTweak("preset", v)}
          />
        </window.TweakSection>
        <window.TweakSection label="Atmosphere">
          <window.TweakToggle label="Show ticker" value={t.showTicker} onChange={v => setTweak("showTicker", v)} />
          <window.TweakToggle label="Background sparks" value={t.showSparks} onChange={v => setTweak("showSparks", v)} />
          <window.TweakToggle label="Forge sounds" value={t.sound} onChange={v => setTweak("sound", v)} />
        </window.TweakSection>
      </window.TweaksPanel>
    </div>
  );
}

function Topbar() {
  return (
    <div className="topbar">
      <div className="brand">
        <div className="mark">
          {/* Anvil + sparks emblem */}
          <svg width="26" height="26" viewBox="0 0 32 32" fill="none">
            <defs>
              <linearGradient id="anvil-grad" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0" stopColor="#c4934e"/>
                <stop offset="1" stopColor="#5a3818"/>
              </linearGradient>
            </defs>
            {/* anvil silhouette */}
            <path d="M5 14 h17 l3 -3 h4 v4 h-3 l-2 2 h-3 v4 h2 v3 h-13 v-3 h2 v-4 h-7 z"
                  fill="url(#anvil-grad)" stroke="#2a1a08" strokeWidth="0.7" strokeLinejoin="round"/>
            {/* base */}
            <rect x="10" y="25" width="13" height="2" fill="#2a1a08"/>
            {/* hot dot on anvil face */}
            <circle cx="17" cy="15" r="1.3" fill="#ffba5e" opacity="0.95">
              <animate attributeName="opacity" values="0.6;1;0.6" dur="2.2s" repeatCount="indefinite"/>
            </circle>
            {/* sparks */}
            <circle cx="9" cy="8" r="0.9" fill="#ffba5e"/>
            <circle cx="13" cy="5" r="0.6" fill="#ff8a3d"/>
            <circle cx="20" cy="6" r="0.7" fill="#ffba5e"/>
          </svg>
        </div>
        <div>
          <div className="name">FORGE</div>
          <div className="tag">a compatibility graph for ML training</div>
        </div>
      </div>

      <div className="legend">
        <span className="swatch"><span className="sw t1" /> T1 · Legendary</span>
        <span className="swatch"><span className="sw t2" /> T2 · Rare</span>
        <span className="swatch"><span className="sw t3" /> T3 · Common</span>
      </div>

      <div className="crumbs">
        <span className="api-pill"><span className="dot" /> api · online</span>
        <span style={{ color: "var(--bone-4)" }}>
          {window.FORGE_NODES.length} nodes · {window.FORGE_EDGES.length} edges
        </span>
        <span style={{ color: "var(--bone-4)" }}>hf.co/spaces/forge</span>
      </div>
    </div>
  );
}

// Sparks background — random spark spans positioned via JS
function Sparks() {
  const sparks = useMemo(() => {
    return Array.from({ length: 18 }, () => ({
      left: Math.random() * 100,
      delay: Math.random() * 16,
      dur: 14 + Math.random() * 18,
      size: 0.6 + Math.random() * 1.6,
    }));
  }, []);
  return (
    <div className="sparks">
      {sparks.map((s, i) => (
        <span key={i} style={{
          left: s.left + "%",
          animationDuration: s.dur + "s",
          animationDelay: -s.delay + "s",
          transform: `scale(${s.size})`,
        }} />
      ))}
    </div>
  );
}

// briefly flash a node red on a blocked-click attempt
function flashBlocked(canon) {
  const svg = document.querySelector(`g.node[data-canon="${cssEscape(canon)}"]`);
  if (!svg) return;
  svg.animate(
    [
      { transform: svg.getAttribute("transform") + " translateX(0)" },
      { transform: svg.getAttribute("transform") + " translateX(-3px)" },
      { transform: svg.getAttribute("transform") + " translateX(3px)" },
      { transform: svg.getAttribute("transform") + " translateX(-2px)" },
      { transform: svg.getAttribute("transform") + " translateX(0)" },
    ],
    { duration: 220, easing: "ease-out" }
  );
}
function cssEscape(s) {
  if (window.CSS && window.CSS.escape) return window.CSS.escape(s);
  return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
}

// Fetch live data (or fall back to bundled mock) before first render.
(window.forgeBootstrap ? window.forgeBootstrap() : Promise.resolve())
  .catch(() => {})
  .finally(() => {
    if (window.FORGE_LIVE) console.info("[forge] live API graph loaded");
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
  });
