import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import ForceGraph2D from "react-force-graph-2d"
import { forceCollide, forceX, forceY } from "d3-force"
import { RefreshCw } from "lucide-react"
import { type MemoryGraph } from "@/lib/api"

const CAT_COLOR: Record<string, string> = {
  identity: "#00f5ff", knowledge: "#3b82f6", rules: "#d946ef", events: "#fbbf24",
}
const CAT_LABEL: Record<string, string> = {
  identity: "Identität", knowledge: "Wissen", rules: "Regeln", events: "Ereignisse",
}
const EDGE_HI = "rgba(99, 102, 241, 0.9)"
const DIM_EDGE = "rgba(255, 255, 255, 0.02)"
const FALLBACK_EDGE = "rgba(100, 116, 139, 0.25)"

// Ruhefarben der Kanten je Kategorie EINMAL vorberechnen — niemals pro Frame rechnen.
const CAT_EDGE_COLOR: Record<string, string> = {
  identity: "rgba(0, 245, 255, 0.25)", knowledge: "rgba(59, 130, 246, 0.25)",
  rules: "rgba(217, 70, 239, 0.25)", events: "rgba(251, 191, 36, 0.25)",
}
// Halo-Farben (billiger Glow-Ersatz für ctx.shadowBlur, das pro Knoten/Frame unbezahlbar ist)
const CAT_HALO_COLOR: Record<string, string> = {
  identity: "rgba(0, 245, 255, 0.16)", knowledge: "rgba(59, 130, 246, 0.16)",
  rules: "rgba(217, 70, 239, 0.16)", events: "rgba(251, 191, 36, 0.16)",
}

export function GraphView({ data, selectedNodeId, onNodeSelect }: {
  data: MemoryGraph
  selectedNodeId: string | null
  onNodeSelect: (id: string | null) => void
}) {
  const [hovered, setHovered] = useState<string | null>(null)
  const containerRef = useRef<HTMLDivElement>(null)
  const fgRef = useRef<any>(null)

  const [dimensions, setDimensions] = useState({ width: 500, height: 560 })

  // ResizeObserver für präzise Canvas-Dimensionen im Layout
  useEffect(() => {
    if (!containerRef.current) return
    const resizeObserver = new ResizeObserver((entries) => {
      for (let entry of entries) {
        if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
          setDimensions({
            width: Math.floor(entry.contentRect.width),
            height: Math.floor(entry.contentRect.height),
          })
        }
      }
    })
    resizeObserver.observe(containerRef.current)
    return () => resizeObserver.disconnect()
  }, [])

  // Knoten-Objekte über Daten-Updates hinweg WIEDERVERWENDEN: D3 behält x/y auf dem
  // Objekt — nur so überleben die Positionen Refetches und Suchfilter, statt dass der
  // ganze Graph bei jedem Update neu explodiert (und Knoten unterm Cursor wegwandern).
  const nodeCacheRef = useRef(new Map<string, any>())

  const graphData = useMemo(() => {
    const cache = nodeCacheRef.current
    const seen = new Set<string>()
    const nodes = data.nodes.map((n) => {
      seen.add(n.id)
      let node = cache.get(n.id)
      if (!node) {
        node = { ...n }
        cache.set(n.id, node)
      } else if (node.content !== n.content || node.category !== n.category) {
        node.content = n.content
        node.category = n.category
        node.__baseTextWidth = undefined
      }
      return node
    })
    for (const id of Array.from(cache.keys())) {
      if (!seen.has(id)) cache.delete(id)
    }

    const catById = new Map(data.nodes.map((n) => [n.id, n.category]))
    const links = data.edges.map((e) => ({
      source: e.source,
      target: e.target,
      weight: e.weight || 0,
      // Stabile String-IDs behalten (D3 ersetzt source/target durch Objektreferenzen)
      __sid: e.source,
      __tid: e.target,
      __restColor: CAT_EDGE_COLOR[catById.get(e.source) ?? ""] ?? FALLBACK_EDGE,
    }))
    return { nodes, links }
  }, [data])

  // Grad (Degree) berechnen
  const degree = useMemo(() => {
    const d: Record<string, number> = {}
    data.edges.forEach((e) => {
      d[e.source] = (d[e.source] || 0) + 1
      d[e.target] = (d[e.target] || 0) + 1
    })
    return d
  }, [data])

  // Quadranten-Fokuskoordinaten für sauberes Clustering
  const getClusterX = (category: string) => {
    return {
      identity: -240,
      knowledge: 240,
      rules: -240,
      events: 240,
    }[category] || 0
  }

  const getClusterY = (category: string) => {
    return {
      identity: -240,
      knowledge: -240,
      rules: 240,
      events: 240,
    }[category] || 0
  }

  // Reheat nur, wenn sich die STRUKTUR ändert (Knoten-/Kantenmenge) — nicht bei jeder
  // neuen Objekt-Identität der Daten. Verhindert Layout-Neustarts bei blossen Refetches.
  const structureKey = useMemo(() => {
    return `${data.nodes.map((n) => n.id).sort().join(",")}#${data.edges.length}`
  }, [data])

  useEffect(() => {
    const fg = fgRef.current
    if (!fg) return

    // 1. Standard-Zentrumskraft entfernen
    fg.d3Force("center", null)

    // 2. Clustering-Kräfte
    fg.d3Force("x", forceX().x((node: any) => getClusterX(node.category)).strength(0.07))
    fg.d3Force("y", forceY().y((node: any) => getClusterY(node.category)).strength(0.07))

    // 3. Starke Abstoßung (Charge) für ein weit gefächertes, luftiges Netz
    const charge = fg.d3Force("charge")
    if (charge) charge.strength(-380)

    // 4. Konstanter, stabiler Kollisionsradius
    fg.d3Force("collide", forceCollide().radius(28).iterations(2))

    // 5. Federkraft (Links)
    const link = fg.d3Force("link")
    if (link) link.distance(85)

    // Simulation einmalig heiss starten für die neue Struktur
    fg.d3ReheatSimulation()
  }, [structureKey])

  const activeNode = hovered || selectedNodeId

  // Layout zurücksetzen/neu anschubsen
  const relayout = () => {
    if (!fgRef.current) return
    fgRef.current.zoomToFit(400, 40)
  }

  // Nachbarmenge für extrem schnellen Lookup
  const connectedNodes = useMemo(() => {
    if (!activeNode) return new Set<string>()
    const set = new Set<string>()
    data.edges.forEach((e) => {
      if (e.source === activeNode) set.add(e.target)
      if (e.target === activeNode) set.add(e.source)
    })
    return set
  }, [activeNode, data])

  // Stabile Accessor-Identitäten: nur neu, wenn sich der aktive Knoten ändert.
  // Nutzt die vorberechneten __sid/__tid/__restColor — KEIN nodes.find() pro Frame mehr.
  const linkColor = useCallback((link: any) => {
    if (activeNode) {
      return (link.__sid === activeNode || link.__tid === activeNode) ? EDGE_HI : DIM_EDGE
    }
    return link.__restColor
  }, [activeNode])

  const linkWidth = useCallback((link: any) => {
    return (activeNode && (link.__sid === activeNode || link.__tid === activeNode)) ? 2.5 : 1.4
  }, [activeNode])

  const linkParticles = useCallback((link: any) => {
    return (activeNode && (link.__sid === activeNode || link.__tid === activeNode)) ? 4 : 0
  }, [activeNode])

  // EIGENES Picking statt der Bibliotheks-Hit-Map: force-graph erkennt Hover/Klick
  // über eine Schatten-Canvas, die nur alle 800 ms aktualisiert wird — solange die
  // Simulation läuft oder gezoomt wird, zeigt die Erkennung auf Positionen von vor
  // fast einer Sekunde. Wir rechnen stattdessen exakt gegen die LIVE-Koordinaten
  // der Knoten (319 Distanzchecks pro Mausbewegung sind vernachlässigbar).
  const pickNode = useCallback((clientX: number, clientY: number) => {
    const fg = fgRef.current
    const el = containerRef.current
    if (!fg || !el) return null
    const rect = el.getBoundingClientRect()
    const { x: gx, y: gy } = fg.screen2GraphCoords(clientX - rect.left, clientY - rect.top)
    const k = Math.max(fg.zoom() || 1, 0.01)
    let best: any = null
    let bestD2 = Infinity
    for (const n of graphData.nodes as any[]) {
      if (n.x == null || n.y == null) continue
      const size = 6 + Math.min(degree[n.id] || 0, 15) * 0.8
      // Trefferradius nie kleiner als ~16 Bildschirm-Pixel, egal wie weit rausgezoomt
      const hitR = Math.max(size + 3, 16 / k)
      const dx = n.x - gx
      const dy = n.y - gy
      const d2 = dx * dx + dy * dy
      if (d2 <= hitR * hitR && d2 < bestD2) { bestD2 = d2; best = n }
    }
    return best
  }, [graphData, degree])

  const handlePointerMove = useCallback((ev: React.PointerEvent) => {
    // Nur auf der Canvas picken — über Buttons/Legende gilt: kein Hover
    const node = ev.target instanceof HTMLCanvasElement ? pickNode(ev.clientX, ev.clientY) : null
    setHovered(node ? node.id : null)
    if (containerRef.current) {
      containerRef.current.style.cursor = node ? "pointer" : "default"
    }
  }, [pickNode])

  const handlePointerLeave = useCallback(() => {
    setHovered(null)
    if (containerRef.current) containerRef.current.style.cursor = "default"
  }, [])

  // Klick selbst auswerten — aber nicht nach einem Pan (Mausweg > 6 px zählt nicht als Klick)
  const downPosRef = useRef<{ x: number; y: number } | null>(null)
  const handlePointerDown = useCallback((ev: React.PointerEvent) => {
    downPosRef.current = { x: ev.clientX, y: ev.clientY }
  }, [])

  const handleClick = useCallback((ev: React.MouseEvent) => {
    if (!(ev.target instanceof HTMLCanvasElement)) return // Buttons/Legende nicht abfangen
    const down = downPosRef.current
    if (down && Math.hypot(ev.clientX - down.x, ev.clientY - down.y) > 6) return
    const node = pickNode(ev.clientX, ev.clientY)
    onNodeSelect(node ? node.id : null)
  }, [pickNode, onNodeSelect])

  const nodeCanvasObject = useCallback((node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
    const x = node.x ?? 0
    const y = node.y ?? 0
    const size = 6 + Math.min(degree[node.id] || 0, 15) * 0.8
    const color = CAT_COLOR[node.category] || "#64748b"
    const isHovered = node.id === hovered
    const isSelected = node.id === selectedNodeId

    const isDimmed = activeNode && node.id !== activeNode && !connectedNodes.has(node.id)
    const finalColor = isDimmed ? "#1e293b" : color

    // 1. Billiger Glow: transparenter Halo-Kreis statt ctx.shadowBlur (das kostet bei
    // hunderten Knoten pro Frame die gesamte Framerate). Echter Blur nur für den
    // EINEN aktiven Knoten.
    if (!isDimmed) {
      ctx.beginPath()
      ctx.arc(x, y, size * 1.9, 0, 2 * Math.PI, false)
      ctx.fillStyle = CAT_HALO_COLOR[node.category] || "rgba(100, 116, 139, 0.14)"
      ctx.fill()
    }

    if (isHovered || isSelected) {
      ctx.shadowColor = color
      ctx.shadowBlur = 15
    }
    ctx.beginPath()
    ctx.arc(x, y, size, 0, 2 * Math.PI, false)
    ctx.fillStyle = finalColor
    ctx.fill()
    ctx.shadowBlur = 0

    if (isHovered || isSelected) {
      ctx.beginPath()
      ctx.arc(x, y, size + 2.5, 0, 2 * Math.PI, false)
      ctx.strokeStyle = isSelected ? "#ffffff" : "rgba(255, 255, 255, 0.7)"
      ctx.lineWidth = 1.5
      ctx.stroke()
    }

    // 2. Label nur für den aktiven Fokus (Hovered, Selected & direkte Nachbarn)
    const showLabel = isHovered || isSelected || (activeNode && connectedNodes.has(node.id))

    if (showLabel) {
      const label = node.content.length > 35 ? node.content.slice(0, 34) + "…" : node.content

      // Textbreite EINMALIG auf dem Objekt cachen (ctx.measureText ist teuer)
      if (node.__baseTextWidth === undefined) {
        const prevFont = ctx.font
        ctx.font = '500 10px "Space Grotesk", sans-serif'
        node.__baseTextWidth = ctx.measureText(label).width
        ctx.font = prevFont
      }

      const baseFontSize = isHovered || isSelected ? 10.5 : 9.5
      const fontSize = baseFontSize / Math.max(0.5, globalScale)

      ctx.font = `${isHovered || isSelected ? "bold" : "500"} ${fontSize}px "Space Grotesk", sans-serif`

      // Breite linear zur geänderten FontSize skalieren — kein measureText pro Frame
      const textWidth = node.__baseTextWidth * (fontSize / 10)
      const bpad = 4.5 / Math.max(0.5, globalScale)

      const rx = x - textWidth / 2 - bpad
      const ry = y + size + (6 / Math.max(0.5, globalScale))
      const rw = textWidth + bpad * 2
      const rh = fontSize + bpad * 1.6

      ctx.fillStyle = isDimmed ? "rgba(10, 15, 25, 0.45)" : "rgba(8, 12, 20, 0.88)"
      ctx.fillRect(rx, ry, rw, rh)

      ctx.strokeStyle = isHovered || isSelected ? "rgba(255, 255, 255, 0.2)" : "rgba(255, 255, 255, 0.08)"
      ctx.lineWidth = 0.6 / Math.max(0.5, globalScale)
      ctx.strokeRect(rx, ry, rw, rh)

      ctx.textAlign = "center"
      ctx.textBaseline = "top"
      ctx.fillStyle = isDimmed ? "#475569" : (isHovered || isSelected ? "#ffffff" : "#cbd5e1")

      ctx.fillText(label, x, y + size + (7 / Math.max(0.5, globalScale)))
    }
  }, [degree, hovered, selectedNodeId, activeNode, connectedNodes])

  return (
    <div className="absolute inset-0" ref={containerRef}
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
      onPointerDown={handlePointerDown}
      onClick={handleClick}
      style={{ background: "radial-gradient(circle at center, rgba(59, 130, 246, 0.08) 0%, #030712 100%)" }}>

      <ForceGraph2D
        ref={fgRef}
        width={dimensions.width}
        height={dimensions.height}
        graphData={graphData}
        backgroundColor="rgba(0,0,0,0)"
        cooldownTicks={120}
        // Bibliotheks-Picking komplett aus: Hover/Klick machen wir selbst (exakt,
        // ohne 800-ms-Throttle), Drag verschluckt sonst Klicks und schubst das Layout an.
        enablePointerInteraction={false}
        enableNodeDrag={false}
        linkColor={linkColor}
        linkWidth={linkWidth}
        linkDirectionalParticles={linkParticles}
        linkDirectionalParticleWidth={2.2}
        linkDirectionalParticleSpeed={0.006}
        nodeCanvasObject={nodeCanvasObject}
      />

      <button onClick={relayout} title="Zentrieren"
        className="absolute right-3 top-3 z-10 flex items-center gap-1.5 rounded-lg border border-border/60 bg-background/50 px-2.5 py-1.5 text-[11px] text-muted-foreground hover:text-foreground backdrop-blur-sm transition-colors cursor-pointer">
        <RefreshCw className="h-3.5 w-3.5" /> Zentrieren
      </button>

      {/* Legende */}
      <div className="absolute left-3 bottom-3 flex flex-wrap gap-2 rounded-lg bg-background/40 px-2.5 py-1.5 backdrop-blur-sm">
        {Object.entries(CAT_LABEL).map(([k, l]) => (
          <span key={k} className="flex items-center gap-1 text-[10px] text-muted-foreground">
            <span className="h-2 w-2 rounded-full" style={{ background: CAT_COLOR[k] }} />{l}
          </span>
        ))}
      </div>
    </div>
  )
}
