import { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import ForceGraph3D, { type ForceGraphMethods } from 'react-force-graph-3d'
import { CanvasTexture, Sprite, SpriteMaterial } from 'three'
import type { DictionaryDefinition, SchemaDependent } from '@/api/client'

/**
 * 3D-режим графа справочников через react-force-graph-3d (three.js +
 * d3-force-3d). Lazy-импортируется из DictionaryGraph чтобы three.js
 * (~650kb) не попадал в default chunk.
 *
 * Космос-стиль:
 *  - Сферы с bundle цветами (terracotta / green / gold / brown / mute)
 *  - Hovered planet → accent (warm orange) glow
 *  - Label-sprite (canvas-rendered text) над каждой планетой: displayName
 *    + record count. Sprite автоматически billboard-ит к камере.
 *  - Edges: subtle violet stardust → bright accent при hover, directional
 *    particles на hovered chain (comet-trail feel).
 *  - Background: transparent (container's radial gradient просвечивает).
 */

type Node3D = {
  id: string
  name: string
  displayName: string
  bundle: string
  scope: string
  recordCount: number
  // Internal three coords — populated by force engine
  x?: number
  y?: number
  z?: number
}

type Link3D = {
  source: string | Node3D
  target: string | Node3D
  sourceField: string
  targetField: string
}

const BUNDLE_COLORS: Record<string, string> = {
  cuod: '#d97757',
  geo: '#5b8044',
  comms: '#b8902f',
  delivery: '#7a5644',
  default: '#7a76b8',
}

const colorForBundle = (bundle: string): string =>
  BUNDLE_COLORS[bundle] ?? BUNDLE_COLORS.default

// Space-palette accents reused across canvases / edges.
const ACCENT_HEX = '#ff9a64'
const EDGE_DIM_HEX = '#4a4470'
const LABEL_FG = '#f0eaff'
const LABEL_SUB = '#a09bc8'
const LABEL_BG = 'rgba(28, 24, 64, 0.85)'
const LABEL_BORDER = 'rgba(61, 56, 99, 1)'

/**
 * Рисует label-sprite для одной планеты. Canvas 256×96, текст в 2 строки:
 *  1) displayName (12px, белый)
 *  2) record count + bundle (10px, mute violet, accent для count > 0)
 *
 * Прячем за rounded-pill background для читаемости поверх любого фона.
 * Sprite scale (16 units high) подобран так, чтобы text был ~ размер node
 * sphere'ы при default camera position.
 */
function makeLabelSprite(node: Node3D): Sprite {
  const dpr = typeof window !== 'undefined' ? Math.min(window.devicePixelRatio ?? 1, 2) : 1
  const cssW = 256
  const cssH = 96
  const canvas = document.createElement('canvas')
  canvas.width = cssW * dpr
  canvas.height = cssH * dpr
  const ctx = canvas.getContext('2d')
  if (!ctx) {
    // Fallback: пустой sprite если ctx недоступен (very rare).
    const mat = new SpriteMaterial({})
    return new Sprite(mat)
  }
  ctx.scale(dpr, dpr)
  ctx.textBaseline = 'middle'

  // Background pill — rounded rect для контраста с фоном (космос-bg
  // переменный, нужен якорь).
  const padX = 14
  const padY = 12
  ctx.fillStyle = LABEL_BG
  ctx.strokeStyle = LABEL_BORDER
  ctx.lineWidth = 1
  const r = 10
  const x = padX
  const y = padY
  const w = cssW - padX * 2
  const h = cssH - padY * 2
  ctx.beginPath()
  ctx.moveTo(x + r, y)
  ctx.lineTo(x + w - r, y)
  ctx.quadraticCurveTo(x + w, y, x + w, y + r)
  ctx.lineTo(x + w, y + h - r)
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
  ctx.lineTo(x + r, y + h)
  ctx.quadraticCurveTo(x, y + h, x, y + h - r)
  ctx.lineTo(x, y + r)
  ctx.quadraticCurveTo(x, y, x + r, y)
  ctx.closePath()
  ctx.fill()
  ctx.stroke()

  // Title — ellipsis в canvas руками, canvas нет text-overflow.
  ctx.fillStyle = LABEL_FG
  ctx.font = '600 18px ui-sans-serif, system-ui, sans-serif'
  const maxTitleW = w - 24
  let title = node.displayName
  if (ctx.measureText(title).width > maxTitleW) {
    while (title.length > 1 && ctx.measureText(title + '…').width > maxTitleW) {
      title = title.slice(0, -1)
    }
    title = title + '…'
  }
  ctx.fillText(title, x + 12, y + 22)

  // Sub-row: bundle code · scope (left), record count (right, accent).
  ctx.font = '500 14px ui-monospace, SFMono-Regular, Menlo, monospace'
  const subRaw = `${node.name} · ${node.bundle}`
  let subText = subRaw
  // Reserve ~70px справа на count
  const subAvail = w - 24 - 70
  if (ctx.measureText(subText).width > subAvail) {
    while (subText.length > 1 && ctx.measureText(subText + '…').width > subAvail) {
      subText = subText.slice(0, -1)
    }
    subText = subText + '…'
  }
  ctx.fillStyle = LABEL_SUB
  ctx.fillText(subText, x + 12, y + 50)

  // Count, right-aligned.
  const countText = node.recordCount > 0 ? `${node.recordCount} зап` : '—'
  ctx.font = '600 14px ui-monospace, SFMono-Regular, Menlo, monospace'
  ctx.fillStyle = node.recordCount > 0 ? ACCENT_HEX : LABEL_SUB
  const cw = ctx.measureText(countText).width
  ctx.fillText(countText, x + w - 12 - cw, y + 50)

  const texture = new CanvasTexture(canvas)
  // Filtering improves readability на subpixel positions.
  texture.needsUpdate = true
  // Без mipmaps — sharper text при отдалении.
  texture.minFilter = 1006 // LinearFilter
  texture.magFilter = 1006 // LinearFilter

  const material = new SpriteMaterial({
    map: texture,
    transparent: true,
    depthWrite: false, // не блокируем edges за sprite'ом
  })
  const sprite = new Sprite(material)
  // Scale: ширина sprite 32 units в world space → ~ диаметр большой планеты.
  // Aspect ratio: cssW/cssH = 256/96 ≈ 2.67.
  sprite.scale.set(28, 10.5, 1)
  // Поднимаем sprite над сферой (sphere radius максимум ~5).
  sprite.position.set(0, 10, 0)
  return sprite
}

type Props = {
  dicts: DictionaryDefinition[]
  dependentsByDict: Map<string, SchemaDependent[]>
  recordCountByDict: Map<string, number>
  width: number
  height: number
}

export default function DictionaryGraph3D({
  dicts,
  dependentsByDict,
  recordCountByDict,
  width,
  height,
}: Props) {
  const navigate = useNavigate()
  const graphRef = useRef<ForceGraphMethods<Node3D, Link3D> | undefined>(undefined)
  const [hoverId, setHoverId] = useState<string | null>(null)

  // Build graph data — same logic as 2D mode.
  const graphData = useMemo(() => {
    const nodes: Node3D[] = dicts.map((d) => ({
      id: d.name,
      name: d.name,
      displayName: d.displayName ?? d.name,
      bundle: d.bundle,
      scope: d.scope,
      recordCount: recordCountByDict.get(d.name) ?? d.recordCount ?? 0,
    }))
    const nodeIds = new Set(nodes.map((n) => n.id))
    const links: Link3D[] = []
    dicts.forEach((target) => {
      const deps = dependentsByDict.get(target.name) ?? []
      deps.forEach((dep) => {
        if (nodeIds.has(dep.sourceDict)) {
          links.push({
            source: dep.sourceDict,
            target: target.name,
            sourceField: dep.sourceField,
            targetField: dep.targetField,
          })
        }
      })
    })
    return { nodes, links }
  }, [dicts, dependentsByDict, recordCountByDict])

  // Tune camera once after mount.
  useEffect(() => {
    const fg = graphRef.current
    if (!fg) return
    fg.cameraPosition({ x: 0, y: 0, z: 500 })
  }, [])

  return (
    <div className="relative w-full h-full">
      <ForceGraph3D<Node3D, Link3D>
        ref={graphRef}
        graphData={graphData}
        width={width}
        height={height}
        backgroundColor="rgba(0,0,0,0)"
        nodeId="id"
        // Tooltip отключаем — sprite уже показывает inline label с тем же
        // contentом, native tooltip был бы дублёром.
        nodeLabel={() => ''}
        nodeColor={(n) => (hoverId === n.id ? ACCENT_HEX : colorForBundle(n.bundle))}
        // nodeVal — "volume" по three convention, sphere radius ≈ cbrt(nodeVal)
        // * nodeRelSize/4. Sqrt-шкала с большим коэффициентом чтобы планеты
        // визуально отличались по counts (раньше log10 давал почти одинаковые
        // sphere).
        //   records=0   → nodeVal=1   (sphere radius ~1.0)
        //   records=5   → nodeVal=4.4 (~1.6)
        //   records=14  → nodeVal=6.6 (~1.9)
        //   records=100 → nodeVal=16  (~2.5)
        nodeVal={(n) => 1 + Math.sqrt(n.recordCount ?? 0) * 1.5}
        nodeOpacity={hoverId ? 0.85 : 0.95}
        nodeResolution={20}
        // Custom three.js object для каждого node — Sprite с текстом
        // displayName + count. nodeThreeObjectExtend=true → sprite
        // дополняет default sphere (sphere остаётся, sprite добавляется).
        nodeThreeObject={makeLabelSprite}
        nodeThreeObjectExtend={true}
        linkColor={(l) => {
          const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id
          const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id
          return hoverId && (sid === hoverId || tid === hoverId) ? ACCENT_HEX : EDGE_DIM_HEX
        }}
        linkOpacity={hoverId ? 0.55 : 0.45}
        linkWidth={(l) => {
          const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id
          const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id
          return hoverId && (sid === hoverId || tid === hoverId) ? 2 : 0.6
        }}
        linkDirectionalArrowLength={3}
        linkDirectionalArrowRelPos={1}
        linkDirectionalArrowColor={() => ACCENT_HEX}
        linkDirectionalParticles={(l) => {
          const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id
          const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id
          return hoverId && (sid === hoverId || tid === hoverId) ? 3 : 0
        }}
        linkDirectionalParticleSpeed={0.006}
        linkDirectionalParticleWidth={2.5}
        linkDirectionalParticleColor={() => ACCENT_HEX}
        onNodeHover={(n) => setHoverId(n?.id ?? null)}
        onNodeClick={(n) => navigate({ to: '/dictionaries/$name', params: { name: n.name } })}
        cooldownTicks={150}
      />
    </div>
  )
}
