// Component graph view — light-mode, warm-cream canvas matching the rest of the app.
// Exposes window.GraphView, window.NodeDetailView, window.GRAPH_COMPONENTS, window.heatForWidget.

const COMPONENTS = [
  {
    id: "chat-core", name: "Chat Core", complexity: "complex",
    x: 60,  y: 40,
    desc: "Message rendering, stream binding, and scroll virtualization. Owns the message-list, tool-call cards, and assistant message lifecycle.",
    tags: ["chat", "rendering", "state"],
    entities: ["ChatThread", "MessageList", "StreamBuffer"],
    flows: ["Render assistant turn", "Replay streamed chunks"],
    files: 108,
    rules: [
      "Each assistant message owns a single stream subscription, replayed from a client-side buffer.",
      "Virtualized rows must measure before auto-scroll commits.",
      "Tool-call expansion must not retrigger stream subscribe.",
    ],
    dependsOn: [
      { id: "streaming", note: "Subscribes to the stream pipeline; replays chunks against optimistic ids." },
      { id: "context-graph", note: "Reads pinned chips to scope retrieval before each turn." },
    ],
    dependedBy: [
      { id: "composer", note: "Composer dispatches user messages into Chat Core's optimistic queue." },
      { id: "auth", note: "Workspace + role checks gate the message API on send." },
    ],
    commits: [
      { sha: "f3a91c2", author: "anita.r", msg: "fix scroll anchor on tool-card expand", when: "2h" },
      { sha: "8d12ab0", author: "felix.h", msg: "defer stream subscribe until commit", when: "6h" },
      { sha: "12f4e1d", author: "felix.h", msg: "wip optimistic insert refactor", when: "1d" },
      { sha: "4a2c0c8", author: "anita.r", msg: "ResizeObserver on each row", when: "2d" },
      { sha: "9c81b14", author: "lukas.b", msg: "rename ChatStream → ChatPipeline", when: "3d" },
    ],
    churn: [3, 5, 4, 12, 8, 6, 9, 4, 2, 7, 11, 18, 22, 14, 9, 6, 4, 3, 5, 8, 12, 10, 7, 3, 2, 1, 4, 9, 7, 13],
    experts: [
      { name: "Felix H.", area: "Streaming & optimistic inserts" },
      { name: "Anita R.", area: "Virtualization & scroll anchor" },
      { name: "Lukas B.", area: "State store & migrations" },
    ],
    jiraOpen: 7, jiraClosed: 23,
    jiraOpenList: ["TEK-345 Implement new chat frontend", "TEK-381 Tool-card expand jitter", "TEK-402 Stream race on slow network"],
    confluence: ["Streaming-state architecture (v2)", "ChatThread render contract", "Virtualization decisions"],
    priorChats: { count: 18, label: "View →" },
    risks: [
      { sev: "high", score: 84, title: "Race condition between stream chunks and optimistic message" },
      { sev: "high", score: 76, title: "Virtualization breaks scroll-to-bottom on tool expansion" },
    ],
  },
  {
    id: "streaming", name: "Streaming & Tools", complexity: "complex",
    x: 360, y: 40,
    desc: "Tool-call orchestration and chunked LLM streaming. Owns the WebSocket boundary, retry, and tool-result envelope.",
    tags: ["streaming", "tools", "websocket"],
    entities: ["Stream", "ToolCall", "Chunk"],
    flows: ["Stream assistant turn", "Tool call → result"],
    files: 53,
    rules: [
      "Each turn opens exactly one stream; reconnects use the same correlation id.",
      "Tool results are delivered before the next assistant chunk binds.",
    ],
    dependsOn: [
      { id: "plugin-host", note: "Resolves skill identity for each tool call." },
    ],
    dependedBy: [
      { id: "chat-core", note: "Consumes chunk + tool events to drive rendering." },
      { id: "context-graph", note: "Reads chunk metadata to attribute sources." },
    ],
    commits: [
      { sha: "b71d9a2", author: "felix.h", msg: "correlation id on reconnect", when: "5h" },
      { sha: "5e09f31", author: "lukas.b", msg: "tool-result envelope v2", when: "1d" },
      { sha: "0a44c11", author: "felix.h", msg: "drop legacy SSE adapter", when: "2d" },
    ],
    churn: [2, 3, 8, 5, 4, 6, 3, 1, 2, 4, 6, 12, 9, 15, 11, 8, 4, 3, 5, 6, 9, 12, 7, 4, 3, 5, 6, 8, 10, 14],
    experts: [
      { name: "Felix H.", area: "Stream lifecycle" },
      { name: "Lukas B.", area: "Tool envelope" },
      { name: "Anita R.", area: "Reconnect & retry" },
    ],
    jiraOpen: 4, jiraClosed: 19,
    jiraOpenList: ["TEK-345 Implement new chat frontend", "TEK-355 Reconnect race"],
    confluence: ["Streaming protocol v2", "Tool envelope schema"],
    priorChats: { count: 11, label: "View →" },
    risks: [
      { sev: "high", score: 71, title: "Reconnect can deliver chunks to a stale subscription" },
    ],
  },
  {
    id: "composer", name: "Composer & Attachments", complexity: "moderate",
    x: 660, y: 40,
    desc: "Composer input surface: paste, drag-drop, chip rail, model switcher, and attachment upload pipeline.",
    tags: ["composer", "input", "uploads"],
    entities: ["Composer", "Dropzone", "AttachmentRail"],
    flows: ["Send user message", "Upload attachment"],
    files: 64,
    rules: [
      "Drag-drop and clipboard paste must not collide at window scope.",
      "Attachment uploads are gated by per-file size + MIME.",
    ],
    dependsOn: [
      { id: "chat-core", note: "Dispatches user messages into the optimistic queue." },
      { id: "context-graph", note: "Reads + mutates pinned chips." },
    ],
    dependedBy: [
      { id: "knowledge", note: "Uploaded attachments are indexed downstream." },
    ],
    commits: [
      { sha: "2bd0a1e", author: "felix.h", msg: "scope paste handler to dropzone ref", when: "3h" },
      { sha: "9912a7c", author: "felix.h", msg: "AttachmentRail layout", when: "1d" },
      { sha: "ee21f55", author: "anita.r", msg: "chip rail keyboard nav", when: "2d" },
    ],
    churn: [4, 6, 3, 2, 5, 7, 9, 8, 4, 3, 5, 11, 14, 18, 22, 16, 9, 6, 4, 3, 7, 10, 8, 5, 4, 6, 9, 12, 15, 11],
    experts: [
      { name: "Felix H.", area: "Composer & paste handling" },
      { name: "Anita R.", area: "Chip rail & accessibility" },
      { name: "Lukas B.", area: "Upload pipeline" },
    ],
    jiraOpen: 6, jiraClosed: 14,
    jiraOpenList: ["TEK-140 Allow attachments in Teklens.AI", "TEK-345 Implement new chat frontend"],
    confluence: ["Attachments architecture", "Composer chip contract"],
    priorChats: { count: 9, label: "View →" },
    risks: [
      { sev: "medium", score: 52, title: "Composer drag-drop swallows clipboard paste on Safari 17" },
    ],
  },
  {
    id: "context-graph", name: "Context Graph", complexity: "complex",
    x: 360, y: 250,
    desc: "Pinned-context store, entity index, and retrieval scoping. Resolves chips → retrieval filters at each turn.",
    tags: ["retrieval", "context", "graph"],
    entities: ["ContextNode", "PinIndex", "Restriction"],
    flows: ["Resolve retrieval scope"],
    files: 47,
    rules: [
      "Pinned chips compose by union except entity-restricted, which intersects.",
      "Schema migrations must run before first hydrate.",
    ],
    dependsOn: [
      { id: "knowledge", note: "Queries the indexer for chunk-level retrieval." },
    ],
    dependedBy: [
      { id: "chat-core", note: "Reads scope to filter sources for each turn." },
      { id: "composer", note: "Reads + mutates pinned chips." },
    ],
    commits: [
      { sha: "1a32d04", author: "lukas.b", msg: "draft contextGraph schema", when: "8h" },
      { sha: "ff09a12", author: "lukas.b", msg: "remove contextChips[] legacy", when: "1d" },
      { sha: "70b2cc1", author: "anita.r", msg: "restriction flag on entity chip", when: "3d" },
    ],
    churn: [1, 2, 1, 3, 4, 6, 8, 5, 4, 2, 1, 3, 5, 7, 9, 12, 16, 11, 8, 6, 4, 3, 5, 7, 4, 2, 3, 5, 6, 9],
    experts: [
      { name: "Lukas B.", area: "Schema & migrations" },
      { name: "Anita R.", area: "Restriction semantics" },
      { name: "Felix H.", area: "Retrieval glue" },
    ],
    jiraOpen: 3, jiraClosed: 11,
    jiraOpenList: ["TEK-345 Implement new chat frontend", "TEK-401 Context migrator backfill"],
    confluence: ["Context graph data model", "Retrieval scoping policy"],
    priorChats: { count: 6, label: "View →" },
    risks: [
      { sev: "medium", score: 64, title: "Pinned-context store migration has no backfill" },
    ],
  },
  {
    id: "knowledge", name: "Knowledge Indexer", complexity: "moderate",
    x: 60,  y: 250,
    desc: "Document ingest, embedding, and chunk retrieval. Backs the RAG layer used by Context Graph.",
    tags: ["embeddings", "rag", "indexing"],
    entities: ["Doc", "Chunk", "Embedding"],
    flows: ["Index document"],
    files: 72,
    rules: [
      "All chunks are addressable by (doc_id, offset).",
      "Re-embedding runs are idempotent.",
    ],
    dependsOn: [],
    dependedBy: [
      { id: "context-graph", note: "Queries chunks at retrieval time." },
    ],
    commits: [
      { sha: "31a0bb4", author: "lukas.b", msg: "batch embedding worker", when: "1d" },
      { sha: "c0987ab", author: "anita.r", msg: "deterministic chunk offsets", when: "3d" },
    ],
    churn: [2, 1, 3, 2, 4, 5, 3, 1, 2, 4, 6, 5, 3, 2, 1, 4, 6, 7, 5, 3, 2, 1, 4, 3, 2, 5, 4, 3, 2, 1],
    experts: [
      { name: "Lukas B.", area: "Ingest pipeline" },
      { name: "Anita R.", area: "Chunking" },
    ],
    jiraOpen: 1, jiraClosed: 8,
    jiraOpenList: ["TEK-220 Re-embed on schema change"],
    confluence: ["Indexer architecture"],
    priorChats: { count: 3, label: "View →" },
    risks: [],
  },
  {
    id: "auth", name: "Auth & RBAC", complexity: "moderate",
    x: 660, y: 250,
    desc: "Workspace authentication, role permissions, and per-resource scoping.",
    tags: ["auth", "rbac", "permissions"],
    entities: ["Role", "Workspace", "Permission"],
    flows: ["Authorize action"],
    files: 31,
    rules: [
      "Every API call is scoped by workspace_id and role.",
      "Role changes propagate within one session.",
    ],
    dependsOn: [],
    dependedBy: [
      { id: "chat-core", note: "Gates message send + retrieval scope." },
    ],
    commits: [
      { sha: "7e1f0b2", author: "anita.r", msg: "role propagation on session refresh", when: "2d" },
      { sha: "2c91a8e", author: "lukas.b", msg: "permission matrix v3", when: "4d" },
    ],
    churn: [1, 0, 0, 2, 3, 1, 0, 1, 2, 1, 0, 0, 3, 4, 2, 1, 0, 2, 3, 1, 0, 0, 1, 2, 3, 4, 2, 1, 0, 1],
    experts: [
      { name: "Anita R.", area: "Session refresh" },
      { name: "Lukas B.", area: "Permission matrix" },
    ],
    jiraOpen: 0, jiraClosed: 5,
    jiraOpenList: [],
    confluence: ["RBAC matrix v3"],
    priorChats: { count: 2, label: "View →" },
    risks: [],
  },
  {
    id: "plugin-host", name: "Plugin Host", complexity: "simple",
    x: 210, y: 460,
    desc: "Skill loading and sandboxed execution boundary for tool calls.",
    tags: ["plugins", "skills"],
    entities: ["Skill", "Plugin"],
    flows: ["Load skill"],
    files: 22,
    rules: [
      "Skills must be sandboxed at the worker boundary.",
    ],
    dependsOn: [],
    dependedBy: [
      { id: "streaming", note: "Resolves which skill to invoke per tool call." },
    ],
    commits: [
      { sha: "a0f1234", author: "felix.h", msg: "worker isolation v1", when: "3d" },
    ],
    churn: [0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0],
    experts: [{ name: "Felix H.", area: "Sandbox boundary" }],
    jiraOpen: 1, jiraClosed: 3,
    jiraOpenList: ["TEK-298 Worker isolation hardening"],
    confluence: ["Skill loading contract"],
    priorChats: { count: 1, label: "View →" },
    risks: [],
  },
  {
    id: "telemetry", name: "Telemetry", complexity: "simple",
    x: 510, y: 460,
    desc: "Event collection, analytics, and rollup pipeline.",
    tags: ["analytics", "events"],
    entities: ["Event", "Rollup"],
    flows: ["Emit event"],
    files: 18,
    rules: [
      "PII is stripped at the edge before persistence.",
    ],
    dependsOn: [],
    dependedBy: [],
    commits: [
      { sha: "55c0fa1", author: "anita.r", msg: "edge PII redactor", when: "5d" },
    ],
    churn: [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
    experts: [{ name: "Anita R.", area: "Edge pipeline" }],
    jiraOpen: 0, jiraClosed: 2,
    jiraOpenList: [],
    confluence: ["Telemetry schema"],
    priorChats: { count: 0, label: "View →" },
    risks: [],
  },
];

const EDGES = [
  { from: "chat-core", to: "streaming", count: 3 },
  { from: "chat-core", to: "composer", count: 2 },
  { from: "chat-core", to: "context-graph", count: 4 },
  { from: "composer", to: "context-graph", count: 2 },
  { from: "chat-core", to: "auth", count: 1 },
  { from: "context-graph", to: "knowledge", count: 2 },
  { from: "plugin-host", to: "streaming", count: 1 },
];

const HEAT_BY_WIDGET = {
  10: { "chat-core": 0.95, "streaming": 0.82, "context-graph": 0.55, "composer": 0.20, "auth": 0.08, "knowledge": 0.04, "plugin-host": 0.02, "telemetry": 0.00 },
  30: { "composer": 0.90, "chat-core": 0.45, "context-graph": 0.40, "streaming": 0.15, "auth": 0.05, "knowledge": 0.10, "plugin-host": 0.00, "telemetry": 0.00 },
  31: { "composer": 0.90, "chat-core": 0.45, "context-graph": 0.40, "streaming": 0.15, "auth": 0.05, "knowledge": 0.10, "plugin-host": 0.00, "telemetry": 0.00 },
};

const HEAT_SOURCE_LABEL = {
  10: "Risk Analysis · TEK-345",
  30: "Story Estimation · TEK-140",
  31: "Story Estimation · TEK-140",
};

function heatForWidget(widgetId) {
  return HEAT_BY_WIDGET[widgetId] || {};
}

// Per-component "Component Analysis" widgets (one per domain). Each opens the graph
// focused on its own component, with neighbors warm and the rest cool.
const COMP_WIDGET_IDS = {
  "chat-core": 40, "streaming": 41, "composer": 42, "context-graph": 43,
  "knowledge": 44, "auth": 45, "plugin-host": 46, "telemetry": 47,
};
Object.entries(COMP_WIDGET_IDS).forEach(([cid, wid]) => {
  const neighbors = new Set();
  EDGES.forEach(e => { if (e.from === cid) neighbors.add(e.to); if (e.to === cid) neighbors.add(e.from); });
  const h = {};
  COMPONENTS.forEach(c => { h[c.id] = c.id === cid ? 0.95 : neighbors.has(c.id) ? 0.55 : 0.06; });
  HEAT_BY_WIDGET[wid] = h;
  HEAT_SOURCE_LABEL[wid] = `Component Analysis · ${(COMPONENTS.find(c => c.id === cid) || {}).name || cid}`;
});

// Heat → background / border / glow.
function heatStyle(h) {
  if (h < 0.12) return { bg: "#ffffff", border: "#ece7da", dim: true };
  if (h < 0.35) return { bg: "#fbf3e1", border: "#e6cf9c" };
  if (h < 0.6)  return { bg: "#fbe3c6", border: "#e0a85c" };
  if (h < 0.8)  return { bg: "#fcd2b7", border: "#d97a3a", glow: 0.25 };
  return            { bg: "#fbc5b9", border: "#d05848", glow: 0.45 };
}

function complexityBadge(c) {
  if (c === "complex")  return { color: "#a83228", bg: "#fdecea", label: "complex" };
  if (c === "moderate") return { color: "#a05a00", bg: "#fef0d9", label: "moderate" };
  return                       { color: "#1f7a3a", bg: "#e8f6ec", label: "simple" };
}

// Card width / height for layout math.
const CARD_W = 230;
const CARD_H = 150;

function GraphView({ widgetId, onSelectNode, overview }) {
  const heat = overview ? {} : heatForWidget(widgetId);
  const heatLabel = HEAT_SOURCE_LABEL[widgetId] || "current analysis";

  // Canvas size derived from node coords.
  const maxX = Math.max(...COMPONENTS.map(c => c.x)) + CARD_W + 60;
  const maxY = Math.max(...COMPONENTS.map(c => c.y)) + CARD_H + 60;

  return (
    <div className="relative w-full h-full bg-[#faf8f3] overflow-auto">
      {/* dot grid */}
      <div className="absolute inset-0 pointer-events-none"
        style={{
          backgroundImage: "radial-gradient(circle, #e6dfcb 1px, transparent 1px)",
          backgroundSize: "16px 16px",
          backgroundPosition: "0 0",
        }} />

      {/* Top toolbar */}
      <div className="sticky top-0 z-20 flex items-center justify-between px-4 py-2.5 bg-gradient-to-b from-[#faf8f3] to-[#faf8f3]/0">
        {/* Left: mode switch */}
        <div className="inline-flex items-center bg-white border border-[#ece7da] rounded-md p-0.5 text-[11.5px] shadow-[0_1px_2px_rgba(0,0,0,0.03)]">
          <button className="px-2.5 py-1 rounded bg-[#f4ecd9] text-[#0e0e10] font-medium">Domain</button>
          <button className="px-2.5 py-1 rounded text-[#bbb3a1] cursor-not-allowed"
            title="v1: Domain only">Structural</button>
        </div>

        {/* Right: legend — composition arrows in overview, heat in analysis */}
        {overview ? (
          <div className="flex items-center gap-2 bg-white border border-[#ece7da] rounded-md px-2.5 py-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.03)] text-[11px] text-[#6b6b6b]">
            <svg width="26" height="10" viewBox="0 0 26 10"><line x1="1" y1="5" x2="20" y2="5" stroke="#b9b09a" strokeWidth="1.4" /><path d="M19 2l5 3-5 3z" fill="#b9b09a" /></svg>
            <span>depends on</span>
            <span className="text-[#cbcbcb]">·</span>
            <span className="font-mono text-[10px] px-1 rounded-full border border-[#d4cdba] bg-white">n</span>
            <span>shared refs</span>
          </div>
        ) : (
          <div className="flex items-center gap-2.5 bg-white border border-[#ece7da] rounded-md px-2.5 py-1.5 shadow-[0_1px_2px_rgba(0,0,0,0.03)]"
            title={`Heat = estimated change magnitude on this component from the current analysis.\nSource: ${heatLabel}`}>
            <span className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-[#9aa0a6]">Heat</span>
            <div className="w-[120px] h-2 rounded-full"
              style={{ background: "linear-gradient(90deg, #ece7da 0%, #e6a23a 55%, #d05848 100%)" }} />
            <div className="flex items-center gap-1.5 text-[10.5px] text-[#6b6b6b]">
              <span>none</span>
              <span className="text-[#cbcbcb]">·</span>
              <span>med</span>
              <span className="text-[#cbcbcb]">·</span>
              <span>high</span>
            </div>
          </div>
        )}
      </div>

      {/* Caption */}
      <div className="px-4 -mt-1 mb-2 flex items-center gap-1.5 text-[11px] text-[#9b8866]">
        <Icon name="info" size={11} />
        {overview ? (
          <span><span className="text-[#0e0e10] font-medium">{COMPONENTS.length} domain components</span> · arrows point from a component to what it depends on. Click any card for detail.</span>
        ) : (
          <span>Heat source: <span className="text-[#0e0e10] font-medium">{heatLabel}</span></span>
        )}
      </div>

      {/* Canvas */}
      <div className="relative" style={{ width: maxX, height: maxY, minHeight: "100%" }}>
        {/* SVG edges */}
        <svg className="absolute inset-0 pointer-events-none" width={maxX} height={maxY}>
          <defs>
            <marker id="arrow-dep" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M0 1L9 5L0 9z" fill="#b9b09a" />
            </marker>
            <marker id="arrow-dep-hot" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M0 1L9 5L0 9z" fill="#d49a82" />
            </marker>
          </defs>
          {EDGES.map((e, i) => {
            const a = COMPONENTS.find(c => c.id === e.from);
            const b = COMPONENTS.find(c => c.id === e.to);
            const ax = a.x + CARD_W / 2, ay = a.y + CARD_H / 2;
            const bx = b.x + CARD_W / 2, by = b.y + CARD_H / 2;
            // Pull endpoints toward the card edges so the arrowhead doesn't hide under the node.
            const dx = bx - ax, dy = by - ay, len = Math.hypot(dx, dy) || 1;
            const ux = dx / len, uy = dy / len;
            const inset = 86;
            const sx = ax + ux * inset, sy = ay + uy * inset;
            const ex = bx - ux * inset, ey = by - uy * inset;
            const mx = (ax + bx) / 2, my = (ay + by) / 2;
            const hot = Math.max(heat[a.id] || 0, heat[b.id] || 0);
            const stroke = hot > 0.5 ? "#d49a82" : "#d4cdba";
            return (
              <g key={i}>
                <line x1={sx} y1={sy} x2={ex} y2={ey} stroke={stroke} strokeWidth={1.4}
                  markerEnd={`url(#${hot > 0.5 ? "arrow-dep-hot" : "arrow-dep"})`} />
                <g transform={`translate(${mx} ${my})`}>
                  <circle r="9" fill="#ffffff" stroke="#d4cdba" strokeWidth="1" />
                  <text textAnchor="middle" dominantBaseline="central" fontSize="10"
                    fontFamily="JetBrains Mono, ui-monospace, monospace" fill="#6b6b6b">{e.count}</text>
                </g>
              </g>
            );
          })}
        </svg>

        {/* Nodes */}
        {COMPONENTS.map(c => {
          const h = heat[c.id] || 0;
          const s = heatStyle(h);
          const cb = complexityBadge(c.complexity);
          return (
            <button
              key={c.id}
              onClick={() => onSelectNode(c.id)}
              className="absolute group text-left rounded-xl transition-shadow"
              style={{
                left: c.x, top: c.y, width: CARD_W, height: CARD_H,
                background: s.bg,
                border: `1px solid ${s.border}`,
                boxShadow: s.glow
                  ? `0 0 0 1px ${s.border}33, 0 6px 20px rgba(208,88,72,${s.glow})`
                  : "0 1px 2px rgba(0,0,0,0.04)",
              }}
              onMouseEnter={(e) => { e.currentTarget.style.outline = "1.5px solid var(--accent)"; e.currentTarget.style.outlineOffset = "1px"; }}
              onMouseLeave={(e) => { e.currentTarget.style.outline = "none"; }}
            >
              <div className="p-3 flex flex-col h-full">
                <div className="flex items-center justify-between">
                  <span className="text-[9.5px] font-semibold tracking-[0.1em] uppercase text-[#a05a00]">DOMAIN</span>
                  <span className="px-1.5 py-[1px] rounded-full text-[10px] font-medium"
                    style={{ background: cb.bg, color: cb.color }}>{cb.label}</span>
                </div>
                <div className={`mt-1.5 text-[14px] font-semibold leading-tight ${s.dim ? "text-[#3a3a3a]" : "text-[#0e0e10]"}`}>{c.name}</div>
                <div className={`mt-1 text-[11.5px] leading-snug ${s.dim ? "text-[#8a8170]" : "text-[#5a4a2f]"} line-clamp-3`}>{c.desc}</div>
                <div className="mt-auto pt-2 flex items-center gap-1 flex-wrap">
                  {c.entities.slice(0, 2).map(en => (
                    <span key={en} className="font-mono text-[10px] px-1.5 py-[1px] rounded bg-white/70 border border-[#ece7da] text-[#3a3a3a]">{en}</span>
                  ))}
                  <span className="ml-auto text-[10px] text-[#9aa0a6]">{c.flows.length} flow{c.flows.length > 1 ? "s" : ""}</span>
                </div>
              </div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ---- Node detail ----
function NodeDetailView({ nodeId, widgetId, onBack }) {
  const node = COMPONENTS.find(c => c.id === nodeId);
  if (!node) return null;
  const cb = complexityBadge(node.complexity);

  const onPin = () => {
    window.dispatchEvent(new CustomEvent("teklens:add-chip", {
      detail: { id: `comp-${node.id}`, kind: "component", label: node.name }
    }));
  };

  return (
    <div className="bg-[#faf8f3] min-h-full">
      {/* Header */}
      <div className="sticky top-0 z-20 bg-[#faf8f3]/95 backdrop-blur border-b border-[#ece7da] px-4 py-2.5 flex items-center gap-2">
        <button onClick={onBack} className="p-1.5 rounded-md hover:bg-black/[0.05] text-[#5a5a5a]" title="Back to graph">
          <Icon name="chevron-left" size={15} />
        </button>
        <div className="flex items-center gap-2 min-w-0">
          <Icon name="hierarchy" size={14} className="text-[#a05a00]" />
          <span className="text-[14px] font-semibold text-[#0e0e10] truncate">{node.name}</span>
          <span className="px-1.5 py-[1px] rounded-full text-[10px] font-medium shrink-0"
            style={{ background: cb.bg, color: cb.color }}>{cb.label}</span>
        </div>
        <div className="flex-1" />
        <button onClick={onPin}
          className="flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-[var(--accent)] text-white text-[12px] font-medium hover:brightness-110">
          <Icon name="pin" size={12} /> Pin as context
        </button>
      </div>

      <div className="p-4 flex flex-col gap-3">
        {/* Definition */}
        <Card title="Definition" icon="info">
          <p className="text-[12.5px] leading-[1.6] text-[#1a1a1a]">{node.desc}</p>
          <div className="mt-2.5 flex flex-wrap gap-1.5">
            {node.tags.map(t => (
              <span key={t} className="px-2 py-[1px] rounded-full text-[10.5px] bg-[#f4ecd9] text-[#7a5d00]">{t}</span>
            ))}
          </div>
          {node.rules.length > 0 && (
            <>
              <div className="mt-3 text-[10.5px] uppercase tracking-wider text-[#9aa0a6]">Business rules</div>
              <ul className="mt-1 ml-4 list-disc space-y-1 text-[12px] text-[#1a1a1a] marker:text-[#cbb98a]">
                {node.rules.map((r, i) => <li key={i}>{r}</li>)}
              </ul>
            </>
          )}
        </Card>

        {/* Structure */}
        <Card title="Structure" icon="entity">
          <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6]">Entities</div>
          <div className="mt-1 flex flex-wrap gap-1.5">
            {node.entities.map(e => (
              <span key={e} className="font-mono text-[11px] px-1.5 py-[1px] rounded bg-[#f4f1ea] border border-[#ece7da] text-[#0e0e10]">{e}</span>
            ))}
          </div>
          <div className="mt-3 text-[10.5px] uppercase tracking-wider text-[#9aa0a6]">Flows</div>
          <ol className="mt-1 ml-4 list-decimal space-y-0.5 text-[12px] text-[#1a1a1a] marker:text-[#9aa0a6]">
            {node.flows.map((f, i) => <li key={i}>{f}</li>)}
          </ol>
          <div className="mt-3 flex items-center text-[11.5px] text-[#6b6b6b]">
            <span className="font-mono text-[12px] text-[#0e0e10]">{node.files}</span>
            <span className="ml-1">files</span>
            <span className="mx-1.5 text-[#cbcbcb]">·</span>
            <button className="text-[var(--accent)] hover:underline inline-flex items-center gap-0.5">
              open in Code <Icon name="chevron-right" size={11} />
            </button>
          </div>
        </Card>

        {/* Connections */}
        <Card title="Connections" icon="link">
          <div className="grid grid-cols-2 gap-3">
            <div>
              <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1">Depends on</div>
              {node.dependsOn.length === 0 && <div className="text-[11.5px] text-[#9aa0a6] italic">none</div>}
              <div className="flex flex-col gap-1.5">
                {node.dependsOn.map(d => {
                  const t = COMPONENTS.find(c => c.id === d.id);
                  return (
                    <div key={d.id}>
                      <DepPill name={t?.name || d.id} />
                      <div className="mt-0.5 text-[10.5px] text-[#9aa0a6] leading-snug">{d.note}</div>
                    </div>
                  );
                })}
              </div>
            </div>
            <div>
              <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1">Depended by</div>
              {node.dependedBy.length === 0 && <div className="text-[11.5px] text-[#9aa0a6] italic">none</div>}
              <div className="flex flex-col gap-1.5">
                {node.dependedBy.map(d => {
                  const t = COMPONENTS.find(c => c.id === d.id);
                  return (
                    <div key={d.id}>
                      <DepPill name={t?.name || d.id} />
                      <div className="mt-0.5 text-[10.5px] text-[#9aa0a6] leading-snug">{d.note}</div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        </Card>

        {/* Activity */}
        <Card title="Activity" icon="clock">
          <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1">Last 5 commits</div>
          <div className="flex flex-col gap-1">
            {node.commits.slice(0, 5).map((co, i) => (
              <div key={i} className="flex items-center gap-2 text-[11.5px]">
                <span className="font-mono text-[10.5px] text-[#a05a00]">{co.sha}</span>
                <span className="font-mono text-[10.5px] text-[#9aa0a6]">{co.author}</span>
                <span className="text-[#1a1a1a] truncate flex-1">{co.msg}</span>
                <span className="text-[10.5px] text-[#9aa0a6] shrink-0">{co.when}</span>
              </div>
            ))}
          </div>
          <div className="mt-3 text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1">Churn · last 30 days</div>
          <Sparkline data={node.churn} />
        </Card>

        {/* People */}
        <Card title="People" icon="users">
          <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1.5">Top experts</div>
          <div className="flex flex-col gap-1.5">
            {node.experts.slice(0, 3).map(e => (
              <div key={e.name} className="flex items-center gap-2.5">
                <div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#a05a00] to-[#d05848] text-white flex items-center justify-center text-[10px] font-semibold">
                  {e.name.split(/\s+/).map(w => w[0]).join("")}
                </div>
                <div className="flex-1 min-w-0">
                  <div className="text-[12.5px] font-medium text-[#0e0e10]">{e.name}</div>
                  <div className="text-[11px] text-[#6b6b6b]">{e.area}</div>
                </div>
                <button className="px-2 py-1 rounded text-[11.5px] text-[var(--accent)] hover:bg-[var(--accent)]/10">Ping</button>
              </div>
            ))}
          </div>
        </Card>

        {/* Knowledge */}
        <Card title="Knowledge" icon="book">
          <KnowledgeBlock node={node} />
        </Card>

        {/* Risk */}
        <Card title="Risk" icon="shield">
          {node.risks.length === 0 ? (
            <div className="text-[11.5px] text-[#9aa0a6]">No analyses currently flag this node.</div>
          ) : (
            <div className="flex flex-col gap-1.5">
              {node.risks.map((r, i) => {
                const sev = r.sev === "high"   ? { bg: "#fdecea", color: "#a83228", label: "High" }
                          : r.sev === "medium" ? { bg: "#fef0d9", color: "#a05a00", label: "Medium" }
                          :                      { bg: "#e8f6ec", color: "#1f7a3a", label: "Low" };
                return (
                  <div key={i} className="flex items-start gap-2">
                    <div className="px-2 py-[1px] rounded-full text-[10.5px] font-medium shrink-0"
                      style={{ background: sev.bg, color: sev.color }}>{sev.label}</div>
                    <div className="text-[12px] text-[#1a1a1a] leading-snug flex-1">{r.title}</div>
                    <div className="text-[11px] text-[#9aa0a6] tabular-nums shrink-0">{r.score}/100</div>
                  </div>
                );
              })}
            </div>
          )}
        </Card>

        {/* Actions */}
        <div className="flex flex-wrap gap-2 pt-1 pb-2">
          <ActionButton icon="message" label="Open virtual chat" />
          <ActionButton icon="code" label="Jump to code" />
          <ActionButton icon="link" label="Copy graph link" />
        </div>
      </div>
    </div>
  );
}

function Card({ title, icon, children }) {
  return (
    <div className="rounded-xl border border-[#ece7da] bg-white p-4">
      <div className="flex items-center gap-1.5 mb-2">
        <Icon name={icon} size={13} className="text-[#a05a00]" />
        <div className="text-[12.5px] font-semibold text-[#0e0e10]">{title}</div>
      </div>
      {children}
    </div>
  );
}

function DepPill({ name }) {
  return (
    <span className="inline-flex items-center gap-1 pl-1.5 pr-2 py-[2px] rounded-md border border-[#ece7da] bg-[#faf8f3] text-[11.5px] text-[#0e0e10]">
      <Icon name="hierarchy" size={10} className="text-[#a05a00]" />
      {name}
    </span>
  );
}

function ActionButton({ icon, label }) {
  return (
    <button className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md border border-[#ece7da] bg-white text-[12px] text-[#1a1a1a] hover:border-[var(--accent)] hover:text-[var(--accent)]">
      <Icon name={icon} size={12} /> {label}
    </button>
  );
}

function Sparkline({ data }) {
  const w = 280, h = 36;
  const max = Math.max(...data, 1);
  const step = w / (data.length - 1);
  const points = data.map((v, i) => `${i * step},${h - (v / max) * h}`).join(" ");
  return (
    <svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      <polyline fill="none" stroke="#a05a00" strokeWidth="1.2" points={points} />
      <polyline fill="rgba(160,90,0,0.08)" stroke="none"
        points={`0,${h} ${points} ${w},${h}`} />
    </svg>
  );
}

function KnowledgeBlock({ node }) {
  const [openJira, setOpenJira] = React.useState(false);
  return (
    <>
      <div className="text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1.5">Jira stories</div>
      <div className="flex items-center gap-1.5">
        <button onClick={() => setOpenJira(o => !o)}
          className="inline-flex items-center gap-1 px-2 py-[2px] rounded-full text-[11px] bg-[#fdecea] text-[#a83228] hover:brightness-95">
          <Icon name="jira" size={10} /> {node.jiraOpen} open
          <Icon name={openJira ? "chevron-down" : "chevron-right"} size={10} />
        </button>
        <span className="inline-flex items-center gap-1 px-2 py-[2px] rounded-full text-[11px] bg-[#e8f6ec] text-[#1f7a3a]">
          <Icon name="check" size={10} /> {node.jiraClosed} closed
        </span>
      </div>
      {openJira && node.jiraOpenList.length > 0 && (
        <ul className="mt-1.5 ml-1 flex flex-col gap-0.5 text-[11.5px] text-[#1a1a1a]">
          {node.jiraOpenList.map((t, i) => (
            <li key={i} className="flex items-start gap-1.5">
              <span className="mt-1 w-1 h-1 rounded-full bg-[#a83228] shrink-0" />
              <span className="leading-snug">{t}</span>
            </li>
          ))}
        </ul>
      )}

      {node.confluence.length > 0 && (
        <>
          <div className="mt-3 text-[10.5px] uppercase tracking-wider text-[#9aa0a6] mb-1.5">Confluence</div>
          <div className="flex flex-col gap-1">
            {node.confluence.map((c, i) => (
              <div key={i} className="flex items-center gap-1.5 text-[11.5px] text-[#1a1a1a]">
                <Icon name="confluence" size={11} className="text-[#1c4f9c]" />
                {c}
              </div>
            ))}
          </div>
        </>
      )}

      <div className="mt-3 flex items-center text-[11.5px] text-[#6b6b6b]">
        <Icon name="message" size={11} className="mr-1 text-[#9aa0a6]" />
        <span className="font-mono text-[12px] text-[#0e0e10]">{node.priorChats.count}</span>
        <span className="ml-1">prior chats mention {node.name}</span>
        <div className="flex-1" />
        <button className="text-[var(--accent)] hover:underline">{node.priorChats.label}</button>
      </div>
    </>
  );
}

// ---- Full-page composition overview (opened from the "Components" rail icon) ----
function ComponentOverview() {
  const [selected, setSelected] = React.useState(null);
  const counts = {
    complex: COMPONENTS.filter(c => c.complexity === "complex").length,
    moderate: COMPONENTS.filter(c => c.complexity === "moderate").length,
    simple: COMPONENTS.filter(c => c.complexity === "simple").length,
  };
  return (
    <div className="flex-1 min-w-0 h-full flex flex-col bg-[#faf8f3]">
      {/* Page header */}
      <div className="shrink-0 border-b border-[#e8e3d8] bg-white px-5 py-3 flex items-center gap-3">
        <div className="w-8 h-8 rounded-lg bg-[#fbf0d4] border border-[#ecdbb0] flex items-center justify-center">
          <Icon name="hierarchy" size={16} className="text-[#a05a00]" />
        </div>
        <div className="min-w-0">
          <div className="text-[15px] font-semibold text-[#0e0e10] leading-tight">Components</div>
          <div className="text-[12px] text-[#6b6b6b]">Composition map · contem/teklens-ai</div>
        </div>
        <div className="flex-1" />
        <div className="hidden sm:flex items-center gap-2 text-[11.5px]">
          <span className="px-2 py-[2px] rounded-full font-medium" style={{ background: "#fdecea", color: "#a83228" }}>{counts.complex} complex</span>
          <span className="px-2 py-[2px] rounded-full font-medium" style={{ background: "#fef0d9", color: "#a05a00" }}>{counts.moderate} moderate</span>
          <span className="px-2 py-[2px] rounded-full font-medium" style={{ background: "#e8f6ec", color: "#1f7a3a" }}>{counts.simple} simple</span>
        </div>
      </div>
      {/* Body: graph or detail */}
      <div className="flex-1 min-h-0 overflow-y-auto">
        {selected
          ? <NodeDetailView nodeId={selected} widgetId={null} onBack={() => setSelected(null)} />
          : <GraphView overview onSelectNode={setSelected} />}
      </div>
    </div>
  );
}

// Expose to other scripts (each <script type="text/babel"> gets its own scope).
Object.assign(window, { GraphView, NodeDetailView, ComponentOverview, GRAPH_COMPONENTS: COMPONENTS, heatForWidget, HEAT_SOURCE_LABEL, COMP_WIDGET_IDS });
