// diagrams.jsx — generated visuals: relationship graph (ego + corpus), structure
// map, lifecycle stepper, lineage timeline, charts, and lazy mermaid rendering.
// SVG on expanded+; vertical timeline/list fallback on compact (no SVG at 360px).

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

// ── responsive + motion hooks ──────────────────────────────────────────────────
function useWindowClass() {
  const get = () => {
    const w = window.innerWidth;
    return w < 600 ? "compact" : w < 840 ? "medium" : w < 1200 ? "expanded" : w < 1600 ? "large" : "xl";
  };
  const [wc, setWc] = useState(get);
  useEffect(() => {
    const on = () => setWc(get());
    window.addEventListener("resize", on);
    return () => window.removeEventListener("resize", on);
  }, []);
  return wc;
}
function prefersReducedMotion() {
  return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
function isSvgClass(wc) { return wc !== "compact"; }

function shortText(s, n = 22) { s = String(s || ""); return s.length > n ? s.slice(0, n - 1) + "…" : s; }
function nodeKind(collection) { return collection === "runbooks" ? "runbook" : collection === "specs" ? "spec" : "external"; }

// ── Generic SVG graph canvas ──────────────────────────────────────────────────
function GraphCanvas({ nodes, edges, viewW, viewH, visibleStep, selected, onSelect, reduced }) {
  const pos = useMemo(() => Object.fromEntries(nodes.map((n) => [n.id, n])), [nodes]);
  const vis = (n) => (n.step == null ? true : n.step <= visibleStep);
  return (
    <svg viewBox={`0 0 ${viewW} ${viewH}`} role="img" preserveAspectRatio="xMidYMid meet" style={{ maxHeight: viewH + "px" }}>
      <g>
        {edges.map((e, i) => {
          const a = pos[e.source], b = pos[e.target];
          if (!a || !b || !vis(a) || !vis(b)) return null;
          const dim = selected && selected !== e.source && selected !== e.target;
          const len = Math.hypot(b.x - a.x, b.y - a.y) || 1;
          return (
            <line key={i} className={cx("gedge", "gedge--" + (e.type || "related"))}
              x1={a.x} y1={a.y} x2={b.x} y2={b.y}
              strokeOpacity={dim ? 0.12 : (e.weakOpacity != null ? e.weakOpacity : 0.85)}
              style={reduced ? undefined : { strokeDasharray: e.type === "related" ? undefined : len, strokeDashoffset: 0, transition: "stroke-opacity var(--uk-duration-short) var(--uk-easing-standard)" }} />
          );
        })}
        {nodes.filter(vis).map((n) => {
          const w = n.w || 144, h = n.h || 44;
          const sel = selected === n.id;
          return (
            <g key={n.id} className={cx("gnode", "gnode--" + n.kind, n.center && "gnode--center")}
               transform={`translate(${n.x - w / 2}, ${n.y - h / 2})`}
               role="button" tabIndex={0} aria-pressed={sel} aria-label={n.aria || n.label}
               onClick={() => onSelect && onSelect(n.id)}
               onKeyDown={(ev) => { if ((ev.key === "Enter" || ev.key === " ") && onSelect) { ev.preventDefault(); onSelect(n.id); } }}>
              {n.r
                ? <circle className="gnode__shape" cx={w / 2} cy={h / 2} r={n.r} />
                : <rect className="gnode__shape" width={w} height={h} rx={n.kind === "runbook" ? 14 : 3} />}
              <text className="gnode__label" x={w / 2} y={h / 2 - (n.sub ? 4 : -4)} textAnchor="middle">{shortText(n.label, n.r ? 10 : 18)}</text>
              {n.sub && <text className="gnode__sub" x={w / 2} y={h / 2 + 11} textAnchor="middle">{shortText(n.sub, n.r ? 12 : 20)}</text>}
            </g>
          );
        })}
      </g>
    </svg>
  );
}

function StepControl({ step, max, onStep, playing, onTogglePlay }) {
  if (max <= 1) return null;
  return (
    <div className="stepctl">
      <button className="iconbtn" onClick={onTogglePlay} aria-label={playing ? "Pause reveal" : "Play reveal"}>{playing ? <I.Pause /> : <I.Play />}</button>
      <input type="range" min="1" max={max} value={step} aria-label="Reveal step"
             onChange={(e) => onStep(Number(e.target.value))} />
      <span className="stepctl__lbl">step {step} / {max}</span>
    </div>
  );
}

// ── Ego relationship graph (per document) ───────────────────────────────────────
function buildEgo(doc) {
  const rel = doc.relationships || { derivedFrom: [], relatedSpecs: [], referencedBy: [] };
  const center = { id: doc.id, kind: nodeKind(doc.collection), label: doc.title, sub: "this document", center: true, step: 0, w: 168, h: 50, aria: "Current document: " + doc.title };
  const mk = (r, sub, type, step) => ({ id: r.id, kind: nodeKind(r.collection), label: r.title || r.id, sub, type, step, ref: r, aria: sub + ": " + (r.title || r.id) });
  const parents = (rel.derivedFrom || []).slice(0, 6).map((r) => mk(r, "derived from", "lineage", 1));
  const bottom = []
    .concat((rel.relatedSpecs || []).slice(0, 5).map((r) => mk(r, "related", "related", 2)))
    .concat((rel.referencedBy || []).slice(0, 6).map((r) => mk(r, "referenced by", r.type || "related", 3)));
  const W = 720;
  const spread = (list, y, step) => {
    const n = list.length; if (!n) return [];
    const gap = W / (n + 1);
    return list.map((nd, i) => ({ ...nd, x: gap * (i + 1), y, step: nd.step || step }));
  };
  const top = spread(parents, 52, 1);
  const mid = [{ ...center, x: W / 2, y: 168 }];
  const bot = spread(bottom, 300, 2);
  const nodes = [...top, ...mid, ...bot];
  const edges = [
    ...top.map((p) => ({ source: doc.id, target: p.id, type: "lineage" })),
    ...bot.map((b) => ({ source: doc.id, target: b.id, type: b.type === "lineage" ? "lineage" : "related" }))
  ];
  const maxStep = nodes.some((n) => n.step >= 2) ? (top.length ? 3 : 2) : 1;
  return { nodes, edges, viewW: W, viewH: 350, maxStep };
}

function RelTimeline({ doc, onOpenRef }) {
  const rel = doc.relationships || {};
  const groups = [
    ["Derived from", rel.derivedFrom],
    ["Related", rel.relatedSpecs],
    ["Referenced by", rel.referencedBy]
  ].filter(([, list]) => list && list.length);
  if (!groups.length) return <EmptyState title="No linked documents" msg="This document declares no related specs or lineage." />;
  return (
    <div className="timeline">
      {groups.map(([label, list]) => list.map((r, i) => (
        // style per-item from the relationship's own type, not the group
        <div key={label + i} className={cx("tnode", r.type === "related" && "tnode--related")}>
          <div className="tnode__rel">{label}</div>
          {onOpenRef && (r.title || r.collection)
            ? <a href="#" onClick={(e) => { e.preventDefault(); onOpenRef(r); }}><div className="tnode__title">{r.title || r.id}</div><div className="mono muted" style={{ fontSize: "11px" }}>{r.id}</div></a>
            : <div><div className="tnode__title">{r.title || r.id}</div></div>}
        </div>
      )))}
    </div>
  );
}

function RelationshipGraph({ doc, onOpenRef }) {
  const wc = useWindowClass();
  const reduced = prefersReducedMotion();
  const { nodes, edges, viewW, viewH, maxStep } = useMemo(() => buildEgo(doc), [doc]);
  const [step, setStep] = useState(reduced ? maxStep : 1);
  const [playing, setPlaying] = useState(!reduced && maxStep > 1);
  const [selId, setSelId] = useState(null);
  useEffect(() => { setStep(reduced ? maxStep : 1); setPlaying(!reduced && maxStep > 1); setSelId(null); }, [doc, maxStep, reduced]);
  useEffect(() => {
    if (!playing) return;
    if (step >= maxStep) { setPlaying(false); return; }
    const t = setTimeout(() => setStep((s) => Math.min(maxStep, s + 1)), 900);
    return () => clearTimeout(t);
  }, [playing, step, maxStep]);

  const total = nodes.length - 1;
  if (total === 0) return <div className="diagram"><div className="diagram__head"><h4 className="diagram__title">Relationships</h4></div><RelTimeline doc={doc} onOpenRef={onOpenRef} /></div>;

  const selNode = nodes.find((n) => n.id === selId && !n.center);
  return (
    <div className="diagram">
      <div className="diagram__head">
        <h4 className="diagram__title">Relationships</h4>
        <span className="diagram__hint">{total} linked {total === 1 ? "document" : "documents"}</span>
      </div>
      {isSvgClass(wc) ? (
        <React.Fragment>
          <div className="diagram__body">
            <GraphCanvas nodes={nodes} edges={edges} viewW={viewW} viewH={viewH} visibleStep={step} selected={selId} onSelect={(id) => setSelId(id === selId ? null : id)} reduced={reduced} />
          </div>
          <div className="glegend">
            <span><i /> spec</span><span><i className="rb" /> runbook</span><span><i className="ext" /> external</span>
            <span style={{ marginLeft: "auto" }}>solid = lineage · dashed = related</span>
          </div>
          {selNode && (
            <div className="gpanel">
              <h5 className="gpanel__title">{selNode.label}</h5>
              <div className="mono muted" style={{ fontSize: "11px", marginBottom: "8px" }}>{selNode.id} · {selNode.sub}</div>
              {onOpenRef && selNode.ref && (selNode.ref.title || selNode.ref.collection)
                ? <button className="btn btn--sm" onClick={() => onOpenRef(selNode.ref)}>Open document <I.ArrowRight /></button>
                : <span className="muted" style={{ fontSize: "12px" }}>External reference — not in this library.</span>}
            </div>
          )}
          <StepControl step={step} max={maxStep} onStep={(s) => { setStep(s); setPlaying(false); }} playing={playing} onTogglePlay={() => setPlaying((p) => !p)} />
        </React.Fragment>
      ) : (
        <div className="diagram__body"><RelTimeline doc={doc} onOpenRef={onOpenRef} /></div>
      )}
    </div>
  );
}

// ── Corpus map (domain clusters) ─────────────────────────────────────────────────
function buildClusters(index) {
  const domains = (index.facets.domains || []).slice(0, 22);
  const id2dom = new Map((index.graph.nodes || []).map((n) => [n.id, n.domainCode]));
  const weights = new Map();
  for (const e of (index.graph.edges || [])) {
    const a = id2dom.get(e.source), b = id2dom.get(e.target);
    if (!a || !b || a === b) continue;
    const key = a < b ? a + "|" + b : b + "|" + a;
    weights.set(key, (weights.get(key) || 0) + 1);
  }
  const W = 760, H = 560, cx0 = W / 2, cy0 = H / 2, R = 220;
  const maxCount = Math.max(...domains.map((d) => d.count), 1);
  const ranked = [...domains].sort((a, b) => b.count - a.count);
  const stepOf = new Map(ranked.map((d, i) => [d.code, Math.floor(i / 4) + 1]));
  const nodes = domains.map((d, i) => {
    const ang = (i / domains.length) * Math.PI * 2 - Math.PI / 2;
    const r = 22 + 22 * Math.sqrt(d.count / maxCount);
    return { id: d.code, kind: "cluster", r, w: r * 2, h: r * 2, label: d.code, sub: String(d.count),
      x: cx0 + R * Math.cos(ang), y: cy0 + R * Math.sin(ang), step: stepOf.get(d.code), domLabel: d.label, count: d.count };
  });
  const present = new Set(domains.map((d) => d.code));
  const maxW = Math.max(...weights.values(), 1);
  const edges = [];
  for (const [key, w] of weights) {
    const [a, b] = key.split("|");
    if (!present.has(a) || !present.has(b)) continue;
    edges.push({ source: a, target: b, type: "related", weight: w, weakOpacity: 0.1 + 0.5 * (w / maxW) });
  }
  const maxStep = Math.max(...stepOf.values(), 1);
  return { nodes, edges, viewW: W, viewH: H, maxStep };
}

function CorpusMap({ index, onOpenDoc }) {
  const wc = useWindowClass();
  const reduced = prefersReducedMotion();
  const { nodes, edges, viewW, viewH, maxStep } = useMemo(() => buildClusters(index), [index]);
  const [step, setStep] = useState(reduced ? maxStep : 1);
  const [playing, setPlaying] = useState(!reduced && maxStep > 1);
  const [sel, setSel] = useState(null);
  useEffect(() => {
    if (!playing) return;
    if (step >= maxStep) { setPlaying(false); return; }
    const t = setTimeout(() => setStep((s) => Math.min(maxStep, s + 1)), 850);
    return () => clearTimeout(t);
  }, [playing, step, maxStep]);

  const docsIn = (code) => (index.documents || []).filter((d) => d.domains && d.domains[0] && d.domains[0].code === code)
    .sort((a, b) => (b.relationCount || 0) - (a.relationCount || 0)).slice(0, 8);
  const selNode = nodes.find((n) => n.id === sel);

  if (!isSvgClass(wc)) {
    const sorted = [...nodes].sort((a, b) => b.count - a.count);
    return (
      <div className="diagram">
        <div className="diagram__head"><h4 className="diagram__title">Corpus by domain</h4><span className="diagram__hint">{nodes.length} domains</span></div>
        <div className="diagram__body">
          <div className="barset">
            {sorted.map((n) => (
              <button key={n.id} className="barrow" style={{ border: 0, background: "transparent", cursor: "pointer", textAlign: "left" }} onClick={() => setSel(n.id === sel ? null : n.id)} aria-pressed={sel === n.id}>
                <span className="barrow__label">{n.domLabel}</span>
                <span className="barrow__track"><span className="barrow__fill" style={{ width: (100 * n.count / Math.max(...nodes.map((x) => x.count))) + "%" }} /></span>
                <span className="barrow__val">{n.count}</span>
              </button>
            ))}
          </div>
          {selNode && <DomainPanel node={selNode} docs={docsIn(selNode.id)} onOpenDoc={onOpenDoc} />}
        </div>
      </div>
    );
  }
  return (
    <div className="diagram">
      <div className="diagram__head"><h4 className="diagram__title">Corpus relationship map</h4><span className="diagram__hint">domains sized by document count · lines = cross-domain references</span></div>
      <div className="diagram__body">
        <GraphCanvas nodes={nodes} edges={edges} viewW={viewW} viewH={viewH} visibleStep={step} selected={sel} onSelect={(id) => setSel(id === sel ? null : id)} reduced={reduced} />
      </div>
      {selNode && <DomainPanel node={selNode} docs={docsIn(selNode.id)} onOpenDoc={onOpenDoc} />}
      <StepControl step={step} max={maxStep} onStep={(s) => { setStep(s); setPlaying(false); }} playing={playing} onTogglePlay={() => setPlaying((p) => !p)} />
    </div>
  );
}
function DomainPanel({ node, docs, onOpenDoc }) {
  return (
    <div className="gpanel">
      <h5 className="gpanel__title">{node.domLabel} <span className="mono muted" style={{ fontSize: "12px" }}>· {node.count} documents</span></h5>
      <div className="reflist" style={{ marginTop: "8px" }}>
        {docs.map((d) => (
          <a key={d.id} href="#" onClick={(e) => { e.preventDefault(); onOpenDoc(d); }}>
            {collectionIcon(d.collection)}<span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{d.title}</span>
          </a>
        ))}
      </div>
    </div>
  );
}

// ── Structure map (heading hierarchy) ────────────────────────────────────────────
function StructureMap({ headings, onJump }) {
  if (!headings || !headings.length) return <EmptyState title="No sections" msg="This document has no sub-headings to map." />;
  return (
    <div className="diagram">
      <div className="diagram__head"><h4 className="diagram__title">Document structure</h4><span className="diagram__hint">{headings.length} sections</span></div>
      <div className="diagram__body">
        <div className="smap">
          {headings.map((h, i) => (
            <button key={i} className={cx("smap__item", "d" + h.depth)} onClick={() => onJump && onJump(h.id)}>
              <span className="smap__bullet" />{h.text}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Lifecycle stepper ────────────────────────────────────────────────────────────
const LIFECYCLE = ["draft", "active", "approved", "stable", "production", "superseded", "archived"];
function LifecycleStepper({ stage }) {
  // collapse the mid stages: show draft → active/approved → in-use → retired-ish, but keep canonical order
  const steps = ["draft", "active", "production", "superseded", "archived"];
  const order = { draft: 0, active: 1, approved: 1, stable: 2, production: 2, superseded: 3, archived: 4 };
  const cur = order[stage] != null ? order[stage] : 1;
  const labelFor = (s, i) => (i === 1 && (stage === "approved")) ? "approved" : (i === 2 && stage === "stable") ? "stable" : s;
  return (
    <div className="lifecycle" role="img" aria-label={"Lifecycle stage: " + stage}>
      {steps.map((s, i) => (
        <div key={s} className={cx("lifestep", i < cur && "done", i === cur && "current")}>
          <span className="lifestep__n">{String(i + 1).padStart(2, "0")}</span>
          <span className="lifestep__name">{labelFor(s, i)}</span>
        </div>
      ))}
    </div>
  );
}

// ── Lineage timeline (derived-from chronology) ───────────────────────────────────
function LineageTimeline({ doc, onOpenRef }) {
  const rel = doc.relationships || {};
  const parents = rel.derivedFrom || [];
  const supersededBy = (rel.referencedBy || []).filter((r) => r.type === "lineage");
  return (
    <div className="timeline">
      {parents.map((r, i) => (
        <div key={"p" + i} className="tnode">
          <div className="tnode__rel">Derived from</div>
          <a href="#" onClick={(e) => { e.preventDefault(); onOpenRef && onOpenRef(r); }}><div className="tnode__title">{r.title || r.id}</div><div className="mono muted" style={{ fontSize: "11px" }}>{r.id}</div></a>
        </div>
      ))}
      <div className="tnode">
        <div className="tnode__rel">This version</div>
        <div className="tnode__title">{doc.title} {doc.version && <span className="mono muted">· {doc.version}</span>}</div>
        <div className="mono muted" style={{ fontSize: "11px" }}>updated {fmtDate(doc.lastUpdated)}</div>
      </div>
      {supersededBy.map((r, i) => (
        <div key={"s" + i} className="tnode">
          <div className="tnode__rel">Built upon by</div>
          <a href="#" onClick={(e) => { e.preventDefault(); onOpenRef && onOpenRef(r); }}><div className="tnode__title">{r.title || r.id}</div><div className="mono muted" style={{ fontSize: "11px" }}>{r.id}</div></a>
        </div>
      ))}
    </div>
  );
}

// ── Charts ───────────────────────────────────────────────────────────────────────
function BarSet({ items }) {
  const max = Math.max(...items.map((i) => i.value), 1);
  return (
    <div className="barset">
      {items.map((it) => (
        <div className="barrow" key={it.label}>
          <span className="barrow__label" title={it.label}>{it.label}</span>
          <span className="barrow__track"><span className="barrow__fill" style={{ width: (100 * it.value / max) + "%" }} /></span>
          <span className="barrow__val">{it.value}</span>
        </div>
      ))}
    </div>
  );
}
function StatGrid({ stats }) {
  return <div className="statgrid">{stats.map((s) => <div className="statcard" key={s.label}><div className="statcard__n">{s.value}</div><div className="statcard__l">{s.label}</div></div>)}</div>;
}
function Treemap({ items, onSelect }) {
  const max = Math.max(...items.map((i) => i.count), 1);
  return (
    <div className="treemap">
      {items.map((it) => {
        const scale = 1 + 1.4 * (it.count / max);
        return (
          <button key={it.code} className="treecell" style={{ flexGrow: it.count, minWidth: (90 * scale) + "px" }} onClick={() => onSelect && onSelect(it)} title={it.label + " · " + it.count}>
            <span className="treecell__l">{it.label}</span>
            <span className="treecell__n">{it.count}</span>
          </button>
        );
      })}
    </div>
  );
}

// ── Mermaid (lazy) ────────────────────────────────────────────────────────────────
let _mermaidPromise = null;
function ensureMermaid() {
  if (window.mermaid) return Promise.resolve(window.mermaid);
  if (_mermaidPromise) return _mermaidPromise;
  _mermaidPromise = new Promise((resolve, reject) => {
    const s = document.createElement("script");
    s.src = "vendor/mermaid.min.js";
    s.onload = () => resolve(window.mermaid);
    s.onerror = () => reject(new Error("mermaid failed to load"));
    document.head.appendChild(s);
  });
  return _mermaidPromise;
}
async function renderMermaidIn(container) {
  const blocks = container.querySelectorAll(".uk-mermaid[data-mermaid-b64]");
  if (!blocks.length) return;
  try {
    const mermaid = await ensureMermaid();
    mermaid.initialize({ startOnLoad: false, theme: "neutral", securityLevel: "strict", fontFamily: "var(--uk-font-family-sans)" });
    let n = 0;
    for (const el of blocks) {
      if (el.dataset.rendered) continue;
      // UTF-8-aware decode (escape() is deprecated and corrupts non-ASCII content)
      const src = new TextDecoder().decode(Uint8Array.from(atob(el.dataset.mermaidB64), (c) => c.charCodeAt(0)));
      try {
        const { svg } = await mermaid.render("ukmmd-" + Date.now() + "-" + (n++), src);
        el.innerHTML = svg;
        el.dataset.rendered = "1";
      } catch (err) {
        const pre = el.querySelector(".uk-mermaid-src"); if (pre) pre.hidden = false;
        el.dataset.rendered = "err";
      }
    }
  } catch (e) {
    // vendor load failed — reveal sources as graceful degradation
    blocks.forEach((el) => { const pre = el.querySelector(".uk-mermaid-src"); if (pre) pre.hidden = false; });
  }
}

Object.assign(window, {
  useWindowClass, prefersReducedMotion, isSvgClass,
  GraphCanvas, StepControl, RelationshipGraph, CorpusMap, StructureMap,
  LifecycleStepper, LineageTimeline, BarSet, StatGrid, Treemap,
  ensureMermaid, renderMermaidIn
});
