// Overview view — live aggregates from sales.sales_core
// Charts: MAKE GROUP donut | Top-10 bar | Category share list | Monthly trend line
// Period selector (top): 전체 / 2024 / 2025 / 2026 / 직접 설정 — drives KPI cards + all charts.

function OverviewView({ lang, perms }) {
  const [loading, setLoading] = React.useState(true);
  const [allRows, setAllRows] = React.useState([]);
  const [kpiTargets, setKpiTargets] = React.useState(null);  // raw kpi_targets rows for current year
  const [err, setErr]         = React.useState(null);

  // Period selector — shared across KPIs + charts
  const [period, setPeriod]     = React.useState(String(new Date().getFullYear()));  // 기본=올해. 'all' | 연도 | 'custom'
  const [dateFrom, setDateFrom] = React.useState('');
  const [dateTo, setDateTo]     = React.useState('');

  // Monthly-trend window — independent of global period
  const [trendWindow, setTrendWindow] = React.useState(12);  // 6 | 12 | 24 | 0(all)

  // Canvas refs
  const donutRef    = React.useRef(null);
  const barTopRef   = React.useRef(null);
  const barCountryRef = React.useRef(null);
  const lineRef     = React.useRef(null);
  const chartsRef   = React.useRef({});

  // 차트별 매출액(krw)/수량(qty) 토글 — 세션 한정(새로고침 시 krw로 리셋).
  const [chartMetric, setChartMetric] = React.useState({
    makeGroup:'krw', makeModel:'krw', country:'krw', category:'krw', trend:'krw',
  });
  const setMetric = (key, val) => setChartMetric(prev => ({ ...prev, [key]: val }));

  const t = lang === 'en' ? T_EN : T_KO;
  const ovCurrentYear = new Date().getFullYear();

  React.useEffect(() => {
    (async () => {
      try {
        setLoading(true);
        const rows = await SALES_DB.fetchAllSalesData();
        setAllRows(rows);
        // KPI targets for current year — best-effort, don't block on failure.
        try {
          const targets = await SALES_DB.fetchKpiTargets(ovCurrentYear);
          setKpiTargets(targets || []);
        } catch (_) {
          setKpiTargets([]);
        }
      } catch (e) {
        setErr(e?.message || String(e));
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  // Filter rows by selected period (used for KPI cards + non-trend charts).
  const periodRows = React.useMemo(() => {
    return filterByPeriod(allRows, period, dateFrom, dateTo);
  }, [allRows, period, dateFrom, dateTo]);

  // Fix 3 — current-month FX missing detection. April 2026 USD→KRW from BOK
  // ECOS publishes early May; until then any US/ROW rows for the current
  // month carry fx_rate=null and sales_amount_krw can't be computed. Surface
  // this as an info notice so the small "이번 달 매출" number isn't read as
  // a real drop.
  const fxNotice = React.useMemo(() => {
    if (!allRows.length) return null;
    const now = new Date();
    const yr = now.getFullYear();
    const mo = now.getMonth() + 1;
    const thisYM = `${yr}-${String(mo).padStart(2,'0')}`;
    let usRowCount = 0, fxMissingCount = 0;
    for (const r of allRows) {
      if ((r.order_date || '').slice(0, 7) !== thisYM) continue;
      const ch = r.channel_group;
      if (ch !== 'US' && ch !== 'ROW') continue;
      usRowCount++;
      const fx = r.fx_rate;
      if (fx == null || fx === 0) fxMissingCount++;
    }
    // Only fire when there ARE current-month US/ROW rows AND a meaningful
    // share are missing FX. Threshold: ≥30% missing OR all missing.
    if (usRowCount === 0) return null;
    if (fxMissingCount === 0) return null;
    if (fxMissingCount / usRowCount < 0.3) return null;
    return { month: mo, missingCount: fxMissingCount, totalCount: usRowCount };
  }, [allRows]);

  // Stats keyed off the period-filtered rows. KPIs and the 3 non-trend charts
  // come from this. The monthly trend is derived from `allRows` separately so
  // its own window-dropdown can override the global period.
  const stats = React.useMemo(() => {
    if (allRows.length === 0) return null;
    return computeStats(periodRows, allRows, { period, dateFrom, dateTo, lang });
  }, [allRows, periodRows, period, dateFrom, dateTo, lang]);

  // Trend rows: respect the global period when it's a year/custom; for "all"
  // fall back to the trendWindow dropdown (default 12 months rolling).
  const trendData = React.useMemo(() => {
    if (!allRows.length) return null;
    let rows;
    if (period === 'all') {
      // Rolling window measured from latest order_date in dataset.
      if (trendWindow === 0) rows = allRows;
      else rows = filterByRollingMonths(allRows, trendWindow);
    } else {
      rows = periodRows;
    }
    const base = buildMonthlyTrend(rows);  // { months, krw:{US,ROW,KR}, qty:{...} }
    // Prior-year overlay: for each visible YYYY-MM, sum all-channel값을
    // (year-1, same month) 기준으로 — krw/qty 둘 다 산출(차트 토글용).
    const priorByYm = buildPriorYearTotals(allRows);
    const priorAt = (ym, metric, scale) => {
      const [y, m] = ym.split('-').map(Number);
      const prevYm = `${y - 1}-${String(m).padStart(2, '0')}`;
      const v = (priorByYm[prevYm] && priorByYm[prevYm][metric]) || 0;
      return scale ? Math.round(v / 1e7) / 10 : v;  // krw는 억, qty는 원자료
    };
    return {
      ...base,
      priorYearTotal: {
        krw: base.months.map(ym => priorAt(ym, 'krw', true)),
        qty: base.months.map(ym => priorAt(ym, 'qty', false)),
      },
    };
  }, [allRows, periodRows, period, trendWindow]);

  // 차트는 각각 독립 effect — 한 차트의 metric 토글이 다른 차트를 다시 그리지
  // 않도록 분리(도넛/막대/막대 + 라인 = 4개 effect). deps에 period·kpiTargets가
  // 있는 이유: TargetProgress/OvInsights가 비동기 mount하며 일으키는 layout
  // shift가 canvas ref를 remount시킬 수 있어 그때 재생성이 필요. rAF 1프레임
  // 지연 + ref 없으면 다음 프레임 재시도(canvas mid-mount 대비).

  // 1. MAKE GROUP donut
  React.useEffect(() => {
    if (!stats) return;
    let cancelled = false, chart = null;
    const build = () => {
      if (cancelled) return;
      if (!donutRef.current) { requestAnimationFrame(build); return; }
      const prev = chartsRef.current[donutRef.current.id];
      if (prev) { try { prev.destroy(); } catch (_) {} }
      const fg = cssVar('--fg');
      const palette = ['#007AFF','#34C759','#FF9500','#FF2D55','#AF52DE','#5AC8FA','#FFCC00','#FF6B6B','#30D158','#FF375F'];
      const dCfg  = ovMetricCfg(chartMetric.makeGroup, t);
      const dData = stats.makeGroups[chartMetric.makeGroup];
      chart = new Chart(donutRef.current, {
        type: 'doughnut',
        data: {
          labels: dData.map(([k]) => k),
          datasets: [{ data: dData.map(([,v]) => v), backgroundColor: palette, borderWidth: 0 }],
        },
        options: {
          responsive: true, maintainAspectRatio: false, cutout: '64%',
          plugins: {
            legend: { position: 'bottom', labels: { color: fg, font: { size: 11 }, boxWidth: 12, padding: 8 } },
            tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${dCfg.fmt(ctx.raw)}` } },
          },
        },
      });
      chartsRef.current[donutRef.current.id] = chart;
    };
    const raf = requestAnimationFrame(build);
    return () => { cancelled = true; cancelAnimationFrame(raf); if (chart) { try { chart.destroy(); } catch (_) {} } };
  }, [stats, lang, period, kpiTargets, chartMetric.makeGroup]);

  // 2. Top-10 MAKE × Model bar — Chart.js는 가로 막대의 FIRST 라벨을 최상단에
  //    그리므로 이미 내림차순인 배열을 그대로 전달(reverse 없음).
  React.useEffect(() => {
    if (!stats) return;
    let cancelled = false, chart = null;
    const build = () => {
      if (cancelled) return;
      if (!barTopRef.current) { requestAnimationFrame(build); return; }
      const prev = chartsRef.current[barTopRef.current.id];
      if (prev) { try { prev.destroy(); } catch (_) {} }
      const fg = cssVar('--fg'), fgMuted = cssVar('--fg-muted');
      const accent = cssVar('--accent'), gridC = 'rgba(128,128,128,0.12)';
      const mCfg  = ovMetricCfg(chartMetric.makeModel, t);
      const mData = stats.makeModelTop[chartMetric.makeModel];
      chart = new Chart(barTopRef.current, {
        type: 'bar',
        data: {
          labels: mData.map(([k]) => k),
          datasets: [{
            label: mCfg.label,
            data: mData.map(([,v]) => chartMetric.makeModel === 'krw' ? Math.round(v / 1e8 * 10) / 10 : v),
            backgroundColor: accent + 'cc', borderRadius: 4,
          }],
        },
        options: {
          responsive: true, maintainAspectRatio: false, indexAxis: 'y',
          plugins: {
            legend: { display: false },
            tooltip: { callbacks: { label: ctx => ' ' + mCfg.fmt(ctx.raw * mCfg.barScale) } },
          },
          scales: {
            x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: mCfg.axisTick } },
            y: { grid: { display: false }, ticks: { color: fg, font: { size: 10.5 } } },
          },
        },
      });
      chartsRef.current[barTopRef.current.id] = chart;
    };
    const raf = requestAnimationFrame(build);
    return () => { cancelled = true; cancelAnimationFrame(raf); if (chart) { try { chart.destroy(); } catch (_) {} } };
  }, [stats, lang, period, kpiTargets, chartMetric.makeModel]);

  // 3. Top-10 Country bar — period-filtered, ROW 채널만. countryTop이 비면
  //    canvas가 렌더되지 않으므로 build 자체를 생략(무한 재시도 방지).
  React.useEffect(() => {
    if (!stats) return;
    const cData = stats.countryTop[chartMetric.country];
    if (!cData || !cData.length) return;  // 빈 데이터 — '—' 표시, 차트 없음
    let cancelled = false, chart = null;
    const build = () => {
      if (cancelled) return;
      if (!barCountryRef.current) { requestAnimationFrame(build); return; }
      const prev = chartsRef.current[barCountryRef.current.id];
      if (prev) { try { prev.destroy(); } catch (_) {} }
      const fg = cssVar('--fg'), fgMuted = cssVar('--fg-muted');
      const gridC = 'rgba(128,128,128,0.12)';
      const cCfg = ovMetricCfg(chartMetric.country, t);
      chart = new Chart(barCountryRef.current, {
        type: 'bar',
        data: {
          labels: cData.map(([k]) => k),
          datasets: [{
            label: cCfg.label,
            data: cData.map(([,v]) => chartMetric.country === 'krw' ? Math.round(v / 1e8 * 10) / 10 : v),
            backgroundColor: '#34C759cc', borderRadius: 4,
          }],
        },
        options: {
          responsive: true, maintainAspectRatio: false, indexAxis: 'y',
          plugins: {
            legend: { display: false },
            tooltip: { callbacks: { label: ctx => ' ' + cCfg.fmt(ctx.raw * cCfg.barScale) } },
          },
          scales: {
            x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: cCfg.axisTick } },
            y: { grid: { display: false }, ticks: { color: fg, font: { size: 10.5 } } },
          },
        },
      });
      chartsRef.current[barCountryRef.current.id] = chart;
    };
    const raf = requestAnimationFrame(build);
    return () => { cancelled = true; cancelAnimationFrame(raf); if (chart) { try { chart.destroy(); } catch (_) {} } };
  }, [stats, lang, period, kpiTargets, chartMetric.country]);

  // Monthly trend chart — separate effect, depends on trendData.
  // Mirror the main chart effect's rAF retry pattern: on first mount the
  // OvChartCard body (position:absolute) lays out AFTER its parent, so
  // lineRef.current can be null/0×0 even after trendData resolves. Without
  // the retry the user sees an empty card until they toggle the period
  // filter (which triggers a re-render). `period` is added to deps for the
  // same reason as the main effect — layout shifts re-fire the build.
  React.useEffect(() => {
    if (!trendData) return;
    let cancelled = false;
    let chart = null;

    const buildLine = () => {
      if (cancelled) return;
      if (!lineRef.current) {
        // Canvas not yet mounted — try next frame.
        requestAnimationFrame(buildLine);
        return;
      }
      const existing = chartsRef.current[lineRef.current.id];
      if (existing) { try { existing.destroy(); } catch (_) {} }

      const fg      = cssVar('--fg');
      const fgMuted = cssVar('--fg-muted');
      const gridC   = 'rgba(128,128,128,0.12)';

      // 매출액/수량 토글 — trendData.krw{...} / trendData.qty{...} 중 선택.
      const lM   = chartMetric.trend;
      const lCfg = ovMetricCfg(lM, t);
      const series = trendData[lM];  // { US, ROW, KR }
      const priorLabel = lang === 'en' ? 'Prior Year Total' : '전년 동기 합계';
      const datasets = [
        { label: 'US',  data: series.US,  borderColor:'#007AFF', backgroundColor:'rgba(0,122,255,0.06)', borderWidth:2, pointRadius:2, tension:0.3, fill:true },
        { label: 'ROW', data: series.ROW, borderColor:'#FF9500', backgroundColor:'rgba(255,149,0,0.06)',  borderWidth:2, pointRadius:2, tension:0.3, fill:true },
        { label: 'KR',  data: series.KR,  borderColor:'#34C759', backgroundColor:'rgba(52,199,89,0.06)',  borderWidth:2, pointRadius:2, tension:0.3, fill:true },
      ];
      // Prior-year overlay — only render when we actually have prior-year
      // data (avoid a flat-zero dashed line when looking at the earliest
      // year).
      const priorSeries = trendData.priorYearTotal[lM];
      if (priorSeries && priorSeries.some(v => v > 0)) {
        datasets.push({
          label: priorLabel,
          data: priorSeries,
          borderColor: 'rgba(142,142,147,0.7)',
          backgroundColor: 'rgba(142,142,147,0.04)',
          borderDash: [5, 5],
          borderWidth: 1.5,
          pointRadius: 1.5,
          tension: 0.3,
          fill: false,
        });
      }

      chart = new Chart(lineRef.current, {
        type: 'line',
        data: { labels: trendData.months, datasets },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          interaction: { mode: 'index', intersect: false },
          plugins: {
            legend: { labels: { color: fg, font: { size: 11 }, boxWidth: 12 } },
            tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${lCfg.fmt(ctx.raw * lCfg.barScale)}` } },
          },
          scales: {
            x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, maxTicksLimit: 18 } },
            y: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: lCfg.axisTick } },
          },
        },
      });
      chartsRef.current[lineRef.current.id] = chart;
    };

    const raf = requestAnimationFrame(buildLine);
    return () => {
      cancelled = true;
      cancelAnimationFrame(raf);
      if (chart) { try { chart.destroy(); } catch (_) {} }
    };
  }, [trendData, lang, period, chartMetric.trend]);

  if (loading) return <Placeholder msg={t.loading}/>;
  if (err)     return <ErrBox err={err} lang={lang}/>;
  if (!stats || stats.totalRows === 0) return <Placeholder msg={t.empty}/>;

  const yoyColor = stats.yoy == null ? 'var(--fg-muted)'
    : stats.yoy >= 0 ? '#34C759' : '#FF453A';
  const yoySign  = stats.yoy == null ? '' : stats.yoy >= 0 ? '+' : '';

  // The "이번 달 매출" card relabels to "기간 매출" when a period is active.
  const periodActive  = period !== 'all';
  const periodCardLbl = periodActive ? t.periodRev : t.thisMonth;
  const periodCardVal = periodActive ? stats.periodKrw : stats.thisMonthKrw;
  const periodCardTip = periodActive ? t.tipPeriodRev : t.tipThisMonth;
  // YoY label adjusts when a specific year is selected
  const yoyLabel = (period !== 'all' && period !== 'custom')
    ? `${period} vs ${parseInt(period,10)-1}`
    : t.yoy;

  // Build comparison sub-text strings for KPI cards.
  const fmtPctDelta = (pct, delta, label) => {
    if (pct == null) return null;
    const sign = pct >= 0 ? '+' : '';
    const deltaStr = delta != null ? ` (${pct >= 0 ? '+' : '−'}${moneyB(Math.abs(delta))})` : '';
    return `${label} ${sign}${pct.toFixed(1)}%${deltaStr}`;
  };
  const fmtPctDeltaQty = (pct, delta, label) => {
    if (pct == null) return null;
    const sign = pct >= 0 ? '+' : '';
    const deltaStr = delta != null
      ? ` (${pct >= 0 ? '+' : '−'}${Math.abs(delta).toLocaleString()} ${t.units})`
      : '';
    return `${label} ${sign}${pct.toFixed(1)}%${deltaStr}`;
  };
  const colorFor = pct => (pct == null ? 'var(--fg-subtle)' : pct >= 0 ? '#34C759' : '#FF453A');

  // Period-aware comparison context label. 월 표기는 직전 완료월 기준 동적 산출
  // (yoyShortNote) — 하드코딩하면 매달 어긋남.
  // - 전체 (all) / 현재 연도: "(1-N월 기준)"  N = 직전 완료월
  // - 2024/2025 (과거 연도): "(연 매출 기준)"
  // - custom: stats.yoyShortNote (지정 기간)
  const periodContextNote = (() => {
    const yr = new Date().getFullYear();
    // 'all'·custom·현재 연도 모두 YoY가 "올해 1-N월 vs 작년 1-N월"로 동일 →
    // 동일한 동적 yoyShortNote 사용. 과거 연도만 연 매출 기준.
    if (period === 'all')    return stats.yoyShortNote || '';
    if (period === 'custom') return stats.yoyShortNote || '';
    const yearNum = parseInt(period, 10);
    if (yearNum === yr)      return stats.yoyShortNote || ''; // 현재 연도: "(1-N월 기준)"
    return lang === 'en' ? '(full year)' : '(연 매출 기준)';
  })();

  // Total revenue card: vs prior-year (capped at last completed month).
  const totalRevSubBase  = fmtPctDelta(stats.totalRevPriorPct, stats.totalRevPriorDelta, t.vsPriorYTD);
  const totalRevSub      = totalRevSubBase ? `${totalRevSubBase} ${periodContextNote}` : null;
  const totalRevSubColor = colorFor(stats.totalRevPriorPct);
  // Total qty card: vs prior-year qty (same cutoff).
  const totalQtySubBase  = fmtPctDeltaQty(stats.totalQtyPriorPct, stats.totalQtyPriorDelta, t.vsPriorYTD);
  const totalQtySub      = totalQtySubBase ? `${totalQtySubBase} ${periodContextNote}` : null;
  const totalQtySubColor = colorFor(stats.totalQtyPriorPct);
  // Period/this-month card sub-text
  let periodCardSub = null, periodCardSubColor = null;
  if (periodActive) {
    if (stats.periodPriorPct != null) {
      periodCardSub      = fmtPctDelta(stats.periodPriorPct, stats.periodPriorDelta, t.vsPriorPeriod);
      periodCardSubColor = colorFor(stats.periodPriorPct);
    }
  } else {
    if (stats.monthMomPct != null) {
      periodCardSub      = fmtPctDelta(stats.monthMomPct, stats.monthMomDelta, t.vsLastYrSameMonth);
      periodCardSubColor = colorFor(stats.monthMomPct);
    }
  }

  return (
    <div>
      <h2 className="view-head">{t.title}</h2>

      {/* Period selector */}
      <PeriodBar
        period={period} setPeriod={setPeriod}
        dateFrom={dateFrom} setDateFrom={setDateFrom}
        dateTo={dateTo} setDateTo={setDateTo}
        t={t}
      />

      {/* Fix 3 — FX missing notice for current-month US/ROW */}
      {fxNotice && (
        <div className="fx-notice">
          <span aria-hidden="true">ⓘ</span>
          <span>{(t.fxNotice || '').replace('{m}', String(fxNotice.month))}</span>
        </div>
      )}

      {/* KPI cards */}
      <div className="kpi-summary-grid" style={{ gridTemplateColumns:'repeat(4,1fr)', marginBottom:20 }}>
        <KpiCard label={t.totalRev}      value={moneyB(stats.totalKrw)}
                                         subText={totalRevSub} subColor={totalRevSubColor}
                                         tip={t.tipTotalRev}/>
        <KpiCard label={t.totalQty}      value={stats.totalQty.toLocaleString()} sub={t.units}
                                         subText={totalQtySub} subColor={totalQtySubColor}
                                         tip={t.tipTotalQty}/>
        <KpiCard label={periodCardLbl}   value={moneyB(periodCardVal)}
                                         subText={periodCardSub} subColor={periodCardSubColor}
                                         tip={periodCardTip}/>
        <KpiCard label={yoyLabel}        value={stats.yoy == null ? '—' : yoySign + stats.yoy.toFixed(1) + '%'}
                                         valueColor={yoyColor}
                                         subText={stats.yoyComparisonNote}
                                         tip={t.tipYoy}/>
      </div>

      {/* 🎯 목표 진행률 — hidden if no kpi_targets data for current year */}
      <TargetProgress kpiTargets={kpiTargets} allRows={allRows} currentYear={ovCurrentYear} t={t}/>

      {/* 💡 Insights — GOOD / RISK signals (Fix 4 — period-aware) */}
      <OvInsights allRows={allRows} periodRows={periodRows} period={period}
                  kpiTargets={kpiTargets} lang={lang} t={t}/>

      {/* Row 2: donut + top-10 bar
          Grid changed from 1fr/1.5fr to 1fr/1fr — donut now uses bottom legend
          so it fills the full square area; equal columns gives matching mass. */}
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14, marginBottom:14 }}>
        <ChartCard title={t.makeGroup} height={OV_CHART_HEIGHT}
          right={<MetricToggle value={chartMetric.makeGroup} onChange={v => setMetric('makeGroup', v)} lang={lang}/>}>
          <canvas ref={donutRef} id="c-donut" style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </ChartCard>
        <ChartCard title={t.top10Model} height={OV_CHART_HEIGHT}
          right={<MetricToggle value={chartMetric.makeModel} onChange={v => setMetric('makeModel', v)} lang={lang}/>}>
          <canvas ref={barTopRef} id="c-bartop" style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </ChartCard>
      </div>

      {/* Row 3: country top-10 (full width) — TALL because 10 horizontal bars
          need extra vertical room or labels get squashed. */}
      <div style={{ marginBottom:14 }}>
        <ChartCard title={t.top10Country} height={OV_CHART_HEIGHT_TALL}
          right={<MetricToggle value={chartMetric.country} onChange={v => setMetric('country', v)} lang={lang}/>}>
          {stats.countryTop[chartMetric.country] && stats.countryTop[chartMetric.country].length > 0 ? (
            <canvas ref={barCountryRef} id="c-barcountry" style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
          ) : (
            <div style={{ padding:'18px 4px', fontSize:12, color:'var(--fg-muted)' }}>—</div>
          )}
        </ChartCard>
      </div>

      {/* Row 4: category share list + monthly trend
          Keep 1fr/2fr — list is shorter content, trend chart wants the wider
          half. Both share OV_CHART_HEIGHT for visual parity; the list aligns
          to the top of its body via bodyAlign='start' so it doesn't try to
          stretch into the empty space below. */}
      <div style={{ display:'grid', gridTemplateColumns:'1fr 2fr', gap:14 }}>
        <ChartCard title={t.catGroup} height={OV_CHART_HEIGHT} bodyAlign="start"
          right={<MetricToggle value={chartMetric.category} onChange={v => setMetric('category', v)} lang={lang}/>}>
          <CategoryShareList
            groups={stats.catGroups[chartMetric.category]}
            total={chartMetric.category === 'krw' ? stats.totalKrw : stats.totalQty}/>
        </ChartCard>
        <ChartCard
          title={t.monthly}
          height={OV_CHART_HEIGHT}
          right={
            <div style={{ display:'flex', gap:6, alignItems:'center' }}>
              <MetricToggle value={chartMetric.trend} onChange={v => setMetric('trend', v)} lang={lang}/>
              {period === 'all' ? (
                <select
                  value={trendWindow}
                  onChange={e => setTrendWindow(+e.target.value)}
                  className="toolbar-select"
                  title={t.trendWindowTitle}
                >
                  <option value={6}>{t.last6}</option>
                  <option value={12}>{t.last12}</option>
                  <option value={24}>{t.last24}</option>
                  <option value={0}>{t.all}</option>
                </select>
              ) : (
                <span style={{ fontSize:11, color:'var(--fg-subtle)' }}>{t.periodLocked}</span>
              )}
            </div>
          }
        >
          <canvas ref={lineRef} id="c-line" style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </ChartCard>
      </div>

      {/* Data health footer */}
      <div style={{ marginTop:12, fontSize:11, color:'var(--fg-subtle)', textAlign:'right' }}>
        {t.rowCount.replace('{n}', stats.totalRows.toLocaleString())}
      </div>
    </div>
  );
}

// --- Period selector bar -------------------------------------------------
function PeriodBar({ period, setPeriod, dateFrom, setDateFrom, dateTo, setDateTo, t }) {
  // Month-to-date 프리셋 — 이번 달 1일~오늘. 현재 날짜 기준 동적 계산(하드코딩 금지).
  // MTD는 'custom' 기간 + 이번 달 범위가 정확히 채워진 상태로 표현 — 기존 custom
  // 필터·비교 로직을 그대로 재사용. 범위가 일치할 때만 '이번 달' 칩이 활성.
  const _now = new Date();
  const _pad = n => String(n).padStart(2, '0');
  const mtdFrom = `${_now.getFullYear()}-${_pad(_now.getMonth() + 1)}-01`;
  const mtdTo   = `${_now.getFullYear()}-${_pad(_now.getMonth() + 1)}-${_pad(_now.getDate())}`;
  const mtdActive = period === 'custom' && dateFrom === mtdFrom && dateTo === mtdTo;
  const applyMtd = () => { setDateFrom(mtdFrom); setDateTo(mtdTo); setPeriod('custom'); };
  // 연도 칩 — 데이터 시작연도(2024)부터 올해까지 동적 생성(하드코딩 금지).
  const years = [];
  for (let y = 2024; y <= _now.getFullYear(); y++) years.push(String(y));
  return (
    <div className="surface-card period-bar">
      <span className="period-bar__label">{t.period}</span>
      <button
        onClick={() => setPeriod('all')}
        className={'filter-pill' + (period === 'all' ? ' is-active' : '')}
      >{t.all}</button>
      {years.map(y => (
        <button
          key={y}
          onClick={() => setPeriod(y)}
          className={'filter-pill' + (period === y ? ' is-active' : '')}
        >{y}</button>
      ))}
      {/* '이번 달'(MTD)은 '직접 설정' 바로 옆 — 둘 다 날짜범위 프리셋이라 같이 묶음 */}
      <button
        onClick={applyMtd}
        title={t.mtdTitle}
        className={'filter-pill' + (mtdActive ? ' is-active' : '')}
      >{t.mtd}</button>
      <button
        onClick={() => setPeriod('custom')}
        className={'filter-pill' + (period === 'custom' && !mtdActive ? ' is-active' : '')}
      >{t.custom}</button>
      {period === 'custom' && (
        <span style={{ display:'flex', gap:4, alignItems:'center', marginLeft:4 }}>
          <KrDateInput value={dateFrom} onChange={setDateFrom} className="toolbar-input" placeholder="YYYY-MM-DD"/>
          <span style={{ fontSize:12, color:'var(--fg-muted)' }}>~</span>
          <KrDateInput value={dateTo}   onChange={setDateTo}   className="toolbar-input" placeholder="YYYY-MM-DD"/>
        </span>
      )}
    </div>
  );
}

// --- Korean-style date input (YYYY-MM-DD masked text input) --------------
// Native <input type="date"> shows MM/DD/YYYY on en-US browsers — confusing
// for Korean users. This wraps a text input with auto-mask so the visible
// format is always YYYY-MM-DD (ISO order, also Korean reading order).
// Stored value remains YYYY-MM-DD so existing date filter logic is untouched.
window.KrDateInput = function KrDateInput({ value, onChange, style, className, placeholder }) {
  function handle(e) {
    let v = e.target.value.replace(/[^\d]/g, '').slice(0, 8);   // strip everything not digits, max 8
    if (v.length > 6)      v = `${v.slice(0,4)}-${v.slice(4,6)}-${v.slice(6)}`;
    else if (v.length > 4) v = `${v.slice(0,4)}-${v.slice(4)}`;
    else if (v.length > 0) v = v;  // YYYY only
    onChange(v);
  }
  return (
    <input type="text" inputMode="numeric"
      value={value || ''}
      onChange={handle}
      placeholder={placeholder || 'YYYY-MM-DD'}
      maxLength={10}
      style={style}
      className={className}
    />
  );
};
const KrDateInput = window.KrDateInput;

// --- KPI card with info-tooltip ------------------------------------------
// `sub`     — small subtitle directly under value (e.g. "QTY" units label)
// `subText` — comparison context line, optionally colorized via `subColor`
function KpiCard({ label, value, sub, subText, subColor, valueColor, tip }) {
  return (
    <div className="kpi-mini">
      {tip && (
        <span className="info-tip" tabIndex="0" aria-label={tip}
          style={{ position:'absolute', top:10, right:10 }}>
          i
          <span className="info-tip-bubble">{tip}</span>
        </span>
      )}
      <div className="kpi-mini__label kpi-mini__label--padded">
        {label}
      </div>
      <div className="kpi-mini__value" style={valueColor ? { color: valueColor } : undefined}>
        {value}
      </div>
      {sub && <div className="kpi-mini__sub">{sub}</div>}
      {subText && (
        <div className="kpi-mini__subtext" style={subColor ? { color: subColor } : undefined}>
          {subText}
        </div>
      )}
    </div>
  );
}

// --- Category share list (Option B: name + mini-bar + %) -----------------
function CategoryShareList({ groups, total }) {
  if (!groups || !groups.length) {
    return <div style={{ padding:'18px 4px', fontSize:12, color:'var(--fg-muted)' }}>—</div>;
  }
  const palette = ['#007AFF','#34C759','#FF9500','#FF2D55','#AF52DE','#5AC8FA','#FFCC00','#FF6B6B','#30D158','#FF375F'];
  const denom = total || groups.reduce((s, [,v]) => s + v, 0) || 1;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
      {groups.map(([k, v], i) => {
        const pct = v / denom * 100;
        const color = palette[i % palette.length];
        return (
          <div key={k} style={{ display:'flex', alignItems:'center', gap:10 }}>
            <div style={{ width: 100, fontSize:12, color:'var(--fg-2)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}
              title={k}>{k}</div>
            <div style={{ flex:1, height:6, background:'var(--surface-2)', borderRadius:99, overflow:'hidden' }}>
              <div style={{
                width: Math.max(0.5, Math.min(pct, 100)) + '%',
                height:'100%', background: color, borderRadius:99,
                transition:'width .35s ease',
              }}/>
            </div>
            <div style={{ width: 56, fontSize:11.5, color:'var(--fg-muted)', textAlign:'right', fontVariantNumeric:'tabular-nums' }}>
              {pct < 0.1 ? '<0.1%' : pct.toFixed(1) + '%'}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// --- Target progress mini-section ----------------------------------------
// Renders 4 channel cards (Total + US + ROW + KR) with horizontal progress
// bars vs kpi_targets for the current year. Hidden gracefully if no targets
// exist for the year, so non-admin views without configured KPIs stay clean.
function TargetProgress({ kpiTargets, allRows, currentYear, t }) {
  const targetMap = React.useMemo(() => {
    if (!kpiTargets) return null;
    const m = {};
    for (const r of kpiTargets) {
      if (r.metric !== 'revenue') continue;
      m[r.segment] = r.target_value || 0;
    }
    return m;
  }, [kpiTargets]);

  const actualByChannel = React.useMemo(() => {
    const out = { Total: 0, US: 0, ROW: 0, KR: 0 };
    if (!allRows) return out;
    for (const r of allRows) {
      const d = r.order_date || '';
      if (!d) continue;
      if (parseInt(d.slice(0, 4), 10) !== currentYear) continue;
      const krw = r.sales_amount_krw || 0;
      out.Total += krw;
      const ch = r.channel_group;
      if (ch === 'US' || ch === 'ROW' || ch === 'KR') out[ch] += krw;
    }
    return out;
  }, [allRows, currentYear]);

  if (!targetMap) return null;
  // Hide entire section when there are no revenue targets for the year.
  const hasAny = ['Total','US','ROW','KR'].some(s => (targetMap[s] || 0) > 0);
  if (!hasAny) return null;

  const segments = [
    { key: 'Total', label: t.tpTotal },
    { key: 'US',    label: t.tpUS },
    { key: 'ROW',   label: t.tpROW },
    { key: 'KR',    label: t.tpKR },
  ];

  // 월평균 환율 미발표 월이 일일환율로 잠정 계산돼 누적 달성률에 들어가 있으면,
  // 월말 확정 후 변동 가능하다는 caveat을 제목 옆에 표시. 월평균 기준일 때만.
  const _fxStats = (window.SALES_DB && SALES_DB.getFxBasisStats) ? SALES_DB.getFxBasisStats() : null;
  const fxCaveat = (_fxStats && _fxStats.basis === 'monthly' && _fxStats.fallbackRows > 0)
    ? t.tpFxCaveat(_fxStats.fallbackMonths)
    : null;

  return (
    <div className="surface-card" style={{ marginBottom: 14 }}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 12, gap: 10 }}>
        <div style={{ display:'flex', alignItems:'baseline', gap: 8, flexWrap:'wrap' }}>
          <div className="section-eyebrow">{t.tpTitle}</div>
          {fxCaveat && (
            <span style={{ fontSize:10.5, color:'var(--fg-subtle)', fontWeight:500, lineHeight:1.4 }}>
              ⓘ {fxCaveat}
            </span>
          )}
        </div>
        <div style={{ fontSize:11, color:'var(--fg-subtle)', whiteSpace:'nowrap' }}>{currentYear}</div>
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:14 }}>
        {segments.map(seg => (
          <TargetBar key={seg.key}
            label={seg.label}
            actual={actualByChannel[seg.key] || 0}
            target={targetMap[seg.key] || 0}
            t={t}
          />
        ))}
      </div>
    </div>
  );
}

function TargetBar({ label, actual, target, t }) {
  const has = target > 0;
  const pct = has ? actual / target * 100 : null;
  const color = pct == null ? '#8E8E93'
    : pct >= 100 ? '#34C759'
    : pct >= 80  ? '#FF9500'
    : '#FF453A';
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
      <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between' }}>
        <div style={{ fontSize:11.5, fontWeight:600, color:'var(--fg-2)' }}>{label}</div>
        <div style={{ fontSize:11, fontWeight:700, color, fontVariantNumeric:'tabular-nums' }}>
          {pct == null ? '—' : pct.toFixed(1) + '%'}
        </div>
      </div>
      <div className="target-bar">
        <div className="target-bar__fill" style={{
          width: pct == null ? '0%' : Math.min(Math.max(pct, 0), 100) + '%',
          background: color,
        }}/>
      </div>
      <div style={{ fontSize:10.5, color:'var(--fg-muted)', fontVariantNumeric:'tabular-nums' }}>
        {moneyB(actual)} <span style={{ color:'var(--fg-subtle)' }}>/ {has ? moneyB(target) : '—'}</span>
      </div>
    </div>
  );
}

// --- Insights section (GOOD / RISK) --------------------------------------
// Fix 4 — Period-aware insights:
//   period === 'all'    → mode 'all-years-trend' (uses full allRows; rules
//                          focus on multi-year YoY deltas, sustained trend
//                          lines, and category-mix shifts).
//   period === '2024' / '2025' / '2026'
//                       → mode 'single-year' (existing rule set scoped to
//                          that year; YoY uses prior-year baseline).
//   period === 'custom' → mode 'period' (existing rules over filtered range).
function OvInsights({ allRows, periodRows, period, kpiTargets, lang, t }) {
  const insights = React.useMemo(() => {
    if (!window.SALES_INSIGHTS || !allRows || allRows.length === 0) return null;
    let mode = 'period';
    let rowsForRules = allRows;
    let asOfDate = new Date();
    if (period === 'all') {
      mode = 'all-years-trend';
      rowsForRules = allRows;
    } else if (/^\d{4}$/.test(period || '')) {
      mode = 'single-year';
      rowsForRules = periodRows || allRows;
      // Anchor "now" inside the selected year so rules like
      // "this month vs prior 3-mo avg" stay meaningful for past years.
      const selYr = parseInt(period, 10);
      const today = new Date();
      if (selYr < today.getFullYear()) asOfDate = new Date(selYr, 11, 31);
    } else if (period === 'custom') {
      mode = 'period';
      rowsForRules = periodRows || allRows;
    }
    try {
      return SALES_INSIGHTS.generateInsights(rowsForRules, kpiTargets, asOfDate, {
        mode,
        allRows,            // multi-year mode needs the full set
        period,
      });
    } catch (e) { console.warn('[insights]', e); return null; }
  }, [allRows, periodRows, period, kpiTargets]);

  if (!insights || (!insights.good.length && !insights.risk.length)) return null;

  return (
    <div className="surface-card" style={{ marginBottom: 14 }}>
      <div className="section-eyebrow" style={{ marginBottom:12 }}>{t.insightsTitle}</div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14 }}>
        <div>
          <div className="insight-list-title insight-list-title--good">● {t.goodTitle}</div>
          {insights.good.length === 0
            ? <div className="insight-empty">—</div>
            : insights.good.map((ins, i) => <OvInsightCard key={i} ins={ins} lang={lang} theme="good"/>)
          }
        </div>
        <div>
          <div className="insight-list-title insight-list-title--risk">● {t.riskTitle}</div>
          {insights.risk.length === 0
            ? <div className="insight-empty">—</div>
            : insights.risk.map((ins, i) => <OvInsightCard key={i} ins={ins} lang={lang} theme="risk"/>)
          }
        </div>
      </div>
    </div>
  );
}

function OvInsightCard({ ins, lang, theme }) {
  // High-severity cards get a stronger border + a subtle lift via box-shadow
  // so they read as more urgent than the LOW/MEDIUM cards beside them.
  const sev    = ins.severity || 1;
  const isHigh = sev >= 3;
  const bg     = theme === 'good' ? 'rgba(52,199,89,0.08)'  : 'rgba(255,149,0,0.08)';
  const borderColor = theme === 'good'
    ? (isHigh ? 'rgba(52,199,89,0.55)'  : 'rgba(52,199,89,0.25)')
    : (isHigh ? 'rgba(255,149,0,0.55)'  : 'rgba(255,149,0,0.25)');
  const col    = theme === 'good' ? '#34C759' : '#FF9500';
  const title  = lang === 'en' ? (ins.titleEn || ins.title) : ins.title;
  const body   = lang === 'en' ? (ins.bodyEn  || ins.body)  : ins.body;
  const hint   = lang === 'en' ? (ins.hintEn  || ins.hint)  : ins.hint;
  // Default icon if the rule didn't supply one — keeps older callers safe.
  const icon = ins.icon || (theme === 'good' ? '📈' : '⚠️');
  // Severity badge — colored pill in top-right.
  const sevLabel = sev >= 3 ? 'HIGH' : sev === 2 ? 'MED' : 'LOW';
  const sevBg = sev >= 3
    ? (theme === 'good' ? 'rgba(52,199,89,0.18)'  : 'rgba(255,69,58,0.18)')
    : sev === 2
    ? 'rgba(255,149,0,0.15)'
    : 'rgba(142,142,147,0.18)';
  const sevColor = sev >= 3
    ? (theme === 'good' ? '#34C759' : '#FF453A')
    : sev === 2 ? '#FF9500' : '#8E8E93';
  return (
    <div style={{
      position:'relative',
      background: bg,
      border: `${isHigh ? 1.5 : 1}px solid ${borderColor}`,
      borderRadius:8,
      padding:'8px 10px',
      marginBottom:6,
      boxShadow: isHigh ? '0 1px 3px rgba(0,0,0,0.06)' : 'none',
    }}>
      <span style={{
        position:'absolute', top:6, right:8,
        fontSize:9, fontWeight:700, letterSpacing:.04,
        padding:'2px 6px', borderRadius:99,
        background: sevBg, color: sevColor,
      }}>{sevLabel}</span>
      <div style={{ display:'flex', alignItems:'center', gap:6, marginBottom:3, paddingRight:42 }}>
        <span style={{ fontSize:13, lineHeight:1 }} aria-hidden="true">{icon}</span>
        <span style={{ fontSize:12, fontWeight:600, color: col }}>{title}</span>
      </div>
      <div style={{ fontSize:11.5, color:'var(--fg-2)', lineHeight:1.4 }}>{body}</div>
      {hint && (
        <div style={{ fontSize:10.5, color:'var(--fg-subtle)', marginTop:4, fontVariantNumeric:'tabular-nums', lineHeight:1.35 }}>
          {hint}
        </div>
      )}
    </div>
  );
}

// --- Filtering helpers ---------------------------------------------------
function filterByPeriod(rows, period, dateFrom, dateTo) {
  if (!rows || rows.length === 0) return rows;
  if (period === 'all') return rows;
  if (period === 'custom') {
    // Only apply bound when fully formed YYYY-MM-DD (length 10) — avoids
    // wiping all rows during keystroke-by-keystroke partial input like "2026-04-2".
    const validFrom = dateFrom && dateFrom.length === 10;
    const validTo   = dateTo   && dateTo.length   === 10;
    return rows.filter(r => {
      const d = r.order_date || '';
      if (!d) return false;
      if (validFrom && d < dateFrom) return false;
      if (validTo   && d > dateTo)   return false;
      return true;
    });
  }
  // Year value e.g. '2024'
  return rows.filter(r => (r.order_date || '').startsWith(period));
}

// Rolling N months ending at the latest order_date present in `rows`.
function filterByRollingMonths(rows, n) {
  if (!rows.length) return rows;
  let maxYm = '0000-00';
  for (const r of rows) {
    const ym = (r.order_date || '').slice(0,7);
    if (ym > maxYm) maxYm = ym;
  }
  if (maxYm === '0000-00') return rows;
  const [y, m] = maxYm.split('-').map(Number);
  // Find the cutoff = (n-1) months before maxYm
  const cutoff = new Date(y, m - 1 - (n - 1), 1);
  const cutoffYm = `${cutoff.getFullYear()}-${String(cutoff.getMonth()+1).padStart(2,'0')}`;
  return rows.filter(r => (r.order_date || '').slice(0,7) >= cutoffYm);
}

// Sum total revenue (all channels) per YYYY-MM across the full dataset.
// Used for the prior-year overlay on the monthly trend chart.
function buildPriorYearTotals(rows) {
  const out = {};  // ym -> { krw, qty }
  for (const row of rows) {
    const ym = (row.order_date || '').slice(0, 7);
    if (!ym) continue;
    const cell = out[ym] || (out[ym] = { krw: 0, qty: 0 });
    cell.krw += row.sales_amount_krw || 0;
    cell.qty += row.qty || 0;
  }
  return out;
}

// Build US/ROW/KR monthly trend from a row set. Sorted ascending by month.
function buildMonthlyTrend(rows) {
  const monthMap = {};
  for (const row of rows) {
    const date = row.order_date || '';
    if (!date) continue;
    const ym = date.slice(0,7);
    const ch = row.channel_group || 'Other';
    if (!monthMap[ym]) monthMap[ym] = { US:{krw:0,qty:0}, ROW:{krw:0,qty:0}, KR:{krw:0,qty:0} };
    const cell = monthMap[ym][ch] || (monthMap[ym][ch] = { krw:0, qty:0 });
    cell.krw += row.sales_amount_krw || 0;
    cell.qty += row.qty || 0;
  }
  const months = Object.keys(monthMap).sort();
  // krw 시리즈는 억 단위(소수 1자리), qty 시리즈는 원자료 수량.
  const krwSeries = ch => months.map(m => Math.round((monthMap[m][ch]?.krw || 0) / 1e7) / 10);
  const qtySeries = ch => months.map(m => monthMap[m][ch]?.qty || 0);
  return {
    months,
    krw: { US:krwSeries('US'), ROW:krwSeries('ROW'), KR:krwSeries('KR') },
    qty: { US:qtySeries('US'), ROW:qtySeries('ROW'), KR:qtySeries('KR') },
  };
}

// --- Aggregation ---------------------------------------------------------
// `periodRows` powers KPIs + non-trend charts. `allRows` is needed for YoY,
// always relative to the LAST FULLY-COMPLETED MONTH — never today's MM-DD.
// Why: comparing 2026 Jan-Apr27 vs 2025 Jan-Apr27 is unfair because April
// 2026 is mid-month (partial) while 2025 April was a full month — the
// shorter current-year window drags the YoY % down. Fix: cap both years at
// the last complete month (e.g. Mar 2026) so we always compare full-month
// totals to full-month totals.
function computeStats(periodRows, allRows, ctx) {
  const now = new Date();
  const yr  = now.getFullYear();
  const mo  = now.getMonth() + 1;
  const thisYM   = `${yr}-${String(mo).padStart(2,'0')}`;
  // Cutoff for YoY = last fully-completed month (string "MM"). When today
  // is the last day of the month we keep the current month; otherwise we
  // step back one (handled by lastCompletedMonth helper).
  const _lcm     = lastCompletedMonth(now);
  const cutoffMM = String(_lcm.month).padStart(2, '0');

  // --- Aggregate over the period-filtered rows for KPIs + charts ----------
  let totalKrw = 0, totalQty = 0, thisMonthKrw = 0, periodKrw = 0;
  const mgMap = {}, mmMap = {}, cgMap = {}, ctyMap = {};

  for (const row of periodRows) {
    const krw  = row.sales_amount_krw || 0;
    const qty  = row.qty || 0;
    const date = row.order_date || '';
    if (!date) continue;

    const ym = date.slice(0,7);

    totalKrw += krw;
    totalQty += qty;
    periodKrw += krw;
    if (ym === thisYM) thisMonthKrw += krw;

    // 각 맵은 { krw, qty } 동시 집계 — 차트별 매출액/수량 토글용.
    const mg = row.make_group || 'Other';
    const mgC = mgMap[mg] || (mgMap[mg] = { krw:0, qty:0 });
    mgC.krw += krw; mgC.qty += qty;

    const mm = `${row.make || '?'} ${row.model || '?'}`.trim();
    const mmC = mmMap[mm] || (mmMap[mm] = { krw:0, qty:0 });
    mmC.krw += krw; mmC.qty += qty;

    const cg = row.category_group || 'Other';
    const cgC = cgMap[cg] || (cgMap[cg] = { krw:0, qty:0 });
    cgC.krw += krw; cgC.qty += qty;

    // Country TOP 10 — restrict to ROW channel so the chart shows meaningful
    // international distribution rather than being dominated by the US country
    // (which mirrors the US channel one-to-one). Other maps stay all-channel.
    if (row.channel_group === 'ROW') {
      const cty = (row.country && String(row.country).trim()) || 'Unknown';
      const ctyC = ctyMap[cty] || (ctyMap[cty] = { krw:0, qty:0 });
      ctyC.krw += krw; ctyC.qty += qty;
    }
  }

  // --- YoY ---------------------------------------------------------------
  // Compare this year (Jan-cutoffMM) vs prior year (Jan-cutoffMM). Cutoff is
  // the LAST FULLY-COMPLETED month so we never include a partial month on
  // the current-year side that the prior year had a full month of.
  // When a specific past year (e.g. 2024) is selected, compare the full
  // year against the prior full year — past years are always complete.
  let yoyThis = 0, yoyPrev = 0, yoyTargetYr = yr, yoyPrevYr = yr - 1;
  let yoyThisQty = 0, yoyPrevQty = 0;
  let yoyCutoffMM = cutoffMM;
  const periodYearMatch = (ctx?.period && /^\d{4}$/.test(ctx.period)) ? parseInt(ctx.period, 10) : null;
  if (periodYearMatch != null) {
    yoyTargetYr = periodYearMatch;
    yoyPrevYr   = periodYearMatch - 1;
    // For a past selected year, compare full year vs full prior year.
    // For the current year, keep the last-completed-month cutoff.
    if (periodYearMatch < yr) yoyCutoffMM = '12';
  }
  for (const row of allRows) {
    const date = row.order_date || '';
    if (!date) continue;
    const rowYr = parseInt(date.slice(0,4), 10);
    const mm    = date.slice(5,7);
    // Both sides capped at the same month-of-year for apples-to-apples.
    if (mm > yoyCutoffMM) continue;
    if (rowYr === yoyTargetYr) {
      yoyThis    += row.sales_amount_krw || 0;
      yoyThisQty += row.qty || 0;
    } else if (rowYr === yoyPrevYr) {
      yoyPrev    += row.sales_amount_krw || 0;
      yoyPrevQty += row.qty || 0;
    }
  }
  const yoy = yoyPrev > 0 ? (yoyThis - yoyPrev) / yoyPrev * 100 : null;

  // Human-readable comparison range for KPI sub-text. "1-3월" style for KO,
  // "Jan-Mar" for EN. When cutoff is January (only 1 month), use "1월".
  const _lang = (ctx && ctx.lang) || 'ko';
  const yoyComparisonNote = formatYoyRangeNote(yoyTargetYr, yoyPrevYr, parseInt(yoyCutoffMM, 10), _lang);
  const yoyShortNote      = formatYoyShortNote(parseInt(yoyCutoffMM, 10), _lang);

  // --- Prior-period context (YoY/MoM sub-text on KPI cards) --------------
  // 1) Period-revenue YoY: same period one year earlier. For 'all' (no period
  //    selected), reuse the YTD vs prior-year-YTD numbers we just computed.
  //    For a specific year, compare full-period totals (yoyThis vs yoyPrev
  //    above already use MD cutoff so it remains apples-to-apples).
  // 2) Total-revenue YoY: same logic — surface the YTD comparison.
  // 3) This-month MoM: current month vs same month last year (Δ = 1 year ago,
  //    matching the spec example). Note: "MoM" here means month-over-month
  //    YEAR — i.e. same month last year — per the user's spec.
  // 4) Total-qty YoY: yoyThisQty vs yoyPrevQty.
  const totalRevPriorPct  = yoyPrev > 0 ? (yoyThis - yoyPrev) / yoyPrev * 100 : null;
  const totalRevPriorDelta = yoyThis - yoyPrev;
  const totalQtyPriorPct  = yoyPrevQty > 0 ? (yoyThisQty - yoyPrevQty) / yoyPrevQty * 100 : null;
  const totalQtyPriorDelta = yoyThisQty - yoyPrevQty;

  // Same month last year for "이번 달 매출"
  const lastYrYM = `${yr - 1}-${String(mo).padStart(2,'0')}`;
  let lastYrMonthKrw = 0;
  for (const row of allRows) {
    const date = row.order_date || '';
    if (!date) continue;
    if (date.slice(0,7) === lastYrYM) lastYrMonthKrw += row.sales_amount_krw || 0;
  }
  const monthMomPct   = lastYrMonthKrw > 0 ? (thisMonthKrw - lastYrMonthKrw) / lastYrMonthKrw * 100 : null;
  const monthMomDelta = thisMonthKrw - lastYrMonthKrw;

  // Period-revenue prior-period comparison: when a specific year is selected,
  // compare that year (up to today MD) vs the previous year same cutoff.
  // For 'custom', shift the date window by exactly one year and re-aggregate.
  let periodPriorKrw = null;
  if (ctx?.period && /^\d{4}$/.test(ctx.period)) {
    periodPriorKrw = yoyPrev;
  } else if (ctx?.period === 'custom' && ctx?.dateFrom && ctx?.dateTo) {
    const shiftYear = (s) => {
      if (!s || s.length < 4) return s;
      const y = parseInt(s.slice(0,4), 10);
      return `${y - 1}${s.slice(4)}`;
    };
    const fPrev = shiftYear(ctx.dateFrom);
    const tPrev = shiftYear(ctx.dateTo);
    let acc = 0;
    for (const row of allRows) {
      const d = row.order_date || '';
      if (!d) continue;
      if (d >= fPrev && d <= tPrev) acc += row.sales_amount_krw || 0;
    }
    periodPriorKrw = acc;
  }
  const periodPriorPct = (periodPriorKrw != null && periodPriorKrw > 0)
    ? (periodKrw - periodPriorKrw) / periodPriorKrw * 100
    : null;
  const periodPriorDelta = (periodPriorKrw != null) ? (periodKrw - periodPriorKrw) : null;

  // 차트 데이터 — 매출액(krw)/수량(qty) 각각 자기 metric 기준으로 정렬.
  // 토글 시 해당 metric으로 재정렬되므로 Top N 구성도 metric별로 달라짐
  // (많이 팔린 것 ≠ 매출 큰 것). 내림차순 — 가로 막대 최상단이 최대값.
  const sortByMetric = (map, metric, n) => {
    const arr = Object.entries(map)
      .map(([k, v]) => [k, v[metric]])
      .sort((a, b) => b[1] - a[1]);
    return n ? arr.slice(0, n) : arr;
  };
  const makeGroups   = { krw: sortByMetric(mgMap,'krw'),     qty: sortByMetric(mgMap,'qty') };
  const makeModelTop = { krw: sortByMetric(mmMap,'krw',10),  qty: sortByMetric(mmMap,'qty',10) };
  const catGroups    = { krw: sortByMetric(cgMap,'krw'),     qty: sortByMetric(cgMap,'qty') };
  const countryTop   = { krw: sortByMetric(ctyMap,'krw',10), qty: sortByMetric(ctyMap,'qty',10) };

  return {
    totalKrw, totalQty, thisMonthKrw, periodKrw, yoy,
    makeGroups, makeModelTop, catGroups, countryTop,
    totalRows: periodRows.length,
    // Prior-period context for KPI sub-text
    totalRevPriorPct, totalRevPriorDelta,
    totalQtyPriorPct, totalQtyPriorDelta, totalQtyPriorAbs: yoyPrevQty,
    monthMomPct, monthMomDelta,
    periodPriorPct, periodPriorDelta, periodPriorKrw,
    // YoY methodology context (Fix 1)
    yoyComparisonNote, yoyShortNote,
    yoyTargetYr, yoyPrevYr, yoyCutoffMM,
  };
}

// --- Helpers -------------------------------------------------------------
function moneyB(n) {
  if (!n) return '₩0';
  if (Math.abs(n) >= 1e8) return '₩' + (n / 1e8).toFixed(1).replace(/\.0$/, '') + '억';
  if (Math.abs(n) >= 1e4) return '₩' + Math.round(n / 1e4) + '만';
  return '₩' + Math.round(n).toLocaleString();
}
// 차트 metric별 표시 설정 — 막대·도넛·라인 공용.
//  barScale: 막대/라인 dataset 값을 사람이 읽는 원단위로 되돌릴 배율
//            (krw는 억 단위로 넣으므로 ×1e8, qty는 원자료라 ×1).
function ovMetricCfg(metric, t) {
  if (metric === 'qty') {
    return {
      label: t.qtyLabel, barScale: 1,
      axisTick: v => Number(v).toLocaleString(),
      fmt: v => Number(v).toLocaleString() + (t.qtyUnit || ''),
    };
  }
  return {
    label: t.salesKrw, barScale: 1e8,
    axisTick: v => (Math.round(v * 10) / 10).toLocaleString() + '억',  // float 오차 제거 + 1자리 반올림
    fmt: v => moneyB(v),
  };
}
// Fix 1 — last fully-completed month. If today is the last day of its month
// the current month counts as complete; otherwise step back one month
// (rolls Jan → Dec of previous year).
function lastCompletedMonth(now) {
  const n = (now instanceof Date) ? now : new Date();
  const yr = n.getFullYear();
  const mo = n.getMonth() + 1; // 1-12
  const lastDayOfThisMonth = new Date(yr, mo, 0).getDate();
  if (n.getDate() >= lastDayOfThisMonth) return { year: yr, month: mo };
  if (mo === 1) return { year: yr - 1, month: 12 };
  return { year: yr, month: mo - 1 };
}
// "기준: 2026 1-3월 vs 2025 1-3월" — full comparison range note
function formatYoyRangeNote(thisYr, prevYr, cutoffMonth, lang) {
  const m = Math.max(1, Math.min(12, cutoffMonth || 1));
  if (lang === 'en') {
    const MS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    const range = m === 1 ? 'Jan' : 'Jan-' + MS[m - 1];
    return 'Period: ' + thisYr + ' ' + range + ' vs ' + prevYr + ' ' + range;
  }
  const range = m === 1 ? '1월' : ('1-' + m + '월');
  return '기준: ' + thisYr + ' ' + range + ' vs ' + prevYr + ' ' + range;
}
// "(1-3월 기준)" — short suffix for KPI sub-text
function formatYoyShortNote(cutoffMonth, lang) {
  const m = Math.max(1, Math.min(12, cutoffMonth || 1));
  if (lang === 'en') {
    const MS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    const range = m === 1 ? 'Jan' : 'Jan-' + MS[m - 1];
    return '(' + range + ')';
  }
  const range = m === 1 ? '1월' : ('1-' + m + '월');
  return '(' + range + ' 기준)';
}
function cssVar(name) {
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
// Unified chart-card sizing for the Overview tab.
// Every chart card on this page should share these dimensions so the visual
// mass stays consistent row-to-row and the user doesn't get whiplash from
// charts of wildly different heights. `TALL` is reserved for the full-width
// rows (e.g. country top-10 with 10 horizontal bars) where extra vertical
// room makes the labels readable.
const OV_CHART_HEIGHT      = 280;
const OV_CHART_HEIGHT_TALL = 320;

// ChartCard: header takes natural height, body fills remaining card height.
// Body is `position:relative; flex:1` so a child <canvas> styled with
// width/height 100% (or absolute inset:0) can fill it precisely — required
// for Chart.js `maintainAspectRatio:false` to behave.
//
// `height` controls TOTAL card height (header + body). Default uses the
// standard Overview chart height; pass `OV_CHART_HEIGHT_TALL` for full-
// width rows that benefit from extra room. Non-chart bodies (e.g. the
// CategoryShareList) can pass a list of items and they'll align top —
// they don't stretch to fill because the body uses flex:1 with a child
// that's content-sized.
// MetricToggle은 components.jsx의 공용 atom (window.MetricToggle) 사용.
function ChartCard({ title, children, right, height = OV_CHART_HEIGHT, bodyAlign = 'stretch' }) {
  const bodyClass = 'chart-card__body' + (bodyAlign === 'start' ? ' chart-card__body--list' : '');
  return (
    <div className="chart-card" style={{ height }}>
      <div className="chart-card__head">
        <div className="chart-card__title">{title}</div>
        {right || null}
      </div>
      <div className={bodyClass}>
        {children}
      </div>
    </div>
  );
}
function Placeholder({ msg }) {
  return <div style={{ padding:40, textAlign:'center', color:'var(--fg-muted)', fontSize:14 }}>{msg}</div>;
}
function ErrBox({ err, lang }) {
  return (
    <div className="surface-card" style={{ background:'rgba(255,69,58,.08)', color:'#FF453A', border:'1px solid rgba(255,69,58,.25)' }}>
      {err}
      <div style={{ fontSize:11, marginTop:8, color:'var(--fg-muted)' }}>
        {lang === 'en' ? 'Run DDL migrations first.' : 'DDL 마이그레이션 먼저 실행하세요.'}
      </div>
    </div>
  );
}

const T_KO = {
  title:'개요', loading:'불러오는 중…', empty:'데이터가 없습니다. CSV import 후 확인하세요.',
  totalRev:'전체 매출 (KRW)', totalQty:'총 판매량', units:'QTY',
  thisMonth:'이번 달 매출', periodRev:'기간 매출',
  yoy:'YoY 성장률', makeGroup:'MAKE GROUP 분포', top10Model:'MAKE × Model Top 10',
  top10Country:'ROW 국가별 Top 10',
  catGroup:'카테고리 그룹 비중', monthly:'월별 채널 추이', salesKrw:'매출 KRW',
  metricKrw:'매출액', metricQty:'수량', qtyLabel:'판매 수량', qtyUnit:'개',
  rowCount:'{n}개 행 기준',
  period:'기간', all:'전체', custom:'직접 설정',
  mtd:'이번 달', mtdTitle:'이번 달 1일부터 오늘까지',
  last6:'최근 6개월', last12:'최근 12개월', last24:'최근 24개월',
  trendWindowTitle:'표시 기간',
  periodLocked:'기간 선택 적용 중',
  // Comparison context labels
  vsPriorYTD:'전년 동기 대비',
  vsPriorPeriod:'전년 동기 대비',
  vsLastYrSameMonth:'전월 대비',
  // Target progress section
  tpTitle:'🎯 목표 진행률',
  tpFxCaveat: (months) => {
    const cy = new Date().getFullYear();
    const ms = (months || []).map(ym => {
      const [y, m] = ym.split('-').map(Number);
      return y === cy ? `${m}월` : `${y}년 ${m}월`;
    }).join('·');
    return `${ms || '당월'} 매출은 일일환율 잠정 적용 — 월평균 환율 확정 후 변동될 수 있습니다`;
  },
  tpTotal:'전체 연 목표',
  tpUS:'US 채널',
  tpROW:'ROW 채널',
  tpKR:'KR 채널',
  // Tooltips
  tipTotalRev:'선택된 기간 내 sales_amount_krw 합계',
  tipTotalQty:'선택된 기간 내 qty 합계',
  tipThisMonth:'현재 월의 sales_amount_krw 합계',
  tipPeriodRev:'선택 기간의 sales_amount_krw 합계',
  tipYoy:'올해 1월~직전 완전월 vs 작년 동일기간 비교. 진행 중인 달은 제외하여 부분월/완전월 왜곡을 방지합니다.',
  // Insights
  insightsTitle:'💡 인사이트',
  goodTitle:'성장 신호',
  riskTitle:'위험 신호',
  // FX notice (Fix 3)
  fxNotice:'{m}월 USD/ROW 환율 미고시 — 5월 초 갱신 예정. 이번 달 US/ROW 매출이 일시적으로 낮게 표시됩니다.',
};
const T_EN = {
  title:'Overview', loading:'Loading…', empty:'No data. Import CSV first.',
  totalRev:'Total Revenue (KRW)', totalQty:'Total Qty', units:'QTY',
  thisMonth:'This Month', periodRev:'Period Revenue',
  yoy:'YoY Growth', makeGroup:'Make Group Breakdown', top10Model:'Top 10 Make × Model',
  top10Country:'Top 10 ROW Countries',
  catGroup:'Category Share', monthly:'Monthly by Channel', salesKrw:'Sales KRW',
  metricKrw:'Revenue', metricQty:'Qty', qtyLabel:'Units Sold', qtyUnit:'',
  rowCount:'{n} rows loaded',
  period:'Period', all:'All', custom:'Custom',
  mtd:'MTD', mtdTitle:'Month-to-date (1st → today)',
  last6:'Last 6 months', last12:'Last 12 months', last24:'Last 24 months',
  trendWindowTitle:'Display range',
  periodLocked:'Synced with period filter',
  vsPriorYTD:'vs prior YTD',
  vsPriorPeriod:'vs prior period',
  vsLastYrSameMonth:'vs same month last year',
  tpTitle:'🎯 Target Progress',
  tpFxCaveat: (months) => {
    const cy = new Date().getFullYear();
    const MS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    const ms = (months || []).map(ym => {
      const [y, m] = ym.split('-').map(Number);
      return y === cy ? MS[m - 1] : `${MS[m - 1]} ${y}`;
    }).join(', ');
    return `${ms || 'Current month'} revenue uses provisional daily FX — may change once the monthly average rate is finalized`;
  },
  tpTotal:'Annual Target',
  tpUS:'US Channel',
  tpROW:'ROW Channel',
  tpKR:'KR Channel',
  tipTotalRev:'Sum of sales_amount_krw within the selected period',
  tipTotalQty:'Sum of qty within the selected period',
  tipThisMonth:'Sum of sales_amount_krw for the current month',
  tipPeriodRev:'Sum of sales_amount_krw across the selected period',
  tipYoy:'This year Jan→last completed month vs prior year same range. The in-progress month is excluded to avoid partial-vs-full-month distortion.',
  // Insights
  insightsTitle:'💡 Insights',
  goodTitle:'Growth Signals',
  riskTitle:'Risk Signals',
  // FX notice (Fix 3)
  fxNotice:'Month {m} USD/ROW FX rate not yet published — refresh expected early next month. Current-month US/ROW revenue is temporarily understated.',
};

window.OverviewView = OverviewView;
