import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Graph } from "@cosmos.gl/graph";
import { subscribeBrowseNodes, fetchRemotePage, type NetworkNode } from "@/api/client";
import { renderMicron } from "@/components/editor/micronRenderer";
import { useWindowManager } from "@/hooks/useWindowManager";
import { getThemeStatusColors } from "@/components/browse/graphColors";
import { buildGraphArrays, SPACE_SIZE } from "@/components/browse/buildGraph";
import type { BrowseWinData, HistoryEntry } from "@/components/browse/types";
import BrowseNodeWindow from "@/components/browse/BrowseNodeWindow";
import BrowseSearchBar from "@/components/browse/BrowseSearchBar";
import Loader from "@/components/shared/Loader";

function cssVarToHex(varName: string): string {
  const raw = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
  if (!raw) return "#888888";
  const ctx = document.createElement("canvas").getContext("2d")!;
  ctx.fillStyle = raw;
  return ctx.fillStyle;
}

// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------

export default function BrowseView() {
  const [nodes, setNodes] = useState<NetworkNode[]>([]);
  const [filter, setFilter] = useState("");
  const { windows, focusedId: focusedWinId, open: openWindow, update: updateWindow, close: closeWindowById, focus: focusWindow } = useWindowManager<BrowseWinData>();
  const [hoveredLabel, setHoveredLabel] = useState<{ text: string; x: number; y: number } | null>(null);

  const [themeRev, setThemeRev] = useState(0);

  // Watch for theme changes (class on <html>)
  useEffect(() => {
    const observer = new MutationObserver(() => setThemeRev(r => r + 1));
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
    return () => observer.disconnect();
  }, []);

  const containerRef = useRef<HTMLDivElement>(null);
  const graphRef = useRef<Graph | null>(null);
  const entriesRef = useRef<NetworkNode[]>([]);
  const nodesMapRef = useRef<Map<string, NetworkNode>>(new Map());
  const hashToIndexRef = useRef<Map<string, number>>(new Map());
  const clusterNamesRef = useRef<string[]>([]);
  const hasUserInteractedRef = useRef(false);
  const initialFitDoneRef = useRef(false);

  // Build graph data, preserving existing positions
  const graphData = useMemo(() => {
    const graph = graphRef.current;
    const prevPositions = graph ? graph.getPointPositions() : [];
    return buildGraphArrays(nodes, hashToIndexRef.current, prevPositions, getThemeStatusColors());
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [nodes, themeRev]);

  useEffect(() => {
    const map = new Map<string, NetworkNode>();
    for (const n of nodes) map.set(n.hash, n);
    nodesMapRef.current = map;
    entriesRef.current = graphData.entries;
    hashToIndexRef.current = graphData.hashToIndex;
    clusterNamesRef.current = graphData.clusterNames;
  }, [nodes, graphData]);

  // Search — matches + autocomplete suggestions
  const searchMatchIndices = useMemo(() => {
    if (!filter.trim()) return null;
    const q = filter.trim().toLowerCase();
    const indices: number[] = [];
    graphData.entries.forEach((entry, i) => {
      if (entry.name.toLowerCase().includes(q)) indices.push(i);
    });
    return indices.length > 0 ? indices : null;
  }, [filter, graphData]);

  const suggestions = useMemo(() => {
    if (!filter.trim()) return [];
    const q = filter.trim().toLowerCase();
    return graphData.entries
      .filter(e => e.name.toLowerCase().includes(q))
      .slice(0, 8);
  }, [filter, graphData]);

  // ── Cluster labels — direct DOM like the example's create-cluster-labels.ts ──
  const labelDivsRef = useRef<HTMLDivElement[]>([]);
  const updateClusterLabels = useCallback(() => {
    const graph = graphRef.current;
    const container = containerRef.current;
    if (!graph || !container) return;

    const positions = graph.getClusterPositions();
    const names = clusterNamesRef.current;
    const nClusters = Math.min(names.length, positions.length / 2);

    // Rebuild label divs if count changed
    if (labelDivsRef.current.length !== nClusters) {
      labelDivsRef.current.forEach(d => d.remove());
      labelDivsRef.current = [];
      for (let i = 0; i < nClusters; i++) {
        const div = document.createElement("div");
        div.style.position = "absolute";
        div.style.pointerEvents = "none";
        div.style.whiteSpace = "nowrap";
        div.style.transform = "translate(-50%, -100%)";
        div.style.padding = "0";
        div.style.background = "none";
        div.style.border = "none";
        div.style.color = "var(--foreground)";
        div.style.fontFamily = "JetBrains Mono, monospace";
        div.style.fontSize = "14px";
        div.style.fontWeight = "700";
        div.style.letterSpacing = "0.05em";
        div.style.textTransform = "uppercase";
        div.style.opacity = "0.85";
        div.textContent = names[i] ?? "";
        container.appendChild(div);
        labelDivsRef.current.push(div);
      }
    }

    const rect = container.getBoundingClientRect();
    const pad = 8;

    for (let i = 0; i < nClusters; i++) {
      const x = positions[i * 2];
      const y = positions[i * 2 + 1];
      if (x === undefined || y === undefined) continue;
      const screen = graph.spaceToScreenPosition([x, y]);
      const div = labelDivsRef.current[i]!;

      // Measure label so we can clamp within container
      const lw = div.offsetWidth;
      const lh = div.offsetHeight;
      // Default transform is translate(-50%, -100%), so anchor is center-bottom
      let left = screen[0] - lw / 2;
      let top = screen[1] - lh;
      // Clamp to container bounds
      left = Math.max(pad, Math.min(left, rect.width - lw - pad));
      top = Math.max(pad, Math.min(top, rect.height - lh - pad));

      div.style.transform = "none";
      div.style.left = `${left}px`;
      div.style.top = `${top}px`;
    }
  }, []);

  // ── Search highlighting labels ──
  const searchLabelDivsRef = useRef<Map<number, HTMLDivElement>>(new Map());
  const searchIndicesRef = useRef<number[] | null>(null);

  const clearSearchLabels = useCallback(() => {
    searchLabelDivsRef.current.forEach(d => d.remove());
    searchLabelDivsRef.current.clear();
    searchIndicesRef.current = null;
  }, []);

  const updateSearchLabels = useCallback(() => {
    const graph = graphRef.current;
    const indices = searchIndicesRef.current;
    if (!graph || !indices || indices.length === 0) return;

    const positions = graph.getPointPositions();
    const entries = entriesRef.current;
    const containerEl = containerRef.current;
    if (!containerEl) return;
    const rect = containerEl.getBoundingClientRect();

    for (const idx of indices) {
      const x = positions[idx * 2];
      const y = positions[idx * 2 + 1];
      if (x === undefined || y === undefined) continue;
      const screen = graph.spaceToScreenPosition([x, y]);

      let div = searchLabelDivsRef.current.get(idx);
      if (!div) {
        div = document.createElement("div");
        div.style.position = "fixed";
        div.style.pointerEvents = "none";
        div.style.padding = "2px 8px";
        div.style.borderRadius = "4px";
        div.style.background = "var(--popover)";
        div.style.border = "1px solid var(--primary)";
        div.style.color = "var(--primary)";
        div.style.fontFamily = "JetBrains Mono, monospace";
        div.style.fontWeight = "bold";
        div.style.fontSize = "11px";
        div.style.boxShadow = "0 2px 8px rgba(0,0,0,0.4)";
        div.style.whiteSpace = "nowrap";
        div.style.zIndex = "998";
        div.textContent = entries[idx]?.name ?? "";
        document.body.appendChild(div);
        searchLabelDivsRef.current.set(idx, div);
      }
      div.style.left = `${rect.left + screen[0] + 10}px`;
      div.style.top = `${rect.top + screen[1] - 8}px`;
    }
  }, []);

  // ── Init Cosmos ──
  useEffect(() => {
    if (!containerRef.current) return;

    const graph = new Graph(containerRef.current, {
      spaceSize: SPACE_SIZE,
      backgroundColor: "transparent",
      pointDefaultColor: "#888888",
      pointDefaultSize: 10,
      renderLinks: false,
      fitViewOnInit: true,
      fitViewDelay: 1500,
      fitViewPadding: 0.2,
      renderHoveredPointRing: true,
      hoveredPointRingColor: cssVarToHex("--primary"),
      scalePointsOnZoom: true,
      pointGreyoutOpacity: 0.1,
      simulationGravity: 0.5,
      simulationRepulsion: 1,
      simulationCluster: 0.5,
      simulationDecay: 5000,
      simulationFriction: 0.85,
      simulationLinkSpring: 0,
      simulationLinkDistance: 1,
      onClick: (index, _pos, _event) => {
        if (index === undefined) return;
        const entry = entriesRef.current[index];
        if (entry && entry.type !== "interface") handleNodeClick(entry);
      },
      onMouseMove: (index, pointPosition) => {
        if (index === undefined || !pointPosition || !graphRef.current) {
          setHoveredLabel(null);
          containerRef.current?.classList.remove("graph-node-hovered");
          return;
        }
        const entry = entriesRef.current[index];
        if (!entry) return;
        if (entry.type !== "interface") {
          containerRef.current?.classList.add("graph-node-hovered");
        } else {
          containerRef.current?.classList.remove("graph-node-hovered");
        }
        const screen = graphRef.current.spaceToScreenPosition(pointPosition);
        setHoveredLabel({ text: entry.name, x: screen[0], y: screen[1] });
      },
      onSimulationTick: () => { updateClusterLabels(); updateSearchLabels(); },
      onSimulationEnd: () => {
        // Only auto-fit on initial load; after that, respect user's viewport
        if (!initialFitDoneRef.current) {
          graphRef.current?.fitView(300, 0.2);
          initialFitDoneRef.current = true;
        }
        updateClusterLabels(); updateSearchLabels();
      },
      onZoom: () => { hasUserInteractedRef.current = true; updateClusterLabels(); updateSearchLabels(); },
    });

    graphRef.current = graph;

    return () => {
      labelDivsRef.current.forEach(d => d.remove());
      labelDivsRef.current = [];
      graph.destroy();
      graphRef.current = null;
    };
  }, []);

  // ── Update graph data ──
  const isFirstLoadRef = useRef(true);
  useEffect(() => {
    const graph = graphRef.current;
    if (!graph || graphData.entries.length === 0) return;

    const n = graphData.entries.length;
    const scale = Math.log10(Math.max(10, n)) / Math.log10(10000);
    // Stronger gravity + lower repulsion for small graphs keeps them compact and centered
    const isSmall = n <= 20;
    graph.setConfig({
      simulationRepulsion: isSmall ? 0.3 : 0.5 + scale * 9.5,
      simulationCluster: 1.0 - scale * 0.75,
      simulationGravity: isSmall ? 1.0 : 0.25 + scale * 1.75,
      simulationDecay: isSmall ? 3000 : 5000,
      hoveredPointRingColor: cssVarToHex("--primary"),
    });

    graph.setPointPositions(graphData.positions);
    graph.setPointColors(graphData.colors);
    graph.setPointSizes(graphData.sizes);
    graph.setPointClusters(graphData.clusterIndices);
    graph.setClusterPositions(graphData.clusterPositions);
    graph.setPointClusterStrength(graphData.clusterStrength);
    graph.setLinks(new Float32Array(0));

    if (isFirstLoadRef.current) {
      graph.render(1);
      isFirstLoadRef.current = false;
    } else {
      graph.render(0.1);
    }
    updateClusterLabels();
  }, [graphData]);

  // ── Search highlighting ──
  useEffect(() => {
    const graph = graphRef.current;
    if (!graph) return;

    clearSearchLabels();

    if (searchMatchIndices) {
      searchIndicesRef.current = searchMatchIndices;
      graph.selectPointsByIndices(searchMatchIndices);
      if (searchMatchIndices.length <= 10) {
        graph.fitViewByPointIndices(searchMatchIndices, 500);
      }
      updateSearchLabels();
    } else {
      graph.unselectPoints();
      graph.fitView(300, 0.2);
    }

    return () => clearSearchLabels();
  }, [searchMatchIndices, clearSearchLabels, updateSearchLabels]);

  // ── SSE stream ──
  useEffect(() => {
    let pending: NetworkNode[] = [];
    let batchTimer: ReturnType<typeof setTimeout> | null = null;
    const flush = () => {
      batchTimer = null;
      if (pending.length === 0) return;
      const batch = pending; pending = [];
      setNodes((prev) => {
        const map = new Map(prev.map((n) => [n.hash, n]));
        for (const node of batch) map.set(node.hash, node);
        return Array.from(map.values());
      });
    };
    const unsub = subscribeBrowseNodes((node) => { pending.push(node); if (!batchTimer) batchTimer = setTimeout(flush, 200); });
    return () => { unsub(); if (batchTimer) clearTimeout(batchTimer); flush(); };
  }, []);

  // ── Navigation helpers ──
  const navigateTo = useCallback((winId: string, node: NetworkNode, path: string, prevData?: BrowseWinData) => {
    const loading: BrowseWinData = {
      node, pageHtml: null, pageLoading: true, pageError: null,
      currentPath: path,
      history: prevData?.history ?? [],
      historyIndex: prevData?.historyIndex ?? -1,
    };
    updateWindow(winId, { data: loading });

    fetchRemotePage(node.hash, path)
      .then((res) => {
        const html = res.content ? renderMicron(res.content, true) : null;
        const error = res.content ? null : (res.error ?? "No content");
        const entry: HistoryEntry = { path, html, error };

        const prevHistory = loading.history.slice(0, loading.historyIndex + 1);
        const newHistory = [...prevHistory, entry];
        const newIndex = newHistory.length - 1;

        updateWindow(winId, { data: { node, pageHtml: html, pageError: error, pageLoading: false, currentPath: path, history: newHistory, historyIndex: newIndex } });
      })
      .catch((e) => {
        const error = String(e);
        const entry: HistoryEntry = { path, html: null, error };
        const prevHistory = loading.history.slice(0, loading.historyIndex + 1);
        const newHistory = [...prevHistory, entry];
        const newIndex = newHistory.length - 1;
        updateWindow(winId, { data: { node, pageError: error, pageLoading: false, pageHtml: null, currentPath: path, history: newHistory, historyIndex: newIndex } });
      });
  }, [updateWindow]);

  const navBack = useCallback((winId: string, data: BrowseWinData) => {
    const newIndex = data.historyIndex - 1;
    if (newIndex < 0) return;
    const entry = data.history[newIndex]!;
    updateWindow(winId, { data: { ...data, pageHtml: entry.html, pageError: entry.error, pageLoading: false, currentPath: entry.path, historyIndex: newIndex } });
  }, [updateWindow]);

  const navForward = useCallback((winId: string, data: BrowseWinData) => {
    const newIndex = data.historyIndex + 1;
    if (newIndex >= data.history.length) return;
    const entry = data.history[newIndex]!;
    updateWindow(winId, { data: { ...data, pageHtml: entry.html, pageError: entry.error, pageLoading: false, currentPath: entry.path, historyIndex: newIndex } });
  }, [updateWindow]);

  const navReload = useCallback((winId: string, data: BrowseWinData) => {
    navigateTo(winId, data.node, data.currentPath, { ...data, historyIndex: data.historyIndex - 1 });
  }, [navigateTo]);

  // ── Node click ──
  const handleNodeClick = useCallback((node: NetworkNode) => {
    const id = node.hash;
    const initData: BrowseWinData = { node, pageHtml: null, pageLoading: true, pageError: null, currentPath: "index.mu", history: [], historyIndex: -1 };
    openWindow(id, initData);
    navigateTo(id, node, "index.mu");
  }, [openWindow, navigateTo]);

  // ── Handle micron link clicks via event delegation ──
  //
  // Micron link destinations come in several forms:
  //   /page.mu              — same-node, absolute path
  //   page.mu               — same-node, relative
  //   :/page/page.mu        — NomadNet "request" link (: prefix + /page/ segment)
  //   <32-hex-hash>/page.mu — cross-node link
  //   nomadnetwork://...    — already stripped by micron-parser's data-destination
  const handleContentClick = useCallback((e: React.MouseEvent, winId: string, data: BrowseWinData) => {
    const anchor = (e.target as HTMLElement).closest("a");
    if (!anchor) return;
    e.preventDefault();

    const dest = anchor.getAttribute("data-destination") ?? anchor.getAttribute("href") ?? "";
    if (!dest) return;

    let raw = dest
      .replace(/^nomadnetwork:\/\//, "")  // strip scheme if present
      .replace(/^:/, "")                  // strip NomadNet request prefix
      .replace(/^\/page\//, "")           // strip /page/ path segment
      .replace(/^\/+/, "");               // strip remaining leading slashes

    // A bare 32-char hex hash with no path — nothing to navigate to
    if (/^[0-9a-f]{32}$/i.test(raw)) return;

    // If the destination starts with a 32-char hex hash followed by "/",
    // it's a cross-node link: <hash>/page.mu → use that node's hash
    let targetNode = data.node;
    let path = raw;
    const crossNodeMatch = raw.match(/^([0-9a-f]{32})\/(.+)$/i);
    if (crossNodeMatch) {
      const targetHash = crossNodeMatch[1]!;
      path = crossNodeMatch[2]!;
      // Strip /page/ from the path portion too
      path = path.replace(/^\/page\//, "").replace(/^\/+/, "");
      const known = nodesMapRef.current.get(targetHash);
      if (known) {
        targetNode = known;
      }
    }

    if (!path || path === "/") return;
    if (!path.endsWith(".mu")) path += ".mu";

    navigateTo(winId, targetNode, path, data);
  }, [navigateTo]);

  const clearSearch = useCallback(() => {
    setFilter("");
    graphRef.current?.unselectPoints();
    // Re-center when explicitly clearing search
    graphRef.current?.fitView(300, 0.2);
    hasUserInteractedRef.current = false;
    updateClusterLabels();
  }, [updateClusterLabels]);

  return (
    <div className="relative overflow-hidden" style={{ height: "100%" }}>
      <div ref={containerRef} className="absolute inset-0 graph-canvas" />

      {/* Hover label */}
      {hoveredLabel && (
        <div className="absolute pointer-events-none px-2 py-0.5 bg-popover border border-border rounded text-xs font-mono text-foreground whitespace-nowrap"
          style={{ left: Math.min(hoveredLabel.x + 12, (containerRef.current?.offsetWidth ?? 9999) - 160), top: Math.max(8, hoveredLabel.y - 10), boxShadow: "0 2px 8px rgba(0,0,0,0.3)" }}>
          {hoveredLabel.text}
        </div>
      )}

      <BrowseSearchBar
        filter={filter}
        onFilterChange={setFilter}
        onClear={clearSearch}
        suggestions={suggestions}
        onSelectNode={handleNodeClick}
        focusedWinId={focusedWinId}
        windowCount={windows.length}
      />

      {nodes.length === 0 && (
        <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground text-sm pointer-events-none">
          <Loader />
          Connecting...
        </div>
      )}

      {windows.map((win) => (
        <BrowseNodeWindow
          key={win.id}
          win={win}
          focused={focusedWinId === win.id}
          onUpdate={updateWindow}
          onClose={closeWindowById}
          onFocus={focusWindow}
          onNavBack={navBack}
          onNavForward={navForward}
          onNavReload={navReload}
          onContentClick={handleContentClick}
        />
      ))}
    </div>
  );
}
