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 FALLBACK_EDGE = "rgba(100, 116, 139, 0.3)"

// Ruhefarben der Kanten je Kategorie EINMAL vorberechnen — niemals pro Frame rechnen.
const CAT_EDGE_COLOR: Record<string, string> = {
  identity: "rgba(0, 245, 255, 0.3)", knowledge: "rgba(59, 130, 246, 0.3)",
  rules: "rgba(217, 70, 239, 0.3)", events: "rgba(251, 191, 36, 0.3)",
}
// 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)",
}
// Cluster-Hüllen: Fläche, Rand und Landkarten-Label je Kategorie
const CAT_HULL_FILL: Record<string, string> = {
  identity: "rgba(0, 245, 255, 0.05)", knowledge: "rgba(59, 130, 246, 0.05)",
  rules: "rgba(217, 70, 239, 0.05)", events: "rgba(251, 191, 36, 0.05)",
}
const CAT_HULL_STROKE: 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)",
}
const CAT_HULL_LABEL: Record<string, string> = {
  identity: "rgba(0, 245, 255, 0.3)", knowledge: "rgba(59, 130, 246, 0.3)",
  rules: "rgba(217, 70, 239, 0.3)", events: "rgba(251, 191, 36, 0.3)",
}

// Konvexe Hülle (Andrew Monotone Chain) — bei ~300 Punkten pro Frame vernachlässigbar
function convexHull(pts: [number, number][]): [number, number][] {
  if (pts.length < 3) return pts
  const p = [...pts].sort((a, b) => a[0] - b[0] || a[1] - b[1])
  const cross = (o: number[], a: number[], b: number[]) =>
    (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
  const lower: [number, number][] = []
  for (const pt of p) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pt) <= 0) lower.pop()
    lower.push(pt)
  }
  const upper: [number, number][] = []
  for (let i = p.length - 1; i >= 0; i--) {
    const pt = p[i]
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pt) <= 0) upper.pop()
    upper.push(pt)
  }
  lower.pop(); upper.pop()
  return lower.concat(upper)
}

export function GraphView({ data, selectedNodeId, onNodeSelect }: {
  data: MemoryGraph
  selectedNodeId: string | null
  onNodeSelect: (id: string | null) => void
}) {
  const [hovered, setHovered] = useState<string | null>(null)
  // Gewichts-Schwelle: nur Kanten ab dieser Ähnlichkeit werden im Ruhezustand gezeichnet
  const [threshold, setThreshold] = useState(0.75)
  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,
      __sameCat: catById.get(e.source) === catById.get(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 — etwas stärker, damit die Inseln klar auseinanderziehen
    fg.d3Force("x", forceX().x((node: any) => getClusterX(node.category)).strength(0.1))
    fg.d3Force("y", forceY().y((node: any) => getClusterY(node.category)).strength(0.1))

    // 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): Kanten innerhalb einer Kategorie ziehen stark an
    // (gewichtet nach Ähnlichkeit), Querkanten fast gar nicht — so trennen sich
    // die Inseln räumlich, statt vom Quernetz zusammengezurrt zu werden.
    const link = fg.d3Force("link")
    if (link) {
      link
        .distance((l: any) => (l.__sameCat ? 70 : 200))
        .strength((l: any) => (l.__sameCat ? 0.3 * (l.weight || 0.6) : 0.01))
    }

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

  const activeNode = hovered || selectedNodeId

  // Anzahl der im Ruhezustand sichtbaren Kanten (für die Anzeige am Regler)
  const visibleEdges = useMemo(() => {
    return graphData.links.filter((l: any) => l.__sameCat && (l.weight || 0) >= threshold).length
  }, [graphData, threshold])

  // 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])

  // Sichtbarkeit statt Durcheinander: im Ruhezustand nur Insel-Kanten (gleiche
  // Kategorie) ab der Gewichts-Schwelle. Querkanten erscheinen NUR am aktiven
  // Knoten (Hover/Auswahl) — dort dann alle seine Verbindungen, auch schwache.
  const linkVisibility = useCallback((link: any) => {
    if (activeNode) return link.__sid === activeNode || link.__tid === activeNode
    return link.__sameCat && (link.weight || 0) >= threshold
  }, [activeNode, threshold])

  const linkColor = useCallback((link: any) => {
    if (activeNode && (link.__sid === activeNode || link.__tid === activeNode)) return EDGE_HI
    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])

  // Landkarten-Look: weiche, getönte Hülle + großes Label hinter jeder Kategorie-Insel.
  // Läuft VOR Kanten/Knoten (onRenderFramePre); Konvexhülle über ~300 Punkte ist billig.
  const paintHulls = useCallback((ctx: CanvasRenderingContext2D, globalScale: number) => {
    const byCat: Record<string, [number, number][]> = {}
    for (const n of graphData.nodes as any[]) {
      if (n.x == null || n.y == null) continue
      ;(byCat[n.category] ??= []).push([n.x, n.y])
    }
    for (const [cat, pts] of Object.entries(byCat)) {
      const fill = CAT_HULL_FILL[cat]
      if (!fill || pts.length === 0) continue
      const hull = convexHull(pts)
      let cx = 0, cy = 0
      hull.forEach((p) => { cx += p[0]; cy += p[1] })
      cx /= hull.length; cy /= hull.length

      const PAD = 34
      ctx.beginPath()
      if (hull.length < 3) {
        ctx.arc(cx, cy, PAD + 16, 0, 2 * Math.PI)
      } else {
        // Hülle vom Schwerpunkt weg aufpolstern und durch Mittelpunkte glätten
        const exp = hull.map(([x, y]) => {
          const dx = x - cx, dy = y - cy
          const d = Math.hypot(dx, dy) || 1
          return [x + (dx / d) * PAD, y + (dy / d) * PAD] as [number, number]
        })
        const mid = (a: number[], b: number[]) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
        const m0 = mid(exp[exp.length - 1], exp[0])
        ctx.moveTo(m0[0], m0[1])
        for (let i = 0; i < exp.length; i++) {
          const cur = exp[i]
          const m = mid(cur, exp[(i + 1) % exp.length])
          ctx.quadraticCurveTo(cur[0], cur[1], m[0], m[1])
        }
        ctx.closePath()
      }
      ctx.fillStyle = fill
      ctx.fill()
      ctx.strokeStyle = CAT_HULL_STROKE[cat]
      ctx.lineWidth = 1.2 / Math.max(globalScale, 0.3)
      ctx.stroke()

      // Insel-Label in Weltkoordinaten — zoomt wie eine Landkarten-Beschriftung mit
      const minY = Math.min(...hull.map((p) => p[1]))
      ctx.font = '600 26px "Space Grotesk", sans-serif'
      ctx.textAlign = "center"
      ctx.textBaseline = "bottom"
      ctx.fillStyle = CAT_HULL_LABEL[cat]
      ctx.fillText(CAT_LABEL[cat] || cat, cx, minY - PAD - 8)
    }
  }, [graphData])

  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}
        onRenderFramePre={paintHulls}
        linkVisibility={linkVisibility}
        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 + Verbindungs-Regler */}
      <div className="absolute left-3 bottom-3 flex flex-wrap items-center gap-x-3 gap-y-1.5 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>
        ))}
        <label className="flex items-center gap-1.5 text-[10px] text-muted-foreground border-l border-border/40 pl-3">
          <span>Verbindungen</span>
          <span className="text-[9px] text-muted-foreground/60">alle</span>
          <input
            type="range" min={0.45} max={0.95} step={0.05} value={threshold}
            onChange={(e) => setThreshold(Number(e.target.value))}
            className="w-20 accent-primary cursor-pointer"
            title="Nur Verbindungen ab dieser Stärke zeigen"
          />
          <span className="text-[9px] text-muted-foreground/60">stark</span>
          <span className="font-mono text-[9px] text-muted-foreground/80 min-w-[3ch] text-right">{visibleEdges}</span>
        </label>
      </div>
    </div>
  )
}
