/* Chart primitives — SVG, no deps */

const { useMemo, useRef, useState, useEffect } = React;

// Tiny scale helpers
function scaleLinear(domain, range) {
  const [d0, d1] = domain, [r0, r1] = range;
  const m = (r1 - r0) / (d1 - d0 || 1);
  return (v) => r0 + (v - d0) * m;
}

function sparkTimeLabels(times) {
  const n = times.length;
  if (n < 2) return [];
  const span = times[n - 1] - times[0];
  const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const dayKey = (t) => {
    const d = new Date(t);
    return d.getFullYear() * 400 + d.getMonth() * 32 + d.getDate();
  };
  const fmt = (t, asDate) => {
    const d = new Date(t);
    if (asDate || span > 3 * 86400000) {
      return months[d.getMonth()] + ' ' + d.getDate();
    }
    return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
  };
  const target = Math.min(5, n);
  const step = Math.max(1, Math.round((n - 1) / (target - 1)));
  const labels = [];
  let lastDay = -1;
  for (let i = 0; i <= n - 1; i += step) {
    const idx = Math.min(i, n - 1);
    const dk = dayKey(times[idx]);
    labels.push({ i: idx, label: fmt(times[idx], dk !== lastDay) });
    lastDay = dk;
  }
  if (labels[labels.length - 1].i !== n - 1) {
    const dk = dayKey(times[n - 1]);
    labels.push({ i: n - 1, label: fmt(times[n - 1], dk !== lastDay) });
  }
  return labels;
}

// === MAIN PRICE CHART (candles + EMAs + patterns) =========================
function PriceChart({ candles, ema50, ema100, ema200, patterns, levels, markers, height = 380, showEmas = true, showPatterns = true, tf = '1D', futurePadding = 0, projection = null }) {
  const wrapRef  = useRef(null);
  const svgRef   = useRef(null);
  const dragRef  = useRef({ active: false, startX: 0, startEnd: 0 });
  const totalSlots = candles.length + futurePadding; // real candles + future space
  const [w,          setW]         = useState(900);
  const [hover,      setHover]     = useState(null);
  const [isDragging, setIsDragging] = useState(false);
  const [viewCount,  setViewCount] = useState(totalSlots);
  const [viewEnd,    setViewEnd]   = useState(totalSlots - 1);

  const MIN_VIEW = 20;
  const padL = 8, padR = 64, padT = 12, padB = 32;

  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  // Reset view when candle set changes
  useEffect(() => {
    setViewCount(totalSlots);
    setViewEnd(totalSlots - 1);
  }, [totalSlots]);

  // Non-passive wheel listener so we can preventDefault (avoids page scroll)
  useEffect(() => {
    const el = svgRef.current;
    if (!el) return;
    const onWheel = (e) => {
      e.preventDefault();
      const factor = e.deltaY > 0 ? 1.15 : 1 / 1.15;
      const rect   = el.getBoundingClientRect();
      const iW     = Math.max(100, rect.width - padL - padR);
      const px     = Math.max(padL, Math.min(padL + iW, e.clientX - rect.left));
      const ratio  = (px - padL) / iW;
      setViewCount(prevCount => {
        const newCount = Math.max(MIN_VIEW, Math.min(totalSlots, Math.round(prevCount * factor)));
        setViewEnd(prevEnd => {
          const prevStart  = prevEnd - prevCount + 1;
          const mouseIdx   = prevStart + ratio * (prevCount - 1);
          const newStart   = Math.round(mouseIdx - ratio * (newCount - 1));
          const newEnd     = Math.min(totalSlots - 1, Math.max(newCount - 1, newStart + newCount - 1));
          return newEnd;
        });
        return newCount;
      });
    };
    el.addEventListener('wheel', onWheel, { passive: false });
    return () => el.removeEventListener('wheel', onWheel);
  }, [candles.length]);

  const innerW    = Math.max(100, w - padL - padR);
  const innerH    = height - padT - padB;
  const viewStart = Math.max(0, viewEnd - viewCount + 1);

  // Y domain from visible REAL candles only (future slots have no price data),
  // widened to also fit every reference level — a far-off SL/target is exactly
  // what the caller wants visible, not clipped out of a tighter candle-only range.
  const ys = useMemo(() => {
    let min = Infinity, max = -Infinity;
    for (let i = viewStart; i <= Math.min(viewEnd, candles.length - 1); i++) {
      if (candles[i].l < min) min = candles[i].l;
      if (candles[i].h > max) max = candles[i].h;
    }
    (levels || []).forEach(lv => {
      if (lv.price == null) return;
      if (lv.price < min) min = lv.price;
      if (lv.price > max) max = lv.price;
    });
    if (!isFinite(min)) return [0, 1];
    const pad = (max - min) * 0.04;
    return [min - pad, max + pad];
  }, [candles, viewStart, viewEnd, levels]);

  const lastCandleT = candles[candles.length - 1]?.t || Date.now();

  const x  = scaleLinear([viewStart, viewEnd], [padL, padL + innerW]);
  const y  = scaleLinear(ys, [padT + innerH, padT]);
  const cw = Math.max(1, innerW / Math.max(1, viewCount) * 0.75);

  const linePath = (vals) => {
    let d = '', first = true;
    for (let i = Math.max(0, viewStart - 1); i <= Math.min(vals.length - 1, viewEnd + 1); i++) {
      d += (first ? 'M' : 'L') + x(i).toFixed(1) + ' ' + y(vals[i]).toFixed(1) + ' ';
      first = false;
    }
    return d;
  };

  const ticks = useMemo(() => {
    const [lo, hi] = ys;
    const step = Math.pow(10, Math.floor(Math.log10(hi - lo || 1))) / 2;
    const ts = [];
    for (let v = Math.ceil(lo / step) * step; v <= hi; v += step) ts.push(v);
    return ts;
  }, [ys]);

  const dateLabels = useMemo(() => {
    const labels = [], months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    let lastMonth = -1;
    // Real candle labels
    for (let i = viewStart; i <= Math.min(viewEnd, candles.length - 1); i++) {
      const c = candles[i];
      if (!c.t) continue;
      const d = new Date(c.t), m = d.getMonth();
      if (m !== lastMonth) {
        labels.push({ i, label: months[m] + ' ' + d.getFullYear().toString().slice(2) });
        lastMonth = m;
      }
    }
    // Future slot labels — synthesise dates from last candle's timestamp
    if (futurePadding > 0 && viewEnd >= candles.length) {
      for (let i = candles.length; i <= viewEnd; i++) {
        const t = lastCandleT + (i - (candles.length - 1)) * 86400000;
        const d = new Date(t), m = d.getMonth();
        if (m !== lastMonth) {
          labels.push({ i, label: months[m] + ' ' + d.getFullYear().toString().slice(2), future: true });
          lastMonth = m;
        }
      }
    }
    return labels;
  }, [candles, viewStart, viewEnd, futurePadding, lastCandleT]);

  // A point-in-time marker (e.g. "where the order was placed") maps to the
  // nearest candle by timestamp, not by index — candles and the marker come
  // from different sources. A marker older than every fetched candle (the
  // server widens the fetch to reach it, but a very old signal at a fine
  // interval can still outrun even 1000 candles) pins to the left edge with
  // `offLeft` rather than silently landing off-chart.
  const markerPositions = useMemo(() => {
    if (!markers || !markers.length || !candles.length) return [];
    return markers.filter(m => m.t != null).map(m => {
      let idx = 0, bestDiff = Infinity;
      for (let i = 0; i < candles.length; i++) {
        const diff = Math.abs(candles[i].t - m.t);
        if (diff < bestDiff) { bestDiff = diff; idx = i; }
      }
      return { ...m, idx, offLeft: m.t < candles[0].t };
    });
  }, [markers, candles]);

  function onMouseDown(e) {
    setIsDragging(true);
    dragRef.current = { active: true, startX: e.clientX, startEnd: viewEnd };
  }
  function onMouseUp() {
    setIsDragging(false);
    dragRef.current.active = false;
  }
  function onMouseLeave() {
    setIsDragging(false);
    dragRef.current.active = false;
    setHover(null);
  }
  function onMouseMove(e) {
    const rect = e.currentTarget.getBoundingClientRect();
    const px   = e.clientX - rect.left;
    if (dragRef.current.active) {
      const dx    = e.clientX - dragRef.current.startX;
      const cpx   = innerW / Math.max(1, viewCount - 1);
      const shift = Math.round(-dx / cpx);
      const newEnd = Math.max(viewCount - 1, Math.min(totalSlots - 1, dragRef.current.startEnd + shift));
      setViewEnd(newEnd);
    } else {
      const idx = Math.round(viewStart + (px - padL) / innerW * (viewEnd - viewStart));
      if (idx >= viewStart && idx <= viewEnd && idx >= 0 && idx < candles.length) setHover(idx);
    }
  }

  const last = candles[candles.length - 1];
  const atEdge = viewEnd >= candles.length - 1; // show last-price label when right edge visible

  return (
    <div ref={wrapRef} style={{ position: 'relative', width: '100%' }}>
      <svg
        ref={svgRef}
        width={w} height={height}
        onMouseDown={onMouseDown}
        onMouseUp={onMouseUp}
        onMouseMove={onMouseMove}
        onMouseLeave={onMouseLeave}
        style={{ display: 'block', cursor: isDragging ? 'grabbing' : 'crosshair', userSelect: 'none' }}
      >
        {/* grid + y-axis */}
        {ticks.map((t, i) => (
          <g key={'t'+i}>
            <line x1={padL} x2={padL+innerW} y1={y(t)} y2={y(t)} stroke="var(--border-soft)" strokeDasharray="2 4" />
            <text x={padL+innerW+6} y={y(t)+3} fontSize="10" fontFamily="var(--font-mono)" fill="var(--text-4)">
              {window.fmtUSD(t)}
            </text>
          </g>
        ))}

        {/* reference levels (entry/SL/targets for the signal chart modal) — drawn
            full-width so a level far outside the candles' own range still reads
            clearly as "price has to move this far to get there" */}
        {levels && levels.map((lv, i) => {
          if (lv.price == null) return null;
          const ly = y(lv.price);
          return (
            <g key={'lv' + i}>
              <line x1={padL} x2={padL + innerW} y1={ly} y2={ly}
                    stroke={lv.color || 'var(--text-3)'} strokeWidth="1.2" strokeDasharray="5 3" opacity="0.85" />
              <text x={padL + 4} y={ly - 3} fontSize="9.5" fontFamily="var(--font-mono)" fontWeight="600"
                    fill={lv.color || 'var(--text-3)'}>
                {lv.label}
              </text>
            </g>
          );
        })}

        {/* point-in-time markers (e.g. where the order was placed) — a vertical
            line at the nearest candle, or pinned to the left edge with an
            arrow when the marker predates every fetched candle */}
        {markerPositions.map((m, i) => {
          const mx = m.offLeft ? padL : x(m.idx);
          if (!m.offLeft && (mx < padL - 1 || mx > padL + innerW + 1)) return null; // panned out of view
          return (
            <g key={'mk' + i}>
              <line x1={mx} x2={mx} y1={padT} y2={padT + innerH}
                    stroke={m.color || 'var(--gold)'} strokeWidth="1.2" strokeDasharray="3 3" opacity="0.8" />
              <text x={mx + 4} y={padT + 10} fontSize="9.5" fontFamily="var(--font-mono)" fontWeight="600"
                    fill={m.color || 'var(--gold)'}>
                {m.offLeft ? '← ' : ''}{m.label || ''}
              </text>
            </g>
          );
        })}

        {/* future zone shading */}
        {futurePadding > 0 && x(candles.length - 1) <= padL + innerW && (
          <>
            <rect
              x={Math.max(padL, x(candles.length - 0.5))} y={padT}
              width={Math.max(0, Math.min(padL + innerW, x(totalSlots - 1)) - Math.max(padL, x(candles.length - 0.5)))}
              height={innerH}
              fill="oklch(0.20 0.008 248 / 0.45)"
            />
            <line x1={x(candles.length - 0.5)} x2={x(candles.length - 0.5)} y1={padT} y2={padT + innerH}
                  stroke="var(--text-4)" strokeDasharray="4 4" opacity="0.5" />
          </>
        )}

        {/* x-axis date labels */}
        {dateLabels.map((dl, i) => (
          <g key={'d'+i}>
            <line x1={x(dl.i)} x2={x(dl.i)} y1={padT+innerH} y2={padT+innerH+4}
                  stroke={dl.future ? 'var(--text-4)' : 'var(--border-soft)'} strokeDasharray={dl.future ? '2 2' : 'none'} />
            <text x={x(dl.i)} y={padT+innerH+14} fontSize="9.5" fontFamily="var(--font-mono)"
                  fill={dl.future ? 'oklch(0.55 0.04 248)' : 'var(--text-4)'} textAnchor="middle">
              {dl.label}
            </text>
          </g>
        ))}

        {/* pattern regions */}
        {showPatterns && patterns && patterns.map((p, pi) => {
          const pw = x(p.to) - x(p.from);
          if (pw <= 0) return null;
          const fills      = { box: 'oklch(0.45 0.08 232 / 0.06)', flag: 'oklch(0.5 0.1 148 / 0.06)', cup: 'oklch(0.5 0.1 82 / 0.06)', short: 'oklch(0.45 0.12 25 / 0.08)', target: 'oklch(0.45 0.12 148 / 0.08)' };
          const strokes    = { box: 'oklch(0.5 0.1 232 / 0.4)',    flag: 'oklch(0.55 0.12 148 / 0.4)', cup: 'oklch(0.6 0.12 82 / 0.4)',  short: 'oklch(0.6 0.15 25 / 0.5)',  target: 'oklch(0.6 0.15 148 / 0.5)' };
          const lblColors  = { box: 'var(--text-3)', flag: 'var(--bull)', cup: 'var(--gold)', short: 'var(--bear)', target: 'var(--bull)' };
          const clamp      = v => Math.max(padT, Math.min(padT + innerH, y(v)));
          const boxTop     = p.priceHi != null ? clamp(p.priceHi) : padT;
          const boxBot     = p.priceLo != null ? clamp(p.priceLo) : padT + innerH;
          const bh         = boxBot - boxTop;
          if (bh <= 0) return null;
          return (
            <g key={'p'+pi}>
              <rect x={x(p.from)} y={boxTop} width={pw} height={bh}
                    fill={fills[p.kind] || fills.box} stroke={strokes[p.kind] || strokes.box} strokeDasharray="3 3" />
              <text x={x(p.from)+6} y={boxTop+12} fontSize="9.5" fontFamily="var(--font-mono)"
                    fill={lblColors[p.kind] || 'var(--text-3)'} letterSpacing="0.05em">
                {p.label.toUpperCase()}
              </text>
            </g>
          );
        })}

        {/* candles */}
        {candles.map((c, i) => {
          if (i < viewStart || i > viewEnd) return null;
          const up = c.c >= c.o, col = up ? 'var(--bull)' : 'var(--bear)', xi = x(i);
          return (
            <g key={i}>
              <line x1={xi} x2={xi} y1={y(c.h)} y2={y(c.l)} stroke={col} strokeWidth="1" />
              <rect x={xi - cw/2} y={y(Math.max(c.o, c.c))} width={cw}
                    height={Math.max(1, Math.abs(y(c.o) - y(c.c)))} fill={col} opacity={0.95} />
            </g>
          );
        })}

        {/* EMA lines */}
        {showEmas && (
          <g fill="none" strokeWidth="1.4">
            <path className="chart-draw" pathLength="1" d={linePath(ema200)} stroke="var(--info-dim)" opacity="0.85" />
            <path className="chart-draw" pathLength="1" d={linePath(ema100)} stroke="var(--violet)"   opacity="0.85" />
            <path className="chart-draw" pathLength="1" d={linePath(ema50)}  stroke="var(--gold)"     opacity="0.95" strokeWidth="1.6" />
          </g>
        )}

        {/* last price label — only when viewing the right edge */}
        {atEdge && (
          <>
            <line x1={padL} x2={padL+innerW} y1={y(last.c)} y2={y(last.c)}
                  stroke={last.c >= last.o ? 'var(--bull)' : 'var(--bear)'} strokeDasharray="3 3" opacity="0.7" />
            <rect x={padL+innerW} y={y(last.c)-9} width={padR-6} height={18} rx="3"
                  fill={last.c >= last.o ? 'var(--bull)' : 'var(--bear)'} opacity="0.95" />
            <text x={padL+innerW+padR/2-3} y={y(last.c)+4} textAnchor="middle"
                  fontSize="10.5" fontFamily="var(--font-mono)" fontWeight="600" fill="var(--bg-base)">
              {window.fmtUSD(last.c)}
            </text>
          </>
        )}

        {/* hover crosshair */}
        {hover !== null && !isDragging && (
          <line x1={x(hover)} x2={x(hover)} y1={padT} y2={padT+innerH}
                stroke="var(--text-3)" strokeDasharray="2 3" opacity="0.6" />
        )}
      </svg>

      {/* hover tooltip */}
      {hover !== null && !isDragging && (() => {
        const isFuture = hover >= candles.length;
        const left     = Math.min(w - 190, Math.max(0, x(hover) + 8));
        if (isFuture) {
          const t       = lastCandleT + (hover - (candles.length - 1)) * 86400000;
          const dateStr = new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
          return (
            <div style={{
              position: 'absolute', top: 6, left,
              background: 'var(--bg-elev)', border: '1px solid var(--border)',
              borderRadius: 6, padding: '6px 8px', fontSize: 10.5,
              fontFamily: 'var(--font-mono)', color: 'var(--text-4)',
              pointerEvents: 'none', whiteSpace: 'nowrap'
            }}>
              <div>{dateStr}</div>
              <div style={{ fontSize: 9.5, marginTop: 2 }}>future — no data</div>
            </div>
          );
        }
        const c       = candles[hover];
        const dateStr = c.t ? new Date(c.t).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' }) : 'day ' + (hover + 1);
        return (
          <div style={{
            position: 'absolute', top: 6, left,
            background: 'var(--bg-elev)', border: '1px solid var(--border)',
            borderRadius: 6, padding: '6px 8px', fontSize: 10.5,
            fontFamily: 'var(--font-mono)', color: 'var(--text-2)',
            pointerEvents: 'none', whiteSpace: 'nowrap'
          }}>
            <div style={{ color: 'var(--text-4)' }}>{dateStr}</div>
            <div>O <span style={{ color: 'var(--text)' }}>{window.fmtUSD(c.o)}</span></div>
            <div>H <span style={{ color: 'var(--bull)' }}>{window.fmtUSD(c.h)}</span></div>
            <div>L <span style={{ color: 'var(--bear)' }}>{window.fmtUSD(c.l)}</span></div>
            <div>C <span style={{ color: 'var(--text)' }}>{window.fmtUSD(c.c)}</span></div>
          </div>
        );
      })()}
    </div>
  );
}

// === SPARKLINE ===========================================================
function Sparkline({ values, times, height = 36, color = 'var(--gold)', fill = true, baseline }) {
  const wrapRef = useRef(null);
  const [w, setW] = useState(180);
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const [mn, mx] = useMemo(() => {
    let mn = Infinity, mx = -Infinity;
    values.forEach(v => { if (v < mn) mn = v; if (v > mx) mx = v; });
    return [mn, mx];
  }, [values]);

  const showX = Array.isArray(times) && times.length === values.length && values.length >= 2;
  const axisH = showX ? 16 : 0;
  const svgH = height + axisH;
  const xs = scaleLinear([0, Math.max(1, values.length - 1)], [1, w - 1]);
  const ys = scaleLinear([mn, mx], [height - 2, 2]);
  const xLabels = showX ? sparkTimeLabels(times) : [];

  let line = '';
  values.forEach((v, i) => { line += (i === 0 ? 'M' : 'L') + xs(i).toFixed(1) + ' ' + ys(v).toFixed(1) + ' '; });
  const area = line + `L${xs(values.length-1).toFixed(1)} ${height} L${xs(0).toFixed(1)} ${height} Z`;

  const baseY = baseline != null ? ys(baseline) : null;

  return (
    <div ref={wrapRef} style={{ width: '100%' }}>
      <svg width={w} height={svgH} style={{ display: 'block', overflow: 'visible' }}>
        {fill && <path d={area} fill={color} opacity="0.12" />}
        {baseY != null && <line x1="0" x2={w} y1={baseY} y2={baseY} stroke="var(--text-4)" strokeDasharray="2 3" opacity="0.5" />}
        <path className="chart-draw" pathLength="1" d={line} fill="none" stroke={color} strokeWidth="1.4" />
        {showX && (
          <>
            <line x1="0" x2={w} y1={height - 0.5} y2={height - 0.5} stroke="var(--border-soft)" />
            {xLabels.map((dl, i) => (
              <g key={'x' + i}>
                <line x1={xs(dl.i)} x2={xs(dl.i)} y1={height} y2={height + 3} stroke="var(--text-4)" />
                <text
                  x={xs(dl.i)} y={svgH - 3}
                  fontSize="9" fontFamily="var(--font-mono)" fill="var(--text-4)"
                  textAnchor={i === 0 ? 'start' : i === xLabels.length - 1 ? 'end' : 'middle'}
                >
                  {dl.label}
                </text>
              </g>
            ))}
          </>
        )}
      </svg>
    </div>
  );
}

// === RSI PANEL ===========================================================
function RSIPanel({ values, height = 110, times }) {
  const wrapRef = useRef(null);
  const [w, setW] = useState(420);
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const showX = Array.isArray(times) && times.length >= 2 && values.length >= 2;
  const axisH = showX ? 16 : 0;
  const svgH  = height + axisH;
  const padL = 8, padR = 28, padT = 8, padB = 8;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;
  const x = scaleLinear([0, Math.max(1, values.length - 1)], [padL, padL + innerW]);
  const y = scaleLinear([0, 100], [padT + innerH, padT]);
  const xLabels = showX ? sparkTimeLabels(times.slice(0, values.length)) : [];

  let path = '';
  values.forEach((v, i) => { path += (i===0?'M':'L') + x(i).toFixed(1) + ' ' + y(v).toFixed(1) + ' '; });

  return (
    <div ref={wrapRef} style={{ width: '100%' }}>
      <svg width={w} height={svgH} style={{ display: 'block', overflow: 'visible' }}>
        {/* zones */}
        <rect x={padL} y={y(70)} width={innerW} height={y(50) - y(70)} fill="oklch(0.55 0.12 25 / 0.07)" />
        <rect x={padL} y={y(50)} width={innerW} height={y(30) - y(50)} fill="oklch(0.55 0.12 148 / 0.07)" />
        <line x1={padL} x2={padL+innerW} y1={y(70)} y2={y(70)} stroke="var(--bear)" strokeDasharray="2 4" opacity="0.6" />
        <line x1={padL} x2={padL+innerW} y1={y(30)} y2={y(30)} stroke="var(--bull)" strokeDasharray="2 4" opacity="0.6" />
        <line x1={padL} x2={padL+innerW} y1={y(50)} y2={y(50)} stroke="var(--border)" strokeDasharray="2 4" opacity="0.7" />
        {[30, 50, 70].map(t => (
          <text key={t} x={padL+innerW+4} y={y(t)+3} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--text-4)">{t}</text>
        ))}
        <path d={path} fill="none" stroke="var(--violet)" strokeWidth="1.4" />
        {showX && (
          <>
            <line x1={padL} x2={padL+innerW} y1={height - 0.5} y2={height - 0.5} stroke="var(--border-soft)" />
            {xLabels.map((dl, i) => (
              <g key={'x'+i}>
                <line x1={x(dl.i)} x2={x(dl.i)} y1={height} y2={height + 3} stroke="var(--text-4)" />
                <text
                  x={x(dl.i)} y={svgH - 3}
                  fontSize="9" fontFamily="var(--font-mono)" fill="var(--text-4)"
                  textAnchor={i === 0 ? 'start' : i === xLabels.length - 1 ? 'end' : 'middle'}
                >
                  {dl.label}
                </text>
              </g>
            ))}
          </>
        )}
      </svg>
    </div>
  );
}

// === MACD PANEL ==========================================================
function MACDPanel({ data, height = 110, times }) {
  const wrapRef = useRef(null);
  const [w, setW] = useState(420);
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const showX = Array.isArray(times) && times.length >= 2 && data.line.length >= 2;
  const axisH = showX ? 16 : 0;
  const svgH  = height + axisH;
  const padL = 8, padR = 8, padT = 8, padB = 8;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;

  const all = [...data.line, ...data.signal, ...data.hist];
  let mn = Math.min(...all), mx = Math.max(...all);
  const m = Math.max(Math.abs(mn), Math.abs(mx));
  mn = -m; mx = m;

  const x = scaleLinear([0, Math.max(1, data.line.length - 1)], [padL, padL + innerW]);
  const y = scaleLinear([mn, mx], [padT + innerH, padT]);
  const xLabels = showX ? sparkTimeLabels(times.slice(0, data.line.length)) : [];

  let lineP = '', sigP = '';
  data.line.forEach((v, i) => { lineP += (i===0?'M':'L') + x(i).toFixed(1) + ' ' + y(v).toFixed(1) + ' '; });
  data.signal.forEach((v, i) => { sigP += (i===0?'M':'L') + x(i).toFixed(1) + ' ' + y(v).toFixed(1) + ' '; });

  const bw = Math.max(1, innerW / data.hist.length * 0.7);

  return (
    <div ref={wrapRef} style={{ width: '100%' }}>
      <svg width={w} height={svgH} style={{ display: 'block', overflow: 'visible' }}>
        <line x1={padL} x2={padL+innerW} y1={y(0)} y2={y(0)} stroke="var(--border)" />
        {data.hist.map((v, i) => {
          const up = v >= 0;
          return (
            <rect key={i}
              x={x(i) - bw/2}
              y={up ? y(v) : y(0)}
              width={bw}
              height={Math.abs(y(v) - y(0))}
              fill={up ? 'var(--bull)' : 'var(--bear)'}
              opacity="0.7"
            />
          );
        })}
        <path d={sigP}  fill="none" stroke="var(--gold)" strokeWidth="1.4" />
        <path d={lineP} fill="none" stroke="var(--info)" strokeWidth="1.4" />
        {showX && (
          <>
            <line x1={padL} x2={padL+innerW} y1={height - 0.5} y2={height - 0.5} stroke="var(--border-soft)" />
            {xLabels.map((dl, i) => (
              <g key={'x'+i}>
                <line x1={x(dl.i)} x2={x(dl.i)} y1={height} y2={height + 3} stroke="var(--text-4)" />
                <text
                  x={x(dl.i)} y={svgH - 3}
                  fontSize="9" fontFamily="var(--font-mono)" fill="var(--text-4)"
                  textAnchor={i === 0 ? 'start' : i === xLabels.length - 1 ? 'end' : 'middle'}
                >
                  {dl.label}
                </text>
              </g>
            ))}
          </>
        )}
      </svg>
    </div>
  );
}

// === GAUGE (semi-circle) — used for Fear & Greed, MVRV, etc. =============
function Gauge({ value, min=0, max=100, segments, label, height = 130 }) {
  const w = 220, cx = w/2, cy = height - 14, r = 92;
  const startA = Math.PI, endA = 0; // 180° -> 0°
  const angleFor = (v) => startA + (endA - startA) * ((v - min) / (max - min));

  // segments
  const arcs = segments.map((s, i) => {
    const a1 = angleFor(s.from), a2 = angleFor(s.to);
    const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
    const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
    const large = Math.abs(a2 - a1) > Math.PI ? 1 : 0;
    const sweep = a2 > a1 ? 1 : 0;
    return (
      <path key={i}
        d={`M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${large} ${sweep} ${x2.toFixed(2)} ${y2.toFixed(2)}`}
        fill="none" stroke={s.color} strokeWidth="14" strokeLinecap="butt" opacity="0.85"
      />
    );
  });

  const ang = angleFor(value);
  const nx = cx + r * Math.cos(ang), ny = cy + r * Math.sin(ang);

  return (
    <svg width={w} height={height} style={{ display: 'block' }}>
      {arcs}
      {/* ticks */}
      {[0, 25, 50, 75, 100].map(t => {
        const a = angleFor(min + (max-min)*t/100);
        const x1 = cx + (r+8) * Math.cos(a), y1 = cy + (r+8) * Math.sin(a);
        const x2 = cx + (r+14) * Math.cos(a), y2 = cy + (r+14) * Math.sin(a);
        return <line key={t} x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--text-4)" strokeWidth="1" />;
      })}
      {/* needle */}
      <line x1={cx} y1={cy} x2={nx} y2={ny} stroke="var(--text)" strokeWidth="2" />
      <circle cx={cx} cy={cy} r="6" fill="var(--bg-elev)" stroke="var(--text)" strokeWidth="1.5" />
      {/* value */}
      <text x={cx} y={cy - 30} textAnchor="middle" fontSize="28" fontWeight="600" fontFamily="var(--font-mono)" fill="var(--text)">{value}</text>
      <text x={cx} y={cy - 14} textAnchor="middle" fontSize="10" letterSpacing="0.1em" fill="var(--text-3)">{label}</text>
    </svg>
  );
}

// === HEATMAP ============================================================
function Heatmap({ data, height = 180 }) {
  const wrapRef = useRef(null);
  const [w, setW] = useState(420);
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const cellW = w / data.cols;
  const cellH = height / data.rows;

  const colorFor = (v) => {
    // dark -> warm yellow -> hot red. v in [0,1]
    if (v < 0.15) return `oklch(0.20 0.01 250 / ${0.0 + v*2})`;
    const t = (v - 0.15) / 0.85;
    // interpolate between violet, gold, red
    if (t < 0.5) {
      const k = t/0.5;
      return `oklch(${0.35 + k*0.25} ${0.10 + k*0.05} ${230 - k*150} / ${0.5 + v*0.5})`;
    } else {
      const k = (t-0.5)/0.5;
      return `oklch(${0.60 + k*0.10} ${0.15 + k*0.05} ${80 - k*55} / ${0.6 + v*0.4})`;
    }
  };

  return (
    <div ref={wrapRef} style={{ width: '100%' }}>
      <svg width={w} height={height} style={{ display: 'block' }}>
        {data.cells.map((c, i) => (
          <rect key={i}
            x={c.c * cellW} y={c.r * cellH}
            width={cellW - 0.5} height={cellH - 0.5}
            fill={colorFor(c.v)}
          />
        ))}
      </svg>
    </div>
  );
}

// === BAR CHART (for ETF flows etc.) ======================================
function BarChart({ values, height = 110, posColor='var(--bull)', negColor='var(--bear)' }) {
  const wrapRef = useRef(null);
  const [w, setW] = useState(420);
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver(es => setW(es[0].contentRect.width));
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const padL = 4, padR = 4, padT = 6, padB = 6;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;

  const mx = Math.max(...values.map(Math.abs));
  const x = scaleLinear([0, values.length], [padL, padL + innerW]);
  const y = scaleLinear([-mx, mx], [padT + innerH, padT]);
  const bw = innerW / values.length * 0.7;

  return (
    <div ref={wrapRef} style={{ width: '100%' }}>
      <svg width={w} height={height} style={{ display: 'block' }}>
        <line x1={padL} x2={padL+innerW} y1={y(0)} y2={y(0)} stroke="var(--border)" />
        {values.map((v, i) => {
          const up = v >= 0;
          return (
            <rect key={i}
              x={x(i) + (x(1)-x(0)-bw)/2}
              y={up ? y(v) : y(0)}
              width={bw}
              height={Math.abs(y(v) - y(0))}
              fill={up ? posColor : negColor}
              opacity="0.9"
            />
          );
        })}
      </svg>
    </div>
  );
}

// === DONUT / RING =======================================================
function Donut({ value, max=100, size=104, stroke=10, color='var(--gold)', label, sub }) {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const offset = c * (1 - value/max);
  return (
    <div style={{ position: 'relative', width: size, height: size }}>
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="var(--bg-elev)" strokeWidth={stroke} />
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={stroke}
                strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round" />
      </svg>
      <div style={{
        position: 'absolute', inset: 0, display: 'grid', placeItems: 'center',
        textAlign: 'center', fontFamily: 'var(--font-mono)'
      }}>
        <div>
          <div style={{ fontSize: 22, fontWeight: 600, color: 'var(--text)' }}>{label}</div>
          {sub && <div style={{ fontSize: 10, color: 'var(--text-3)', letterSpacing: '0.06em', marginTop: 2 }}>{sub}</div>}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  PriceChart, Sparkline, RSIPanel, MACDPanel, Gauge, Heatmap, BarChart, Donut, scaleLinear, sparkTimeLabels,
});
