import { useEffect, useRef, useState } from "react";
import { Renderer } from "./renderer";

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

type GraphMeta = {
  backend?: string;
  ttl_path?: string | null;
  sparql_endpoint?: string | null;
  include_bnodes?: boolean;
  node_limit?: number;
  edge_limit?: number;
  nodes?: number;
  edges?: number;
};

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

  // 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[]>([]);

  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;

    (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;
        }

        setStatus("Fetching graph…");
        const graphRes = await fetch("/api/graph");
        if (!graphRes.ok) throw new Error(`Failed to fetch graph: ${graphRes.status}`);
        const graph = await graphRes.json();
        if (cancelled) 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));

        const buildMs = renderer.init(xs, ys, vertexIds, edgeData);
        setNodeCount(renderer.getNodeCount());
        setStatus("");
        console.log(`Init complete: ${count.toLocaleString()} nodes, ${edges.length.toLocaleString()} edges in ${buildMs.toFixed(0)}ms`);
      } 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;
      cancelAnimationFrame(raf);
      canvas.removeEventListener("mousedown", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      canvas.removeEventListener("wheel", onWheel);
      canvas.removeEventListener("mouseleave", onMouseLeave);
    };
  }, []);

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

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

    // Invalidate any in-flight neighbor request for the previous selection.
    const reqId = ++neighborsReqIdRef.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 nodeId = nodesRef.current?.[origIdx]?.id;
      if (typeof nodeId === "number") selectedIds.push(nodeId);
    }

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

    // Always send the full current selection list; backend returns the merged neighbor set.
    const ctrl = new AbortController();

    (async () => {
      try {
        const meta = graphMetaRef.current;
        const body = {
          selected_ids: selectedIds,
          node_limit: typeof meta?.node_limit === "number" ? meta.node_limit : undefined,
          edge_limit: typeof meta?.edge_limit === "number" ? meta.edge_limit : undefined,
        };

        const res = await fetch("/api/neighbors", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify(body),
          signal: ctrl.signal,
        });
        if (!res.ok) throw new Error(`POST /api/neighbors failed: ${res.status}`);
        const data = await res.json();
        if (ctrl.signal.aborted) return;
        if (reqId !== neighborsReqIdRef.current) return;

        const neighborIds: unknown = data?.neighbor_ids;
        const neighborSorted = new Set<number>();
        if (Array.isArray(neighborIds)) {
          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]);

  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>

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