/* ============================================================
   Oziq AI — chart primitives library
   All hand-drawn SVG; consistent visual language
   ============================================================ */

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

// Use ResizeObserver to track parent width
function useSize() {
  const ref = useRef(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  useLayoutEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        setSize({ w: e.contentRect.width, h: e.contentRect.height });
      }
    });
    ro.observe(ref.current);
    return () => ro.disconnect();
  }, []);
  return [ref, size];
}

// ============================================================
// Sparkline — for KPI tiles and inline use
// ============================================================
function Sparkline({ data, width = 80, height = 24, color = 'var(--ink-2)', fill = false, dot = false, strokeWidth = 1.25 }) {
  if (!data || data.length < 2) return null;
  const min = Math.min(...data);
  const max = Math.max(...data);
  const range = max - min || 1;
  const step = width / (data.length - 1);
  const points = data.map((v, i) => [i * step, height - 2 - ((v - min) / range) * (height - 4)]);
  const path = points.map((p, i) => (i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`)).join(' ');
  const areaPath = `${path} L ${width} ${height} L 0 ${height} Z`;
  return (
    <svg width={width} height={height} style={{ display: 'block', overflow: 'visible' }}>
      {fill && <path d={areaPath} fill={color} opacity="0.14" />}
      <path d={path} stroke={color} strokeWidth={strokeWidth} fill="none" strokeLinejoin="round" strokeLinecap="round" />
      {dot && <circle cx={points[points.length - 1][0]} cy={points[points.length - 1][1]} r="2" fill={color} />}
    </svg>
  );
}

// ============================================================
// Stacked area time series
// ============================================================
function StackedArea({ data, series, height = 240, xLabels = [], onHover }) {
  const [ref, size] = useSize();
  const [hover, setHover] = useState(null);
  const w = size.w || 600;
  const padL = 36, padR = 8, padT = 12, padB = 22;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;

  // compute stacks
  const stacks = data.map(d => {
    let acc = 0;
    const out = {};
    series.forEach(s => { out[s.key] = [acc, acc + (d[s.key] || 0)]; acc += (d[s.key] || 0); });
    out._total = acc;
    return out;
  });
  const max = Math.max(...stacks.map(s => s._total));
  const xs = data.map((_, i) => padL + (i / (data.length - 1)) * innerW);
  const y = (v) => padT + innerH - (v / max) * innerH;

  const paths = series.map(s => {
    const top = stacks.map((st, i) => `${i === 0 ? 'M' : 'L'} ${xs[i]} ${y(st[s.key][1])}`).join(' ');
    const bottom = stacks.map((st, i) => `L ${xs[stacks.length - 1 - i]} ${y(stacks[stacks.length - 1 - i][s.key][0])}`).join(' ').replace(/^L/, 'L');
    return `${top} ${bottom} Z`;
  });

  // y ticks
  const ticks = [0, max * 0.25, max * 0.5, max * 0.75, max];
  return (
    <div ref={ref} style={{ position: 'relative', width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height} style={{ display: 'block' }} onMouseMove={(e) => {
          const r = e.currentTarget.getBoundingClientRect();
          const x = e.clientX - r.left;
          const idx = Math.max(0, Math.min(data.length - 1, Math.round((x - padL) / innerW * (data.length - 1))));
          setHover(idx);
          onHover && onHover(idx);
        }} onMouseLeave={() => setHover(null)}>
          {/* grid */}
          {ticks.map((t, i) => (
            <g key={i}>
              <line x1={padL} x2={w - padR} y1={y(t)} y2={y(t)} stroke="var(--line)" strokeWidth="0.5" strokeDasharray={i === 0 ? '0' : '2 3'} />
              <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{formatNum(Math.round(t))}</text>
            </g>
          ))}
          {/* stacks */}
          {paths.map((p, i) => (
            <path key={i} d={p} fill={series[i].color} opacity="0.92" />
          ))}
          {/* hover line */}
          {hover != null && (
            <g>
              <line x1={xs[hover]} x2={xs[hover]} y1={padT} y2={padT + innerH} stroke="var(--ink)" strokeWidth="1" />
              <circle cx={xs[hover]} cy={y(stacks[hover]._total)} r="3" fill="var(--ink)" />
            </g>
          )}
          {/* x labels */}
          {xLabels.map((lbl, i) => {
            const idx = Math.round((i / (xLabels.length - 1)) * (data.length - 1));
            return <text key={i} x={xs[idx]} y={height - 6} textAnchor="middle" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{lbl}</text>;
          })}
        </svg>
      )}
      {hover != null && size.w > 0 && (
        <div className="chart-tip" style={{ left: xs[hover], top: padT + 4 }}>
          <div style={{ fontWeight: 600, marginBottom: 3 }}>{xLabels[Math.round(hover / (data.length - 1) * (xLabels.length - 1))] || `Day ${hover}`}</div>
          {series.slice().reverse().map(s => (
            <div key={s.key} style={{ display: 'flex', gap: 8, justifyContent: 'space-between' }}>
              <span><span style={{ display: 'inline-block', width: 7, height: 7, background: s.color, marginRight: 5 }} />{s.label}</span>
              <span>{formatNum(data[hover][s.key] || 0)}</span>
            </div>
          ))}
          <div style={{ borderTop: '1px solid rgba(255,255,255,0.2)', marginTop: 4, paddingTop: 3, fontWeight: 600, display: 'flex', justifyContent: 'space-between', gap: 8 }}>
            <span>Jami</span><span>{formatNum(stacks[hover]._total)}</span>
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================
// Brushable mini-timeline (under hero chart)
// ============================================================
function BrushTimeline({ data, height = 36, range, onChange }) {
  const [ref, size] = useSize();
  const w = size.w || 600;
  const padL = 36, padR = 8;
  const innerW = w - padL - padR;
  const max = Math.max(...data.map(d => d.total));
  const points = data.map((d, i) => [padL + (i / (data.length - 1)) * innerW, height - 2 - (d.total / max) * (height - 6)]);
  const areaPath = points.map((p, i) => (i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`)).join(' ') + ` L ${padL + innerW} ${height} L ${padL} ${height} Z`;

  const [from, to] = range || [data.length - 30, data.length - 1];
  const x1 = padL + (from / (data.length - 1)) * innerW;
  const x2 = padL + (to / (data.length - 1)) * innerW;

  return (
    <div ref={ref} style={{ position: 'relative', width: '100%', marginTop: 4 }}>
      {size.w > 0 && (
        <svg width={w} height={height + 16} style={{ display: 'block' }}>
          <path d={areaPath} fill="var(--ink-5)" />
          <rect x={x1} y={0} width={x2 - x1} height={height} fill="var(--accent)" opacity="0.16" />
          <rect x={x1 - 1} y={0} width={2} height={height} fill="var(--accent-ink)" />
          <rect x={x2 - 1} y={0} width={2} height={height} fill="var(--accent-ink)" />
          <text x={padL} y={height + 12} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">90d</text>
          <text x={w - padR} y={height + 12} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)" textAnchor="end">bugun</text>
        </svg>
      )}
    </div>
  );
}

// ============================================================
// Donut / Sunburst — 12 topics + drill-in
// ============================================================
function Donut({ data, size = 220, inner = 0.62, label = null, sub = null, onHover }) {
  const r = size / 2;
  const total = data.reduce((s, d) => s + d.value, 0);
  const ri = r * inner;
  let acc = -Math.PI / 2;
  const arcs = data.map((d, i) => {
    const ang = (d.value / total) * Math.PI * 2;
    const a0 = acc, a1 = acc + ang;
    acc = a1;
    const large = ang > Math.PI ? 1 : 0;
    const p0 = [r + r * Math.cos(a0), r + r * Math.sin(a0)];
    const p1 = [r + r * Math.cos(a1), r + r * Math.sin(a1)];
    const pi0 = [r + ri * Math.cos(a1), r + ri * Math.sin(a1)];
    const pi1 = [r + ri * Math.cos(a0), r + ri * Math.sin(a0)];
    return {
      ...d,
      path: `M ${p0[0]} ${p0[1]} A ${r} ${r} 0 ${large} 1 ${p1[0]} ${p1[1]} L ${pi0[0]} ${pi0[1]} A ${ri} ${ri} 0 ${large} 0 ${pi1[0]} ${pi1[1]} Z`,
      pct: (d.value / total) * 100,
      angle: (a0 + a1) / 2,
    };
  });
  const [hov, setHov] = useState(null);
  return (
    <div style={{ position: 'relative', width: size, height: size }}>
      <svg width={size} height={size}>
        {arcs.map((a, i) => (
          <path
            key={a.id || i}
            d={a.path}
            fill={a.color}
            opacity={hov == null ? 1 : hov === i ? 1 : 0.35}
            stroke="var(--panel)"
            strokeWidth="1.5"
            style={{ cursor: 'pointer', transition: 'opacity 120ms' }}
            onMouseEnter={() => { setHov(i); onHover && onHover(a); }}
            onMouseLeave={() => { setHov(null); onHover && onHover(null); }}
          />
        ))}
      </svg>
      <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
        {hov != null ? (
          <>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 500 }}>{arcs[hov].pct.toFixed(1)}%</div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', textAlign: 'center', marginTop: 2 }}>{arcs[hov].label}</div>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)', marginTop: 2 }}>{formatNum(arcs[hov].value)}</div>
          </>
        ) : (
          <>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 500 }}>{formatNum(total)}</div>
            <div style={{ fontSize: 10.5, color: 'var(--ink-3)', letterSpacing: '0.05em', textTransform: 'uppercase', marginTop: 2 }}>{label || 'Jami'}</div>
            {sub && <div style={{ fontSize: 10, color: 'var(--ink-4)', marginTop: 2 }}>{sub}</div>}
          </>
        )}
      </div>
    </div>
  );
}

// ============================================================
// Choropleth — Uzbekistan map
// ============================================================
// Lerp two hex colors. t ∈ [0,1].
function lerpHex(a, b, t) {
  const ah = parseInt(a.slice(1), 16);
  const bh = parseInt(b.slice(1), 16);
  const ar = (ah >> 16) & 0xff, ag = (ah >> 8) & 0xff, ab = ah & 0xff;
  const br = (bh >> 16) & 0xff, bg = (bh >> 8) & 0xff, bb = bh & 0xff;
  const r = Math.round(ar + (br - ar) * t);
  const g = Math.round(ag + (bg - ag) * t);
  const bl = Math.round(ab + (bb - ab) * t);
  return '#' + ((r << 16) | (g << 8) | bl).toString(16).padStart(6, '0');
}
// ngo.uz teal palette — light water → mid base → deep ink
function tealScale(t) {
  // 3-stop: very light → ngo.uz base → ngo.uz focus
  if (t < 0.5) return lerpHex('#e7f5f8', '#a5dae6', t * 2);
  return lerpHex('#a5dae6', '#16566c', (t - 0.5) * 2);
}

function UzMap({ values, height = 260, colorFn = null, onClick, selected = null, showLabels = true, layerLabel = '' }) {
  const max = Math.max(...Object.values(values || {}));
  const min = Math.min(...Object.values(values || {}));
  const cf = colorFn || ((v) => tealScale((v - min) / Math.max(1, max - min)));
  const [hover, setHover] = useState(null);
  const [svgText, setSvgText] = useState(null);
  const wrapRef = useRef(null);

  useEffect(() => {
    fetch('/uzbekistan.svg').then(r => r.text()).then(setSvgText).catch(() => setSvgText(''));
  }, []);

  // After SVG is injected, recolor paths and wire hover/click
  useEffect(() => {
    if (!svgText || !wrapRef.current) return;
    const svg = wrapRef.current.querySelector('svg');
    if (!svg) return;
    // The source SVG has width/height but no viewBox, so removing the
    // sizing attrs breaks scaling. Set viewBox from the original W/H first.
    if (!svg.getAttribute('viewBox')) {
      const w = parseFloat(svg.getAttribute('width') || '792.4873');
      const h = parseFloat(svg.getAttribute('height') || '516.87848');
      svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
    }
    svg.removeAttribute('width');
    svg.removeAttribute('height');
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    svg.style.width = '100%';
    svg.style.height = '100%';
    svg.style.display = 'block';

    // Style water + neighbour paths (anything not UZ-* or the Aral).
    // UZ-AS has an inline style="fill:#fff..." so we override via style
    // since attribute fill loses to inline style.
    svg.querySelectorAll('path').forEach(p => {
      const id = p.getAttribute('id') || '';
      if (id === 'UZ-AS' || !id.startsWith('UZ-')) {
        p.setAttribute('style', 'fill:#eaf4f7;stroke:#cfe4ea;stroke-width:0.6;pointer-events:none;');
      }
    });

    // Per-region fill + interaction
    const handlers = [];
    Object.entries(UZ_ID_MAP).forEach(([ozId, uzId]) => {
      const path = svg.querySelector('#' + uzId);
      if (!path) return;
      const v = values[ozId] || 0;
      const isSel = selected === ozId;
      const isHover = hover === ozId;
      path.setAttribute('fill', cf(v, REGION_BY_ID[ozId]));
      path.setAttribute('stroke', isSel ? '#16566c' : (isHover ? '#16566c' : '#6fb5c4'));
      path.setAttribute('stroke-width', isSel ? 1.6 : (isHover ? 1.4 : 0.9));
      path.style.cursor = onClick ? 'pointer' : 'default';
      path.style.transition = 'fill 120ms ease, stroke 120ms ease';
      const onEnter = () => setHover(ozId);
      const onLeave = () => setHover(null);
      const onClk = () => onClick && onClick(ozId);
      path.addEventListener('mouseenter', onEnter);
      path.addEventListener('mouseleave', onLeave);
      path.addEventListener('click', onClk);
      handlers.push([path, onEnter, onLeave, onClk]);
    });

    // Region code labels at path centroids
    if (showLabels) {
      // remove any previously added labels before re-adding
      svg.querySelectorAll('text.uz-label').forEach(t => t.remove());
      Object.entries(UZ_ID_MAP).forEach(([ozId, uzId]) => {
        const path = svg.querySelector('#' + uzId);
        if (!path) return;
        const bb = path.getBBox();
        const cx = bb.x + bb.width / 2;
        const cy = bb.y + bb.height / 2 + 3;
        const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        t.setAttribute('class', 'uz-label');
        t.setAttribute('x', cx);
        t.setAttribute('y', cy);
        t.setAttribute('text-anchor', 'middle');
        t.setAttribute('font-family', 'var(--font-mono)');
        t.setAttribute('font-size', '10');
        t.setAttribute('font-weight', '600');
        t.setAttribute('fill', '#0f4452');
        t.setAttribute('letter-spacing', '0.04em');
        t.style.pointerEvents = 'none';
        t.textContent = REGION_BY_ID[ozId]?.code || '';
        svg.appendChild(t);
      });
    }

    return () => {
      handlers.forEach(([p, e, l, c]) => {
        p.removeEventListener('mouseenter', e);
        p.removeEventListener('mouseleave', l);
        p.removeEventListener('click', c);
      });
    };
  }, [svgText, values, hover, selected, showLabels]);

  return (
    <div style={{ position: 'relative', width: '100%' }}>
      <div
        ref={wrapRef}
        className="uz-map-wrap"
        style={{ width: '100%', height, display: 'block' }}
        dangerouslySetInnerHTML={{ __html: svgText || '' }}
      />
      {layerLabel && (
        <div style={{
          position: 'absolute', top: 6, left: 8,
          fontSize: 10, fontFamily: 'var(--font-mono)', fontWeight: 600,
          color: 'var(--ink-3)', letterSpacing: '0.06em', textTransform: 'uppercase',
        }}>{layerLabel}</div>
      )}
      {hover && REGION_BY_ID[hover] && (
        <div className="chart-tip" style={{ left: '50%', top: 8, transform: 'translateX(-50%)' }}>
          <div style={{ fontWeight: 600 }}>{REGION_BY_ID[hover].uz}</div>
          <div style={{ color: 'oklch(0.7 0 0)', fontSize: 10 }}>{REGION_BY_ID[hover].ru}</div>
          <div style={{ marginTop: 3 }}>{formatNum(values[hover] || 0)} murojaat</div>
          {(() => {
            const tops = Object.entries(BY_REGION[hover].topics).sort((a, b) => b[1] - a[1]).slice(0, 3);
            return (
              <div style={{ borderTop: '1px solid rgba(255,255,255,0.2)', marginTop: 4, paddingTop: 4 }}>
                {tops.map(([tid, n]) => (
                  <div key={tid} style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
                    <span style={{ color: 'oklch(0.8 0 0)' }}>{TOPIC_BY_ID[tid].uz}</span><span>{n}</span>
                  </div>
                ))}
              </div>
            );
          })()}
        </div>
      )}
    </div>
  );
}

// ============================================================
// 24h × 7d heatmap
// ============================================================
function HourlyHeatmap({ data, height = 200 }) {
  const max = Math.max(...data.flat());
  const days = ['Du', 'Se', 'Ch', 'Pa', 'Ju', 'Sh', 'Ya'];
  const [hov, setHov] = useState(null);
  const [ref, size] = useSize();
  const w = size.w || 600;
  const padL = 26, padR = 8, padT = 18, padB = 6;
  const cellW = (w - padL - padR) / 24;
  const cellH = (height - padT - padB) / 7;

  return (
    <div ref={ref} style={{ position: 'relative', width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height} style={{ display: 'block' }}>
          {[0, 4, 8, 12, 16, 20].map(h => (
            <text key={h} x={padL + h * cellW + cellW / 2} y={12} textAnchor="middle" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{String(h).padStart(2, '0')}</text>
          ))}
          {days.map((d, i) => (
            <text key={d} x={padL - 6} y={padT + i * cellH + cellH / 2 + 3} textAnchor="end" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{d}</text>
          ))}
          {data.map((row, i) =>
            row.map((v, j) => {
              const t = v / max;
              const fill = `oklch(${(0.97 - t * 0.50).toFixed(3)} ${(0.01 + t * 0.10).toFixed(3)} 195)`;
              return (
                <rect
                  key={`${i}-${j}`}
                  x={padL + j * cellW + 1} y={padT + i * cellH + 1}
                  width={cellW - 2} height={cellH - 2}
                  fill={fill}
                  rx="1"
                  onMouseEnter={() => setHov({ d: i, h: j, v })}
                  onMouseLeave={() => setHov(null)}
                  style={{ cursor: 'pointer' }}
                />
              );
            })
          )}
        </svg>
      )}
      {hov && (
        <div className="chart-tip" style={{ left: padL + hov.h * cellW + cellW / 2, top: padT + hov.d * cellH }}>
          {days[hov.d]} {String(hov.h).padStart(2, '0')}:00 · <strong>{hov.v}</strong>
        </div>
      )}
    </div>
  );
}

// ============================================================
// Funnel
// ============================================================
function Funnel({ steps, height = 200 }) {
  const max = steps[0].value;
  const [ref, size] = useSize();
  const w = size.w || 400;
  const stepH = (height - 12) / steps.length;
  const labelW = 86;
  const valueColW = 52;
  const dropColW = 48;
  const barX = labelW + 6;
  const barMaxW = Math.max(40, w - barX - valueColW - dropColW - 8);
  const valueX = w - dropColW - 8;
  const dropX = w - 4;
  return (
    <div ref={ref} style={{ width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height}>
          {steps.map((s, i) => {
            const pct = s.value / max;
            const barW = barMaxW * pct;
            const y = i * stepH;
            const dropPct = i > 0 ? ((steps[i - 1].value - s.value) / steps[i - 1].value) * 100 : 0;
            return (
              <g key={i}>
                <text x={6} y={y + stepH / 2 + 3} fontSize="11.5" fill="var(--ink-2)">{s.uz}</text>
                <rect x={barX} y={y + 6} width={barW} height={stepH - 12} fill="var(--accent)" opacity={0.85 - i * 0.10} rx="2" />
                <text x={valueX} y={y + stepH / 2 + 3} fontSize="11" fontFamily="var(--font-mono)" textAnchor="end" fill="var(--ink-2)">{formatNum(s.value)}</text>
                {i > 0 && (
                  <text x={dropX} y={y + stepH / 2 + 3} fontSize="10" fontFamily="var(--font-mono)" textAnchor="end" fill="var(--ink-4)">
                    −{dropPct.toFixed(1)}%
                  </text>
                )}
              </g>
            );
          })}
        </svg>
      )}
    </div>
  );
}

// ============================================================
// Diverging bar (topic momentum)
// ============================================================
function DivergingBar({ data, height = 240 }) {
  const [ref, size] = useSize();
  const w = size.w || 400;
  const padL = 110;
  const valueColW = 56;
  const padR = 8;
  const innerW = w - padL - valueColW - padR;
  const maxAbs = Math.max(...data.map(d => Math.abs(d.delta)));
  const center = padL + innerW / 2;
  const barH = Math.min(18, (height - 8) / data.length - 4);
  const valueX = w - padR;

  return (
    <div ref={ref} style={{ width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={data.length * (barH + 4) + 8}>
          <line x1={center} x2={center} y1={0} y2={data.length * (barH + 4)} stroke="var(--line-strong)" strokeWidth="1" />
          {data.map((d, i) => {
            const y = 4 + i * (barH + 4);
            const isPos = d.delta >= 0;
            const len = (Math.abs(d.delta) / maxAbs) * (innerW / 2);
            const color = isPos ? 'var(--jade)' : 'var(--rose)';
            return (
              <g key={d.id}>
                <text x={padL - 6} y={y + barH / 2 + 3} textAnchor="end" fontSize="11" fill="var(--ink-2)">{d.label}</text>
                <rect x={isPos ? center : center - len} y={y} width={len} height={barH} fill={color} opacity="0.85" rx="1" />
                <text x={valueX} y={y + barH / 2 + 3} fontSize="10.5" fontFamily="var(--font-mono)" textAnchor="end" fill={isPos ? 'oklch(0.40 0.10 155)' : 'oklch(0.45 0.14 25)'} fontWeight="600">
                  {isPos ? '+' : ''}{d.delta.toFixed(1)}%
                </text>
              </g>
            );
          })}
        </svg>
      )}
    </div>
  );
}

// ============================================================
// Sentiment gauge (half donut)
// ============================================================
function SentGauge({ pos, neu, neg, label, sub }) {
  const total = pos + neu + neg;
  const r = 52, cx = 60, cy = 56;
  const arcLen = Math.PI; // half circle
  const a0 = Math.PI;
  const empty = total === 0;
  const pPos = empty ? 0 : (pos / total);
  const pNeu = empty ? 1 : (neu / total);
  const pNeg = empty ? 0 : (neg / total);
  const arc = (from, to, color) => {
    const a1 = a0 + from * arcLen;
    const a2 = a0 + to * arcLen;
    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);
    return <path d={`M ${x1} ${y1} A ${r} ${r} 0 0 1 ${x2} ${y2}`} stroke={color} strokeWidth="9" fill="none" strokeLinecap="butt" />;
  };
  const netScore = empty ? 0 : ((pos - neg) / total) * 100;
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '4px 4px 0' }}>
      <svg width="120" height="68" style={{ overflow: 'visible' }}>
        {arc(0, pNeg, 'var(--rose)')}
        {arc(pNeg, pNeg + pNeu, 'var(--ink-4)')}
        {arc(pNeg + pNeu, 1, 'var(--jade)')}
        <text x={cx} y={cy - 4} textAnchor="middle" fontSize="16" fontFamily="var(--font-mono)" fontWeight="500" fill={netScore >= 0 ? 'var(--jade)' : 'var(--rose)'}>{netScore >= 0 ? '+' : ''}{netScore.toFixed(0)}</text>
        <text x={cx} y={cy + 8} textAnchor="middle" fontSize="8.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">NET</text>
      </svg>
      <div style={{ fontSize: 11.5, fontWeight: 500, marginTop: 4, textAlign: 'center' }}>{label}</div>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 9.5, color: 'var(--ink-4)', marginTop: 2, display: 'flex', gap: 6 }}>
        <span style={{ color: 'var(--rose)' }}>−{neg}</span>
        <span>·{neu}·</span>
        <span style={{ color: 'var(--jade)' }}>+{pos}</span>
      </div>
    </div>
  );
}

// ============================================================
// Word cloud (sized by count)
// ============================================================
function WordCloud({ words, onClick }) {
  const max = Math.max(...words.map(w => w.count));
  const min = Math.min(...words.map(w => w.count));
  return (
    <div className="cloud">
      {words.map((w, i) => {
        const t = (w.count - min) / Math.max(1, max - min);
        const size = 11 + t * 18;
        const weight = 400 + Math.round(t * 3) * 100;
        const grew = w.count > w.prev;
        return (
          <span
            key={w.w}
            onClick={() => onClick && onClick(w)}
            style={{
              fontSize: size,
              fontWeight: weight,
              color: t > 0.6 ? 'var(--ink)' : 'var(--ink-2)',
              fontFamily: 'var(--font-serif)',
              letterSpacing: '-0.01em',
              borderBottom: grew ? '1.5px solid var(--accent)' : '1.5px solid transparent',
              paddingBottom: 1,
            }}
            title={`${w.count} · was ${w.prev}`}
          >
            {w.w}
          </span>
        );
      })}
    </div>
  );
}

// ============================================================
// Mini bar (used in leaderboards)
// ============================================================
function MiniBar({ value, max, height = 8, color = 'var(--accent)' }) {
  return (
    <div style={{ width: '100%', height, background: 'var(--bg-sunken)', borderRadius: 1, overflow: 'hidden' }}>
      <div style={{ width: `${(value / max) * 100}%`, height: '100%', background: color }} />
    </div>
  );
}

// ============================================================
// Treemap (used for sub-categories)
// ============================================================
function Treemap({ data, width, height }) {
  // simple squarified-ish layout
  const total = data.reduce((s, d) => s + d.value, 0);
  let x = 0, y = 0;
  let remainingW = width, remainingH = height;
  const cells = [];
  // alternate row/col splits
  let horizontal = width >= height;
  let remainingData = [...data];
  while (remainingData.length > 0) {
    const item = remainingData.shift();
    const pct = item.value / (remainingData.reduce((s, d) => s + d.value, 0) + item.value);
    if (remainingData.length === 0) {
      cells.push({ ...item, x, y, w: remainingW, h: remainingH });
      break;
    }
    if (horizontal) {
      const cw = remainingW * pct;
      cells.push({ ...item, x, y, w: cw, h: remainingH });
      x += cw; remainingW -= cw;
    } else {
      const ch = remainingH * pct;
      cells.push({ ...item, x, y, w: remainingW, h: ch });
      y += ch; remainingH -= ch;
    }
    horizontal = remainingW >= remainingH;
  }
  return (
    <svg width={width} height={height} style={{ display: 'block' }}>
      {cells.map((c, i) => (
        <g key={i}>
          <rect x={c.x} y={c.y} width={c.w - 2} height={c.h - 2} fill={c.color} opacity="0.92" rx="2" />
          {c.w > 60 && c.h > 24 && (
            <>
              <text x={c.x + 8} y={c.y + 16} fontSize="10.5" fontWeight="600" fill="oklch(0.20 0.012 250)">{c.label}</text>
              <text x={c.x + 8} y={c.y + 30} fontSize="10" fontFamily="var(--font-mono)" fill="oklch(0.30 0.012 250)">{formatNum(c.value)}</text>
            </>
          )}
        </g>
      ))}
    </svg>
  );
}

// ============================================================
// Topic mark (color square + glyph)
// ============================================================
function TopicMark({ topicId, size = 18 }) {
  const t = TOPIC_BY_ID[topicId];
  if (!t) return null;
  return (
    <span style={{
      width: size, height: size,
      background: t.color,
      borderRadius: 3,
      display: 'inline-flex',
      alignItems: 'center', justifyContent: 'center',
      fontFamily: 'var(--font-mono)',
      fontSize: size * 0.5,
      fontWeight: 600,
      color: 'oklch(0.18 0.012 250)',
      letterSpacing: '-0.03em',
      flexShrink: 0,
    }}>
      {t.glyph}
    </span>
  );
}

// ============================================================
// Line chart with optional anomaly markers
// ============================================================
function LineChart({ data, height = 200, color = 'var(--accent)', fill = true, anomalies = [], yLabel = '', xLabels = [], strokeWidth = 1.5 }) {
  const [ref, size] = useSize();
  const [hov, setHov] = useState(null);
  const w = size.w || 600;
  const padL = 36, padR = 8, padT = 12, padB = 22;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;
  if (data.length === 0) return <div ref={ref} />;

  const max = Math.max(...data) * 1.1;
  const min = 0;
  const range = max - min || 1;
  const xs = data.map((_, i) => padL + (i / (data.length - 1)) * innerW);
  const y = (v) => padT + innerH - (v / range) * innerH;
  const points = data.map((v, i) => [xs[i], y(v)]);
  const path = points.map((p, i) => (i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`)).join(' ');
  const areaPath = `${path} L ${xs[xs.length - 1]} ${y(0)} L ${xs[0]} ${y(0)} Z`;

  const ticks = [0, max * 0.5, max];

  return (
    <div ref={ref} style={{ position: 'relative', width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height} onMouseMove={(e) => {
          const r = e.currentTarget.getBoundingClientRect();
          const x = e.clientX - r.left;
          const idx = Math.max(0, Math.min(data.length - 1, Math.round((x - padL) / innerW * (data.length - 1))));
          setHov(idx);
        }} onMouseLeave={() => setHov(null)}>
          {ticks.map((t, i) => (
            <g key={i}>
              <line x1={padL} x2={w - padR} y1={y(t)} y2={y(t)} stroke="var(--line)" strokeWidth="0.5" strokeDasharray={i === 0 ? '0' : '2 3'} />
              <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{formatNum(Math.round(t))}</text>
            </g>
          ))}
          {fill && <path d={areaPath} fill={color} opacity="0.13" />}
          <path d={path} stroke={color} strokeWidth={strokeWidth} fill="none" strokeLinejoin="round" strokeLinecap="round" />
          {anomalies.map((a, i) => (
            <g key={i}>
              <circle cx={xs[a]} cy={y(data[a])} r="5" fill="none" stroke="var(--rose)" strokeWidth="1.5" />
              <circle cx={xs[a]} cy={y(data[a])} r="2.5" fill="var(--rose)" />
              <line x1={xs[a]} x2={xs[a]} y1={y(data[a]) - 8} y2={y(data[a]) - 18} stroke="var(--rose)" strokeWidth="1" />
              <text x={xs[a]} y={y(data[a]) - 22} fontSize="9" fontFamily="var(--font-mono)" textAnchor="middle" fill="var(--rose)" fontWeight="600">⚠</text>
            </g>
          ))}
          {hov != null && (
            <g>
              <line x1={xs[hov]} x2={xs[hov]} y1={padT} y2={padT + innerH} stroke="var(--ink)" strokeWidth="0.5" />
              <circle cx={xs[hov]} cy={y(data[hov])} r="3" fill="var(--ink)" />
            </g>
          )}
          {xLabels.length > 0 && xLabels.map((lbl, i) => {
            const idx = Math.round((i / (xLabels.length - 1)) * (data.length - 1));
            return <text key={i} x={xs[idx]} y={height - 6} textAnchor="middle" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{lbl}</text>;
          })}
        </svg>
      )}
      {hov != null && size.w > 0 && (
        <div className="chart-tip" style={{ left: xs[hov], top: y(data[hov]) }}>
          {xLabels[Math.round(hov / (data.length - 1) * (xLabels.length - 1))] || `t=${hov}`}: <strong>{formatNum(data[hov])}</strong>
        </div>
      )}
    </div>
  );
}

// Multi-line chart (sentiment over time per channel)
function MultiLine({ series, height = 200, xLabels = [] }) {
  const [ref, size] = useSize();
  const w = size.w || 600;
  const padL = 36, padR = 8, padT = 12, padB = 22;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;
  const allValues = series.flatMap(s => s.data);
  const max = Math.max(...allValues) * 1.1;
  const min = Math.min(...allValues, 0);
  const range = max - min || 1;
  const n = series[0]?.data.length || 0;
  const xs = Array.from({ length: n }, (_, i) => padL + (i / (n - 1)) * innerW);
  const y = (v) => padT + innerH - ((v - min) / range) * innerH;
  const ticks = [min, (min + max) / 2, max];

  return (
    <div ref={ref} style={{ position: 'relative', width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height}>
          {ticks.map((t, i) => (
            <g key={i}>
              <line x1={padL} x2={w - padR} y1={y(t)} y2={y(t)} stroke="var(--line)" strokeWidth="0.5" strokeDasharray={i === 1 ? '2 3' : '0'} />
              <text x={padL - 6} y={y(t) + 3} textAnchor="end" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{Math.round(t)}</text>
            </g>
          ))}
          {series.map((s, i) => {
            const path = s.data.map((v, j) => (j === 0 ? `M ${xs[j]} ${y(v)}` : `L ${xs[j]} ${y(v)}`)).join(' ');
            return <path key={i} d={path} stroke={s.color} strokeWidth={1.5} fill="none" strokeLinejoin="round" />;
          })}
          {xLabels.map((lbl, i) => {
            const idx = Math.round((i / (xLabels.length - 1)) * (n - 1));
            return <text key={i} x={xs[idx]} y={height - 6} textAnchor="middle" fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-4)">{lbl}</text>;
          })}
        </svg>
      )}
    </div>
  );
}

// ============================================================
// Responsive treemap
// ============================================================
function TreemapResp({ data, height = 160 }) {
  const [ref, size] = useSize();
  return (
    <div ref={ref} style={{ width: '100%', height }}>
      {size.w > 0 && <Treemap data={data} width={size.w} height={height} />}
    </div>
  );
}

// ============================================================
// Radar / spider chart
// ============================================================
function Radar({ axes, series, size = 200, max = null }) {
  const r = size / 2 - 24;
  const cx = size / 2, cy = size / 2;
  const n = axes.length;
  const maxV = max || Math.max(...series.flatMap(s => s.values));
  const angle = (i) => -Math.PI / 2 + (i / n) * Math.PI * 2;
  const point = (v, i) => {
    const t = v / maxV;
    return [cx + Math.cos(angle(i)) * r * t, cy + Math.sin(angle(i)) * r * t];
  };
  const rings = [0.25, 0.5, 0.75, 1];
  return (
    <svg width={size} height={size}>
      {/* rings */}
      {rings.map((rr, i) => {
        const pts = axes.map((_, j) => {
          const x = cx + Math.cos(angle(j)) * r * rr;
          const y = cy + Math.sin(angle(j)) * r * rr;
          return `${x},${y}`;
        }).join(' ');
        return <polygon key={i} points={pts} fill="none" stroke="var(--line)" strokeWidth={i === rings.length - 1 ? 1 : 0.5} strokeDasharray={i === rings.length - 1 ? '0' : '2 3'} />;
      })}
      {/* axes */}
      {axes.map((a, i) => {
        const [x, y] = [cx + Math.cos(angle(i)) * r, cy + Math.sin(angle(i)) * r];
        const [lx, ly] = [cx + Math.cos(angle(i)) * (r + 12), cy + Math.sin(angle(i)) * (r + 12)];
        const anchor = Math.abs(Math.cos(angle(i))) < 0.3 ? 'middle' : (Math.cos(angle(i)) > 0 ? 'start' : 'end');
        return (
          <g key={i}>
            <line x1={cx} y1={cy} x2={x} y2={y} stroke="var(--line)" strokeWidth="0.5" />
            <text x={lx} y={ly + 3} fontSize="9.5" fontFamily="var(--font-mono)" fill="var(--ink-3)" textAnchor={anchor}>{a}</text>
          </g>
        );
      })}
      {/* series */}
      {series.map((s, si) => {
        const pts = s.values.map((v, i) => point(v, i));
        const path = pts.map((p, i) => (i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`)).join(' ') + ' Z';
        return (
          <g key={si}>
            <path d={path} fill={s.color} fillOpacity={s.fillOpacity ?? 0.18} stroke={s.color} strokeWidth={1.5} strokeDasharray={s.dashed ? '4 3' : '0'} />
            {!s.dashed && pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r={2.5} fill={s.color} />)}
          </g>
        );
      })}
    </svg>
  );
}

// ============================================================
// Bubble chart
// ============================================================
function BubbleChart({ data, height = 280, xLabel = 'X', yLabel = 'Y', onHover }) {
  const [ref, size] = useSize();
  const [hov, setHov] = useState(null);
  const w = size.w || 600;
  const padL = 44, padR = 16, padT = 14, padB = 28;
  const innerW = w - padL - padR;
  const innerH = height - padT - padB;
  const xMax = Math.max(...data.map(d => d.x)) * 1.1;
  const xMin = 0;
  const yMin = Math.min(...data.map(d => d.y));
  const yMax = Math.max(...data.map(d => d.y));
  const yPad = (yMax - yMin) * 0.18;
  const rMax = Math.max(...data.map(d => d.r));
  const x = (v) => padL + ((v - xMin) / (xMax - xMin)) * innerW;
  const y = (v) => padT + innerH - ((v - (yMin - yPad)) / ((yMax + yPad) - (yMin - yPad))) * innerH;
  const r = (v) => 4 + (v / rMax) * 18;

  return (
    <div ref={ref} style={{ position: 'relative', width: '100%' }}>
      {size.w > 0 && (
        <svg width={w} height={height}>
          {/* grid lines */}
          {[0, 0.25, 0.5, 0.75, 1].map((t, i) => (
            <line key={i} x1={padL} x2={w - padR} y1={padT + t * innerH} y2={padT + t * innerH} stroke="var(--line)" strokeWidth="0.5" strokeDasharray={t === 0 || t === 1 ? '0' : '2 3'} />
          ))}
          {/* zero line on y if appropriate */}
          {yMin < 0 && yMax > 0 && (
            <line x1={padL} x2={w - padR} y1={y(0)} y2={y(0)} stroke="var(--ink-4)" strokeWidth="1" strokeDasharray="3 2" />
          )}
          {/* labels */}
          <text x={padL} y={height - 8} fontSize="10" fontFamily="var(--font-mono)" fill="var(--ink-4)">{xLabel} →</text>
          <text x={padL - 38} y={padT + 4} fontSize="10" fontFamily="var(--font-mono)" fill="var(--ink-4)">↑ {yLabel}</text>
          {/* bubbles */}
          {data.map((d, i) => (
            <g key={i}
              onMouseEnter={() => { setHov(i); onHover && onHover(d); }}
              onMouseLeave={() => { setHov(null); onHover && onHover(null); }}
              style={{ cursor: 'pointer' }}>
              <circle cx={x(d.x)} cy={y(d.y)} r={r(d.r)} fill={d.color || 'var(--accent)'} fillOpacity={hov === i ? 0.5 : 0.32} stroke={d.color || 'var(--accent)'} strokeWidth="1.2" />
              <text x={x(d.x)} y={y(d.y) + 3} textAnchor="middle" fontSize="9" fontFamily="var(--font-mono)" fontWeight="600" fill="var(--ink)">{d.label}</text>
            </g>
          ))}
        </svg>
      )}
      {hov != null && size.w > 0 && (
        <div className="chart-tip" style={{ left: x(data[hov].x), top: y(data[hov].y) - r(data[hov].r) }}>
          <div style={{ fontWeight: 600 }}>{data[hov].fullLabel || data[hov].label}</div>
          <div>{xLabel}: {data[hov].x}</div>
          <div>{yLabel}: {data[hov].y.toFixed(2)}</div>
          <div>hajm: {data[hov].r}</div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  useSize, Sparkline, StackedArea, BrushTimeline, Donut, UzMap, HourlyHeatmap,
  Funnel, DivergingBar, SentGauge, WordCloud, MiniBar, Treemap, TopicMark,
  LineChart, MultiLine, TreemapResp,
  Radar, BubbleChart,
});
