/* Nutshell shell prototype — CORE
   - EngineProvider / useEngine : the one shared store + playback simulation
   - DiagramCanvas : pan / zoom / Focus, nodes light up per chapter
   - Transport, CaptionStrip, ZoomPanel, Avatar, Pill, IBtn : shared chrome atoms
   Exposes window.SHELL_CORE */
(function () {
  const { Icon } = window.NSIcons;
  const D = window.SHELL;
  const STAGE_W = 1160, STAGE_H = 560;

  const fmt = (s) => {
    s = Math.max(0, Math.round(s));
    return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
  };

  // ---- chapter boundaries -------------------------------------------------
  const starts = []; let acc = 0;
  D.chapters.forEach((c) => { starts.push(acc); acc += c.duration; });
  const ends = starts.map((s, i) => s + D.chapters[i].duration);
  const dividers = starts.slice(1).map((s) => s / D.totalSeconds);

  // sentence start times across the whole timeline
  const sentenceTimes = [];
  D.chapters.forEach((c, ci) => {
    const list = D.transcript.map((t, i) => ({ ...t, i })).filter((t) => t.ch === ci);
    list.forEach((t, k) => sentenceTimes.push({ globalI: t.i, start: starts[ci] + (k / list.length) * c.duration }));
  });

  // =========================================================================
  //  ENGINE
  // =========================================================================
  const EngineContext = React.createContext(null);
  const useEngine = () => React.useContext(EngineContext);

  function EngineProvider({ children, initialAdaptive = "rich", initialTheme = "dark" }) {
    const [concept, setConcept] = React.useState(1);
    const [adaptive, setAdaptive] = React.useState(initialAdaptive);   // rich | portable
    const [theme, setTheme] = React.useState(initialTheme);

    const [playing, setPlaying] = React.useState(false);
    const [pos, setPos] = React.useState(20);                  // sec — mid ch.2
    const [rate, setRate] = React.useState(0.9);
    const [captionsOn, setCaptionsOn] = React.useState(true);
    const [playbackMode, setPlaybackMode] = React.useState("visual");   // visual | audio

    const [zoom, setZoom] = React.useState(0.62);
    const [pan, setPan] = React.useState({ x: 0, y: 0 });
    const [dragging, setDragging] = React.useState(false);

    const [overlay, setOverlay] = React.useState(null);        // audience|nuggets|source|library|compose|progress|settings|palette|sourceFull
    const [chatOpen, setChatOpen] = React.useState(false);
    const [menuOpen, setMenuOpen] = React.useState(false);

    const [audience, setAudience] = React.useState(D.meta.audience);
    const [regenerating, setRegenerating] = React.useState(false);
    const [engineOnline, setEngineOnline] = React.useState(true);
    const [toast, setToast] = React.useState(null);

    // ---- YouTube Import plugin (premium add-on) — off by default ----
    const [ytOwned, setYtOwned] = React.useState(false);     // purchased
    const [ytEnabled, setYtEnabled] = React.useState(false); // toggle in Settings › Plugins
    const [ytConnected, setYtConnected] = React.useState(true); // YouTube OAuth token present
    const [ytProfile, setYtProfile] = React.useState("dc"); // active channel
    const [settingsTab, setSettingsTab] = React.useState("general"); // general | plugins

    const canvasRef = React.useRef(null);
    const total = D.totalSeconds;

    // theme → document attribute
    React.useEffect(() => {
      document.documentElement.setAttribute("data-appearance", theme);
    }, [theme]);

    // playback sim (smooth, real-time × rate)
    React.useEffect(() => {
      if (!playing) return;
      let raf, last = performance.now();
      const tick = (t) => {
        const dt = (t - last) / 1000; last = t;
        setPos((p) => {
          const np = p + dt * rate;
          if (np >= total) { setPlaying(false); return total; }
          return np;
        });
        raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => cancelAnimationFrame(raf);
    }, [playing, rate, total]);

    // derived
    const ci = (() => { const f = ends.findIndex((e) => pos < e); return f === -1 ? D.chapters.length - 1 : f; })();
    let curSentence = 0;
    for (let k = 0; k < sentenceTimes.length; k++) if (pos >= sentenceTimes[k].start - 0.001) curSentence = sentenceTimes[k].globalI;
    const litNodes = D.chapters[ci].nodes;

    const flash = (msg) => { setToast(msg); clearTimeout(flash._t); flash._t = setTimeout(() => setToast(null), 1700); };

    // ---- Focus: fit a set of nodes into the canvas viewport ----
    const focusNodes = React.useCallback((ids, animate = true) => {
      const el = canvasRef.current; if (!el) return;
      const r = el.getBoundingClientRect();
      const set = (ids && ids.length) ? ids : Object.keys(D.nodes);
      let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
      set.forEach((id) => { const n = D.nodes[id]; if (!n) return;
        minX = Math.min(minX, n.x); minY = Math.min(minY, n.y);
        maxX = Math.max(maxX, n.x + n.w); maxY = Math.max(maxY, n.y + n.h); });
      const bw = maxX - minX, bh = maxY - minY, cx = minX + bw / 2, cy = minY + bh / 2;
      const padX = 110, padY = 96;
      const sc = Math.max(0.3, Math.min(1.5, Math.min((r.width - padX * 2) / bw, (r.height - padY * 2) / bh)));
      if (!animate) setDragging(true);            // suppress transition for instant fit
      setZoom(sc);
      setPan({ x: r.width / 2 - cx * sc, y: r.height / 2 - cy * sc });
      if (!animate) requestAnimationFrame(() => setDragging(false));
    }, []);

    // auto-follow narration: refit lit nodes when the chapter changes
    const fitRef = React.useRef(true);
    React.useEffect(() => {
      const t = setTimeout(() => focusNodes(D.chapters[ci].nodes, true), fitRef.current ? 0 : 0);
      fitRef.current = false;
      return () => clearTimeout(t);
      // eslint-disable-next-line
    }, [ci]);

    // ---- seek helpers — NEVER touch `playing` ----
    const seekSec = (s) => setPos(Math.max(0, Math.min(total, s)));
    const seekFrac = (f) => seekSec(f * total);
    const gotoChapter = (i) => seekSec(starts[Math.max(0, Math.min(D.chapters.length - 1, i))]);
    const prevChapter = () => gotoChapter(ci - 1);
    const nextChapter = () => gotoChapter(ci + 1);
    const rewind = () => seekSec(pos - 10);
    const forward = () => seekSec(pos + 10);
    const nodeSeek = (id) => { const i = D.chapters.findIndex((c) => c.nodes.includes(id)); if (i >= 0) gotoChapter(i); };
    const sentenceSeek = (gi) => { const st = sentenceTimes.find((s) => s.globalI === gi); if (st) seekSec(st.start); };

    const stepRate = (dir) => setRate((r) => {
      const nr = dir > 0 ? r + 0.25 : r - 0.15;
      return Math.max(0.5, Math.min(2.5, +nr.toFixed(2)));
    });
    const zoomBy = (d) => { const el = canvasRef.current; if (!el) { setZoom(z => Math.max(0.3, Math.min(1.5, +(z + d).toFixed(2)))); return; }
      const r = el.getBoundingClientRect(), cx = r.width / 2, cy = r.height / 2;
      setZoom((z) => { const nz = Math.max(0.3, Math.min(1.5, +(z + d).toFixed(2)));
        setPan((p) => ({ x: cx - (cx - p.x) * (nz / z), y: cy - (cy - p.y) * (nz / z) })); return nz; }); };

    const openOverlay = (o) => { setMenuOpen(false); setOverlay(o); };
    const closeOverlay = () => setOverlay(null);

    const regenerate = (aud) => {
      setRegenerating(true);
      setTimeout(() => { setAudience(aud); setRegenerating(false); setOverlay(null); flash("Narration rewritten for " + aud); }, 1500);
    };

    const value = {
      D, STAGE_W, STAGE_H, fmt, starts, ends, dividers, total,
      concept, setConcept, adaptive, setAdaptive, theme, setTheme,
      toggleTheme: () => setTheme((t) => (t === "dark" ? "light" : "dark")),
      playing, setPlaying, togglePlay: () => setPlaying((p) => !p),
      pos, rate, captionsOn, setCaptionsOn, toggleCaptions: () => setCaptionsOn((c) => !c),
      playbackMode, setPlaybackMode,
      zoom, pan, setPan, dragging, setDragging, zoomBy, setZoom,
      ci, curSentence, litNodes,
      seekSec, seekFrac, gotoChapter, prevChapter, nextChapter, rewind, forward, nodeSeek, sentenceSeek,
      stepRate, focusNodes, canvasRef,
      overlay, openOverlay, closeOverlay, setOverlay,
      chatOpen, setChatOpen, toggleChat: () => { setChatOpen((c) => !c); },
      menuOpen, setMenuOpen,
      audience, setAudience, regenerating, regenerate,
      engineOnline, setEngineOnline, toast, flash,
      ytOwned, setYtOwned, ytEnabled, setYtEnabled, ytConnected, setYtConnected, ytProfile, setYtProfile,
      settingsTab, setSettingsTab,
      unlockYouTube: () => { setYtOwned(true); setYtEnabled(true); },
      portable: adaptive === "portable",
    };
    return React.createElement(EngineContext.Provider, { value }, children);
  }

  // =========================================================================
  //  ATOMS
  // =========================================================================
  function IBtn({ icon, size = 16, label, active, onClick, style }) {
    return (
      <button className={"ib" + (active ? " ib--on" : "")} title={label} aria-label={label}
        onClick={onClick} style={style}>
        <Icon name={icon} size={size} />
      </button>
    );
  }
  function Pill({ icon, children, variant = "sec", onClick, style, title }) {
    return (
      <button className={"pill pill--" + variant} onClick={onClick} style={style} title={title}>
        {icon && <Icon name={icon} size={13} />}{children && <span>{children}</span>}
      </button>
    );
  }
  function Avatar({ speaking, size = 36 }) {
    return (
      <div className={"avatar" + (speaking ? " avatar--on" : "")} style={{ width: size, height: size }}>
        <Icon name="wave" size={size * 0.46} />
      </div>
    );
  }

  // =========================================================================
  //  DIAGRAM CANVAS
  // =========================================================================
  function DiagramCanvas({ dim }) {
    const e = useEngine();
    const { D, zoom, pan, setPan, dragging, setDragging, litNodes, nodeSeek, canvasRef } = e;
    const lit = new Set(litNodes);
    const drag = React.useRef(null);

    // refit when this canvas mounts (e.g. switching concepts → new container size)
    React.useEffect(() => { const t = setTimeout(() => e.focusNodes(e.litNodes, false), 0); return () => clearTimeout(t); }, []); // eslint-disable-line

    const onDown = (ev) => {
      if (ev.target.closest(".dg-node")) return;
      setDragging(true);
      drag.current = { x: ev.clientX, y: ev.clientY, ox: pan.x, oy: pan.y };
      const move = (m) => setPan({ x: drag.current.ox + (m.clientX - drag.current.x), y: drag.current.oy + (m.clientY - drag.current.y) });
      const up = () => { drag.current = null; setDragging(false); window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up);
    };

    const anchor = (a, b) => {
      const ac = { x: a.x + a.w / 2, y: a.y + a.h / 2 }, bc = { x: b.x + b.w / 2, y: b.y + b.h / 2 };
      const dx = bc.x - ac.x, dy = bc.y - ac.y;
      const p = (rect, tx, ty, cx, cy) => { const hw = rect.w / 2, hh = rect.h / 2;
        const sx = tx === 0 ? 1e9 : hw / Math.abs(tx), sy = ty === 0 ? 1e9 : hh / Math.abs(ty), s = Math.min(sx, sy);
        return { x: cx + tx * s, y: cy + ty * s }; };
      return [p(a, dx, dy, ac.x, ac.y), p(b, -dx, -dy, bc.x, bc.y)];
    };

    return (
      <div className={"dg" + (dim ? " dg--dim" : "")} ref={canvasRef} onPointerDown={onDown}>
        <div className="dg-grid" />
        <div className="dg-stage" style={{
          width: STAGE_W, height: STAGE_H,
          transform: `translate(${pan.x}px,${pan.y}px) scale(${zoom})`,
          transition: dragging ? "none" : "transform 560ms cubic-bezier(.22,.61,.36,1)",
        }}>
          <svg width={STAGE_W} height={STAGE_H} viewBox={`0 0 ${STAGE_W} ${STAGE_H}`} style={{ position: "absolute", inset: 0, overflow: "visible", pointerEvents: "none" }}>
            <defs>
              <marker id="dgA" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M1 1l6 3.5L1 8z" fill="var(--text-quaternary)" /></marker>
              <marker id="dgL" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M1 1l6 3.5L1 8z" fill="var(--brand-strong)" /></marker>
            </defs>
            {D.edges.map((ed, i) => {
              const [s, t] = anchor(D.nodes[ed.from], D.nodes[ed.to]);
              const on = lit.has(ed.from) && lit.has(ed.to);
              const mx = (s.x + t.x) / 2;
              return <path key={i} d={`M${s.x} ${s.y} C${mx} ${s.y} ${mx} ${t.y} ${t.x} ${t.y}`} fill="none"
                stroke={on ? "var(--brand-strong)" : "var(--text-quaternary)"} strokeWidth={on ? 2.5 : 1.6}
                strokeDasharray={ed.dashed ? "5 6" : "none"} markerEnd={`url(#${on ? "dgL" : "dgA"})`}
                style={{ filter: on ? "drop-shadow(0 0 5px var(--glow-soft))" : "none", transition: "stroke 480ms ease, stroke-width 480ms ease" }} />;
            })}
          </svg>
          {Object.entries(D.nodes).map(([id, n]) => {
            const on = lit.has(id);
            return (
              <button key={id} className={"dg-node" + (on ? " dg-node--lit" : "")} title={`Jump to “${n.label}”`}
                style={{ left: n.x, top: n.y, width: n.w, height: n.h }} onClick={() => nodeSeek(id)}>
                {n.label}
              </button>
            );
          })}
        </div>
      </div>
    );
  }

  // =========================================================================
  //  ZOOM PANEL  ( − · slider · 🎯 focus · + )
  // =========================================================================
  function ZoomPanel({ compact, collapsible }) {
    const e = useEngine();
    const [open, setOpen] = React.useState(false);     // hover preview
    const [pinned, setPinned] = React.useState(false); // fixate expanded
    const expanded = open || pinned;
    if (collapsible && !expanded) {
      return (
        <button className="zoom glass zoom-mini" onMouseEnter={() => setOpen(true)} onClick={() => setPinned(true)} title="Zoom & focus — click to pin open" aria-label="Zoom controls">
          <Icon name="zoomIn" size={15} /><span className="zoom-pct">{Math.round(e.zoom * 100)}%</span>
        </button>
      );
    }
    return (
      <div className={"zoom glass" + (compact ? " zoom--c" : "")} onMouseLeave={collapsible ? () => setOpen(false) : undefined}>
        {collapsible
          ? <button className={"ib" + (pinned ? " ib--on" : "")} onClick={() => setPinned((p) => !p)} title={pinned ? "Unpin (collapses when you move away)" : "Pin open"} aria-label="Pin zoom controls" aria-pressed={pinned}><Icon name="chevronRight" size={15} /></button>
          : (!compact && <span className="zoom-lbl">Zoom</span>)}
        <button className="ib" onClick={() => e.zoomBy(-0.12)} aria-label="Zoom out"><Icon name="zoomOut" size={15} /></button>
        <input className="zoom-slider" type="range" min="0.3" max="1.5" step="0.01" value={e.zoom}
          onChange={(ev) => { const z = parseFloat(ev.target.value); e.zoomBy(z - e.zoom); }} aria-label="Zoom level" />
        <button className="ib ib--focus" onClick={() => e.focusNodes(e.litNodes, true)} aria-label="Focus current section" title="Focus current section">
          <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round">
            <circle cx="12" cy="12" r="3.4" /><path d="M12 3v3M12 18v3M3 12h3M18 12h3" /></svg>
        </button>
        <button className="ib" onClick={() => e.zoomBy(0.12)} aria-label="Zoom in"><Icon name="zoomIn" size={15} /></button>
      </div>
    );
  }

  // =========================================================================
  //  CAPTION STRIP  (read-along; toggled by Captions)
  // =========================================================================
  function CaptionStrip({ floating, translucent }) {
    const e = useEngine();
    if (!e.captionsOn) return null;
    const s = e.D.transcript[e.curSentence];
    return (
      <div className={"caption" + (floating ? " caption--float" : "") + (translucent ? " caption--glass glassT" : "")} aria-live="polite">
        <span className="caption-tag">{e.curSentence !== undefined ? `${e.ci + 1}/${e.D.chapters.length}` : ""} · {e.D.chapters[e.ci].title}</span>
        <span className="caption-text">{s ? s.text : ""}</span>
      </div>
    );
  }

  // =========================================================================
  //  TRANSPORT  (scrubber + controls + speed + captions toggle)
  // =========================================================================
  function Transport({ inline, caption, collapsible }) {
    const e = useEngine();
    const [open, setOpen] = React.useState(false);     // hover preview
    const [pinned, setPinned] = React.useState(false); // fixate expanded
    const [chapPop, setChapPop] = React.useState(false); // popup showing the full chapter title
    const trackRef = React.useRef(null);
    const curS = e.D.transcript[e.curSentence];
    const seekAt = (clientX) => { const r = trackRef.current.getBoundingClientRect(); e.seekFrac(Math.max(0, Math.min(1, (clientX - r.left) / r.width))); };
    const onTrack = (ev) => { seekAt(ev.clientX);
      const move = (m) => seekAt(m.clientX);
      const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
      window.addEventListener("pointermove", move); window.addEventListener("pointerup", up); };
    const f = e.pos / e.total;

    if (collapsible && !(open || pinned)) {
      return (
        <div className="transport-mini glassT" onMouseEnter={() => setOpen(true)}>
          <button className="tp-play tp-play--mini" onClick={e.togglePlay} aria-label={e.playing ? "Pause" : "Play"}>
            <Icon name={e.playing ? "pause" : "play"} size={16} />
          </button>
          <span className="tp-mini-chap">{e.ci + 1}/{e.D.chapters.length}</span>
          <span className="tp-time">{e.fmt(e.pos)}</span>
          <button className="ib" onClick={() => setPinned(true)} title="Expand transport" aria-label="Expand transport"><Icon name="chevronUp" size={15} /></button>
        </div>
      );
    }

    return (
      <div className={"transport" + (inline ? " transport--inline" : " glassT")}
        onMouseLeave={collapsible ? () => setOpen(false) : undefined}>
        {caption && e.captionsOn && e.playbackMode !== "audio" && (
          <div className="tp-caption" aria-live="polite">{curS ? curS.text : ""}</div>
        )}
        <div className="tp-scrubrow">
          <span className="tp-time">{e.fmt(e.pos)}</span>
          <div className="scrub" ref={trackRef} onPointerDown={onTrack}>
            <div className="scrub-buf" style={{ width: "84%" }} />
            <div className="scrub-fill" style={{ width: f * 100 + "%" }} />
            {e.dividers.map((d, i) => <i key={i} className="scrub-div" style={{ left: d * 100 + "%" }} />)}
            <div className="scrub-knob" style={{ left: f * 100 + "%" }} />
          </div>
          <span className="tp-time tp-time--total">{e.fmt(e.total)}</span>
        </div>
        <div className="tp-controls">
          {collapsible && (
            <button className={"ib" + (pinned ? " ib--on" : "")} onClick={() => setPinned((p) => !p)} title={pinned ? "Unpin (collapses when you move away)" : "Pin open"} aria-label="Pin transport" aria-pressed={pinned}><Icon name="chevronDown" size={15} /></button>
          )}
          <span className="tp-chap-wrap">
            <button
              className="tp-chapter"
              onClick={() => setChapPop((v) => !v)}
              title={(e.ci + 1) + "/" + e.D.chapters.length + " — " + e.D.chapters[e.ci].title}
              aria-expanded={chapPop}
            >{e.ci + 1}/{e.D.chapters.length} — {e.D.chapters[e.ci].title}</button>
            {chapPop && (
              <div className="tp-chap-pop" role="tooltip">{e.ci + 1}/{e.D.chapters.length} — {e.D.chapters[e.ci].title}</div>
            )}
          </span>
          <div className="tp-center">
            <button className="ib tp-skip" title="Start from beginning" aria-label="Start from beginning" onClick={() => e.seekSec(0)}>
              <svg viewBox="0 0 24 24" width="18" height="18"><path d="M14.6 5.5 A7 7 0 1 1 8.7 5.6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /><path d="M5.2 5.8 L9.9 3.2 L9.6 8.7 Z" fill="currentColor" /></svg>
            </button>
            <IBtn icon="prev" size={17} label="Previous chapter" onClick={e.prevChapter} />
            <button className="tp-play" onClick={e.togglePlay} aria-label={e.playing ? "Pause" : "Play"}>
              <Icon name={e.playing ? "pause" : "play"} size={22} />
            </button>
            <IBtn icon="next" size={17} label="Next chapter" onClick={e.nextChapter} />
          </div>
          <div className="tp-right">
            <div className="speed">
              <button className="ib" onClick={() => e.stepRate(-1)} aria-label="Slower"><Icon name="zoomOut" size={14} /></button>
              <span className="speed-val">{e.rate.toFixed(2).replace(/0$/, "")}×</span>
              <button className="ib" onClick={() => e.stepRate(1)} aria-label="Faster"><Icon name="plus" size={13} /></button>
            </div>
            <button className={"cc" + (e.captionsOn ? " cc--on" : "")} onClick={e.toggleCaptions} aria-pressed={e.captionsOn} title="Toggle captions">
              <svg viewBox="0 0 24 24" width="15" height="15" fill={e.captionsOn ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.7"><rect x="3" y="5" width="18" height="14" rx="3" /><path d="M7 11h3M7 14h6M13 11h4" stroke={e.captionsOn ? "var(--accent)" : "currentColor"} strokeLinecap="round" /></svg>
              <span>CC</span>
            </button>
          </div>
        </div>
      </div>
    );
  }

  window.SHELL_CORE = { EngineProvider, useEngine, EngineContext, DiagramCanvas, ZoomPanel, CaptionStrip, Transport, IBtn, Pill, Avatar, fmt };
})();
