<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { MemoryGraphLink, MemoryGraphNode } from '../api/request'
import type { PracticeResult } from '../utils/practiceGraphSession'

const props = defineProps<{
  nodes: MemoryGraphNode[]
  links: MemoryGraphLink[]
  /** 练习结果着色：错=红，对=绿，未练=原状态色 */
  practiceResults?: Record<string, PracticeResult>
  /** 全部练完后才给连线着色 */
  sessionComplete?: boolean
}>()

const emit = defineEmits<{
  select: [id: string]
}>()

const canvasRef = ref<HTMLCanvasElement | null>(null)
const wrapRef = ref<HTMLDivElement | null>(null)
const selectedId = ref<string | null>(null)
const cursorStyle = ref('default')
const hoverId = ref<string | null>(null)
const tooltipPos = ref({ x: 0, y: 0 })

const hoverTip = computed(() => {
  const id = hoverId.value
  if (!id) return null
  const node = props.nodes.find((n) => n.id === id)
  if (!node) return null
  return {
    label: node.label,
    status: node.status,
    mastery: node.mastery,
  }
})

const statusLabel: Record<string, string> = {
  new: '新词',
  learning: '学习中',
  mastered: '已掌握',
  weak: '易错词',
}

interface SimNode extends MemoryGraphNode {
  x: number
  y: number
  vx: number
  vy: number
  pinned?: boolean
}

let simNodes: SimNode[] = []
let animId = 0
let width = 0
let height = 0
let draggingId: string | null = null
let dragOffsetX = 0
let dragOffsetY = 0
let pointerMoved = false
let frameCount = 0

const statusColor: Record<string, string> = {
  new: '#94a3b8',
  learning: '#4f6ef7',
  mastered: '#22c55e',
  weak: '#ef4444',
}

const COLOR_CORRECT = '#22c55e'
const COLOR_WRONG = '#ef4444'

function nodeFillColor(n: MemoryGraphNode): string {
  const pr = props.practiceResults?.[n.id]
  if (pr === 'wrong') return COLOR_WRONG
  if (pr === 'correct') return COLOR_CORRECT
  return statusColor[n.status] || '#64748b'
}

function linkStrokeStyle(l: MemoryGraphLink): { color: string; width: number; dash: number[] } {
  if (props.sessionComplete && props.practiceResults) {
    const a = props.practiceResults[l.source]
    const b = props.practiceResults[l.target]
    if (a === 'wrong' || b === 'wrong') {
      return { color: 'rgba(239,68,68,0.65)', width: 2, dash: [] }
    }
    if (a === 'correct' && b === 'correct') {
      return { color: 'rgba(34,197,94,0.55)', width: 1.5, dash: [] }
    }
    return { color: 'rgba(148,163,184,0.35)', width: 1, dash: [3, 3] }
  }
  if (l.kind === 'co_review') {
    return { color: 'rgba(79,110,247,0.45)', width: 1.5, dash: [] }
  }
  if (l.kind === 'similar') {
    return { color: 'rgba(245,158,11,0.4)', width: 1, dash: [4, 4] }
  }
  return { color: 'rgba(148,163,184,0.3)', width: 1, dash: [2, 3] }
}

function nodeRadius(n: SimNode) {
  return 6 + n.size * 0.25
}

function clampPos(x: number, y: number) {
  return {
    x: Math.max(24, Math.min(width - 24, x)),
    y: Math.max(24, Math.min(height - 24, y)),
  }
}

function pointerPos(e: PointerEvent) {
  const canvas = canvasRef.value!
  const rect = canvas.getBoundingClientRect()
  return {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
  }
}

function hitNode(x: number, y: number): SimNode | null {
  for (let i = simNodes.length - 1; i >= 0; i--) {
    const n = simNodes[i]
    const r = nodeRadius(n) + 10
    if ((x - n.x) ** 2 + (y - n.y) ** 2 <= r * r) return n
  }
  return null
}

function initSim() {
  simNodes = props.nodes.map((n, i) => {
    const angle = (i / Math.max(props.nodes.length, 1)) * Math.PI * 2
    const r = Math.min(width, height) * 0.28
    return {
      ...n,
      x: width / 2 + Math.cos(angle) * r,
      y: height / 2 + Math.sin(angle) * r,
      vx: 0,
      vy: 0,
    }
  })
}

function tick() {
  const centerX = width / 2
  const centerY = height / 2

  const heavy = simNodes.length > 35
  const pairStep = heavy ? 2 : 1

  for (let i = 0; i < simNodes.length; i += pairStep) {
    for (let j = i + 1; j < simNodes.length; j += pairStep) {
      const a = simNodes[i]
      const b = simNodes[j]
      if (a.id === draggingId || b.id === draggingId) continue
      let dx = a.x - b.x
      let dy = a.y - b.y
      let dist = Math.sqrt(dx * dx + dy * dy) || 1
      const repulse = (12000 / (dist * dist)) * 0.016
      dx /= dist
      dy /= dist
      a.vx += dx * repulse
      a.vy += dy * repulse
      b.vx -= dx * repulse
      b.vy -= dy * repulse
    }
  }

  for (const l of props.links) {
    const a = simNodes.find((n) => n.id === l.source)
    const b = simNodes.find((n) => n.id === l.target)
    if (!a || !b) continue
    if (a.id === draggingId) {
      const dx = b.x - a.x
      const dy = b.y - a.y
      const dist = Math.sqrt(dx * dx + dy * dy) || 1
      const pull = (dist - 90) * 0.004 * l.strength
      b.vx -= (dx / dist) * pull * 0.5
      b.vy -= (dy / dist) * pull * 0.5
      continue
    }
    if (b.id === draggingId) {
      const dx = b.x - a.x
      const dy = b.y - a.y
      const dist = Math.sqrt(dx * dx + dy * dy) || 1
      const pull = (dist - 90) * 0.004 * l.strength
      a.vx += (dx / dist) * pull * 0.5
      a.vy += (dy / dist) * pull * 0.5
      continue
    }
    const dx = b.x - a.x
    const dy = b.y - a.y
    const dist = Math.sqrt(dx * dx + dy * dy) || 1
    const pull = (dist - 90) * 0.004 * l.strength
    a.vx += (dx / dist) * pull
    a.vy += (dy / dist) * pull
    b.vx -= (dx / dist) * pull
    b.vy -= (dy / dist) * pull
  }

  for (const n of simNodes) {
    if (n.id === draggingId) continue
    n.vx += (centerX - n.x) * 0.0008
    n.vy += (centerY - n.y) * 0.0008
    n.vx *= 0.86
    n.vy *= 0.86
    n.x += n.vx
    n.y += n.vy
    const c = clampPos(n.x, n.y)
    n.x = c.x
    n.y = c.y
  }
}

function draw() {
  const canvas = canvasRef.value
  if (!canvas) return
  const ctx = canvas.getContext('2d')
  if (!ctx) return

  ctx.clearRect(0, 0, width, height)

  for (const l of props.links) {
    const a = simNodes.find((n) => n.id === l.source)
    const b = simNodes.find((n) => n.id === l.target)
    if (!a || !b) continue
    ctx.beginPath()
    ctx.moveTo(a.x, a.y)
    ctx.lineTo(b.x, b.y)
    const style = linkStrokeStyle(l)
    ctx.strokeStyle = style.color
    ctx.lineWidth = style.width
    ctx.setLineDash(style.dash)
    ctx.stroke()
    ctx.setLineDash([])
  }

  for (const n of simNodes) {
    const r = nodeRadius(n)
    const color = nodeFillColor(n)
    ctx.beginPath()
    ctx.arc(n.x, n.y, r, 0, Math.PI * 2)
    ctx.fillStyle = n.id === selectedId.value ? color : color + 'cc'
    ctx.fill()
    if (n.id === selectedId.value) {
      ctx.strokeStyle = '#1e293b'
      ctx.lineWidth = 2
      ctx.stroke()
    }
    ctx.fillStyle = '#1e293b'
    ctx.font = '10px system-ui'
    ctx.textAlign = 'center'
    ctx.fillText(n.label, n.x, n.y + r + 11)
  }
}

function loop() {
  frameCount += 1
  const heavy = simNodes.length > 35
  const runPhysics = !heavy || frameCount % 2 === 0 || draggingId !== null
  if (runPhysics) tick()
  draw()
  animId = requestAnimationFrame(loop)
}

function resize() {
  const canvas = canvasRef.value
  const wrap = wrapRef.value
  if (!canvas || !wrap) return
  width = wrap.clientWidth
  height = Math.max(280, wrap.clientHeight - 36)
  canvas.width = width * devicePixelRatio
  canvas.height = height * devicePixelRatio
  canvas.style.width = `${width}px`
  canvas.style.height = `${height}px`
  const ctx = canvas.getContext('2d')
  if (ctx) ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
}

function updateHover(clientX: number, clientY: number) {
  if (draggingId) return
  const canvas = canvasRef.value
  const wrap = wrapRef.value
  if (!canvas || !wrap) return

  const canvasRect = canvas.getBoundingClientRect()
  const inside =
    clientX >= canvasRect.left &&
    clientX <= canvasRect.right &&
    clientY >= canvasRect.top &&
    clientY <= canvasRect.bottom

  if (!inside) {
    hoverId.value = null
    return
  }

  const x = clientX - canvasRect.left
  const y = clientY - canvasRect.top
  const hit = hitNode(x, y)
  cursorStyle.value = hit ? 'grab' : 'default'

  if (hit) {
    hoverId.value = hit.id
    const wrapRect = wrap.getBoundingClientRect()
    tooltipPos.value = {
      x: Math.min(clientX - wrapRect.left + 12, wrapRect.width - 220),
      y: Math.max(clientY - wrapRect.top - 12, 8),
    }
  } else {
    hoverId.value = null
  }
}

function onPointerDown(e: PointerEvent) {
  const { x, y } = pointerPos(e)
  const hit = hitNode(x, y)
  if (!hit) return

  draggingId = hit.id
  dragOffsetX = x - hit.x
  dragOffsetY = y - hit.y
  hit.vx = 0
  hit.vy = 0
  pointerMoved = false
  selectedId.value = hit.id
  emit('select', hit.id)
  cursorStyle.value = 'grabbing'
  canvasRef.value?.setPointerCapture(e.pointerId)
  e.preventDefault()
}

function onPointerMove(e: PointerEvent) {
  const { x, y } = pointerPos(e)

  if (draggingId) {
    const n = simNodes.find((node) => node.id === draggingId)
    if (n) {
      pointerMoved = true
      hoverId.value = null
      const c = clampPos(x - dragOffsetX, y - dragOffsetY)
      n.x = c.x
      n.y = c.y
      n.vx = 0
      n.vy = 0
    }
    e.preventDefault()
    return
  }

  updateHover(e.clientX, e.clientY)
}

function onPointerUp(e: PointerEvent) {
  if (draggingId) {
    const n = simNodes.find((node) => node.id === draggingId)
    if (n && !pointerMoved) {
      selectedId.value = n.id
      emit('select', n.id)
    }
    draggingId = null
    canvasRef.value?.releasePointerCapture(e.pointerId)
  }
  pointerMoved = false
  cursorStyle.value = 'default'
  updateHover(e.clientX, e.clientY)
}

function onResize() {
  resize()
  if (!draggingId) initSim()
}

function onWrapMouseMove(e: MouseEvent) {
  updateHover(e.clientX, e.clientY)
}

function onWrapMouseLeave() {
  if (!draggingId) hoverId.value = null
}

onMounted(() => {
  const canvas = canvasRef.value
  const wrap = wrapRef.value
  if (!canvas || !wrap) return
  resize()
  initSim()
  loop()
  window.addEventListener('resize', onResize)
  wrap.addEventListener('mousemove', onWrapMouseMove)
  wrap.addEventListener('mouseleave', onWrapMouseLeave)
  canvas.addEventListener('pointerdown', onPointerDown)
  canvas.addEventListener('pointermove', onPointerMove)
  canvas.addEventListener('pointerup', onPointerUp)
  canvas.addEventListener('pointercancel', onPointerUp)
})

watch(
  () => [props.nodes, props.links, props.practiceResults, props.sessionComplete],
  () => {
    resize()
    draggingId = null
    initSim()
  },
  { deep: true }
)

onBeforeUnmount(() => {
  cancelAnimationFrame(animId)
  window.removeEventListener('resize', onResize)
  const canvas = canvasRef.value
  const wrap = wrapRef.value
  wrap?.removeEventListener('mousemove', onWrapMouseMove)
  wrap?.removeEventListener('mouseleave', onWrapMouseLeave)
  if (!canvas) return
  canvas.removeEventListener('pointerdown', onPointerDown)
  canvas.removeEventListener('pointermove', onPointerMove)
  canvas.removeEventListener('pointerup', onPointerUp)
  canvas.removeEventListener('pointercancel', onPointerUp)
})
</script>

<template>
  <div ref="wrapRef" class="graph-wrap">
    <canvas
      ref="canvasRef"
      class="graph-canvas"
      :style="{ cursor: cursorStyle }"
    />
    <div
      v-if="hoverTip"
      class="graph-tooltip"
      :style="{ left: `${tooltipPos.x}px`, top: `${tooltipPos.y}px` }"
    >
      <div class="tip-title">{{ hoverTip.label }}</div>
      <div class="tip-meta">
        {{ statusLabel[hoverTip.status] || hoverTip.status }} · 掌握 {{ hoverTip.mastery }}%
      </div>
      <div class="tip-hint">点击查看释义</div>
    </div>
    <p class="graph-hint">悬停看状态 · 点击查看释义 · 拖动调整节点</p>
  </div>
</template>

<style scoped>
.graph-wrap {
  position: relative;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: #fafbff;
  overflow: hidden;
}
.graph-wrap:has(.graph-tooltip) {
  overflow: visible;
}
.graph-tooltip {
  position: absolute;
  z-index: 20;
  max-width: 240px;
  padding: 8px 10px;
  border-radius: 8px;
  background: rgba(30, 41, 59, 0.92);
  color: #fff;
  font-size: 12px;
  pointer-events: none;
  line-height: 1.4;
}
.tip-title {
  font-weight: 600;
  margin-bottom: 2px;
}
.tip-meta {
  opacity: 0.85;
  font-size: 11px;
}
.tip-hint {
  margin-top: 4px;
  font-size: 10px;
  opacity: 0.7;
}
.graph-canvas {
  display: block;
  touch-action: none;
  user-select: none;
}
.graph-hint {
  font-size: 11px;
  color: var(--muted);
  padding: 8px 12px;
  margin: 0;
  border-top: 1px solid var(--border);
}
</style>
