import { useEffect, useRef, useState } from "react";
import { Renderer } from "./renderer";
import { fetchGraphQueries } from "./graph_queries";
import type { GraphQueryMeta } from "./graph_queries";
import { fetchSelectionQueries, runSelectionQuery, runSelectionTripleQuery } from "./selection_queries";
import { cosmosRuntimeConfig } from "./cosmos_config";
import type { GraphMeta, SelectionQueryMeta, SelectionTriple } from "./selection_queries";
import { TripleGraphView } from "./TripleGraphView";
import { buildTripleGraphModel, type TripleGraphModel } from "./triple_graph";
import { readGraphArrow } from "./graph_arrow";

function sleep(ms: number): Promise<void> {
  return new Promise((r) => setTimeout(r, ms));
}

type GraphNodeLookup = {
  vertexIds: Uint32Array;
  labels: (string | undefined)[];
  iris: string[];
};

type TripleResultState = {
  status: "idle" | "loading" | "ready" | "error";
  queryId: string;
  selectedIds: number[];
  triples: SelectionTriple[];
  errorMessage?: string;
};

function idleTripleResult(queryId: string): TripleResultState {
  return {
    status: "idle",
    queryId,
    selectedIds: [],
    triples: [],
  };
}

function formatBytes(bytes: number): string {
  if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
  const units = ["B", "KB", "MB", "GB", "TB"];
  let value = bytes;
  let unitIndex = 0;
  while (value >= 1024 && unitIndex < units.length - 1) {
    value /= 1024;
    unitIndex++;
  }
  return `${value.toFixed(value >= 100 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
}

function estimateFrontendTypedArrayBytes(nodeCount: number, edgeCount: number, routeLineFloatCount: number): { app: number; renderer: number; total: number } {
  const app =
    nodeCount * Float32Array.BYTES_PER_ELEMENT * 2 +
    nodeCount * Uint32Array.BYTES_PER_ELEMENT +
    edgeCount * Uint32Array.BYTES_PER_ELEMENT * 2 +
    routeLineFloatCount * Float32Array.BYTES_PER_ELEMENT;

  const renderer =
    nodeCount * Float32Array.BYTES_PER_ELEMENT * 2 +
    nodeCount * Uint32Array.BYTES_PER_ELEMENT * 4 +
    edgeCount * Uint32Array.BYTES_PER_ELEMENT * 4;

  return {
    app,
    renderer,
    total: app + renderer,
  };
}

export default function App() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const rendererRef = useRef<Renderer | null>(null);
  const [status, setStatus] = useState("Waiting for backend…");
  const [nodeCount, setNodeCount] = useState(0);
  const [stats, setStats] = useState({
    fps: 0,
    drawn: 0,
    mode: "",
    zoom: 0,
    ptSize: 0,
  });
  const [error, setError] = useState("");
  const [hoveredNode, setHoveredNode] = useState<{ x: number; y: number; screenX: number; screenY: number; label?: string; iri?: string } | null>(null);
  const [selectedNodes, setSelectedNodes] = useState<Set<number>>(new Set());
  const [graphQueries, setGraphQueries] = useState<GraphQueryMeta[]>([]);
  const [activeGraphQueryId, setActiveGraphQueryId] = useState<string>("default");
  const [selectionQueries, setSelectionQueries] = useState<SelectionQueryMeta[]>([]);
  const [activeSelectionQueryId, setActiveSelectionQueryId] = useState<string>("neighbors");
  const [tripleResult, setTripleResult] = useState<TripleResultState>(() => idleTripleResult("neighbors"));
  const [tripleGraphModel, setTripleGraphModel] = useState<TripleGraphModel | null>(null);
  const [backendStats, setBackendStats] = useState<{ nodes: number; edges: number; backend?: string } | null>(null);
  const graphMetaRef = useRef<GraphMeta | null>(null);
  const selectionReqIdRef = useRef(0);
  const tripleReqIdRef = useRef(0);
  const graphInitializedRef = useRef(false);

  // Store mouse position in a ref so it can be accessed in render loop without re-renders
  const mousePos = useRef({ x: 0, y: 0 });
  const nodeLookupRef = useRef<GraphNodeLookup | null>(null);

  async function loadGraph(graphQueryId: string, signal: AbortSignal): Promise<void> {
    const renderer = rendererRef.current;
    if (!renderer) return;

    const loadStartedAt = performance.now();
    const logPhase = (phase: string, extra?: Record<string, unknown>) => {
      console.log(`[graph-load] ${phase}`, {
        elapsed_ms: Math.round(performance.now() - loadStartedAt),
        graph_query_id: graphQueryId,
        ...(extra || {}),
      });
    };

    const updateStatus = async (nextStatus: string, extra?: Record<string, unknown>): Promise<void> => {
      setStatus(nextStatus);
      logPhase(nextStatus, extra);
      await sleep(0);
    };

    await updateStatus("Fetching graph…");
    const graphRes = await fetch(`/api/graph?graph_query_id=${encodeURIComponent(graphQueryId)}`, { signal });
    logPhase("graph response headers received", {
      status: graphRes.status,
      content_type: graphRes.headers.get("content-type"),
      content_length: graphRes.headers.get("content-length"),
    });
    if (!graphRes.ok) {
      let detail = "";
      try {
        const err = await graphRes.json();
        if (err && typeof err === "object" && typeof (err as any).detail === "string") {
          detail = (err as any).detail;
        }
      } catch {
        // ignore
      }
      throw new Error(`Failed to fetch graph: ${graphRes.status}${detail ? ` (${detail})` : ""}`);
    }
    await updateStatus("Streaming Arrow graph…");
    await updateStatus("Decoding Arrow batches…");
    const decodeStartedAt = performance.now();
    let graph: Awaited<ReturnType<typeof readGraphArrow>>;
    try {
      graph = await readGraphArrow(graphRes, logPhase);
    } catch (e) {
      logPhase("arrow graph decode failed", {
        error: e instanceof Error ? e.message : String(e),
      });
      throw e;
    }
    logPhase("arrow graph decoded", {
      decode_ms: Math.round(performance.now() - decodeStartedAt),
    });
    if (signal.aborted) return;

    const meta = graph.meta || null;
    const vertexIds = graph.vertexIds;
    const xs = graph.xs;
    const ys = graph.ys;
    const edgeData = graph.edgeData;
    const routeLineVertices = graph.routeLineVertices;
    const labels = graph.labels;
    const iris = graph.iris;
    const count = vertexIds.length;
    const edgeCount = edgeData.length / 2;
    logPhase("graph payload ready", {
      nodes: count,
      edges: edgeCount,
      route_line_segments: routeLineVertices.length / 4,
      backend_nodes: meta && typeof meta.nodes === "number" ? meta.nodes : undefined,
      backend_edges: meta && typeof meta.edges === "number" ? meta.edges : undefined,
    });

    nodeLookupRef.current = { vertexIds, labels, iris };
    graphMetaRef.current = meta && typeof meta === "object" ? (meta as GraphMeta) : null;
    setTripleResult(idleTripleResult(activeSelectionQueryId));

    await updateStatus("Preparing buffers…", {
      nodes: count,
      edges: edgeCount,
    });
    const bufferPrepStartedAt = performance.now();
    const typedArrayBytes = estimateFrontendTypedArrayBytes(count, edgeCount, routeLineVertices.length);
    logPhase("buffer prep done", {
      buffer_prep_ms: Math.round(performance.now() - bufferPrepStartedAt),
      app_typed_arrays: formatBytes(typedArrayBytes.app),
      renderer_typed_arrays_estimate: formatBytes(typedArrayBytes.renderer),
      total_typed_arrays_estimate: formatBytes(typedArrayBytes.total),
    });

    // Use /api/graph meta; don't do a second expensive backend call.
    if (meta && typeof meta.nodes === "number" && typeof meta.edges === "number") {
      setBackendStats({
        nodes: meta.nodes,
        edges: meta.edges,
        backend: typeof meta.backend === "string" ? meta.backend : undefined,
      });
    } else {
      setBackendStats({ nodes: count, edges: edgeCount });
    }

    await updateStatus("Building spatial index…", {
      nodes: count,
      edges: edgeCount,
    });
    if (signal.aborted) return;

    let buildMs: number;
    try {
      buildMs = renderer.init(
        xs,
        ys,
        vertexIds,
        edgeData,
        routeLineVertices.length > 0 ? routeLineVertices : null
      );
    } catch (e) {
      logPhase("renderer.init failed", {
        error: e instanceof Error ? e.message : String(e),
      });
      throw e;
    }
    setNodeCount(renderer.getNodeCount());
    setSelectedNodes(new Set());
    setStatus("");
      logPhase("init complete", {
        renderer_init_ms: Math.round(buildMs),
        nodes: count,
        edges: edgeCount,
      });
  }

  function getSelectedIds(renderer: Renderer, selected: Set<number>): number[] {
    const lookup = nodeLookupRef.current;
    if (!lookup) return [];
    const selectedIds: number[] = [];
    for (const sortedIdx of selected) {
      const origIdx = renderer.sortedIndexToOriginalIndex(sortedIdx);
      if (origIdx === null) continue;
      const nodeId = lookup.vertexIds[origIdx];
      if (typeof nodeId !== "number") continue;
      selectedIds.push(nodeId);
    }
    return selectedIds;
  }

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    let renderer: Renderer;
    try {
      renderer = new Renderer(canvas);
      rendererRef.current = renderer;
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      return;
    }

    let cancelled = false;
    const initCtrl = new AbortController();
    graphInitializedRef.current = false;

    (async () => {
      try {
        // Wait for backend (docker-compose also gates startup via healthcheck, but this
        // handles running the frontend standalone).
        const deadline = performance.now() + 180_000;
        let attempt = 0;
        while (performance.now() < deadline) {
          attempt++;
          setStatus(`Waiting for backend… (attempt ${attempt})`);
          try {
            const res = await fetch("/api/health");
            if (res.ok) break;
          } catch {
            // ignore and retry
          }
          await sleep(1000);
          if (cancelled) return;
        }

        let graphQueryToLoad = activeGraphQueryId;
        try {
          setStatus("Fetching graph modes…");
          const gqs = await fetchGraphQueries(initCtrl.signal);
          if (cancelled || initCtrl.signal.aborted) return;
          setGraphQueries(gqs);
          graphQueryToLoad = gqs.some((q) => q.id === graphQueryToLoad) ? graphQueryToLoad : (gqs[0]?.id ?? "default");
          setActiveGraphQueryId(graphQueryToLoad);
        } catch {
          if (cancelled || initCtrl.signal.aborted) return;
          setGraphQueries([{ id: "default", label: "Default" }]);
          graphQueryToLoad = "default";
          setActiveGraphQueryId("default");
        }

        await loadGraph(graphQueryToLoad, initCtrl.signal);
        if (cancelled || initCtrl.signal.aborted) return;

        try {
          const qs = await fetchSelectionQueries(initCtrl.signal);
          if (cancelled) return;
          setSelectionQueries(qs);
          setActiveSelectionQueryId((prev) => (qs.length > 0 && !qs.some((q) => q.id === prev) ? qs[0].id : prev));
        } catch {
          if (cancelled) return;
          setSelectionQueries([{ id: "neighbors", label: "Neighbors" }]);
          setActiveSelectionQueryId((prev) => (prev ? prev : "neighbors"));
        }

        graphInitializedRef.current = true;
      } catch (e) {
        if (!cancelled) {
          setError(e instanceof Error ? e.message : String(e));
        }
      }
    })();

    // Input handling
    let dragging = false;
    let didDrag = false;
    let downX = 0;
    let downY = 0;
    let lastX = 0;
    let lastY = 0;
    const DRAG_THRESHOLD = 5;

    const onDown = (e: MouseEvent) => {
      dragging = true;
      didDrag = false;
      downX = e.clientX;
      downY = e.clientY;
      lastX = e.clientX;
      lastY = e.clientY;
    };
    const onMove = (e: MouseEvent) => {
      mousePos.current = { x: e.clientX, y: e.clientY };
      if (!dragging) return;

      const dx = e.clientX - downX;
      const dy = e.clientY - downY;
      if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) {
        didDrag = true;
      }

      renderer.pan(e.clientX - lastX, e.clientY - lastY);
      lastX = e.clientX;
      lastY = e.clientY;
    };
    const onUp = (e: MouseEvent) => {
      if (dragging && !didDrag) {
        const node = renderer.findNodeIndexAt(e.clientX, e.clientY);
        if (node) {
          setSelectedNodes((prev: Set<number>) => {
            const next = new Set(prev);
            if (next.has(node.index)) {
              next.delete(node.index);
            } else {
              next.add(node.index);
            }
            return next;
          });
        }
      }
      dragging = false;
      didDrag = false;
    };
    const onWheel = (e: WheelEvent) => {
      e.preventDefault();
      const factor = e.deltaY > 0 ? 0.9 : 1 / 0.9;
      renderer.zoomAt(factor, e.clientX, e.clientY);
    };
    const onMouseLeave = () => {
      setHoveredNode(null);
    };

    canvas.addEventListener("mousedown", onDown);
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    canvas.addEventListener("wheel", onWheel, { passive: false });
    canvas.addEventListener("mouseleave", onMouseLeave);

    // Render loop
    let frameCount = 0;
    let lastTime = performance.now();
    let raf = 0;

    const frame = () => {
      const result = renderer.render();
      frameCount++;

      const hit = renderer.findNodeIndexAt(mousePos.current.x, mousePos.current.y);
      if (hit) {
        const origIdx = renderer.sortedIndexToOriginalIndex(hit.index);
        const lookup = nodeLookupRef.current;
        const label = origIdx === null || !lookup ? undefined : lookup.labels[origIdx];
        const iri = origIdx === null || !lookup ? undefined : lookup.iris[origIdx];
        setHoveredNode({
          x: hit.x,
          y: hit.y,
          screenX: mousePos.current.x,
          screenY: mousePos.current.y,
          label: typeof label === "string" ? label : undefined,
          iri: typeof iri === "string" ? iri : undefined,
        });
      } else {
        setHoveredNode(null);
      }

      const now = performance.now();
      if (now - lastTime >= 500) {
        const fps = (frameCount / (now - lastTime)) * 1000;
        setStats({
          fps: Math.round(fps),
          drawn: result.drawnCount,
          mode: result.mode,
          zoom: result.zoom,
          ptSize: result.ptSize,
        });
        frameCount = 0;
        lastTime = now;
      }

      raf = requestAnimationFrame(frame);
    };
    raf = requestAnimationFrame(frame);

    return () => {
      cancelled = true;
      initCtrl.abort();
      cancelAnimationFrame(raf);
      canvas.removeEventListener("mousedown", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      canvas.removeEventListener("wheel", onWheel);
      canvas.removeEventListener("mouseleave", onMouseLeave);
    };
  }, []);

  // Reload graph when the graph query mode changes (after initial load)
  useEffect(() => {
    if (!graphInitializedRef.current) return;
    const renderer = rendererRef.current;
    if (!renderer) return;
    if (!activeGraphQueryId) return;

    const ctrl = new AbortController();
    (async () => {
      try {
        await loadGraph(activeGraphQueryId, ctrl.signal);
      } catch (e) {
        if (ctrl.signal.aborted) return;
        setError(e instanceof Error ? e.message : String(e));
      }
    })();

    return () => ctrl.abort();
  }, [activeGraphQueryId]);

  // Left-side selection highlighting path
  useEffect(() => {
    const renderer = rendererRef.current;
    if (!renderer) return;

    renderer.updateSelection(selectedNodes, new Set());
    const reqId = ++selectionReqIdRef.current;
    const selectedIds = getSelectedIds(renderer, selectedNodes);
    const queryId = (activeSelectionQueryId || selectionQueries[0]?.id || "neighbors").trim();

    if (selectedIds.length === 0) {
      return;
    }

    const ctrl = new AbortController();

    (async () => {
      try {
        const result = await runSelectionQuery(queryId, selectedIds, graphMetaRef.current, ctrl.signal);
        if (ctrl.signal.aborted) return;
        if (reqId !== selectionReqIdRef.current) return;

        const neighborSorted = new Set<number>();
        for (const id of result.neighborIds) {
          if (typeof id !== "number") continue;
          const sorted = renderer.vertexIdToSortedIndexOrNull(id);
          if (sorted === null) continue;
          if (!selectedNodes.has(sorted)) neighborSorted.add(sorted);
        }

        renderer.updateSelection(selectedNodes, neighborSorted);
      } catch (e) {
        if (ctrl.signal.aborted) return;
        if (reqId !== selectionReqIdRef.current) return;
        console.warn(e);
        renderer.updateSelection(selectedNodes, new Set());
      }
    })();

    return () => ctrl.abort();
  }, [selectedNodes, activeSelectionQueryId]);

  // Right-side triple graph path
  useEffect(() => {
    const renderer = rendererRef.current;
    if (!renderer) return;

    const reqId = ++tripleReqIdRef.current;
    const selectedIds = getSelectedIds(renderer, selectedNodes);
    const queryId = (activeSelectionQueryId || selectionQueries[0]?.id || "neighbors").trim();

    if (selectedIds.length === 0) {
      setTripleResult(idleTripleResult(queryId));
      return;
    }

    const ctrl = new AbortController();
    setTripleResult({
      status: "loading",
      queryId,
      selectedIds,
      triples: [],
    });

    (async () => {
      try {
        const result = await runSelectionTripleQuery(queryId, selectedIds, graphMetaRef.current, ctrl.signal);
        if (ctrl.signal.aborted) return;
        if (reqId !== tripleReqIdRef.current) return;

        setTripleResult({
          status: "ready",
          queryId: result.queryId,
          selectedIds: result.selectedIds,
          triples: result.triples,
        });
      } catch (e) {
        if (ctrl.signal.aborted) return;
        if (reqId !== tripleReqIdRef.current) return;
        console.warn(e);
        setTripleResult({
          status: "error",
          queryId,
          selectedIds,
          triples: [],
          errorMessage: e instanceof Error ? e.message : String(e),
        });
      }
    })();

    return () => ctrl.abort();
  }, [selectedNodes, activeSelectionQueryId]);

  useEffect(() => {
    if (tripleResult.status !== "ready") {
      setTripleGraphModel(null);
      return;
    }
    setTripleGraphModel(buildTripleGraphModel(tripleResult.triples, tripleResult.selectedIds));
  }, [tripleResult]);

  const resultQueryId = (tripleResult.queryId || activeSelectionQueryId || selectionQueries[0]?.id || "neighbors").trim();
  const resultQueryLabel = selectionQueries.find((q) => q.id === resultQueryId)?.label ?? resultQueryId;

  return (
    <div style={{ width: "100vw", height: "100vh", display: "flex", overflow: "hidden", background: "#000" }}>
      <div style={{ position: "relative", flex: "1 1 50%", minWidth: 0, background: "#000" }}>
        <canvas
          ref={canvasRef}
          style={{ display: "block", width: "100%", height: "100%" }}
        />

        {status && (
          <div
            style={{
              position: "absolute",
              inset: 0,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              background: "rgba(0,0,0,0.9)",
              color: "#0f0",
              fontFamily: "monospace",
              fontSize: "16px",
            }}
          >
            {status}
          </div>
        )}

        {error && (
          <div
            style={{
              position: "absolute",
              inset: 0,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              background: "rgba(0,0,0,0.9)",
              color: "#f44",
              fontFamily: "monospace",
              fontSize: "16px",
            }}
          >
            Error: {error}
          </div>
        )}

        {!status && !error && (
          <>
            <div
              style={{
                position: "absolute",
                top: 10,
                left: 10,
                background: "rgba(0,0,0,0.75)",
                color: "#0f0",
                fontFamily: "monospace",
                padding: "8px 12px",
                fontSize: "12px",
                lineHeight: "1.6",
                borderRadius: "4px",
                pointerEvents: "none",
              }}
            >
              <div>FPS: {stats.fps}</div>
              <div>Drawn: {stats.drawn.toLocaleString()} / {nodeCount.toLocaleString()}</div>
              <div>Mode: {stats.mode}</div>
              <div>Zoom: {stats.zoom < 0.01 ? stats.zoom.toExponential(2) : stats.zoom.toFixed(2)} px/unit</div>
              <div>Pt Size: {stats.ptSize.toFixed(1)}px</div>
              <div style={{ color: "#f80" }}>Selected: {selectedNodes.size}</div>
              {backendStats && (
                <div style={{ color: "#8f8" }}>
                  Backend{backendStats.backend ? ` (${backendStats.backend})` : ""}: {backendStats.nodes.toLocaleString()} nodes, {backendStats.edges.toLocaleString()} edges
                </div>
              )}
            </div>
            <div
              style={{
                position: "absolute",
                bottom: 10,
                left: 10,
                background: "rgba(0,0,0,0.75)",
                color: "#888",
                fontFamily: "monospace",
                padding: "6px 10px",
                fontSize: "11px",
                borderRadius: "4px",
                pointerEvents: "none",
              }}
            >
              Drag to pan · Scroll to zoom · Click to select
            </div>

            {selectionQueries.length > 0 && (
              <div
                style={{
                  position: "absolute",
                  top: 10,
                  right: 10,
                  display: "flex",
                  flexDirection: "column",
                  gap: "6px",
                  background: "rgba(0,0,0,0.55)",
                  padding: "8px",
                  borderRadius: "6px",
                  border: "1px solid rgba(255,255,255,0.08)",
                  pointerEvents: "auto",
                }}
              >
                {selectionQueries.map((q) => {
                  const active = q.id === activeSelectionQueryId;
                  return (
                    <button
                      key={q.id}
                      onClick={() => setActiveSelectionQueryId(q.id)}
                      style={{
                        cursor: "pointer",
                        fontFamily: "monospace",
                        fontSize: "12px",
                        padding: "6px 10px",
                        borderRadius: "4px",
                        border: active ? "1px solid rgba(0,255,255,0.8)" : "1px solid rgba(255,255,255,0.12)",
                        background: active ? "rgba(0,255,255,0.12)" : "rgba(255,255,255,0.04)",
                        color: active ? "#0ff" : "#bbb",
                        textAlign: "left",
                      }}
                      aria-pressed={active}
                    >
                      {q.label}
                    </button>
                  );
                })}
              </div>
            )}

            {graphQueries.length > 0 && (
              <div
                style={{
                  position: "absolute",
                  bottom: 10,
                  right: 10,
                  display: "flex",
                  flexDirection: "column",
                  gap: "6px",
                  background: "rgba(0,0,0,0.55)",
                  padding: "8px",
                  borderRadius: "6px",
                  border: "1px solid rgba(255,255,255,0.08)",
                  pointerEvents: "auto",
                }}
              >
                {graphQueries.map((q) => {
                  const active = q.id === activeGraphQueryId;
                  return (
                    <button
                      key={q.id}
                      onClick={() => setActiveGraphQueryId(q.id)}
                      style={{
                        cursor: "pointer",
                        fontFamily: "monospace",
                        fontSize: "12px",
                        padding: "6px 10px",
                        borderRadius: "4px",
                        border: active ? "1px solid rgba(0,255,0,0.8)" : "1px solid rgba(255,255,255,0.12)",
                        background: active ? "rgba(0,255,0,0.12)" : "rgba(255,255,255,0.04)",
                        color: active ? "#8f8" : "#bbb",
                        textAlign: "left",
                      }}
                      aria-pressed={active}
                    >
                      {q.label}
                    </button>
                  );
                })}
              </div>
            )}

            {hoveredNode && (
              <div
                style={{
                  position: "absolute",
                  left: hoveredNode.screenX + 15,
                  top: hoveredNode.screenY + 15,
                  background: "rgba(0,0,0,0.85)",
                  color: "#0ff",
                  fontFamily: "monospace",
                  padding: "6px 10px",
                  fontSize: "12px",
                  borderRadius: "4px",
                  pointerEvents: "none",
                  whiteSpace: "nowrap",
                  border: "1px solid rgba(0,255,255,0.3)",
                  boxShadow: "0 2px 8px rgba(0,0,0,0.5)",
                }}
              >
                <div style={{ color: "#0ff" }}>
                  {hoveredNode.label || hoveredNode.iri || "(unknown)"}
                </div>
                <div style={{ color: "#688" }}>
                  ({hoveredNode.x.toFixed(2)}, {hoveredNode.y.toFixed(2)})
                </div>
              </div>
            )}
          </>
        )}
      </div>

      <div
        style={{
          flex: "1 1 50%",
          minWidth: 0,
          display: "flex",
          flexDirection: "column",
          background: "#050505",
          borderLeft: "1px solid rgba(255,255,255,0.08)",
        }}
      >
        <div
          style={{
            padding: "18px 20px 14px",
            borderBottom: "1px solid rgba(255,255,255,0.08)",
            background: "rgba(255,255,255,0.02)",
            fontFamily: "monospace",
          }}
        >
          <div style={{ color: "#688", fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.08em" }}>
            {resultQueryLabel}
          </div>
          <div style={{ marginTop: "6px", color: "#eee", fontSize: "16px" }}>Selection Graph</div>
          <div style={{ marginTop: "8px", color: "#8ab", fontSize: "12px" }}>
            Nodes: {(tripleGraphModel?.nodeCount ?? 0).toLocaleString()} · Edges: {(tripleGraphModel?.edgeCount ?? 0).toLocaleString()}
          </div>
          <div style={{ marginTop: "4px", color: "#6f8b98", fontSize: "11px" }}>
            Layout: {cosmosRuntimeConfig.enableSimulation ? "force-directed" : "static"} · Camera: static · Center force: {cosmosRuntimeConfig.simulationCenter} · Repulsion: {cosmosRuntimeConfig.simulationRepulsion} · Link spring: {cosmosRuntimeConfig.simulationLinkSpring} · Friction: {cosmosRuntimeConfig.simulationFriction}
          </div>
        </div>

        <div
          style={{
            flex: 1,
            minHeight: 0,
            fontFamily: "monospace",
            position: "relative",
            display: "flex",
            flexDirection: "column",
          }}
        >
          {tripleResult.status === "idle" && (
            <div
              style={{
                color: "#8a8a8a",
                fontSize: "13px",
                lineHeight: 1.6,
                padding: "14px",
              }}
            >
              Select nodes on the left to view returned triples
            </div>
          )}

          {tripleResult.status === "loading" && (
            <div
              style={{
                color: "#8ab",
                fontSize: "13px",
                lineHeight: 1.6,
                padding: "14px",
              }}
            >
              Running triple query…
            </div>
          )}

          {tripleResult.status === "error" && (
            <div
              style={{
                color: "#f88",
                fontSize: "13px",
                lineHeight: 1.6,
                padding: "14px",
              }}
            >
              {tripleResult.errorMessage || "Triple query failed"}
            </div>
          )}

          {tripleResult.status === "ready" && (!tripleGraphModel || tripleGraphModel.edgeCount === 0) && (
            <div
              style={{
                color: "#8a8a8a",
                fontSize: "13px",
                lineHeight: 1.6,
                padding: "14px",
              }}
            >
              No returned graph
            </div>
          )}

          {tripleResult.status === "ready" && tripleGraphModel && tripleGraphModel.edgeCount > 0 && (
            <TripleGraphView model={tripleGraphModel} />
          )}
        </div>
      </div>
    </div>
  );
}
