// stmt-parser.jsx — Multi-Statement Parser & Error Handling
// ─────────────────────────────────────────────────────────────────
// Universal ingest pipeline for royalty statements from any source:
// detects vendor signature, picks parser, normalizes lines into a
// canonical schema, surfaces errors with full context + recovery
// actions. Plugs alongside the existing statements-bridge (which
// hydrates pre-parsed statements) for live ingestion of new files.
//
// Pipeline:
//   FILE → SNIFF (detect vendor + format)
//        → PARSE (per-vendor adapter; CSV/TSV/XLSX/JSON/PDF-tabular)
//        → NORMALIZE (canonical line schema)
//        → VALIDATE (row-level + statement-level checks)
//        → MATCH (work/recording/agreement resolution)
//        → COMMIT (write to __STMT_INDEX) or QUARANTINE
//
// Tabs:
//   01 INGEST     — drop zone + run history + currently-processing
//   02 ADAPTERS   — registry of parsers (vendors, signatures, fields)
//   03 ERRORS     — error inbox with severity + recovery actions
//   04 SCHEMA     — canonical line schema + field mapping reference
//
// Exports: window.ScreenStmtParser, window.STMT_PARSER_ENGINE
// ─────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined' || !window.React) return;
  const _S = React.useState, _E = React.useEffect, _M = React.useMemo;

  function Mono({ children, upper, size, color, style, ...rest }) {
    return <span className={'ff-mono' + (upper?' upper':'')} style={{ fontSize: size||11, color: color||'var(--ink)', letterSpacing: upper?'.08em':0, ...style }} {...rest}>{children}</span>;
  }

  // ════════════════════════════════════════════════════════════════
  // CANONICAL LINE SCHEMA
  // ════════════════════════════════════════════════════════════════
  const CANONICAL_FIELDS = [
    { k: 'lineId',         t: 'string',  req: true,  desc: 'Stable unique line ID (vendor txn + row hash)' },
    { k: 'sourceId',       t: 'string',  req: true,  desc: 'Statement source key (e.g. src_ascap_us)' },
    { k: 'periodStart',    t: 'date',    req: true,  desc: 'Earnings period start (ISO 8601)' },
    { k: 'periodEnd',      t: 'date',    req: true,  desc: 'Earnings period end (ISO 8601)' },
    { k: 'productType',    t: 'enum',    req: true,  desc: 'mechanical | performance | sync | neighbour | print | other' },
    { k: 'usageType',      t: 'enum',    req: false, desc: 'stream | download | physical | radio-tv | live | ugc | sync-fee' },
    { k: 'dsp',            t: 'string',  req: false, desc: 'Originating DSP / broadcaster / venue (Spotify, Apple, BBC...)' },
    { k: 'territory',      t: 'iso2',    req: true,  desc: 'ISO-3166 alpha-2 territory of usage' },
    { k: 'workTitle',      t: 'string',  req: false, desc: 'Title as appearing on statement' },
    { k: 'iswc',           t: 'string',  req: false, desc: 'ISWC if present, normalized (T-XXX.XXX.XXX-X)' },
    { k: 'isrc',           t: 'string',  req: false, desc: 'ISRC if recording-level' },
    { k: 'writers',        t: 'string',  req: false, desc: 'Writer name(s) — pipe-separated' },
    { k: 'units',          t: 'number',  req: false, desc: 'Streams / plays / sales count' },
    { k: 'grossAmount',    t: 'number',  req: true,  desc: 'Gross royalty amount in stmt currency' },
    { k: 'netAmount',      t: 'number',  req: false, desc: 'Net to publisher after admin fees' },
    { k: 'currency',       t: 'iso4217', req: true,  desc: 'ISO 4217 currency code (USD, EUR, GBP...)' },
    { k: 'fxToUsd',        t: 'number',  req: false, desc: 'FX rate to USD at booking time' },
    { k: 'sharePct',       t: 'pct',     req: false, desc: 'Claimed share % on the work' },
    { k: 'rightsType',     t: 'enum',    req: false, desc: 'PR | MR | SR (perf / mech / synchro)' },
    { k: 'rawRowIndex',    t: 'number',  req: true,  desc: 'Original row number for traceback' },
    { k: 'rawRow',         t: 'object',  req: false, desc: 'Original row object preserved for diff' },
  ];

  // ════════════════════════════════════════════════════════════════
  // ADAPTER REGISTRY — vendor signatures + parsers
  // ════════════════════════════════════════════════════════════════
  // Each adapter declares:
  //   id, label, vendor, format, sniff(headers, sample) → 0..1 confidence
  //   parse(file, ctx) → { lines: [canonical], warnings, errors }
  //   sample columns
  const ADAPTERS = [
    {
      id: 'ascap-domestic',
      label: 'ASCAP · Domestic',
      vendor: 'ASCAP', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('ascap')) s += 0.4;
        if (has('work id') && has('performances')) s += 0.4;
        if (has('amount') && has('survey source')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['Work ID', 'Title', 'Writers', 'Performances', 'Survey Source', 'Amount', 'Period'],
      mapping: {
        workTitle: 'Title', iswc: 'ISWC', writers: 'Writers',
        units: 'Performances', grossAmount: 'Amount',
        usageType: { from: 'Survey Source', map: { 'Radio Feature': 'radio-tv', 'TV': 'radio-tv', 'Live': 'live', 'Cable': 'radio-tv' } },
      },
    },
    {
      id: 'ascap-intl',
      label: 'ASCAP · International',
      vendor: 'ASCAP', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('ascap') && has('foreign')) s += 0.5;
        if (has('country of') || has('foreign society')) s += 0.3;
        if (has('amount paid') || has('gross amount')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['Foreign Society', 'Country', 'Title', 'ISWC', 'Distribution Type', 'Performances', 'Amount Paid'],
      mapping: {
        workTitle: 'Title', iswc: 'ISWC', territory: 'Country',
        units: 'Performances', grossAmount: 'Amount Paid',
        usageType: { from: 'Distribution Type', default: 'radio-tv' },
      },
    },
    {
      id: 'bmi',
      label: 'BMI · Quarterly',
      vendor: 'BMI', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('bmi work')) s += 0.5;
        if (has('writers') && has('publishers')) s += 0.2;
        if (has('royalty') && has('source of payment')) s += 0.3;
        return Math.min(1, s);
      },
      cols: ['BMI Work#', 'Title', 'Writers', 'Publishers', 'Source of Payment', 'Royalty', 'Period'],
      mapping: {
        workTitle: 'Title', writers: 'Writers',
        grossAmount: 'Royalty',
        dsp: 'Source of Payment',
      },
    },
    {
      id: 'mlc',
      label: 'MLC · Monthly',
      vendor: 'MLC', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('mlc song code') || has('msc')) s += 0.5;
        if (has('iswc') && has('royalty')) s += 0.3;
        if (has('dsp')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['MLC Song Code', 'ISWC', 'Title', 'DSP', 'Streams', 'Royalty Amount', 'Currency'],
      mapping: {
        workTitle: 'Title', iswc: 'ISWC',
        units: 'Streams', grossAmount: 'Royalty Amount',
        currency: 'Currency', dsp: 'DSP', productType: { static: 'mechanical' },
      },
    },
    {
      id: 'hfa',
      label: 'HFA / Rumblefish · Mechanical',
      vendor: 'HFA', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('hfa song')) s += 0.4;
        if (has('licensee') && has('mechanical')) s += 0.4;
        if (has('rate') && has('units')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['HFA Song Code', 'Title', 'Licensee', 'Configuration', 'Units', 'Rate', 'Mechanical Royalty'],
      mapping: {
        workTitle: 'Title', units: 'Units',
        grossAmount: 'Mechanical Royalty', productType: { static: 'mechanical' },
      },
    },
    {
      id: 'soundexchange',
      label: 'SoundExchange · Performance',
      vendor: 'SoundExchange', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('soundexchange') || has('sx ')) s += 0.4;
        if (has('isrc') && has('featured artist')) s += 0.4;
        if (has('royalty')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['ISRC', 'Track Title', 'Featured Artist', 'Service', 'Plays', 'Royalty Amount'],
      mapping: {
        workTitle: 'Track Title', isrc: 'ISRC', dsp: 'Service',
        units: 'Plays', grossAmount: 'Royalty Amount',
        productType: { static: 'neighbour' },
      },
    },
    {
      id: 'rsd-distributor',
      label: 'RSD / Symphonic · Distributor',
      vendor: 'RSD', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('isrc') && has('store') && has('quantity')) s += 0.5;
        if (has('artist') && has('upc')) s += 0.3;
        if (has('country')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['ISRC', 'UPC', 'Track', 'Artist', 'Store', 'Country', 'Quantity', 'Royalty USD'],
      mapping: {
        isrc: 'ISRC', workTitle: 'Track', territory: 'Country',
        dsp: 'Store', units: 'Quantity', grossAmount: 'Royalty USD',
        usageType: { from: 'Store', map: { 'Spotify': 'stream', 'Apple Music': 'stream', 'iTunes': 'download', 'Amazon Music': 'stream', 'YouTube': 'stream' }, default: 'stream' },
      },
    },
    {
      id: 'tiktok-mri',
      label: 'TikTok / MRI · UGC',
      vendor: 'TikTok', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('tiktok')) s += 0.5;
        if (has('iswc') && has('views')) s += 0.3;
        if (has('royalty')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['TikTok Sound ID', 'ISWC', 'Title', 'Views', 'Territory', 'Royalty Amount'],
      mapping: {
        workTitle: 'Title', iswc: 'ISWC', territory: 'Territory',
        units: 'Views', grossAmount: 'Royalty Amount',
        dsp: { static: 'TikTok' }, usageType: { static: 'ugc' },
      },
    },
    {
      id: 'youtube-cms',
      label: 'YouTube · CMS Music Report',
      vendor: 'YouTube', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('asset id') && has('youtube')) s += 0.5;
        if (has('partner revenue') || has('claimed views')) s += 0.5;
        return Math.min(1, s);
      },
      cols: ['Asset ID', 'Asset Title', 'Owned Views', 'Claimed Views', 'Partner Revenue', 'Country'],
      mapping: {
        workTitle: 'Asset Title', territory: 'Country',
        units: 'Claimed Views', grossAmount: 'Partner Revenue',
        dsp: { static: 'YouTube' }, usageType: { static: 'ugc' },
      },
    },
    {
      id: 'prs-cwr-stmt',
      label: 'PRS / MCPS · Statement',
      vendor: 'PRS', format: 'CSV',
      sniff: (h) => {
        if (!h) return 0;
        const set = h.map(s => s.toLowerCase());
        const has = w => set.some(s => s.includes(w));
        let s = 0;
        if (has('prs') || has('mcps')) s += 0.5;
        if (has('tunecode') || has('iswc')) s += 0.3;
        if (has('amount')) s += 0.2;
        return Math.min(1, s);
      },
      cols: ['Tunecode', 'ISWC', 'Title', 'Distribution Source', 'Use Type', 'Amount GBP'],
      mapping: {
        workTitle: 'Title', iswc: 'ISWC',
        dsp: 'Distribution Source', grossAmount: 'Amount GBP',
        currency: { static: 'GBP' },
      },
    },
  ];

  // ════════════════════════════════════════════════════════════════
  // CSV PARSER — RFC 4180 with quote/escape handling
  // ════════════════════════════════════════════════════════════════
  function parseCSV(text) {
    const rows = [];
    let row = [], cell = '', q = false, i = 0;
    while (i < text.length) {
      const c = text[i];
      if (q) {
        if (c === '"' && text[i+1] === '"') { cell += '"'; i += 2; continue; }
        if (c === '"') { q = false; i++; continue; }
        cell += c; i++; continue;
      }
      if (c === '"') { q = true; i++; continue; }
      if (c === ',') { row.push(cell); cell = ''; i++; continue; }
      if (c === '\r') { i++; continue; }
      if (c === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; i++; continue; }
      cell += c; i++;
    }
    if (cell.length > 0 || row.length > 0) { row.push(cell); rows.push(row); }
    return rows;
  }

  function csvToObjects(text) {
    const rows = parseCSV(text);
    if (rows.length === 0) return { headers: [], objs: [] };
    const headers = rows[0].map(h => h.trim());
    const objs = [];
    for (let r = 1; r < rows.length; r++) {
      const obj = {};
      for (let c = 0; c < headers.length; c++) obj[headers[c]] = rows[r][c] != null ? rows[r][c] : '';
      objs.push(obj);
    }
    return { headers, objs };
  }

  // ════════════════════════════════════════════════════════════════
  // ERROR TAXONOMY
  // ════════════════════════════════════════════════════════════════
  const ERROR_KINDS = {
    'unknown-vendor':       { sev: 'warn',  desc: 'No adapter matched the file signature with confidence ≥ 0.4' },
    'low-confidence':       { sev: 'warn',  desc: 'Top adapter matched 0.4–0.6 — review parse output before commit' },
    'missing-required':     { sev: 'err',   desc: 'A required canonical field is empty after mapping' },
    'invalid-currency':     { sev: 'err',   desc: 'Currency code not in ISO 4217 list' },
    'invalid-date':         { sev: 'err',   desc: 'Date column unparseable' },
    'invalid-territory':    { sev: 'warn',  desc: 'Territory not in ISO 3166 alpha-2 (auto-resolved if name)' },
    'invalid-iswc':         { sev: 'warn',  desc: 'ISWC fails check-digit validation' },
    'invalid-isrc':         { sev: 'warn',  desc: 'ISRC fails format validation' },
    'duplicate-line':       { sev: 'warn',  desc: 'Identical (vendor txn id, row hash) seen previously' },
    'amount-out-of-range':  { sev: 'warn',  desc: 'Amount > 4σ from per-source mean — likely fat-finger or unit mistake' },
    'currency-mismatch':    { sev: 'warn',  desc: 'Statement currency differs from line-level currency' },
    'unmatched-work':       { sev: 'info',  desc: 'No matching work in catalog — quarantined for learning matcher' },
    'parse-malformed':      { sev: 'crit',  desc: 'Row could not be parsed (column count mismatch / encoding)' },
    'header-mismatch':      { sev: 'err',   desc: 'Required header column absent' },
    'encoding-error':       { sev: 'crit',  desc: 'File not valid UTF-8 / decoding failed' },
    'empty-file':           { sev: 'err',   desc: 'File contained no rows' },
  };

  // ════════════════════════════════════════════════════════════════
  // ENGINE
  // ════════════════════════════════════════════════════════════════
  function sniff(headers, sample) {
    const scored = ADAPTERS.map(a => ({ adapter: a, score: a.sniff(headers, sample) || 0 }));
    scored.sort((a, b) => b.score - a.score);
    return scored;
  }

  function applyMapping(rowObj, mapping, rowIndex) {
    const out = { rawRowIndex: rowIndex, rawRow: rowObj };
    for (const [k, def] of Object.entries(mapping)) {
      if (typeof def === 'string') {
        out[k] = rowObj[def] != null ? rowObj[def] : '';
      } else if (def && typeof def === 'object') {
        if (def.static !== undefined) out[k] = def.static;
        else if (def.from) {
          const raw = rowObj[def.from] || '';
          if (def.map && def.map[raw] !== undefined) out[k] = def.map[raw];
          else if (def.default !== undefined) out[k] = def.default;
          else out[k] = raw;
        }
      }
    }
    return out;
  }

  // Validators
  const ISO_4217 = ['USD','EUR','GBP','JPY','CAD','AUD','BRL','MXN','KRW','CNY','INR','CHF','SEK','NOK','DKK','TRY','ZAR','SGD','HKD','NZD'];
  function isoTerritory(s) {
    if (!s) return null;
    s = s.trim().toUpperCase();
    if (/^[A-Z]{2}$/.test(s)) return s;
    const NAMES = { 'UNITED STATES':'US','USA':'US','UNITED KINGDOM':'GB','UK':'GB','GERMANY':'DE','FRANCE':'FR','JAPAN':'JP','CANADA':'CA','AUSTRALIA':'AU','BRAZIL':'BR','MEXICO':'MX','SOUTH KOREA':'KR','KOREA':'KR','CHINA':'CN','INDIA':'IN','SPAIN':'ES','ITALY':'IT','NETHERLANDS':'NL','SWEDEN':'SE','NORWAY':'NO','DENMARK':'DK','FINLAND':'FI'};
    return NAMES[s] || null;
  }
  function validateISWC(s) {
    if (!s) return true;
    return /^T-?\d{3}\.?\d{3}\.?\d{3}-?\d$/.test(s.trim());
  }
  function validateISRC(s) {
    if (!s) return true;
    return /^[A-Z]{2}-?[A-Z0-9]{3}-?\d{2}-?\d{5}$/.test(s.trim().toUpperCase());
  }

  function parseAmount(s) {
    if (typeof s === 'number') return s;
    if (!s) return null;
    const cleaned = String(s).replace(/[$£€¥,\s]/g, '');
    const n = parseFloat(cleaned);
    return isNaN(n) ? null : n;
  }

  function ingest(file, opts = {}) {
    // file: { name, text } — text already decoded
    const errors = [];
    const warnings = [];
    const lines = [];
    let adapter = null, confidence = 0;
    const startedAt = Date.now();

    try {
      if (!file || !file.text) {
        errors.push({ kind: 'empty-file', row: -1, msg: 'No content' });
        return { ok: false, errors, warnings, lines, adapter, confidence, ms: 0 };
      }

      const { headers, objs } = csvToObjects(file.text);
      if (objs.length === 0) {
        errors.push({ kind: 'empty-file', row: -1, msg: 'No data rows after header' });
        return { ok: false, errors, warnings, lines, adapter, confidence, ms: Date.now() - startedAt };
      }

      // Sniff
      const sample = objs.slice(0, 5);
      const scored = sniff(headers, sample);
      const top = scored[0];
      if (!top || top.score < 0.4) {
        errors.push({ kind: 'unknown-vendor', row: -1, msg: `Top match ${top ? top.adapter.label : 'none'} @ ${top ? top.score.toFixed(2) : '0'}` });
        return { ok: false, errors, warnings, lines, adapter: top?.adapter || null, confidence: top?.score || 0, ms: Date.now() - startedAt };
      }
      adapter = top.adapter; confidence = top.score;
      if (top.score < 0.6) warnings.push({ kind: 'low-confidence', row: -1, msg: `${adapter.label} @ ${(confidence*100|0)}%` });

      // Header check
      for (const requiredCol of (adapter.cols || [])) {
        const exact = headers.some(h => h.toLowerCase() === requiredCol.toLowerCase());
        if (!exact) {
          warnings.push({ kind: 'header-mismatch', row: -1, msg: `Expected column "${requiredCol}"` });
        }
      }

      // Per-row parse
      const seenHashes = new Set();
      let amountSum = 0, amountCount = 0;
      const allAmounts = [];

      objs.forEach((rowObj, idx) => {
        try {
          const mapped = applyMapping(rowObj, adapter.mapping || {}, idx);

          // Required-field check
          for (const f of CANONICAL_FIELDS.filter(f => f.req)) {
            if (mapped[f.k] == null || mapped[f.k] === '') {
              // allow inferred fields
              if (f.k === 'sourceId' || f.k === 'periodStart' || f.k === 'periodEnd' || f.k === 'lineId') continue;
              if (f.k === 'productType' && adapter.mapping?.productType?.static) continue;
              warnings.push({ kind: 'missing-required', row: idx, msg: `Missing ${f.k}`, line: mapped });
            }
          }

          // Currency
          if (mapped.currency) {
            const cu = String(mapped.currency).toUpperCase();
            if (!ISO_4217.includes(cu)) errors.push({ kind: 'invalid-currency', row: idx, msg: `Got "${mapped.currency}"`, line: mapped });
            else mapped.currency = cu;
          } else {
            // default for vendor
            mapped.currency = (adapter.id === 'prs-cwr-stmt') ? 'GBP' : 'USD';
          }

          // Territory
          if (mapped.territory) {
            const iso = isoTerritory(mapped.territory);
            if (!iso) warnings.push({ kind: 'invalid-territory', row: idx, msg: `Could not resolve "${mapped.territory}"`, line: mapped });
            else mapped.territory = iso;
          }

          // ISWC / ISRC
          if (mapped.iswc && !validateISWC(mapped.iswc)) warnings.push({ kind: 'invalid-iswc', row: idx, msg: mapped.iswc, line: mapped });
          if (mapped.isrc && !validateISRC(mapped.isrc)) warnings.push({ kind: 'invalid-isrc', row: idx, msg: mapped.isrc, line: mapped });

          // Amount
          const amt = parseAmount(mapped.grossAmount);
          if (amt === null) {
            errors.push({ kind: 'missing-required', row: idx, msg: `grossAmount unparseable: "${mapped.grossAmount}"`, line: mapped });
            return;
          }
          mapped.grossAmount = amt;
          amountSum += amt; amountCount++; allAmounts.push(amt);

          // Units coercion
          if (mapped.units != null && mapped.units !== '') {
            const n = parseInt(String(mapped.units).replace(/,/g, ''), 10);
            mapped.units = isNaN(n) ? null : n;
          }

          // Dedupe
          const hash = `${mapped.workTitle || ''}|${mapped.iswc || ''}|${mapped.isrc || ''}|${amt}|${mapped.dsp || ''}|${mapped.territory || ''}|${idx}`;
          if (seenHashes.has(hash)) {
            warnings.push({ kind: 'duplicate-line', row: idx, msg: 'Identical to prior row', line: mapped });
          }
          seenHashes.add(hash);

          // ID
          mapped.lineId = `ln_${(file.name || 'stmt').slice(0, 8)}_${idx}`;
          mapped.sourceId = opts.sourceId || `src_${adapter.id}`;
          if (adapter.mapping?.productType?.static) mapped.productType = adapter.mapping.productType.static;
          if (!mapped.productType) mapped.productType = adapter.id.includes('mech') ? 'mechanical' : adapter.id.includes('hfa') ? 'mechanical' : adapter.id.includes('mlc') ? 'mechanical' : adapter.id.includes('ascap') || adapter.id.includes('bmi') || adapter.id.includes('prs') ? 'performance' : 'performance';

          lines.push(mapped);
        } catch (e) {
          errors.push({ kind: 'parse-malformed', row: idx, msg: e.message || String(e) });
        }
      });

      // Statement-level outlier detection
      if (amountCount > 8) {
        const mean = amountSum / amountCount;
        const variance = allAmounts.reduce((s, x) => s + (x - mean) ** 2, 0) / amountCount;
        const sd = Math.sqrt(variance);
        if (sd > 0) {
          allAmounts.forEach((amt, i) => {
            const z = Math.abs(amt - mean) / sd;
            if (z > 4) warnings.push({ kind: 'amount-out-of-range', row: i, msg: `${amt.toFixed(2)} · z=${z.toFixed(1)}σ`, line: lines[i] });
          });
        }
      }

      return { ok: errors.length === 0, errors, warnings, lines, adapter, confidence, ms: Date.now() - startedAt };
    } catch (e) {
      errors.push({ kind: 'parse-malformed', row: -1, msg: e.message || String(e) });
      return { ok: false, errors, warnings, lines, adapter, confidence, ms: Date.now() - startedAt };
    }
  }

  // Engine — exposed
  const ENGINE = { ADAPTERS, CANONICAL_FIELDS, ERROR_KINDS, ingest, sniff, parseCSV, csvToObjects };
  window.STMT_PARSER_ENGINE = ENGINE;

  // ════════════════════════════════════════════════════════════════
  // RUN HISTORY (simulated — ingests of seed statements)
  // ════════════════════════════════════════════════════════════════
  // Seed a few realistic recent runs by inspecting __STMT_INDEX
  function seedRunHistory() {
    if (window.__STMT_PARSER_RUNS) return window.__STMT_PARSER_RUNS;
    const runs = [];
    const idx = window.__STMT_INDEX;
    if (idx && idx.statements) {
      idx.statements.forEach((s, i) => {
        const ad = ADAPTERS.find(a => a.id === s.parser) || ADAPTERS[i % ADAPTERS.length];
        // Generate plausible error counts
        const errs = i % 7 === 3 ? Math.floor(Math.random() * 4) : 0;
        const warns = Math.floor(Math.random() * 12) + (i % 5 === 2 ? 8 : 0);
        runs.push({
          id: `run_${s.id}`,
          fileName: `${ad.id}_${s.period || 'q'}.csv`,
          startedAt: s.processedDate || (Date.now() - i * 86400_000),
          finishedAt: s.processedDate ? s.processedDate + 1500 + Math.random() * 4000 : Date.now() - i * 86400_000 + 2000,
          adapter: ad,
          confidence: 0.78 + Math.random() * 0.2,
          totalLines: s.lineCount || s.lines?.length || 0,
          errorCount: errs,
          warningCount: warns,
          status: errs > 0 ? 'partial' : 'ok',
          stmtId: s.id,
        });
      });
    }
    // Also seed a couple of failed/quarantined examples
    runs.unshift({
      id: 'run_failed_demo',
      fileName: 'unknown_vendor_2026q1.csv',
      startedAt: Date.now() - 2 * 86400_000,
      finishedAt: Date.now() - 2 * 86400_000 + 800,
      adapter: null,
      confidence: 0.31,
      totalLines: 0,
      errorCount: 1,
      warningCount: 0,
      status: 'failed',
      errorSample: 'unknown-vendor',
    });
    runs.unshift({
      id: 'run_partial_demo',
      fileName: 'spotify_q4_2025_publisher.csv',
      startedAt: Date.now() - 4 * 3600_000,
      finishedAt: Date.now() - 4 * 3600_000 + 12_400,
      adapter: ADAPTERS.find(a => a.id === 'rsd-distributor'),
      confidence: 0.92,
      totalLines: 28_417,
      errorCount: 14,
      warningCount: 312,
      status: 'partial',
    });
    runs.sort((a, b) => b.startedAt - a.startedAt);
    window.__STMT_PARSER_RUNS = runs;
    return runs;
  }

  // ════════════════════════════════════════════════════════════════
  // UI
  // ════════════════════════════════════════════════════════════════
  function Cell({ label, value, sub, tone }) {
    return (
      <div style={{ padding: '18px 22px', borderRight: '1px solid var(--rule)' }}>
        <Mono upper size={9} color="var(--ink-3)">{label}</Mono>
        <div className="ff-display" style={{ fontSize: 24, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4, color: tone || 'var(--ink)' }}>{value}</div>
        <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 3 }}>{sub}</div>
      </div>
    );
  }

  // ─── 01 INGEST TAB ────────────────────────────────────────────
  function IngestTab({ onSelectRun }) {
    const runs = seedRunHistory();
    const [draggedOver, setDraggedOver] = _S(false);
    const [activeRun, setActiveRun] = _S(null);
    const [paste, setPaste] = _S('');

    const stats = _M(() => {
      const total = runs.length;
      const ok = runs.filter(r => r.status === 'ok').length;
      const partial = runs.filter(r => r.status === 'partial').length;
      const failed = runs.filter(r => r.status === 'failed').length;
      const totalLines = runs.reduce((s, r) => s + (r.totalLines || 0), 0);
      return { total, ok, partial, failed, totalLines };
    }, [runs]);

    function handleFile(e) {
      const file = e.target.files?.[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = () => {
        const result = ingest({ name: file.name, text: reader.result });
        runs.unshift({
          id: 'run_live_' + Date.now(),
          fileName: file.name,
          startedAt: Date.now(),
          finishedAt: Date.now() + result.ms,
          adapter: result.adapter,
          confidence: result.confidence,
          totalLines: result.lines.length,
          errorCount: result.errors.length,
          warningCount: result.warnings.length,
          status: result.errors.length === 0 ? 'ok' : (result.lines.length === 0 ? 'failed' : 'partial'),
          live: true,
          result,
        });
        setActiveRun(runs[0]);
      };
      reader.readAsText(file);
    }

    function runPaste() {
      if (!paste.trim()) return;
      const result = ingest({ name: 'pasted.csv', text: paste });
      const newRun = {
        id: 'run_paste_' + Date.now(),
        fileName: 'pasted.csv',
        startedAt: Date.now(),
        finishedAt: Date.now() + result.ms,
        adapter: result.adapter,
        confidence: result.confidence,
        totalLines: result.lines.length,
        errorCount: result.errors.length,
        warningCount: result.warnings.length,
        status: result.errors.length === 0 ? 'ok' : (result.lines.length === 0 ? 'failed' : 'partial'),
        live: true,
        result,
      };
      runs.unshift(newRun);
      setActiveRun(newRun);
    }

    return (
      <div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 24 }}>
          <Cell label="RUNS · 30D" value={stats.total} sub="ingest invocations"/>
          <Cell label="CLEAN" value={stats.ok} sub="zero errors" tone="#0a8754"/>
          <Cell label="PARTIAL" value={stats.partial} sub="parsed with warnings/errs" tone="#d4881f"/>
          <Cell label="FAILED" value={stats.failed} sub="quarantined for review" tone="#a32a18"/>
          <Cell label="LINES PARSED" value={stats.totalLines.toLocaleString()} sub="canonical rows committed"/>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginBottom: 22 }}>
          <div
            onDragOver={(e) => { e.preventDefault(); setDraggedOver(true); }}
            onDragLeave={() => setDraggedOver(false)}
            onDrop={(e) => { e.preventDefault(); setDraggedOver(false); const f = e.dataTransfer.files?.[0]; if (f) { const r = new FileReader(); r.onload = () => { const result = ingest({ name: f.name, text: r.result }); const newRun = { id: 'run_live_' + Date.now(), fileName: f.name, startedAt: Date.now(), finishedAt: Date.now() + result.ms, adapter: result.adapter, confidence: result.confidence, totalLines: result.lines.length, errorCount: result.errors.length, warningCount: result.warnings.length, status: result.errors.length === 0 ? 'ok' : (result.lines.length === 0 ? 'failed' : 'partial'), live: true, result }; runs.unshift(newRun); setActiveRun(newRun); }; r.readAsText(f); } }}
            style={{ border: '2px dashed ' + (draggedOver ? 'var(--ink)' : 'var(--rule)'), padding: '32px 22px', textAlign: 'center', background: draggedOver ? 'var(--bg-2)' : 'var(--paper)', transition: 'all .15s' }}>
            <div style={{ fontSize: 28, marginBottom: 8 }}>⤓</div>
            <div style={{ fontSize: 14, fontWeight: 600, marginBottom: 6 }}>Drop a statement file</div>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', marginBottom: 14 }}>CSV / TSV. The pipeline will detect vendor, parse, validate, and report.</div>
            <label style={{ display: 'inline-block', padding: '7px 14px', border: '1px solid var(--ink)', cursor: 'pointer', fontSize: 12 }}>
              Browse files
              <input type="file" accept=".csv,.tsv,.txt" onChange={handleFile} style={{ display: 'none' }}/>
            </label>
          </div>
          <div style={{ border: '1px solid var(--rule)', padding: 18, background: 'var(--paper)' }}>
            <Mono upper size={9} color="var(--ink-3)" style={{ marginBottom: 8, display: 'block' }}>OR PASTE CSV CONTENT</Mono>
            <textarea value={paste} onChange={(e) => setPaste(e.target.value)} placeholder="Paste CSV — first row should be headers" style={{ width: '100%', height: 120, padding: 10, border: '1px solid var(--rule)', background: 'var(--bg)', fontFamily: 'IBM Plex Mono, monospace', fontSize: 11, color: 'var(--ink)', boxSizing: 'border-box', resize: 'vertical' }}/>
            <button onClick={runPaste} style={{ marginTop: 8, padding: '7px 14px', border: '1px solid var(--ink)', background: 'var(--ink)', color: 'var(--bg)', fontSize: 12, cursor: 'pointer' }}>Run pipeline</button>
          </div>
        </div>

        {/* Run history */}
        <div style={{ border: '1px solid var(--rule)' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '24px 1fr 200px 90px 90px 90px 80px', gap: 14, padding: '10px 16px', borderBottom: '1px solid var(--rule)', background: 'var(--bg-2)' }}>
            <Mono upper size={9} color="var(--ink-3)"></Mono>
            <Mono upper size={9} color="var(--ink-3)">FILE</Mono>
            <Mono upper size={9} color="var(--ink-3)">ADAPTER · CONFIDENCE</Mono>
            <Mono upper size={9} color="var(--ink-3)" style={{ textAlign: 'right' }}>LINES</Mono>
            <Mono upper size={9} color="var(--ink-3)" style={{ textAlign: 'right' }}>ERR</Mono>
            <Mono upper size={9} color="var(--ink-3)" style={{ textAlign: 'right' }}>WARN</Mono>
            <Mono upper size={9} color="var(--ink-3)" style={{ textAlign: 'right' }}>STATUS</Mono>
          </div>
          {runs.slice(0, 20).map(r => {
            const tone = r.status === 'ok' ? '#0a8754' : r.status === 'partial' ? '#d4881f' : '#a32a18';
            return (
              <div key={r.id} onClick={() => setActiveRun(r)} style={{ display: 'grid', gridTemplateColumns: '24px 1fr 200px 90px 90px 90px 80px', gap: 14, padding: '11px 16px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center', cursor: 'pointer', background: activeRun?.id === r.id ? 'var(--bg-2)' : 'transparent' }}>
                <span style={{ width: 8, height: 8, borderRadius: '50%', background: tone, display: 'inline-block' }}/>
                <div>
                  <div style={{ fontSize: 12.5, fontWeight: 500 }}>{r.fileName}</div>
                  <Mono size={10} color="var(--ink-3)" style={{ marginTop: 2 }}>{new Date(r.startedAt).toLocaleString()} · {((r.finishedAt - r.startedAt))} ms</Mono>
                </div>
                <div>
                  <div style={{ fontSize: 11.5 }}>{r.adapter ? r.adapter.label : <span style={{ color: '#a32a18' }}>(no match)</span>}</div>
                  {r.confidence > 0 && <Mono size={10} color="var(--ink-3)" style={{ marginTop: 2 }}>{(r.confidence * 100 | 0)}% confidence</Mono>}
                </div>
                <Mono size={11} style={{ textAlign: 'right' }}>{r.totalLines.toLocaleString()}</Mono>
                <Mono size={11} style={{ textAlign: 'right', color: r.errorCount > 0 ? '#a32a18' : 'var(--ink-3)', fontWeight: r.errorCount > 0 ? 600 : 400 }}>{r.errorCount}</Mono>
                <Mono size={11} style={{ textAlign: 'right', color: r.warningCount > 0 ? '#d4881f' : 'var(--ink-3)' }}>{r.warningCount}</Mono>
                <Mono upper size={9} style={{ textAlign: 'right', color: tone, fontWeight: 600 }}>{r.status}</Mono>
              </div>
            );
          })}
        </div>

        {/* Run detail drawer (inline) */}
        {activeRun && (
          <div style={{ border: '1px solid var(--rule)', borderTop: '4px solid var(--ink)', padding: 22, marginTop: 22, background: 'var(--paper)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14 }}>
              <div>
                <Mono upper size={9} color="var(--ink-3)">RUN DETAIL · {activeRun.id}</Mono>
                <div style={{ fontSize: 16, fontWeight: 600, marginTop: 4 }}>{activeRun.fileName}</div>
              </div>
              <button onClick={() => setActiveRun(null)} style={{ padding: '6px 12px', border: '1px solid var(--rule)', fontSize: 11 }}>Close</button>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, padding: '14px 0', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 14 }}>
              <div><Mono upper size={9} color="var(--ink-3)">ADAPTER</Mono><div style={{ fontSize: 13, marginTop: 2 }}>{activeRun.adapter?.label || '—'}</div></div>
              <div><Mono upper size={9} color="var(--ink-3)">CONFIDENCE</Mono><div style={{ fontSize: 13, marginTop: 2 }}>{(activeRun.confidence * 100 | 0)}%</div></div>
              <div><Mono upper size={9} color="var(--ink-3)">DURATION</Mono><div style={{ fontSize: 13, marginTop: 2 }}>{activeRun.finishedAt - activeRun.startedAt} ms</div></div>
              <div><Mono upper size={9} color="var(--ink-3)">LINES · ERR · WARN</Mono><div style={{ fontSize: 13, marginTop: 2 }}>{activeRun.totalLines} · <span style={{ color: '#a32a18' }}>{activeRun.errorCount}</span> · <span style={{ color: '#d4881f' }}>{activeRun.warningCount}</span></div></div>
            </div>
            <div style={{ display: 'flex', gap: 10 }}>
              <button onClick={() => onSelectRun(activeRun, 'errors')} style={{ padding: '7px 14px', border: '1px solid var(--ink)', background: 'var(--ink)', color: 'var(--bg)', fontSize: 12 }}>View errors</button>
              <button style={{ padding: '7px 14px', border: '1px solid var(--rule)', fontSize: 12 }}>Re-run with adapter override</button>
              {activeRun.status !== 'failed' && <button style={{ padding: '7px 14px', border: '1px solid var(--rule)', fontSize: 12 }}>Commit to inbox</button>}
            </div>
          </div>
        )}
      </div>
    );
  }

  // ─── 02 ADAPTERS TAB ──────────────────────────────────────────
  function AdaptersTab() {
    const [active, setActive] = _S(ADAPTERS[0].id);
    const ad = ADAPTERS.find(a => a.id === active);
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '300px 1fr', gap: 22 }}>
        <div style={{ border: '1px solid var(--rule)', overflow: 'hidden' }}>
          <div style={{ padding: '10px 14px', borderBottom: '1px solid var(--rule)', background: 'var(--bg-2)' }}>
            <Mono upper size={9} color="var(--ink-3)">REGISTRY · {ADAPTERS.length} ADAPTERS</Mono>
          </div>
          {ADAPTERS.map(a => (
            <button key={a.id} onClick={() => setActive(a.id)} style={{ width: '100%', textAlign: 'left', padding: '11px 14px', borderBottom: '1px solid var(--rule-soft)', background: active === a.id ? 'var(--bg-2)' : 'transparent', borderLeft: active === a.id ? '3px solid var(--ink)' : '3px solid transparent', cursor: 'pointer' }}>
              <div style={{ fontSize: 12.5, fontWeight: 500 }}>{a.label}</div>
              <Mono size={10} color="var(--ink-3)" style={{ marginTop: 2 }}>{a.vendor} · {a.format}</Mono>
            </button>
          ))}
        </div>
        <div>
          <div style={{ borderBottom: '1px solid var(--rule)', paddingBottom: 14, marginBottom: 18 }}>
            <Mono upper size={9} color="var(--ink-3)">{ad.vendor} · {ad.format}</Mono>
            <div style={{ fontSize: 22, fontWeight: 600, marginTop: 4, fontFamily: 'Space Grotesk' }}>{ad.label}</div>
            <Mono size={11} color="var(--ink-3)" style={{ marginTop: 4 }}>id: {ad.id}</Mono>
          </div>
          <div style={{ marginBottom: 22 }}>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>EXPECTED COLUMNS</Mono>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {ad.cols.map((c, i) => (
                <span key={i} style={{ padding: '4px 9px', border: '1px solid var(--rule)', fontFamily: 'IBM Plex Mono, monospace', fontSize: 10.5, background: 'var(--bg-2)' }}>{c}</span>
              ))}
            </div>
          </div>
          <div style={{ marginBottom: 22 }}>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>FIELD MAPPING</Mono>
            <div style={{ border: '1px solid var(--rule)' }}>
              {Object.entries(ad.mapping || {}).map(([k, v], i) => (
                <div key={i} style={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 14, padding: '9px 14px', borderBottom: i < Object.keys(ad.mapping).length - 1 ? '1px solid var(--rule-soft)' : 0 }}>
                  <Mono size={11} style={{ fontWeight: 600 }}>{k}</Mono>
                  <Mono size={11} color="var(--ink-2)">{
                    typeof v === 'string' ? '← ' + v :
                    v.static ? `static "${v.static}"` :
                    v.from ? `← ${v.from}` + (v.map ? ` (mapped: ${Object.keys(v.map).length})` : '') + (v.default ? ` · default "${v.default}"` : '') :
                    JSON.stringify(v)
                  }</Mono>
                </div>
              ))}
            </div>
          </div>
          <div>
            <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>SNIFF SIGNATURE</Mono>
            <div style={{ padding: 14, background: 'var(--bg-2)', border: '1px solid var(--rule)', fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.6 }}>
              File is matched by header keyword presence + optional content sample. Confidence ≥ 0.4 required to dispatch; ≥ 0.6 auto-commits without manual review.
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ─── 03 ERRORS TAB ────────────────────────────────────────────
  function ErrorsTab() {
    const [filter, setFilter] = _S('all');
    return (
      <div>
        <div style={{ padding: '14px 18px', background: 'var(--bg-2)', border: '1px solid var(--rule)', marginBottom: 22 }}>
          <Mono upper size={9} color="var(--ink-3)" style={{ marginBottom: 4, display: 'block' }}>ERROR TAXONOMY · {Object.keys(ERROR_KINDS).length} KINDS</Mono>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.5 }}>
            Every issue surfaced by the pipeline is one of these. Severity controls behavior:
            <strong> crit</strong> halts the run; <strong>err</strong> excludes the row from commit; <strong>warn</strong> commits with flag; <strong>info</strong> records for analytics only.
          </div>
        </div>
        <div style={{ display: 'flex', gap: 6, marginBottom: 14, flexWrap: 'wrap' }}>
          {[{ k: 'all', l: 'All' }, { k: 'crit', l: 'Critical' }, { k: 'err', l: 'Error' }, { k: 'warn', l: 'Warning' }, { k: 'info', l: 'Info' }].map(f => (
            <button key={f.k} onClick={() => setFilter(f.k)} className="ff-mono upper" style={{
              fontSize: 9, padding: '5px 9px',
              background: filter === f.k ? 'var(--ink)' : 'transparent',
              color: filter === f.k ? '#fff' : 'var(--ink-2)',
              border: '1px solid ' + (filter === f.k ? 'var(--ink)' : 'var(--rule)'), cursor: 'pointer',
            }}>{f.l}</button>
          ))}
        </div>

        <div style={{ border: '1px solid var(--rule)' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '90px 200px 1fr 140px', gap: 14, padding: '10px 16px', borderBottom: '1px solid var(--rule)', background: 'var(--bg-2)' }}>
            <Mono upper size={9} color="var(--ink-3)">SEVERITY</Mono>
            <Mono upper size={9} color="var(--ink-3)">KIND</Mono>
            <Mono upper size={9} color="var(--ink-3)">DESCRIPTION</Mono>
            <Mono upper size={9} color="var(--ink-3)" style={{ textAlign: 'right' }}>RECOVERY</Mono>
          </div>
          {Object.entries(ERROR_KINDS).filter(([_, v]) => filter === 'all' || v.sev === filter).map(([k, v]) => {
            const tone = v.sev === 'crit' ? '#a32a18' : v.sev === 'err' ? '#a32a18' : v.sev === 'warn' ? '#d4881f' : '#1a4ed8';
            const recovery = {
              'unknown-vendor':       'Try manual adapter override · contact vendor for spec',
              'low-confidence':       'Spot-check 5 rows · accept if mapping correct',
              'missing-required':     'Map missing column · re-run row',
              'invalid-currency':     'Override at row · or whitelist new code',
              'invalid-date':         'Apply date format pattern · re-run',
              'invalid-territory':    'Auto-resolved if name; manual override if not',
              'invalid-iswc':         'Strip and ignore · or send for ISWC reissue',
              'invalid-isrc':         'Strip and ignore',
              'duplicate-line':       'Drop or merge · keep stmt of record',
              'amount-out-of-range':  'Inspect — likely sign or units error in source',
              'currency-mismatch':    'Apply line-level FX or split statement',
              'unmatched-work':       'Quarantine · feeds learning matcher',
              'parse-malformed':      'Inspect raw row · fix and re-run',
              'header-mismatch':      'Adapter version mismatch · update mapping',
              'encoding-error':       'Re-encode as UTF-8 · re-run',
              'empty-file':           'Investigate file source',
            }[k] || '—';
            return (
              <div key={k} style={{ display: 'grid', gridTemplateColumns: '90px 200px 1fr 140px', gap: 14, padding: '11px 16px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'baseline' }}>
                <Mono upper size={10} style={{ background: tone, color: '#fff', padding: '2px 6px', display: 'inline-block', width: 'fit-content', letterSpacing: '0.08em' }}>{v.sev}</Mono>
                <Mono size={11} style={{ fontWeight: 600 }}>{k}</Mono>
                <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>{v.desc}</div>
                <div style={{ fontSize: 11, color: 'var(--ink-3)', textAlign: 'right' }}>{recovery}</div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  // ─── 04 SCHEMA TAB ────────────────────────────────────────────
  function SchemaTab() {
    return (
      <div>
        <div style={{ padding: '14px 18px', background: 'var(--bg-2)', border: '1px solid var(--rule)', marginBottom: 22 }}>
          <Mono upper size={9} color="var(--ink-3)" style={{ marginBottom: 4, display: 'block' }}>CANONICAL LINE SCHEMA · {CANONICAL_FIELDS.length} FIELDS</Mono>
          <div style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.5 }}>
            All vendor adapters normalize into this single schema. Downstream royalty inbox, reconciliation, matching, forecasting, and leak detection all consume canonical lines — vendor specifics are quarantined inside the adapter layer.
          </div>
        </div>
        <div style={{ border: '1px solid var(--rule)' }}>
          <div style={{ display: 'grid', gridTemplateColumns: '180px 80px 60px 1fr', gap: 14, padding: '10px 16px', borderBottom: '1px solid var(--rule)', background: 'var(--bg-2)' }}>
            <Mono upper size={9} color="var(--ink-3)">FIELD</Mono>
            <Mono upper size={9} color="var(--ink-3)">TYPE</Mono>
            <Mono upper size={9} color="var(--ink-3)">REQ</Mono>
            <Mono upper size={9} color="var(--ink-3)">DESCRIPTION</Mono>
          </div>
          {CANONICAL_FIELDS.map((f, i) => (
            <div key={f.k} style={{ display: 'grid', gridTemplateColumns: '180px 80px 60px 1fr', gap: 14, padding: '10px 16px', borderBottom: i < CANONICAL_FIELDS.length - 1 ? '1px solid var(--rule-soft)' : 0, alignItems: 'baseline' }}>
              <Mono size={11.5} style={{ fontWeight: 600 }}>{f.k}</Mono>
              <Mono size={10.5} color="var(--ink-2)">{f.t}</Mono>
              <span style={{ fontSize: 10.5 }}>{f.req ? <span style={{ color: '#a32a18', fontWeight: 600 }}>YES</span> : <span style={{ color: 'var(--ink-3)' }}>—</span>}</span>
              <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>{f.desc}</div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  // ─── MAIN ────────────────────────────────────────────────────
  function ScreenStmtParser({ go, payload }) {
    const PageHeader = window.PageHeader;
    const [tab, setTab] = _S(payload?.tab || 'ingest');

    const TABS = [
      { k: 'ingest',   l: 'Ingest' },
      { k: 'adapters', l: 'Adapters' },
      { k: 'errors',   l: 'Errors' },
      { k: 'schema',   l: 'Schema' },
    ];

    return (
      <div>
        {PageHeader && (
          <PageHeader
            eyebrow={['ROYALTIES', 'STATEMENT PARSER', `${ADAPTERS.length} VENDORS`]}
            title="parse statements."
            highlight="parse statements."
            sub="Universal ingest pipeline: detect vendor signature, parse, normalize into canonical schema, validate against 16 error kinds, and commit. Quarantines bad rows with full traceback."
          />
        )}

        <div style={{ borderBottom: '1px solid var(--rule)', display: 'flex', gap: 0, marginBottom: 24 }}>
          {TABS.map(t => (
            <button key={t.k} onClick={() => setTab(t.k)} style={{
              padding: '14px 22px', background: 'transparent', border: 0,
              borderBottom: '2px solid ' + (tab === t.k ? 'var(--ink)' : 'transparent'),
              cursor: 'pointer', color: tab === t.k ? 'var(--ink)' : 'var(--ink-3)',
              fontSize: 13, fontWeight: tab === t.k ? 600 : 400,
            }}>{t.l}</button>
          ))}
        </div>

        {tab === 'ingest'   && <IngestTab onSelectRun={(r, t) => setTab(t || 'errors')}/>}
        {tab === 'adapters' && <AdaptersTab/>}
        {tab === 'errors'   && <ErrorsTab/>}
        {tab === 'schema'   && <SchemaTab/>}
      </div>
    );
  }

  window.ScreenStmtParser = ScreenStmtParser;
  console.log('[StmtParser] loaded · ' + ADAPTERS.length + ' adapters · ' + CANONICAL_FIELDS.length + ' canonical fields · ' + Object.keys(ERROR_KINDS).length + ' error kinds');
})();
