/* OM Group ERP — KPI Intelligence Sheet
   The premium right-side drill-down every Analytics KPI opens into. It is a
   pure view over one KpiIntel model: value → how it is calculated → what it
   reconciles against → why it moved → who contributed → the exact ERP
   records → one transaction. The Analytics Center stays mounted behind it,
   so closing returns the user exactly where they were.                    */
const { useState: kxSt, useEffect: kxEf, useMemo: kxMemo, useRef: kxRef } = React;

const kxF = (v, k) => window.KpiIntel.fmt(v, k);
const kxRankKind = f => f === 'cur' ? 'short' : f === 'ltr' ? 'int' : f === 'num' ? 'int' : f;
/* "purchase entry" → "purchase entries", never "entrys" */
const kxPlural = (noun, n) => n === 1 ? noun : /(s|x|ch|sh)$/.test(noun) ? noun + 'es' : /[^aeiou]y$/.test(noun) ? noun.slice(0, -1) + 'ies' : noun + 's';
const kxCount = (n, noun) => n + ' ' + kxPlural(noun, n);

function KxDelta({ d, good }) {
  if (!d) return <span className="kx-nocmp">no comparison period</span>;
  const cls = d.flat ? 'fl' : good === 'flat' ? 'fl' : d.better ? 'up' : 'dn';
  return (
    <span className={'i-delta ' + cls}>
      {d.abs > 0
        ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"><path d="M7 17L17 7M17 7H9M17 7v8" /></svg>
        : d.abs < 0 ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"><path d="M7 7l10 10M17 17H9M17 17V9" /></svg>
          : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"><path d="M5 12h14" /></svg>}
      {Math.abs(d.pct) > 999 ? '>999' : Math.abs(d.pct).toFixed(1)}%
    </span>
  );
}

function kxCsv(name, cols, recs, rowOf) {
  const esc = v => '"' + String(v == null ? '' : v).replace(/"/g, '""') + '"';
  const head = cols.map(c => c[1]);
  const body = recs.map(r => { const o = rowOf(r); return cols.map(c => esc(c[2] === 'ton' || c[2] === 'num' ? window.formatQuantityRaw(o[c[0]]) : o[c[0]])); });
  const txt = [head.map(esc).join(',')].concat(body.map(r => r.join(','))).join('\n');
  const a = document.createElement('a');
  a.href = URL.createObjectURL(new Blob(['\ufeff' + txt], { type: 'text/csv;charset=utf-8' }));
  a.download = name.replace(/[^\w]+/g, '-') + '.csv';
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 400);
  window.toast && window.toast('Exported ' + recs.length + ' records', 'ok');
}

/* ── source-record table ─────────────────────────────────────────────────── */
function KxTable({ model, recs, onOpen, page, preview, total }) {
  const cols = model.cols;
  const [q, setQ] = kxSt('');
  const [sort, setSort] = kxSt({ k: cols[0][0], dir: -1 });
  const [lim, setLim] = kxSt(page || 60);
  const shaped = kxMemo(() => recs.map(r => ({ r, v: model.rowOf(r) })), [recs, model]);
  const rows = kxMemo(() => {
    const s = q.trim().toLowerCase();
    const f = s ? shaped.filter(x => cols.some(c => String(x.v[c[0]] == null ? '' : x.v[c[0]]).toLowerCase().indexOf(s) >= 0)) : shaped;
    const k = sort.k;
    return f.slice().sort((a, b) => {
      const av = a.v[k], bv = b.v[k];
      if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * sort.dir;
      return String(av == null ? '' : av).localeCompare(String(bv == null ? '' : bv)) * sort.dir;
    });
  }, [shaped, q, sort]);
  const sums = kxMemo(() => {
    const o = {};
    cols.forEach(c => { if (['cur', 'ton', 'ltr', 'num'].indexOf(c[2]) >= 0) o[c[0]] = rows.reduce((s, x) => s + (Number(x.v[c[0]]) || 0), 0); });
    return o;
  }, [rows]);
  const isNum = k => ['cur', 'ton', 'ltr', 'num', 'rate', 'days'].indexOf(k) >= 0;
  return (
    <div className="kx-tblbox">
      {preview ? (
        <div className="kx-tbltools"><span className="kx-tblcount">First {recs.length} of {kxCount(total, model.recordNoun)} — open the full list for search, sorting and CSV</span></div>
      ) : (
      <div className="kx-tbltools">
        <input className="kx-search" value={q} placeholder="Search these records…" onChange={e => setQ(e.target.value)} aria-label="Search source records" />
        <span className="kx-tblcount">{rows.length} of {recs.length}</span>
        <button className="i-ctl sm" onClick={() => kxCsv('OM-' + model.label + '-' + model.period.from + '_' + model.period.to, cols, rows.map(x => x.r), model.rowOf)}>Export CSV</button>
      </div>
      )}
      <div className="kx-tblwrap">
        <table className="i-dl-tbl kx-tbl">
          <thead><tr>
            {cols.map(c => (
              <th key={c[0]} className={isNum(c[2]) ? 'r' : ''} onClick={() => setSort(s => ({ k: c[0], dir: s.k === c[0] ? -s.dir : -1 }))}>
                {c[1]}{sort.k === c[0] ? <em>{sort.dir < 0 ? ' ↓' : ' ↑'}</em> : null}
              </th>
            ))}
            <th style={{ width: 26 }}></th>
          </tr></thead>
          <tbody>
            {!rows.length && <tr><td colSpan={cols.length + 1} className="kx-tblempty">No record matches this search.</td></tr>}
            {rows.slice(0, lim).map((x, i) => (
              <tr key={(x.r.id || i) + '-' + i} onClick={() => onOpen && onOpen(x.r)} className={onOpen ? 'kx-clickrow' : ''}>
                {cols.map(c => {
                  const v = x.v[c[0]];
                  return (
                    <td key={c[0]} className={isNum(c[2]) ? 'r' : ''}>
                      {c[2] === 'badge' ? <span className="i-ldg-badge">{v || '—'}</span>
                        : c[2] === 'date' ? (v ? window.IntelEngine.util.fmtD(v) : '—')
                          : isNum(c[2]) ? kxF(v, c[2]) : (v == null || v === '' ? '—' : String(v))}
                    </td>
                  );
                })}
                <td className="kx-go">›</td>
              </tr>
            ))}
          </tbody>
          {rows.length > 0 && !preview && (
            <tfoot><tr>
              {cols.map((c, i) => (
                <td key={c[0]} className={isNum(c[2]) ? 'r' : ''}>{i === 0 ? rows.length + ' records' : (sums[c[0]] != null ? kxF(sums[c[0]], c[2]) : '')}</td>
              ))}
              <td></td>
            </tr></tfoot>
          )}
        </table>
      </div>
      {rows.length > lim && (
        <button className="kx-more" onClick={() => setLim(l => l + 120)}>Show {Math.min(120, rows.length - lim)} more · {rows.length - lim} remaining</button>
      )}
    </div>
  );
}

/* ── single ERP record ───────────────────────────────────────────────────── */
function KxRecord({ rec, model, onNavigate }) {
  const raw = rec.raw || rec;
  const items = (raw.items && raw.items.length) ? raw.items : null;
  let gst = null;
  try { if (window.GstEngine && (items || raw.subtotal != null)) gst = window.GstEngine.recalcRecord(raw); } catch (e) { gst = null; }
  const skip = { items: 1, _autoGenerated: 1 };
  const fields = Object.keys(raw).filter(k => k[0] !== '_' && !skip[k] && typeof raw[k] !== 'object' && raw[k] !== undefined);
  const pretty = k => k.replace(/([A-Z])/g, ' $1').replace(/^./, m => m.toUpperCase()).replace(/ Id$/, ' ID');
  const val = (k, v) => {
    if (v === '' || v == null) return '—';
    if (/^(customerId)$/.test(k)) return window.Store.name('customers', v);
    if (/^(vendorId)$/.test(k)) return window.Store.name('vendors', v);
    if (/^(materialId)$/.test(k)) return window.Store.name('materials', v);
    if (/^(companyId|sourceCompanyId|destCompanyId)$/.test(k)) return window.Store.name('companies', v);
    if (/^(crusherSite)$/.test(k)) return window.Store.name('crushers', v);
    if (/^(stockyardId)$/.test(k)) return window.Store.name('stockyards', v);
    if (/^(date|periodFrom|periodTo|periodStart|periodEnd|createdDate|dueDate)$/.test(k) && /^\d{4}-\d{2}-\d{2}/.test(String(v))) return window.IntelEngine.util.fmtD(v);
    if (typeof v === 'boolean') return v ? 'Yes' : 'No';
    if (/(quantity|litres|qty)/i.test(k) && isFinite(Number(v))) return window.formatQuantity(v);
    return String(v);
  };
  const page = model.pageOf ? model.pageOf(rec) : null;
  return (
    <>
      {gst && (
        <div className="kx-facts">
          <div className="dr-fact"><span>Taxable</span><b>{kxF(gst.subtotal, 'cur')}</b></div>
          <div className="dr-fact"><span>GST</span><b>{kxF(gst.gstAmount, 'cur')}</b></div>
          <div className="dr-fact"><span>Total</span><b style={{ color: model.accent }}>{kxF(gst.total, 'cur')}</b></div>
        </div>
      )}
      <div className="kx-sec-t">Every field on this record</div>
      <div className="kx-kv">
        {fields.map(k => (<div key={k}><span>{pretty(k)}</span><b>{val(k, raw[k])}</b></div>))}
      </div>
      {items && (
        <>
          <div className="kx-sec-t">Line items · {items.length}</div>
          <div className="kx-tblwrap">
            <table className="i-dl-tbl kx-tbl">
              <thead><tr><th>Material</th><th className="r">Qty</th><th>UOM</th><th className="r">Rate</th><th className="r">Amount</th></tr></thead>
              <tbody>
                {items.map((it, i) => {
                  const q = Number(it.quantity) || 0, rt = Number(it.ratePerTon || it.rate) || 0;
                  const amt = it.subtotal != null ? it.subtotal : (it.amount != null ? it.amount : q * rt);
                  return <tr key={i}><td>{window.Store.name('materials', it.materialId)}</td><td className="r">{window.formatQuantity(q)}</td><td>{it.uom || 'Ton'}</td><td className="r">{kxF(rt, 'cur')}</td><td className="r">{kxF(amt, 'cur')}</td></tr>;
                })}
              </tbody>
            </table>
          </div>
        </>
      )}
      <div className="kx-note">
        This is the original ERP record — the Analytics Center reads it, never copies it. Edit or delete it in its own module and every KPI above recalculates from this same row.
        {page && onNavigate && <button className="kx-link" onClick={() => onNavigate(page)}>Open in ERP →</button>}
      </div>
    </>
  );
}

/* ── level 0: the KPI intelligence view ──────────────────────────────────── */
function KxKpiView({ model, onGroup, onRecords, onRecord, onMetric }) {
  const [dim, setDim] = kxSt(() => (model.breakdowns[0] || {}).id);
  kxEf(() => { setDim((model.breakdowns[0] || {}).id); }, [model.key, model.period.from, model.period.to]);
  const bd = model.breakdowns.find(b => b.id === dim) || model.breakdowns[0];
  const trend = model.points.filter(p => p && isFinite(p.value));
  const att = model.attribution;
  const attRow = r => (
    <div className="kx-att-row" key={r.key} onClick={() => onGroup && att && onGroup({ id: att.dim.id, title: att.dim.title }, r)} role="button" tabIndex={0}
      onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onGroup && onGroup({ id: att.dim.id, title: att.dim.title }, r); } }}>
      <span className="kx-att-l">{r.label}</span>
      <span className="kx-att-v">{kxF(r.prev, model.fmt)} → {kxF(r.cur, model.fmt)}</span>
      <b className={r.delta >= 0 ? 'up' : 'dn'}>{r.delta >= 0 ? '+' : '−'}{kxF(Math.abs(r.delta), model.fmt)}</b>
    </div>
  );
  return (
    <>
      <div className="kx-hero">
        <div className="kx-hero-main">
          <div className="kx-hero-k">{model.label}</div>
          <div className="kx-hero-v">{model.empty ? kxF(0, model.fmt) : kxF(model.value, model.fmt)}</div>
          <div className="kx-hero-ft">
            <KxDelta d={model.delta} good={model.good} />
            {model.delta
              ? <span className="kx-hero-prev">{kxF(model.prevValue, model.fmt)} in {model.cmpPeriod.label}</span>
              : <span className="kx-hero-prev">{model.period.label}</span>}
          </div>
        </div>
        <div className="kx-hero-side">
          <div className="kx-formula-t">How this number is produced</div>
          <div className="kx-formula">{model.formula}</div>
          {model.basisNote && <div className="kx-formula-n">{model.basisNote}</div>}
        </div>
      </div>

      {model.verify && (
        <div className={'kx-verify' + (model.verify.ok ? ' ok' : ' warn')}>
          <i></i>
          <div>
            <b>{model.verify.ok ? 'Reconciled against source records' : 'Recomputed value differs from the card'}</b>
            <span>
              Recomputing this metric from the {kxCount(model.verify.scope, model.recordNoun)} in scope gives {kxF(model.verify.recomputed, model.verify.unit || model.fmt)}
              {model.verify.ok ? ' — exactly the value on the card.' : ' against ' + kxF(model.verify.expected, model.fmt) + ' on the card. Report this: the KPI and its records disagree.'}
            </span>
          </div>
        </div>
      )}

      {model.empty ? (
        <div className="kx-empty">
          <b>No source records for this period</b>
          <span>Nothing was recorded for {model.workspace.toLowerCase()} in {model.period.label}{model.filterLabel ? ' under the current cross-filters' : ''}. The value above is a true zero, not a missing figure.</span>
        </div>
      ) : (<>
        {model.calc.length > 1 && (<>
          <div className="kx-sec-t">Calculation</div>
          <div className="kx-calc">
            {model.calc.map((c, i) => (
              <div className={'kx-calc-row' + (c.total ? ' tot' : '')} key={i}>
                <span>{c.label}</span><b>{kxF(c.value, c.kind)}</b>
              </div>
            ))}
          </div>
        </>)}

        {trend.length > 1 && (
          <div className="kx-card">
            <div className="kx-card-hd"><div><div className="kx-card-t">{model.label} across {model.period.label}</div>
              <div className="kx-card-s">Every bucket re-derived from the live ERP · click a point to inspect that window</div></div></div>
            <window.IC.Area height={168} yKind={model.fmt === 'days' || model.fmt === 'ltr' || model.fmt === 'num' ? 'int' : model.fmt}
              labels={trend.map(p => p.label)} subLabels={trend.map(p => p.sub)}
              series={[{ key: 'v', label: model.label, color: model.accent, values: trend.map(p => p.value) }]}
              onPointClick={i => { const p = trend[i]; if (p && p.period && onGroup) onGroup({ id: '__time', title: 'Date-wise contribution' }, null, p); }} />
          </div>
        )}

        {att && (att.up.length > 0 || att.down.length > 0) && (
          <div className="kx-card">
            <div className="kx-card-hd">
              <div><div className="kx-card-t">Why it changed</div>
                <div className="kx-card-s">{att.additive ? 'Movement of ' + kxF(att.total, model.fmt) + ' attributed across ' + att.dim.title.toLowerCase() + ' — computed from both periods’ own records' : 'This metric is a ratio, so contributions do not add up to the total movement — each line is that group’s own movement'}</div></div>
            </div>
            <div className="kx-att">
              {att.up.length > 0 && <div className="kx-att-grp"><div className="kx-att-h up">Pushed it up</div>{att.up.map(attRow)}</div>}
              {att.down.length > 0 && <div className="kx-att-grp"><div className="kx-att-h dn">Pulled it down</div>{att.down.map(attRow)}</div>}
            </div>
          </div>
        )}

        {bd && (
          <div className="kx-card">
            <div className="kx-card-hd">
              <div><div className="kx-card-t">{bd.title}</div>
                <div className="kx-card-s">{model.additive ? 'Adds back to ' + kxF(model.value, model.fmt) + ' · click any row for its records' : 'Recomputed per group with the same formula · click any row for its records'}</div></div>
            </div>
            <div className="kx-dimtabs">
              {model.breakdowns.map(b => (
                <button key={b.id} className={'i-ctl sm' + (b.id === bd.id ? ' on' : '')} onClick={() => setDim(b.id)}>{b.title.replace(/ contribution$/, '').replace(/^Contribution by /, 'By ')}</button>
              ))}
            </div>
            <window.IC.Rank limit={12} valueKind={kxRankKind(model.fmt)}
              rows={bd.rows.slice(0, 12).map(r => ({ label: r.label, value: Math.abs(r.value), key: r.key }))}
              right={r => { const m = bd.rows.find(x => x.key === r.key); return m ? (m.pct.toFixed(1) + '% · ' + m.count + ' rec' + (m.count === 1 ? '' : 's')) : ''; }}
              onClick={r => { const m = bd.rows.find(x => x.key === r.key); if (m) onGroup(bd, m); }} />
            {bd.rows.length > 12 && <div className="kx-note sm">Showing the 12 largest of {bd.rows.length} groups.</div>}
          </div>
        )}

        {model.facts.length > 0 && (<>
          <div className="kx-sec-t">Supporting metrics · same records, same period</div>
          <div className="kx-facts">
            {model.facts.slice(0, 12).map((f, i) => (
              <button className="dr-fact kx-fact" key={i} onClick={() => onMetric && onMetric(f.key)} disabled={!onMetric || !f.key}>
                <span>{f.label}</span><b>{kxF(f.value, f.kind)}</b>
              </button>
            ))}
          </div>
        </>)}

        <div className="kx-sec-t">Source records</div>
        <div className="kx-srcbar">
          <div>
            <b>{kxCount(model.records.length, model.recordNoun)}</b>
            <span>{model.recordScopeAll ? 'Every record in scope contributes to this KPI.' : model.recordScopeLabel ? 'Scoped to ' + model.recordScopeLabel + ' — the group this KPI measures.' : 'Only the records that actually contribute are listed.'}</span>
          </div>
          <button className="kx-btn" onClick={onRecords}>View all records →</button>
        </div>
        <KxTable model={model} recs={model.records.slice(0, 12)} onOpen={onRecord} page={12} preview total={model.records.length} />
      </>)}
    </>
  );
}

/* ── level 1: one contributor inside the KPI ─────────────────────────────── */
function KxGroupView({ model, node, onGroup, onRecord }) {
  const recs = node.recs;
  const others = model.dims.filter(d => d.id !== node.dimId);
  const [dim, setDim] = kxSt(() => (others[0] || {}).id);
  kxEf(() => { setDim((others[0] || {}).id); }, [node.key]);
  const rows = kxMemo(() => dim ? model.regroup(recs, dim) : [], [recs, dim]);
  const value = model.valueOf(recs);
  const share = Math.abs(model.value) > 0 ? Math.abs(value) / Math.abs(model.value) * 100 : 0;
  const dimMeta = others.find(d => d.id === dim);
  return (
    <>
      <div className="kx-hero">
        <div className="kx-hero-main">
          <div className="kx-hero-k">{node.label}</div>
          <div className="kx-hero-v">{kxF(value, model.fmt)}</div>
          <div className="kx-hero-ft">
            <span className="kx-share">{share.toFixed(1)}% of {model.label}</span>
            <span className="kx-hero-prev">{recs.length} record{recs.length === 1 ? '' : 's'}</span>
          </div>
        </div>
        <div className="kx-hero-side">
          <div className="kx-formula-t">Still the same formula</div>
          <div className="kx-formula">{model.formula}</div>
          <div className="kx-formula-n">Applied to the {recs.length} record{recs.length === 1 ? '' : 's'} of {node.label} only.</div>
        </div>
      </div>
      {rows.length > 0 && dimMeta && (
        <div className="kx-card">
          <div className="kx-card-hd">
            <div><div className="kx-card-t">{dimMeta.title} inside {node.label}</div>
              <div className="kx-card-s">Keep drilling — every level recomputes from the records it holds</div></div>
          </div>
          <div className="kx-dimtabs">
            {others.map(d => <button key={d.id} className={'i-ctl sm' + (d.id === dim ? ' on' : '')} onClick={() => setDim(d.id)}>{d.title.replace(/ contribution$/, '').replace(/^Contribution by /, 'By ')}</button>)}
          </div>
          <window.IC.Rank limit={10} valueKind={kxRankKind(model.fmt)}
            rows={rows.slice(0, 10).map(r => ({ label: r.label, value: Math.abs(r.value), key: r.key }))}
            right={r => { const m = rows.find(x => x.key === r.key); return m ? (m.pct.toFixed(1) + '% · ' + m.count + ' rec' + (m.count === 1 ? '' : 's')) : ''; }}
            onClick={r => { const m = rows.find(x => x.key === r.key); if (m) onGroup({ id: dim, title: dimMeta.title }, m); }} />
        </div>
      )}
      <div className="kx-sec-t">Records behind {node.label}</div>
      <KxTable model={model} recs={recs} onOpen={onRecord} />
    </>
  );
}

/* ── the sheet ───────────────────────────────────────────────────────────── */
function KpiSheet({ model, onClose, onNavigate, onMetric }) {
  const [stack, setStack] = kxSt([{ kind: 'kpi', label: model.label }]);
  const closeRef = kxRef(null);
  const bodyRef = kxRef(null);
  const sheetRef = kxRef(null);
  kxEf(() => { setStack([{ kind: 'kpi', label: model.label }]); }, [model.key, model.period.from, model.period.to, model.companyId]);
  /* Esc steps back a level then closes; Tab is trapped inside the window so
     the dashboard behind it never takes focus while the modal is open. */
  kxEf(() => {
    const h = e => {
      if (e.key === 'Escape') { e.stopPropagation(); setStack(s => s.length > 1 ? s.slice(0, -1) : (onClose(), s)); return; }
      if (e.key !== 'Tab' || !sheetRef.current) return;
      const f = Array.prototype.filter.call(
        sheetRef.current.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'),
        el => !el.disabled && el.offsetParent !== null);
      if (!f.length) return;
      const first = f[0], last = f[f.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    };
    document.addEventListener('keydown', h);
    if (closeRef.current) closeRef.current.focus();
    return () => document.removeEventListener('keydown', h);
  }, [onClose]);
  /* Lock the page behind the modal — the dashboard stays put, no scroll bleed
     and no horizontal shift (the scrollbar width is compensated). */
  kxEf(() => {
    const b = document.body, ov = b.style.overflow, pr = b.style.paddingRight;
    const sw = window.innerWidth - document.documentElement.clientWidth;
    b.style.overflow = 'hidden';
    if (sw > 0) b.style.paddingRight = sw + 'px';
    return () => { b.style.overflow = ov; b.style.paddingRight = pr; };
  }, []);
  kxEf(() => { if (bodyRef.current) bodyRef.current.scrollTop = 0; }, [stack.length]);

  const node = stack[stack.length - 1];
  const push = nd => setStack(s => s.concat([nd]));
  const popTo = i => setStack(s => s.slice(0, i + 1));
  const openGroup = (bd, row, point) => {
    if (point) { push({ kind: 'group', dimId: '__time', label: point.label, key: point.label, recs: (model.regroup(model.records, '__time').find(r => r.label === point.label) || { recs: [] }).recs }); return; }
    if (!row) return;
    push({ kind: 'group', dimId: bd.id, label: row.label, key: bd.id + '|' + row.key, recs: row.recs });
  };
  const openRecords = () => push({ kind: 'records', label: 'All ' + model.records.length + ' records', recs: model.records });
  const openRecord = rec => push({ kind: 'record', label: (model.rowOf(rec).ref && model.rowOf(rec).ref !== '—' ? model.rowOf(rec).ref : (rec.party || model.recordNoun)) || 'Record', rec });

  let body = null;
  try {
    if (node.kind === 'kpi') body = <KxKpiView model={model} onGroup={openGroup} onRecords={openRecords} onRecord={openRecord} onMetric={onMetric} />;
    else if (node.kind === 'group') body = <KxGroupView model={model} node={node} onGroup={openGroup} onRecord={openRecord} />;
    else if (node.kind === 'records') body = <KxTable model={model} recs={node.recs} onOpen={openRecord} />;
    else if (node.kind === 'record') body = <KxRecord rec={node.rec} model={model} onNavigate={onNavigate} />;
  } catch (e) {
    console.error('[Analytics] KPI drill-down failed:', e);
    body = <div className="kx-empty"><b>This view could not be derived</b><span>{String(e && e.message || e)} — the rest of the Analytics Center is unaffected. Close and try again, or narrow the period.</span></div>;
  }

  const page = model.pageOf ? model.pageOf(node.rec || model.records[0]) : null;
  return ReactDOM.createPortal((
    <div className="kx-bg" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
      <aside className="kx-sheet" ref={sheetRef} role="dialog" aria-modal="true" aria-label={model.label + ' — detailed analysis'} style={{ '--kxA': model.accent }}>
        <div className="kx-hd">
          <div className="kx-hd-t">
            <div className="dr-eyebrow" style={{ '--drA': model.accent }}><i></i>{model.workspace} · traceable KPI</div>
            <div className="kx-hd-title">{node.kind === 'kpi' ? model.label + ' — detailed analysis' : node.label}</div>
            <div className="kx-hd-sub">{model.period.label}{model.cmpPeriod ? ' vs ' + model.cmpPeriod.label : ''} · {model.companyLabel}{model.filterLabel ? ' · ' + model.filterLabel : ''}</div>
          </div>
          <button className="dr-ico x" ref={closeRef} onClick={onClose} title="Close (Esc)" aria-label="Close detailed analysis">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M6 6l12 12M18 6L6 18" /></svg>
          </button>
        </div>
        {stack.length > 1 && (
          <div className="kx-crumbs">
            {stack.map((s, i) => (
              <React.Fragment key={i}>
                {i > 0 && <span className="kx-crumb-sep">›</span>}
                <button className={'kx-crumb' + (i === stack.length - 1 ? ' cur' : '')} onClick={() => i < stack.length - 1 && popTo(i)}>{s.label}</button>
              </React.Fragment>
            ))}
          </div>
        )}
        <div className="kx-bd" ref={bodyRef}>{body}</div>
        <div className="kx-ft">
          <span>Live from {kxCount(model.records.length, 'ERP record')} · nothing on this panel is stored or cached</span>
          {page && onNavigate && <button className="kx-btn ghost" onClick={() => onNavigate(page)}>Open module →</button>}
        </div>
      </aside>
    </div>
  ), document.body);
}

/* Card-level guard: a metric whose model cannot be built shows a controlled
   error state instead of taking the Analytics Center down with it. The model
   is rebuilt on every render of the guard, so an ERP write behind the sheet
   is reflected the moment the workspace re-derives — nothing here is cached. */
function KpiSheetGuard(props) {
  let model = null;
  try { model = props.build(); } catch (e) { console.error('[Analytics] KPI model failed:', props.metricKey, e); model = null; }
  if (!model) return ReactDOM.createPortal((
    <div className="kx-bg" onMouseDown={e => { if (e.target === e.currentTarget) props.onClose(); }}>
      <aside className="kx-sheet" role="dialog" aria-modal="true">
        <div className="kx-hd"><div className="kx-hd-t"><div className="kx-hd-title">Unable to calculate</div>
          <div className="kx-hd-sub">This KPI could not be derived from the current records.</div></div>
          <button className="dr-ico x" onClick={props.onClose} aria-label="Close">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M6 6l12 12M18 6L6 18" /></svg></button>
        </div>
        <div className="kx-bd"><div className="kx-empty"><b>Nothing was substituted</b>
          <span>The Analytics Center deliberately shows no number rather than a wrong one. Every other KPI on the page is unaffected.</span></div></div>
      </aside>
    </div>
  ), document.body);
  return <KpiSheet model={model} onClose={props.onClose} onNavigate={props.onNavigate} onMetric={props.onMetric} />;
}

Object.assign(window, { KpiSheet, KpiSheetGuard, KxTable, KxRecord, KxDelta });
