import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Activity, Radio, RefreshCw, PlusCircle, Pencil, Trash2, Database, Layers, TrendingUp, Cable } from 'lucide-react'
import { fetchChanges, fetchChangeStats, resyncSources } from '../../lib/api'
import type { CdcChange, CdcStats } from '../../types'
import { cn } from '../../lib/utils'

const OP_STYLE: Record<string, { label: string; cls: string; color: string }> = {
  insert: { label: 'INSERT', cls: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30', color: '#34d399' },
  update: { label: 'UPDATE', cls: 'bg-amber-500/15 text-amber-300 border-amber-500/30', color: '#fbbf24' },
  delete: { label: 'DELETE', cls: 'bg-rose-500/15 text-rose-300 border-rose-500/30', color: '#fb7185' },
  snapshot: { label: 'SNAPSHOT', cls: 'bg-sky-500/15 text-sky-300 border-sky-500/30', color: '#38bdf8' },
}

const SOURCE_COLOR: Record<string, string> = {
  postgres: '#38bdf8',
  mysql: '#f59e0b',
  mongodb: '#34d399',
  cassandra: '#a78bfa',
  neo4j: '#f472b6',
}

const SOURCES = ['all', 'postgres', 'mysql', 'mongodb', 'cassandra', 'neo4j']
const OPS = ['all', 'insert', 'update', 'delete']
const TIME_WINDOWS = [
  { label: '15 min', minutes: 15 },
  { label: '1 hour', minutes: 60 },
  { label: '6 hours', minutes: 360 },
  { label: '24 hours', minutes: 1440 },
] as const

function opOf(o: string) {
  return OP_STYLE[o] || { label: o.toUpperCase(), cls: 'bg-slate-500/15 text-slate-300 border-slate-500/30', color: '#94a3b8' }
}

function timeAgo(ts: string) {
  const d = Date.now() - new Date(ts).getTime()
  if (d < 1000) return 'now'
  if (d < 60000) return `${Math.floor(d / 1000)}s ago`
  if (d < 3600000) return `${Math.floor(d / 60000)}m ago`
  return `${Math.floor(d / 3600000)}h ago`
}

// Smoothly animates a number toward its target so counters tick up nicely.
function useTween(target: number, ms = 700) {
  const [val, setVal] = useState(target)
  const from = useRef(target)
  const start = useRef(0)
  const raf = useRef(0)
  useEffect(() => {
    from.current = val
    start.current = performance.now()
    const step = (now: number) => {
      const t = Math.min(1, (now - start.current) / ms)
      const eased = 1 - Math.pow(1 - t, 3)
      setVal(from.current + (target - from.current) * eased)
      if (t < 1) raf.current = requestAnimationFrame(step)
    }
    raf.current = requestAnimationFrame(step)
    return () => cancelAnimationFrame(raf.current)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [target, ms])
  return val
}

function KpiCard({ label, value, accent, icon: Icon, sub }: {
  label: string; value: number; accent: string; icon: typeof PlusCircle; sub?: string
}) {
  const v = useTween(value)
  return (
    <div className="relative overflow-hidden rounded-lg border border-border/60 bg-surface-raised p-3">
      <div className="absolute -right-3 -top-3 h-14 w-14 rounded-full opacity-[0.12] blur-xl" style={{ background: accent }} />
      <div className="flex items-center gap-1.5 text-[9px] uppercase tracking-wide text-foreground-faint">
        <Icon className="h-3 w-3" style={{ color: accent }} /> {label}
      </div>
      <div className="mt-1 text-2xl font-semibold tabular-nums text-foreground" style={{ textShadow: `0 0 18px ${accent}22` }}>
        {Math.round(v).toLocaleString()}
      </div>
      {sub && <div className="text-[10px] text-foreground-muted">{sub}</div>}
    </div>
  )
}

// SVG donut for the operation mix.
function Donut({ segments, total }: { segments: { label: string; value: number; color: string }[]; total: number }) {
  const R = 42
  const C = 2 * Math.PI * R
  let offset = 0
  return (
    <div className="flex items-center gap-4">
      <svg viewBox="0 0 110 110" className="h-28 w-28 shrink-0 -rotate-90">
        <circle cx="55" cy="55" r={R} fill="none" stroke="rgba(148,163,184,0.12)" strokeWidth="13" />
        {total > 0 && segments.map((s) => {
          const frac = s.value / total
          const dash = frac * C
          const el = (
            <circle key={s.label} cx="55" cy="55" r={R} fill="none" stroke={s.color} strokeWidth="13"
              strokeDasharray={`${dash} ${C - dash}`} strokeDashoffset={-offset} strokeLinecap="butt"
              style={{ transition: 'stroke-dasharray .6s ease, stroke-dashoffset .6s ease' }} />
          )
          offset += dash
          return el
        })}
        <g className="rotate-90" style={{ transformOrigin: '55px 55px' }}>
          <text x="55" y="51" textAnchor="middle" className="fill-foreground text-[16px] font-semibold tabular-nums">{total.toLocaleString()}</text>
          <text x="55" y="65" textAnchor="middle" className="fill-foreground-faint text-[7px] uppercase tracking-wider">changes</text>
        </g>
      </svg>
      <div className="flex-1 space-y-1.5">
        {segments.map((s) => (
          <div key={s.label} className="flex items-center gap-2 text-[11px]">
            <span className="h-2.5 w-2.5 rounded-sm" style={{ background: s.color }} />
            <span className="capitalize text-foreground-muted">{s.label}</span>
            <span className="ml-auto font-mono text-foreground">{s.value.toLocaleString()}</span>
            <span className="w-9 text-right font-mono text-foreground-faint">{total ? Math.round((s.value / total) * 100) : 0}%</span>
          </div>
        ))}
      </div>
    </div>
  )
}

// Smooth area chart of per-minute change volume.
function VolumeArea({ buckets }: { buckets: { t: string; n: number }[] }) {
  const w = 600
  const h = 120
  const pad = 6
  const data = buckets.length ? buckets : [{ t: '', n: 0 }]
  const max = Math.max(1, ...data.map((b) => b.n))
  const stepX = data.length > 1 ? (w - pad * 2) / (data.length - 1) : 0
  const pts = data.map((b, i) => {
    const x = pad + i * stepX
    const y = h - pad - (b.n / max) * (h - pad * 2)
    return [x, y] as const
  })
  const line = pts.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(1)},${y.toFixed(1)}`).join(' ')
  const area = `${line} L${pts[pts.length - 1][0].toFixed(1)},${h - pad} L${pts[0][0].toFixed(1)},${h - pad} Z`
  const last = data[data.length - 1]
  return (
    <div>
      <div className="relative">
        <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="h-28 w-full">
          <defs>
            <linearGradient id="cdcvol" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="#38bdf8" stopOpacity="0.45" />
              <stop offset="100%" stopColor="#38bdf8" stopOpacity="0" />
            </linearGradient>
          </defs>
          {[0.25, 0.5, 0.75].map((g) => (
            <line key={g} x1={pad} x2={w - pad} y1={pad + g * (h - pad * 2)} y2={pad + g * (h - pad * 2)} stroke="rgba(148,163,184,0.08)" strokeWidth="1" />
          ))}
          {buckets.length > 0 && <path d={area} fill="url(#cdcvol)" />}
          {buckets.length > 0 && <path d={line} fill="none" stroke="#38bdf8" strokeWidth="2" vectorEffect="non-scaling-stroke" />}
          {buckets.length > 0 && (
            <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r="3.5" fill="#38bdf8">
              <animate attributeName="r" values="3.5;6;3.5" dur="1.6s" repeatCount="indefinite" />
            </circle>
          )}
        </svg>
        <div className="pointer-events-none absolute left-1.5 top-1 text-[9px] font-mono text-foreground-faint">{max}/min</div>
      </div>
      <div className="mt-1 flex justify-between text-[9px] font-mono text-foreground-faint">
        <span>{data[0]?.t || '—'}</span>
        <span className="text-docker">now · {last?.n ?? 0}/min</span>
      </div>
      {buckets.length === 0 && <div className="mt-1 text-center text-[10px] text-foreground-faint">No changes in the window yet…</div>}
    </div>
  )
}

function BarRow({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
  return (
    <div className="flex items-center gap-2">
      <span className="flex w-24 shrink-0 items-center gap-1.5 text-[11px] capitalize text-foreground-muted">
        <span className="h-2 w-2 shrink-0 rounded-full" style={{ background: color }} /> {label}
      </span>
      <div className="h-2.5 flex-1 overflow-hidden rounded-full bg-surface">
        <div className="h-full rounded-full transition-all duration-500" style={{ width: `${Math.max(3, (value / max) * 100)}%`, background: color }} />
      </div>
      <span className="w-12 shrink-0 text-right font-mono text-[11px] text-foreground">{value.toLocaleString()}</span>
    </div>
  )
}

function Diff({ change }: { change: CdcChange }) {
  const keys = useMemo(() => {
    const set = new Set<string>()
    for (const k of Object.keys(change.before || {})) set.add(k)
    for (const k of Object.keys(change.after || {})) set.add(k)
    return Array.from(set).filter((k) => k !== 'payload').slice(0, 24)
  }, [change])
  const fmt = (v: unknown) => {
    if (v === null || v === undefined) return '∅'
    if (typeof v === 'object') return JSON.stringify(v).slice(0, 60)
    return String(v).slice(0, 60)
  }
  return (
    <div className="mt-2 overflow-hidden rounded border border-border/60">
      <table className="w-full text-[10px]">
        <thead>
          <tr className="bg-surface-raised text-foreground-faint">
            <th className="px-2 py-1 text-left font-medium">column</th>
            <th className="px-2 py-1 text-left font-medium">before</th>
            <th className="px-2 py-1 text-left font-medium">after</th>
          </tr>
        </thead>
        <tbody>
          {keys.map((k) => {
            const b = (change.before || {})[k]
            const a = (change.after || {})[k]
            const changed = JSON.stringify(b) !== JSON.stringify(a)
            return (
              <tr key={k} className={cn('border-t border-border/40', changed && 'bg-amber-500/5')}>
                <td className="px-2 py-1 font-mono text-foreground-muted">{k}</td>
                <td className="px-2 py-1 font-mono text-rose-300/80">{fmt(b)}</td>
                <td className={cn('px-2 py-1 font-mono', changed ? 'text-emerald-300' : 'text-foreground-muted')}>{fmt(a)}</td>
              </tr>
            )
          })}
        </tbody>
      </table>
    </div>
  )
}

export function ChangesView({ liveChanges }: { liveChanges: CdcChange[] }) {
  const [seed, setSeed] = useState<CdcChange[]>([])
  const [stats, setStats] = useState<CdcStats | null>(null)
  const [source, setSource] = useState('all')
  const [op, setOp] = useState('all')
  const [expanded, setExpanded] = useState<string | null>(null)
  const [connected, setConnected] = useState(false)
  const [flash, setFlash] = useState(false)
  const [resyncing, setResyncing] = useState(false)
  const [resyncMsg, setResyncMsg] = useState<string | null>(null)
  const [windowMin, setWindowMin] = useState(15)
  // Live overlay: CDC events counted straight off the WebSocket stream since the
  // last server stats snapshot. The top KPIs/charts = authoritative server stats
  // (refreshed every 2.5s) + this overlay, so they move in lock-step with the
  // bottom feed instead of lagging behind it.
  const emptyOverlay = { total: 0, by_op: {} as Record<string, number>, by_source: {} as Record<string, number>, by_table: {} as Record<string, number> }
  const [overlay, setOverlay] = useState(emptyOverlay)
  const lastSeenId = useRef<string | null>(null)
  const primed = useRef(false)

  const applyStats = useCallback((s: CdcStats | null) => {
    if (!s) return
    setStats(s)
    setOverlay({ total: 0, by_op: {}, by_source: {}, by_table: {} })  // server is now authoritative
  }, [])

  const load = useCallback(async () => {
    const [c, s] = await Promise.all([fetchChanges({ limit: 200, minutes: windowMin }), fetchChangeStats(windowMin)])
    setSeed(c.changes)
    setConnected(c.connected)
    applyStats(s)
  }, [applyStats, windowMin])

  const doResync = useCallback(async () => {
    setResyncing(true)
    setResyncMsg('Restarting Debezium connectors…')
    try {
      const res = await resyncSources(true)
      if (res) {
        setResyncMsg(`Re-synced · ${res.healthy ?? 0}/${res.total ?? 0} connectors healthy`)
        setTimeout(() => load(), 2500)
      } else {
        setResyncMsg('Re-sync failed — check ETL Guardian terminal')
      }
    } catch {
      setResyncMsg('Re-sync failed — check ETL Guardian terminal')
    } finally {
      setResyncing(false)
      setTimeout(() => setResyncMsg(null), 7000)
    }
  }, [load])

  useEffect(() => {
    load()
    const iv = setInterval(() => fetchChangeStats(windowMin).then((s) => applyStats(s)), 2500)
    return () => clearInterval(iv)
  }, [load, applyStats, windowMin])

  // Fold freshly-arrived WS changes into the overlay → instant top-of-page update.
  useEffect(() => {
    if (!liveChanges.length) return
    if (!primed.current) {
      primed.current = true
      lastSeenId.current = liveChanges[0].id
      return
    }
    const idx = liveChanges.findIndex((c) => c.id === lastSeenId.current)
    const fresh = idx === -1 ? liveChanges : liveChanges.slice(0, idx)
    if (!fresh.length) return
    lastSeenId.current = liveChanges[0].id
    setOverlay((o) => {
      const next = { total: o.total + fresh.length, by_op: { ...o.by_op }, by_source: { ...o.by_source }, by_table: { ...o.by_table } }
      for (const c of fresh) {
        next.by_op[c.op] = (next.by_op[c.op] || 0) + 1
        next.by_source[c.source] = (next.by_source[c.source] || 0) + 1
        next.by_table[c.table] = (next.by_table[c.table] || 0) + 1
      }
      return next
    })
    setFlash(true)
    const id = setTimeout(() => setFlash(false), 800)
    return () => clearTimeout(id)
  }, [liveChanges])

  // Merge live (WS) with seeded backlog, dedupe by id, newest first.
  const merged = useMemo(() => {
    const byId = new Map<string, CdcChange>()
    for (const c of liveChanges) byId.set(c.id, c)
    for (const c of seed) if (!byId.has(c.id)) byId.set(c.id, c)
    return Array.from(byId.values()).sort((x, y) => (y.ts > x.ts ? 1 : -1))
  }, [liveChanges, seed])

  const filtered = useMemo(
    () => merged.filter((c) => (source === 'all' || c.source === source) && (op === 'all' || c.op === op)).slice(0, 200),
    [merged, source, op],
  )

  const opVal = (k: string) => (stats?.by_op?.[k] || 0) + (overlay.by_op[k] || 0)
  const inserts = opVal('insert')
  const updates = opVal('update')
  const deletes = opVal('delete')
  const total = (stats?.total ?? 0) + overlay.total
  const perMin = total / Math.max(1, stats?.window_minutes ?? 15)

  const opSegments = useMemo(() => (
    ['insert', 'update', 'delete', 'snapshot']
      .map((k) => ({ label: k, value: (stats?.by_op?.[k] || 0) + (overlay.by_op[k] || 0), color: opOf(k).color }))
      .filter((s) => s.value > 0)
  ), [stats?.by_op, overlay])

  const sourceRows = useMemo(() => {
    const m: Record<string, number> = { ...(stats?.by_source || {}) }
    for (const [k, v] of Object.entries(overlay.by_source)) m[k] = (m[k] || 0) + v
    return Object.entries(m).sort((a, b) => b[1] - a[1])
  }, [stats?.by_source, overlay])
  const maxSource = Math.max(1, ...sourceRows.map(([, v]) => v))

  const tableRows = useMemo(() => {
    const m: Record<string, number> = { ...(stats?.by_table || {}) }
    for (const [k, v] of Object.entries(overlay.by_table)) m[k] = (m[k] || 0) + v
    return Object.entries(m).sort((a, b) => b[1] - a[1]).slice(0, 7)
  }, [stats?.by_table, overlay])
  const maxTable = Math.max(1, ...tableRows.map(([, v]) => v))

  // Volume chart: bump the current-minute bar with the live overlay so the curve
  // visibly rises as changes stream in.
  const liveBuckets = useMemo(() => {
    const b = (stats?.buckets || []).map((x) => ({ ...x }))
    if (overlay.total) {
      if (b.length) b[b.length - 1] = { ...b[b.length - 1], n: b[b.length - 1].n + overlay.total }
      else b.push({ t: 'now', n: overlay.total })
    }
    return b
  }, [stats?.buckets, overlay.total])

  return (
    <div className="flex h-full min-h-0 flex-col gap-3 overflow-y-auto scrollbar-thin pr-1">
      {/* Header */}
      <div className="flex shrink-0 items-center justify-between">
        <div>
          <h1 className="flex items-center gap-2 text-base font-semibold text-foreground">
            <Activity className={cn('h-4 w-4 text-docker', flash && 'animate-pulse')} /> New &amp; Changed Data
          </h1>
          <p className="text-[11px] text-foreground-muted">
            Live Debezium change data capture — every insert, update &amp; delete across all source databases, streamed via Kafka
          </p>
        </div>
        <div className="flex items-center gap-3">
          <div className="flex items-center gap-1 rounded border border-border/60 p-0.5">
            {TIME_WINDOWS.map((w) => (
              <button
                key={w.minutes}
                type="button"
                onClick={() => setWindowMin(w.minutes)}
                className={cn(
                  'rounded px-2 py-0.5 text-[10px]',
                  windowMin === w.minutes
                    ? 'bg-docker/20 text-docker'
                    : 'text-foreground-muted hover:text-foreground',
                )}
              >
                {w.label}
              </button>
            ))}
          </div>
          <span className={cn('flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-medium',
            connected ? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-300' : 'border-rose-500/40 bg-rose-500/10 text-rose-300')}>
            <Radio className={cn('h-3 w-3', connected && 'animate-pulse')} /> {connected ? 'STREAMING' : 'OFFLINE'}
          </span>
          {resyncMsg && (
            <span className="hidden text-[10px] text-amber-300/90 md:inline">{resyncMsg}</span>
          )}
          <button
            type="button"
            onClick={doResync}
            disabled={resyncing}
            title="Restart all Debezium source connectors so CDC catches up after a database outage"
            className="flex items-center gap-1 rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1 text-[10px] font-medium text-amber-300 hover:bg-amber-500/20 disabled:opacity-60"
          >
            <Cable className={cn('h-3 w-3', resyncing && 'animate-spin')} /> {resyncing ? 'Re-syncing…' : 'Re-sync sources'}
          </button>
          <button type="button" onClick={load} className="flex items-center gap-1 rounded border border-border/60 px-2 py-1 text-[10px] text-foreground-muted hover:text-docker">
            <RefreshCw className="h-3 w-3" /> Refresh
          </button>
        </div>
      </div>

      {/* KPI row */}
      <div className="grid shrink-0 grid-cols-2 gap-2 lg:grid-cols-4">
        <KpiCard label="New records" value={inserts} accent="#34d399" icon={PlusCircle} sub={`inserts · last ${windowMin}m`} />
        <KpiCard label="Updates" value={updates} accent="#fbbf24" icon={Pencil} sub={`modified rows · ${windowMin}m`} />
        <KpiCard label="Deletes" value={deletes} accent="#fb7185" icon={Trash2} sub={`removed rows · ${windowMin}m`} />
        <KpiCard label="Throughput" value={Math.round(perMin)} accent="#38bdf8" icon={TrendingUp} sub={`changes/min · ${(stats?.consumed ?? 0).toLocaleString()} total consumed`} />
      </div>

      {/* Volume + operation mix */}
      <div className="grid shrink-0 grid-cols-1 gap-2 lg:grid-cols-3">
        <div className="rounded-lg border border-border/60 bg-surface-raised p-3 lg:col-span-2">
          <div className="mb-1 flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-foreground-faint">
            <TrendingUp className="h-3 w-3" /> Change volume — selected window
          </div>
          <VolumeArea buckets={liveBuckets} />
        </div>
        <div className="rounded-lg border border-border/60 bg-surface-raised p-3">
          <div className="mb-2 flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-foreground-faint">
            <Layers className="h-3 w-3" /> Operation mix
          </div>
          {opSegments.length ? <Donut segments={opSegments} total={total} /> : (
            <div className="flex h-28 items-center justify-center text-[10px] text-foreground-faint">No changes yet…</div>
          )}
        </div>
      </div>

      {/* By system + top tables */}
      <div className="grid shrink-0 grid-cols-1 gap-2 lg:grid-cols-2">
        <div className="rounded-lg border border-border/60 bg-surface-raised p-3">
          <div className="mb-2 flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-foreground-faint">
            <Database className="h-3 w-3" /> New &amp; changed by system
          </div>
          <div className="space-y-2">
            {sourceRows.length ? sourceRows.map(([k, v]) => (
              <BarRow key={k} label={k} value={v} max={maxSource} color={SOURCE_COLOR[k] || '#94a3b8'} />
            )) : <div className="text-[10px] text-foreground-faint">No source activity in the window…</div>}
          </div>
        </div>
        <div className="rounded-lg border border-border/60 bg-surface-raised p-3">
          <div className="mb-2 flex items-center gap-1.5 text-[10px] uppercase tracking-wide text-foreground-faint">
            <Layers className="h-3 w-3" /> Most active tables / collections
          </div>
          <div className="space-y-2">
            {tableRows.length ? tableRows.map(([k, v]) => (
              <BarRow key={k} label={k} value={v} max={maxTable} color="#818cf8" />
            )) : <div className="text-[10px] text-foreground-faint">No table activity yet…</div>}
          </div>
        </div>
      </div>

      {/* Filters */}
      <div className="flex shrink-0 flex-wrap items-center gap-2">
        <span className="text-[10px] font-semibold uppercase tracking-wide text-foreground-muted">Latest changes</span>
        <span className="mx-1 h-3 w-px bg-border/60" />
        <span className="text-[9px] uppercase tracking-wide text-foreground-faint">Source</span>
        {SOURCES.map((s) => (
          <button key={s} type="button" onClick={() => setSource(s)}
            className={cn('rounded-full border px-2.5 py-0.5 text-[10px] capitalize',
              source === s ? 'border-docker/50 bg-docker/15 text-docker' : 'border-border/60 text-foreground-muted hover:text-foreground')}>
            {s}
          </button>
        ))}
        <span className="ml-3 text-[9px] uppercase tracking-wide text-foreground-faint">Op</span>
        {OPS.map((o) => (
          <button key={o} type="button" onClick={() => setOp(o)}
            className={cn('rounded-full border px-2.5 py-0.5 text-[10px] capitalize',
              op === o ? 'border-docker/50 bg-docker/15 text-docker' : 'border-border/60 text-foreground-muted hover:text-foreground')}>
            {o}
          </button>
        ))}
      </div>

      {/* Live list */}
      <div className="min-h-[180px] rounded-lg border border-border/60 bg-surface-raised">
        {filtered.length === 0 && (
          <div className="p-6 text-center text-[11px] text-foreground-faint">
            Waiting for changes… trigger data generation (Data Flow → Generate data) or agent DML to see live CDC events.
          </div>
        )}
        {filtered.map((c) => (
          <div key={c.id} className="border-b border-border/40 last:border-0">
            <button type="button" onClick={() => setExpanded(expanded === c.id ? null : c.id)}
              className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-surface">
              <span className={cn('w-[68px] shrink-0 rounded border px-1 py-0.5 text-center text-[9px] font-semibold', opOf(c.op).cls)}>{opOf(c.op).label}</span>
              <span className="flex shrink-0 items-center gap-1 text-[10px] text-foreground-muted">
                <span className="h-2 w-2 rounded-full" style={{ background: SOURCE_COLOR[c.source] || '#94a3b8' }} />
                <span className="font-mono">{c.source}.{c.table}</span>
              </span>
              <span className="min-w-0 flex-1 truncate font-mono text-[10px] text-foreground">{c.summary || '—'}</span>
              <span className="shrink-0 text-[9px] text-foreground-faint">{timeAgo(c.ts)}</span>
            </button>
            {expanded === c.id && <div className="px-3 pb-3"><Diff change={c} /></div>}
          </div>
        ))}
      </div>
    </div>
  )
}
