import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { api, fmtNum } from '../api'
import type { CurveData, FileSummary, RoleInfo, RolesResponse } from '../types'

const PALETTE = ['#4aa3ff', '#36c6a0', '#e0a23c', '#e9645f', '#b48ead', '#8fbcbb', '#a3be8c', '#d08770']
const GUTTER = 72

interface Curve { key: string; color: string }
interface Track { id: string; curves: Curve[] }

interface Props { file: FileSummary; onError: (e: unknown) => void }

export default function LogPlot({ file, onError }: Props) {
  const [roles, setRoles] = useState<RolesResponse | null>(null)
  const roleMap = useMemo(() => {
    const m = new Map<string, RoleInfo>()
    roles?.roles.forEach(r => m.set(r.key, r))
    return m
  }, [roles])

  const [axis, setAxis] = useState<'depth' | 'time'>('depth')
  const [tracks, setTracks] = useState<Track[]>([])
  const [autoscale, setAutoscale] = useState(false)
  const [playing, setPlaying] = useState(false)
  const [speed, setSpeed] = useState(600)            // index units / sec
  const [pickerOpen, setPickerOpen] = useState(false)
  const [readout, setReadout] = useState<{ x: number; y: number; index: number; rows: { label: string; color: string; v: number | null; unit: string }[] } | null>(null)
  const [detail, setDetail] = useState(false)
  const [showStats, setShowStats] = useState(false)
  const [stats, setStats] = useState<{ key: string; mnemonic: string; unit: string; min: number | null; avg: number | null; max: number | null }[]>([])

  const containerRef = useRef<HTMLDivElement>(null)
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const viewRef = useRef<{ from: number; to: number }>({ from: 0, to: 1 })
  const dataRef = useRef<CurveData | null>(null)
  const sizeRef = useRef<{ w: number; h: number }>({ w: 800, h: 600 })
  const drawPending = useRef(false)
  const fetchState = useRef<{ last: number; timer: number; seq: number }>({ last: 0, timer: 0, seq: 0 })
  const trackRef = useRef<HTMLDivElement>(null)
  const thumbRef = useRef<HTMLDivElement>(null)
  const windowRangeRef = useRef<Map<string, [number, number]>>(new Map())

  const extent = useMemo(() => {
    const e = axis === 'depth' ? roles?.depthExtent : roles?.timeExtent
    return e && e.max > e.min ? { min: e.min, max: e.max } : null
  }, [roles, axis])

  /* ---------- load roles for this file ---------- */
  useEffect(() => {
    let alive = true
    setRoles(null); setTracks([]); setReadout(null); setPlaying(false)
    api.roles(file.id).then(r => {
      if (!alive) return
      setRoles(r)
      setAxis(r.hasDepthAxis ? 'depth' : 'time')
      // build default tracks from resolved roles
      const m = new Map(r.roles.map(x => [x.key, x]))
      let ci = 0
      const ts: Track[] = []
      for (const group of r.defaultTracks) {
        const curves = group.filter(k => m.has(k)).map(k => ({ key: k, color: PALETTE[ci++ % PALETTE.length] }))
        if (curves.length) ts.push({ id: 't' + ts.length, curves })
      }
      setTracks(ts)
    }).catch(onError)
    return () => { alive = false }
  }, [file.id, onError])

  // poll roles until ready (pyramid finishes building) while indexing
  useEffect(() => {
    if (!roles || roles.ready) return
    const t = window.setInterval(() => {
      api.roles(file.id).then(r => { if (r.ready) setRoles(r) }).catch(() => {})
    }, 1500)
    return () => window.clearInterval(t)
  }, [roles, file.id])

  /* ---------- initialize view when extent becomes available ---------- */
  useEffect(() => {
    if (extent) { viewRef.current = { from: extent.min, to: extent.max }; scheduleFetch(true); requestDraw() }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [extent])

  /* ---------- size / resize ---------- */
  useEffect(() => {
    const el = containerRef.current
    if (!el) return
    const ro = new ResizeObserver(() => {
      sizeRef.current = { w: el.clientWidth, h: el.clientHeight }
      const c = canvasRef.current
      if (c) {
        const dpr = window.devicePixelRatio || 1
        c.width = Math.round(sizeRef.current.w * dpr)
        c.height = Math.round(sizeRef.current.h * dpr)
        c.style.width = sizeRef.current.w + 'px'
        c.style.height = sizeRef.current.h + 'px'
      }
      scheduleFetch(true); requestDraw()
    })
    ro.observe(el)
    return () => ro.disconnect()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  /* ---------- data fetch (throttled so replay doesn't spam) ---------- */
  const keysSig = useMemo(() => tracks.flatMap(t => t.curves.map(c => c.key)).join(','), [tracks])

  const doFetch = useCallback(async () => {
    if (!extent) return
    fetchState.current.last = performance.now()
    const keys = Array.from(new Set(tracks.flatMap(t => t.curves.map(c => c.key))))
    if (keys.length === 0) { dataRef.current = null; requestDraw(); return }
    const v = viewRef.current
    const span = v.to - v.from
    const pad = span * 0.3
    const from = Math.max(extent.min, v.from - pad)
    const to = Math.min(extent.max, v.to + pad)
    const h = sizeRef.current.h
    const width = Math.min(4000, Math.max(64, Math.round(h * 1.6)))
    const seq = ++fetchState.current.seq
    try {
      const cd = await api.curveData(file.id, axis, keys, from, to, width)
      if (seq !== fetchState.current.seq) return  // stale
      dataRef.current = cd
      setDetail(cd.detail)
      // window statistics over the *visible* range (min/avg/max per channel) + per-curve fit range
      const vw = viewRef.current
      const wr = new Map<string, [number, number]>()
      setStats(cd.curves.map(s => {
        let mn = Infinity, mx = -Infinity, sum = 0, cnt = 0
        for (let j = 0; j < cd.n; j++) {
          const p = cd.pos[j]; if (p == null || p < vw.from || p > vw.to) continue
          const a = s.min[j], b = s.max[j]; if (a == null || b == null) continue
          if (a < mn) mn = a; if (b > mx) mx = b; sum += (a + b) / 2; cnt++
        }
        if (cnt) wr.set(s.key, [mn, mx])
        return { key: s.key, mnemonic: s.mnemonic, unit: s.unit, min: cnt ? mn : null, avg: cnt ? sum / cnt : null, max: cnt ? mx : null }
      }))
      windowRangeRef.current = wr
      requestDraw()
    } catch (e) { onError(e) }
  }, [extent, tracks, axis, file.id, onError])

  const scheduleFetch = useCallback((force = false) => {
    const interval = 150
    const now = performance.now()
    const since = now - fetchState.current.last
    window.clearTimeout(fetchState.current.timer)
    if (force || since >= interval) doFetch()
    else fetchState.current.timer = window.setTimeout(doFetch, interval - since)
  }, [doFetch])

  useEffect(() => { scheduleFetch(true) }, [keysSig, axis, scheduleFetch])

  /* ---------- drawing ---------- */
  const v2y = (v: number, h: number) => {
    const { from, to } = viewRef.current
    return ((v - from) / (to - from)) * h
  }
  const y2v = (y: number, h: number) => {
    const { from, to } = viewRef.current
    return from + (y / h) * (to - from)
  }

  const scaleFor = useCallback((key: string): [number, number] => {
    // "Auto-fit": scale each track to the data actually visible in the current window.
    if (autoscale) {
      const wr = windowRangeRef.current.get(key)
      if (wr) {
        if (wr[1] > wr[0]) { const p = (wr[1] - wr[0]) * 0.06; return [wr[0] - p, wr[1] + p] }
        return [wr[0] - 1, wr[1] + 1]
      }
    }
    const r = roleMap.get(key)
    if (!r) return [0, 1]
    if (r.defMax > r.defMin) return [r.defMin, r.defMax]          // fixed physical scale
    if (r.dataMin != null && r.dataMax != null && r.dataMax > r.dataMin) return [r.dataMin, r.dataMax]
    return [0, 1]
  }, [roleMap, autoscale])

  const draw = useCallback(() => {
    drawPending.current = false
    const c = canvasRef.current
    if (!c || !extent) return
    const ctx = c.getContext('2d')!
    const dpr = window.devicePixelRatio || 1
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
    const { w, h } = sizeRef.current
    ctx.clearRect(0, 0, w, h)
    ctx.fillStyle = '#0a0e13'; ctx.fillRect(0, 0, w, h)

    const nT = tracks.length
    const plotW = w - GUTTER
    const trackW = nT > 0 ? plotW / nT : plotW
    const data = dataRef.current
    const { from, to } = viewRef.current

    // index gridlines + gutter labels
    const ticks = niceTicks(from, to, 8)
    ctx.font = '10px ui-monospace, monospace'
    ctx.textBaseline = 'middle'
    for (const tk of ticks) {
      const y = v2y(tk, h)
      if (y < 0 || y > h) continue
      ctx.strokeStyle = '#162028'; ctx.beginPath(); ctx.moveTo(GUTTER, y); ctx.lineTo(w, y); ctx.stroke()
      ctx.fillStyle = '#5c6b7c'; ctx.textAlign = 'right'
      ctx.fillText(fmtIndex(tk, axis), GUTTER - 6, y)
    }
    // gutter divider
    ctx.strokeStyle = '#25323f'; ctx.beginPath(); ctx.moveTo(GUTTER, 0); ctx.lineTo(GUTTER, h); ctx.stroke()

    // tracks
    for (let ti = 0; ti < nT; ti++) {
      const x0 = GUTTER + ti * trackW
      ctx.strokeStyle = '#1b2531'; ctx.beginPath(); ctx.moveTo(x0, 0); ctx.lineTo(x0, h); ctx.stroke()
      const t = tracks[ti]
      for (const cv of t.curves) {
        const s = data?.curves.find(x => x.key === cv.key)
        if (!s || !data) continue
        const [cmin, cmax] = scaleFor(cv.key)
        const pad = 4
        const sx = (val: number) => {
          let f = (val - cmin) / (cmax - cmin)
          if (f < 0) f = 0; else if (f > 1) f = 1
          return x0 + pad + f * (trackW - 2 * pad)
        }
        // min/max envelope (preserves spikes)
        ctx.strokeStyle = cv.color; ctx.globalAlpha = 0.45; ctx.lineWidth = 1; ctx.beginPath()
        for (let j = 0; j < data.n; j++) {
          const pv = data.pos[j]; if (pv == null) continue
          const mn = s.min[j], mx = s.max[j]
          if (mn == null || mx == null) continue
          const y = v2y(pv, h); if (y < -2 || y > h + 2) continue
          ctx.moveTo(sx(mn), y); ctx.lineTo(sx(mx), y)
        }
        ctx.stroke()
        // mid trace
        ctx.globalAlpha = 1; ctx.beginPath(); let started = false
        for (let j = 0; j < data.n; j++) {
          const pv = data.pos[j]; if (pv == null) { started = false; continue }
          const mn = s.min[j], mx = s.max[j]
          if (mn == null || mx == null) { started = false; continue }
          const y = v2y(pv, h)
          const x = sx((mn + mx) / 2)
          if (!started) { ctx.moveTo(x, y); started = true } else ctx.lineTo(x, y)
        }
        ctx.stroke()
      }
    }
    ctx.globalAlpha = 1

    // crosshair
    if (readout) {
      ctx.strokeStyle = '#36c6a0'; ctx.globalAlpha = 0.6; ctx.setLineDash([4, 3])
      ctx.beginPath(); ctx.moveTo(GUTTER, readout.y); ctx.lineTo(w, readout.y); ctx.stroke()
      ctx.setLineDash([]); ctx.globalAlpha = 1
    }
    // playhead at bottom while replaying
    if (playing) {
      ctx.strokeStyle = '#e0a23c'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(GUTTER, h - 1); ctx.lineTo(w, h - 1); ctx.stroke()
    }

    // position the scrollbar thumb to mirror the visible window
    const track = trackRef.current, thumb = thumbRef.current
    if (track && thumb) {
      const trackH = track.clientHeight
      const full = extent.max - extent.min
      const span = to - from
      const th = Math.max(28, full > 0 ? (trackH * span) / full : trackH)
      const maxTop = trackH - th
      const top = full > span ? ((from - extent.min) / (full - span)) * maxTop : 0
      thumb.style.height = th + 'px'
      thumb.style.top = top + 'px'
    }
  }, [extent, tracks, axis, scaleFor, readout, playing])

  const requestDraw = useCallback(() => {
    if (drawPending.current) return
    drawPending.current = true
    requestAnimationFrame(() => draw())
  }, [draw])

  useEffect(() => { requestDraw() }, [requestDraw])

  /* ---------- interactions ---------- */
  const clampView = useCallback((from: number, to: number) => {
    if (!extent) return { from, to }
    const full = extent.max - extent.min
    let span = to - from
    if (span >= full) return { from: extent.min, to: extent.max }
    if (span < 1e-9) span = 1e-9
    if (from < extent.min) { from = extent.min; to = from + span }
    if (to > extent.max) { to = extent.max; from = to - span }
    return { from, to }
  }, [extent])

  const setView = useCallback((from: number, to: number) => {
    viewRef.current = clampView(from, to)
    scheduleFetch(); requestDraw()
  }, [clampView, scheduleFetch, requestDraw])

  const onWheel = useCallback((e: React.WheelEvent) => {
    if (!extent) return
    e.preventDefault()
    const rect = canvasRef.current!.getBoundingClientRect()
    const y = e.clientY - rect.top
    const h = sizeRef.current.h
    const v = y2v(y, h)
    const { from, to } = viewRef.current
    const factor = e.deltaY > 0 ? 1.2 : 0.83
    let span = (to - from) * factor
    const full = extent.max - extent.min
    const minSpan = axis === 'time' ? 5 : 1
    span = Math.max(minSpan, Math.min(full, span))
    const frac = y / h
    setView(v - frac * span, v - frac * span + span)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [extent, axis, setView])

  const onMouseMove = (e: React.MouseEvent) => {
    const rect = canvasRef.current!.getBoundingClientRect()
    const h = sizeRef.current.h
    const y = e.clientY - rect.top
    // hover readout
    const data = dataRef.current
    const idx = y2v(y, h)
    const rows: { label: string; color: string; v: number | null; unit: string }[] = []
    if (data) {
      const j = nearest(data.pos, idx)
      for (const t of tracks) for (const cv of t.curves) {
        const s = data.curves.find(x => x.key === cv.key)
        const r = roleMap.get(cv.key)
        let val: number | null = null
        if (s && j >= 0) { const mn = s.min[j], mx = s.max[j]; if (mn != null && mx != null) val = (mn + mx) / 2 }
        rows.push({ label: r?.mnemonic ?? cv.key, color: cv.color, v: val, unit: r?.unit ?? '' })
      }
    }
    setReadout({ x: e.clientX - rect.left, y, index: idx, rows })
  }
  const onLeave = () => { setReadout(null) }

  const zoomBy = useCallback((factor: number) => {
    if (!extent) return
    const { from, to } = viewRef.current
    const c = (from + to) / 2
    const full = extent.max - extent.min
    const minSpan = axis === 'time' ? 5 : 1
    const span = Math.max(minSpan, Math.min(full, (to - from) * factor))
    setView(c - span / 2, c + span / 2)
  }, [extent, axis, setView])

  /* ---------- scrollbar (pan through depth/time) ---------- */
  const thumbGeom = useCallback(() => {
    const trackH = trackRef.current?.clientHeight ?? 1
    const full = extent ? extent.max - extent.min : 1
    const span = viewRef.current.to - viewRef.current.from
    const th = Math.max(28, full > 0 ? (trackH * span) / full : trackH)
    return { trackH, full, span, th, maxTop: trackH - th }
  }, [extent])

  const sbDrag = useRef<{ y: number; from: number } | null>(null)
  const onThumbDown = (e: React.MouseEvent) => {
    e.preventDefault(); e.stopPropagation()
    if (!extent) return
    sbDrag.current = { y: e.clientY, from: viewRef.current.from }
    const move = (ev: MouseEvent) => {
      if (!sbDrag.current) return
      const g = thumbGeom()
      const dy = ev.clientY - sbDrag.current.y
      const dFrac = g.maxTop > 0 ? dy / g.maxTop : 0
      const nf = sbDrag.current.from + dFrac * (g.full - g.span)
      setView(nf, nf + g.span)
    }
    const up = () => { sbDrag.current = null; document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up) }
    document.addEventListener('mousemove', move)
    document.addEventListener('mouseup', up)
  }
  const onTrackDown = (e: React.MouseEvent) => {
    if (!extent) return
    const rect = trackRef.current!.getBoundingClientRect()
    const clickY = e.clientY - rect.top
    const g = thumbGeom()
    const top = g.full > g.span ? ((viewRef.current.from - extent.min) / (g.full - g.span)) * g.maxTop : 0
    const page = g.span * 0.9
    if (clickY < top) setView(viewRef.current.from - page, viewRef.current.to - page)
    else if (clickY > top + g.th) setView(viewRef.current.from + page, viewRef.current.to + page)
  }

  /* ---------- replay ---------- */
  useEffect(() => {
    if (!playing || !extent) return
    let raf = 0
    let last = performance.now()
    const tick = (t: number) => {
      const dt = (t - last) / 1000; last = t
      const { from, to } = viewRef.current
      const span = to - from
      let nt = to + speed * dt
      if (nt >= extent.max) {
        viewRef.current = { from: extent.max - span, to: extent.max }
        scheduleFetch(); requestDraw(); setPlaying(false); return
      }
      viewRef.current = clampView(nt - span, nt)
      scheduleFetch(); requestDraw()
      raf = requestAnimationFrame(tick)
    }
    raf = requestAnimationFrame(tick)
    return () => cancelAnimationFrame(raf)
  }, [playing, speed, extent, clampView, scheduleFetch, requestDraw])

  const resetView = () => { if (extent) setView(extent.min, extent.max) }

  /* ---------- track editing ---------- */
  const toggleRole = (key: string) => {
    setTracks(prev => {
      const has = prev.some(t => t.curves.some(c => c.key === key))
      if (has) {
        return prev.map(t => ({ ...t, curves: t.curves.filter(c => c.key !== key) })).filter(t => t.curves.length)
      }
      const used = new Set(prev.flatMap(t => t.curves.map(c => c.key)))
      const color = PALETTE[used.size % PALETTE.length]
      return [...prev, { id: 't' + Date.now(), curves: [{ key, color }] }]
    })
  }
  const removeCurve = (key: string) =>
    setTracks(prev => prev.map(t => ({ ...t, curves: t.curves.filter(c => c.key !== key) })).filter(t => t.curves.length))

  /* ---------- render ---------- */
  const nT = tracks.length
  const speedLabel = axis === 'time'
    ? `${fmtNum(Math.round(speed))} s/s`
    : `${fmtNum(Math.round(speed))} ft/s`

  if (roles && !roles.ready) {
    return <div className="empty"><div><div className="spin" style={{ width: 18, height: 18 }} />
      <div style={{ marginTop: 10 }}>Building curve overview…</div>
      <div className="muted" style={{ fontSize: 12, marginTop: 6 }}>parsing channels for the log plot</div></div></div>
  }
  if (roles && roles.roles.length === 0) {
    return <div className="empty"><div>No drilling channels recognized in this file.<div className="muted" style={{ fontSize: 12, marginTop: 6 }}>Use the Raw / QC tab to inspect it.</div></div></div>
  }

  return (
    <div className="lp">
      <div className="viewer-bar">
        {roles?.hasDepthAxis && roles?.hasTimeAxis && (
          <div className="seg" style={{ margin: 0, width: 150 }}>
            <button className={axis === 'depth' ? 'on' : ''} onClick={() => setAxis('depth')}>Depth</button>
            <button className={axis === 'time' ? 'on' : ''} onClick={() => setAxis('time')}>Time</button>
          </div>
        )}
        <button className="btn primary" onClick={() => setPlaying(p => !p)}>{playing ? '⏸ Pause' : '▶ Replay'}</button>
        <div className="speed" title="replay speed">
          <span>🐌</span>
          <input type="range" min={axis === 'time' ? 30 : 2} max={axis === 'time' ? 14400 : 500}
                 value={speed} onChange={e => setSpeed(parseInt(e.target.value, 10))} />
          <span>🚀</span>
          <span style={{ fontFamily: 'var(--mono)', width: 78, textAlign: 'right' }}>{speedLabel}</span>
        </div>
        <div className="sep" />
        <button className="btn ghost" onClick={() => zoomBy(0.6)} title="zoom in (or scroll the wheel over the plot)">＋</button>
        <button className="btn ghost" onClick={() => zoomBy(1.7)} title="zoom out">－</button>
        <button className="btn ghost" onClick={resetView}>⤢ Fit</button>
        <label className="speed" style={{ gap: 5 }} title="auto-fit: scale each track to the data visible in the current window">
          <input type="checkbox" checked={autoscale} onChange={e => setAutoscale(e.target.checked)} /> auto-fit
        </label>
        <button className="btn ghost" onClick={() => setPickerOpen(o => !o)}>＋ Curves</button>
        <button className={`btn ghost${showStats ? ' primary' : ''}`} onClick={() => setShowStats(s => !s)}>Σ Stats</button>
        <div className="grow" />
        <span className="stat" style={{ fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--txt-dim)' }}>
          {detail ? 'detail' : 'overview'} · {axis}
        </span>
      </div>

      {/* track headers aligned with canvas tracks */}
      <div className="lp-heads">
        <div className="lp-gutter-sp" style={{ width: GUTTER }}>
          <span className="muted" style={{ fontSize: 10 }}>{axis === 'depth' ? 'DEPTH ft' : 'TIME'}</span>
        </div>
        {tracks.map((t) => (
          <div className="lp-head" key={t.id} style={{ width: `calc((100% - ${GUTTER}px) / ${nT})` }}>
            {t.curves.map(cv => {
              const r = roleMap.get(cv.key)
              const [mn, mx] = scaleFor(cv.key)
              return (
                <div className="lp-curve" key={cv.key} title={r?.description}>
                  <span className="dot" style={{ background: cv.color }} />
                  <span className="mn" style={{ color: cv.color }}>{r?.mnemonic ?? cv.key}</span>
                  <span className="rng">{fmtNum(Math.round(mn))}–{fmtNum(Math.round(mx))}{r?.unit ? ' ' + r.unit : ''}</span>
                  <span className="rm" onClick={() => removeCurve(cv.key)}>×</span>
                </div>
              )
            })}
          </div>
        ))}
      </div>

      <div className="lp-body">
        <div className="lp-canvas-wrap" ref={containerRef}>
        <canvas
          ref={canvasRef}
          onWheel={onWheel}
          onMouseMove={onMouseMove}
          onMouseLeave={onLeave}
          style={{ cursor: 'crosshair' }}
        />
        {showStats && stats.length > 0 && (
          <div className="lp-stats scroll">
            <div className="sh">Window stats <span className="x" onClick={() => setShowStats(false)}>×</span></div>
            <table>
              <thead><tr><th>chan</th><th>min</th><th>avg</th><th>max</th></tr></thead>
              <tbody>
                {stats.map(s => (
                  <tr key={s.key}>
                    <td title={s.unit}>{s.mnemonic}</td>
                    <td>{s.min == null ? '—' : trim(s.min)}</td>
                    <td>{s.avg == null ? '—' : trim(s.avg)}</td>
                    <td>{s.max == null ? '—' : trim(s.max)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
        {readout && (
          <div className="lp-readout" style={{ left: Math.min(readout.x + GUTTER + 8, sizeRef.current.w - 190), top: Math.min(readout.y + 8, sizeRef.current.h - 10 - readout.rows.length * 15) }}>
            <div className="ix">{fmtIndex(readout.index, axis)}{axis === 'depth' ? ' ft' : ''}</div>
            {readout.rows.map((r, i) => (
              <div className="ro" key={i}>
                <span className="dot" style={{ background: r.color }} />
                <span className="l">{r.label}</span>
                <span className="vv">{r.v == null ? '—' : trim(r.v)}</span>
              </div>
            ))}
          </div>
        )}
        </div>
        <div className="lp-scrollbar" ref={trackRef} onMouseDown={onTrackDown} title="drag to scroll through the well; wheel over the plot to zoom">
          <div className="lp-thumb" ref={thumbRef} onMouseDown={onThumbDown} />
        </div>
      </div>

      {pickerOpen && roles && (
        <CurvePicker roles={roles} active={new Set(tracks.flatMap(t => t.curves.map(c => c.key)))}
                     onToggle={toggleRole} onClose={() => setPickerOpen(false)} />
      )}
    </div>
  )
}

/* ---------- curve picker popover ---------- */
function CurvePicker({ roles, active, onToggle, onClose }:
  { roles: RolesResponse; active: Set<string>; onToggle: (k: string) => void; onClose: () => void }) {
  const groups = ['mechanics', 'hydraulics', 'gas', 'directional', 'index']
  const labels: Record<string, string> = {
    mechanics: 'Drilling mechanics', hydraulics: 'Hydraulics & well control',
    gas: 'Mud gas / formation', directional: 'Directional & dynamics', index: 'Index / state',
  }
  return (
    <div className="lp-picker">
      <div className="hd">Curves <span className="x" onClick={onClose}>×</span></div>
      <div className="bd scroll">
        {groups.map(g => {
          const rs = roles.roles.filter(r => r.group === g)
          if (!rs.length) return null
          return (
            <div key={g} className="grp">
              <div className="gh">{labels[g]}</div>
              {rs.map(r => (
                <label key={r.key} className={`chip${active.has(r.key) ? ' on' : ''}`}>
                  <input type="checkbox" checked={active.has(r.key)} onChange={() => onToggle(r.key)} />
                  <span className="mn">{r.mnemonic}</span>
                  <span className="ds">{r.label}</span>
                </label>
              ))}
            </div>
          )
        })}
      </div>
    </div>
  )
}

/* ---------- helpers ---------- */
function nearest(pos: (number | null)[], v: number): number {
  let lo = 0, hi = pos.length - 1, best = -1, bestD = Infinity
  // pos is ascending but may contain nulls; linear-ish guarded binary
  while (lo <= hi) {
    const mid = (lo + hi) >> 1
    const pv = pos[mid]
    if (pv == null) { // probe outward
      let k = mid + 1; while (k <= hi && pos[k] == null) k++
      if (k > hi) { hi = mid - 1; continue }
      const d = Math.abs((pos[k] as number) - v); if (d < bestD) { bestD = d; best = k }
      if ((pos[k] as number) < v) lo = k + 1; else hi = mid - 1
      continue
    }
    const d = Math.abs(pv - v); if (d < bestD) { bestD = d; best = mid }
    if (pv < v) lo = mid + 1; else hi = mid - 1
  }
  return best
}

function niceTicks(from: number, to: number, count: number): number[] {
  const span = to - from
  if (span <= 0) return []
  const raw = span / count
  const mag = Math.pow(10, Math.floor(Math.log10(raw)))
  const norm = raw / mag
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag
  const start = Math.ceil(from / step) * step
  const out: number[] = []
  for (let v = start; v <= to; v += step) out.push(v)
  return out
}

function fmtIndex(v: number, axis: 'depth' | 'time'): string {
  if (axis === 'depth') return fmtNum(Math.round(v))
  const d = new Date(v * 1000)
  const hh = String(d.getHours()).padStart(2, '0')
  const mm = String(d.getMinutes()).padStart(2, '0')
  const ss = String(d.getSeconds()).padStart(2, '0')
  return `${hh}:${mm}:${ss}`
}

function trim(v: number): string {
  if (Math.abs(v) >= 1000) return fmtNum(Math.round(v))
  return (Math.round(v * 100) / 100).toString()
}
