/**
 * ForceGraphCanvas — D3 force-directed entity graph (Obsidian-style).
 *
 * Layout:
 *   - Left sidebar: filters (classes, limit) — sempre visível, fora do canvas
 *   - Right side panel: detalhe da entidade selecionada (quando clica num nó)
 *   - Center: canvas fullscreen com nodes coloridos por classe + edges
 *     coloridas por peso (low=cinza, mid=cyan, high=verde)
 *
 * Interação:
 *   - HOVER: tooltip flutuante com nome + classe + mentions
 *   - CLICK: abre side panel direito com info da entidade + top neighbors + botão "abrir página"
 *   - DOUBLE-CLICK: navega direto para /e/<class>/<id>
 *   - Scroll: zoom; drag canvas: pan
 */
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";

const ForceGraph2D = dynamic(() => import("react-force-graph-2d"), { ssr: false });

interface RawNode {
  entity_pk: number;
  entity_class: string;
  entity_id: string;
  canonical_name: string;
  total_mentions: number;
  documents_count: number;
}
interface RawLink {
  source: number;
  target: number;
  weight: number;
}
interface GraphNode extends RawNode {
  id: number;
  x?: number;
  y?: number;
  vx?: number;
  vy?: number;
}
interface GraphLink {
  source: number | GraphNode;
  target: number | GraphNode;
  weight: number;
}

const CLASS_COLOR: Record<string, string> = {
  person: "#ff6ec7",
  organization: "#ff8a4d",
  location: "#3fde6a",
  event: "#ffa500",
  uap_object: "#ff3344",
  vehicle: "#5b9bd5",
  operation: "#9b5de5",
  concept: "#06d6a0",
};
const CLASS_FOLDER: Record<string, string> = {
  person: "people",
  organization: "organizations",
  location: "locations",
  event: "events",
  uap_object: "uap-objects",
  vehicle: "vehicles",
  operation: "operations",
  concept: "concepts",
};
const CLASS_LABEL: Record<string, string> = {
  person: "Pessoas",
  organization: "Organizações",
  location: "Locais",
  event: "Eventos",
  uap_object: "UAP",
  vehicle: "Veículos",
  operation: "Operações",
  concept: "Conceitos",
};

const ALL_CLASSES = ["person", "organization", "location", "event", "uap_object", "vehicle", "operation", "concept"];

/** Color edge by weight tier — visual diferenciação por intensidade */
function edgeColor(weight: number): string {
  if (weight >= 10) return "rgba(0,255,156,0.55)"; // strong: green
  if (weight >= 5) return "rgba(127,219,255,0.45)"; // medium: cyan
  if (weight >= 3) return "rgba(167,139,250,0.35)"; // mild: purple
  return "rgba(127,219,255,0.18)"; // weak: faded cyan
}

function edgeWidth(weight: number): number {
  return Math.max(0.5, Math.min(6, Math.log2(weight + 1) * 1.2));
}

interface EntityDetail {
  entity_pk: number;
  entity_class: string;
  entity_id: string;
  canonical_name: string;
  total_mentions: number;
  documents_count: number;
  neighbors: Array<{
    entity_pk: number;
    entity_class: string;
    entity_id: string;
    canonical_name: string;
    weight: number;
    total_mentions: number;
  }>;
}

const detailCache = new Map<number, EntityDetail>();

interface ForceGraph2DRef {
  d3Force: (name: string) => { strength?: (v: number) => unknown; distance?: (v: number) => unknown } | null;
  d3ReheatSimulation: () => void;
  zoomToFit: (durationMs?: number, paddingPx?: number) => void;
  centerAt: (x?: number, y?: number, durationMs?: number) => void;
}

export function ForceGraphCanvas() {
  const fgRef = useRef<ForceGraph2DRef | null>(null);
  const [nodes, setNodes] = useState<GraphNode[]>([]);
  const [links, setLinks] = useState<GraphLink[]>([]);
  const [selectedClasses, setSelectedClasses] = useState<Set<string>>(new Set(ALL_CLASSES));
  const [loading, setLoading] = useState(true);
  const [hoverNode, setHoverNode] = useState<GraphNode | null>(null);
  const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null);
  const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
  const [detail, setDetail] = useState<EntityDetail | null>(null);
  const [detailLoading, setDetailLoading] = useState(false);
  const [limit, setLimit] = useState(40);
  const [minWeight, setMinWeight] = useState(3);
  const [search, setSearch] = useState("");

  // Tune d3-force after the graph mounts and on data change — STRONGER repulsion + LONGER links
  useEffect(() => {
    const fg = fgRef.current;
    if (!fg) return;
    const charge = fg.d3Force("charge");
    if (charge?.strength) charge.strength(-450);
    const link = fg.d3Force("link");
    if (link?.distance) link.distance(120);
    const center = fg.d3Force("center");
    if (center?.strength) center.strength(0.04);
    fg.d3ReheatSimulation();
    setTimeout(() => fg.zoomToFit?.(800, 80), 1500);
  }, [nodes.length, links.length]);

  // Initial seed load — re-runs when filters change
  useEffect(() => {
    setLoading(true);
    const classesParam = Array.from(selectedClasses).join(",");
    fetch(`/api/graph/seed?limit=${limit}&min_weight=${minWeight}&classes=${classesParam}`)
      .then((r) => r.json())
      .then((data: { nodes?: RawNode[]; links?: RawLink[] }) => {
        const ns = (data.nodes ?? []).map((n) => ({ ...n, id: n.entity_pk } as GraphNode));
        const ls = (data.links ?? []).map((l) => ({ source: l.source, target: l.target, weight: l.weight } as GraphLink));
        setNodes(ns);
        setLinks(ls);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, [limit, minWeight, selectedClasses]);

  // Fetch detail when node selected
  useEffect(() => {
    if (!selectedNode) {
      setDetail(null);
      return;
    }
    const cached = detailCache.get(selectedNode.entity_pk);
    if (cached) {
      setDetail(cached);
      return;
    }
    setDetail(null);
    setDetailLoading(true);
    fetch(
      `/api/graph?op=neighbors&class=${selectedNode.entity_class}&id=${encodeURIComponent(selectedNode.entity_id)}&limit=12`,
    )
      .then((r) => r.json())
      .then((data: { entity?: RawNode; neighbors?: EntityDetail["neighbors"] }) => {
        const d: EntityDetail = {
          entity_pk: selectedNode.entity_pk,
          entity_class: selectedNode.entity_class,
          entity_id: selectedNode.entity_id,
          canonical_name: selectedNode.canonical_name,
          total_mentions: data.entity?.total_mentions ?? selectedNode.total_mentions,
          documents_count: data.entity?.documents_count ?? selectedNode.documents_count,
          neighbors: data.neighbors ?? [],
        };
        detailCache.set(selectedNode.entity_pk, d);
        setDetail(d);
      })
      .catch(() => setDetail(null))
      .finally(() => setDetailLoading(false));
  }, [selectedNode]);

  const onNodeClick = useCallback(async (node: GraphNode) => {
    setSelectedNode(node);
  }, []);

  const expandNode = useCallback(
    async (node: GraphNode) => {
      try {
        const r = await fetch(
          `/api/graph?op=neighbors&class=${node.entity_class}&id=${encodeURIComponent(node.entity_id)}&limit=15`,
        );
        if (!r.ok) return;
        const data = (await r.json()) as { neighbors?: Array<RawNode & { weight: number }> };
        if (!data.neighbors) return;
        setNodes((prev) => {
          const existing = new Set(prev.map((p) => p.id));
          const additions = data.neighbors!
            .filter((n) => !existing.has(n.entity_pk))
            .map((n) => ({ ...n, id: n.entity_pk } as GraphNode));
          return [...prev, ...additions];
        });
        setLinks((prev) => {
          const seen = new Set(
            prev.map((l) => {
              const s = typeof l.source === "object" ? (l.source as GraphNode).id : l.source;
              const t = typeof l.target === "object" ? (l.target as GraphNode).id : l.target;
              return `${Math.min(s, t)}-${Math.max(s, t)}`;
            }),
          );
          const additions: GraphLink[] = [];
          for (const n of data.neighbors!) {
            const a = node.entity_pk;
            const b = n.entity_pk;
            const key = `${Math.min(a, b)}-${Math.max(a, b)}`;
            if (!seen.has(key)) {
              additions.push({ source: a, target: b, weight: n.weight });
              seen.add(key);
            }
          }
          return [...prev, ...additions];
        });
      } catch {
        /* ignore */
      }
    },
    [],
  );

  const toggleClass = useCallback((cls: string) => {
    setSelectedClasses((prev) => {
      const next = new Set(prev);
      if (next.has(cls)) next.delete(cls);
      else next.add(cls);
      return next.size > 0 ? next : prev;
    });
  }, []);

  const visibleData = useMemo(() => {
    let filteredNodes = nodes.filter((n) => selectedClasses.has(n.entity_class));
    if (search.trim()) {
      const sl = search.toLowerCase();
      filteredNodes = filteredNodes.filter((n) =>
        n.canonical_name.toLowerCase().includes(sl) || n.entity_id.toLowerCase().includes(sl),
      );
    }
    const allowed = new Set(filteredNodes.map((n) => n.id));
    const filteredLinks = links.filter((l) => {
      const s = typeof l.source === "object" ? (l.source as GraphNode).id : l.source;
      const t = typeof l.target === "object" ? (l.target as GraphNode).id : l.target;
      return allowed.has(s) && allowed.has(t);
    });
    return { nodes: filteredNodes, links: filteredLinks };
  }, [nodes, links, selectedClasses, search]);

  return (
    <div className="relative w-full h-full bg-[#040810] overflow-hidden">
      {/* LEFT sidebar — filters (sempre visível, fora do z-30 do page header) */}
      <div className="absolute top-20 left-4 z-20 w-[240px] max-h-[calc(100vh-180px)] overflow-y-auto bg-[#0a121e]/95 backdrop-blur border border-[rgba(0,255,156,0.20)] rounded p-3 space-y-4">
        <div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
            🔍 buscar nó
          </div>
          <input
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            placeholder="nome ou id..."
            className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-xs text-[#c8d4e6] outline-none"
          />
        </div>

        <div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
            classes
          </div>
          <div className="space-y-1">
            {ALL_CLASSES.map((cls) => {
              const active = selectedClasses.has(cls);
              const color = CLASS_COLOR[cls] ?? "#7fdbff";
              return (
                <button
                  key={cls}
                  onClick={() => toggleClass(cls)}
                  className={`w-full flex items-center gap-2 px-2 py-1 font-mono text-[11px] rounded border transition ${
                    active ? "" : "opacity-30 hover:opacity-60"
                  }`}
                  style={{
                    color,
                    borderColor: color,
                    background: active ? `${color}12` : "transparent",
                  }}
                >
                  <span className="inline-block w-2 h-2 rounded-full" style={{ background: color }} />
                  <span className="flex-1 text-left">{CLASS_LABEL[cls]}</span>
                  {active && <span>✓</span>}
                </button>
              );
            })}
          </div>
        </div>

        <div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
            top entidades
          </div>
          <div className="flex flex-wrap gap-1">
            {[20, 40, 80, 150].map((n) => (
              <button
                key={n}
                onClick={() => setLimit(n)}
                className={`px-2 py-1 font-mono text-[11px] rounded border ${
                  limit === n
                    ? "border-[#00ff9c] text-[#00ff9c] bg-[rgba(0,255,156,0.10)]"
                    : "border-[rgba(127,219,255,0.20)] text-[#8896aa] hover:text-[#7fdbff]"
                }`}
              >
                {n}
              </button>
            ))}
          </div>
        </div>

        <div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
            mostrar vínculos com ≥
          </div>
          <div className="flex flex-wrap gap-1">
            {[2, 3, 5, 10].map((n) => (
              <button
                key={n}
                onClick={() => setMinWeight(n)}
                className={`px-2 py-1 font-mono text-[11px] rounded border ${
                  minWeight === n
                    ? "border-[#00ff9c] text-[#00ff9c] bg-[rgba(0,255,156,0.10)]"
                    : "border-[rgba(127,219,255,0.20)] text-[#8896aa] hover:text-[#7fdbff]"
                }`}
              >
                {n}×
              </button>
            ))}
          </div>
        </div>

        <div>
          <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
            força do vínculo
          </div>
          <div className="space-y-1 text-[10px] font-mono">
            <div className="flex items-center gap-2">
              <span className="inline-block w-6 h-0.5" style={{ background: "rgba(0,255,156,0.55)" }} />
              <span className="text-[#8896aa]">≥ 10 co-menções</span>
            </div>
            <div className="flex items-center gap-2">
              <span className="inline-block w-6 h-0.5" style={{ background: "rgba(127,219,255,0.45)" }} />
              <span className="text-[#8896aa]">5–9</span>
            </div>
            <div className="flex items-center gap-2">
              <span className="inline-block w-6 h-0.5" style={{ background: "rgba(167,139,250,0.35)" }} />
              <span className="text-[#8896aa]">3–4</span>
            </div>
            <div className="flex items-center gap-2">
              <span className="inline-block w-6 h-0.5" style={{ background: "rgba(127,219,255,0.18)" }} />
              <span className="text-[#8896aa]">2 (mín.)</span>
            </div>
          </div>
        </div>

        <div className="pt-2 border-t border-[rgba(0,255,156,0.10)] font-mono text-[10px] text-[#5a6678]">
          {loading ? "carregando…" : `${visibleData.nodes.length} nós · ${visibleData.links.length} arestas`}
        </div>
      </div>

      {/* RIGHT side panel — entidade selecionada */}
      {selectedNode && (
        <div className="absolute top-20 right-4 z-20 w-[340px] max-h-[calc(100vh-180px)] overflow-y-auto bg-[#0a121e]/95 backdrop-blur border-2 rounded p-4 space-y-4"
             style={{ borderColor: CLASS_COLOR[selectedNode.entity_class] ?? "#7fdbff" }}>
          <div className="flex items-start justify-between gap-2">
            <div className="flex-1 min-w-0">
              <div
                className="font-mono text-[10px] uppercase tracking-widest mb-1"
                style={{ color: CLASS_COLOR[selectedNode.entity_class] ?? "#7fdbff" }}
              >
                {CLASS_LABEL[selectedNode.entity_class] ?? selectedNode.entity_class}
              </div>
              <h3 className="font-mono text-base text-[#c8d4e6] font-bold leading-tight break-words">
                {selectedNode.canonical_name}
              </h3>
              <div className="font-mono text-[10px] text-[#5a6678] mt-1 truncate">
                {selectedNode.entity_id}
              </div>
            </div>
            <button
              onClick={() => setSelectedNode(null)}
              className="text-[#5a6678] hover:text-[#ff6b6b] flex-shrink-0"
              aria-label="fechar"
            >
              ✕
            </button>
          </div>

          <div className="grid grid-cols-2 gap-2">
            <div className="px-3 py-2 bg-[#060a13] border border-[rgba(0,255,156,0.20)] rounded">
              <div className="font-mono text-[9px] uppercase text-[#5a6678]">menções</div>
              <div className="font-mono text-lg text-[#00ff9c] mt-0.5">{selectedNode.total_mentions}</div>
            </div>
            <div className="px-3 py-2 bg-[#060a13] border border-[rgba(127,219,255,0.20)] rounded">
              <div className="font-mono text-[9px] uppercase text-[#5a6678]">documentos</div>
              <div className="font-mono text-lg text-[#7fdbff] mt-0.5">{selectedNode.documents_count}</div>
            </div>
          </div>

          {/* Action buttons */}
          <div className="space-y-1.5">
            <Link
              href={`/e/${CLASS_FOLDER[selectedNode.entity_class]}/${selectedNode.entity_id}`}
              className="block w-full px-3 py-2 font-mono text-xs uppercase tracking-widest border-2 border-[#00ff9c] text-[#00ff9c] bg-[rgba(0,255,156,0.08)] hover:bg-[rgba(0,255,156,0.18)] rounded text-center"
            >
              abrir página completa →
            </Link>
            <button
              onClick={() => expandNode(selectedNode)}
              className="block w-full px-3 py-2 font-mono text-xs uppercase tracking-widest border border-[#7fdbff] text-[#7fdbff] hover:bg-[rgba(127,219,255,0.10)] rounded"
            >
              + expandir vizinhos no grafo
            </button>
          </div>

          {/* Neighbors list */}
          <div>
            <div className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] mb-2">
              {detailLoading ? "carregando vizinhos…" : `top vínculos (${detail?.neighbors.length ?? 0})`}
            </div>
            {detail?.neighbors && detail.neighbors.length > 0 ? (
              <ul className="space-y-1">
                {detail.neighbors.map((n) => {
                  const color = CLASS_COLOR[n.entity_class] ?? "#7fdbff";
                  return (
                    <li key={n.entity_pk}>
                      <button
                        onClick={() => {
                          const folded = nodes.find((nn) => nn.entity_pk === n.entity_pk);
                          if (folded) {
                            setSelectedNode(folded);
                          } else {
                            // Inject into graph + select
                            const newNode: GraphNode = {
                              entity_pk: n.entity_pk,
                              entity_class: n.entity_class,
                              entity_id: n.entity_id,
                              canonical_name: n.canonical_name,
                              total_mentions: n.total_mentions,
                              documents_count: 0,
                              id: n.entity_pk,
                            };
                            setNodes((prev) => [...prev, newNode]);
                            setLinks((prev) => [...prev, { source: selectedNode.entity_pk, target: n.entity_pk, weight: n.weight }]);
                            setSelectedNode(newNode);
                          }
                        }}
                        className="group w-full flex items-center gap-2 text-left p-1.5 -mx-1.5 rounded hover:bg-[rgba(0,255,156,0.04)]"
                      >
                        <span
                          className="inline-block w-2 h-2 rounded-full flex-shrink-0"
                          style={{ background: color }}
                        />
                        <span className="text-[11px] text-[#c8d4e6] group-hover:text-[#00ff9c] flex-1 truncate">
                          {n.canonical_name}
                        </span>
                        <span className="font-mono text-[10px] text-[#5a6678] flex-shrink-0">×{n.weight}</span>
                      </button>
                    </li>
                  );
                })}
              </ul>
            ) : !detailLoading ? (
              <p className="font-mono text-[10px] text-[#5a6678] italic">sem co-menções</p>
            ) : null}
          </div>

          <p className="font-mono text-[9px] text-[#5a6678] pt-2 border-t border-[rgba(0,255,156,0.10)]">
            duplo-clique no nó: abre página da entidade · clique vizinho: foca nele
          </p>
        </div>
      )}

      {/* Hover tooltip — segue o mouse */}
      {hoverNode && hoverPos && (
        <div
          className="absolute z-30 pointer-events-none px-2.5 py-1.5 bg-[#0a121e] border-2 rounded text-xs font-mono"
          style={{
            left: hoverPos.x + 12,
            top: hoverPos.y + 12,
            borderColor: CLASS_COLOR[hoverNode.entity_class] ?? "#7fdbff",
          }}
        >
          <div className="text-[#c8d4e6] font-bold">{hoverNode.canonical_name}</div>
          <div className="text-[10px] text-[#5a6678]">
            {hoverNode.entity_class} · {hoverNode.total_mentions} menções · {hoverNode.documents_count} docs
          </div>
          <div className="text-[9px] text-[#7fdbff] mt-0.5">clique para detalhes</div>
        </div>
      )}

      {/* Canvas */}
      <ForceGraph2D
        ref={fgRef as never}
        graphData={visibleData as never}
        backgroundColor="#040810"
        nodeRelSize={2.5}
        nodeVal={(n) => Math.max(1.5, Math.log2((n as GraphNode).total_mentions + 2) * 0.8)}
        nodeColor={(n) => CLASS_COLOR[(n as GraphNode).entity_class] ?? "#7fdbff"}
        nodeLabel={() => ""}
        linkColor={(l) => edgeColor((l as GraphLink).weight)}
        linkWidth={(l) => edgeWidth((l as GraphLink).weight)}
        // Physics — separa nós com força + arestas mais longas
        d3VelocityDecay={0.3}
        d3AlphaDecay={0.015}
        cooldownTicks={250}
        warmupTicks={60}
        // Use d3-force charge customization via dagMode? No, lib has limited API; rely on defaults + manual.
        onNodeClick={onNodeClick as never}
        onNodeHover={(n) => {
          setHoverNode(n as GraphNode | null);
          if (!n) setHoverPos(null);
        }}
        onBackgroundClick={() => setSelectedNode(null)}
        nodeCanvasObjectMode={() => "after"}
        nodeCanvasObject={(n, ctx, scale) => {
          const node = n as GraphNode;
          const isSelected = selectedNode?.entity_pk === node.entity_pk;
          const isHovered = hoverNode?.entity_pk === node.entity_pk;
          // Anti-clutter: with many nodes, mostrar label só:
          //   - quando zoomado (scale ≥ 1.5)
          //   - OU é hub (>200 mentions)
          //   - OU é hover/selected
          const isHub = node.total_mentions >= 200;
          const showLabel = isSelected || isHovered || isHub || scale >= 1.5;
          if (!showLabel) {
            if (isSelected) {
              ctx.beginPath();
              ctx.arc(node.x ?? 0, node.y ?? 0, 10 / scale, 0, 2 * Math.PI, false);
              ctx.strokeStyle = "#00ff9c";
              ctx.lineWidth = 2.5 / scale;
              ctx.stroke();
            }
            return;
          }
          const fontSize = Math.max(10, 14 / scale);
          ctx.font = `${isHub ? "bold " : ""}${fontSize}px sans-serif`;
          const label =
            node.canonical_name.length > 28
              ? node.canonical_name.slice(0, 26) + "…"
              : node.canonical_name;
          const tw = ctx.measureText(label).width;
          const pad = 4 / scale;
          // Background pill behind text — readability
          ctx.fillStyle = isSelected ? "rgba(0,255,156,0.85)" : "rgba(10,18,30,0.85)";
          ctx.fillRect(
            (node.x ?? 0) - tw / 2 - pad,
            (node.y ?? 0) + 8 / scale,
            tw + pad * 2,
            fontSize + pad,
          );
          ctx.fillStyle = isSelected ? "#040810" : "#c8d4e6";
          ctx.textAlign = "center";
          ctx.textBaseline = "top";
          ctx.fillText(label, node.x ?? 0, (node.y ?? 0) + 8 / scale + pad / 2);
          // Selected ring
          if (isSelected) {
            ctx.beginPath();
            ctx.arc(node.x ?? 0, node.y ?? 0, 10 / scale, 0, 2 * Math.PI, false);
            ctx.strokeStyle = "#00ff9c";
            ctx.lineWidth = 2.5 / scale;
            ctx.stroke();
          }
        }}
      />
    </div>
  );
}
