// variance-engine.jsx — Module 1: variance flagging with tolerance configuration

// ── Levenshtein distance ────────────────────────────────────────────────────
function levenshtein(a, b) {
  const m = a.length, n = b.length;
  const dp = [];
  for (let i = 0; i <= m; i++) {
    dp[i] = [i];
    for (let j = 1; j <= n; j++) dp[i][j] = i === 0 ? j : 0;
  }
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      dp[i][j] = a[i-1] === b[j-1]
        ? dp[i-1][j-1]
        : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
    }
  }
  return dp[m][n];
}

// ── Variance constants ──────────────────────────────────────────────────────
const DEFAULT_TOLERANCES = {
  quantity_tolerance_pct: 5,
  price_tolerance_pct: 3,
  price_tolerance_flat: 500,
  footage_tolerance_pct: 5,
};

const FOOTAGE_UOMS = new Set(['LF', 'SF', 'SY']);

const VARIANCE_META = {
  WITHIN_TOLERANCE: { label: 'OK',        tone: 'ok',       pulse: false, short: '✓' },
  SOFT_FLAG:        { label: 'Soft Flag',  tone: 'warn',     pulse: false, short: '~' },
  HARD_FLAG:        { label: 'Hard Flag',  tone: 'bad',      pulse: false, short: '!' },
  CRITICAL:         { label: 'Critical',   tone: 'critical', pulse: true,  short: '!!' },
};

// ── Classify a row against tolerance thresholds ─────────────────────────────
function classifyVariance(row, tols) {
  const { quantity_tolerance_pct, price_tolerance_pct, price_tolerance_flat, footage_tolerance_pct } = tols;

  if (row.flags.missing) return 'WITHIN_TOLERANCE';

  if (row.flags.added) {
    if (Math.abs(row.invoicedTotal) > 10000) return 'CRITICAL';
    return Math.abs(row.invoicedTotal) > 1000 ? 'HARD_FLAG' : 'SOFT_FLAG';
  }

  const absDelta = Math.abs(row.delta);
  if (absDelta > 10000) return 'CRITICAL';

  const isFootage = FOOTAGE_UOMS.has(row.uomNorm);
  const priceTolPct = isFootage ? footage_tolerance_pct : price_tolerance_pct;

  const qtyDeltaPct = (row.quotedQty > 0 && row.invoicedQty != null)
    ? Math.abs((row.invoicedQty - row.quotedQty) / row.quotedQty * 100)
    : 0;
  const priceDeltaPct = row.quotedTotal > 0
    ? Math.abs(row.delta / row.quotedTotal * 100)
    : 0;

  const qtyExceeds   = qtyDeltaPct   > quantity_tolerance_pct;
  const pricePctEx   = priceDeltaPct > priceTolPct;
  const priceFlatEx  = absDelta      > price_tolerance_flat;
  const priceExceeds = pricePctEx || priceFlatEx;

  if (!qtyExceeds && !priceExceeds) return 'WITHIN_TOLERANCE';
  if (qtyExceeds && priceExceeds)   return 'CRITICAL';

  const qtyFactor   = quantity_tolerance_pct > 0 ? qtyDeltaPct   / quantity_tolerance_pct : 0;
  const pctFactor   = priceTolPct            > 0 ? priceDeltaPct / priceTolPct            : 0;
  const flatFactor  = price_tolerance_flat   > 0 ? absDelta      / price_tolerance_flat   : 0;

  if (qtyFactor >= 2 || pctFactor >= 2 || flatFactor >= 2) return 'HARD_FLAG';
  return 'SOFT_FLAG';
}

// ── Enhanced variance computation ───────────────────────────────────────────
// Uses: exact SKU → exact desc → fuzzy desc (≤2) → cost_code → new charge
function computeVariances(quote, invoices, tolerances, manualOverrides) {
  if (!quote || !invoices) return { rows: [], summary: { totalExposure: 0, flaggedCount: 0, byStatus: {}, rowCount: 0 } };
  const tols = { ...DEFAULT_TOLERANCES, ...(tolerances || {}) };
  const overrides = manualOverrides || {};

  const allInvItems = invoices.flatMap((inv) =>
    inv.items.map((it, idx) => ({
      ...it,
      _id: `${inv.id}::${idx}`,
      _invoiceId: inv.id,
      _invoiceDate: inv.date,
      uomNorm: normalizeUOM(it.uom),
    }))
  );
  const qItems = quote.items.map((it, idx) => ({
    ...it,
    _id: `q::${idx}`,
    uomNorm: normalizeUOM(it.uom),
  }));

  const qMatched   = new Set();
  const invMatched = new Set();
  const pairs = [];

  // Pass 1: manual overrides
  for (const [key] of Object.entries(overrides)) {
    const [qId, iId] = key.split('||');
    const qi = qItems.find((x) => x._id === qId);
    const ii = allInvItems.find((x) => x._id === iId);
    if (qi && ii && !qMatched.has(qId) && !invMatched.has(iId)) {
      pairs.push({ quote: qi, iis: [ii], matchType: 'manual' });
      qMatched.add(qId); invMatched.add(iId);
    }
  }

  // Pass 2: exact SKU (multi-invoice grouping)
  for (const qi of qItems) {
    if (qMatched.has(qi._id)) continue;
    const ms = allInvItems.filter((ii) => !invMatched.has(ii._id) && ii.sku && ii.sku === qi.sku);
    if (ms.length > 0) {
      pairs.push({ quote: qi, iis: ms, matchType: 'exact_sku' });
      qMatched.add(qi._id); ms.forEach((m) => invMatched.add(m._id));
    }
  }

  // Pass 3: exact description
  for (const qi of qItems) {
    if (qMatched.has(qi._id)) continue;
    const descL = (qi.desc || '').toLowerCase();
    const ms = allInvItems.filter((ii) => !invMatched.has(ii._id) && (ii.desc || '').toLowerCase() === descL);
    if (ms.length > 0) {
      pairs.push({ quote: qi, iis: ms, matchType: 'exact_desc' });
      qMatched.add(qi._id); ms.forEach((m) => invMatched.add(m._id));
    }
  }

  // Pass 4: fuzzy description (Levenshtein ≤ 2)
  for (const qi of qItems) {
    if (qMatched.has(qi._id)) continue;
    const descL = (qi.desc || '').toLowerCase();
    let best = null, bestD = 3;
    for (const ii of allInvItems) {
      if (invMatched.has(ii._id)) continue;
      const d = levenshtein(descL, (ii.desc || '').toLowerCase());
      if (d < bestD) { best = ii; bestD = d; }
    }
    if (best) {
      pairs.push({ quote: qi, iis: [best], matchType: 'fuzzy_desc', matchScore: bestD });
      qMatched.add(qi._id); invMatched.add(best._id);
    }
  }

  // Pass 5: cost_code match
  for (const qi of qItems) {
    if (qMatched.has(qi._id) || !qi.cost_code) continue;
    const ms = allInvItems.filter((ii) => !invMatched.has(ii._id) && ii.cost_code && ii.cost_code === qi.cost_code);
    if (ms.length > 0) {
      pairs.push({ quote: qi, iis: ms, matchType: 'cost_code' });
      qMatched.add(qi._id); ms.forEach((m) => invMatched.add(m._id));
    }
  }

  // Unmatched quote items → missing
  for (const qi of qItems) {
    if (!qMatched.has(qi._id)) pairs.push({ quote: qi, iis: [], matchType: 'missing_from_invoice' });
  }

  // Unmatched invoice items → new charges
  for (const ii of allInvItems) {
    if (!invMatched.has(ii._id)) pairs.push({ quote: null, iis: [ii], matchType: 'new_charge' });
  }

  // Build rows
  const rows = pairs.map((pair) => {
    const { quote: qi, iis, matchType } = pair;
    const uomNorm     = normalizeUOM(qi?.uom || iis[0]?.uom || null);
    const quotedQty   = qi?.qty ?? null;
    const quotedUnit  = qi?.unit ?? null;
    const quotedTotal = qi ? +(qi.qty * qi.unit).toFixed(2) : 0;
    const invQty      = iis.reduce((s, ii) => s + (ii.qty || 0), 0);
    const invLineSum  = +iis.reduce((s, ii) => s + (ii.qty * ii.unit), 0).toFixed(2);
    const invAvgUnit  = invQty > 0 ? +(invLineSum / invQty).toFixed(4) : null;
    const delta       = +(invLineSum - quotedTotal).toFixed(2);

    const flags = {
      price:   qi && iis.length > 0 && invAvgUnit !== quotedUnit,
      qty:     qi && iis.length > 0 && invQty < (quotedQty || 0),
      over:    qi && iis.length > 0 && invQty > (quotedQty || 0),
      missing: matchType === 'missing_from_invoice',
      added:   matchType === 'new_charge',
    };

    const row = {
      key:          qi?._id || iis[0]?._id || Math.random().toString(36).slice(2),
      sku:          qi?.sku || iis[0]?.sku || '—',
      desc:         qi?.desc || iis[0]?.desc || '',
      cost_code:    qi?.cost_code || iis[0]?.cost_code || null,
      phase:        qi?.phase   || iis[0]?.phase   || null,
      uomNorm,
      quotedQty, quotedUnit, quotedTotal,
      invoicedQty: invQty, invoicedTotal: invLineSum, invoicedAvgUnit: invAvgUnit,
      delta, flags, matchType, matchScore: pair.matchScore,
      breakdown: iis.map((ii) => ({
        invoiceId: ii._invoiceId, date: ii._invoiceDate,
        qty: ii.qty, unit: ii.unit, lineTotal: +(ii.qty * ii.unit).toFixed(2),
      })),
    };
    row.varianceStatus = classifyVariance(row, tols);
    return row;
  });

  // Summary
  const flagged = rows.filter((r) => r.varianceStatus !== 'WITHIN_TOLERANCE');
  const byStatus = {
    WITHIN_TOLERANCE: 0, SOFT_FLAG: 0, HARD_FLAG: 0, CRITICAL: 0,
  };
  for (const r of rows) byStatus[r.varianceStatus] = (byStatus[r.varianceStatus] || 0) + 1;

  return {
    rows,
    summary: {
      rowCount:      rows.length,
      flaggedCount:  flagged.length,
      totalExposure: +flagged.reduce((s, r) => s + Math.abs(r.delta), 0).toFixed(2),
      byStatus,
    },
  };
}

// ── VarianceBadge ───────────────────────────────────────────────────────────
function VarianceBadge({ status }) {
  const m = VARIANCE_META[status] || VARIANCE_META.SOFT_FLAG;
  return (
    <span className={`vbadge vbadge-${m.tone}${m.pulse ? ' vbadge-pulse' : ''}`}>
      {m.label}
    </span>
  );
}

// ── VarianceSummaryBanner ───────────────────────────────────────────────────
function VarianceSummaryBanner({ summary, onOpenTolerance }) {
  const { flaggedCount, totalExposure, byStatus } = summary;
  const allOk = flaggedCount === 0;
  return (
    <div className={`vsb ${allOk ? 'vsb-ok' : byStatus.CRITICAL > 0 ? 'vsb-critical' : 'vsb-flagged'}`}>
      <div className="vsb-main">
        {allOk
          ? <><Icon name="check" size={16} /><span className="vsb-ok-txt">All line items within tolerance</span></>
          : <>
              <div className="vsb-stat">
                <span className="vsb-v num">{flaggedCount}</span>
                <span className="vsb-l">item{flaggedCount !== 1 ? 's' : ''} flagged</span>
              </div>
              <div className="vsb-sep" />
              <div className="vsb-stat">
                <span className="vsb-v num">{fmtUSD(totalExposure)}</span>
                <span className="vsb-l">total exposure</span>
              </div>
              <div className="vsb-chips">
                {byStatus.CRITICAL  > 0 && <span className="vsb-chip vsb-chip-critical">{byStatus.CRITICAL} CRITICAL</span>}
                {byStatus.HARD_FLAG > 0 && <span className="vsb-chip vsb-chip-bad">{byStatus.HARD_FLAG} Hard</span>}
                {byStatus.SOFT_FLAG > 0 && <span className="vsb-chip vsb-chip-warn">{byStatus.SOFT_FLAG} Soft</span>}
              </div>
            </>
        }
      </div>
      <button className="btn btn-ghost btn-sm vsb-gear" onClick={onOpenTolerance} title="Tolerance settings">
        <Icon name="rule" size={14} /> Tolerances
      </button>
    </div>
  );
}

// ── ToleranceSettingsPanel ──────────────────────────────────────────────────
function ToleranceSettingsPanel({ tolerances, onSave, onSaveGlobal, onClose }) {
  const [local, setLocal] = React.useState({ ...DEFAULT_TOLERANCES, ...tolerances });
  const set = (k, v) => setLocal((p) => ({ ...p, [k]: +v || 0 }));

  const Field = ({ label, field, suffix }) => (
    <div className="tsp-field">
      <label className="tsp-label">{label}</label>
      <div className="tsp-input-wrap">
        <input
          type="number" min="0" step={suffix === '%' ? '0.5' : '50'}
          className="input tsp-input"
          value={local[field]}
          onChange={(e) => set(field, e.target.value)}
        />
        <span className="tsp-suffix">{suffix}</span>
      </div>
    </div>
  );

  return (
    <div className="tsp card">
      <div className="tsp-hd">
        <div>
          <div className="tsp-tag mono">TOLERANCE SETTINGS</div>
          <div className="tsp-title">Flag thresholds for this job</div>
        </div>
        <button className="btn btn-ghost btn-sm" onClick={onClose}><Icon name="x" size={13}/></button>
      </div>
      <div className="tsp-body">
        <Field label="Quantity tolerance"       field="quantity_tolerance_pct" suffix="%" />
        <Field label="Price tolerance (percent)" field="price_tolerance_pct"    suffix="%" />
        <Field label="Price tolerance (flat)"    field="price_tolerance_flat"   suffix="$" />
        <Field label="Footage tolerance (LF/SF/SY)" field="footage_tolerance_pct" suffix="%" />
        <div className="tsp-hint">
          Items are flagged when <em>either</em> the % or flat $ threshold is exceeded.
          Exceeding both marks as CRITICAL.
        </div>
      </div>
      <div className="tsp-foot">
        <button className="btn btn-ghost btn-sm" onClick={() => onSaveGlobal && onSaveGlobal(local)}>
          Set as global default
        </button>
        <button className="btn btn-primary btn-sm" onClick={() => { onSave(local); onClose(); }}>
          Save for this job
        </button>
      </div>
    </div>
  );
}

// ── VarianceFilterBar ───────────────────────────────────────────────────────
function VarianceFilterBar({ filter, onFilter, counts, onToggleFlaggedOnly, flaggedOnly }) {
  const STATUS_OPTS = [
    { key: 'all',               label: 'All' },
    { key: 'CRITICAL',          label: 'Critical' },
    { key: 'HARD_FLAG',         label: 'Hard Flag' },
    { key: 'SOFT_FLAG',         label: 'Soft Flag' },
    { key: 'WITHIN_TOLERANCE',  label: 'OK' },
  ];
  return (
    <div className="vfb">
      <div className="vfb-filters">
        {STATUS_OPTS.map(({ key, label }) => (
          <button key={key} className={`vfb-btn ${filter === key ? 'is-on' : ''}`} onClick={() => onFilter(key)}>
            {label}
            <span className="vfb-count num">{key === 'all' ? counts.total : (counts[key] || 0)}</span>
          </button>
        ))}
      </div>
      <button className={`vfb-toggle ${flaggedOnly ? 'is-on' : ''}`} onClick={onToggleFlaggedOnly}>
        <Icon name="flag" size={12} /> Flagged only
      </button>
    </div>
  );
}

// ── VarianceLineTable ───────────────────────────────────────────────────────
function VarianceLineTable({ rows, filter, flaggedOnly, sortKey, sortDir, onSort }) {
  const filtered = React.useMemo(() => {
    let r = rows;
    if (filter && filter !== 'all') r = r.filter((x) => x.varianceStatus === filter);
    if (flaggedOnly) r = r.filter((x) => x.varianceStatus !== 'WITHIN_TOLERANCE');
    return r;
  }, [rows, filter, flaggedOnly]);

  const sorted = React.useMemo(() => {
    if (!sortKey) return filtered;
    return [...filtered].sort((a, b) => {
      let av = a[sortKey], bv = b[sortKey];
      if (sortKey === 'absDelta') { av = Math.abs(a.delta); bv = Math.abs(b.delta); }
      if (sortKey === 'deltaPct') {
        av = a.quotedTotal > 0 ? Math.abs(a.delta / a.quotedTotal) : 0;
        bv = b.quotedTotal > 0 ? Math.abs(b.delta / b.quotedTotal) : 0;
      }
      return sortDir === 'asc' ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1);
    });
  }, [filtered, sortKey, sortDir]);

  const SortBtn = ({ k, label }) => (
    <button className={`vlt-sort-btn ${sortKey === k ? 'is-on' : ''}`} onClick={() => onSort(k)}>
      {label}{sortKey === k ? (sortDir === 'asc' ? ' ↑' : ' ↓') : ''}
    </button>
  );

  const pctStr = (num, denom) =>
    denom > 0 ? ((num / denom) * 100).toFixed(1) + '%' : '—';

  if (sorted.length === 0) {
    return (
      <div className="vlt-empty">
        <Icon name="check" size={20} />
        <span>No items match this filter.</span>
      </div>
    );
  }

  return (
    <div className="vlt">
      <div className="vlt-head">
        <div className="vlt-h vlt-h-desc">Description</div>
        <div className="vlt-h vlt-h-qty">Qtd qty</div>
        <div className="vlt-h vlt-h-qty">Inv qty</div>
        <div className="vlt-h vlt-h-qty">Qty Δ%</div>
        <div className="vlt-h vlt-h-num">Quoted $</div>
        <div className="vlt-h vlt-h-num">Invoiced $</div>
        <div className="vlt-h vlt-h-num">
          <SortBtn k="absDelta" label="$ Δ" />
        </div>
        <div className="vlt-h vlt-h-num">
          <SortBtn k="deltaPct" label="Δ %" />
        </div>
        <div className="vlt-h vlt-h-st">Status</div>
      </div>

      <div className="vlt-body">
        {sorted.map((r) => {
          const deltaPct = r.quotedTotal > 0 ? r.delta / r.quotedTotal * 100 : null;
          const qtyDeltaPct = (r.quotedQty > 0 && r.invoicedQty != null)
            ? (r.invoicedQty - r.quotedQty) / r.quotedQty * 100
            : null;
          const tone = VARIANCE_META[r.varianceStatus]?.tone || 'ok';
          return (
            <div key={r.key} className={`vlt-row vlt-row-${tone}`}>
              <div className="vlt-c vlt-c-desc">
                <span className="vlt-desc" title={r.desc}>{r.desc}</span>
                {r.sku !== '—' && <span className="vlt-sku mono">{r.sku}</span>}
                {r.matchType === 'fuzzy_desc' && <span className="vlt-match-badge">fuzzy</span>}
                {r.matchType === 'new_charge' && <span className="vlt-match-badge vlt-new">new charge</span>}
              </div>
              <div className="vlt-c vlt-c-qty num">
                {r.quotedQty != null ? r.quotedQty.toLocaleString() : <span className="vlt-dash">—</span>}
                {r.uomNorm && <span className="vlt-uom"> {r.uomNorm}</span>}
              </div>
              <div className={`vlt-c vlt-c-qty num ${r.flags.qty || r.flags.over ? 'vlt-flag' : ''}`}>
                {r.invoicedQty > 0 ? r.invoicedQty.toLocaleString() : <span className="vlt-dash">—</span>}
              </div>
              <div className={`vlt-c vlt-c-qty num ${qtyDeltaPct != null && Math.abs(qtyDeltaPct) > 0 ? 'vlt-flag' : ''}`}>
                {qtyDeltaPct != null ? (qtyDeltaPct >= 0 ? '+' : '') + qtyDeltaPct.toFixed(1) + '%' : '—'}
              </div>
              <div className="vlt-c vlt-c-num num">
                {r.quotedTotal > 0 ? fmtUSD(r.quotedTotal) : <span className="vlt-dash">—</span>}
              </div>
              <div className="vlt-c vlt-c-num num">
                {r.invoicedTotal > 0 ? fmtUSD(r.invoicedTotal) : <span className="vlt-dash">—</span>}
              </div>
              <div className={`vlt-c vlt-c-num num ${r.delta > 0 ? 'vlt-pos' : r.delta < 0 ? 'vlt-neg' : ''}`}>
                {r.delta !== 0 ? fmtDelta(r.delta) : '—'}
              </div>
              <div className={`vlt-c vlt-c-num num ${deltaPct != null && Math.abs(deltaPct) > 0 ? (r.delta > 0 ? 'vlt-pos' : 'vlt-neg') : ''}`}>
                {deltaPct != null && deltaPct !== 0 ? (deltaPct >= 0 ? '+' : '') + deltaPct.toFixed(1) + '%' : '—'}
              </div>
              <div className="vlt-c vlt-c-st">
                <VarianceBadge status={r.varianceStatus} />
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ── FootageReconcTable ──────────────────────────────────────────────────────
function FootageReconcTable({ rows }) {
  const footageRows = rows.filter((r) => FOOTAGE_UOMS.has(r.uomNorm) && !r.flags.added);
  if (footageRows.length === 0) return null;

  const totals = footageRows.reduce(
    (s, r) => ({
      quoted:   s.quoted   + (r.quotedQty   || 0),
      invoiced: s.invoiced + (r.invoicedQty || 0),
    }),
    { quoted: 0, invoiced: 0 }
  );
  totals.delta = totals.invoiced - totals.quoted;

  return (
    <div className="frt card">
      <div className="frt-hd">
        <Icon name="arrow" size={14} />
        <span className="frt-title">Footage Reconciliation</span>
        <span className="frt-sub">Items billed by linear / square measure</span>
      </div>
      <div className="frt-table">
        <div className="frt-head">
          <div className="frt-h frt-h-desc">Description</div>
          <div className="frt-h frt-h-uom">UOM</div>
          <div className="frt-h frt-h-qty">Quoted</div>
          <div className="frt-h frt-h-qty">Invoiced</div>
          <div className="frt-h frt-h-qty">Delta</div>
          <div className="frt-h frt-h-qty">Delta %</div>
          <div className="frt-h frt-h-st">Status</div>
        </div>
        {footageRows.map((r) => {
          const delta   = (r.invoicedQty || 0) - (r.quotedQty || 0);
          const deltaPct = r.quotedQty > 0 ? delta / r.quotedQty * 100 : 0;
          return (
            <div key={r.key} className="frt-row">
              <div className="frt-c frt-c-desc" title={r.desc}>{r.desc}</div>
              <div className="frt-c frt-c-uom mono">{r.uomNorm}</div>
              <div className="frt-c frt-c-qty num">{r.quotedQty?.toLocaleString() ?? '—'}</div>
              <div className="frt-c frt-c-qty num">{r.invoicedQty > 0 ? r.invoicedQty.toLocaleString() : '—'}</div>
              <div className={`frt-c frt-c-qty num ${delta > 0 ? 'frt-pos' : delta < 0 ? 'frt-neg' : ''}`}>
                {delta !== 0 ? (delta > 0 ? '+' : '') + delta.toLocaleString() : '—'}
              </div>
              <div className={`frt-c frt-c-qty num ${Math.abs(deltaPct) > 0 ? (delta > 0 ? 'frt-pos' : 'frt-neg') : ''}`}>
                {deltaPct !== 0 ? (deltaPct > 0 ? '+' : '') + deltaPct.toFixed(1) + '%' : '—'}
              </div>
              <div className="frt-c frt-c-st"><VarianceBadge status={r.varianceStatus} /></div>
            </div>
          );
        })}
        <div className="frt-foot">
          <div className="frt-f frt-f-lbl">Totals</div>
          <div className="frt-f frt-f-qty num">{totals.quoted.toLocaleString()}</div>
          <div className="frt-f frt-f-qty num">{totals.invoiced.toLocaleString()}</div>
          <div className={`frt-f frt-f-qty num ${totals.delta > 0 ? 'frt-pos' : totals.delta < 0 ? 'frt-neg' : ''}`}>
            {totals.delta !== 0 ? (totals.delta > 0 ? '+' : '') + totals.delta.toLocaleString() : '—'}
          </div>
          <div className="frt-f" />
          <div className="frt-f" />
        </div>
      </div>
    </div>
  );
}

// ── VarianceView ─────────────────────────────────────────────────────────────
function VarianceView({ job, userId }) {
  const [quote, setQuote]         = React.useState(null);
  const [invoices, setInvoices]   = React.useState([]);
  const [synced, setSynced]       = React.useState(false);
  const [tolerances, setTols]     = React.useState(DEFAULT_TOLERANCES);
  const [showTolPanel, setShowTol] = React.useState(false);
  const [filter, setFilter]       = React.useState('all');
  const [flaggedOnly, setFlagged] = React.useState(false);
  const [sortKey, setSortKey]     = React.useState('absDelta');
  const [sortDir, setSortDir]     = React.useState('desc');

  const isSample = job.id === '_sample';
  const jobRef = React.useMemo(
    () => !isSample ? fbDb.collection('users').doc(userId).collection('jobs').doc(job.id) : null,
    [userId, job.id]
  );

  React.useEffect(() => {
    if (isSample) {
      setQuote(INVOICE_A);
      setInvoices([INVOICE_B]);
      setSynced(true);
      return;
    }
    setSynced(false);
    jobRef.get().then((doc) => {
      if (doc.exists) {
        const d = doc.data();
        setQuote(d.quote ?? null);
        setInvoices(d.invoices ?? []);
        setTols({ ...DEFAULT_TOLERANCES, ...(d.tolerances || {}) });
      }
      setSynced(true);
    }).catch(() => setSynced(true));
  }, [job.id]);

  const saveTolerance = (tols) => {
    setTols(tols);
    if (!isSample) jobRef.update({ tolerances: tols }).catch(console.error);
  };

  const handleSort = (k) => {
    if (sortKey === k) setSortDir((d) => d === 'asc' ? 'desc' : 'asc');
    else { setSortKey(k); setSortDir('desc'); }
  };

  const result = React.useMemo(
    () => quote && invoices.length > 0 ? computeVariances(quote, invoices, tolerances) : null,
    [quote, invoices, tolerances]
  );

  const counts = React.useMemo(() => {
    if (!result) return { total: 0 };
    const c = { total: result.rows.length, ...result.summary.byStatus };
    return c;
  }, [result]);

  if (!synced) {
    return (
      <div className="vv-loading">
        <div className="vv-spin" />
        <VarianceEngineStyles />
      </div>
    );
  }

  if (!quote) {
    return (
      <div className="vv vv-empty">
        <div className="vv-empty-icon"><Icon name="delta" size={32} /></div>
        <div className="vv-empty-h">No quote loaded</div>
        <div className="vv-empty-sub">Upload a quote in the Budget tab to enable variance analysis.</div>
        <VarianceEngineStyles />
      </div>
    );
  }

  if (invoices.length === 0) {
    return (
      <div className="vv vv-empty">
        <div className="vv-empty-icon"><Icon name="file" size={32} /></div>
        <div className="vv-empty-h">No invoices yet</div>
        <div className="vv-empty-sub">Add invoices in the Budget tab — variance analysis will appear here.</div>
        <VarianceEngineStyles />
      </div>
    );
  }

  return (
    <div className="vv">
      <div className="vv-hero">
        <div className="vv-hero-tag mono">JOB {job.jobNumber} · VARIANCES</div>
        <h1 className="vv-hero-h">Variance Analysis</h1>
        <p className="vv-hero-sub">
          Line-item comparison of your quote baseline against all invoices, with configurable tolerance thresholds.
        </p>
      </div>

      {result && (
        <>
          <VarianceSummaryBanner summary={result.summary} onOpenTolerance={() => setShowTol((v) => !v)} />

          {showTolPanel && (
            <ToleranceSettingsPanel
              tolerances={tolerances}
              onSave={saveTolerance}
              onSaveGlobal={(t) => {
                saveTolerance(t);
                if (!isSample) {
                  fbDb.collection('users').doc(userId)
                    .set({ globalTolerances: t }, { merge: true })
                    .catch(console.error);
                }
              }}
              onClose={() => setShowTol(false)}
            />
          )}

          <VarianceFilterBar
            filter={filter} onFilter={setFilter}
            counts={counts}
            flaggedOnly={flaggedOnly} onToggleFlaggedOnly={() => setFlagged((v) => !v)}
          />

          <VarianceLineTable
            rows={result.rows}
            filter={filter} flaggedOnly={flaggedOnly}
            sortKey={sortKey} sortDir={sortDir} onSort={handleSort}
          />

          <FootageReconcTable rows={result.rows} />
        </>
      )}

      <VarianceEngineStyles />
    </div>
  );
}

const VarianceEngineStyles = () => (
  <style>{`
    /* ── Loading / empty ─────────────────────────────────────────── */
    .vv { max-width: 1680px; margin: 0 auto; padding: 48px 24px 80px; display: flex; flex-direction: column; gap: 18px; }
    .vv-loading { display: flex; align-items: center; justify-content: center; min-height: 60vh; }
    .vv-spin { width: 28px; height: 28px; border-radius: 50%; border: 2.5px solid var(--border); border-top-color: var(--accent); animation: dz-spin .7s linear infinite; }
    .vv-empty { align-items: center; justify-content: center; min-height: 40vh; text-align: center; }
    .vv-empty-icon { color: var(--fg-dim); opacity: 0.3; }
    .vv-empty-h   { font-size: 15px; font-weight: 500; color: var(--fg-muted); }
    .vv-empty-sub { font-size: 13px; color: var(--fg-dim); max-width: 340px; line-height: 1.55; }

    /* ── Hero ────────────────────────────────────────────────────── */
    .vv-hero-tag { font-size: 10.5px; letter-spacing: 0.14em; color: var(--fg-dim); margin-bottom: 6px; }
    .vv-hero-h   { font-size: 30px; font-weight: 600; letter-spacing: -0.025em; margin: 0 0 8px; }
    .vv-hero-sub { font-size: 14.5px; color: var(--fg-muted); margin: 0; line-height: 1.55; max-width: 560px; }

    /* ── VarianceBadge ───────────────────────────────────────────── */
    .vbadge {
      display: inline-flex; align-items: center;
      height: 20px; padding: 0 8px; border-radius: 999px;
      font-size: 10.5px; font-weight: 600; font-family: "IBM Plex Mono", monospace;
      letter-spacing: 0.04em; white-space: nowrap; border: 1px solid transparent;
    }
    .vbadge-ok       { background: var(--ok-bg);   color: var(--ok);   border-color: oklch(from var(--ok)   l calc(c*.5) h / .35); }
    .vbadge-warn     { background: var(--warn-bg); color: var(--warn); border-color: oklch(from var(--warn) l calc(c*.5) h / .35); }
    .vbadge-bad      { background: var(--bad-bg);  color: var(--bad);  border-color: oklch(from var(--bad)  l calc(c*.5) h / .35); }
    .vbadge-critical { background: var(--bad-bg);  color: var(--bad);  border-color: oklch(from var(--bad)  l calc(c*.5) h / .35); }
    .vbadge-pulse    { animation: vbadge-pulse 1.4s ease-in-out infinite; }
    @keyframes vbadge-pulse {
      0%, 100% { box-shadow: 0 0 0 0 oklch(from var(--bad) l c h / .4); }
      50%      { box-shadow: 0 0 0 5px oklch(from var(--bad) l c h / 0); }
    }

    /* ── Summary Banner ──────────────────────────────────────────── */
    .vsb {
      display: flex; align-items: center; justify-content: space-between; gap: 16px;
      padding: 14px 18px; border-radius: var(--r-lg);
      border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow-1);
    }
    .vsb-ok       { border-color: oklch(from var(--ok)   l calc(c*.5) h / .45); background: oklch(from var(--ok)   l c h / 0.04); }
    .vsb-flagged  { border-color: oklch(from var(--warn) l calc(c*.5) h / .45); background: oklch(from var(--warn) l c h / 0.04); }
    .vsb-critical { border-color: oklch(from var(--bad)  l calc(c*.5) h / .45); background: oklch(from var(--bad)  l c h / 0.04); }
    .vsb-main { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
    .vsb-ok-txt { font-size: 13.5px; font-weight: 500; color: var(--ok); }
    .vsb-stat { display: flex; flex-direction: column; gap: 1px; }
    .vsb-v { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
    .vsb-l { font-size: 10.5px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.06em; }
    .vsb-sep { width: 1px; height: 30px; background: var(--border); }
    .vsb-chips { display: flex; gap: 6px; flex-wrap: wrap; }
    .vsb-chip { font-size: 10.5px; padding: 2px 9px; border-radius: 999px; font-weight: 600; border: 1px solid transparent; }
    .vsb-chip-critical { background: var(--bad-bg);  color: var(--bad);  border-color: oklch(from var(--bad)  l calc(c*.5) h / .4); }
    .vsb-chip-bad      { background: var(--bad-bg);  color: var(--bad);  border-color: oklch(from var(--bad)  l calc(c*.5) h / .35); }
    .vsb-chip-warn     { background: var(--warn-bg); color: var(--warn); border-color: oklch(from var(--warn) l calc(c*.5) h / .35); }

    /* ── Tolerance Settings Panel ────────────────────────────────── */
    .tsp { padding: 0; overflow: hidden; }
    .tsp-hd { display: flex; align-items: flex-start; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid var(--border); }
    .tsp-tag { font-size: 10px; letter-spacing: 0.12em; color: var(--fg-dim); margin-bottom: 2px; }
    .tsp-title { font-size: 13.5px; font-weight: 600; }
    .tsp-body { padding: 14px 18px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px 20px; }
    .tsp-field { display: flex; flex-direction: column; gap: 5px; }
    .tsp-label { font-size: 11px; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg-dim); }
    .tsp-input-wrap { display: flex; align-items: center; gap: 8px; }
    .tsp-input { width: 100px; height: 34px; }
    .tsp-suffix { font-size: 13px; color: var(--fg-muted); font-weight: 500; }
    .tsp-hint { grid-column: 1 / -1; font-size: 12px; color: var(--fg-dim); line-height: 1.5; font-style: italic; }
    .tsp-foot { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--border); background: var(--surface-2); }

    /* ── Filter Bar ──────────────────────────────────────────────── */
    .vfb { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
    .vfb-filters { display: flex; gap: 2px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 3px; }
    .vfb-btn { appearance: none; border: 0; background: transparent; color: var(--fg-muted); padding: 5px 10px; border-radius: 4px; font: inherit; font-size: 12.5px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
    .vfb-btn.is-on { background: var(--surface-3); color: var(--fg); }
    .vfb-btn:hover:not(.is-on) { color: var(--fg); }
    .vfb-count { font-size: 11px; background: var(--surface-2); color: var(--fg-dim); padding: 1px 6px; border-radius: 999px; }
    .vfb-btn.is-on .vfb-count { background: var(--bg); }
    .vfb-toggle { appearance: none; border: 1px solid var(--border); background: transparent; color: var(--fg-muted); padding: 5px 12px; border-radius: var(--r); font: inherit; font-size: 12.5px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: all .12s; }
    .vfb-toggle.is-on { background: oklch(from var(--warn) l c h / 0.12); color: var(--warn); border-color: oklch(from var(--warn) l calc(c*.5) h / .4); }
    .vfb-toggle:hover:not(.is-on) { background: var(--surface-2); color: var(--fg); }

    /* ── Variance Line Table ─────────────────────────────────────── */
    .vlt { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-1); }
    .vlt-head, .vlt-row {
      display: grid;
      grid-template-columns: minmax(200px,1fr) 72px 72px 68px 90px 90px 90px 72px 120px;
    }
    .vlt-head { background: var(--surface-2); border-bottom: 1px solid var(--border); }
    .vlt-h { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--fg-dim); font-weight: 500; padding: 8px 10px; border-right: 1px solid var(--border); }
    .vlt-h:last-child { border-right: none; }
    .vlt-h-num, .vlt-h-qty { text-align: right; }
    .vlt-sort-btn { appearance: none; border: 0; background: transparent; color: inherit; font: inherit; font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; cursor: pointer; padding: 0; }
    .vlt-sort-btn.is-on { color: var(--accent); }

    .vlt-body {}
    .vlt-row { border-bottom: 1px solid var(--border); min-height: 34px; position: relative; }
    .vlt-row:last-child { border-bottom: none; }
    .vlt-row-warn::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--warn); }
    .vlt-row-bad::before, .vlt-row-critical::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--bad); }

    .vlt-c { padding: 0 10px; font-size: 11.5px; color: var(--fg); border-right: 1px solid var(--border); display: flex; align-items: center; min-height: 34px; }
    .vlt-c:last-child { border-right: none; }
    .vlt-c-qty, .vlt-c-num { justify-content: flex-end; font-variant-numeric: tabular-nums; font-family: "IBM Plex Mono", monospace; }
    .vlt-c-desc { gap: 6px; overflow: hidden; flex-wrap: wrap; }
    .vlt-c-st { justify-content: flex-start; }
    .vlt-desc { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
    .vlt-sku  { font-size: 10px; color: var(--fg-dim); background: var(--surface-3); border: 1px solid var(--border); padding: 1px 5px; border-radius: 3px; flex-shrink: 0; }
    .vlt-match-badge { font-size: 9.5px; padding: 1px 6px; border-radius: 3px; font-family: "IBM Plex Mono", monospace; flex-shrink: 0; background: var(--info-bg); color: var(--info); border: 1px solid oklch(from var(--info) l calc(c*.4) h / .3); }
    .vlt-new  { background: var(--warn-bg); color: var(--warn); border-color: oklch(from var(--warn) l calc(c*.4) h / .3); }
    .vlt-dash { color: var(--fg-dim); }
    .vlt-uom  { color: var(--fg-dim); font-size: 10px; }
    .vlt-flag { color: var(--warn); font-weight: 500; }
    .vlt-pos  { color: var(--warn); }
    .vlt-neg  { color: var(--info); }
    .vlt-empty { display: flex; align-items: center; gap: 10px; padding: 28px; color: var(--fg-dim); font-size: 13.5px; justify-content: center; }

    /* ── Footage Table ───────────────────────────────────────────── */
    .frt { overflow: hidden; }
    .frt-hd { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
    .frt-title { font-size: 13px; font-weight: 600; }
    .frt-sub { font-size: 12px; color: var(--fg-dim); margin-left: 4px; }
    .frt-table {}
    .frt-head, .frt-row, .frt-foot { display: grid; grid-template-columns: minmax(160px,1fr) 60px 80px 80px 80px 72px 120px; }
    .frt-head { background: var(--surface-2); border-bottom: 1px solid var(--border); }
    .frt-h { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--fg-dim); font-weight: 500; padding: 7px 10px; border-right: 1px solid var(--border); }
    .frt-h:last-child { border-right: none; }
    .frt-h-qty, .frt-h-uom { text-align: right; }
    .frt-row { border-bottom: 1px solid var(--border); }
    .frt-row:last-child { border-bottom: none; }
    .frt-c { padding: 0 10px; font-size: 11.5px; color: var(--fg); border-right: 1px solid var(--border); display: flex; align-items: center; min-height: 32px; }
    .frt-c:last-child { border-right: none; }
    .frt-c-qty, .frt-c-uom { justify-content: flex-end; font-variant-numeric: tabular-nums; }
    .frt-c-desc { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    .frt-pos { color: var(--warn); }
    .frt-neg { color: var(--info); }
    .frt-foot { background: var(--surface-2); border-top: 1px solid var(--border-strong); min-height: 32px; align-items: center; }
    .frt-f { padding: 0 10px; font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; }
    .frt-f-lbl { grid-column: 1 / 3; font-size: 10px; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.06em; }
    .frt-f-qty { text-align: right; }

    @media (max-width: 900px) {
      .tsp-body { grid-template-columns: 1fr; }
      .vlt-head, .vlt-row { grid-template-columns: minmax(140px,1fr) 60px 60px 60px 80px 80px 80px 64px 110px; }
    }
  `}</style>
);

Object.assign(window, {
  levenshtein,
  DEFAULT_TOLERANCES,
  VARIANCE_META,
  classifyVariance,
  computeVariances,
  VarianceBadge,
  VarianceSummaryBanner,
  ToleranceSettingsPanel,
  VarianceFilterBar,
  VarianceLineTable,
  FootageReconcTable,
  VarianceView,
});
