'use client'

import { useId, useMemo } from 'react'
import {
  Area,
  AreaChart,
  Bar,
  BarChart,
  CartesianGrid,
  Line,
  LineChart,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from 'recharts'
import {
  badgeGlyph,
  dimOpacity,
  intentColor,
  spotlightColor,
} from '@/lib/interactive-demo/intent-colors'
import type { DemoScene, Panel } from '@/lib/interactive-demo/types'
import type {
  ResolvedAnnotation,
  StepResolvedState,
} from '@/lib/interactive-demo/resolve'
import { useDarkMode } from '@/components/interactive-demo/demo-speak'
import { DemoRichLabel } from '@/components/interactive-demo/demo-rich-label'
import { cn } from '@/lib/utils'

type AnnKind = ResolvedAnnotation['kind']

export type DemoSceneViewProps = {
  scene: DemoScene
  state: StepResolvedState
  reducedMotion?: boolean
  className?: string
}

type NodePos = {
  x: number
  y: number
  w: number
  h: number
}

function elementOpacity(
  id: string,
  state: StepResolvedState,
  dim: number
): number {
  // Overview = full scene at full brightness (matches edges behavior)
  if (state.overview) return 1
  if (!(id in state.revealed)) return 0
  if (state.spotlight.length === 0) return 1
  return state.spotlight.includes(id) ? 1 : dim
}

function estimateNodeSize(label: string): { w: number; h: number } {
  const lines = label.split(/\\n|\n/)
  const hasMath = /\$/.test(label)
  const visualLen = (s: string) =>
    s.replace(/\$[^$]+\$/g, (m) => 'x'.repeat(Math.min(18, Math.max(6, m.length * 0.45))))
      .length
  const longest = Math.max(...lines.map(visualLen), 6)
  const w = Math.min(280, Math.max(128, longest * 7.4 + 40))
  const h = Math.max(
    hasMath ? 58 : 48,
    30 + lines.length * (hasMath ? 24 : 18)
  )
  return { w, h }
}

/** Prefer circular layout when edges form a closed loop covering most nodes. */
function findCycleOrder(
  ids: string[],
  edges: { from: string; to: string }[]
): string[] | null {
  if (ids.length < 3) return null
  const idSet = new Set(ids)
  const outs = new Map<string, string[]>()
  for (const id of ids) outs.set(id, [])
  for (const e of edges) {
    if (!idSet.has(e.from) || !idSet.has(e.to) || e.from === e.to) continue
    outs.get(e.from)!.push(e.to)
  }

  for (const start of ids) {
    const path = [start]
    const seen = new Set([start])
    let cur = start
    while (path.length < ids.length) {
      const nexts = (outs.get(cur) ?? []).filter((t) => !seen.has(t))
      if (nexts.length === 0) break
      // Prefer continuing a simple cycle (single unused neighbor)
      const n = nexts[0]!
      path.push(n)
      seen.add(n)
      cur = n
    }
    const closes = (outs.get(cur) ?? []).includes(start)
    if (closes && path.length >= 3 && path.length >= Math.ceil(ids.length * 0.75)) {
      return path
    }
  }
  return null
}

/**
 * Layout strategies:
 * 1. AttnRes trunk pattern (residualTrunk + layers)
 * 2. Closed cycle → circular
 * 3. DAG layered (top → bottom)
 * 4. Fallback grid
 */
function layoutNodes(
  nodes: { id: string; label?: string }[],
  edges: { from: string; to: string }[]
): { positions: Map<string, NodePos>; width: number; height: number } {
  const sizes = new Map(
    nodes.map((n) => [n.id, estimateNodeSize(n.label ?? n.id)] as const)
  )
  const positions = new Map<string, NodePos>()
  const padX = 40
  const padY = 36
  const gapX = 52
  const gapY = 32

  const trunk = nodes.find((n) => n.id === 'residualTrunk')
  const layersOnly = nodes.filter((n) => n.id !== 'residualTrunk')

  if (trunk && layersOnly.length >= 2) {
    const layerSizes = layersOnly.map((n) => sizes.get(n.id)!)
    const maxLayerW = Math.max(...layerSizes.map((s) => s.w), 96)
    const trunkSize = sizes.get(trunk.id)!
    const contentH =
      padY * 2 +
      layerSizes.reduce((acc, s) => acc + s.h, 0) +
      gapY * Math.max(0, layersOnly.length - 1)
    const height = Math.max(240, contentH)
    const width = padX * 2 + maxLayerW + gapX + trunkSize.w + 24

    let y = padY
    layersOnly.forEach((n) => {
      const s = sizes.get(n.id)!
      positions.set(n.id, {
        x: padX + maxLayerW / 2,
        y: y + s.h / 2,
        w: s.w,
        h: s.h,
      })
      y += s.h + gapY
    })
    positions.set(trunk.id, {
      x: padX + maxLayerW + gapX + trunkSize.w / 2,
      y: height / 2,
      w: trunkSize.w,
      h: trunkSize.h,
    })
    return { positions, width, height }
  }

  const cycle = findCycleOrder(
    nodes.map((n) => n.id),
    edges
  )
  if (cycle) {
    const maxW = Math.max(...cycle.map((id) => sizes.get(id)!.w), 128)
    const maxH = Math.max(...cycle.map((id) => sizes.get(id)!.h), 48)
    const radius = Math.max(110, (cycle.length * 42) / (2 * Math.PI) + maxW * 0.35)
    const leftovers = nodes.filter((n) => !cycle.includes(n.id))
    const leftoverRowH = leftovers.length ? maxH + gapY : 0
    const width = Math.max(
      420,
      padX * 2 + radius * 2 + maxW,
      padX * 2 + leftovers.reduce((acc, n) => acc + sizes.get(n.id)!.w, 0) + gapX * Math.max(0, leftovers.length - 1)
    )
    const height = Math.max(360, padY * 2 + radius * 2 + maxH + leftoverRowH)
    const cx = width / 2
    const cy = padY + radius + maxH / 2
    cycle.forEach((id, i) => {
      const s = sizes.get(id)!
      const angle = -Math.PI / 2 + (2 * Math.PI * i) / cycle.length
      positions.set(id, {
        x: cx + radius * Math.cos(angle),
        y: cy + radius * Math.sin(angle),
        w: s.w,
        h: s.h,
      })
    })
    // Leftover nodes: horizontal row BELOW the ring (never overlapping it)
    if (leftovers.length) {
      const totalW =
        leftovers.reduce((acc, n) => acc + sizes.get(n.id)!.w, 0) +
        gapX * (leftovers.length - 1)
      let x = cx - totalW / 2
      const y = cy + radius + maxH / 2 + gapY + maxH / 2
      for (const n of leftovers) {
        const s = sizes.get(n.id)!
        positions.set(n.id, { x: x + s.w / 2, y, w: s.w, h: s.h })
        x += s.w + gapX
      }
    }
    return { positions, width, height }
  }

  // Topological layers (sources at top)
  const ids = nodes.map((n) => n.id)
  const idSet = new Set(ids)
  const indeg = new Map(ids.map((id) => [id, 0]))
  const outs = new Map(ids.map((id) => [id, [] as string[]]))
  for (const e of edges) {
    if (!idSet.has(e.from) || !idSet.has(e.to) || e.from === e.to) continue
    indeg.set(e.to, (indeg.get(e.to) ?? 0) + 1)
    outs.get(e.from)!.push(e.to)
  }

  const queue = ids.filter((id) => (indeg.get(id) ?? 0) === 0)
  const order: string[] = []
  const depth = new Map<string, number>()
  queue.forEach((id) => depth.set(id, 0))
  const q = [...queue]
  while (q.length) {
    const u = q.shift()!
    order.push(u)
    for (const v of outs.get(u) ?? []) {
      depth.set(v, Math.max(depth.get(v) ?? 0, (depth.get(u) ?? 0) + 1))
      indeg.set(v, (indeg.get(v) ?? 1) - 1)
      if ((indeg.get(v) ?? 0) === 0) q.push(v)
    }
  }

  const isDag = order.length === ids.length && edges.length > 0

  if (isDag) {
    const byDepth = new Map<number, string[]>()
    for (const id of ids) {
      const d = depth.get(id) ?? 0
      if (!byDepth.has(d)) byDepth.set(d, [])
      byDepth.get(d)!.push(id)
    }
    const maxDepth = Math.max(...byDepth.keys(), 0)
    const rowHeights: number[] = []
    let width = padX * 2
    for (let d = 0; d <= maxDepth; d++) {
      const row = byDepth.get(d) ?? []
      const rowH = Math.max(...row.map((id) => sizes.get(id)!.h), 36)
      rowHeights.push(rowH)
      const rowW =
        row.reduce((acc, id) => acc + sizes.get(id)!.w, 0) +
        gapX * Math.max(0, row.length - 1)
      width = Math.max(width, padX * 2 + rowW)
    }
    const height =
      padY * 2 +
      rowHeights.reduce((a, b) => a + b, 0) +
      gapY * Math.max(0, maxDepth)

    let y = padY
    for (let d = 0; d <= maxDepth; d++) {
      const row = byDepth.get(d) ?? []
      const rowH = rowHeights[d]!
      const rowW =
        row.reduce((acc, id) => acc + sizes.get(id)!.w, 0) +
        gapX * Math.max(0, row.length - 1)
      let x = (width - rowW) / 2
      for (const id of row) {
        const s = sizes.get(id)!
        positions.set(id, {
          x: x + s.w / 2,
          y: y + rowH / 2,
          w: s.w,
          h: s.h,
        })
        x += s.w + gapX
      }
      y += rowH + gapY
    }
    return { positions, width: Math.max(400, width), height: Math.max(220, height) }
  }

  // Grid fallback
  const cols = Math.min(3, Math.max(1, Math.ceil(Math.sqrt(nodes.length))))
  const rows = Math.ceil(nodes.length / cols)
  const colW: number[] = Array.from({ length: cols }, (_, c) => {
    let max = 120
    nodes.forEach((n, i) => {
      if (i % cols === c) max = Math.max(max, sizes.get(n.id)!.w)
    })
    return max
  })
  const rowH: number[] = Array.from({ length: rows }, (_, r) => {
    let max = 48
    nodes.forEach((n, i) => {
      if (Math.floor(i / cols) === r) max = Math.max(max, sizes.get(n.id)!.h)
    })
    return max
  })
  const width =
    padX * 2 + colW.reduce((a, b) => a + b, 0) + gapX * Math.max(0, cols - 1)
  const height =
    padY * 2 + rowH.reduce((a, b) => a + b, 0) + gapY * Math.max(0, rows - 1)

  nodes.forEach((n, i) => {
    const c = i % cols
    const r = Math.floor(i / cols)
    const s = sizes.get(n.id)!
    const xOff =
      padX +
      colW.slice(0, c).reduce((a, b) => a + b, 0) +
      gapX * c +
      colW[c]! / 2
    const yOff =
      padY +
      rowH.slice(0, r).reduce((a, b) => a + b, 0) +
      gapY * r +
      rowH[r]! / 2
    positions.set(n.id, { x: xOff, y: yOff, w: s.w, h: s.h })
  })

  return { positions, width: Math.max(400, width), height: Math.max(220, height) }
}

/** Border intersection: line from center A → center B, clipped to rect A. */
function borderPoint(
  from: NodePos,
  to: NodePos
): { x: number; y: number } {
  const dx = to.x - from.x
  const dy = to.y - from.y
  if (dx === 0 && dy === 0) return { x: from.x, y: from.y }
  const hw = from.w / 2
  const hh = from.h / 2
  const ax = Math.abs(dx) / (hw || 1)
  const ay = Math.abs(dy) / (hh || 1)
  const t = 1 / Math.max(ax, ay)
  return { x: from.x + dx * t, y: from.y + dy * t }
}

function edgePath(a: NodePos, b: NodePos, tipInset = 10): string {
  const p0 = borderPoint(a, b)
  const p1 = borderPoint(b, a)
  const dx = p1.x - p0.x
  const dy = p1.y - p0.y
  const len = Math.hypot(dx, dy) || 1
  // Stop short of the target so markerEnd tip lands on the node border, not under the card
  const inset = Math.min(tipInset, len * 0.35)
  const endX = p1.x - (dx / len) * inset
  const endY = p1.y - (dy / len) * inset
  const mx = (p0.x + endX) / 2
  const my = (p0.y + endY) / 2
  const bend = Math.min(36, len * 0.22)
  const cx = mx - (dy / len) * bend
  const cy = my + (dx / len) * bend
  return `M ${p0.x} ${p0.y} Q ${cx} ${cy} ${endX} ${endY}`
}

function AnnotationsOverlay({
  annotations,
  getAnchor,
  dark,
  markerId,
  shadowId,
}: {
  annotations: ResolvedAnnotation[]
  getAnchor: (
    id: string,
    kind: AnnKind
  ) => { x: number; y: number } | null
  dark: boolean
  markerId: string
  shadowId?: string
}) {
  const outline = spotlightColor(dark)
  return (
    <g className="demo-annotations" pointerEvents="none">
      {annotations.map((ann, i) => {
        const target = ann.targetIds[0]
        if (!target) return null
        const pos = getAnchor(target, ann.kind)
        if (!pos) return null
        const color = intentColor(ann.intent, dark)
        const key = `${ann.kind}-${target}-${i}`

        if (ann.kind === 'circle') {
          return (
            <circle
              key={key}
              cx={pos.x}
              cy={pos.y}
              r={22}
              fill="none"
              stroke={color}
              strokeWidth={2.5}
              opacity={0.9}
            />
          )
        }
        if (ann.kind === 'badge') {
          const n = ann.badgeIndex ?? i + 1
          return (
            <g key={key} transform={`translate(${pos.x}, ${pos.y})`}>
              <circle r={11} fill={outline} opacity={0.95} />
              <text
                textAnchor="middle"
                dominantBaseline="central"
                fill="#fff"
                fontSize={11}
                fontWeight={700}
                fontFamily="ui-sans-serif, system-ui, sans-serif"
              >
                {badgeGlyph(n)}
              </text>
            </g>
          )
        }
        if (ann.kind === 'callout') {
          const text = ann.text ?? ''
          const w = Math.min(200, Math.max(72, text.length * 6.5 + 20))
          return (
            <g key={key} transform={`translate(${pos.x}, ${pos.y})`}>
              <rect
                x={8}
                y={-14}
                width={w}
                height={28}
                rx={8}
                fill={dark ? '#1a1a1a' : '#ffffff'}
                stroke={color}
                strokeWidth={1.5}
                filter={shadowId ? `url(#${shadowId})` : undefined}
              />
              <text
                x={18}
                y={5}
                fontSize={12}
                fontWeight={500}
                fill={dark ? '#f0f0f0' : '#1a1a1a'}
                fontFamily="ui-sans-serif, system-ui, sans-serif"
              >
                {text}
              </text>
            </g>
          )
        }
        if (ann.kind === 'arrow') {
          return (
            <path
              key={key}
              d={`M ${pos.x - 28} ${pos.y - 28} L ${pos.x - 6} ${pos.y - 6}`}
              stroke={color}
              strokeWidth={2.5}
              strokeLinecap="round"
              markerEnd={`url(#${markerId})`}
              fill="none"
            />
          )
        }
        return null
      })}
    </g>
  )
}

function SvgScenePanel({
  panel,
  state,
  reducedMotion,
  dark,
}: {
  panel: Extract<Panel, { type: 'svg-scene' }>
  state: StepResolvedState
  reducedMotion?: boolean
  dark: boolean
}) {
  const uid = useId().replace(/:/g, '')
  const nodes = panel.payload.nodes
  const edges = panel.payload.edges ?? []
  const dim = dimOpacity(dark)
  const outline = spotlightColor(dark)
  const markerId = `demo-arrow-${uid}`
  const dotsId = `demo-dots-${uid}`

  const { positions, width, height } = useMemo(
    () => layoutNodes(nodes, edges),
    [nodes, edges]
  )

  const transition = reducedMotion
    ? 'none'
    : 'opacity 220ms ease, transform 220ms ease, box-shadow 220ms ease'

  const gridStroke = dark ? 'rgba(255,255,255,0.07)' : 'rgba(15,23,42,0.07)'
  const boardBg = dark ? '#0c0e12' : '#f7f5f0'

  return (
    <div
      className="relative w-full overflow-hidden"
      style={{
        background: boardBg,
        aspectRatio: `${width} / ${height}`,
      }}
    >
      <svg
        viewBox={`0 0 ${width} ${height}`}
        className="absolute inset-0 h-full w-full"
        aria-hidden
      >
        <defs>
          <pattern
            id={dotsId}
            width="18"
            height="18"
            patternUnits="userSpaceOnUse"
          >
            <circle cx="1.2" cy="1.2" r="1" fill={gridStroke} />
          </pattern>
          <marker
            id={markerId}
            markerWidth="10"
            markerHeight="8"
            refX="9"
            refY="4"
            orient="auto"
            markerUnits="userSpaceOnUse"
          >
            <path d="M0,0 L10,4 L0,8 Z" fill={outline} />
          </marker>
        </defs>
        <rect width={width} height={height} fill={`url(#${dotsId})`} />

        {edges.map((e) => {
          const a = positions.get(e.from)
          const b = positions.get(e.to)
          if (!a || !b) return null
          const revealed = e.id in state.revealed || state.overview
          if (!revealed) return null
          const op = elementOpacity(e.id, state, dim)
          if (op === 0) return null
          const lit = state.overview || state.spotlight.includes(e.id)
          const stroke = intentColor(e.intent, dark)
          const widthStroke = (lit ? 2.8 : 1.8) + (e.weight ?? 1) * 0.45
          return (
            <path
              key={e.id}
              d={edgePath(a, b)}
              stroke={stroke}
              strokeWidth={widthStroke}
              strokeDasharray={e.style === 'dashed' ? '7 5' : undefined}
              strokeLinecap="round"
              fill="none"
              opacity={op}
              markerEnd={`url(#${markerId})`}
              style={{ transition }}
            />
          )
        })}

        <AnnotationsOverlay
          annotations={state.annotations}
          dark={dark}
          markerId={markerId}
          getAnchor={(id, kind) => {
            const p = positions.get(id)
            if (!p) return null
            if (kind === 'badge') {
              return { x: p.x + p.w / 2 - 2, y: p.y - p.h / 2 + 2 }
            }
            if (kind === 'callout') {
              return { x: p.x + p.w / 2 - 4, y: p.y }
            }
            return { x: p.x, y: p.y }
          }}
        />
      </svg>

      {nodes.map((n) => {
        const p = positions.get(n.id)
        if (!p) return null
        const op = elementOpacity(n.id, state, dim)
        if (op === 0) return null
        const lit = state.overview || state.spotlight.includes(n.id)
        const accent = intentColor(n.intent, dark)
        const label = n.label ?? n.id
        return (
          <div
            key={n.id}
            className={cn(
              'absolute flex flex-col justify-center rounded-2xl px-3 py-2.5 backdrop-blur-[2px]',
              dark ? 'bg-[#141820]/ee text-zinc-50' : 'bg-white/95 text-zinc-900'
            )}
            style={{
              left: `${((p.x - p.w / 2) / width) * 100}%`,
              top: `${((p.y - p.h / 2) / height) * 100}%`,
              width: `${(p.w / width) * 100}%`,
              minHeight: `${(p.h / height) * 100}%`,
              opacity: op,
              borderStyle: 'solid',
              borderTopWidth: lit ? 2 : 1.5,
              borderRightWidth: lit ? 2 : 1.5,
              borderBottomWidth: lit ? 2 : 1.5,
              borderLeftWidth: 5,
              borderTopColor: lit ? outline : accent,
              borderRightColor: lit ? outline : accent,
              borderBottomColor: lit ? outline : accent,
              borderLeftColor: accent,
              boxShadow: lit
                ? `0 0 0 3px ${outline}33, 0 10px 28px -8px ${outline}66`
                : dark
                  ? '0 8px 24px -10px rgba(0,0,0,0.65)'
                  : '0 8px 22px -10px rgba(15,23,42,0.18)',
              transition,
            }}
          >
            <DemoRichLabel text={label} lit={lit} />
          </div>
        )
      })}
    </div>
  )
}

function ChartPanel({
  panel,
  state,
  reducedMotion,
  dark,
}: {
  panel: Extract<Panel, { type: 'chart' }>
  state: StepResolvedState
  reducedMotion?: boolean
  dark: boolean
}) {
  const series = panel.payload.series
  const dim = dimOpacity(dark)
  const maxLen = Math.max(...series.map((s) => s.values.length), 0)
  const data = useMemo(() => {
    return Array.from({ length: maxLen }, (_, i) => {
      const row: Record<string, number | string> = { i: String(i + 1) }
      for (const s of series) {
        if (!(s.id in state.revealed)) continue
        row[s.id] = s.values[i] ?? 0
      }
      return row
    })
  }, [maxLen, series, state.revealed])

  const Chart =
    panel.payload.chartType === 'bar'
      ? BarChart
      : panel.payload.chartType === 'area'
        ? AreaChart
        : LineChart

  return (
    <div className="w-full h-52 min-h-[13rem] min-w-0">
      <ResponsiveContainer width="100%" height="100%">
        <Chart data={data} margin={{ top: 12, right: 16, left: 4, bottom: 8 }}>
          <CartesianGrid
            strokeDasharray="3 3"
            stroke={dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}
          />
          <XAxis
            dataKey="i"
            tick={{ fontSize: 11, fill: dark ? '#a1a1aa' : '#71717a' }}
            axisLine={false}
            tickLine={false}
          />
          <YAxis
            tick={{ fontSize: 11, fill: dark ? '#a1a1aa' : '#71717a' }}
            width={40}
            axisLine={false}
            tickLine={false}
          />
          <Tooltip
            contentStyle={{
              borderRadius: 10,
              border: '1px solid var(--border)',
              background: dark ? '#18181b' : '#fff',
              fontSize: 12,
            }}
          />
          {series.map((s) => {
            if (!(s.id in state.revealed)) return null
            const op = elementOpacity(s.id, state, dim)
            const stroke = intentColor(s.intent, dark)
            if (panel.payload.chartType === 'bar') {
              return (
                <Bar
                  key={s.id}
                  dataKey={s.id}
                  fill={stroke}
                  fillOpacity={op}
                  radius={[4, 4, 0, 0]}
                  isAnimationActive={!reducedMotion}
                />
              )
            }
            if (panel.payload.chartType === 'area') {
              return (
                <Area
                  key={s.id}
                  type="monotone"
                  dataKey={s.id}
                  stroke={stroke}
                  fill={stroke}
                  fillOpacity={op * 0.22}
                  strokeOpacity={op}
                  strokeWidth={2}
                  isAnimationActive={!reducedMotion}
                />
              )
            }
            return (
              <Line
                key={s.id}
                type="monotone"
                dataKey={s.id}
                stroke={stroke}
                strokeOpacity={op}
                strokeWidth={2.5}
                dot={false}
                isAnimationActive={!reducedMotion}
              />
            )
          })}
        </Chart>
      </ResponsiveContainer>
    </div>
  )
}

function HeatmapPanel({
  panel,
  state,
  reducedMotion,
  dark,
}: {
  panel: Extract<Panel, { type: 'heatmap-matrix' }>
  state: StepResolvedState
  reducedMotion?: boolean
  dark: boolean
}) {
  const { rows, cols, values, rowLabels, colLabels, triangular } = panel.payload
  const cell = 34
  const labelW = 44
  const labelH = 26
  const w = labelW + cols * cell + 8
  const h = labelH + rows * cell + 8
  const transition = reducedMotion ? 'none' : 'opacity 220ms ease'
  const dim = dimOpacity(dark)
  const fillIntent = spotlightColor(dark)
  const ghostStroke = dark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.16)'
  // Intensity ∝ value — normalized so real-world scales (not just [0,1]) work
  const maxAbsV = Math.max(
    1e-9,
    ...values.flat().map((v) => Math.abs(v))
  )

  const cellRect = (r: number, c: number) => ({
    x: labelW + (c - 1) * cell,
    y: labelH + (r - 1) * cell,
  })

  const positions = useMemo(() => {
    const map = new Map<
      string,
      { cx: number; cy: number; trX: number; trY: number }
    >()
    for (let r = 1; r <= rows; r++) {
      for (let c = 1; c <= cols; c++) {
        if (triangular === 'lower' && c > r) continue
        if (triangular === 'upper' && c < r) continue
        const id = `r${r}.c${c}`
        const { x, y } = cellRect(r, c)
        map.set(id, {
          cx: x + cell / 2,
          cy: y + cell / 2,
          trX: x + cell - 2,
          trY: y + 2,
        })
      }
    }
    return map
  }, [rows, cols, triangular])

  const uid = useId().replace(/:/g, '')
  const markerId = `demo-arrowhead-hm-${uid}`

  return (
    <svg viewBox={`0 0 ${w} ${h}`} className="w-full h-auto max-w-lg mx-auto">
      <defs>
        <marker
          id={markerId}
          markerWidth="9"
          markerHeight="9"
          refX="7"
          refY="3.5"
          orient="auto"
        >
          <path d="M0,0 L7,3.5 L0,7 Z" fill={fillIntent} />
        </marker>
      </defs>
      {colLabels?.map((lab, i) => (
        <text
          key={`c-${i}`}
          x={labelW + i * cell + cell / 2}
          y={labelH / 2 + 1}
          textAnchor="middle"
          dominantBaseline="central"
          fontSize={10}
          fontWeight={500}
          fill={dark ? '#a1a1aa' : '#71717a'}
        >
          {lab}
        </text>
      ))}
      {rowLabels?.map((lab, i) => (
        <text
          key={`r-${i}`}
          x={labelW / 2}
          y={labelH + i * cell + cell / 2}
          textAnchor="middle"
          dominantBaseline="central"
          fontSize={10}
          fontWeight={500}
          fill={dark ? '#a1a1aa' : '#71717a'}
        >
          {lab}
        </text>
      ))}
      {Array.from({ length: rows }, (_, ri) =>
        Array.from({ length: cols }, (_, ci) => {
          const r = ri + 1
          const c = ci + 1
          if (triangular === 'lower' && c > r) return null
          if (triangular === 'upper' && c < r) return null
          const id = `r${r}.c${c}`
          const { x, y } = cellRect(r, c)
          const revealed = id in state.revealed
          const v = values[ri]?.[ci] ?? 0
          const lit = state.overview || state.spotlight.includes(id)
          const op = revealed ? elementOpacity(id, state, dim) : 1

          if (!revealed) {
            return (
              <rect
                key={id}
                x={x + 1.5}
                y={y + 1.5}
                width={cell - 3}
                height={cell - 3}
                rx={5}
                fill="none"
                stroke={ghostStroke}
                strokeWidth={1}
                strokeDasharray="3 2"
                opacity={0.8}
              />
            )
          }

          const vNorm = Math.min(1, Math.abs(v) / maxAbsV)
          const fillOpacity = 0.14 + vNorm * 0.78
          const textFill =
            vNorm > 0.45 ? (dark ? '#0a0a0a' : '#fff') : dark ? '#eee' : '#111'

          return (
            <g key={id} opacity={op} style={{ transition }}>
              <rect
                x={x + 1.5}
                y={y + 1.5}
                width={cell - 3}
                height={cell - 3}
                rx={5}
                fill={fillIntent}
                fillOpacity={fillOpacity}
                stroke={lit ? fillIntent : 'transparent'}
                strokeWidth={lit ? 2 : 0}
              />
              <text
                x={x + cell / 2}
                y={y + cell / 2}
                textAnchor="middle"
                dominantBaseline="central"
                fontSize={9}
                fontWeight={600}
                fill={textFill}
              >
                {v.toFixed(2)}
              </text>
            </g>
          )
        })
      )}
      <AnnotationsOverlay
        annotations={state.annotations}
        dark={dark}
        markerId={markerId}
        getAnchor={(id, kind) => {
          const p = positions.get(id)
          if (!p) return null
          if (kind === 'badge') return { x: p.trX, y: p.trY }
          if (kind === 'callout') return { x: p.trX + 4, y: p.cy }
          return { x: p.cx, y: p.cy }
        }}
      />
    </svg>
  )
}

export function DemoSceneView({
  scene,
  state,
  reducedMotion,
  className,
}: DemoSceneViewProps) {
  const dark = useDarkMode()

  return (
    <div
      className={cn(
        'grid gap-4',
        scene.panels.length === 2 ? 'md:grid-cols-2' : 'grid-cols-1',
        className
      )}
    >
      {scene.panels.map((panel) => (
        <div
          key={panel.id}
          className={cn(
            'overflow-hidden rounded-xl border shadow-sm',
            dark
              ? 'border-white/10 bg-[#0f1115]'
              : 'border-black/10 bg-[#faf9f6]'
          )}
        >
          {panel.type === 'svg-scene' && (
            <SvgScenePanel
              panel={panel}
              state={state}
              reducedMotion={reducedMotion}
              dark={dark}
            />
          )}
          {panel.type === 'chart' && (
            <div className="p-3">
              <ChartPanel
                panel={panel}
                state={state}
                reducedMotion={reducedMotion}
                dark={dark}
              />
            </div>
          )}
          {panel.type === 'heatmap-matrix' && (
            <div className="p-3">
              <HeatmapPanel
                panel={panel}
                state={state}
                reducedMotion={reducedMotion}
                dark={dark}
              />
            </div>
          )}
        </div>
      ))}
    </div>
  )
}
