import { useEffect, useRef, useState } from "react";
import { Renderer } from "./renderer";
import { fetchGraphQueries } from "./graph_queries";
import type { GraphQueryMeta } from "./graph_queries";
import { fetchSelectionQueries, runSelectionQuery } from "./selection_queries";
import type { GraphMeta, SelectionQueryMeta } from "./selection_queries";

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

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 [backendStats, setBackendStats] = useState<{ nodes: number; edges: number; backend?: string } | null>(null);
  const graphMetaRef = useRef<GraphMeta | null>(null);
  const selectionReqIdRef = 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 nodesRef = useRef<any[]>([]);

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

    setStatus("Fetching graph…");
    const graphRes = await fetch(`/api/graph?graph_query_id=${encodeURIComponent(graphQueryId)}`, { signal });
    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})` : ""}`);
    }
    const graph = await graphRes.json();
    if (signal.aborted) return;

    const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
    const edges = Array.isArray(graph.edges) ? graph.edges : [];
    const meta = graph.meta || null;
    const count = nodes.length;

    nodesRef.current = nodes;
    graphMetaRef.current = meta && typeof meta === "object" ? (meta as GraphMeta) : null;

    // Build positions from backend-provided node coordinates.
    setStatus("Preparing buffers…");
    const xs = new Float32Array(count);
    const ys = new Float32Array(count);
    for (let i = 0; i < count; i++) {
      const nx = nodes[i]?.x;
      const ny = nodes[i]?.y;
      xs[i] = typeof nx === "number" ? nx : 0;
      ys[i] = typeof ny === "number" ? ny : 0;
    }
    const vertexIds = new Uint32Array(count);
    for (let i = 0; i < count; i++) {
      const id = nodes[i]?.id;
      vertexIds[i] = typeof id === "number" ? id >>> 0 : i;
    }

    // Build edges as vertex-id pairs.
    const edgeData = new Uint32Array(edges.length * 2);
    for (let i = 0; i < edges.length; i++) {
      const s = edges[i]?.source;
      const t = edges[i]?.target;
      edgeData[i * 2] = typeof s === "number" ? s >>> 0 : 0;
      edgeData[i * 2 + 1] = typeof t === "number" ? t >>> 0 : 0;
    }

    // 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: nodes.length, edges: edges.length });
    }

    setStatus("Building spatial index…");
    await new Promise((r) => setTimeout(r, 0));
    if (signal.aborted) return;

    const buildMs = renderer.init(xs, ys, vertexIds, edgeData);
    setNodeCount(renderer.getNodeCount());
    setSelectedNodes(new Set());
    setStatus("");
    console.log(`Init complete: ${count.toLocaleString()} nodes, ${edges.length.toLocaleString()} edges in ${buildMs.toFixed(0)}ms`);
  }

  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; // true if mouse moved significantly during drag
    let downX = 0;
    let downY = 0;
    let lastX = 0;
    let lastY = 0;
    const DRAG_THRESHOLD = 5; // pixels

    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;

      // Check if we've moved enough to consider it a drag
      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) {
        // This was a click, not a drag - handle selection
        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); // Deselect if already selected
            } else {
              next.add(node.index); // Select
            }
            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++;

      // Find hovered node using quadtree
      const hit = renderer.findNodeIndexAt(mousePos.current.x, mousePos.current.y);
      if (hit) {
        const origIdx = renderer.sortedIndexToOriginalIndex(hit.index);
        const meta = origIdx === null ? null : nodesRef.current[origIdx];
        setHoveredNode({
          x: hit.x,
          y: hit.y,
          screenX: mousePos.current.x,
          screenY: mousePos.current.y,
          label: meta && typeof meta.label === "string" ? meta.label : undefined,
          iri: meta && typeof meta.iri === "string" ? meta.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]);

  // Sync selection state to renderer
  useEffect(() => {
    const renderer = rendererRef.current;
    if (!renderer) return;

    // Optimistically reflect selection immediately; highlights will be filled in by backend.
    renderer.updateSelection(selectedNodes, new Set());

    // Invalidate any in-flight request for the previous selection/mode.
    const reqId = ++selectionReqIdRef.current;

    // Convert selected sorted indices to backend node IDs (graph-export dense IDs).
    const selectedIds: number[] = [];
    for (const sortedIdx of selectedNodes) {
      const origIdx = renderer.sortedIndexToOriginalIndex(sortedIdx);
      if (origIdx === null) continue;
      const n = nodesRef.current?.[origIdx];
      const nodeId = n?.id;
      if (typeof nodeId !== "number") continue;
      selectedIds.push(nodeId);
    }

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

    const queryId = (activeSelectionQueryId || selectionQueries[0]?.id || "neighbors").trim();

    const ctrl = new AbortController();

    (async () => {
      try {
        const neighborIds = 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 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;
        console.warn(e);
        // Keep the UI usable even if neighbors fail to load.
        renderer.updateSelection(selectedNodes, new Set());
      }
    })();

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

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

      {/* Loading overlay */}
      {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 overlay */}
      {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>
      )}

      {/* HUD */}
      {!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>

          {/* Selection query buttons */}
          {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>
          )}

          {/* Graph query buttons */}
          {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>
          )}

          {/* Hover tooltip */}
          {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>
  );
}
