// Shared components for the Revenue Dashboard
const { useState, useMemo, useRef, useEffect } = React;

// ---------- Formatters ----------
// Core: comma thousands separator, configurable decimals
const numFmt = (n, d = 1) =>
  (parseFloat(n) || 0).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });

// fmtTHB – input in millions (M), revenue dashboard
const fmtTHB = (v, { compact = true, sign = false } = {}) => {
  const n = parseFloat(v) || 0;
  const abs = Math.abs(n);
  let str;
  if (compact && abs >= 1000) str = numFmt(n / 1000) + 'B';
  else if (compact && abs >= 1) str = numFmt(n) + 'M';
  else str = numFmt(n) + 'M';
  if (sign && n > 0) str = '+' + str;
  return '฿' + str;
};
const fmtPct = (v, sign = true) => (sign && v > 0 ? '+' : '') + numFmt(v) + '%';
const fmtInt = (v) => Math.round(parseFloat(v) || 0).toLocaleString('en-US');
const fmtCustomers = (v) =>
  v >= 1_000_000 ? numFmt(v / 1_000_000) + 'M' : v >= 1000 ? numFmt(v / 1000) + 'K' : String(Math.round(v));

// ---------- Icons (minimal stroke) ----------
const Icon = ({ name, size = 14 }) => {
  const props = { width: size, height: size, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
  const paths = {
    grid: <><rect x="2" y="2" width="5" height="5" /><rect x="9" y="2" width="5" height="5" /><rect x="2" y="9" width="5" height="5" /><rect x="9" y="9" width="5" height="5" /></>,
    chart: <><path d="M2 13V3M2 13h11" /><path d="M5 10l2-3 2 2 3-5" /></>,
    users: <><circle cx="6" cy="6" r="2.5" /><path d="M2 13c0-2.5 1.8-4 4-4s4 1.5 4 4" /><path d="M11 5.5a2 2 0 1 1 0 4" /><path d="M11 9.5c1.8 0 3 1.2 3 3" /></>,
    file: <><path d="M3 2h6l4 4v8H3z" /><path d="M9 2v4h4" /></>,
    cog: <><circle cx="8" cy="8" r="2" /><path d="M8 1v2M8 13v2M15 8h-2M3 8H1M13 3l-1.4 1.4M4.4 11.6 3 13M13 13l-1.4-1.4M4.4 4.4 3 3" /></>,
    search: <><circle cx="7" cy="7" r="4.5" /><path d="M14 14l-3.5-3.5" /></>,
    bell: <><path d="M4 11V7a4 4 0 1 1 8 0v4l1 2H3z" /><path d="M6.5 14a1.5 1.5 0 0 0 3 0" /></>,
    chevron: <path d="M5 3l5 5-5 5" />,
    arrowDown: <><path d="M8 3v10" /><path d="M4 9l4 4 4-4" /></>,
    arrowUp: <><path d="M8 13V3" /><path d="M4 7l4-4 4 4" /></>,
    download: <><path d="M8 2v8" /><path d="M4 7l4 3 4-3" /><path d="M2 13h12" /></>,
    filter: <><path d="M2 3h12l-4.5 6V14L6.5 12V9z" /></>,
    close: <><path d="M3 3l10 10M13 3 3 13" /></>,
    spark: <path d="M2 11l3-3 3 2 3-5 3 2" />,
    plus: <><path d="M8 3v10M3 8h10" /></>,
    info: <><circle cx="8" cy="8" r="6" /><path d="M8 7v4M8 5v.5" /></>,
    dot: <circle cx="8" cy="8" r="2.5" fill="currentColor" stroke="none" />,
    drag: <><circle cx="6" cy="4" r="1" fill="currentColor" stroke="none" /><circle cx="10" cy="4" r="1" fill="currentColor" stroke="none" /><circle cx="6" cy="8" r="1" fill="currentColor" stroke="none" /><circle cx="10" cy="8" r="1" fill="currentColor" stroke="none" /><circle cx="6" cy="12" r="1" fill="currentColor" stroke="none" /><circle cx="10" cy="12" r="1" fill="currentColor" stroke="none" /></>,
    list: <><path d="M3 4h10M3 8h10M3 12h6" /></>,
    pipeline: <><circle cx="8" cy="8" r="6" /><polyline points="8,5 8,8 10.5,9.5" /></>,
  };
  return <svg {...props}>{paths[name] || null}</svg>;
};

// ---------- Mini sparkline ----------
const Sparkline = ({ data, width = 120, height = 36, color = '#2d3a8c', fill = true, strokeWidth = 1.5 }) => {
  if (!data || !data.length) return null;
  const max = Math.max(...data);
  const min = Math.min(...data);
  const range = max - min || 1;
  const stepX = width / (data.length - 1);
  const points = data.map((v, i) => [i * stepX, height - 4 - ((v - min) / range) * (height - 8)]);
  const path = points.map(([x, y], i) => (i === 0 ? `M${x},${y}` : `L${x},${y}`)).join(' ');
  const areaPath = path + ` L${width},${height} L0,${height} Z`;
  const last = points[points.length - 1];
  return (
    <svg width={width} height={height} style={{ display: 'block', overflow: 'visible' }}>
      {fill && <path d={areaPath} fill={color} opacity="0.08" />}
      <path d={path} stroke={color} strokeWidth={strokeWidth} fill="none" strokeLinecap="round" strokeLinejoin="round" />
      <circle cx={last[0]} cy={last[1]} r="2.5" fill={color} />
    </svg>
  );
};

// ---------- Delta chip ----------
const Delta = ({ value, suffix = '%', size = 'sm' }) => {
  const pos = value >= 0;
  const color = pos ? 'var(--positive)' : 'var(--negative)';
  return (
    <span className="num" style={{
      color,
      fontSize: size === 'lg' ? 13 : 11,
      fontWeight: 500,
      display: 'inline-flex',
      alignItems: 'center',
      gap: 2,
    }}>
      <span style={{ fontSize: size === 'lg' ? 11 : 9, lineHeight: 1 }}>{pos ? '▲' : '▼'}</span>
      {Math.abs(value).toFixed(1)}{suffix}
    </span>
  );
};

// ---------- Line Chart (responsive) ----------
const LineChart = ({ series, labels, height = 220, showAxis = true, colors = ['#2d3a8c', '#d97b2e'], showLegend = true, annotation, yFormat, onClickPoint }) => {
  const ref = useRef(null);
  const [width, setWidth] = useState(600);
  const [hoverIdx, setHoverIdx] = useState(null);
  useEffect(() => {
    if (!ref.current) return;
    const obs = new ResizeObserver(() => { if (ref.current) setWidth(ref.current.clientWidth); });
    obs.observe(ref.current);
    setWidth(ref.current.clientWidth);
    return () => obs.disconnect();
  }, []);

  const padL = 44, padR = 16, padT = 16, padB = 28;
  const W = width, H = height;
  const innerW = W - padL - padR;
  const innerH = H - padT - padB;
  // Sanitize: replace NaN/Infinity with 0
  const cleanSeries = series.map(s => ({ ...s, data: (s.data || []).map(v => (isFinite(v) ? v : 0)) }));
  const all = cleanSeries.flatMap(s => s.data);
  const max = all.length ? Math.max(...all) : 0;
  const min = 0;
  const niceMax = (() => {
    if (max <= 0) return 100;
    const mag = Math.pow(10, Math.floor(Math.log10(max)));
    const steps = [1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10];
    for (const s of steps) if (s * mag >= max) return s * mag;
    return 10 * mag;
  })();
  const stepX = labels.length > 1 ? innerW / (labels.length - 1) : innerW;
  const yTicks = [0, 0.25, 0.5, 0.75, 1].map(t => niceMax * t);

  const toXY = (v, i) => {
    const sv = isFinite(v) ? v : 0;
    const x = padL + (isFinite(i) ? i : 0) * stepX;
    const y = padT + innerH - ((sv - min) / (niceMax - min || 1)) * innerH;
    return [isFinite(x) ? x : 0, isFinite(y) ? y : 0];
  };

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <svg width={W} height={H} style={{ display: 'block', overflow: 'visible' }}>
        {/* gridlines */}
        {yTicks.map((t, i) => {
          const y = padT + innerH - (t / niceMax) * innerH;
          return (
            <g key={i}>
              <line x1={padL} y1={y} x2={W - padR} y2={y} stroke="#e7e4dc" strokeDasharray={i === 0 ? '' : '2 3'} />
              {showAxis && <text x={padL - 8} y={y + 3} textAnchor="end" fontSize="10" fontFamily="IBM Plex Mono" fill="#8b8f99">{yFormat ? yFormat(t) : `฿${t >= 1000 ? (t / 1000).toFixed(1) + 'B' : t + 'M'}`}</text>}
            </g>
          );
        })}
        {/* x labels */}
        {labels.map((l, i) => {
          if (labels.length > 12 && i % 2 !== 0) return null;
          const x = padL + i * stepX;
          const isHov = hoverIdx === i;
          return <text key={i} x={x} y={H - 8} textAnchor="middle" fontSize="10" fontFamily="IBM Plex Mono"
            fill={isHov && onClickPoint ? '#2d3a8c' : '#8b8f99'} fontWeight={isHov && onClickPoint ? '700' : '400'}>{l}</text>;
        })}
        {/* series */}
        {cleanSeries.map((s, si) => {
          const pts = s.data.map(toXY);
          const path = pts.map(([x, y], i) => (i === 0 ? `M${x},${y}` : `L${x},${y}`)).join(' ');
          const area = path + ` L${pts[pts.length - 1][0]},${padT + innerH} L${pts[0][0]},${padT + innerH} Z`;
          const c = colors[si % colors.length];
          return (
            <g key={si}>
              {si === 0 && <path d={area} fill={c} opacity="0.06" />}
              <path d={path} stroke={c} strokeWidth={s.dashed ? 1.5 : 2} fill="none" strokeDasharray={s.dashed ? '4 3' : ''} strokeLinecap="round" strokeLinejoin="round" />
              {pts.map(([x, y], i) => (
                <circle key={i} cx={x} cy={y}
                  r={hoverIdx === i ? 5 : (i === pts.length - 1 ? 3.5 : 2)}
                  fill={c} stroke="#fff" strokeWidth={1}
                  style={{ transition: 'r 0.1s' }} />
              ))}
            </g>
          );
        })}
        {annotation && (() => {
          const [x, y] = toXY(annotation.value, annotation.index);
          return (
            <g>
              <line x1={x} y1={padT} x2={x} y2={padT + innerH} stroke="#1a1d24" strokeDasharray="2 2" opacity="0.3" />
              <rect x={x + 6} y={y - 28} width="86" height="22" rx="3" fill="#1a1d24" />
              <text x={x + 12} y={y - 13} fontSize="10" fontFamily="IBM Plex Mono" fill="#fff">{annotation.label}</text>
            </g>
          );
        })()}
        {/* clickable hover columns (rendered last so they are on top) */}
        {onClickPoint && labels.map((l, i) => {
          const x = padL + i * stepX;
          const colW = labels.length > 1 ? stepX : innerW;
          const isHov = hoverIdx === i;
          return (
            <g key={`hit-${i}`}>
              {isHov && <rect x={x - colW / 2} y={padT} width={colW} height={innerH} fill="#2d3a8c" opacity="0.06" rx="2" />}
              <rect
                x={x - colW / 2} y={padT} width={colW} height={innerH + padB}
                fill="transparent"
                style={{ cursor: 'pointer' }}
                onMouseEnter={() => setHoverIdx(i)}
                onMouseLeave={() => setHoverIdx(null)}
                onClick={() => onClickPoint(i)}
              />
            </g>
          );
        })}
      </svg>
      {showLegend && (
        <div style={{ display: 'flex', gap: 16, paddingLeft: padL, marginTop: 4 }}>
          {series.map((s, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: 'var(--ink-2)' }}>
              <span style={{ width: 18, height: 0, borderTop: `2px ${s.dashed ? 'dashed' : 'solid'} ${colors[i % colors.length]}` }} />
              {s.label}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

// ---------- Horizontal stacked bar (group shares) ----------
const StackedBar = ({ data, total }) => {
  return (
    <div style={{ display: 'flex', width: '100%', height: 14, borderRadius: 2, overflow: 'hidden', background: '#f1eee5' }}>
      {data.map((d, i) => (
        <div key={i} title={`${d.name}: ${(d.value / total * 100).toFixed(1)}%`}
             style={{ width: `${d.value / total * 100}%`, background: d.color, borderRight: i < data.length - 1 ? '1px solid #fff' : 'none' }} />
      ))}
    </div>
  );
};

// ---------- KPI Card ----------
const KPICard = ({ label, value, sublabel, delta, deltaLabel, trend, trendColor, accent }) => (
  <div style={{
    background: 'var(--panel)',
    border: '1px solid var(--line)',
    padding: '16px 18px',
    display: 'flex',
    flexDirection: 'column',
    gap: 10,
    position: 'relative',
    overflow: 'hidden',
  }}>
    {accent && <div style={{ position: 'absolute', top: 0, left: 0, width: 3, height: '100%', background: accent }} />}
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 500 }}>{label}</div>
      <Icon name="info" size={12} />
    </div>
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
      <div className="num" style={{ fontSize: 26, fontWeight: 500, letterSpacing: '-0.02em' }}>{value}</div>
      {delta !== undefined && <Delta value={delta} size="lg" />}
    </div>
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: 12 }}>
      <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>{sublabel || deltaLabel}</div>
      {trend && <Sparkline data={trend} width={84} height={28} color={trendColor || 'var(--accent)'} fill={true} />}
    </div>
  </div>
);

// ---------- Pill / segmented control ----------
const Segmented = ({ options, value, onChange, dense = false }) => (
  <div style={{
    display: 'inline-flex',
    background: '#ece9df',
    padding: 2,
    borderRadius: 3,
    fontSize: 12,
  }}>
    {options.map(o => (
      <button key={o} onClick={() => onChange(o)}
        style={{
          padding: dense ? '3px 9px' : '5px 12px',
          background: value === o ? 'var(--panel)' : 'transparent',
          color: value === o ? 'var(--ink)' : 'var(--ink-3)',
          fontWeight: value === o ? 500 : 400,
          borderRadius: 2,
          boxShadow: value === o ? '0 1px 2px rgba(0,0,0,0.06)' : 'none',
          transition: 'all 120ms',
        }}>
        {o}
      </button>
    ))}
  </div>
);

// ---------- Group color mapping ----------
const GROUP_COLORS = {
  mobile: '#2d3a8c',
  ict: '#d97b2e',
  naas: '#1f7a4d',
  in: '#6b4a8a',
  ucaas: '#3a6b8a',
  sms: '#a8553a',
};

// Mutable name lookup — populated at runtime from API (same pattern as GROUP_COLORS)
const GROUP_NAMES = {
  mobile: 'Mobile',
  ict: 'ICT Solution',
  naas: 'Network as a Service',
  in: 'Intelligent Network',
  ucaas: 'UC as a Service',
  sms: 'Bulk SMS',
};

// Palette for sub-category dots (deterministic hash → color)
const SUB_CAT_PALETTE = [
  '#2d3a8c','#d97b2e','#1f7a4d','#6b4a8a','#3a6b8a','#a8553a',
  '#c0392b','#16a085','#8e44ad','#2980b9','#27ae60','#b8860b',
  '#5d6d7e','#1abc9c','#e74c3c','#8e6b3e',
];
function strToColor(str) {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0;
  return SUB_CAT_PALETTE[h % SUB_CAT_PALETTE.length];
}

// Shared tooltip rendered via fixed positioning so table overflow:hidden never clips it
function DotTooltip({ label, color, anchorRef }) {
  const [pos, setPos] = React.useState({ x: 0, y: 0 });
  React.useLayoutEffect(() => {
    if (anchorRef.current) {
      const r = anchorRef.current.getBoundingClientRect();
      setPos({ x: r.left + r.width / 2, y: r.top });
    }
  }, []);
  return ReactDOM.createPortal(
    <span style={{
      position: 'fixed',
      left: pos.x, top: pos.y - 6,
      transform: 'translate(-50%, -100%)',
      background: '#1a1a2e', color: '#fff',
      padding: '4px 9px', borderRadius: 5, fontSize: 11.5,
      whiteSpace: 'nowrap', zIndex: 99999, pointerEvents: 'none',
      boxShadow: '0 3px 10px rgba(0,0,0,0.28)',
      display: 'inline-flex', alignItems: 'center', gap: 5,
    }}>
      <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, flexShrink: 0 }} />
      {label}
      <span style={{
        position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
        width: 0, height: 0,
        borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
        borderTop: '5px solid #1a1a2e',
      }} />
    </span>,
    document.body
  );
}

// Generic dot with hover tooltip (label + color) — used for sub-category dots
function ColorDot({ label, color, size = 7 }) {
  const [show, setShow] = React.useState(false);
  const ref = React.useRef(null);
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}
      onMouseEnter={() => setShow(true)}
      onMouseLeave={() => setShow(false)}>
      <span style={{ width: size, height: size, borderRadius: '50%', background: color,
        display: 'block', cursor: 'default', flexShrink: 0 }} />
      {show && <DotTooltip label={label} color={color} anchorRef={ref} />}
    </span>
  );
}

// Dot with hover tooltip showing group name + color swatch
function GroupDot({ groupId, size = 7 }) {
  const [show, setShow] = React.useState(false);
  const ref = React.useRef(null);
  const color = GROUP_COLORS[groupId] || 'var(--ink-3)';
  const name  = GROUP_NAMES[groupId]  || groupId;
  return (
    <span ref={ref} style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}
      onMouseEnter={() => setShow(true)}
      onMouseLeave={() => setShow(false)}>
      <span style={{ width: size, height: size, borderRadius: '50%', background: color,
        display: 'block', cursor: 'default', flexShrink: 0 }} />
      {show && <DotTooltip label={name} color={color} anchorRef={ref} />}
    </span>
  );
}

// ---------- Toast system (DOM-based, callable from anywhere) ----------
function ensureToastHost() {
  let host = document.getElementById('__toast_host');
  if (!host) {
    host = document.createElement('div');
    host.id = '__toast_host';
    Object.assign(host.style, {
      position: 'fixed', bottom: '24px', right: '24px', zIndex: 9999,
      display: 'flex', flexDirection: 'column', gap: '8px',
      pointerEvents: 'none',
    });
    document.body.appendChild(host);
    const style = document.createElement('style');
    style.textContent = `
      @keyframes __toastIn { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
      @keyframes __toastOut { to { transform: translateY(-4px); opacity: 0; } }
    `;
    document.head.appendChild(style);
  }
  return host;
}
function showToast(message, opts = {}) {
  const host = ensureToastHost();
  const { variant = 'info', detail, duration = 3200 } = opts;
  const colors = {
    success: { bg: '#1f7a4d', dot: '#7ad6a3' },
    error:   { bg: '#b8492f', dot: '#f5b9a8' },
    info:    { bg: '#1a1d24', dot: '#d97b2e' },
  };
  const c = colors[variant] || colors.info;
  const toast = document.createElement('div');
  Object.assign(toast.style, {
    background: c.bg, color: '#fff',
    padding: '12px 16px', borderRadius: '4px',
    fontFamily: 'Kanit, sans-serif', fontSize: '12.5px',
    minWidth: '240px', maxWidth: '380px',
    boxShadow: '0 8px 24px rgba(0,0,0,0.18)',
    pointerEvents: 'auto', cursor: 'pointer',
    animation: '__toastIn 180ms ease-out',
    display: 'flex', gap: '10px', alignItems: 'flex-start',
  });
  toast.innerHTML = `
    <span style="width:8px;height:8px;border-radius:50%;background:${c.dot};margin-top:6px;flex-shrink:0"></span>
    <div style="flex:1;line-height:1.4">
      <div style="font-weight:500">${message}</div>
      ${detail ? `<div style="font-size:11px;color:rgba(255,255,255,0.7);margin-top:2px">${detail}</div>` : ''}
    </div>
  `;
  toast.onclick = () => { toast.style.animation = '__toastOut 160ms forwards'; setTimeout(() => toast.remove(), 160); };
  host.appendChild(toast);
  setTimeout(() => {
    if (toast.parentNode) {
      toast.style.animation = '__toastOut 200ms forwards';
      setTimeout(() => toast.remove(), 200);
    }
  }, duration);
}

// ---------- Dropdown menu ----------
const Dropdown = ({ trigger, items, align = 'left' }) => {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open]);
  return (
    <div ref={ref} style={{ position: 'relative', display: 'inline-flex' }}>
      <div onClick={() => setOpen(o => !o)} style={{ display: 'inline-flex' }}>{trigger}</div>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 6px)',
          [align]: 0,
          background: 'var(--panel)', border: '1px solid var(--line)',
          boxShadow: '0 10px 28px rgba(0,0,0,0.10)',
          minWidth: 200, padding: 4, zIndex: 100, borderRadius: 4,
        }}>
          {items.map((it, i) => it === '---' ? (
            <div key={i} style={{ height: 1, background: 'var(--line-2)', margin: '4px 0' }} />
          ) : (
            <div key={i} onClick={() => { it.onClick && it.onClick(); setOpen(false); }} style={{
              padding: '8px 12px', fontSize: 12.5, cursor: 'pointer', borderRadius: 2,
              display: 'flex', alignItems: 'center', gap: 10,
              color: it.danger ? 'var(--negative)' : 'var(--ink)',
            }}
              onMouseEnter={e => e.currentTarget.style.background = '#fbfaf6'}
              onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
              {it.icon && <Icon name={it.icon} size={12} />}
              <span style={{ flex: 1 }}>{it.label}</span>
              {it.hint && <span style={{ fontSize: 10, color: 'var(--ink-3)', fontFamily: 'IBM Plex Mono' }}>{it.hint}</span>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

// ---------- Modal ----------
const Modal = ({ open, title, subtitle, onClose, children, footer, width = 480 }) => {
  if (!open) return null;
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(20,22,28,0.45)',
      zIndex: 200, display: 'grid', placeItems: 'center',
      animation: '__toastIn 140ms ease-out',
    }}>
      <div onClick={e => e.stopPropagation()} style={{
        background: 'var(--panel)', width, maxWidth: '92vw', maxHeight: '88vh',
        display: 'flex', flexDirection: 'column',
        boxShadow: '0 24px 64px rgba(0,0,0,0.22)', borderRadius: 4,
      }}>
        <div style={{ padding: '20px 24px', borderBottom: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16 }}>
          <div>
            <h2 className="serif" style={{ fontSize: 18, fontWeight: 500, letterSpacing: '-0.01em' }}>{title}</h2>
            {subtitle && <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 4 }}>{subtitle}</div>}
          </div>
          <button onClick={onClose} style={{ color: 'var(--ink-3)' }}><Icon name="close" size={14} /></button>
        </div>
        <div style={{ padding: '20px 24px', overflowY: 'auto', flex: 1 }}>{children}</div>
        {footer && <div style={{ padding: '14px 24px', borderTop: '1px solid var(--line)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>{footer}</div>}
      </div>
    </div>
  );
};

// ---------- Group Multi-Year Bar Chart ----------
const YEAR_PALETTE = ['#c8cee8', '#8896cc', '#2d3a8c']; // light → dark for year[0] → year[2]

const GroupBarChart = ({ data, years, onDrill }) => {
  const containerRef = useRef(null);
  const [width, setWidth] = useState(800);
  const [tooltip, setTooltip] = useState(null);   // { x, y, group, yi }
  const [metric, setMetric] = useState('revenue'); // 'revenue' | 'customers'

  useEffect(() => {
    if (!containerRef.current) return;
    const obs = new ResizeObserver(() => { if (containerRef.current) setWidth(containerRef.current.clientWidth || 800); });
    obs.observe(containerRef.current);
    setWidth(containerRef.current.clientWidth || 800);
    return () => obs.disconnect();
  }, []);

  if (!data || data.length === 0) {
    return <div style={{ height: 200, display: 'grid', placeItems: 'center', color: 'var(--ink-3)', fontSize: 12 }}>กำลังโหลด…</div>;
  }

  const padL = 52, padR = 16, padT = 36, padB = 72;
  const H = 300;
  const innerW = width - padL - padR;
  const innerH = H - padT - padB;

  const nGroups = data.length;
  const nYears  = years.length || 3;
  const groupW  = innerW / nGroups;
  const barGap  = 3;
  const groupGap = Math.max(6, groupW * 0.1); // 10% of groupW as inter-group space, min 6px
  const barW    = Math.max(8, Math.min(52, (groupW - groupGap) / nYears - barGap));

  // Values
  const vals = data.flatMap(g => years.map(y => metric === 'revenue' ? (g.revenue?.[y] || 0) : (g.customers?.[y] || 0)));
  const targetVals = metric === 'revenue' ? data.map(g => g.target || 0) : [];
  const maxV = Math.max(...vals, ...targetVals, 1);
  const niceMax = (() => {
    const mag = Math.pow(10, Math.floor(Math.log10(maxV)));
    const steps = [1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10];
    for (const s of steps) if (s * mag >= maxV) return s * mag;
    return 10 * mag;
  })();

  const yTicks = [0, 0.25, 0.5, 0.75, 1].map(t => niceMax * t);
  const toY = v => padT + innerH - (Math.max(0, v) / niceMax) * innerH;

  const fmtY = v => {
    if (metric === 'revenue') return v >= 1000 ? `฿${(v / 1000).toFixed(1)}B` : `฿${v.toFixed(0)}M`;
    return v >= 1000 ? `${(v / 1000).toFixed(1)}K` : String(Math.round(v));
  };

  const curY = years[years.length - 1];

  return (
    <div>
      {/* legend + toggle */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
        <div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
          {years.map((y, i) => (
            <div key={y} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
              <div style={{ width: 10, height: 10, background: YEAR_PALETTE[i], borderRadius: 2 }} />
              <span style={{ color: 'var(--ink-2)' }}>{i === years.length - 1 ? `${y} YTD` : String(y)}</span>
            </div>
          ))}
          <div style={{ width: 20, height: 1, background: '#d97b2e', borderTop: '2px dashed #d97b2e' }} />
          <span style={{ fontSize: 11, color: 'var(--ink-3)' }}>Target {curY}</span>
        </div>
        <div style={{ display: 'inline-flex', background: '#ece9df', padding: 2, borderRadius: 3, fontSize: 11 }}>
          {[['revenue', 'Revenue'], ['customers', 'Customers']].map(([k, lbl]) => (
            <button key={k} onClick={() => setMetric(k)} style={{
              padding: '3px 9px', background: metric === k ? 'var(--panel)' : 'transparent',
              color: metric === k ? 'var(--ink)' : 'var(--ink-3)',
              fontWeight: metric === k ? 500 : 400, borderRadius: 2,
            }}>{lbl}</button>
          ))}
        </div>
      </div>

      <div ref={containerRef} style={{ position: 'relative' }}>
        <svg width={width} height={H} style={{ display: 'block', overflow: 'visible' }}>
          {/* grid + y-axis labels */}
          {yTicks.map((t, i) => {
            const y = toY(t);
            return (
              <g key={i}>
                <line x1={padL} y1={y} x2={width - padR} y2={y}
                  stroke={i === 0 ? '#d6d3cc' : '#ece9df'} strokeDasharray={i === 0 ? '' : '3 4'} />
                <text x={padL - 6} y={y + 3.5} textAnchor="end" fontSize="10"
                  fontFamily="IBM Plex Mono" fill="#8b8f99">{fmtY(t)}</text>
              </g>
            );
          })}

          {/* bars */}
          {data.map((g, gi) => {
            const cx     = padL + gi * groupW + groupW / 2;
            const totalBarW = (barW + barGap) * nYears - barGap;
            const startX = cx - totalBarW / 2;

            const yoyPct = metric === 'revenue' ? g.yoy : null;
            const cagrPct = g.cagr;

            return (
              <g key={g.id}>
                {/* bars for each year */}
                {years.map((y, yi) => {
                  const v  = metric === 'revenue' ? (g.revenue?.[y] || 0) : (g.customers?.[y] || 0);
                  const bx = startX + yi * (barW + barGap);
                  const by = toY(v);
                  const bh = Math.max(1, padT + innerH - by);
                  const isActive = yi === years.length - 1;
                  const color = isActive ? (g.color || YEAR_PALETTE[yi]) : YEAR_PALETTE[yi];
                  return (
                    <g key={y}
                      onMouseEnter={e => setTooltip({ x: bx + barW / 2, y: by - 8, group: g, yi, y })}
                      onMouseLeave={() => setTooltip(null)}
                      onClick={() => onDrill && onDrill(g.id)}
                      style={{ cursor: onDrill ? 'pointer' : 'default' }}>
                      <rect x={bx} y={by} width={barW} height={bh}
                        fill={color} rx={2}
                        opacity={tooltip && tooltip.group.id === g.id && tooltip.y === y ? 0.85 : 1} />
                      {/* value label on top of bar (only if bar tall enough) */}
                      {bh > 22 && (
                        <text x={bx + barW / 2} y={by - 3} textAnchor="middle" fontSize="8.5"
                          fontFamily="IBM Plex Mono" fill={isActive ? g.color || '#2d3a8c' : '#8b8f99'} fontWeight={isActive ? '600' : '400'}>
                          {metric === 'revenue'
                            ? v >= 1000 ? `${(v/1000).toFixed(1)}B` : `${v.toFixed(0)}M`
                            : v >= 1000 ? `${(v/1000).toFixed(1)}K` : String(Math.round(v))}
                        </text>
                      )}
                    </g>
                  );
                })}

                {/* YoY badge above current-year bar */}
                {yoyPct !== null && yoyPct !== undefined && (() => {
                  const curV = metric === 'revenue' ? (g.revenue?.[curY] || 0) : (g.customers?.[curY] || 0);
                  const bx   = startX + (years.length - 1) * (barW + barGap);
                  const by   = toY(curV) - 22;
                  const pos  = yoyPct >= 0;
                  const badgeW = barW + 4;
                  return (
                    <g>
                      <rect x={bx - 2} y={by - 11} width={badgeW} height={12} rx={2}
                        fill={pos ? '#e3f1ea' : '#fce8e4'} />
                      <text x={bx + barW / 2} y={by - 1} textAnchor="middle" fontSize="8.5"
                        fontFamily="IBM Plex Mono" fill={pos ? '#1f7a4d' : '#b8492f'} fontWeight="700">
                        {pos ? '+' : ''}{yoyPct.toFixed(1)}%
                      </text>
                    </g>
                  );
                })()}

                {/* Target dashed line for current year (revenue only) */}
                {metric === 'revenue' && g.target && (() => {
                  const ty = toY(g.target);
                  return (
                    <line x1={startX - 4} y1={ty} x2={startX + (barW + barGap) * 3 - barGap + 4} y2={ty}
                      stroke="#d97b2e" strokeWidth={1.5} strokeDasharray="3 2" />
                  );
                })()}

                {/* CAGR chip below group name */}
                {cagrPct !== null && cagrPct !== undefined && (
                  <text x={cx} y={H - padB + 42} textAnchor="middle" fontSize="9"
                    fontFamily="IBM Plex Mono"
                    fill={cagrPct >= 0 ? '#1f7a4d' : '#b8492f'}>
                    CAGR {cagrPct >= 0 ? '+' : ''}{cagrPct.toFixed(1)}%
                  </text>
                )}

                {/* Group name label */}
                {(() => {
                  const words = g.name.split(' ');
                  const lines = [];
                  let cur = '';
                  words.forEach(w => {
                    if ((cur + ' ' + w).trim().length > 12 && cur) { lines.push(cur); cur = w; }
                    else cur = (cur + ' ' + w).trim();
                  });
                  if (cur) lines.push(cur);
                  return lines.map((ln, li) => (
                    <text key={li} x={cx} y={H - padB + 10 + li * 12} textAnchor="middle"
                      fontSize="10.5" fill="var(--ink-2)" fontFamily="Kanit, sans-serif">
                      {ln}
                    </text>
                  ));
                })()}
              </g>
            );
          })}

          {/* Zero axis */}
          <line x1={padL} y1={toY(0)} x2={width - padR} y2={toY(0)} stroke="#d6d3cc" />
        </svg>

        {/* Hover tooltip */}
        {tooltip && (
          <div style={{
            position: 'absolute',
            left: Math.min(tooltip.x + 8, width - 160),
            top: Math.max(4, tooltip.y - 60),
            background: 'var(--ink)', color: '#fff',
            padding: '8px 12px', borderRadius: 4, fontSize: 11,
            pointerEvents: 'none', whiteSpace: 'nowrap',
            boxShadow: '0 4px 12px rgba(0,0,0,0.18)',
          }}>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>{tooltip.group.name}</div>
            <div style={{ opacity: 0.8 }}>
              {tooltip.y} {tooltip.yi === years.length - 1 ? '(YTD)' : '(Full year)'}
            </div>
            <div style={{ fontFamily: 'IBM Plex Mono', fontSize: 12, marginTop: 2 }}>
              {metric === 'revenue'
                ? (() => { const v = tooltip.group.revenue?.[tooltip.y] || 0; return v >= 1000 ? `฿${(v/1000).toFixed(2)}B` : `฿${v.toFixed(1)}M`; })()
                : (() => { const v = tooltip.group.customers?.[tooltip.y] || 0; return v.toLocaleString() + ' customers'; })()}
            </div>
            {metric === 'revenue' && tooltip.group.target && tooltip.yi === years.length - 1 && (
              <div style={{ opacity: 0.7, fontSize: 10, marginTop: 4 }}>
                Target: {tooltip.group.target >= 1000 ? `฿${(tooltip.group.target/1000).toFixed(2)}B` : `฿${tooltip.group.target.toFixed(1)}M`}
                {' · '}{((tooltip.group.revenue?.[tooltip.y] || 0) / tooltip.group.target * 100).toFixed(1)}% achieved
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  );
};

Object.assign(window, { showToast, Dropdown, Modal });

Object.assign(window, {
  fmtTHB, fmtPct, fmtInt, fmtCustomers,
  Icon, Sparkline, Delta, LineChart, StackedBar, KPICard, Segmented,
  GROUP_COLORS, GROUP_NAMES, GroupDot, ColorDot, strToColor, GroupBarChart,
});

