/* global window, React */
const { useMemo, useRef, useState, useEffect } = React;

function Graph({ activeIds, activeQ, onSelect, selectedId, visibleTiers, confidenceMin = 0, theme, zoom = 1, pan = { x: 0, y: 0 } }) {
  const { NODES, EDGES, ENTITY_KINDS, DOMAINS } = window.CIQ;
  const edgeRoleMap = useMemo(() => {
    const m = new Map();
    if (activeQ && activeQ.subgraphEdges) {
      activeQ.subgraphEdges.forEach(se => {
        m.set(`${se.s}|${se.t}|${se.label}`, se.role);
        m.set(`${se.t}|${se.s}|${se.label}`, se.role);
      });
    }
    return m;
  }, [activeQ]);
  const svgRef = useRef(null);
  const [tooltip, setTooltip] = useState(null);

  // ── Viewport-aware text scaling ──────────────────────────────
  // The SVG renders into a fixed 1100×680 viewBox. At desktop the
  // canvas is ~1100px wide so 1 viewBox unit ≈ 1 screen px and an
  // 11-unit fontSize reads as 11px. At mobile (~390px wide) the
  // SVG scales by ~0.36, dropping that 11px label to ~4px screen —
  // unreadable. We track the rendered width and bump fontSize +
  // strokeWidth inversely so labels stay legible on every viewport.
  const [renderedWidth, setRenderedWidth] = useState(1100);
  useEffect(() => {
    if (!svgRef.current) return;
    const measure = () => {
      if (svgRef.current) {
        const w = svgRef.current.getBoundingClientRect().width;
        if (w > 0) setRenderedWidth(w);
      }
    };
    measure();
    let ro;
    if (typeof ResizeObserver !== "undefined") {
      ro = new ResizeObserver(measure);
      ro.observe(svgRef.current);
    } else if (typeof window !== "undefined") {
      window.addEventListener("resize", measure);
      return () => window.removeEventListener("resize", measure);
    }
    return () => { if (ro) ro.disconnect(); };
  }, []);
  // mobileScale ≥ 1; scales up only when the SVG is rendered smaller
  // than its viewBox (i.e. on mobile / narrow embeds).
  const mobileScale = Math.max(1, 1100 / Math.max(1, renderedWidth));
  // Smooth easing — full bump only kicks in at significant downscale
  const ts = (n) => n * Math.min(mobileScale, 3.2);

  const activeSet = useMemo(() => new Set(activeIds || []), [activeIds]);
  const hasActive = activeSet.size > 0;

  // Each tier is an independent on/off; absence defaults to on.
  const tierOn = (t) => !visibleTiers || visibleTiers[t] !== false;
  const visibleEdges = EDGES.filter(e => tierOn(e.type) && (e.weight ?? 0.55) >= confidenceMin);

  const edgeColor = (type) => {
    if (type === "direct") return "var(--edge-direct)";
    if (type === "inferred") return "var(--edge-inferred)";
    return "var(--edge-predicted)";
  };
  const edgeDash = (type) => {
    if (type === "direct") return "0";
    if (type === "inferred") return "5 3";
    return "2 3";
  };

  const W = 1100, H = 680;
  const vbW = W / zoom;
  const vbH = H / zoom;
  const vbX = (W - vbW) / 2 - pan.x;
  const vbY = (H - vbH) / 2 - pan.y;

  // Domain band geometry — computed once per render
  const domainBands = useMemo(() => {
    const byDomain = {};
    NODES.forEach(n => {
      const arr = byDomain[n.kind] || (byDomain[n.kind] = []);
      arr.push(n);
    });
    return Object.entries(byDomain).map(([kind, nodes]) => {
      const k = ENTITY_KINDS[kind]; if (!k) return null;
      const xs = nodes.map(n => n.x), ys = nodes.map(n => n.y), rs = nodes.map(n => n.r);
      const minX = Math.min(...xs.map((x,i) => x - rs[i])) - 18;
      const maxX = Math.max(...xs.map((x,i) => x + rs[i])) + 18;
      const minY = Math.min(...ys.map((y,i) => y - rs[i])) - 30;
      const maxY = Math.max(...ys.map((y,i) => y + rs[i])) + 22;
      return { kind, k, nodes, minX, maxX, minY, maxY };
    }).filter(Boolean);
  }, [NODES, ENTITY_KINDS]);

  return React.createElement(
    React.Fragment, null,
    React.createElement("svg", {
      ref: svgRef,
      viewBox: `${vbX} ${vbY} ${vbW} ${vbH}`,
      preserveAspectRatio: "xMidYMid meet",
      className: "schema-svg",
    },
      // ── Domain bands — corner caps label only, no frame ───
      // Frames competed with the edge structure they were meant to organize.
      // The label alone, anchored top-left of each cluster, reads as an
      // architectural section header without adding scaffolding noise.
      React.createElement("g", { className: "domain-bands" },
        domainBands.map(({ kind, k, nodes, minX, minY }) => {
          const dim = hasActive && !nodes.some(n => activeSet.has(n.id));
          return React.createElement("g", {
            key: kind,
            opacity: dim ? 0.35 : 1,
            style: { transition: "opacity 240ms" },
          },
            React.createElement("text", {
              x: minX + 4, y: minY + 14,
              textAnchor: "start",
              fontSize: ts(9),
              fontWeight: 600,
              letterSpacing: "0.18em",
              fill: k.color,
              // Bumped in light mode — saturated brand colors at 70% on
              // cream canvas borderlined WCAG AA. 0.92 clears AA across
              // all six domain colors.
              fillOpacity: theme === "light" ? 0.92 : 0.7,
              style: { textTransform: "uppercase", fontFamily: "var(--font-display)" },
            }, k.label)
          );
        })
      ),

      // ── Edges — straight hairlines, no glow ──────────────
      React.createElement("g", { className: "edges" },
        visibleEdges.map((e, i) => {
          const s = NODES.find(n => n.id === e.s);
          const t = NODES.find(n => n.id === e.t);
          if (!s || !t) return null;
          if (s === t) return null; // skip self-loops in canvas; surface them in inspector
          const role = edgeRoleMap.get(`${e.s}|${e.t}|${e.label}`);
          const isAnchor = role === "anchor";
          const isEvidence = role === "evidence";
          const isInSubgraph = isAnchor || isEvidence;
          const inRoute = hasActive && activeSet.has(e.s) && activeSet.has(e.t);
          const isActive = inRoute || isInSubgraph;
          const isDim = hasActive && !isActive;
          const path = `M ${s.x} ${s.y} L ${t.x} ${t.y}`;
          let strokeDash = edgeDash(e.type);
          if (isAnchor) strokeDash = "0";
          else if (isEvidence) strokeDash = "4 3";
          const labelMid = activeQ && isInSubgraph;
          const mx = (s.x + t.x) / 2;
          const my = (s.y + t.y) / 2;
          // Edge hover tooltip — closes the provenance loop. The edge
          // path itself is too thin to be a reliable hit target, so we
          // render an invisible wider stroke ON TOP for the hover area.
          const edgeSource = (window.CIQ.SAMPLE_EDGE_SOURCES || {})[e.label] || null;
          const onEdgeEnter = (ev) => {
            const rect = svgRef.current.getBoundingClientRect();
            setTooltip({
              kind: "edge",
              edgeLabel: e.label,
              edgeType: e.type,
              edgeWeight: e.weight,
              edgeSource: edgeSource,
              edgeFrom: s.name,
              edgeTo: t.name,
              x: ev.clientX - rect.left + 12,
              y: ev.clientY - rect.top + 12,
            });
          };
          const onEdgeMove = (ev) => {
            if (!tooltip || tooltip.kind !== "edge") return;
            const rect = svgRef.current.getBoundingClientRect();
            setTooltip(prev => prev && ({ ...prev, x: ev.clientX - rect.left + 12, y: ev.clientY - rect.top + 12 }));
          };
          const onEdgeLeave = () => setTooltip(null);
          return React.createElement(React.Fragment, { key: i },
            React.createElement("path", {
              d: path,
              className: "edge",
              stroke: edgeColor(e.type),
              strokeWidth: ts(isAnchor ? 1.8 : (isEvidence ? 1.4 : (isActive ? 1.6 : 1))),
              strokeOpacity: isActive ? 0.95 : (isDim ? 0.05 : 0.4),
              strokeDasharray: strokeDash,
              fill: "none",
              strokeLinecap: "round",
              "data-active": isActive ? "true" : "false",
              "data-dim": isDim ? "true" : "false",
              pointerEvents: "none",
            }),
            // Invisible thicker hit target for hover — 8px wide so the
            // edge is reliably tappable / hoverable even at thin stroke.
            React.createElement("path", {
              d: path,
              fill: "none",
              stroke: "transparent",
              strokeWidth: ts(10),
              strokeLinecap: "round",
              style: { cursor: "help" },
              onMouseEnter: onEdgeEnter,
              onMouseMove: onEdgeMove,
              onMouseLeave: onEdgeLeave,
            }),
            labelMid && React.createElement("text", {
              x: mx, y: my - 4,
              textAnchor: "middle",
              fontSize: ts(8.5),
              fontFamily: "var(--font-mono)",
              fill: isAnchor ? "var(--text-1)" : "var(--text-2)",
              opacity: 0.9,
              style: { paintOrder: "stroke", stroke: "var(--canvas-bg)", strokeWidth: ts(3) },
            }, e.label)
          );
        })
      ),

      // ── Nodes — flat discs, hairline ring, no glow ───────
      React.createElement("g", { className: "nodes" },
        NODES.map(n => {
          const k = ENTITY_KINDS[n.kind];
          const isActive = hasActive && activeSet.has(n.id);
          const isSelected = selectedId === n.id;
          const isDim = hasActive && !isActive;
          const isHi = isActive || isSelected;
          // Selected node gets internal hierarchy: slightly larger radius
          // and an outer halo ring so it visually wins over its neighbors.
          // Active-but-not-selected stays at the resting radius.
          const baseR = Math.min(n.r * 0.72, 24);
          const r = isSelected ? baseR + 2 : baseR;
          return React.createElement("g", {
            key: n.id,
            className: "node-group",
            "data-active": isHi ? "true" : "false",
            "data-dim": isDim ? "true" : "false",
            "data-selected": isSelected ? "true" : "false",
            transform: `translate(${n.x} ${n.y})`,
            onMouseEnter: (ev) => {
              const rect = svgRef.current.getBoundingClientRect();
              setTooltip({
                kind: k.label,
                name: n.name,
                sub: n.sub,
                x: ev.clientX - rect.left + 12,
                y: ev.clientY - rect.top + 12,
              });
            },
            onMouseMove: (ev) => {
              if (!tooltip) return;
              const rect = svgRef.current.getBoundingClientRect();
              setTooltip(prev => prev && ({ ...prev, x: ev.clientX - rect.left + 12, y: ev.clientY - rect.top + 12 }));
            },
            onMouseLeave: () => setTooltip(null),
            onClick: () => onSelect && onSelect(n.id),
            style: { "--node-color": k.color, cursor: "pointer" },
          },
            // Outer halo ring — only on the selected node, two stops
            // wider than the prior single ring so the node clearly wins
            // even when it has 35 neighbors lit at full opacity.
            isSelected && React.createElement("circle", {
              r: r + 12,
              fill: "none",
              stroke: k.color,
              strokeWidth: ts(1),
              strokeOpacity: 0.25,
            }),
            isSelected && React.createElement("circle", {
              r: r + 6,
              fill: "none",
              stroke: k.color,
              strokeWidth: ts(1.4),
              strokeOpacity: 0.85,
            }),
            // Solid disc with subtle inner highlight. Stroke is white in
            // dark mode (glassy gloss on saturated discs) and a soft
            // dark/translucent in light mode (white would vanish on light-
            // theme node fills against the cream canvas).
            React.createElement("circle", {
              r,
              className: "node-disc",
              fill: k.color,
              fillOpacity: isSelected ? 1 : (isHi ? 0.95 : 0.85),
              stroke: theme === "light"
                ? (isSelected ? "rgba(15,23,42,0.55)"
                  : (isHi    ? "rgba(15,23,42,0.30)"
                             : "rgba(15,23,42,0.10)"))
                : (isSelected ? "rgba(255,255,255,0.7)"
                  : (isHi    ? "rgba(255,255,255,0.45)"
                             : "rgba(255,255,255,0.12)")),
              strokeWidth: ts(isSelected ? 1.4 : 1),
            }),
            // One-pixel top specular — restraint, not blob.
            React.createElement("circle", {
              r: r - 1,
              fill: "none",
              stroke: theme === "light" ? "rgba(255,255,255,0.55)" : "rgba(255,255,255,0.25)",
              strokeWidth: ts(0.6),
              transform: "translate(0, -0.5)",
              pointerEvents: "none",
            }),
            // Label below — capitals for category, name in sans.
            // Offset scales too so labels don't overlap discs at mobile.
            React.createElement("text", {
              className: "node-label",
              y: r + ts(14),
              textAnchor: "middle",
              fontSize: ts(isSelected ? 12 : 11),
              fontWeight: isSelected ? 600 : 500,
              fill: "var(--text-1)",
              fillOpacity: isDim ? 0.35 : 1,
              style: { fontFamily: "var(--font-sans)", letterSpacing: "0.005em" },
            }, n.name)
          );
        })
      )
    ),
    tooltip && tooltip.kind !== "edge" && React.createElement("div", {
      className: "graph-tooltip glass-3",
      "data-show": "true",
      style: { left: tooltip.x, top: tooltip.y },
    },
      React.createElement("div", { className: "tt-kind" }, tooltip.kind),
      React.createElement("div", null, React.createElement("strong", null, tooltip.name)),
      React.createElement("div", { style: { color: "var(--text-3)", fontSize: 10.5, marginTop: 2 } }, tooltip.sub)
    ),

    // Edge tooltip — closes the provenance loop. Shows edge name,
    // source database (when known), evidence tier, and confidence.
    tooltip && tooltip.kind === "edge" && React.createElement("div", {
      className: "graph-tooltip glass-3",
      "data-show": "true",
      style: { left: tooltip.x, top: tooltip.y, minWidth: 220 },
    },
      React.createElement("div", { className: "tt-kind" }, tooltip.edgeType),
      React.createElement("div", { style: { fontFamily: "var(--font-mono)", fontWeight: 600, color: "var(--text-1)" } },
        tooltip.edgeLabel
      ),
      React.createElement("div", { style: { fontSize: 10.5, color: "var(--text-3)", marginTop: 2 } },
        tooltip.edgeFrom + " → " + tooltip.edgeTo
      ),
      React.createElement("div", {
        style: {
          marginTop: 6, paddingTop: 6,
          borderTop: "1px solid var(--hairline)",
          display: "flex", alignItems: "center", gap: 8,
          fontSize: 10.5,
        },
      },
        tooltip.edgeSource
          ? React.createElement("span", {
              style: {
                padding: "1px 6px",
                borderRadius: 4,
                color: "var(--text-2)",
                background: "var(--glass-ground)",
                border: "1px solid var(--hairline)",
                fontFamily: "var(--font-mono)",
              },
            }, tooltip.edgeSource)
          : React.createElement("span", { style: { color: "var(--text-3)", fontStyle: "italic" } }, "Predicted (no source)"),
        React.createElement("span", { style: { marginLeft: "auto", color: "var(--text-3)", fontFamily: "var(--font-mono)" } },
          "conf ", tooltip.edgeWeight ? tooltip.edgeWeight.toFixed(2) : "—"
        )
      )
    )
  );
}

window.Graph = Graph;
