// app/charts.jsx — Ribbon 챗봇 답변용 시각화 프리미티브 (순수 SVG/CSS, 마운트 애니메이션)
// window.RibbonCharts 로 노출. 강조색은 CSS var(--accent) 사용.

const { useState, useEffect, useRef } = React;

function useMounted(delay = 40) {
  const [on, setOn] = useState(false);
  useEffect(() => { const id = setTimeout(() => setOn(true), delay); return () => clearTimeout(id); }, []);
  return on;
}

// **굵게** 인라인 마크다운
function mdInline(text) {
  const parts = String(text).split(/(\*\*[^*]+\*\*)/g);
  return parts.map((p, i) => {
    if (p.startsWith('**') && p.endsWith('**')) {
      return React.createElement('strong', { key: i, style: { fontWeight: 600, color: 'var(--text-heading)' } }, p.slice(2, -2));
    }
    return p;
  });
}

function BlockTitle({ children }) {
  return <div style={{ fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600, color: 'var(--text-muted)',
    marginBottom: 12, letterSpacing: '0.2px' }}>{children}</div>;
}

// ── 경량 마크다운 렌더러 (실 LLM 답변용: 제목·리스트·표·문단·볼드) ──────
// 외부 의존 없이 브라우저 babel 로 컴파일된다. 완전한 CommonMark 는 아니고 LLM 답변에
// 흔한 블록만 처리한다(### 제목 / - · 1. 리스트 / | 표 / 빈 줄 문단, 인라인 **볼드**).
function Markdown({ md }) {
  const lines = String(md || '').replace(/\r/g, '').split('\n');
  const nodes = [];
  let i = 0;
  let key = 0;
  const isTableRow = (s) => /^\s*\|.*\|\s*$/.test(s);
  const isDivider = (s) => /^\s*\|?[\s:|-]+\|?\s*$/.test(s) && s.includes('-');

  while (i < lines.length) {
    const line = lines[i];
    if (!line.trim()) { i++; continue; }

    // 제목
    const h = /^(#{1,4})\s+(.*)$/.exec(line);
    if (h) {
      const lvl = h[1].length;
      const size = lvl <= 1 ? 19 : lvl === 2 ? 16.5 : 14.5;
      nodes.push(
        <div key={key++} style={{ fontFamily: 'var(--font-sans)', fontSize: size, fontWeight: 700,
          color: 'var(--text-heading)', margin: nodes.length ? '6px 0 2px' : '0 0 2px', lineHeight: 1.35 }}>
          {mdInline(h[2])}</div>);
      i++; continue;
    }

    // 표 (헤더 | 구분선 | 본문...)
    if (isTableRow(line) && i + 1 < lines.length && isDivider(lines[i + 1])) {
      const cells = (s) => s.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
      const header = cells(line);
      i += 2;
      const rows = [];
      while (i < lines.length && isTableRow(lines[i])) { rows.push(cells(lines[i])); i++; }
      nodes.push(
        <div key={key++} style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden', margin: '2px 0' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-sans)', fontSize: 13 }}>
            <thead><tr>{header.map((c, j) =>
              <th key={j} style={{ textAlign: 'left', padding: '8px 11px', background: 'var(--surface-soft)',
                color: 'var(--text-muted)', fontWeight: 600, borderBottom: '1px solid var(--border-hairline)' }}>{mdInline(c)}</th>)}</tr></thead>
            <tbody>{rows.map((r, ri) =>
              <tr key={ri}>{r.map((c, j) =>
                <td key={j} style={{ padding: '8px 11px', color: 'var(--text-body)', borderBottom: ri < rows.length - 1 ? '1px solid var(--border-hairline)' : 'none' }}>{mdInline(c)}</td>)}</tr>)}</tbody>
          </table></div>);
      continue;
    }

    // 리스트 (- · * · 1.)
    if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
      const items = [];
      while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i])) {
        items.push(lines[i].replace(/^\s*([-*]|\d+\.)\s+/, '')); i++;
      }
      nodes.push(
        <ul key={key++} style={{ margin: '2px 0', paddingLeft: 18, display: 'flex', flexDirection: 'column', gap: 4 }}>
          {items.map((it, j) =>
            <li key={j} style={{ fontFamily: 'var(--font-sans)', fontSize: 14, lineHeight: 1.55, color: 'var(--text-body)' }}>{mdInline(it)}</li>)}
        </ul>);
      continue;
    }

    // 문단 (연속 비어있지 않은 줄 묶음)
    const para = [];
    while (i < lines.length && lines[i].trim() && !/^(#{1,4})\s|^\s*([-*]|\d+\.)\s/.test(lines[i]) && !isTableRow(lines[i])) {
      para.push(lines[i]); i++;
    }
    nodes.push(
      <p key={key++} style={{ fontFamily: 'var(--font-sans)', fontSize: 14.5, lineHeight: 1.62, color: 'var(--text-body)', margin: 0 }}>
        {mdInline(para.join(' '))}</p>);
  }
  return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{nodes}</div>;
}

// ── KPI 요약 카드 ────────────────────────────────────────────
function KpiCards({ items }) {
  return (
    <div className="rb-kpi" style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.min(items.length, 4)}, 1fr)`, gap: 10 }}>
      {items.map((it, i) => (
        <div key={i} style={{ background: 'var(--surface-page)', border: '1px solid var(--border-hairline)',
          borderRadius: 'var(--radius-md)', padding: '14px 14px 13px' }}>
          <div style={{ fontSize: 11.5, color: 'var(--text-muted)', fontWeight: 500, marginBottom: 8, lineHeight: 1.3 }}>{it.label}</div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
            <span style={{ fontFamily: 'var(--font-display)', fontSize: 27, color: 'var(--text-heading)', letterSpacing: '-0.5px', lineHeight: 1 }}>{it.value}</span>
            {/* delta below */}
            <span style={{ fontSize: 12, color: 'var(--text-muted)', fontWeight: 500 }}>{it.unit}</span>
          </div>
          {it.delta ? (
            <div style={{ marginTop: 7, fontSize: 11.5, fontWeight: 600,
              color: it.dir === 'down' ? 'var(--color-error)' : 'var(--color-success)' }}>
              {it.dir === 'down' ? '▾' : '▴'} {it.delta}
            </div>
          ) : <div style={{ marginTop: 7, height: 14 }} />}
        </div>
      ))}
    </div>
  );
}

// ── 막대 차트 ────────────────────────────────────────────────
function BarChart({ title, data, unit, max }) {
  const m = true;
  const top = max || Math.max(...data.map(d => d.value)) * 1.15;
  return (
    <div>
      {title && <BlockTitle>{title}</BlockTitle>}
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 14, height: 150, padding: '0 2px' }}>
        {data.map((d, i) => {
          const h = (d.value / top) * 100;
          const col = d.color || 'var(--accent)';
          return (
            <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%', justifyContent: 'flex-end', gap: 6 }}>
              <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-strong)',
                opacity: m ? 1 : 0, transition: `opacity .4s ${300 + i * 60}ms ease` }}>
                {d.value}{unit === '%' ? '' : ''}</span>
              <div style={{ width: '100%', maxWidth: 46, borderRadius: '5px 5px 0 0', background: col,
                height: m ? `${h}%` : '0%', transition: `height .6s ${i * 60}ms cubic-bezier(.22,1,.36,1)` }} />
              <span style={{ fontSize: 11.5, color: 'var(--text-muted)', fontWeight: 500 }}>{d.label}</span>
            </div>
          );
        })}
      </div>
      {unit && <div style={{ fontSize: 11, color: 'var(--text-faint)', marginTop: 8, textAlign: 'right' }}>단위: {unit}</div>}
    </div>
  );
}

// ── 라인 차트 (1~2 시리즈, 예측 점선 지원) ───────────────────
function LineChart({ title, series, xLabels, unit }) {
  const m = true;
  const W = 460, H = 150, padX = 14, padY = 16;
  const all = series.flatMap(s => s.points);
  const maxV = Math.max(...all) * 1.12, minV = Math.min(...all, 0) * 0.9;
  const n = xLabels.length;
  const px = i => padX + (i / (n - 1)) * (W - padX * 2);
  const py = v => H - padY - ((v - minV) / (maxV - minV)) * (H - padY * 2);

  return (
    <div>
      {title && <BlockTitle>{title}</BlockTitle>}
      <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
        {[0.25, 0.5, 0.75, 1].map((g, i) => (
          <line key={i} x1={padX} x2={W - padX} y1={padY + g * (H - padY * 2)} y2={padY + g * (H - padY * 2)}
            stroke="var(--border-hairline)" strokeWidth="1" />
        ))}
        {series.map((s, si) => {
          const solidPts = s.points.map((v, i) => [px(i), py(v)]);
          const fcFrom = s.forecastFrom;
          const solidEnd = fcFrom === undefined ? solidPts.length : fcFrom + 1;
          const solid = solidPts.slice(0, solidEnd);
          const dashed = fcFrom === undefined ? [] : solidPts.slice(fcFrom);
          const toPath = pts => pts.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join(' ');
          const areaPts = [...solid];
          const area = `${toPath(areaPts)} L${areaPts[areaPts.length - 1][0]},${H - padY} L${areaPts[0][0]},${H - padY} Z`;
          return (
            <g key={si}>
              {si === 0 && <path d={area} fill={s.color} opacity={m ? 0.08 : 0} style={{ transition: 'opacity .8s ease' }} />}
              <path d={toPath(solid)} fill="none" stroke={s.color} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"
                style={{ strokeDasharray: 1000, strokeDashoffset: m ? 0 : 1000, transition: `stroke-dashoffset 1s ${si * 200}ms ease` }} />
              {dashed.length > 1 && (
                <path d={toPath(dashed)} fill="none" stroke={s.color} strokeWidth="2.4" strokeDasharray="2 5" strokeLinecap="round"
                  opacity={m ? 0.9 : 0} style={{ transition: 'opacity .5s 900ms ease' }} />
              )}
              {solidPts.map((p, i) => (
                <circle key={i} cx={p[0]} cy={p[1]} r="3.2" fill="var(--surface-page)" stroke={s.color} strokeWidth="2"
                  opacity={m ? 1 : 0} style={{ transition: `opacity .3s ${500 + i * 60}ms ease` }} />
              ))}
            </g>
          );
        })}
      </svg>
      <div style={{ display: 'flex', justifyContent: 'space-between', padding: '0 10px', marginTop: 2 }}>
        {xLabels.map((l, i) => <span key={i} style={{ fontSize: 11, color: 'var(--text-muted)', fontWeight: 500 }}>{l}</span>)}
      </div>
      <div style={{ display: 'flex', gap: 16, marginTop: 10, flexWrap: 'wrap' }}>
        {series.map((s, i) => (
          <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, color: 'var(--text-muted)', fontWeight: 500 }}>
            <span style={{ width: 10, height: 10, borderRadius: 3, background: s.color }} />{s.label}
          </span>
        ))}
        {series.some(s => s.forecastFrom !== undefined) && (
          <span style={{ fontSize: 11, color: 'var(--text-faint)' }}>· · · 예측치</span>
        )}
      </div>
    </div>
  );
}

// ── 도넛 차트 ────────────────────────────────────────────────
function Donut({ title, data }) {
  const m = true;
  const total = data.reduce((a, d) => a + d.value, 0);
  const R = 52, C = 64, sw = 18, circ = 2 * Math.PI * R;
  let acc = 0;
  return (
    <div>
      {title && <BlockTitle>{title}</BlockTitle>}
      <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
        <svg width="128" height="128" viewBox="0 0 128 128" style={{ flexShrink: 0 }}>
          <circle cx={C} cy={C} r={R} fill="none" stroke="var(--border-hairline)" strokeWidth={sw} />
          {data.map((d, i) => {
            const frac = d.value / total;
            const dash = frac * circ;
            const off = -acc * circ;
            acc += frac;
            return (
              <circle key={i} cx={C} cy={C} r={R} fill="none" stroke={d.color} strokeWidth={sw}
                strokeDasharray={`${m ? dash : 0} ${circ}`} strokeDashoffset={off}
                transform={`rotate(-90 ${C} ${C})`}
                style={{ transition: `stroke-dasharray .8s ${i * 120}ms cubic-bezier(.22,1,.36,1)` }} />
            );
          })}
          <text x={C} y={C - 4} textAnchor="middle" style={{ fontFamily: 'var(--font-display)', fontSize: 22, fill: 'var(--text-heading)' }}>{total.toLocaleString()}</text>
          <text x={C} y={C + 13} textAnchor="middle" style={{ fontFamily: 'var(--font-sans)', fontSize: 10, fontWeight: 600, fill: 'var(--text-muted)' }}>총 대수</text>
        </svg>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 7, flex: 1 }}>
          {data.map((d, i) => (
            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12 }}>
              <span style={{ width: 10, height: 10, borderRadius: 3, background: d.color, flexShrink: 0 }} />
              <span style={{ color: 'var(--text-body)', fontWeight: 500, flex: 1 }}>{d.label}</span>
              <span style={{ color: 'var(--text-heading)', fontWeight: 600 }}>{Math.round(d.value / total * 100)}%</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── 데이터 테이블 ────────────────────────────────────────────
const TAG_TONE = {
  ok: { bg: 'rgba(93,184,114,0.14)', fg: '#3f8a52' },
  low: { bg: 'rgba(198,69,69,0.12)', fg: '#b03a3a' },
  mid: { bg: 'rgba(212,160,23,0.16)', fg: '#a07c12' },
};
function DataTable({ title, columns, rows }) {
  return (
    <div>
      {title && <BlockTitle>{title}</BlockTitle>}
      <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-sans)' }}>
          <thead>
            <tr style={{ background: 'var(--surface-soft)' }}>
              {columns.map((c, i) => (
                <th key={i} style={{ textAlign: i === 0 ? 'left' : 'left', padding: '9px 12px', fontSize: 11.5,
                  fontWeight: 600, color: 'var(--text-muted)', borderBottom: '1px solid var(--border-hairline)', whiteSpace: 'nowrap' }}>{c}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((r, ri) => (
              <tr key={ri} style={{ borderBottom: ri < rows.length - 1 ? '1px solid var(--border-hairline-soft)' : 'none' }}>
                {r.map((cell, ci) => (
                  <td key={ci} style={{ padding: '9px 12px', fontSize: 12.5, color: 'var(--text-body)', whiteSpace: 'nowrap' }}>
                    {cell && typeof cell === 'object' && cell.tag ? (
                      <span style={{ display: 'inline-block', padding: '2px 9px', borderRadius: 'var(--radius-pill)',
                        fontSize: 11, fontWeight: 600, background: (TAG_TONE[cell.tone] || TAG_TONE.ok).bg, color: (TAG_TONE[cell.tone] || TAG_TONE.ok).fg }}>{cell.tag}</span>
                    ) : (ci === 0 ? <span style={{ fontWeight: 600, color: 'var(--text-heading)' }}>{cell}</span> : cell)}
                  </td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ── 추천 카드 ────────────────────────────────────────────────
const REC_TONE = {
  coral: 'var(--accent)', teal: 'var(--color-teal)', muted: 'var(--text-faint)',
};
function RecommendCards({ items }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
      {items.map((it, i) => {
        const bar = REC_TONE[it.tone] || 'var(--accent)';
        return (
          <div key={i} style={{ display: 'flex', gap: 0, background: 'var(--surface-page)', border: '1px solid var(--border-hairline)',
            borderRadius: 'var(--radius-md)', overflow: 'hidden' }}>
            <div style={{ width: 4, background: bar, flexShrink: 0 }} />
            <div style={{ padding: '13px 15px', flex: 1 }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 4 }}>
                <span style={{ fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 600, color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0 }}>{it.model}</span>
                <span style={{ fontFamily: 'var(--font-display)', fontSize: 18, color: bar, letterSpacing: '-0.3px', whiteSpace: 'nowrap', flexShrink: 0 }}>{it.qty}</span>
              </div>
              <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8, marginBottom: 9 }}>
                <span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '0.6px', textTransform: 'uppercase', whiteSpace: 'nowrap', flexShrink: 0,
                  color: bar, background: it.tone === 'muted' ? 'var(--surface-card)' : `color-mix(in srgb, ${bar} 14%, transparent)`,
                  padding: '3px 8px', borderRadius: 'var(--radius-pill)' }}>{it.priority}</span>
                <span style={{ fontSize: 12, color: 'var(--text-body)', lineHeight: 1.45 }}>{it.reason}</span>
              </div>
              <div style={{ display: 'flex', gap: 22, paddingTop: 9, borderTop: '1px solid var(--border-hairline-soft)' }}>
                {it.metrics.map((mt, mi) => (
                  <div key={mi} style={{ whiteSpace: 'nowrap' }}>
                    <div style={{ fontSize: 10.5, color: 'var(--text-faint)', fontWeight: 500 }}>{mt.label}</div>
                    <div style={{ fontSize: 13, color: 'var(--text-heading)', fontWeight: 600, marginTop: 2 }}>{mt.value}</div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── 레이더(육각형) 차트 — N축 정규화(0~1) 다지표 비교 ──────────────
// props: { title?, axes:[라벨...], series:[{label, color, values:[0..1 축별]}], max? }
function Radar({ title, axes, series, unit }) {
  const m = useMounted(60);
  const N = axes.length;
  const W = 300, C = 150, R = 96;
  const ang = (i) => -Math.PI / 2 + (i * 2 * Math.PI) / N;
  const pt = (i, r) => [C + Math.cos(ang(i)) * R * r, C + Math.sin(ang(i)) * R * r];
  const polyPath = (vals) => vals.map((v, i) => { const [x, y] = pt(i, Math.max(0, Math.min(1, v))); return `${i ? 'L' : 'M'}${x.toFixed(1)},${y.toFixed(1)}`; }).join(' ') + ' Z';
  const grid = [0.25, 0.5, 0.75, 1];
  return (
    <div>
      {title && <BlockTitle>{title}</BlockTitle>}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <svg viewBox={`0 0 ${W} ${W === 300 ? 300 : W}`} width="300" height="300" style={{ maxWidth: '100%', height: 'auto' }}>
          {/* 동심 그리드 */}
          {grid.map((g, gi) => (
            <polygon key={gi} points={axes.map((_, i) => pt(i, g).join(',')).join(' ')}
              fill="none" stroke="var(--border-hairline)" strokeWidth="1" />
          ))}
          {/* 축 스포크 + 라벨 */}
          {axes.map((ax, i) => {
            const [ex, ey] = pt(i, 1);
            const [lx, ly] = pt(i, 1.16);
            return (
              <g key={i}>
                <line x1={C} y1={C} x2={ex} y2={ey} stroke="var(--border-hairline)" strokeWidth="1" />
                <text x={lx} y={ly} textAnchor={Math.abs(lx - C) < 8 ? 'middle' : lx > C ? 'start' : 'end'}
                  dominantBaseline="middle" style={{ fontSize: 11, fontWeight: 600, fill: 'var(--text-muted)' }}>{ax}</text>
              </g>
            );
          })}
          {/* 시리즈 폴리곤 */}
          {series.map((s, si) => (
            <path key={si} d={polyPath(s.values)} fill={s.color} fillOpacity={m ? 0.16 : 0} stroke={s.color}
              strokeWidth="2.2" strokeLinejoin="round"
              style={{ transition: `fill-opacity .6s ${si * 150}ms ease`, opacity: m ? 1 : 0 }} />
          ))}
          {series.map((s, si) => s.values.map((v, i) => { const [x, y] = pt(i, Math.max(0, Math.min(1, v)));
            return <circle key={si + '-' + i} cx={x} cy={y} r="2.6" fill="var(--surface-page)" stroke={s.color} strokeWidth="1.8"
              opacity={m ? 1 : 0} style={{ transition: `opacity .3s ${300 + si * 100}ms ease` }} />; }))}
        </svg>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 7 }}>
          {series.map((s, i) => (
            <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontSize: 12, color: 'var(--text-body)', fontWeight: 500 }}>
              <span style={{ width: 11, height: 11, borderRadius: 3, background: s.color }} />{s.label}</span>
          ))}
          {unit && <span style={{ fontSize: 10.5, color: 'var(--text-faint)', marginTop: 2 }}>{unit}</span>}
        </div>
      </div>
    </div>
  );
}

// ── 달력 차트 — 날짜별 값(요금 추천 등)을 캘린더 그리드에 얹는다 ──────────
// props: { title?, unit?, cells:[{ date:'YYYY-MM-DD', value:number, label?:string, sub?:string, tone?:'up'|'down'|'flat' }] }
// 데이터가 있는 주(week)만 렌더(주 시작=일요일). 범위 밖 날짜는 흐리게. 토=파랑·일=빨강(한국 관례).
const CAL_TONE = { up: 'var(--accent)', down: '#5a86c4', flat: 'var(--text-muted)' };
function ymd(d) { // Date(UTC) → 'YYYY-MM-DD'
  return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0') + '-' + String(d.getUTCDate()).padStart(2, '0');
}
function Calendar({ title, unit, cells }) {
  const m = useMounted(60);
  const list = (cells || []).filter(c => /^\d{4}-\d{2}-\d{2}$/.test(c.date)).slice().sort((a, b) => a.date.localeCompare(b.date));
  if (list.length === 0) return null;
  const map = new Map(list.map(c => [c.date, c]));
  const parse = (s) => { const [y, mo, d] = s.split('-').map(Number); return new Date(Date.UTC(y, mo - 1, d)); };
  const first = parse(list[0].date), last = parse(list[list.length - 1].date);
  // 주 경계로 확장(일요일 시작).
  const start = new Date(first); start.setUTCDate(start.getUTCDate() - start.getUTCDay());
  const end = new Date(last); end.setUTCDate(end.getUTCDate() + (6 - end.getUTCDay()));
  const days = [];
  for (let t = new Date(start); t <= end; t.setUTCDate(t.getUTCDate() + 1)) days.push(new Date(t));
  const weeks = [];
  for (let i = 0; i < days.length; i += 7) weeks.push(days.slice(i, i + 7));
  const WD = ['일', '월', '화', '수', '목', '금', '토'];
  return (
    <div>
      {title && <BlockTitle>{title}{unit ? <span style={{ fontWeight: 400, color: 'var(--text-faint)', fontSize: 12 }}> · 단위 {unit}</span> : null}</BlockTitle>}
      <div style={{ border: '1px solid var(--border-hairline)', borderRadius: 'var(--radius-md)', overflow: 'hidden', background: 'var(--surface-page)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)' }}>
          {WD.map((w, i) => (
            <div key={w} style={{ textAlign: 'center', padding: '7px 0', fontSize: 11, fontWeight: 600,
              color: i === 0 ? '#c0504d' : i === 6 ? '#5a86c4' : 'var(--text-muted)', background: 'var(--surface-soft)',
              borderBottom: '1px solid var(--border-hairline)' }}>{w}</div>
          ))}
          {weeks.map((wk, wi) => wk.map((d, di) => {
            const key = ymd(d);
            const c = map.get(key);
            const dow = d.getUTCDay();
            const dayNum = d.getUTCDate();
            const isFirst = dayNum === 1;
            const tone = c ? (CAL_TONE[c.tone] || CAL_TONE.flat) : null;
            return (
              <div key={key} style={{ minHeight: 62, padding: '6px 7px', borderRight: di < 6 ? '1px solid var(--border-hairline-soft)' : 'none',
                borderBottom: wi < weeks.length - 1 ? '1px solid var(--border-hairline-soft)' : 'none',
                background: c ? 'var(--surface-page)' : 'var(--surface-soft)', opacity: m ? 1 : 0, transition: `opacity .3s ${(wi * 7 + di) * 12}ms` }}>
                <div style={{ fontSize: 10.5, fontWeight: 600, marginBottom: 3,
                  color: !c ? 'var(--text-faint)' : dow === 0 ? '#c0504d' : dow === 6 ? '#5a86c4' : 'var(--text-muted)' }}>
                  {isFirst ? `${d.getUTCMonth() + 1}/${dayNum}` : dayNum}
                </div>
                {c && (
                  <div>
                    <div style={{ fontFamily: 'var(--font-display)', fontSize: 15, letterSpacing: '-0.3px', color: 'var(--text-heading)', lineHeight: 1.1 }}>
                      {c.label != null ? c.label : (c.value != null ? Number(c.value).toLocaleString('en-US') : '')}
                    </div>
                    {c.sub != null && c.sub !== '' &&
                      <div style={{ fontSize: 10.5, fontWeight: 600, color: tone, marginTop: 2 }}>{c.sub}</div>}
                  </div>
                )}
              </div>
            );
          }))}
        </div>
      </div>
    </div>
  );
}

// ── 블록 렌더러 ──────────────────────────────────────────────
function Block({ block }) {
  switch (block.type) {
    case 'text':
      return <p style={{ fontFamily: 'var(--font-sans)', fontSize: 14.5, lineHeight: 1.62, color: 'var(--text-body)', margin: 0 }}>{mdInline(block.md)}</p>;
    case 'markdown':
      return <Markdown md={block.md} />;
    case 'kpi': return <KpiCards items={block.items} />;
    case 'bar': return <BarChart {...block} />;
    case 'line': return <LineChart {...block} />;
    case 'donut': return <Donut {...block} />;
    case 'radar': return <Radar {...block} />;
    case 'table': return <DataTable {...block} />;
    case 'calendar': return <Calendar {...block} />;
    case 'recommend': return <RecommendCards items={block.items} />;
    case 'split':
      return (
        <div className="rb-split" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, alignItems: 'start' }}>
          <Block block={block.left} /><Block block={block.right} />
        </div>
      );
    default: return null;
  }
}

window.RibbonCharts = { Block, KpiCards, BarChart, LineChart, Donut, DataTable, Calendar, RecommendCards, mdInline, Markdown };
