// Query view — natural language question → template matching → results
// Phase 3 will add LLM; for now, keyword-based template matching.

// Toggle to log entity extraction details to the console for debugging.
const DEBUG_QUERY = false;

// Normalize string: lowercase, trim, collapse all whitespace runs to single space.
// Used on BOTH the query input AND row field values so case/spacing variations
// (e.g. "GR Supra" vs "GR  SUPRA") match consistently.
function normStr(s) {
  return String(s == null ? '' : s).toLowerCase().trim().replace(/\s+/g, ' ');
}

function QueryView({ lang, perms }) {
  const [input, setInput]       = React.useState('');
  const [mappings, setMappings] = React.useState(null);
  const [allRows, setAllRows]   = React.useState([]);
  const [dataLoading, setDataLoading] = React.useState(true);
  const [result, setResult]     = React.useState(null);
  const [err, setErr]           = React.useState(null);
  const [querying, setQuerying] = React.useState(false);
  // Previous input for "back" after drill-down clicks. Single-step history —
  // direct user search clears it. Crosstab-cell drill-down sets it.
  const [previousInput, setPreviousInput] = React.useState(null);
  const lineRef  = React.useRef(null);
  const chartRef = React.useRef(null);
  // 월별 추이 차트(채널 라인)의 매출액(krw)/수량(qty) 토글 — 세션 한정.
  const [trendMetric, setTrendMetric] = React.useState('krw');

  const t = lang === 'en' ? QT_EN : QT_KO;

  React.useEffect(() => {
    (async () => {
      try {
        const [m, rows] = await Promise.all([
          fetch('data/query-mappings.json').then(r => r.json()),
          SALES_DB.fetchAllSalesData(),
        ]);
        setMappings(m);
        setAllRows(rows);
      } catch (e) {
        setErr(e?.message || String(e));
      } finally {
        setDataLoading(false);
      }
    })();
  }, []);

  // Render monthly trend chart after result changes.
  // deps에 trendMetric 포함 — 매출/수량 토글 시 이 차트만 재생성.
  React.useEffect(() => {
    if (!result?.monthly || !lineRef.current) return;
    if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; }
    const { months, byChannel, byChannelQty } = result.monthly;
    if (!months.length) return;

    const cfg = qMetricCfg(trendMetric, lang);
    const src = trendMetric === 'qty' ? (byChannelQty || {}) : byChannel;
    const fgMuted = getComputedStyle(document.documentElement).getPropertyValue('--fg-muted').trim();
    const gridC   = 'rgba(128,128,128,0.12)';
    // Build datasets only for channels with non-zero data so a country
    // filter (e.g. Germany → only ROW) doesn't drag two flat-zero lines
    // through the chart.
    const channelDefs = [
      { key:'US',  data: src.US  || [], color:'#007AFF' },
      { key:'ROW', data: src.ROW || [], color:'#FF9500' },
      { key:'KR',  data: src.KR  || [], color:'#34C759' },
    ];
    const activeChannels = channelDefs.filter(d => d.data.some(v => v > 0));
    // Context-aware line label: mirror the card-label fix — when a country/
    // region geo filter is active and exactly one channel has data, swap the
    // dataset label from "ROW"/"US" to the geo value (e.g. "Germany").
    const geoFilter = (result.appliedFilters || []).find(f =>
      f.label === '국가' || f.label === 'Country' || f.label === '지역' || f.label === 'Region'
    );
    const useGeoLabel = !!geoFilter && activeChannels.length === 1;
    const datasets = (activeChannels.length > 0 ? activeChannels : channelDefs).map(d => ({
      label: useGeoLabel ? geoFilter.value : d.key,
      data: d.data,
      borderColor: d.color, backgroundColor: d.color + '22',
      borderWidth: 2, pointRadius: 2, tension: .3, fill: false,
    }));

    chartRef.current = new Chart(lineRef.current, {
      type: 'line',
      data: { labels: months, datasets },
      options: {
        // v3.3: 부모 컨테이너 크기에 맞춰 그림 (canvas height 속성 무시)
        // → 부모 div에 maxHeight 200px 주면 차트가 그만큼만 차지.
        responsive: true,
        maintainAspectRatio: false,
        interaction: { mode:'index', intersect:false },
        plugins: {
          legend: { labels: { color:fgMuted, font:{ size:11 }, boxWidth:12 } },
          tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${cfg.fmt(ctx.raw)}` } },
        },
        scales: {
          x: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 } } },
          y: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 }, callback: cfg.axisTick } },
        },
      },
    });
    return () => { if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } };
  }, [result, trendMetric, lang]);

  // Build a runtime index of actual MAKE / MODEL / COUNTRY / REGION values
  // from the live dataset, so the query view can recognise any value present
  // in the DB even if it isn't in the hand-curated mappings file. Keys are
  // normalised (lowercased + whitespace-collapsed) → original DB value.
  // For models we also index a few common transforms (without spaces, hyphens
  // → spaces, last-token alone) so "GR SUPRA" can match "supra" or "gr-supra"
  // even though only the canonical value lives in the DB.
  const dataIndex = React.useMemo(() => {
    const models = {};
    const makes  = {};
    const countries = {};
    const regions = {};
    const wingTypes = {};   // v3.3: WING TYPE 동적 인덱싱
    const companies = {};   // v3.3: Company 동적 인덱싱 (raw 회사명 다 등록)
    function add(map, key, value) {
      const k = normStr(key);
      if (k && !map[k]) map[k] = value;
    }
    for (const r of allRows) {
      if (r.model) {
        const orig = String(r.model).trim();
        const norm = normStr(orig);
        if (!norm) continue;
        add(models, norm, orig);
        // Hyphens ↔ spaces (e.g. "gr-supra" matches "GR SUPRA")
        if (norm.includes('-')) add(models, norm.replace(/-/g, ' '), orig);
        if (norm.includes(' ')) add(models, norm.replace(/ /g, '-'), orig);
        // Squashed (e.g. "grsupra")
        if (norm.includes(' ')) add(models, norm.replace(/\s+/g, ''), orig);
        // Last token alone if multi-word AND last token is at least 3 chars
        // (so "GR SUPRA" → "supra", but we don't index "M3" → "3")
        const parts = norm.split(' ');
        if (parts.length > 1) {
          const last = parts[parts.length - 1];
          if (last.length >= 3) add(models, last, orig);
        }
      }
      if (r.make) {
        const orig = String(r.make).trim();
        add(makes, normStr(orig), orig);
      }
      if (r.country) {
        const orig = String(r.country).trim();
        add(countries, normStr(orig), orig);
      }
      if (r.region) {
        const orig = String(r.region).trim();
        add(regions, normStr(orig), orig);
      }
      if (r.universal_wing_type) {
        const orig = String(r.universal_wing_type).trim();
        add(wingTypes, normStr(orig), orig);
      }
      if (r.company) {
        const orig = String(r.company).trim();
        add(companies, normStr(orig), orig);
      }
    }
    return { models, makes, countries, regions, wingTypes, companies };
  }, [allRows]);

  function handleQuery() {
    if (!input.trim() || !mappings || allRows.length === 0) return;
    setQuerying(true);
    setResult(null);
    // User-initiated search starts a fresh trail — clear any drill-down history
    // so the back chip doesn't surprise them after they typed a new query.
    setPreviousInput(null);
    try {
      // Expose current language for helper fns that don't receive it via params
      // (suggestion chip labels need localized type names)
      if (typeof window !== 'undefined') window.__queryLang = lang;
      const r = runQuery(input.trim(), mappings, allRows, lang, dataIndex);
      setResult(r);
      logQuery(input.trim(), r);
    } catch (e) {
      setErr(e?.message || String(e));
      logQuery(input.trim(), { error: e?.message });
    } finally {
      setQuerying(false);
    }
  }

  const EXAMPLES = lang === 'en'
    ? ['GR Supra vs Stinger', 'best sellers by country', 'this quarter revenue', 'last 3 months BMW', 'Germany performance', 'YoY growth']
    : ['수프라 vs 스팅어', '국가별로 뭐가 잘 팔려', '이번 분기 매출', '비엠 미국에서 뭐가 잘 팔려', '독일 실적', '전년 대비 성장률'];

  return (
    <div>
      <h2 style={{ fontSize:22, fontWeight:700, marginBottom:6, letterSpacing:-.01 }}>{t.title}</h2>
      <p style={{ fontSize:13, color:'var(--fg-muted)', marginBottom:18, marginTop:0 }}>{t.desc}</p>

      {/* Input row */}
      <div style={{ display:'flex', gap:8, marginBottom:10 }}>
        <input
          value={input}
          onChange={e => {
            const v = e.target.value;
            setInput(v);
            // When the user clears the input, also reset prior result/error so
            // the templates section reappears (instead of staying stuck on the
            // previous answer).
            if (!v.trim()) { setResult(null); setErr(null); }
          }}
          onKeyDown={e => e.key === 'Enter' && handleQuery()}
          placeholder={t.placeholder}
          disabled={dataLoading}
          style={{
            flex:1, padding:'10px 14px', fontSize:14, borderRadius:10,
            border:'1px solid var(--border)', background:'var(--bg-solid)',
            color:'var(--fg)', outline:'none',
          }}
        />
        <button onClick={handleQuery} disabled={querying || dataLoading || !input.trim()} style={{
          padding:'10px 20px', fontSize:13, fontWeight:600, borderRadius:10,
          background:'var(--accent)', color:'#fff', border:'none', cursor:'pointer',
          opacity: (querying || dataLoading || !input.trim()) ? .5 : 1,
        }}>
          {querying ? '…' : t.search}
        </button>
      </div>

      {/* Back chip — restore the search that produced the crosstab cards
          the user just drilled into. Single-step; clears on direct search. */}
      {previousInput && (
        <div style={{ marginBottom:10 }}>
          <button onClick={() => {
            const prev = previousInput;
            setInput(prev);
            setPreviousInput(null);
            setTimeout(() => {
              if (typeof window !== 'undefined') window.__queryLang = lang;
              try {
                const r = runQuery(prev, mappings, allRows, lang, dataIndex);
                setResult(r);
              } catch (e) { setErr(e?.message || String(e)); }
            }, 0);
          }} style={{
            padding:'4px 10px', fontSize:11.5, borderRadius:14, cursor:'pointer',
            background:'var(--surface-2)', color:'var(--fg-2)',
            border:'1px solid var(--border)',
            display:'inline-flex', alignItems:'center', gap:4,
          }}>
            <span style={{ opacity:.7 }}>←</span>
            <span style={{ color:'var(--fg-muted)' }}>{lang === 'en' ? 'Back to' : '이전 검색'}:</span>
            <span style={{ fontWeight:500 }}>{previousInput.length > 50 ? previousInput.slice(0,50) + '…' : previousInput}</span>
          </button>
        </div>
      )}

      {/* Example chips */}
      <div style={{ display:'flex', gap:6, flexWrap:'wrap', marginBottom:20 }}>
        {EXAMPLES.map(ex => (
          <button key={ex} onClick={() => { setInput(ex); setResult(null); setErr(null); }} style={{
            padding:'4px 10px', fontSize:11.5, borderRadius:20,
            background:'var(--surface-2)', color:'var(--fg-2)',
            border:'1px solid var(--border)', cursor:'pointer',
          }}>{ex}</button>
        ))}
      </div>

      {dataLoading && <div style={{ color:'var(--fg-muted)', fontSize:13 }}>{t.loading}</div>}
      {err && <div style={{ color:'#FF453A', fontSize:13, marginBottom:12 }}>Error: {err}</div>}

      {result && (
        <QueryResult
          result={result} lineRef={lineRef} lang={lang} t={t}
          trendMetric={trendMetric} onTrendMetricChange={setTrendMetric}
          onSuggestionClick={(val) => {
            setInput(val);
            // Re-run query immediately with the suggested value
            setTimeout(() => {
              if (typeof window !== 'undefined') window.__queryLang = lang;
              try {
                const r = runQuery(val, mappings, allRows, lang, dataIndex);
                setResult(r);
              } catch (e) { setErr(e?.message || String(e)); }
            }, 0);
          }}
          onCrosstabCellClick={(cellLabel) => {
            // Drill into the clicked group: remember current input so the
            // back chip can restore it, strip the crosstab trigger word,
            // append the cell label, then re-run.
            const next = buildDrillDownQuery(input, cellLabel);
            setPreviousInput(input);
            setInput(next);
            setTimeout(() => {
              if (typeof window !== 'undefined') window.__queryLang = lang;
              try {
                const r = runQuery(next, mappings, allRows, lang, dataIndex);
                setResult(r);
              } catch (e) { setErr(e?.message || String(e)); }
            }, 0);
          }}
        />
      )}

      {!result && !dataLoading && (
        <div style={{ marginTop:8 }}>
          <div style={{ fontSize:11, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.06, marginBottom:10 }}>
            {t.templates}
          </div>
          {/* v3.4: 카테고리별 그룹 + 칩. 첫 토큰 (예시 부분)만 input에 채움.
              {placeholder} 문법은 보존 — 사용자가 그 부분만 직접 편집. */}
          <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
            {TEMPLATE_LIST[lang === 'en' ? 'en' : 'ko'].map((grp, gi) => (
              <div key={gi}>
                <div style={{ fontSize:10.5, fontWeight:700, color:'var(--fg-subtle)', textTransform:'uppercase', letterSpacing:.06, marginBottom:6 }}>
                  {grp.group}
                </div>
                <div style={{ display:'flex', gap:6, flexWrap:'wrap' }}>
                  {grp.items.map((tmpl, i) => {
                    const [pattern, hint] = tmpl.split('  ');
                    return (
                      <button key={i} onClick={() => { setInput(pattern); setResult(null); setErr(null); }}
                        title={hint || ''}
                        style={{
                          padding:'5px 11px', fontSize:11.5,
                          background:'var(--bg-solid)', border:'1px solid var(--border)',
                          borderRadius:14, cursor:'pointer', color:'var(--fg-2)',
                          textAlign:'left', lineHeight:1.4,
                        }}>
                        {pattern}
                      </button>
                    );
                  })}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// --- Query engine --------------------------------------------------------
function runQuery(input, mappings, allRows, lang, dataIndex, _depth) {
  // ── Compare mode: "A vs B", "A 대비 B", "A랑 B 비교" ─────────────────
  // Only at top level (_depth=0) to prevent infinite recursion.
  // Explicit separators (vs/대비) always trigger compare.
  // Korean particles (과/와/랑/이랑/하고) only trigger when "비교" appears at end —
  // otherwise "BMW와 관련된 매출" would false-positive.
  if (!_depth) {
    const vsExplicit = input.match(/^(.+?)\s+(?:vs\.?|versus|대비)\s+(.+?)$/i);
    const vsParticle = input.match(/^(.+?)(?:랑|이랑|하고|과|와)\s+(.+?)\s+비교$/i);
    const vsMatch = vsExplicit || vsParticle;
    if (vsMatch) {
      let a = vsMatch[1].trim(), b = vsMatch[2].trim();
      if (a && b) {
        // Symmetric crosstab: if a trailing 별-keyword is on either side
        // (e.g. "수프라 vs 스팅어 파츠별"), pull it out and apply to BOTH so
        // the comparison shows the breakdown for each side, not just one.
        const xtSuffix = /\s+(국가별|나라별|by\s*country|each\s*country|per\s*country|채널별|by\s*channel|메이커별|메이크별|브랜드별|by\s*make|by\s*brand|sku별|by\s*sku|상품별|제품별|아이템별|by\s*product|by\s*item|카테고리별|cat별|파츠별|부품별|파트별|by\s*category|by\s*parts?|모델별|by\s*model|연도별|년도별|by\s*year)\s*$/i;
        const bMatch = b.match(xtSuffix);
        const aMatch = !bMatch && a.match(xtSuffix);
        const xtKw = (bMatch || aMatch)?.[1];
        if (xtKw) {
          a = a.replace(xtSuffix, '').trim() + ' ' + xtKw;
          b = b.replace(xtSuffix, '').trim() + ' ' + xtKw;
        }
        const rA = runQuery(a, mappings, allRows, lang, dataIndex, 1);
        const rB = runQuery(b, mappings, allRows, lang, dataIndex, 1);
        // Detect time-series bucket on the FULL input so "수프라 vs 스팅어 연도별"
        // aggregates the chart by year. Default is monthly. Keywords inside
        // sub-queries are ignored here — the chart bucket is a top-level concern.
        const bucket = /연도별|년도별|by\s*year|yearly|annually/i.test(input) ? 'year'
          : /분기별|by\s*quarter|quarterly/i.test(input) ? 'quarter'
          : 'month';
        return {
          query: input, appliedFilters: [], rowCount: rA.rowCount + rB.rowCount,
          summary: {}, monthly: null, groupDim: null, groupedResult: null,
          yoyData: null, shareData: null, crosstabData: null, crosstabDim: null,
          orderData: null, orderTokens: [], reportData: null, suggestions: null,
          compareData: { labelA: rA.appliedFilters.map(f=>f.value).join(' ') || a, labelB: rB.appliedFilters.map(f=>f.value).join(' ') || b, a: rA, b: rB, bucket },
          isEmpty: false,
        };
      }
    }
  }

  const q = normStr(input);
  // Merge the curated mappings with the runtime data index so that any model
  // or make actually present in sales_core can be recognised — even when the
  // mappings file hasn't been updated yet. Curated entries take priority for
  // alias support (e.g. "현대" → "HYUNDAI"), but unseen DB values still match.
  const modelMap   = dataIndex ? Object.assign({}, dataIndex.models,    mappings.models)    : mappings.models;
  const makeMap    = dataIndex ? Object.assign({}, dataIndex.makes,     mappings.makes)     : mappings.makes;
  const countryMap = dataIndex ? Object.assign({}, dataIndex.countries, mappings.countries) : mappings.countries;
  const regionMap  = dataIndex ? Object.assign({}, dataIndex.regions,   mappings.regions)   : (mappings.regions || {});
  // v3.3: WING TYPE은 큐레이션된 동의어(at-r1, atr1, 알1 등)와 dataIndex(원본 값) 머지
  const wingTypeMap = dataIndex ? Object.assign({}, dataIndex.wingTypes, mappings.wing_types || {}) : (mappings.wing_types || {});
  // v3.3: Company는 raw 회사명이 너무 다양해서 큐레이션 사전 없이 dataIndex만 사용
  const companyMap  = dataIndex ? dataIndex.companies : {};

  const found = {
    model:    findInMap(q, modelMap),
    models:   extractModels(q, modelMap),  // multi-model OR; [] when single
    make:     findInMap(q, makeMap),
    makeGrp:  findInMap(q, mappings.make_groups),
    category: findInMap(q, mappings.categories),
    catGrp:   findInMap(q, mappings.category_groups),
    country:  findInMap(q, countryMap),
    region:   findInMap(q, regionMap),
    channel:  findInMap(q, mappings.channels),
    wingType: findInMap(q, wingTypeMap),  // v3.3
    company:  findInMap(q, companyMap),   // v3.3
    year:     extractYear(q),
    month:    extractMonth(q),
    topN:     extractTopN(q),
    dateRange: extractDateRange(q),
  };

  const isYoY   = /yoy|성장률|year.over|전년\s*대비|작년\s*대비/i.test(q);
  const isShare = /비중|share|percent|퍼센트|몇\s*%/i.test(q);
  const isTop   = /top\s*\d+|상위\s*\d+/i.test(q);
  const isReport = /보고서|리포트|report|정리해줘|요약해줘|summarize/i.test(q);
  // "뭐가 제일 잘 팔려" / "best seller" / "인기" — implicit top intent
  const isBestSeller = /뭐가.*(?:잘|많이|제일)|best\s*sell|인기|most\s*popular|most\s*sold|효자/i.test(q);
  // "특이사항" / "이상" / "변화" / "트렌드" / "추이" — anomaly/insight mode
  const isInsight = /특이사항|이상|눈에\s*띄|변화|트렌드|추이|trend|anomal|notable|highlight|insight/i.test(q);
  // "매출대비 판매량" / "객단가" / "단가" / "효율" — unit economics sort mode
  const isEfficiency = /매출\s*대비|객단가|단가|효율|평균\s*단가|asp|avg.*price|unit.*eco|revenue.*per|per.*unit/i.test(q);

  // Crosstab intent: "국가별로 뭐가 잘 팔려", "by country what sells", etc.
  // Detects two parts: a grouping dimension ("by X") + a "what sells" verb.
  const _crosstabRe =
    /국가별|나라별|by\s*country|each\s*country|per\s*country|채널별|by\s*channel|메이커별|메이크별|브랜드별|by\s*make|by\s*brand|카테고리별|cat별|파츠별|부품별|파트별|by\s*category|by\s*parts?|sku별|by\s*sku|상품별|제품별|아이템별|by\s*product|by\s*item|모델별|by\s*model|연도별|년도별|by\s*year/i;
  const isCrosstab = _crosstabRe.test(q);
  let crosstabDim = null;
  if (isCrosstab) {
    if (/국가별|나라별|by\s*country|each\s*country|per\s*country/i.test(q)) crosstabDim = 'country';
    else if (/채널별|by\s*channel/i.test(q))                               crosstabDim = 'channel_group';
    else if (/메이커별|메이크별|브랜드별|by\s*make|by\s*brand/i.test(q))    crosstabDim = 'make';
    // SKU/상품/제품 — finer than category, check before category fallthrough
    else if (/sku별|by\s*sku/i.test(q))                                    crosstabDim = 'sku_raw';
    else if (/상품별|제품별|아이템별|by\s*product|by\s*item/i.test(q))      crosstabDim = 'product_name';
    // Category synonyms: 카테고리별/CAT별/파츠별/부품별/파트별 — all roll up to category_group
    else if (/카테고리별|cat별|파츠별|부품별|파트별|by\s*category|by\s*parts?/i.test(q)) crosstabDim = 'category_group';
    else if (/모델별|by\s*model/i.test(q))                                 crosstabDim = 'model';
    else if (/연도별|년도별|by\s*year/i.test(q))                           crosstabDim = 'year';
  }

  // Order lookup intent: query contains an invoice_no-like token
  // Patterns observed in this dataset:
  //   US Shopify: ADRO_1009834, #ADRO_1009834
  //   KR Cafe24:  20260427-0000123  (date-then-seq with dash)
  //   ROW invoice: 25100000333ID  (digits + 2-letter country suffix)
  //   ROW general: 25-A001, INV-001
  // Detection rule: any token of length ≥5 with at least one digit AND
  //   matches one of the known invoice patterns
  const orderTokens = (input.match(/[A-Za-z0-9_#\-]+/g) || [])
    .map(t => t.replace(/^#/, ''))
    .filter(t => t.length >= 5 && /\d/.test(t))
    .filter(t =>
      /^ADRO[_-]?\d+$/i.test(t) ||
      /^\d{6,}[-_]\d+$/.test(t) ||
      /^[A-Z]{2,}[-_]?\d+$/i.test(t) ||
      /^\d{6,}$/.test(t) ||
      /^\d{8,}[A-Z]{2}$/i.test(t)
    );
  const isOrderLookup = orderTokens.length > 0;

  if (DEBUG_QUERY) {
    // eslint-disable-next-line no-console
    console.log('[query] input:', input, '| normalized:', q, '| found:', found,
      '| isYoY:', isYoY, '| isShare:', isShare, '| isTop:', isTop);
  }

  let rows = allRows;
  const appliedFilters = [];

  // dateRange takes precedence over year/month if present (more specific)
  if (found.dateRange) {
    const { from, to, label } = found.dateRange;
    rows = rows.filter(r => {
      const d = r.order_date || '';
      return d >= from && d <= to;
    });
    appliedFilters.push({ label: lang === 'en' ? 'Period' : '기간', value: label });
  } else {
    if (found.year) {
      rows = rows.filter(r => r.order_date?.startsWith(String(found.year)));
      appliedFilters.push({ label: lang === 'en' ? 'Year' : '연도', value: found.year });
    }
    if (found.month) {
      rows = rows.filter(r => r.order_date?.startsWith(found.month));
      appliedFilters.push({ label: lang === 'en' ? 'Month' : '월', value: found.month });
    }
  }
  // Country/channel ambiguity: tokens like "미국", "한국", "독일" exist in BOTH
  // mappings.countries and mappings.channels. AND-ing both filters yields zero
  // rows when DB country values don't perfectly match the channel code (e.g.
  // country="USA" vs channel="US"). Sales analysts almost always mean country
  // when they say "미국 매출", so country wins.
  if (found.country && found.channel) {
    found.channel = null;
  }

  // Model/make collision: if both maps return the SAME canonical value (e.g.
  // "GR SUPRA" winds up in both because some r.make rows are mis-entered as
  // model names), the AND-ed filters collapse to ~0 rows. The model match is
  // more specific, so prefer it and drop the (likely spurious) make filter.
  if (found.model && found.make && normStr(found.model) === normStr(found.make)) {
    found.make = null;
  }

  // Wing-type vs model collision: e.g. "at-m3" matches wing_types ("AT-M3")
  // but the trailing "m3" substring also matches the BMW M3 model alias,
  // so the AND-filter (model=M3 ∧ wing=AT-M3) returns 0 rows. Wing-type
  // intent is more specific — drop the spurious model when the wing label
  // contains it.
  if (found.wingType && found.model) {
    const wt = normStr(found.wingType);
    const md = normStr(found.model);
    if (wt.includes(md)) found.model = null;
  }

  // Helper: relaxed equality. Curated mappings + dataIndex return canonical
  // values, but the DB column may store variants like "BMW M3" or
  // "M3 COMPETITION". Accept exact match OR substring in either direction.
  const includesEither = (rowVal, tgtNorm) => {
    const rv = normStr(rowVal);
    if (!rv) return false;
    return rv === tgtNorm || rv.includes(tgtNorm) || tgtNorm.includes(rv);
  };

  if (found.models && found.models.length >= 2) {
    // Multi-model OR: each segment matched a distinct model. Count rows per
    // model BEFORE the IN filter to label as "GR SUPRA, STINGER (28+34 rows)".
    const tgts = found.models.map(m => normStr(m));
    const counts = tgts.map(t => rows.filter(r => includesEither(r.model, t)).length);
    rows = rows.filter(r => tgts.some(t => includesEither(r.model, t)));
    appliedFilters.push({
      label: 'Model',
      value: `${found.models.join(', ')} (${counts.join('+')} rows)`,
    });
  } else if (found.model) {
    const tgt = normStr(found.model);
    rows = rows.filter(r => includesEither(r.model, tgt));
    appliedFilters.push({ label: 'Model', value: found.model });
  }
  if (found.make) {
    const tgt = normStr(found.make);
    rows = rows.filter(r => includesEither(r.make, tgt));
    appliedFilters.push({ label: 'Make', value: found.make });
  }
  if (found.makeGrp) {
    const tgt = normStr(found.makeGrp);
    rows = rows.filter(r => includesEither(r.make_group, tgt));
    appliedFilters.push({ label: 'Make Group', value: found.makeGrp });
  }
  if (found.catGrp) {
    const tgt = normStr(found.catGrp);
    rows = rows.filter(r => includesEither(r.category_group, tgt));
    appliedFilters.push({ label: 'Cat Group', value: found.catGrp });
  } else if (found.category) {
    const tgt = normStr(found.category);
    rows = rows.filter(r => includesEither(r.category, tgt));
    appliedFilters.push({ label: 'Category', value: found.category });
  }
  if (found.country) {
    // Exact match: country canonical은 raw DB 값과 일치 (query-mappings.json 주석 참조).
    // includesEither의 양방향 substring은 "us"가 "australia"/"darussalam"에 오탐.
    const tgt = normStr(found.country);
    rows = rows.filter(r => normStr(r.country) === tgt);
    appliedFilters.push({ label: lang === 'en' ? 'Country' : '국가', value: found.country });
  }
  if (found.region) {
    const tgt = normStr(found.region);
    rows = rows.filter(r => includesEither(r.region, tgt));
    appliedFilters.push({ label: lang === 'en' ? 'Region' : '지역', value: found.region });
  }
  if (found.channel) {
    if (['US','ROW','KR'].includes(found.channel)) {
      rows = rows.filter(r => r.channel_group === found.channel);
    } else if (['B2B','B2C'].includes(found.channel)) {
      rows = rows.filter(r => normStr(r.sales_channel) === normStr(found.channel));
    }
    appliedFilters.push({ label: 'Channel', value: found.channel });
  }
  // v3.3: 윙 종류 (AT-R1/R2/R3/R4, AT-S, AT-P/P1, AT-M3, ARW-300)
  if (found.wingType) {
    const tgt = normStr(found.wingType);
    rows = rows.filter(r => normStr(r.universal_wing_type) === tgt);
    appliedFilters.push({ label: lang === 'en' ? 'Wing Type' : '윙 종류', value: found.wingType });
  }
  // v3.3: 회사 (B2B 도매처, B2C 리터럴)
  if (found.company) {
    const tgt = normStr(found.company);
    rows = rows.filter(r => includesEither(r.company, tgt));
    appliedFilters.push({ label: 'Company', value: found.company });
  }

  if (DEBUG_QUERY) {
    // eslint-disable-next-line no-console
    console.log('[query] matched rows:', rows.length, '/', allRows.length);
  }
  // Runtime-toggleable debug: enable in browser console with
  //   window.DEBUG_QUERY = true
  // to inspect entity extraction + candidate filters without rebuilding.
  if (typeof window !== 'undefined' && window.DEBUG_QUERY) {
    // eslint-disable-next-line no-console
    console.log('[query]', { q, found, appliedFilters, rowCount: rows.length });
  }

  const summary = aggregateSummary(rows);
  const monthly  = buildMonthlyQ(rows);

  let groupedResult = null, groupDim = null, groupSortMode = 'krw';
  if (isEfficiency) {
    // Unit economics mode — sort by ASP (avg selling price = KRW/QTY)
    groupDim = found.catGrp || found.category ? 'category' : 'model';
    groupedResult = groupByQ(rows, groupDim).map(g => ({ ...g, asp: g.qty > 0 ? g.krw / g.qty : 0 }));
    groupedResult.sort((a, b) => b.asp - a.asp);
    groupedResult = groupedResult.slice(0, found.topN || 10);
    groupSortMode = 'asp';
  } else if (found.make && !found.model) {
    groupDim = 'model';
    groupedResult = groupByQ(rows, 'model');
  } else if (found.catGrp && !found.category && !isCrosstab) {
    // Cat-group filter active (e.g. "카본파츠") — drill into sub-categories
    // so sales rep can see WHICH parts (rear diffuser / side skirt / wing)
    // drive the group's revenue. Auto-applies whenever the user filters by
    // a category group without picking a specific category.
    groupDim = 'category';
    groupedResult = groupByQ(rows, 'category');
  } else if (found.category && !isCrosstab) {
    // Specific category active — one level deeper to product names.
    groupDim = 'product_name';
    groupedResult = groupByQ(rows, 'product_name').slice(0, 30);
  } else if ((isTop || isBestSeller) && !isCrosstab) {
    // "뭐가 제일 잘 팔려" — auto top 10 with smart dim selection
    groupDim = found.catGrp || found.category ? 'category'
      : found.country || found.region ? 'model'
      : found.channel ? 'model'
      : found.makeGrp ? 'model'
      : 'model';
    groupedResult = groupByQ(rows, groupDim).slice(0, found.topN || 10);
  }

  let yoyData = null;
  if (isYoY) yoyData = computeYoYQ(allRows, found.channel);

  let shareData = null;
  if (isShare) {
    const totalKrw = allRows.reduce((s, r) => s + (r.sales_amount_krw || 0), 0);
    shareData = { filteredKrw: summary.krw_total, totalKrw, pct: totalKrw > 0 ? summary.krw_total / totalKrw * 100 : 0 };
  }

  // ── Crosstab — group by dim, then top N models within each group ─────────
  // "TOP 5 상품" → 내부 N / "TOP 5 국가별" → 그룹 N / "TOP 5"만 있으면 그룹 N (기존 동작)
  let crosstabData = null;
  if (isCrosstab && crosstabDim) {
    const innerN = extractInnerTopN(q) || 3;
    const groupN = extractGroupTopN(q) || (extractInnerTopN(q) ? null : found.topN);
    crosstabData = buildCrosstab(rows, crosstabDim, innerN);
    if (groupN && crosstabData) crosstabData = crosstabData.slice(0, groupN);
  }

  // ── Insight / anomaly annotations on crosstab groups ───────────────────
  let insightData = null;
  if ((isInsight || isReport) && crosstabData && crosstabData.length > 0) {
    insightData = computeCrosstabInsights(crosstabData, allRows, rows, crosstabDim, found, lang);
  } else if (isInsight && !isCrosstab) {
    // Insight without crosstab — run on overall filtered data by a reasonable dim
    const autoDim = found.country || found.region ? 'model' : 'country';
    const tempCross = buildCrosstab(rows, autoDim).slice(0, 10);
    insightData = computeCrosstabInsights(tempCross, allRows, rows, autoDim, found, lang);
  }

  // ── Order lookup — direct invoice_no search overrides normal filtering ──
  let orderData = null;
  if (isOrderLookup) {
    orderData = lookupOrders(allRows, orderTokens);
  }

  // ── Report — comprehensive multi-section view ───────────────────────────
  let reportData = null;
  if (isReport) {
    reportData = buildReport(rows, allRows, appliedFilters, found, lang);
  }

  // ── Grouped trend — when breakdown is by category/product, also build a
  // per-group monthly series for top 6 entries so the result page can show
  // a multi-line trend chart alongside the breakdown table.
  let groupedTrend = null;
  if (groupedResult && groupedResult.length > 0 && (groupDim === 'category' || groupDim === 'product_name')) {
    const topLabels = groupedResult.slice(0, 6).map(g => g.label);
    groupedTrend = buildGroupedTrend(rows, groupDim, topLabels);
  }

  // ── Did-you-mean — empty result OR strong fuzzy candidate for unmatched company ──
  let suggestions = null;
  const noEntities = !found.model && !found.make && !found.makeGrp && !found.category && !found.catGrp && !found.country && !found.region && !found.channel && !found.company;
  if (dataIndex && !orderData) {
    const empty = rows.length === 0;
    const trigger = empty || (noEntities && !isYoY && !isShare && !isTop && !isCrosstab);
    if (trigger) {
      suggestions = findSuggestions(q, dataIndex, mappings, found);
    } else {
      // 결과는 있지만 사용자가 비슷한 이름을 잘못 친 경우 (예: "GP Product" 매칭 실패).
      // 강한 fuzzy 후보 (score >= 50)만 살려서 결과 위에 칩으로 표시.
      const strong = findSuggestions(q, dataIndex, mappings, found);
      if (strong && strong.length > 0 && strong[0].score >= 50) suggestions = strong.slice(0, 3);
    }
  }

  return {
    query: input, appliedFilters, rowCount: rows.length,
    summary, monthly, groupDim, groupedResult, groupedTrend, groupSortMode, yoyData, shareData,
    crosstabData, crosstabDim,
    orderData, orderTokens,
    reportData,
    insightData,
    suggestions,
    // v3.3: 매칭된 주문 행 (최대 200건). 펼침 토글로 표시.
    matchedRows: rows.slice(0, 200),
    matchedTotal: rows.length,
    isEmpty: rows.length === 0 && !orderData && !reportData,
  };
}

// Build a comprehensive report: KPIs + monthly trend (with prior-year overlay)
// + top breakdowns + auto insights. Used by report mode and as the body when
// the user explicitly asks for a 보고서/report.
function buildReport(rows, allRows, appliedFilters, found, lang) {
  // Header / period label
  const filterStr = appliedFilters.map(f => `${f.label}: ${f.value}`).join(' · ') ||
    (lang === 'en' ? 'All segments' : '전체');
  const headerTitle = (lang === 'en' ? 'Performance Report' : '실적 보고서');

  // KPI block
  let totalKrw = 0, totalQty = 0, lineCount = rows.length;
  const invSet = new Set();
  for (const r of rows) {
    totalKrw += r.sales_amount_krw || 0;
    totalQty += r.qty || 0;
    if (r.invoice_no) invSet.add(r.invoice_no);
  }
  const orderCount = invSet.size;
  const avgOrderKrw = orderCount > 0 ? totalKrw / orderCount : 0;

  // YoY (year-over-year, current period vs same period prior year)
  // 현재 연도(YTD): cutoff 적용해 1-현재월까지만 비교 (apples-to-apples)
  // 과거 연도(이미 끝난 해): full year vs full year 비교
  const now = new Date();
  const lcm = (typeof lastCompletedMonth === 'function')
    ? lastCompletedMonth(now)
    : { year: now.getFullYear(), month: now.getMonth() };
  const targetYear = found.year || lcm.year;
  const isCurrentYear = targetYear === lcm.year;
  const cutoffMM = isCurrentYear ? String(lcm.month).padStart(2, '0') : '12';
  // 라벨: 현재 연도 → "1-N월 vs 동기" / 과거 연도 → "2025 vs 2024"
  const yoyLabelKo = isCurrentYear
    ? `${targetYear}년 1-${lcm.month}월 vs ${targetYear-1}년 동기`
    : `${targetYear}년 vs ${targetYear-1}년`;
  const yoyLabelEn = isCurrentYear
    ? `${targetYear} Jan-${lcm.month} vs ${targetYear-1} Jan-${lcm.month}`
    : `${targetYear} vs ${targetYear-1}`;
  let yoyThis = 0, yoyPrev = 0;
  // For YoY we need to reach beyond filtered rows (prior year may be filtered out)
  // Strategy: take applied filters EXCLUDING year, apply to allRows, then YoY-cap
  const _nonYearFilters = (r) => {
    if (found.channel  && !['all'].includes(found.channel) && !channelMatch(r, found.channel)) return false;
    if (found.country  && !sm(r.country, found.country)) return false;
    if (found.region   && !sm(r.region, found.region)) return false;
    if (found.make     && !sm(r.make, found.make)) return false;
    if (found.makeGrp  && !sm(r.make_group, found.makeGrp)) return false;
    if (found.model    && !sm(r.model, found.model)) return false;
    if (found.catGrp   && !sm(r.category_group, found.catGrp)) return false;
    if (found.category && !sm(r.category, found.category)) return false;
    return true;
  };
  for (const r of allRows) {
    if (!_nonYearFilters(r)) continue;
    const d = r.order_date || ''; if (!d) continue;
    const yr = parseInt(d.slice(0, 4), 10);
    const mm = d.slice(5, 7);
    if (mm > cutoffMM) continue;
    if (yr === targetYear)        yoyThis += r.sales_amount_krw || 0;
    else if (yr === targetYear-1) yoyPrev += r.sales_amount_krw || 0;
  }
  const yoyPct = yoyPrev > 0 ? (yoyThis - yoyPrev) / yoyPrev * 100 : null;

  // Monthly trend with prior-year overlay
  const monthlyMap = {};        // ym → krw (current period)
  const priorYearMap = {};      // ym → krw (one year before, same month)
  const monthlyQtyMap = {};     // ym → qty (current period)
  const priorYearQtyMap = {};   // ym → qty (one year before, same month)
  for (const r of rows) {
    const ym = (r.order_date || '').slice(0, 7);
    if (ym) {
      monthlyMap[ym]    = (monthlyMap[ym]    || 0) + (r.sales_amount_krw || 0);
      monthlyQtyMap[ym] = (monthlyQtyMap[ym] || 0) + (r.qty || 0);
    }
  }
  // For prior-year overlay: take all rows matching non-year filter, in (targetYear-1)
  const months = Object.keys(monthlyMap).sort();
  for (const ym of months) {
    const [y, m] = ym.split('-').map(Number);
    const prevYm = `${y - 1}-${String(m).padStart(2,'0')}`;
    priorYearMap[prevYm] = 0;
    priorYearQtyMap[prevYm] = 0;
  }
  for (const r of allRows) {
    if (!_nonYearFilters(r)) continue;
    const ym = (r.order_date || '').slice(0, 7);
    if (ym in priorYearMap) {
      priorYearMap[ym]    += r.sales_amount_krw || 0;
      priorYearQtyMap[ym] += r.qty || 0;
    }
  }

  // Breakdowns
  const topModels = aggGroup(rows, r => `${r.make ? r.make + ' ' : ''}${r.model || '?'}`).slice(0, 10);
  const topMakes  = aggGroup(rows, r => r.make || 'Other').slice(0, 8);
  const topCats   = aggGroup(rows, r => r.category_group || 'Other').slice(0, 6);
  const topCountries = aggGroup(rows, r => r.country || 'Other').slice(0, 8);

  // Channel breakdown
  const channelBreakdown = aggGroup(rows, r => r.channel_group || 'Other');

  // Auto insights — rule-based, no LLM needed
  const insights = [];
  if (yoyPct != null) {
    if (yoyPct >= 30) insights.push({ type:'good', text: lang === 'en'
      ? `Strong YoY growth: +${yoyPct.toFixed(1)}% vs prior year same period`
      : `전년 동기 대비 +${yoyPct.toFixed(1)}% — 강한 성장세` });
    else if (yoyPct <= -10) insights.push({ type:'risk', text: lang === 'en'
      ? `Declining YoY: ${yoyPct.toFixed(1)}% — investigate`
      : `전년 동기 대비 ${yoyPct.toFixed(1)}% 감소 — 원인 점검 필요` });
    else insights.push({ type:'neutral', text: lang === 'en'
      ? `YoY: ${yoyPct >= 0 ? '+' : ''}${yoyPct.toFixed(1)}% vs prior year same period`
      : `전년 동기 대비 ${yoyPct >= 0 ? '+' : ''}${yoyPct.toFixed(1)}%` });
  }
  // Top model concentration
  if (topModels.length > 0 && totalKrw > 0) {
    const topPct = topModels[0].krw / totalKrw * 100;
    if (topPct >= 25) insights.push({ type:'good', text: lang === 'en'
      ? `${topModels[0].label} drives ${topPct.toFixed(1)}% of revenue (concentrated)`
      : `${topModels[0].label}이 전체 매출의 ${topPct.toFixed(1)}% 차지 (집중도 높음)` });
  }
  // Top 3 model concentration
  if (topModels.length >= 3 && totalKrw > 0) {
    const top3Pct = (topModels[0].krw + topModels[1].krw + topModels[2].krw) / totalKrw * 100;
    insights.push({ type:'neutral', text: lang === 'en'
      ? `Top 3 models = ${top3Pct.toFixed(1)}% of revenue`
      : `Top 3 모델이 전체 매출의 ${top3Pct.toFixed(1)}%` });
  }
  // Channel skew
  if (channelBreakdown.length > 1 && totalKrw > 0) {
    const lead = channelBreakdown[0];
    const leadPct = lead.krw / totalKrw * 100;
    if (leadPct >= 60) insights.push({ type:'neutral', text: lang === 'en'
      ? `${lead.label} dominates: ${leadPct.toFixed(1)}% of revenue`
      : `${lead.label} 채널이 ${leadPct.toFixed(1)}% — 단일 채널 의존도 높음` });
  }

  // ── Breakout chart data: model & country YoY growth ──
  let modelBreakout = [], countryBreakout = [], newEntries = [];
  if (targetYear && months.length > 0) {
    const overallGrowth = yoyPct || 0;

    // Model-level YoY
    const modelYoY = _entityYoY(allRows, _nonYearFilters, targetYear, cutoffMM,
      r => `${r.make ? r.make + ' ' : ''}${r.model || '?'}`);
    // Top growers + decliners with meaningful volume (prev >= 50만 or curr >= 100만)
    const significantModels = modelYoY.filter(e => e.prev >= 500000 || e.curr >= 1000000);
    const growers = significantModels.filter(e => e.prev > 0 && e.growth > 20)
      .sort((a, b) => b.growth - a.growth).slice(0, 5);
    const decliners = significantModels.filter(e => e.prev > 0 && e.growth < -20)
      .sort((a, b) => a.growth - b.growth).slice(0, 3);
    modelBreakout = [...growers, ...decliners]
      .sort((a, b) => b.growth - a.growth);
    // Tag overall growth line for reference
    modelBreakout._overallGrowth = overallGrowth;

    // Country-level YoY
    const countryYoY = _entityYoY(allRows, _nonYearFilters, targetYear, cutoffMM,
      r => r.country || 'Other');
    const sigCountries = countryYoY.filter(e => e.prev >= 300000 || e.curr >= 500000);
    const cGrowers = sigCountries.filter(e => e.prev > 0 && e.growth > 20)
      .sort((a, b) => b.growth - a.growth).slice(0, 4);
    const cDecliners = sigCountries.filter(e => e.prev > 0 && e.growth < -20)
      .sort((a, b) => a.growth - b.growth).slice(0, 2);
    countryBreakout = [...cGrowers, ...cDecliners]
      .sort((a, b) => b.growth - a.growth);
    countryBreakout._overallGrowth = overallGrowth;

    // New entries — models/countries with zero prior year
    const newModels = modelYoY.filter(e => e.prev === 0 && e.curr >= 1000000)
      .sort((a, b) => b.curr - a.curr).slice(0, 4);
    const newCountriesArr = countryYoY.filter(e => e.prev === 0 && e.curr >= 500000)
      .sort((a, b) => b.curr - a.curr).slice(0, 3);
    newEntries = [
      ...newModels.map(e => ({ ...e, kind: 'model' })),
      ...newCountriesArr.map(e => ({ ...e, kind: 'country' })),
    ];
  }

  // Monthly peak / dip (keep as text insight — concise)
  if (months.length >= 3) {
    const monthEntries = months.map(ym => ({ ym, krw: monthlyMap[ym] || 0 }));
    const peak = monthEntries.reduce((a, b) => a.krw >= b.krw ? a : b);
    const dip  = monthEntries.reduce((a, b) => a.krw <= b.krw ? a : b);
    if (peak.ym !== dip.ym) {
      insights.push({ type:'neutral', text: lang === 'en'
        ? `Peak: ${peak.ym} (${_krwShort(peak.krw, lang)}) · Dip: ${dip.ym} (${_krwShort(dip.krw, lang)})`
        : `최고: ${peak.ym} (${_krwShort(peak.krw, lang)}) · 최저: ${dip.ym} (${_krwShort(dip.krw, lang)})` });
    }
  }

  return {
    headerTitle, filterStr,
    generatedAt: now.toISOString(),
    kpi: { totalKrw, totalQty, lineCount, orderCount, avgOrderKrw, yoyPct, yoyThis, yoyPrev, targetYear, cutoffMonth: lcm.month, isCurrentYear, yoyLabelKo, yoyLabelEn },
    monthlyMap, priorYearMap, monthlyQtyMap, priorYearQtyMap, months,
    topModels, topMakes, topCats, topCountries, channelBreakdown,
    insights, modelBreakout, countryBreakout, newEntries,
  };
}

// String-match helper for non-year filter reapplication in YoY/overlay calcs
function sm(rowVal, target) {
  const rv = normStr(rowVal); const tg = normStr(target);
  if (!rv) return false;
  return rv === tg || rv.includes(tg) || tg.includes(rv);
}
function channelMatch(r, ch) {
  if (['US','ROW','KR'].includes(ch)) return r.channel_group === ch;
  if (['B2B','B2C'].includes(ch))     return normStr(r.sales_channel) === normStr(ch);
  return false;
}
// Entity-level YoY: compute this-year vs prior-year totals for each entity
function _entityYoY(allRows, nonYearFilter, targetYear, cutoffMM, keyFn) {
  const curr = {}, prev = {};
  for (const r of allRows) {
    if (!nonYearFilter(r)) continue;
    const d = r.order_date || ''; if (!d) continue;
    const yr = parseInt(d.slice(0, 4), 10);
    const mm = d.slice(5, 7);
    if (mm > cutoffMM) continue;
    const k = keyFn(r);
    const krw = r.sales_amount_krw || 0;
    if (yr === targetYear)        curr[k] = (curr[k] || 0) + krw;
    else if (yr === targetYear-1) prev[k] = (prev[k] || 0) + krw;
  }
  const keys = new Set([...Object.keys(curr), ...Object.keys(prev)]);
  const result = [];
  for (const k of keys) {
    const c = curr[k] || 0, p = prev[k] || 0;
    const growth = p > 0 ? (c - p) / p * 100 : (c > 0 ? Infinity : 0);
    result.push({ label: k, curr: c, prev: p, growth: growth === Infinity ? 999 : growth });
  }
  return result;
}

// Short KRW formatter for insights text
function _krwShort(val, lang) {
  if (val >= 1e8) return `₩${(val / 1e8).toFixed(1)}억`;
  if (val >= 1e4) return `₩${Math.round(val / 1e4).toLocaleString()}만`;
  return `₩${Math.round(val).toLocaleString()}`;
}

function aggGroup(rows, keyFn) {
  const m = {};
  for (const r of rows) {
    const k = keyFn(r);
    if (!m[k]) m[k] = { label: k, krw: 0, qty: 0 };
    m[k].krw += r.sales_amount_krw || 0;
    m[k].qty += r.qty || 0;
  }
  return Object.values(m).sort((a, b) => b.krw - a.krw);
}

// Did-you-mean: tokenize query and find closest matches in dataIndex.
// Strategy:
//   1. Tokenize query (drop common stop-words like 매출/팔렸/얼마)
//   2. For each token (length ≥3), look for index keys that:
//      - share a prefix of length ≥ min(3, token.length)
//      - OR contain the token as a substring
//      - OR token contains the key as a substring (typo tolerance)
//   3. Score by overlap length, dedupe by canonical value, sort, take top 5
//   4. Tag each suggestion with its entity type for the chip label
function findSuggestions(q, dataIndex, mappings, found) {
  const STOP = new Set(['매출','얼마나','팔렸어','팔렸나','어땠어','어때','얼마','보여줘','정리해줘','보고서','revenue','sales','sold','show','tell','me','what','how','much']);
  // v3.4: 토큰화 + n-gram (2~3 단어 결합) — "GP Product" / "AUTO ID" 같은 멀티워드 회사명 매칭
  const rawTokens = q.split(/[\s,.]+/).filter(t => t.length >= 2 && !STOP.has(t));
  const tokens = rawTokens.filter(t => t.length >= 3);
  for (let i = 0; i < rawTokens.length - 1; i++) {
    tokens.push(rawTokens[i] + ' ' + rawTokens[i + 1]);
    tokens.push(rawTokens[i] + rawTokens[i + 1]);  // squashed (e.g. "auto id" → "autoid")
    if (i < rawTokens.length - 2) tokens.push(rawTokens[i] + ' ' + rawTokens[i+1] + ' ' + rawTokens[i+2]);
  }
  if (tokens.length === 0) return null;

  // 이미 매칭된 entity는 제외해서 중복 추천 방지
  const excludeValues = new Set();
  if (found) {
    ['model','make','makeGrp','category','catGrp','country','region','channel','company','wingType']
      .forEach(k => { if (found[k]) excludeValues.add(String(found[k]).toLowerCase()); });
  }

  const buckets = [
    { type: lang_label('model'),   map: dataIndex.models    || {} },
    { type: lang_label('make'),    map: dataIndex.makes     || {} },
    { type: lang_label('country'), map: dataIndex.countries || {} },
    { type: lang_label('region'),  map: dataIndex.regions   || {} },
    { type: lang_label('company'), map: dataIndex.companies || {} },  // v3.4
  ];

  const candidates = [];
  for (const tok of tokens) {
    for (const b of buckets) {
      for (const [k, v] of Object.entries(b.map)) {
        if (k.length < 2) continue;
        if (excludeValues.has(String(v).toLowerCase())) continue;
        let score = 0;
        if (k === tok) score = 1000;
        else if (k.startsWith(tok) || tok.startsWith(k)) score = 100 + Math.min(k.length, tok.length);
        else if (k.includes(tok)) score = 50 + tok.length;
        else if (tok.includes(k)) score = 30 + k.length;
        // 1-edit (오타 1글자)
        else if (Math.abs(k.length - tok.length) <= 1 && tok.length >= 4 && _oneEditAway(k, tok)) score = 60;
        // 2-edit (오타 2글자) for longer tokens
        else if (tok.length >= 5 && k.length >= 5 && Math.abs(k.length - tok.length) <= 2 && _editDistanceAtMost(k, tok, 2)) score = 40;
        if (score > 0) candidates.push({ value: v, type: b.type, score, token: tok });
      }
    }
  }
  if (candidates.length === 0) return null;
  // Dedupe by value, keep highest score
  const seen = {};
  for (const c of candidates) {
    if (!seen[c.value] || seen[c.value].score < c.score) seen[c.value] = c;
  }
  return Object.values(seen).sort((a, b) => b.score - a.score).slice(0, 5);
}

// Bounded Levenshtein — early-exits if distance exceeds maxD. Cheap for short strings.
function _editDistanceAtMost(a, b, maxD) {
  const la = a.length, lb = b.length;
  if (Math.abs(la - lb) > maxD) return false;
  let prev = new Array(lb + 1);
  let curr = new Array(lb + 1);
  for (let j = 0; j <= lb; j++) prev[j] = j;
  for (let i = 1; i <= la; i++) {
    curr[0] = i;
    let rowMin = curr[0];
    for (let j = 1; j <= lb; j++) {
      const cost = a[i-1] === b[j-1] ? 0 : 1;
      curr[j] = Math.min(prev[j] + 1, curr[j-1] + 1, prev[j-1] + cost);
      if (curr[j] < rowMin) rowMin = curr[j];
    }
    if (rowMin > maxD) return false;
    [prev, curr] = [curr, prev];
  }
  return prev[lb] <= maxD;
}

// Lightweight: returns true if str a and b differ by at most 1 character
// (insertion, deletion, or substitution). Good enough for short tokens
// where we don't need full Levenshtein.
function _oneEditAway(a, b) {
  if (a === b) return true;
  const la = a.length, lb = b.length;
  if (Math.abs(la - lb) > 1) return false;
  let i = 0, j = 0, edits = 0;
  while (i < la && j < lb) {
    if (a[i] !== b[j]) {
      if (++edits > 1) return false;
      if (la > lb) i++;
      else if (lb > la) j++;
      else { i++; j++; }
    } else { i++; j++; }
  }
  return true;
}

// Localized label for suggestion-chip. Uses module-scoped `_currentLang` set
// by QueryView via window.__queryLang so we don't need to pass through every call.
function lang_label(kind) {
  const ln = (typeof window !== 'undefined' && window.__queryLang === 'en') ? 'en' : 'ko';
  const M = {
    model:   { ko:'모델',   en:'Model'   },
    make:    { ko:'메이커', en:'Maker'   },
    country: { ko:'국가',   en:'Country' },
    region:  { ko:'지역',   en:'Region'  },
    company: { ko:'회사',   en:'Company' },
  };
  return M[kind] ? M[kind][ln] : kind;
}

// Build a crosstab: for each value of `dim`, compute total KRW + total QTY
// + top N models by KRW. Sorted by total KRW desc.
// innerN: 그룹당 표시할 상품 수 (default 3, "TOP 5 상품" 같은 표현으로 override).
function buildCrosstab(rows, dim, innerN) {
  const N = innerN || 3;
  const buckets = {};
  for (const r of rows) {
    const k = dim === 'year' ? (r.order_date || '').slice(0, 4) || '(blank)' : String(r[dim] || '(blank)');
    if (!buckets[k]) buckets[k] = { label:k, krw:0, qty:0, modelMap:{} };
    buckets[k].krw += r.sales_amount_krw || 0;
    buckets[k].qty += r.qty || 0;
    const mdl = (r.make ? r.make + ' ' : '') + (r.model || '?');
    if (!buckets[k].modelMap[mdl]) buckets[k].modelMap[mdl] = { krw:0, qty:0 };
    buckets[k].modelMap[mdl].krw += r.sales_amount_krw || 0;
    buckets[k].modelMap[mdl].qty += r.qty || 0;
  }
  const out = Object.values(buckets).map(b => {
    const top = Object.entries(b.modelMap)
      .map(([label, v]) => ({ label, krw:v.krw, qty:v.qty, pct: b.krw > 0 ? v.krw / b.krw * 100 : 0 }))
      .sort((a,c) => c.krw - a.krw)
      .slice(0, N);
    return { label:b.label, krw:b.krw, qty:b.qty, top };
  });
  return out.sort((a, b) => b.krw - a.krw);
}

// ── Insight engine: compute YoY change + share shift for each crosstab group ──
// Returns an array of { label, thisKrw, prevKrw, yoyPct, sharePct, shareChange, flag }
// flag: 'surge' | 'drop' | 'new' | 'normal'
function computeCrosstabInsights(crosstabData, allRows, filteredRows, dim, found, lang) {
  // Determine the reference period: use the filtered rows' date range
  const dates = filteredRows.map(r => r.order_date || '').filter(d => d).sort();
  if (dates.length === 0) return null;
  const minDate = dates[0], maxDate = dates[dates.length - 1];
  // Calculate a "previous period" of the same length shifted back
  const msRange = new Date(maxDate) - new Date(minDate);
  const dayRange = Math.max(1, Math.round(msRange / 86400000));
  const prevEnd = new Date(new Date(minDate).getTime() - 86400000); // day before minDate
  const prevStart = new Date(prevEnd.getTime() - msRange);
  const fmtD = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const prevFrom = fmtD(prevStart), prevTo = fmtD(prevEnd);

  // Build previous period data for same channel/make/etc filters (excluding date + dim)
  const channelFilter = found.channel;
  const prevRows = allRows.filter(r => {
    const d = r.order_date || '';
    if (d < prevFrom || d > prevTo) return false;
    if (channelFilter && !['all'].includes(channelFilter)) {
      if (['US','ROW','KR'].includes(channelFilter) && r.channel_group !== channelFilter) return false;
    }
    return true;
  });

  // Aggregate previous by dim
  const prevBuckets = {};
  for (const r of prevRows) {
    const k = dim === 'year' ? (r.order_date || '').slice(0, 4) : String(r[dim] || '(blank)');
    prevBuckets[k] = (prevBuckets[k] || 0) + (r.sales_amount_krw || 0);
  }

  const totalKrw = filteredRows.reduce((s, r) => s + (r.sales_amount_krw || 0), 0);
  const prevTotalKrw = prevRows.reduce((s, r) => s + (r.sales_amount_krw || 0), 0);

  const insights = crosstabData.map(g => {
    const prevKrw = prevBuckets[g.label] || 0;
    const yoyPct = prevKrw > 0 ? (g.krw - prevKrw) / prevKrw * 100 : null;
    const sharePct = totalKrw > 0 ? g.krw / totalKrw * 100 : 0;
    const prevSharePct = prevTotalKrw > 0 ? prevKrw / prevTotalKrw * 100 : 0;
    const shareChange = sharePct - prevSharePct;
    let flag = 'normal';
    if (prevKrw === 0 && g.krw > 0) flag = 'new';
    else if (yoyPct != null && yoyPct > 50) flag = 'surge';
    else if (yoyPct != null && yoyPct < -30) flag = 'drop';
    return { label: g.label, thisKrw: g.krw, prevKrw, yoyPct, sharePct, shareChange, flag };
  });
  // Sort: most notable first (surges, drops, new)
  const priority = { surge: 0, drop: 1, new: 2, normal: 3 };
  insights.sort((a, b) => (priority[a.flag] - priority[b.flag]) || Math.abs(b.yoyPct || 0) - Math.abs(a.yoyPct || 0));
  return { insights, periodLabel: `${minDate} ~ ${maxDate}`, prevLabel: `${prevFrom} ~ ${prevTo}`, dayRange };
}

// Lookup orders by invoice_no — matches if any candidate token is contained
// in (or contains) the row's invoice_no (case-insensitive). Returns rows
// grouped by invoice_no with order summary.
function lookupOrders(allRows, tokens) {
  const tokensLc = tokens.map(t => t.toLowerCase());
  const matched = allRows.filter(r => {
    const inv = String(r.invoice_no || '').toLowerCase();
    if (!inv) return false;
    return tokensLc.some(t => inv === t || inv.includes(t) || t.includes(inv));
  });
  if (matched.length === 0) return { tokens, orders: [], totalLines: 0 };

  // Group by invoice_no
  const byInv = {};
  for (const r of matched) {
    const inv = r.invoice_no;
    if (!byInv[inv]) byInv[inv] = {
      invoice_no: inv,
      order_date: r.order_date,
      fulfilled_date: r.fulfilled_date,
      channel_group: r.channel_group,
      country: r.country,
      company: r.company,
      sales_channel: r.sales_channel,
      currency: r.currency,
      lines: [],
      total_qty: 0,
      total_krw: 0,
      total_native: 0,
    };
    byInv[inv].lines.push(r);
    byInv[inv].total_qty += r.qty || 0;
    byInv[inv].total_krw += r.sales_amount_krw || 0;
    byInv[inv].total_native += r.sales_amount || 0;
  }
  const orders = Object.values(byInv).sort((a, b) =>
    (b.order_date || '').localeCompare(a.order_date || '')
  );
  return { tokens, orders, totalLines: matched.length };
}

function aggregateSummary(rows) {
  const s = { qty_US:0, qty_ROW:0, qty_KR:0, qty_total:0, krw_US:0, krw_ROW:0, krw_KR:0, krw_total:0 };
  for (const r of rows) {
    const qty = r.qty || 0; const krw = r.sales_amount_krw || 0; const ch = r.channel_group || 'Other';
    s.qty_total += qty; s.krw_total += krw;
    if (['US','ROW','KR'].includes(ch)) { s[`qty_${ch}`] += qty; s[`krw_${ch}`] += krw; }
  }
  return s;
}
function buildMonthlyQ(rows) {
  // krw 버킷(억 단위)과 qty 버킷(원자료)을 함께 산출 — 차트 매출/수량 토글용.
  const mm = {};   // ym → { US,ROW,KR } krw
  const mq = {};   // ym → { US,ROW,KR } qty
  for (const r of rows) {
    const ym = r.order_date?.slice(0,7); if (!ym) continue;
    const ch = r.channel_group || 'Other';
    if (!mm[ym]) mm[ym] = { US:0, ROW:0, KR:0 };
    if (!mq[ym]) mq[ym] = { US:0, ROW:0, KR:0 };
    mm[ym][ch] = (mm[ym][ch] || 0) + (r.sales_amount_krw || 0);
    mq[ym][ch] = (mq[ym][ch] || 0) + (r.qty || 0);
  }
  const months = Object.keys(mm).sort();
  return { months,
    byChannel: {
      US:  months.map(m => Math.round((mm[m].US  || 0) / 1e7) / 10),
      ROW: months.map(m => Math.round((mm[m].ROW || 0) / 1e7) / 10),
      KR:  months.map(m => Math.round((mm[m].KR  || 0) / 1e7) / 10),
    },
    byChannelQty: {
      US:  months.map(m => mq[m].US  || 0),
      ROW: months.map(m => mq[m].ROW || 0),
      KR:  months.map(m => mq[m].KR  || 0),
    },
  };
}
function groupByQ(rows, dim) {
  const map = {};
  for (const r of rows) {
    const k = String(r[dim] || '(blank)');
    if (!map[k]) map[k] = { label:k, krw:0, qty:0 };
    map[k].krw += r.sales_amount_krw || 0;
    map[k].qty += r.qty || 0;
  }
  return Object.values(map).sort((a,b) => b.krw - a.krw);
}
function computeYoYQ(rows, channelFilter) {
  const now = new Date(); const yr = now.getFullYear();
  const todayMD = `${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}`;
  let thisYr = 0, lastYr = 0;
  for (const r of rows) {
    const date = r.order_date || ''; if (!date) continue;
    if (channelFilter && ['US','ROW','KR'].includes(channelFilter) && r.channel_group !== channelFilter) continue;
    const rowYr = parseInt(date.slice(0,4),10); const md = date.slice(5,10);
    if (rowYr === yr     && md <= todayMD) thisYr += r.sales_amount_krw || 0;
    if (rowYr === yr - 1 && md <= todayMD) lastYr += r.sales_amount_krw || 0;
  }
  return { thisYr, lastYr, growth: lastYr > 0 ? (thisYr - lastYr) / lastYr * 100 : null, year: yr };
}
// Match a keyword from `map` inside `q`. Longest-match wins. For short or
// fully alphanumeric keys (e.g. "m3", "86", "g80") we require a word-boundary
// match so "1986" doesn't false-match "86" and "amg" doesn't shadow "m3".
// `q` is expected to be already normalised via normStr(); keys are normalised
// here so curated mappings with stray uppercase / extra whitespace still match.
function findInMap(q, map) {
  if (!map) return null;
  let best = null, bestLen = 0;
  // Tolerate stray spaces adjacent to dots ("Gen. 2" → "Gen.2") in user input.
  // normStr only collapses whitespace runs to a single space; the space next
  // to a dot would otherwise leave the key mismatched against the canonical
  // DB form. Try both variants — longest match across both wins.
  const qDots = q.replace(/\s*\.\s*/g, '.');
  const tryQs = qDots === q ? [q] : [q, qDots];
  for (const tq of tryQs) {
    for (const [k, v] of Object.entries(map)) {
      const kl = normStr(k);
      if (kl.length === 0) continue;
      let hit = false;
      if (kl.length <= 3 || /^[a-z0-9]+$/.test(kl)) {
        // Word-boundary match for short / alphanumeric keys.
        // \b doesn't always work cleanly with non-Latin scripts, so we test
        // boundary chars manually: anything not [a-z0-9] is a boundary.
        const re = new RegExp('(^|[^a-z0-9])' + kl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '($|[^a-z0-9])', 'i');
        hit = re.test(tq);
      } else {
        hit = tq.includes(kl);
      }
      if (hit && kl.length > bestLen) { best = v; bestLen = kl.length; }
    }
  }
  return best;
}
// Multi-model OR parser. Splits q on OR separators (과/와/,/+/&///그리고/및/또(는))
// and returns canonical model array (length ≥ 2) only when EVERY segment maps
// cleanly to a model. Otherwise returns [] so callers fall back to single-model
// mode — this is the false-positive guard for cases like "BMW와 관련된 매출".
// vs/대비 are handled by compare mode upstream and never reach here.
function extractModels(q, modelMap) {
  if (!modelMap) return [];
  // Order matters: longer/explicit separators first; particle-style 과/와 last.
  const SEP = /\s*,\s*|\s*\+\s*|\s*&\s*|\s*\/\s*|\s+그리고\s+|\s+및\s+|\s+또는\s+|\s+또\s+|\s*과\s+|\s*와\s+/;
  const segments = q.split(SEP).map(s => s.trim()).filter(Boolean);
  if (segments.length < 2) return [];
  const matches = [];
  const seen = new Set();
  for (const seg of segments) {
    // Try direct match, then fallback with dot-space collapsed:
    // "Gen. 2" → "Gen.2" matches DB raw value when user added a stray space.
    // normStr only collapses whitespace runs, not the space adjacent to a dot.
    let m = findInMap(seg, modelMap);
    if (!m) m = findInMap(seg.replace(/\s*\.\s*/g, '.'), modelMap);
    if (!m) return [];
    if (!seen.has(m)) { seen.add(m); matches.push(m); }
  }
  return matches.length >= 2 ? matches : [];
}
function extractYear(q) {
  // Explicit 4-digit year
  const m = q.match(/20(2[0-9])/);
  if (m) return parseInt(m[0],10);
  // Relative year keywords
  const now = new Date();
  if (/올해|this\s*year/i.test(q))  return now.getFullYear();
  if (/작년|지난해|last\s*year/i.test(q)) return now.getFullYear() - 1;
  if (/재작년|two\s*years\s*ago/i.test(q)) return now.getFullYear() - 2;
  return null;
}
function extractMonth(q) {
  const iso = q.match(/20\d{2}-\d{2}/); if (iso) return iso[0];
  const now = new Date();
  // Korean relative month
  if (/이번\s*달|이달|this\s*month/i.test(q)) {
    return `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`;
  }
  if (/저번\s*달|지난\s*달|전월|last\s*month/i.test(q)) {
    const d = new Date(now.getFullYear(), now.getMonth() - 1, 1);
    return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
  }
  const kr = q.match(/(\d{1,2})월/);
  if (kr) { const yr = extractYear(q) || new Date().getFullYear(); return `${yr}-${String(parseInt(kr[1],10)).padStart(2,'0')}`; }
  return null;
}
function extractTopN(q) { const m = q.match(/top\s*(\d+)|상위\s*(\d+)/i); return m ? parseInt(m[1]||m[2],10) : null; }

// "TOP N" 직후 어휘로 그룹 N vs 내부 N 구분.
//   "ROW 국가별 TOP 5 상품"        → innerN=5 (그룹당 상품 5개)
//   "TOP 5 국가별 매출"             → groupN=5 (그룹 5개)
//   "TOP 5"만 있으면 둘 다 null → 호출부에서 fallback(extractTopN) 사용
function extractInnerTopN(q) {
  const m = q.match(/(?:top|상위)\s*(\d+)\s*(?:개\s*)?(?:상품|모델|아이템|products?|models?|items?)/i);
  return m ? parseInt(m[1], 10) : null;
}
function extractGroupTopN(q) {
  const m = q.match(/(?:top|상위)\s*(\d+)\s*(?:국가|나라|채널|메이커|메이크|브랜드|카테고리|모델|연도|년도)별?/i)
        || q.match(/top\s*(\d+)\s*(?:by\s*(?:country|channel|maker|make|brand|category|model|year))/i);
  return m ? parseInt(m[1], 10) : null;
}

// Extract a YYYY-MM-DD..YYYY-MM-DD date range from relative time references.
// Returns null if no relative-range pattern matched, otherwise { from, to }.
// Patterns:
//   "지난 N개월" / "최근 N개월" / "last N months"  → N months ending today
//   "지난 N주" / "최근 N주" / "last N weeks"        → N weeks ending today
//   "Q1/Q2/Q3/Q4" + optional year                  → that quarter's months
//   "1분기/2분기/3분기/4분기" + optional year       → 한국식 분기
//   "상반기/하반기"                                  → H1/H2 of year
function extractDateRange(q) {
  const now = new Date();
  const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  const pad2 = (n) => String(n).padStart(2, '0');
  const lastDayOf = (y, m) => new Date(y, m, 0).getDate();
  // 구분자 — "부터/에서 ~ 까지", "~", "-", "–", "—", "to", "through"
  // 캡처 그룹 안 만들도록 non-capturing.
  const SEP = String.raw`(?:\s*(?:부터|에서)?\s*(?:~|–|—|-|to|through)\s*|\s*부터\s*)`;

  // 1) 한국어 월 단위 range — "YYYY년 M월부터 YYYY년 M월까지"
  //    두 번째 연도 생략 가능 → 첫 번째와 같은 연도. "까지" 생략 허용.
  //    예: "2025년 4월부터 2026년 3월까지", "2025년 4월~6월"
  const krMonthRange = q.match(
    new RegExp(String.raw`(\d{4})\s*년\s*(\d{1,2})\s*월${SEP}(?:(\d{4})\s*년\s*)?(\d{1,2})\s*월(?:\s*까지)?`)
  );
  if (krMonthRange) {
    const y1 = parseInt(krMonthRange[1], 10);
    const m1 = parseInt(krMonthRange[2], 10);
    const y2 = krMonthRange[3] ? parseInt(krMonthRange[3], 10) : y1;
    const m2 = parseInt(krMonthRange[4], 10);
    if (m1 >= 1 && m1 <= 12 && m2 >= 1 && m2 <= 12) {
      const from = `${y1}-${pad2(m1)}-01`;
      const to   = `${y2}-${pad2(m2)}-${pad2(lastDayOf(y2, m2))}`;
      const label = y1 === y2
        ? `${y1}-${pad2(m1)} ~ ${pad2(m2)}`
        : `${y1}-${pad2(m1)} ~ ${y2}-${pad2(m2)}`;
      return { from, to, label };
    }
  }

  // 2) ISO range — "YYYY-MM(-DD)? 부터/~ YYYY-MM(-DD)? 까지"
  //    예: "2025-04부터 2026-03까지", "2025-04-01 ~ 2025-06-30"
  const isoRange = q.match(
    new RegExp(String.raw`(\d{4})-(\d{1,2})(?:-(\d{1,2}))?${SEP}(\d{4})-(\d{1,2})(?:-(\d{1,2}))?(?:\s*까지)?`)
  );
  if (isoRange) {
    const y1 = parseInt(isoRange[1], 10);
    const m1 = parseInt(isoRange[2], 10);
    const d1 = isoRange[3] ? parseInt(isoRange[3], 10) : 1;
    const y2 = parseInt(isoRange[4], 10);
    const m2 = parseInt(isoRange[5], 10);
    const d2 = isoRange[6] ? parseInt(isoRange[6], 10) : lastDayOf(y2, m2);
    if (m1 >= 1 && m1 <= 12 && m2 >= 1 && m2 <= 12) {
      const from = `${y1}-${pad2(m1)}-${pad2(d1)}`;
      const to   = `${y2}-${pad2(m2)}-${pad2(d2)}`;
      return { from, to, label: `${from} ~ ${to}` };
    }
  }

  // 3) 한국어 연도-only range — "YYYY년부터 YYYY년까지"
  //    예: "2024년부터 2026년까지" → 3년 전체
  const krYearRange = q.match(
    new RegExp(String.raw`(\d{4})\s*년${SEP}(\d{4})\s*년(?:\s*까지)?`)
  );
  if (krYearRange) {
    const y1 = parseInt(krYearRange[1], 10);
    const y2 = parseInt(krYearRange[2], 10);
    return {
      from: `${y1}-01-01`,
      to:   `${y2}-12-31`,
      label: y1 === y2 ? `${y1}` : `${y1} ~ ${y2}`,
    };
  }

  // 지난/최근 N개월 (N months back from today)
  const monthsBack = q.match(/(?:지난|최근|past|last)\s*(\d{1,2})\s*(?:개월|month)/i);
  if (monthsBack) {
    const n = parseInt(monthsBack[1], 10);
    const from = new Date(now.getFullYear(), now.getMonth() - n, now.getDate());
    return { from: fmt(from), to: fmt(now), label: `${lang_label_kr_or('지난 '+n+'개월', 'last '+n+' months')}` };
  }
  // 지난/최근 N주 (N weeks back)
  const weeksBack = q.match(/(?:지난|최근|past|last)\s*(\d{1,2})\s*(?:주|week)/i);
  if (weeksBack) {
    const n = parseInt(weeksBack[1], 10);
    const from = new Date(now); from.setDate(now.getDate() - n * 7);
    return { from: fmt(from), to: fmt(now), label: `${lang_label_kr_or('지난 '+n+'주', 'last '+n+' weeks')}` };
  }
  // 지난/최근 N년 (N years back — "지난 2년", "last 3 years")
  const yearsBack = q.match(/(?:지난|최근|past|last)\s*(\d{1,2})\s*(?:년|year)/i);
  if (yearsBack) {
    const n = parseInt(yearsBack[1], 10);
    const from = new Date(now.getFullYear() - n, now.getMonth(), now.getDate());
    return { from: fmt(from), to: fmt(now), label: `${lang_label_kr_or('지난 '+n+'년', 'last '+n+' years')}` };
  }

  // 이번 분기 / 지난 분기 / this quarter / last quarter
  const thisQ = /이번\s*분기|this\s*quarter|금분기/i.test(q);
  const lastQ = /지난\s*분기|전\s*분기|last\s*quarter|prev(ious)?\s*quarter/i.test(q);
  if (thisQ || lastQ) {
    const refMonth = lastQ ? now.getMonth() - 3 : now.getMonth();
    const refDate = new Date(now.getFullYear(), refMonth, 1);
    const qn = Math.floor(refDate.getMonth() / 3) + 1;
    const yr = refDate.getFullYear();
    const startM = (qn - 1) * 3 + 1, endM = qn * 3;
    const from = `${yr}-${String(startM).padStart(2,'0')}-01`;
    const lastDay = new Date(yr, endM, 0).getDate();
    const to = `${yr}-${String(endM).padStart(2,'0')}-${String(lastDay).padStart(2,'0')}`;
    return { from, to, label: `${yr} Q${qn}` };
  }

  // Q1/Q2/Q3/Q4 or 1분기~4분기
  const qMatch = q.match(/q\s*([1-4])|([1-4])\s*분기/i);
  if (qMatch) {
    const qn = parseInt(qMatch[1] || qMatch[2], 10);
    const yr = extractYear(q) || now.getFullYear();
    const startM = (qn - 1) * 3 + 1, endM = qn * 3;
    const from = `${yr}-${String(startM).padStart(2,'0')}-01`;
    const lastDay = new Date(yr, endM, 0).getDate();
    const to = `${yr}-${String(endM).padStart(2,'0')}-${String(lastDay).padStart(2,'0')}`;
    return { from, to, label: `${yr} Q${qn}` };
  }
  // 상반기/하반기 (H1/H2)
  const halfMatch = q.match(/상반기|하반기|h1|h2|first\s*half|second\s*half/i);
  if (halfMatch) {
    const hi = /상반|h1|first/i.test(halfMatch[0]) ? 1 : 2;
    const yr = extractYear(q) || now.getFullYear();
    const from = `${yr}-${hi === 1 ? '01' : '07'}-01`;
    const to   = `${yr}-${hi === 1 ? '06-30' : '12-31'}`;
    return { from, to, label: `${yr} H${hi}` };
  }

  // 올해 / 작년 / 재작년 as date range — only when no explicit 4-digit year is present
  // (e.g. "올해" → YTD, but "2026 올해" should let the explicit year win)
  const hasExplicit4y = /20[2-9]\d/.test(q);
  if (/올해|금년|this\s*year/i.test(q) && !hasExplicit4y) {
    const yr = now.getFullYear();
    return { from: `${yr}-01-01`, to: fmt(now), label: `${yr} YTD` };
  }
  if (/작년|지난해|전년|last\s*year/i.test(q) && !hasExplicit4y) {
    const yr = now.getFullYear() - 1;
    return { from: `${yr}-01-01`, to: `${yr}-12-31`, label: `${yr}` };
  }
  if (/재작년|two\s*years?\s*ago|2년\s*전/i.test(q) && !hasExplicit4y) {
    const yr = now.getFullYear() - 2;
    return { from: `${yr}-01-01`, to: `${yr}-12-31`, label: `${yr}` };
  }

  return null;
}
function lang_label_kr_or(ko, en) {
  const ln = (typeof window !== 'undefined' && window.__queryLang === 'en') ? 'en' : 'ko';
  return ln === 'en' ? en : ko;
}

// Query log — persists to sessionStorage (max 100 entries) and prints to
// console. This is groundwork for Phase 4 LLM integration: the log captures
// the queries users actually try (especially the ones that returned 0 rows
// or no entity matches), which will inform prompt design and template gaps.
// Inspect from the browser console:
//   JSON.parse(sessionStorage.getItem('adro.query.log') || '[]')
const QUERY_LOG_KEY = 'adro.query.log';
const QUERY_LOG_MAX = 100;
function logQuery(input, result) {
  const entry = {
    ts: new Date().toISOString(),
    input,
    rowCount: result?.rowCount ?? null,
    appliedFilters: (result?.appliedFilters || []).map(f => `${f.label}:${f.value}`),
    matched: {
      crosstab: !!result?.crosstabData,
      orderLookup: !!result?.orderData,
      yoy: !!result?.yoyData,
      share: !!result?.shareData,
      grouped: !!result?.groupedResult,
    },
    suggested: (result?.suggestions || []).map(s => s.value),
    isEmpty: !!result?.isEmpty,
    error: result?.error || null,
  };
  // eslint-disable-next-line no-console
  console.log('[query.log]', entry);
  try {
    const existing = JSON.parse(sessionStorage.getItem(QUERY_LOG_KEY) || '[]');
    existing.push(entry);
    if (existing.length > QUERY_LOG_MAX) existing.splice(0, existing.length - QUERY_LOG_MAX);
    sessionStorage.setItem(QUERY_LOG_KEY, JSON.stringify(existing));
  } catch (_) { /* sessionStorage may be unavailable; console fallback is fine */ }
}

// --- Result component ----------------------------------------------------
function QueryResult({ result, lineRef, lang, t, trendMetric, onTrendMetricChange, onSuggestionClick, onCrosstabCellClick }) {
  if (result.isEmpty) {
    return (
      <div style={{ ...qCardS, padding:20 }}>
        <div style={{ textAlign:'center', color:'var(--fg-muted)', fontSize:13, marginBottom: result.suggestions ? 16 : 0 }}>
          {lang === 'en' ? 'No matching rows found.' : '조건에 맞는 데이터가 없습니다.'}
        </div>
        {result.suggestions && result.suggestions.length > 0 && (
          <div>
            <div style={{ fontSize:11, color:'var(--fg-muted)', marginBottom:8, textAlign:'center' }}>
              {lang === 'en' ? 'Did you mean…' : '혹시 이거 말씀이세요?'}
            </div>
            <div style={{ display:'flex', gap:6, flexWrap:'wrap', justifyContent:'center' }}>
              {result.suggestions.map((s, i) => (
                <button key={i} onClick={() => onSuggestionClick && onSuggestionClick(s.value)} style={{
                  padding:'6px 12px', fontSize:12, borderRadius:20,
                  background:'var(--accent-soft)', color:'var(--accent)',
                  border:'1px solid var(--accent)', cursor:'pointer',
                  display:'inline-flex', alignItems:'center', gap:6,
                }}>
                  <span style={{ fontWeight:600 }}>{s.value}</span>
                  <span style={{ fontSize:10, opacity:.7 }}>{s.type}</span>
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }
  // ── Compare mode ────────────────────────────────────────────────
  if (result.compareData) {
    const { labelA, labelB, a, b, bucket } = result.compareData;
    const sA = a.summary || {}, sB = b.summary || {};
    const metrics = [
      { key: lang === 'en' ? 'Revenue' : '매출', vA: sA.krw_total || 0, vB: sB.krw_total || 0, fmt: qKrw },
      { key: lang === 'en' ? 'Units' : '판매량', vA: sA.qty_total || 0, vB: sB.qty_total || 0, fmt: v => v.toLocaleString() },
      { key: lang === 'en' ? 'Rows' : '행 수', vA: a.rowCount || 0, vB: b.rowCount || 0, fmt: v => v.toLocaleString() },
    ];
    const sideHeader = (label, color) => (
      <div style={{ fontSize:11, fontWeight:700, color, textTransform:'uppercase', letterSpacing:.04, marginBottom:6 }}>{label}</div>
    );
    return (
      <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
        {/* Headline comparison summary table */}
        <div style={qCardS}>
          <div style={{ fontSize:13, fontWeight:700, marginBottom:14, textAlign:'center' }}>
            <span style={{ color:'#007AFF' }}>{labelA}</span>
            {' vs '}
            <span style={{ color:'#FF9500' }}>{labelB}</span>
          </div>
          <table style={{ width:'100%', borderCollapse:'collapse', fontSize:13 }}>
            <thead><tr style={{ borderBottom:'2px solid var(--border)' }}>
              <th style={{ padding:'6px 8px', textAlign:'left', color:'var(--fg-muted)', fontSize:11 }}></th>
              <th style={{ padding:'6px 8px', textAlign:'right', color:'#007AFF', fontWeight:700 }}>{labelA}</th>
              <th style={{ padding:'6px 8px', textAlign:'right', color:'#FF9500', fontWeight:700 }}>{labelB}</th>
              <th style={{ padding:'6px 8px', textAlign:'right', color:'var(--fg-muted)', fontSize:11 }}>{lang === 'en' ? 'Diff' : '차이'}</th>
            </tr></thead>
            <tbody>{metrics.map(m => {
              const diff = m.vA - m.vB;
              const pct = m.vB > 0 ? ((m.vA - m.vB) / m.vB * 100) : null;
              return (
                <tr key={m.key} style={{ borderBottom:'1px solid var(--divider)' }}>
                  <td style={{ padding:'6px 8px', fontWeight:600, color:'var(--fg-muted)', fontSize:12 }}>{m.key}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right', fontWeight:600 }}>{m.fmt(m.vA)}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right', fontWeight:600 }}>{m.fmt(m.vB)}</td>
                  <td style={{ padding:'6px 8px', textAlign:'right', fontSize:12,
                    color: diff > 0 ? '#34C759' : diff < 0 ? '#FF453A' : 'var(--fg-muted)' }}>
                    {pct != null ? `${diff > 0 ? '+' : ''}${pct.toFixed(1)}%` : '—'}
                  </td>
                </tr>
              );
            })}</tbody>
          </table>
        </div>

        {/* A side — KPI cards (+ crosstab if "파츠별" etc was applied) */}
        <div>
          {sideHeader(labelA, '#007AFF')}
          <QKpiCards summary={sA} appliedFilters={a.appliedFilters} lang={lang}/>
          {a.crosstabData && a.crosstabData.length > 0 && (
            <div style={{ marginTop:10 }}>
              <CrosstabView data={a.crosstabData} dim={a.crosstabDim} lang={lang}/>
            </div>
          )}
        </div>

        {/* B side — KPI cards (+ crosstab) */}
        <div>
          {sideHeader(labelB, '#FF9500')}
          <QKpiCards summary={sB} appliedFilters={b.appliedFilters} lang={lang}/>
          {b.crosstabData && b.crosstabData.length > 0 && (
            <div style={{ marginTop:10 }}>
              <CrosstabView data={b.crosstabData} dim={b.crosstabDim} lang={lang}/>
            </div>
          )}
        </div>

        {/* Trend chart + bucketed data table (defaults to monthly) */}
        {(a.monthly?.months?.length > 0 || b.monthly?.months?.length > 0) && (
          <CompareTrendCard a={a} b={b} labelA={labelA} labelB={labelB} bucket={bucket || 'month'} lang={lang}/>
        )}

        {/* Per-side matched orders — MatchedOrdersTable already has its own
            collapse toggle (default closed), so we just label each block. */}
        {a.matchedRows?.length > 0 && (
          <div>
            {sideHeader(labelA, '#007AFF')}
            <MatchedOrdersTable rows={a.matchedRows} total={a.matchedTotal} lang={lang}/>
          </div>
        )}
        {b.matchedRows?.length > 0 && (
          <div>
            {sideHeader(labelB, '#FF9500')}
            <MatchedOrdersTable rows={b.matchedRows} total={b.matchedTotal} lang={lang}/>
          </div>
        )}
      </div>
    );
  }

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {/* v3.4: 결과 위 fuzzy 추천 칩 — "혹시 ___?" */}
      {result.suggestions && result.suggestions.length > 0 && (
        <div style={{
          padding:'8px 12px', borderRadius:8,
          background:'var(--surface-2)', border:'1px solid var(--border)',
          display:'flex', alignItems:'center', gap:8, flexWrap:'wrap',
        }}>
          <span style={{ fontSize:11, color:'var(--fg-muted)' }}>
            {lang === 'en' ? 'Did you mean' : '혹시 이거 말씀이세요?'}
          </span>
          {result.suggestions.slice(0, 3).map((s, i) => (
            <button key={i} onClick={() => onSuggestionClick && onSuggestionClick(s.value)} style={{
              padding:'3px 10px', fontSize:11.5, borderRadius:14,
              background:'var(--accent-soft)', color:'var(--accent)',
              border:'1px solid var(--accent)', cursor:'pointer',
              display:'inline-flex', alignItems:'center', gap:6,
            }}>
              <span style={{ fontWeight:600 }}>{s.value}</span>
              <span style={{ fontSize:10, opacity:.7 }}>{s.type}</span>
            </button>
          ))}
        </div>
      )}
      {result.appliedFilters.length > 0 && (
        <div style={{ display:'flex', gap:6, flexWrap:'wrap', alignItems:'center' }}>
          <span style={{ fontSize:11, color:'var(--fg-muted)' }}>{t.filtered}:</span>
          {result.appliedFilters.map((f,i) => (
            <span key={i} style={{ padding:'2px 8px', fontSize:11, borderRadius:20, background:'var(--accent-soft)', color:'var(--accent)', border:'1px solid var(--accent)' }}>
              {f.label}: {f.value}
            </span>
          ))}
          <span style={{ fontSize:11, color:'var(--fg-muted)' }}>({result.rowCount.toLocaleString()} rows)</span>
        </div>
      )}

      {/* Report mode — comprehensive multi-section view */}
      {result.reportData && <ReportView report={result.reportData} lang={lang}/>}

      {/* Order lookup mode — when invoice_no detected in query */}
      {result.orderData && <OrderLookupView orderData={result.orderData} lang={lang}/>}

      {/* Crosstab mode — "국가별로 뭐가 잘 팔려" → per-country cards w/ top 3 */}
      {result.crosstabData && result.crosstabData.length > 0 && (
        <CrosstabView data={result.crosstabData} dim={result.crosstabDim} lang={lang} onCellClick={onCrosstabCellClick}/>
      )}

      {/* Insight / anomaly annotations — "특이사항", "트렌드" */}
      {result.insightData && result.insightData.insights && result.insightData.insights.length > 0 && (
        <InsightView data={result.insightData} lang={lang}/>
      )}

      {result.yoyData && (
        <div style={{ display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:12 }}>
          {[
            { label:`${result.yoyData.year} YTD`, v: result.yoyData.thisYr },
            { label:`${result.yoyData.year-1} YTD`, v: result.yoyData.lastYr },
            { label:'YoY', v:null, pct: result.yoyData.growth },
          ].map((k,i) => (
            <div key={i} style={qCardS}>
              <div style={{ fontSize:11, color:'var(--fg-muted)', fontWeight:600 }}>{k.label}</div>
              <div style={{ fontSize:22, fontWeight:700, marginTop:6,
                color: k.pct != null ? (k.pct >= 0 ? '#34C759':'#FF453A'):'var(--fg)' }}>
                {k.pct != null ? (k.pct >= 0 ? '+' : '') + k.pct.toFixed(1) + '%' : qKrw(k.v)}
              </div>
            </div>
          ))}
        </div>
      )}

      {result.shareData && (
        <div style={qCardS}>
          <div style={{ fontSize:12, color:'var(--fg-muted)', marginBottom:6 }}>{lang === 'en' ? 'Revenue Share' : '매출 비중'}</div>
          <div style={{ fontSize:28, fontWeight:700, color:'var(--accent)' }}>{result.shareData.pct.toFixed(1)}%</div>
          <div style={{ fontSize:12, color:'var(--fg-muted)', marginTop:4 }}>
            {qKrw(result.shareData.filteredKrw)} / {qKrw(result.shareData.totalKrw)} total
          </div>
          <div style={{ background:'var(--surface-2)', borderRadius:99, height:6, overflow:'hidden', marginTop:8 }}>
            <div style={{ width: Math.min(result.shareData.pct,100) + '%', height:'100%', background:'var(--accent)', borderRadius:99 }}/>
          </div>
        </div>
      )}

      {!result.yoyData && (
        <QKpiCards summary={result.summary} appliedFilters={result.appliedFilters} lang={lang}/>
      )}

      {result.groupedResult?.length > 0 && (() => {
        const isAsp = result.groupSortMode === 'asp';
        // Friendly column header for the leftmost column. raw dim names like
        // "PRODUCT_NAME" read awkwardly — translate into 카테고리/상품/모델.
        const dimLabelMap = lang === 'en'
          ? { category: 'Category', product_name: 'Product', model: 'Model', make: 'Maker', country: 'Country' }
          : { category: '카테고리', product_name: '상품', model: '모델', make: '메이커', country: '국가' };
        const dimLabel = dimLabelMap[result.groupDim] || (result.groupDim || '').toUpperCase();
        const cols = isAsp
          ? ['', lang === 'en' ? 'ASP' : '객단가', 'KRW', 'QTY']
          : ['', 'KRW', 'QTY'];
        return (
        <div style={qCardS}>
          <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:8, textTransform:'uppercase', letterSpacing:.04 }}>
            {isAsp
              ? (lang === 'en' ? 'Unit Economics (sorted by ASP)' : '객단가 분석 (높은 순)')
              : (dimLabel + ' ' + (lang === 'en' ? 'breakdown' : '별 분석'))}
          </div>
          <table style={{ width:'100%', borderCollapse:'collapse', fontSize:12 }}>
            <thead><tr style={{ borderBottom:'1px solid var(--border)' }}>
              {cols.map(h => <th key={h} style={{ padding:'5px 8px', textAlign: h ? 'right':'left', color:'var(--fg-muted)', fontWeight:600, fontSize:10.5, textTransform:'uppercase' }}>{h || dimLabel}</th>)}
            </tr></thead>
            <tbody>
              {result.groupedResult.map((g,i) => (
                <tr key={i} style={{ borderBottom:'1px solid var(--divider)' }}>
                  <td style={{ padding:'5px 8px', fontWeight: i < 3 ? 600 : 400 }}>{g.label}</td>
                  {isAsp && <td style={{ padding:'5px 8px', textAlign:'right', fontWeight:600, color:'var(--accent)' }}>{qKrw(g.asp)}</td>}
                  <td style={{ padding:'5px 8px', textAlign:'right' }}>{qKrw(g.krw)}</td>
                  <td style={{ padding:'5px 8px', textAlign:'right', color:'var(--fg-muted)' }}>{g.qty.toLocaleString()}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        );
      })()}

      {/* Drill-down trend — sub-category/product lines over time. Renders only
          when the current breakdown is by category or product_name. Sits next
          to the breakdown table so user sees "what" + "trend" together. */}
      {result.groupedTrend && result.groupedTrend.months.length > 1 && (
        <GroupedTrendChart data={result.groupedTrend} dim={result.groupDim} lang={lang}/>
      )}

      {/* v3.3: 매칭된 주문 리스트 — 차트 앞으로 (스크롤 단축).
          일반 검색 모드에서만 (orderData/report 모드 제외) */}
      {result.matchedRows && result.matchedRows.length > 0 && !result.orderData && !result.reportData && (
        <MatchedOrdersTable
          rows={result.matchedRows}
          total={result.matchedTotal}
          lang={lang}
        />
      )}

      {result.monthly?.months?.length > 1 && (
        <QTrendCard lineRef={lineRef} lang={lang}
          metric={trendMetric} onMetricChange={onTrendMetricChange}/>
      )}
    </div>
  );
}

// Reusable KPI cards block — extracted from inline IIFE so compare mode can
// render two of these (one per side). Adaptive: only renders channels that
// have rows + always shows Total. Geo-aware label: when a country/region
// filter is active and only one channel has data, swap "ROW" → e.g. "Germany".
function QKpiCards({ summary, appliedFilters, lang }) {
  const channelKeys = ['US','ROW','KR'];
  const visible = channelKeys.filter(ch =>
    (summary[`qty_${ch}`] || 0) > 0 || (summary[`krw_${ch}`] || 0) > 0
  );
  const geoFilter = (appliedFilters || []).find(f =>
    f.label === '국가' || f.label === 'Country' || f.label === '지역' || f.label === 'Region'
  );
  const useGeoLabel = !!geoFilter && visible.length === 1;
  const cards = [
    ...visible.map(ch => ({
      label: useGeoLabel ? geoFilter.value : ch,
      sublabel: useGeoLabel ? `${ch} ${lang === 'en' ? 'channel' : '채널'}` : null,
      krw: summary[`krw_${ch}`] || 0,
      qty: summary[`qty_${ch}`] || 0,
      primary: false,
    })),
    { label: lang === 'en' ? 'Total' : '합계', krw: summary.krw_total || 0, qty: summary.qty_total || 0, primary: true },
  ];
  return (
    <div style={{ display:'grid', gridTemplateColumns:`repeat(${cards.length},1fr)`, gap:10 }}>
      {cards.map(c => (
        <div key={c.label} style={{ ...qCardS, background: c.primary ? 'var(--accent-soft)' : undefined }}>
          <div style={{ fontSize:11, color:'var(--fg-muted)', fontWeight:600, textTransform:'uppercase', letterSpacing:.04 }}>{c.label}</div>
          {c.sublabel && (
            <div style={{ fontSize:10, color:'var(--fg-subtle)', marginTop:1 }}>{c.sublabel}</div>
          )}
          <div style={{ display:'flex', flexDirection:'column', gap:6, marginTop:8 }}>
            <div>
              <div style={{ fontSize:18, fontWeight:700, lineHeight:1.1 }}>{qKrw(c.krw)}</div>
              <div style={{ fontSize:10, color:'var(--fg-muted)', marginTop:2, textTransform:'uppercase', letterSpacing:.04 }}>{lang === 'en' ? 'Revenue' : '매출'}</div>
            </div>
            <div>
              <div style={{ fontSize:18, fontWeight:700, lineHeight:1.1, color:'var(--fg-2)' }}>
                {c.qty > 0 ? c.qty.toLocaleString() : '—'}
              </div>
              <div style={{ fontSize:10, color:'var(--fg-muted)', marginTop:2, textTransform:'uppercase', letterSpacing:.04 }}>{lang === 'en' ? 'Units' : '판매량'}</div>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

// Compare-mode trend chart + companion data table. Aggregates each side's
// monthly array into bucket (month/quarter/year) and renders 2 lines (A/B
// totals). Independent of QTrendCard (single-mode) so the two coexist —
// QueryView's chart useEffect only fires when result.monthly exists,
// which compare results don't have.
function CompareTrendCard({ a, b, labelA, labelB, bucket, lang }) {
  const ref = React.useRef(null);
  const chartRef = React.useRef(null);
  const [size, setSize] = React.useState('M');
  // 이 차트 전용 매출액/수량 토글 — 다른 차트와 독립.
  const [metric, setMetric] = React.useState('krw');
  const heights = { S: 140, M: 200, L: 320 };
  const data = React.useMemo(
    () => buildCompareTrend(a.monthly, b.monthly, bucket, metric),
    [a, b, bucket, metric]
  );
  React.useEffect(() => {
    if (!ref.current || data.labels.length === 0) return;
    if (chartRef.current) chartRef.current.destroy();
    const cfg = qMetricCfg(metric, lang);
    const fgMuted = getComputedStyle(document.documentElement).getPropertyValue('--fg-muted').trim();
    const gridC = 'rgba(128,128,128,0.12)';
    chartRef.current = new Chart(ref.current, {
      type: 'line',
      data: {
        labels: data.labels,
        datasets: [
          { label: labelA, data: data.totalA, borderColor:'#007AFF', backgroundColor:'#007AFF22', borderWidth:2, pointRadius:2, tension:.3, fill:false },
          { label: labelB, data: data.totalB, borderColor:'#FF9500', backgroundColor:'#FF950022', borderWidth:2, pointRadius:2, tension:.3, fill:false },
        ],
      },
      options: {
        responsive: true, maintainAspectRatio: false,
        interaction: { mode:'index', intersect:false },
        plugins: {
          legend: { labels: { color:fgMuted, font:{ size:11 }, boxWidth:12 } },
          tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${cfg.fmt(ctx.raw)}` } },
        },
        scales: {
          x: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 } } },
          y: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 }, callback: cfg.axisTick } },
        },
      },
    });
    return () => { if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } };
  }, [data, labelA, labelB, metric, lang]);

  const bucketTitle = bucket === 'year' ? (lang === 'en' ? 'Yearly trend' : '연도별 추이')
    : bucket === 'quarter' ? (lang === 'en' ? 'Quarterly trend' : '분기별 추이')
    : (lang === 'en' ? 'Monthly trend' : '월별 추이');

  return (
    <div style={qCardS}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:8 }}>
        <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.04 }}>
          {bucketTitle}
        </div>
        <div style={{ display:'flex', gap:8, alignItems:'center' }}>
          <MetricToggle value={metric} onChange={setMetric} lang={lang}/>
          <div style={{ display:'flex', gap:4 }}>
            {['S','M','L'].map(s => (
              <button key={s} onClick={() => setSize(s)} style={{
                padding:'2px 8px', fontSize:10, borderRadius:4, cursor:'pointer',
                background: size === s ? 'var(--accent-soft)' : 'transparent',
                color: size === s ? 'var(--accent)' : 'var(--fg-muted)',
                border:'1px solid var(--border)',
              }}>{s}</button>
            ))}
          </div>
        </div>
      </div>
      <div style={{ height: heights[size] }}>
        <canvas ref={ref}/>
      </div>
      {data.labels.length > 0 && (
        <details style={{ marginTop:10 }}>
          <summary style={{ fontSize:11, color:'var(--fg-muted)', cursor:'pointer' }}>
            {lang === 'en' ? 'Show data table' : '데이터 표 보기'}
          </summary>
          <table style={{ width:'100%', borderCollapse:'collapse', fontSize:11.5, marginTop:8 }}>
            <thead><tr style={{ borderBottom:'1px solid var(--border)' }}>
              <th style={{ padding:'4px 6px', textAlign:'left', color:'var(--fg-muted)' }}>{bucket === 'year' ? (lang === 'en' ? 'Year' : '연도') : bucket === 'quarter' ? (lang === 'en' ? 'Quarter' : '분기') : (lang === 'en' ? 'Month' : '월')}</th>
              <th style={{ padding:'4px 6px', textAlign:'right', color:'#007AFF', fontWeight:700 }}>{labelA}</th>
              <th style={{ padding:'4px 6px', textAlign:'right', color:'#FF9500', fontWeight:700 }}>{labelB}</th>
              <th style={{ padding:'4px 6px', textAlign:'right', color:'var(--fg-muted)' }}>{lang === 'en' ? 'Diff' : '차이'}</th>
            </tr></thead>
            <tbody>
              {data.labels.map((lbl, i) => {
                const va = data.totalA[i], vb = data.totalB[i];
                const pct = vb > 0 ? (va - vb) / vb * 100 : null;
                return (
                  <tr key={lbl} style={{ borderBottom:'1px solid var(--divider)' }}>
                    <td style={{ padding:'4px 6px' }}>{lbl}</td>
                    <td style={{ padding:'4px 6px', textAlign:'right' }}>{metric === 'qty' ? Math.round(va).toLocaleString() : va.toFixed(1) + '억'}</td>
                    <td style={{ padding:'4px 6px', textAlign:'right' }}>{metric === 'qty' ? Math.round(vb).toLocaleString() : vb.toFixed(1) + '억'}</td>
                    <td style={{ padding:'4px 6px', textAlign:'right',
                      color: pct == null ? 'var(--fg-muted)' : pct > 0 ? '#34C759' : pct < 0 ? '#FF453A' : 'var(--fg-muted)' }}>
                      {pct == null ? '—' : `${pct > 0 ? '+' : ''}${pct.toFixed(1)}%`}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </details>
      )}
    </div>
  );
}

// Strip crosstab trigger words (별-keywords) from the original query and
// append the clicked cell label so the next search drills into that group.
// Example: "BMW G82 M4 파츠별" + "WING" → "BMW G82 M4 WING".
// findInMap handles the cell label whether it's a country/channel/maker/
// category/model/year — they all share the same lookup pipeline.
function buildDrillDownQuery(input, cellLabel) {
  const STRIP = /\s*(?:국가별|나라별|by\s*country|each\s*country|per\s*country|채널별|by\s*channel|메이커별|메이크별|브랜드별|by\s*make|by\s*brand|sku별|by\s*sku|상품별|제품별|아이템별|by\s*product|by\s*item|카테고리별|cat별|파츠별|부품별|파트별|by\s*category|by\s*parts?|모델별|by\s*model|연도별|년도별|by\s*year)\s*/gi;
  const stripped = input.replace(STRIP, ' ').replace(/\s+/g, ' ').trim();
  return stripped ? `${stripped} ${cellLabel}` : cellLabel;
}

// For a given dim and a list of top labels, build a per-label monthly series
// in 억(KRW/1e8) units. Used by GroupedTrendChart so the result page can show
// a multi-line trend alongside the breakdown table (e.g. CARBON PARTS →
// REAR DIFFUSER / SIDE SKIRTS / SWAN NECK WING ... over time).
function buildGroupedTrend(rows, dim, topLabels) {
  if (!topLabels || topLabels.length === 0) return null;
  const monthSet = new Set();
  const map = {};    // label → ym → krw
  const mapQ = {};   // label → ym → qty
  for (const lbl of topLabels) { map[lbl] = {}; mapQ[lbl] = {}; }
  for (const r of rows) {
    const lbl = String(r[dim] || '(blank)');
    if (!(lbl in map)) continue;
    const ym = r.order_date?.slice(0, 7);
    if (!ym) continue;
    monthSet.add(ym);
    map[lbl][ym]  = (map[lbl][ym]  || 0) + (r.sales_amount_krw || 0);
    mapQ[lbl][ym] = (mapQ[lbl][ym] || 0) + (r.qty || 0);
  }
  const months = Array.from(monthSet).sort();
  // 각 series는 krw(억)·qty(원자료) 데이터를 함께 보유 — 차트 토글용.
  return {
    months,
    series: topLabels.map(lbl => ({
      label: lbl,
      data:    months.map(m => Math.round((map[lbl][m] || 0) / 1e7) / 10),
      dataQty: months.map(m => mapQ[lbl][m] || 0),
    })),
  };
}

// Aggregate two monthly series into bucketed totals (combined channels).
// Returns parallel arrays so the chart and table stay in sync.
// metric: 'krw'(억) | 'qty'(원자료) — 차트 매출/수량 토글용. 기본 'krw'.
function buildCompareTrend(monthlyA, monthlyB, bucket, metric) {
  const field = metric === 'qty' ? 'byChannelQty' : 'byChannel';
  const sumOf = (m) => {
    if (!m || !m.months) return {};
    const out = {};
    m.months.forEach((mo, i) => {
      const ch = m[field] || {};
      out[mo] = (ch.US?.[i] || 0) + (ch.ROW?.[i] || 0) + (ch.KR?.[i] || 0);
    });
    return out;
  };
  const ma = sumOf(monthlyA), mb = sumOf(monthlyB);
  const allMonths = Array.from(new Set([...Object.keys(ma), ...Object.keys(mb)])).sort();
  const bucketKey = (mo) => {
    if (bucket === 'year')    return mo.slice(0, 4);
    if (bucket === 'quarter') {
      const yr = mo.slice(0, 4);
      const m = parseInt(mo.slice(5, 7), 10);
      return `${yr}-Q${Math.ceil(m / 3)}`;
    }
    return mo;
  };
  const buckets = {};
  for (const mo of allMonths) {
    const k = bucketKey(mo);
    if (!buckets[k]) buckets[k] = { a: 0, b: 0 };
    buckets[k].a += ma[mo] || 0;
    buckets[k].b += mb[mo] || 0;
  }
  const labels = Object.keys(buckets).sort();
  return {
    labels,
    totalA: labels.map(k => buckets[k].a),
    totalB: labels.map(k => buckets[k].b),
  };
}

// Multi-line monthly trend for category/product drill-downs. Renders one line
// per top entry (max 6) so the user sees which sub-categories are growing
// vs. flat over time. Independent of QTrendCard which handles channel lines.
function GroupedTrendChart({ data, dim, lang }) {
  const ref = React.useRef(null);
  const chartRef = React.useRef(null);
  const [size, setSize] = React.useState('M');
  // 이 차트 전용 매출액/수량 토글 — 다른 차트와 독립.
  const [metric, setMetric] = React.useState('krw');
  const heights = { S: 140, M: 200, L: 320 };
  React.useEffect(() => {
    if (!ref.current || !data || !data.months.length) return;
    if (chartRef.current) chartRef.current.destroy();
    const cfg = qMetricCfg(metric, lang);
    const palette = ['#007AFF', '#FF9500', '#34C759', '#AF52DE', '#FF2D55', '#5856D6'];
    const fgMuted = getComputedStyle(document.documentElement).getPropertyValue('--fg-muted').trim();
    const gridC = 'rgba(128,128,128,0.12)';
    chartRef.current = new Chart(ref.current, {
      type: 'line',
      data: {
        labels: data.months,
        datasets: data.series.map((s, i) => ({
          label: s.label,
          data: metric === 'qty' ? (s.dataQty || []) : s.data,
          borderColor: palette[i % palette.length],
          backgroundColor: palette[i % palette.length] + '22',
          borderWidth: 2, pointRadius: 1.5, tension: .3, fill: false,
        })),
      },
      options: {
        responsive: true, maintainAspectRatio: false,
        interaction: { mode: 'index', intersect: false },
        plugins: {
          legend: { labels: { color: fgMuted, font: { size: 11 }, boxWidth: 12 } },
          tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${cfg.fmt(ctx.raw)}` } },
        },
        scales: {
          x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 } } },
          y: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: cfg.axisTick } },
        },
      },
    });
    return () => { if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } };
  }, [data, metric, lang]);

  const title = dim === 'category' ? (lang === 'en' ? 'Category trend (top 6)' : '카테고리별 추이 (상위 6)')
    : dim === 'product_name' ? (lang === 'en' ? 'Product trend (top 6)' : '상품별 추이 (상위 6)')
    : `${dim} trend`;

  return (
    <div style={qCardS}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--fg-muted)', textTransform: 'uppercase', letterSpacing: .04 }}>
          {title}
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <MetricToggle value={metric} onChange={setMetric} lang={lang}/>
          <div style={{ display: 'flex', gap: 4 }}>
            {['S', 'M', 'L'].map(s => (
              <button key={s} onClick={() => setSize(s)} style={{
                padding: '2px 8px', fontSize: 10, borderRadius: 4, cursor: 'pointer',
                background: size === s ? 'var(--accent-soft)' : 'transparent',
                color: size === s ? 'var(--accent)' : 'var(--fg-muted)',
                border: '1px solid var(--border)',
              }}>{s}</button>
            ))}
          </div>
        </div>
      </div>
      <div style={{ height: heights[size] }}>
        <canvas ref={ref}/>
      </div>
    </div>
  );
}

// v3.3: 월별 추이 차트 카드 — 높이 S/M/L 토글 (이름 prefix 'Q' 사용 — 다른 view의 ChartCard와 충돌 회피)
// metric/onMetricChange: 매출액/수량 토글 — 실제 차트 재생성은 QueryView의 effect가 담당.
function QTrendCard({ lineRef, lang, metric, onMetricChange }) {
  const [size, setSize] = React.useState('M');
  const heights = { S: 140, M: 200, L: 320 };
  const sizeBtnStyle = (active) => ({
    padding:'2px 8px', fontSize:11, fontWeight:600, borderRadius:4, cursor:'pointer',
    background: active ? 'var(--accent-soft)' : 'var(--surface-2)',
    color:      active ? 'var(--accent)'      : 'var(--fg-muted)',
    border:     `1px solid ${active ? 'var(--accent)' : 'var(--border)'}`,
  });
  return (
    <div style={qCardS}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:10 }}>
        <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.04 }}>
          {lang === 'en' ? 'Monthly Trend' : '월별 추이'}
        </div>
        <div style={{ display:'flex', gap:8, alignItems:'center' }}>
          {onMetricChange && (
            <MetricToggle value={metric || 'krw'} onChange={onMetricChange} lang={lang}/>
          )}
        <div style={{ display:'flex', gap:4 }}>
          {['S','M','L'].map(s => (
            <button key={s} onClick={() => setSize(s)} style={sizeBtnStyle(size === s)}
              title={s === 'S' ? (lang === 'en' ? 'Small' : '작게')
                  : s === 'M' ? (lang === 'en' ? 'Medium' : '보통')
                              : (lang === 'en' ? 'Large' : '크게')}>{s}</button>
          ))}
        </div>
        </div>
      </div>
      <div style={{ position:'relative', height:heights[size], transition:'height 0.2s ease' }}>
        <canvas ref={lineRef} id="q-line"/>
      </div>
    </div>
  );
}

// v3.3: 검색 결과 매칭 주문 리스트 — 펼침 토글 + CSV export
function MatchedOrdersTable({ rows, total, lang }) {
  const [expanded, setExpanded] = React.useState(false);

  // CSV 내보내기 — 표시되는 행 그대로
  function exportRowsCsv() {
    const headers = ['invoice_no','order_date','channel_group','company','make','model','category_group','category','sku_raw','product_name','qty','sales_amount_krw'];
    const lines = [headers.join(',')];
    for (const r of rows) {
      const cells = headers.map(h => {
        const v = r[h];
        if (v == null) return '';
        const s = String(v);
        return /[,"\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
      });
      lines.push(cells.join(','));
    }
    const csv = lines.join('\n');
    const blob = new Blob(['﻿' + csv], { type: 'text/csv;charset=utf-8;' });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement('a');
    a.href = url;
    a.download = 'query-orders-' + new Date().toISOString().slice(0,10) + '.csv';
    a.click();
    URL.revokeObjectURL(url);
  }

  const cellS = { padding:'5px 8px', whiteSpace:'nowrap', color:'var(--fg-2)', fontSize:11.5 };
  const cols = [
    lang === 'en' ? 'Invoice'  : '주문번호',
    lang === 'en' ? 'Date'     : '날짜',
    lang === 'en' ? 'Market'   : '시장',
    'Company',
    'Make',
    'Model',
    'Cat',
    'SKU',
    lang === 'en' ? 'Product'  : '제품명',
    'QTY',
    'KRW',
  ];
  return (
    <div style={qCardS}>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: expanded ? 8 : 0, gap:8 }}>
        <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.04 }}>
          {lang === 'en' ? 'Matched orders' : '매칭된 주문'}
        </div>
        <div style={{ display:'flex', gap:6 }}>
          {expanded && (
            <button onClick={exportRowsCsv} style={{
              padding:'4px 10px', fontSize:11.5, fontWeight:500, borderRadius:6, cursor:'pointer',
              background:'var(--surface-2)', color:'var(--fg-2)', border:'1px solid var(--border)',
            }}>
              ⬇ CSV
            </button>
          )}
          <button onClick={() => setExpanded(e => !e)} style={{
            padding:'4px 12px', fontSize:11.5, fontWeight:500, borderRadius:6, cursor:'pointer',
            background: expanded ? 'var(--accent-soft)' : 'var(--surface-2)',
            color:      expanded ? 'var(--accent)'      : 'var(--fg-2)',
            border:     `1px solid ${expanded ? 'var(--accent)' : 'var(--border)'}`,
          }}>
            {expanded
              ? (lang === 'en' ? `▴ Hide list` : `▴ 접기`)
              : (lang === 'en'
                  ? `▾ Show ${Math.min(total, 200).toLocaleString()} of ${total.toLocaleString()}`
                  : `▾ 주문별 보기 (${total.toLocaleString()}건${total > 200 ? ', 최대 200건' : ''})`)
            }
          </button>
        </div>
      </div>
      {expanded && (
        <div style={{ overflow:'auto', maxHeight:420, marginTop:6, border:'1px solid var(--border)', borderRadius:8 }}>
          <table style={{ width:'100%', borderCollapse:'collapse' }}>
            <thead>
              <tr style={{ background:'var(--surface-2)' }}>
                {cols.map(h => (
                  <th key={h} style={{
                    padding:'6px 8px', textAlign: (h === 'QTY' || h === 'KRW') ? 'right' : 'left',
                    fontWeight:700, color:'var(--fg-muted)', fontSize:10, textTransform:'uppercase',
                    letterSpacing:.04, whiteSpace:'nowrap',
                    position:'sticky', top:0, zIndex:1, background:'var(--surface-2)',
                    borderBottom:'1px solid var(--border)',
                  }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.map((r, i) => (
                <tr key={i} style={{ borderBottom:'1px solid var(--divider)' }}>
                  <td style={cellS}>{r.invoice_no || '—'}</td>
                  <td style={cellS}>{r.order_date || '—'}</td>
                  <td style={cellS}>{r.channel_group || '—'}</td>
                  <td style={cellS}>{r.company || '—'}</td>
                  <td style={cellS}>{r.make || '—'}</td>
                  <td style={cellS}>{r.model || '—'}</td>
                  <td style={cellS}>{r.category_group || '—'}</td>
                  <td style={cellS}>{r.sku_raw || '—'}</td>
                  <td style={{ ...cellS, maxWidth:240, overflow:'hidden', textOverflow:'ellipsis' }} title={r.product_name}>{r.product_name || '—'}</td>
                  <td style={{ ...cellS, textAlign:'right' }}>{(r.qty || 0).toLocaleString()}</td>
                  <td style={{ ...cellS, textAlign:'right', fontWeight:600 }}>{qKrw(r.sales_amount_krw)}</td>
                </tr>
              ))}
              {total > rows.length && (
                <tr><td colSpan={cols.length} style={{
                  padding:8, fontSize:11, color:'var(--fg-muted)',
                  textAlign:'center', fontStyle:'italic',
                }}>
                  {lang === 'en'
                    ? `... and ${(total - rows.length).toLocaleString()} more (use filters to narrow)`
                    : `... 외 ${(total - rows.length).toLocaleString()}건 더 (필터로 좁히세요)`
                  }
                </td></tr>
              )}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

function qKrw(n) {
  if (n == null || n === undefined) return '—';
  if (n === 0) 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별 표시 설정 — 막대·라인 공용 (Overview의 ovMetricCfg와 동일 패턴).
//  axisTick: 축 눈금 포맷, fmt: 툴팁 값 포맷.
//  krw 차트는 dataset 값을 억 단위로 넣고, qty 차트는 원자료를 그대로 넣는다.
function qMetricCfg(metric, lang) {
  if (metric === 'qty') {
    return {
      label: lang === 'en' ? 'Units' : '수량',
      axisTick: v => Number(v).toLocaleString(),
      fmt: v => Number(v).toLocaleString() + (lang === 'en' ? '' : '개'),
    };
  }
  return {
    label: lang === 'en' ? 'Revenue' : '매출액',
    axisTick: v => (Math.round(v * 10) / 10).toLocaleString() + '억',  // float 오차 제거 + 1자리 반올림
    fmt: v => qKrw(v),
  };
}
const qCardS = { background:'var(--bg-solid)', border:'1px solid var(--border)', borderRadius:12, padding:14, boxShadow:'var(--shadow-1)' };

// ─── Crosstab view (per-group cards w/ top 3 inside) ─────────────────────
// Renders 2-3 column responsive grid of cards. Each card shows the group
// total + a mini ranked list of the top 3 contributing models.
function CrosstabView({ data, dim, lang, onCellClick }) {
  const [hoveredIdx, setHoveredIdx] = React.useState(-1);
  const dimLabel = {
    country:        lang === 'en' ? 'Country' : '국가',
    channel_group:  lang === 'en' ? 'Channel' : '채널',
    make:           lang === 'en' ? 'Maker'   : '메이커',
    category_group: lang === 'en' ? 'Category Group' : '카테고리 그룹',
    model:          lang === 'en' ? 'Model'   : '모델',
    year:           lang === 'en' ? 'Year'    : '연도',
  }[dim] || dim;
  const clickable = !!onCellClick;
  return (
    <div style={qCardS}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:12 }}>
        <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.04 }}>
          {(lang === 'en' ? 'Best sellers by ' : '') + dimLabel + (lang === 'en' ? '' : '별 효자상품')}
        </div>
        {clickable && (
          <div style={{ fontSize:10.5, color:'var(--fg-subtle)' }}>
            {lang === 'en' ? 'Click a card to drill down' : '카드 클릭 시 세부 분석'}
          </div>
        )}
      </div>
      <div style={{
        display:'grid',
        gridTemplateColumns:'repeat(auto-fill, minmax(260px, 1fr))',
        gap:12,
      }}>
        {data.map((b, i) => (
          <div key={b.label + i}
            onClick={clickable ? () => onCellClick(b.label) : undefined}
            onMouseEnter={clickable ? () => setHoveredIdx(i) : undefined}
            onMouseLeave={clickable ? () => setHoveredIdx(-1) : undefined}
            style={{
              background:'var(--surface-2)',
              border:`1px solid ${hoveredIdx === i ? 'var(--accent)' : 'var(--border)'}`,
              borderRadius:10,
              padding:12,
              cursor: clickable ? 'pointer' : 'default',
              transform: hoveredIdx === i ? 'translateY(-1px)' : 'none',
              transition: 'transform 120ms, border-color 120ms, box-shadow 120ms',
              boxShadow: hoveredIdx === i ? '0 2px 8px rgba(0,122,255,0.12)' : 'none',
            }}>
            <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:8 }}>
              <div style={{ fontSize:13, fontWeight:700, color:'var(--fg)' }}>{b.label}</div>
              <div style={{ fontSize:11, color:'var(--fg-muted)' }}>{b.qty.toLocaleString()} {lang === 'en' ? 'units' : '개'}</div>
            </div>
            <div style={{ fontSize:16, fontWeight:700, color:'var(--accent)', marginBottom:10 }}>{qKrw(b.krw)}</div>
            {b.top.length > 0 ? (
              <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
                {b.top.map((m, j) => (
                  <div key={m.label + j} style={{ display:'flex', alignItems:'center', gap:8 }}>
                    <div style={{
                      width:18, height:18, borderRadius:5,
                      background: j === 0 ? 'var(--accent)' : 'var(--surface-3, var(--border))',
                      color: j === 0 ? '#fff' : 'var(--fg-muted)',
                      display:'flex', alignItems:'center', justifyContent:'center',
                      fontSize:10, fontWeight:700, flexShrink:0,
                    }}>{j+1}</div>
                    <div style={{ flex:1, minWidth:0 }}>
                      <div style={{ fontSize:11.5, color:'var(--fg)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }} title={m.label}>{m.label}</div>
                      <div style={{ fontSize:10, color:'var(--fg-muted)' }}>
                        {qKrw(m.krw)} · {m.pct.toFixed(1)}%
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            ) : (
              <div style={{ fontSize:11, color:'var(--fg-muted)' }}>—</div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Insight view — YoY change + share shift per group ──────────────────
function InsightView({ data, lang }) {
  const { insights, periodLabel, prevLabel } = data;
  const flagEmoji = { surge: '📈', drop: '📉', new: '🆕', normal: '' };
  const flagLabel = {
    surge: lang === 'en' ? 'Surge' : '급증',
    drop: lang === 'en' ? 'Drop' : '급감',
    new: lang === 'en' ? 'New' : '신규',
    normal: '',
  };
  const flagColor = { surge: '#34C759', drop: '#FF453A', new: '#007AFF', normal: 'var(--fg-muted)' };

  return (
    <div style={qCardS}>
      <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:4, textTransform:'uppercase', letterSpacing:.04 }}>
        {lang === 'en' ? 'Insights & Anomalies' : '특이사항 분석'}
      </div>
      <div style={{ fontSize:10, color:'var(--fg-muted)', marginBottom:12 }}>
        {lang === 'en' ? 'Current' : '현재'}: {periodLabel}
        {' · '}
        {lang === 'en' ? 'Previous' : '이전'}: {prevLabel}
      </div>
      <table style={{ width:'100%', borderCollapse:'collapse', fontSize:12 }}>
        <thead><tr style={{ borderBottom:'2px solid var(--border)' }}>
          <th style={{ padding:'5px 8px', textAlign:'left', fontSize:10, color:'var(--fg-muted)' }}></th>
          <th style={{ padding:'5px 8px', textAlign:'left', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Group' : '그룹'}</th>
          <th style={{ padding:'5px 8px', textAlign:'right', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Current' : '현재'}</th>
          <th style={{ padding:'5px 8px', textAlign:'right', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Previous' : '이전'}</th>
          <th style={{ padding:'5px 8px', textAlign:'right', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Change' : '변화율'}</th>
          <th style={{ padding:'5px 8px', textAlign:'right', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Share' : '비중'}</th>
          <th style={{ padding:'5px 8px', textAlign:'right', fontSize:10, color:'var(--fg-muted)' }}>{lang === 'en' ? 'Share Δ' : '비중변화'}</th>
        </tr></thead>
        <tbody>{insights.map((ins, i) => (
          <tr key={i} style={{ borderBottom:'1px solid var(--divider)', background: ins.flag !== 'normal' ? (flagColor[ins.flag] + '08') : undefined }}>
            <td style={{ padding:'5px 8px', fontSize:13 }}>{flagEmoji[ins.flag]}</td>
            <td style={{ padding:'5px 8px', fontWeight:600 }}>{ins.label}
              {ins.flag !== 'normal' && <span style={{ fontSize:10, marginLeft:6, color:flagColor[ins.flag], fontWeight:700 }}>{flagLabel[ins.flag]}</span>}
            </td>
            <td style={{ padding:'5px 8px', textAlign:'right' }}>{qKrw(ins.thisKrw)}</td>
            <td style={{ padding:'5px 8px', textAlign:'right', color:'var(--fg-muted)' }}>{qKrw(ins.prevKrw)}</td>
            <td style={{ padding:'5px 8px', textAlign:'right', fontWeight:600,
              color: ins.yoyPct != null ? (ins.yoyPct > 0 ? '#34C759' : ins.yoyPct < 0 ? '#FF453A' : 'var(--fg-muted)') : 'var(--fg-muted)' }}>
              {ins.yoyPct != null ? `${ins.yoyPct > 0 ? '+' : ''}${ins.yoyPct.toFixed(1)}%` : (ins.flag === 'new' ? 'NEW' : '—')}
            </td>
            <td style={{ padding:'5px 8px', textAlign:'right' }}>{ins.sharePct.toFixed(1)}%</td>
            <td style={{ padding:'5px 8px', textAlign:'right', fontSize:11,
              color: ins.shareChange > 1 ? '#34C759' : ins.shareChange < -1 ? '#FF453A' : 'var(--fg-muted)' }}>
              {ins.shareChange > 0 ? '+' : ''}{ins.shareChange.toFixed(1)}p
            </td>
          </tr>
        ))}</tbody>
      </table>
    </div>
  );
}

// ─── Report view (comprehensive multi-section report) ────────────────────
// 8 sections rendered in order: Header / Exec Summary / KPI / Monthly trend
// (with prior-year overlay) / Top Models / Top Makes / Top Categories /
// Country breakdown (when multi-country) / Auto Insights
function ReportView({ report, lang }) {
  const r = report;
  const trendRef = React.useRef(null);
  const trendChart = React.useRef(null);
  // 보고서 월별 추이 차트 전용 매출액/수량 토글 — 다른 차트와 독립.
  const [trendMetric, setTrendMetric] = React.useState('krw');

  // Build & destroy the line chart for monthly trend.
  // deps에 trendMetric 포함 — 매출/수량 토글 시 이 차트만 재생성.
  React.useEffect(() => {
    if (!trendRef.current || r.months.length === 0) return;
    if (trendChart.current) { try { trendChart.current.destroy(); } catch(_){}; trendChart.current = null; }

    const cfg = qMetricCfg(trendMetric, lang);
    const fgMuted = getComputedStyle(document.documentElement).getPropertyValue('--fg-muted').trim() || '#999';
    const accent  = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#FF5000';
    const gridC   = 'rgba(128,128,128,0.12)';

    // Build prior-year overlay aligned to current months.
    // krw는 억 단위, qty는 원자료 — metric에 따라 분기.
    const isQty = trendMetric === 'qty';
    const curMap   = isQty ? (r.monthlyQtyMap   || {}) : r.monthlyMap;
    const priorMap = isQty ? (r.priorYearQtyMap || {}) : r.priorYearMap;
    const scale = (v) => isQty ? (v || 0) : Math.round((v || 0) / 1e7) / 10;
    const priorVals = r.months.map(ym => {
      const [y, m] = ym.split('-').map(Number);
      const py = `${y - 1}-${String(m).padStart(2, '0')}`;
      return scale(priorMap[py]);
    });
    const currVals = r.months.map(ym => scale(curMap[ym]));

    trendChart.current = new Chart(trendRef.current, {
      type: 'line',
      data: {
        labels: r.months,
        datasets: [
          {
            label: lang === 'en' ? 'Current period' : '현재',
            data: currVals,
            borderColor: accent,
            backgroundColor: accent + '22',
            borderWidth: 2.5,
            tension: .3,
            fill: false,
            pointRadius: 3,
          },
          {
            label: lang === 'en' ? 'Same period prior year' : '전년 동기',
            data: priorVals,
            borderColor: fgMuted,
            backgroundColor: fgMuted + '11',
            borderWidth: 1.5,
            borderDash: [4, 4],
            tension: .3,
            fill: false,
            pointRadius: 2,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        interaction: { mode:'index', intersect:false },
        plugins: {
          legend: { labels: { color:fgMuted, font:{ size:11 }, boxWidth:12 } },
          tooltip: { callbacks: { label: ctx => ` ${ctx.dataset.label}: ${cfg.fmt(ctx.raw)}` } },
        },
        scales: {
          x: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 } } },
          y: { grid:{ color:gridC }, ticks:{ color:fgMuted, font:{ size:10 }, callback: cfg.axisTick } },
        },
      },
    });
    return () => { if (trendChart.current) { try { trendChart.current.destroy(); } catch(_){}; trendChart.current = null; } };
  }, [r, lang, trendMetric]);

  function copyMarkdown() {
    const md = reportToMarkdown(r, lang);
    if (navigator?.clipboard?.writeText) {
      navigator.clipboard.writeText(md).then(() => {
        // Quick feedback — could replace with toast later
        console.log('[report] copied to clipboard:', md.length, 'chars');
      });
    } else {
      console.log('[report] markdown:', md);
    }
  }

  const yoyColor = r.kpi.yoyPct == null ? 'var(--fg-muted)' : (r.kpi.yoyPct >= 0 ? '#34C759' : '#FF453A');

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
      {/* Header */}
      <div style={{ ...qCardS, background:'var(--accent-soft)', borderColor:'var(--accent)' }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap:12 }}>
          <div>
            <div style={{ fontSize:11, color:'var(--accent)', fontWeight:700, textTransform:'uppercase', letterSpacing:.06 }}>
              📊 {lang === 'en' ? 'Auto Report' : '자동 보고서'}
            </div>
            <div style={{ fontSize:18, fontWeight:700, marginTop:4, color:'var(--fg)' }}>{r.headerTitle}</div>
            <div style={{ fontSize:12, color:'var(--fg-2)', marginTop:6 }}>{r.filterStr}</div>
            <div style={{ fontSize:10.5, color:'var(--fg-muted)', marginTop:4 }}>
              {lang === 'en' ? 'Generated: ' : '생성: '}{r.generatedAt.slice(0, 16).replace('T', ' ')}
            </div>
          </div>
          <button onClick={copyMarkdown} style={{
            padding:'8px 14px', fontSize:12, fontWeight:600, borderRadius:8,
            background:'var(--accent)', color:'#fff', border:'none', cursor:'pointer',
            flexShrink:0,
          }}>
            📋 {lang === 'en' ? 'Copy as Markdown' : 'Markdown 복사'}
          </button>
        </div>
      </div>

      {/* KPI 4종 */}
      <div style={{ display:'grid', gridTemplateColumns:'repeat(4,1fr)', gap:10 }}>
        <div style={qCardS}>
          <div style={{ fontSize:10, color:'var(--fg-muted)', fontWeight:600, textTransform:'uppercase' }}>{lang === 'en' ? 'Total Revenue' : '총 매출'}</div>
          <div style={{ fontSize:20, fontWeight:700, marginTop:6 }}>{qKrw(r.kpi.totalKrw)}</div>
        </div>
        <div style={qCardS}>
          <div style={{ fontSize:10, color:'var(--fg-muted)', fontWeight:600, textTransform:'uppercase' }}>{lang === 'en' ? 'Total Units' : '판매량'}</div>
          <div style={{ fontSize:20, fontWeight:700, marginTop:6 }}>{r.kpi.totalQty.toLocaleString()}</div>
        </div>
        <div style={qCardS}>
          <div style={{ fontSize:10, color:'var(--fg-muted)', fontWeight:600, textTransform:'uppercase' }}>{lang === 'en' ? 'Orders' : '주문 건수'}</div>
          <div style={{ fontSize:20, fontWeight:700, marginTop:6 }}>{r.kpi.orderCount.toLocaleString()}</div>
          <div style={{ fontSize:10, color:'var(--fg-muted)', marginTop:2 }}>
            {lang === 'en' ? `avg ${qKrw(r.kpi.avgOrderKrw)}/order` : `객단가 ${qKrw(r.kpi.avgOrderKrw)}`}
          </div>
        </div>
        <div style={qCardS}>
          <div style={{ fontSize:10, color:'var(--fg-muted)', fontWeight:600, textTransform:'uppercase' }}>YoY</div>
          <div style={{ fontSize:20, fontWeight:700, marginTop:6, color: yoyColor }}>
            {r.kpi.yoyPct == null ? '—' : `${r.kpi.yoyPct >= 0 ? '+' : ''}${r.kpi.yoyPct.toFixed(1)}%`}
          </div>
          <div style={{ fontSize:10, color:'var(--fg-muted)', marginTop:2 }}>
            {lang === 'en' ? r.kpi.yoyLabelEn : r.kpi.yoyLabelKo}
          </div>
        </div>
      </div>

      {/* Auto Insights — compact summary */}
      {r.insights.length > 0 && (
        <div style={qCardS}>
          <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:10, textTransform:'uppercase', letterSpacing:.04 }}>
            🔍 {lang === 'en' ? 'Auto Insights' : '자동 인사이트'}
          </div>
          <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
            {r.insights.map((ins, i) => (
              <div key={i} style={{
                display:'flex', alignItems:'flex-start', gap:8, padding:'6px 10px',
                background: ins.type === 'good' ? 'rgba(52,199,89,0.08)' : ins.type === 'risk' ? 'rgba(255,69,58,0.08)' : 'var(--surface-2)',
                borderLeft: `3px solid ${ins.type === 'good' ? '#34C759' : ins.type === 'risk' ? '#FF453A' : 'var(--fg-muted)'}`,
                borderRadius:6, fontSize:12.5, color:'var(--fg)',
              }}>
                <span>{ins.type === 'good' ? '✓' : ins.type === 'risk' ? '!' : '·'}</span>
                <span>{ins.text}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Breakout Chart — model YoY growth rates (horizontal bar) */}
      {r.modelBreakout && r.modelBreakout.length > 0 && (
        <div style={qCardS}>
          <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:2, textTransform:'uppercase', letterSpacing:.04 }}>
            📈 {lang === 'en' ? 'Model YoY Growth' : '모델별 YoY 성장률'}
          </div>
          <div style={{ fontSize:10.5, color:'var(--fg-muted)', marginBottom:10 }}>
            {lang === 'en' ? r.kpi.yoyLabelEn : r.kpi.yoyLabelKo}
          </div>
          <BreakoutBar items={r.modelBreakout} overallGrowth={r.modelBreakout._overallGrowth} lang={lang}/>
        </div>
      )}

      {/* Country breakout chart */}
      {r.countryBreakout && r.countryBreakout.length > 0 && (
        <div style={qCardS}>
          <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:2, textTransform:'uppercase', letterSpacing:.04 }}>
            🌍 {lang === 'en' ? 'Market YoY Growth' : '시장별 YoY 성장률'}
          </div>
          <div style={{ fontSize:10.5, color:'var(--fg-muted)', marginBottom:10 }}>
            {lang === 'en' ? r.kpi.yoyLabelEn : r.kpi.yoyLabelKo}
          </div>
          <BreakoutBar items={r.countryBreakout} overallGrowth={r.countryBreakout._overallGrowth} lang={lang}/>
        </div>
      )}

      {/* New entries — badge style */}
      {r.newEntries && r.newEntries.length > 0 && (
        <div style={qCardS}>
          <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:10, textTransform:'uppercase', letterSpacing:.04 }}>
            🆕 {lang === 'en' ? 'New This Year' : '올해 신규 진입'}
          </div>
          <div style={{ display:'flex', flexWrap:'wrap', gap:8 }}>
            {r.newEntries.map((e, i) => (
              <div key={i} style={{
                padding:'6px 12px', borderRadius:20,
                background: e.kind === 'country' ? 'rgba(0,122,255,0.1)' : 'rgba(52,199,89,0.1)',
                border: `1px solid ${e.kind === 'country' ? 'rgba(0,122,255,0.25)' : 'rgba(52,199,89,0.25)'}`,
                fontSize:12, color:'var(--fg)',
              }}>
                <span style={{ fontWeight:600 }}>{e.label}</span>
                <span style={{ color:'var(--fg-muted)', marginLeft:6 }}>{_krwShort(e.curr, lang)}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Monthly trend */}
      {r.months.length > 1 && (
        <div style={qCardS}>
          <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:10 }}>
            <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing:.04 }}>
              {lang === 'en' ? 'Monthly Trend (vs prior year)' : '월별 추이 (전년 동기 비교)'}
            </div>
            <MetricToggle value={trendMetric} onChange={setTrendMetric} lang={lang}/>
          </div>
          <div style={{ position:'relative', height:240 }}>
            <canvas ref={trendRef}/>
          </div>
        </div>
      )}

      {/* Top breakdowns — 2 column grid */}
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14 }}>
        {r.topModels.length > 0 && (
          <ReportTable
            title={lang === 'en' ? 'Top 10 Models' : 'TOP 10 모델'}
            rows={r.topModels} totalKrw={r.kpi.totalKrw} maxRows={10}/>
        )}
        {r.topMakes.length > 0 && (
          <ReportTable
            title={lang === 'en' ? 'Top 8 Makers' : 'TOP 8 메이커'}
            rows={r.topMakes} totalKrw={r.kpi.totalKrw} maxRows={8}/>
        )}
      </div>
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14 }}>
        {r.topCats.length > 0 && (
          <ReportTable
            title={lang === 'en' ? 'Top Categories' : 'TOP 카테고리'}
            rows={r.topCats} totalKrw={r.kpi.totalKrw} maxRows={6}/>
        )}
        {r.topCountries.length > 1 && (
          <ReportTable
            title={lang === 'en' ? 'Top Countries' : 'TOP 국가'}
            rows={r.topCountries} totalKrw={r.kpi.totalKrw} maxRows={8}/>
        )}
      </div>
    </div>
  );
}

// Horizontal bar chart for YoY growth rates — pure CSS, no Chart.js dependency
function BreakoutBar({ items, overallGrowth, lang }) {
  if (!items || items.length === 0) return null;
  const maxAbs = Math.max(100, ...items.map(e => Math.min(Math.abs(e.growth), 500)));
  const barScale = 40; // max bar width percentage

  return (
    <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
      {items.map((e, i) => {
        const g = Math.max(-500, Math.min(e.growth, 500));
        const pct = Math.abs(g) / maxAbs * barScale;
        const isPos = g >= 0;
        const color = isPos ? '#34C759' : '#FF453A';
        const bgColor = isPos ? 'rgba(52,199,89,0.12)' : 'rgba(255,69,58,0.12)';
        return (
          <div key={i} style={{ display:'grid', gridTemplateColumns:'140px 1fr 70px', alignItems:'center', gap:8, padding:'5px 0' }}>
            <div style={{ fontSize:11.5, color:'var(--fg)', fontWeight:500, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
              {e.label}
            </div>
            <div style={{ position:'relative', height:18, background:'var(--surface-2)', borderRadius:4, overflow:'hidden' }}>
              <div style={{
                position:'absolute', top:0, height:'100%', borderRadius:4,
                background: bgColor,
                left: isPos ? '50%' : `${50 - pct}%`,
                width: `${pct}%`,
                borderLeft: isPos ? `2px solid ${color}` : 'none',
                borderRight: !isPos ? `2px solid ${color}` : 'none',
              }}/>
              {/* Center line = 0% */}
              <div style={{ position:'absolute', left:'50%', top:0, width:1, height:'100%', background:'var(--fg-muted)', opacity:0.3 }}/>
              {/* Overall growth reference line */}
              {overallGrowth != null && Math.abs(overallGrowth) <= maxAbs && (
                <div style={{
                  position:'absolute', top:0, width:1.5, height:'100%',
                  background:'var(--accent)', opacity:0.6,
                  left: `${50 + (overallGrowth / maxAbs * barScale)}%`,
                }} title={`Overall: ${overallGrowth >= 0 ? '+' : ''}${overallGrowth.toFixed(0)}%`}/>
              )}
            </div>
            <div style={{ fontSize:11, fontWeight:600, color, textAlign:'right' }}>
              {g >= 0 ? '+' : ''}{g >= 500 ? '500+' : g <= -500 ? '-500+' : g.toFixed(0)}%
            </div>
          </div>
        );
      })}
      {/* Legend */}
      <div style={{ display:'flex', gap:16, marginTop:4, fontSize:10, color:'var(--fg-muted)' }}>
        <span>── <span style={{ color:'var(--accent)' }}>■</span> {lang === 'en' ? 'Overall' : '전체'} {overallGrowth != null ? `${overallGrowth >= 0 ? '+' : ''}${overallGrowth.toFixed(0)}%` : ''}</span>
        <span><span style={{ color:'#34C759' }}>■</span> {lang === 'en' ? 'Growth' : '성장'}</span>
        <span><span style={{ color:'#FF453A' }}>■</span> {lang === 'en' ? 'Decline' : '하락'}</span>
      </div>
    </div>
  );
}

function ReportTable({ title, rows, totalKrw, maxRows }) {
  return (
    <div style={qCardS}>
      <div style={{ fontSize:12, fontWeight:600, color:'var(--fg-muted)', marginBottom:8, textTransform:'uppercase', letterSpacing:.04 }}>{title}</div>
      <table style={{ width:'100%', borderCollapse:'collapse', fontSize:11.5 }}>
        <tbody>
          {rows.slice(0, maxRows).map((g, i) => {
            const pct = totalKrw > 0 ? (g.krw / totalKrw * 100) : 0;
            return (
              <tr key={i} style={{ borderBottom:'1px solid var(--divider)' }}>
                <td style={{ padding:'5px 4px', width:18, color:'var(--fg-muted)', fontSize:10, fontWeight:600 }}>{i+1}</td>
                <td style={{ padding:'5px 4px', fontWeight: i < 3 ? 600 : 400 }}>{g.label}</td>
                <td style={{ padding:'5px 4px', textAlign:'right', fontWeight:600 }}>{qKrw(g.krw)}</td>
                <td style={{ padding:'5px 4px', textAlign:'right', color:'var(--fg-muted)', width:50 }}>{pct.toFixed(1)}%</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

// Convert a report object to copy-pasteable Markdown for Slack/email/Notion.
function reportToMarkdown(r, lang) {
  const L = lang === 'en'
    ? { title:'Performance Report', filter:'Filters', generated:'Generated', revenue:'Revenue', units:'Units', orders:'Orders', avg:'Avg per order', yoy:'YoY', insights:'Auto Insights', monthly:'Monthly Trend', topModels:'Top 10 Models', topMakes:'Top 8 Makers', topCats:'Top Categories', topCountries:'Top Countries' }
    : { title:'실적 보고서', filter:'필터', generated:'생성', revenue:'총 매출', units:'판매량', orders:'주문 건수', avg:'객단가', yoy:'YoY', insights:'자동 인사이트', monthly:'월별 추이', topModels:'TOP 10 모델', topMakes:'TOP 8 메이커', topCats:'TOP 카테고리', topCountries:'TOP 국가' };

  const lines = [];
  lines.push(`# 📊 ${r.headerTitle || L.title}`);
  lines.push(`**${L.filter}:** ${r.filterStr}`);
  lines.push(`**${L.generated}:** ${r.generatedAt.slice(0, 16).replace('T', ' ')}`);
  lines.push('');
  lines.push(`## KPI`);
  lines.push(`- ${L.revenue}: **${qKrw(r.kpi.totalKrw)}**`);
  lines.push(`- ${L.units}: **${r.kpi.totalQty.toLocaleString()}**`);
  lines.push(`- ${L.orders}: **${r.kpi.orderCount.toLocaleString()}** (${L.avg} ${qKrw(r.kpi.avgOrderKrw)})`);
  if (r.kpi.yoyPct != null) {
    lines.push(`- ${L.yoy}: **${r.kpi.yoyPct >= 0 ? '+' : ''}${r.kpi.yoyPct.toFixed(1)}%** (${lang === 'en' ? r.kpi.yoyLabelEn : r.kpi.yoyLabelKo})`);
  }
  if (r.insights.length > 0) {
    lines.push('');
    lines.push(`## 🔍 ${L.insights}`);
    for (const ins of r.insights) {
      const prefix = ins.type === 'good' ? '✓' : ins.type === 'risk' ? '⚠️' : '·';
      lines.push(`- ${prefix} ${ins.text}`);
    }
  }
  if (r.topModels.length > 0) {
    lines.push('');
    lines.push(`## ${L.topModels}`);
    r.topModels.slice(0, 10).forEach((m, i) => {
      const pct = r.kpi.totalKrw > 0 ? (m.krw / r.kpi.totalKrw * 100).toFixed(1) : '0.0';
      lines.push(`${i+1}. ${m.label} — ${qKrw(m.krw)} (${pct}%)`);
    });
  }
  if (r.topMakes.length > 0) {
    lines.push('');
    lines.push(`## ${L.topMakes}`);
    r.topMakes.slice(0, 8).forEach((m, i) => {
      const pct = r.kpi.totalKrw > 0 ? (m.krw / r.kpi.totalKrw * 100).toFixed(1) : '0.0';
      lines.push(`${i+1}. ${m.label} — ${qKrw(m.krw)} (${pct}%)`);
    });
  }
  if (r.topCountries.length > 1) {
    lines.push('');
    lines.push(`## ${L.topCountries}`);
    r.topCountries.slice(0, 8).forEach((m, i) => {
      const pct = r.kpi.totalKrw > 0 ? (m.krw / r.kpi.totalKrw * 100).toFixed(1) : '0.0';
      lines.push(`${i+1}. ${m.label} — ${qKrw(m.krw)} (${pct}%)`);
    });
  }
  return lines.join('\n');
}

// ─── Order lookup view (invoice_no detail) ────────────────────────────────
function OrderLookupView({ orderData, lang }) {
  if (!orderData.orders || orderData.orders.length === 0) {
    return (
      <div style={{ ...qCardS, textAlign:'center' }}>
        <div style={{ fontSize:13, color:'var(--fg-muted)' }}>
          {lang === 'en' ? 'No orders matched: ' : '매칭되는 주문 없음: '}
          <code>{orderData.tokens.join(', ')}</code>
        </div>
      </div>
    );
  }
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:12 }}>
      <div style={{ fontSize:11, color:'var(--fg-muted)' }}>
        {(lang === 'en' ? 'Found ' : '검색 결과 ')}
        <strong style={{ color:'var(--fg)' }}>{orderData.orders.length}</strong>
        {(lang === 'en' ? ' order(s), ' : '건, 라인 ')}
        <strong style={{ color:'var(--fg)' }}>{orderData.totalLines}</strong>
        {(lang === 'en' ? ' line(s)' : '개')}
      </div>
      {orderData.orders.map((o) => (
        <div key={o.invoice_no} style={qCardS}>
          {/* Order header */}
          <div style={{
            display:'flex', justifyContent:'space-between', alignItems:'flex-start',
            paddingBottom:10, marginBottom:10, borderBottom:'1px solid var(--divider)',
          }}>
            <div>
              <div style={{ fontSize:14, fontWeight:700, color:'var(--fg)' }}>
                {o.invoice_no}
              </div>
              <div style={{ fontSize:11, color:'var(--fg-muted)', marginTop:4 }}>
                {o.order_date}
                {o.fulfilled_date && o.fulfilled_date !== o.order_date && ` · ${lang === 'en' ? 'fulfilled' : '출고'} ${o.fulfilled_date}`}
                {' · '}{o.channel_group}{o.country ? ` / ${o.country}` : ''}
                {o.sales_channel ? ` · ${o.sales_channel}` : ''}
              </div>
              {o.company && (
                <div style={{ fontSize:11.5, color:'var(--fg-2)', marginTop:4 }}>{o.company}</div>
              )}
            </div>
            <div style={{ textAlign:'right', flexShrink:0, marginLeft:12 }}>
              <div style={{ fontSize:16, fontWeight:700, color:'var(--accent)' }}>{qKrw(o.total_krw)}</div>
              {o.currency && o.currency !== 'KRW' && (
                <div style={{ fontSize:10.5, color:'var(--fg-muted)', marginTop:2 }}>
                  {o.currency} {Math.round(o.total_native).toLocaleString()}
                </div>
              )}
              <div style={{ fontSize:10.5, color:'var(--fg-muted)', marginTop:2 }}>
                {o.total_qty} {lang === 'en' ? 'units' : '개'} · {o.lines.length} {lang === 'en' ? 'line(s)' : '라인'}
              </div>
            </div>
          </div>
          {/* Line items */}
          <table style={{ width:'100%', borderCollapse:'collapse', fontSize:11.5 }}>
            <thead>
              <tr style={{ borderBottom:'1px solid var(--divider)', color:'var(--fg-muted)' }}>
                <th style={{ padding:'4px 6px', textAlign:'left', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>{lang === 'en' ? 'Make' : '메이커'}</th>
                <th style={{ padding:'4px 6px', textAlign:'left', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>{lang === 'en' ? 'Model' : '모델'}</th>
                <th style={{ padding:'4px 6px', textAlign:'left', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>SKU</th>
                <th style={{ padding:'4px 6px', textAlign:'left', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>{lang === 'en' ? 'Category' : '카테고리'}</th>
                <th style={{ padding:'4px 6px', textAlign:'right', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>QTY</th>
                <th style={{ padding:'4px 6px', textAlign:'right', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>{lang === 'en' ? 'Price' : '단가'}</th>
                <th style={{ padding:'4px 6px', textAlign:'right', fontWeight:600, fontSize:10, textTransform:'uppercase' }}>KRW</th>
              </tr>
            </thead>
            <tbody>
              {o.lines.map((l, i) => (
                <tr key={i} style={{ borderBottom:'1px solid var(--divider)' }}>
                  <td style={{ padding:'4px 6px', color:'var(--fg-2)' }}>{l.make || '—'}</td>
                  <td style={{ padding:'4px 6px', color:'var(--fg)' }}>{l.model || '—'}</td>
                  <td style={{ padding:'4px 6px', color:'var(--fg-muted)', fontFamily:'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize:10.5 }}>{l.sku_raw || '—'}</td>
                  <td style={{ padding:'4px 6px', color:'var(--fg-muted)' }}>{l.category || '—'}</td>
                  <td style={{ padding:'4px 6px', textAlign:'right' }}>{l.qty || 0}</td>
                  <td style={{ padding:'4px 6px', textAlign:'right', color:'var(--fg-muted)' }}>
                    {l.currency && l.currency !== 'KRW'
                      ? `${l.currency} ${Math.round(l.price || 0).toLocaleString()}`
                      : qKrw(l.price || 0)}
                  </td>
                  <td style={{ padding:'4px 6px', textAlign:'right', fontWeight:600 }}>{qKrw(l.sales_amount_krw || 0)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ))}
    </div>
  );
}

// v3.4: 카테고리별 그룹 구조. 각 항목은 'pattern  hint' 형식 (공백 2개로 구분).
// {placeholder}는 클릭 시 input에 그대로 채워져 사용자가 편집.
const TEMPLATE_LIST = {
  ko: [
    { group: '기본', items: [
      '{모델명} 얼마나 팔렸어  예: GT3 얼마나 팔렸어',
      '{국가} 실적  예: 독일 실적',
      '{메이크} TOP 모델  예: Toyota TOP 모델',
      'YoY 성장률  전년 대비 성장률',
    ]},
    { group: '시계열', items: [
      '지난 3개월 매출  상대 날짜',
      '최근 6주 매출  주 단위',
      '{연도}년 Q1 {채널} 매출  예: 2025년 Q1 US 매출',
      '{YYYY}-{MM}부터 {YYYY}-{MM}까지 윙 판매량  기간 범위',
      '월별 추이  시계열 보기',
    ]},
    { group: '조합', items: [
      '{국가} TOP {N} 상품  예: 독일 TOP 5 상품',
      'ROW 국가별 TOP 5 상품  국가별 효자상품 5개씩',
      '{차종} 윙 매출 비중  예: GT3 윙 매출 비중',
      '{카테고리} 전체 매출 비중  예: 범퍼 전체 매출 비중',
      '국가별로 뭐가 잘 팔려  각 국가 효자상품 TOP 3',
      '{메이크그룹} 카테고리별 매출  예: JDM 카테고리별 매출',
    ]},
    { group: '비교', items: [
      '{A} vs {B}  예: 2025년 vs 2024년',
      '올해 vs 작년  YoY 비교',
      '{국가1}과 {국가2} 비교  예: 독일과 일본 비교',
    ]},
    { group: '인보이스', items: [
      'ADRO_1009834  US 주문번호 조회',
      '25100000333ID  ROW 인보이스 조회',
    ]},
    { group: '보고서', items: [
      '2026 ROW 실적 보고서  8섹션 풀 보고서',
      '{국가} 특이사항  급증/급감/신규 모델',
    ]},
  ],
  en: [
    { group: 'Basic', items: [
      '{model} sales  e.g. GT3 sales',
      '{country} performance  e.g. Germany performance',
      '{make} top models  e.g. Toyota top models',
      'YoY growth  year-over-year',
    ]},
    { group: 'Time series', items: [
      'last 3 months revenue  relative date',
      'last 6 weeks revenue  weekly',
      '{year} Q1 {channel} revenue  e.g. 2025 Q1 US revenue',
      'wing sales from {YYYY}-{MM} to {YYYY}-{MM}  date range',
      'monthly trend  time series',
    ]},
    { group: 'Combination', items: [
      '{country} TOP {N} products  e.g. Germany top 5 products',
      'ROW top 5 products by country  5 per country',
      '{model} wing revenue share  e.g. GT3 wing share',
      '{category} revenue share  e.g. bumper share',
      'best sellers by country  top 3 per country',
      '{makeGroup} revenue by category  e.g. JDM by category',
    ]},
    { group: 'Compare', items: [
      '{A} vs {B}  e.g. 2025 vs 2024',
      'this year vs last year  YoY compare',
      'compare {country1} and {country2}  e.g. Germany vs Japan',
    ]},
    { group: 'Invoice', items: [
      'ADRO_1009834  US invoice lookup',
      '25100000333ID  ROW invoice lookup',
    ]},
    { group: 'Report', items: [
      '2026 ROW performance report  full 8-section report',
      '{country} highlights  surge/drop/new',
    ]},
  ],
};
const QT_KO = { title:'질문', desc:'제품/채널/기간에 대해 질문하세요.', placeholder:'예: GT3 윙 얼마나 팔렸어', search:'검색', loading:'데이터 로드 중…', templates:'질문 템플릿', filtered:'필터' };
const QT_EN = { title:'Ask', desc:'Ask about products, channels, or time periods.', placeholder:'e.g. GT3 wing sales', search:'Search', loading:'Loading data…', templates:'Question templates', filtered:'Filters' };

window.QueryView = QueryView;
