// ============================================================================
// CWR ACK IMPORT  ·  parse acknowledgement files + update registration status
// ----------------------------------------------------------------------------
// Societies return ACK files referencing the original CWR transmission. This
// module parses them, maps each ACK record back to a registration, and writes
// the new status back into window.__REG_STATUS (a session-scoped map keyed on
// `${societyAcronym}:${submitterWorkRef}`). The Acks tab and registrations
// elsewhere can read from this.
//
// CWR ACK file structure (standard CISAC):
//   HDR ........ transmission header (sender = society, receiver = us)
//   GRH ........ group header, transaction type = ACK
//   ACK ........ acknowledgement transaction (one per original work)
//     MSG .... 0..N validation messages with severity + field reference
//     [the original NWR/REV/AGR follows the ACK in some implementations]
//   GRT ........ group trailer
//   GRH ........ next group (if multiple transaction types)
//   ...
//   TRL ........ transmission trailer
//
// Status mapping per CWR spec (Transaction Status field, ACK record):
//   AS  → Accepted (final)
//   AC  → Accepted with Changes (society modified our payload)
//   CO  → Conflict (writers/publishers conflict with another submission)
//   DU  → Duplicate (we already submitted this)
//   IF  → Information (advisory, no action required)
//   IR  → Incomplete Registration (missing required field)
//   IE  → Invalid Element (malformed data)
//   NP  → No Participation (writer not represented by this society)
//   RA  → Registration Accepted
//   RJ  → Rejected (final)
//   AR  → Accepted (no further info)
//
// Engine:    window.CwrAckImport.parse(text) → { header, groups, acks, summary, errors }
// Apply:     window.CwrAckImport.apply(parsed) → writes to __REG_STATUS, fires event
// Tab:       window.CwrAckImportTab — drop-zone UI, parse + apply flow
// ============================================================================

(function () {
  'use strict';
  const { useState, useMemo, useCallback, useRef, useEffect } = React;

  // ─── ACK code dictionary (mirrors cwr-acks.jsx + spec) ──────────────────
  const ACK_CODES = {
    AS: { tone: 'ok',     label: 'Accepted',              terminal: true  },
    AC: { tone: 'ok',     label: 'Accepted with changes', terminal: true  },
    AR: { tone: 'ok',     label: 'Accepted',              terminal: true  },
    RA: { tone: 'ok',     label: 'Registration accepted', terminal: true  },
    IF: { tone: 'info',   label: 'Information',           terminal: false },
    NP: { tone: 'warn',   label: 'No participation',      terminal: true  },
    DU: { tone: 'warn',   label: 'Duplicate',             terminal: true  },
    CO: { tone: 'danger', label: 'Conflict',              terminal: false },
    IR: { tone: 'danger', label: 'Incomplete',            terminal: false },
    IE: { tone: 'danger', label: 'Invalid element',       terminal: false },
    RJ: { tone: 'danger', label: 'Rejected',              terminal: true  },
  };

  const TONE_BG = { ok:'#e8f0e9', warn:'#f4ecd9', danger:'#f4e3df', info:'#e6eef5', neutral:'#eee' };
  const TONE_FG = { ok:'#1e6b3f', warn:'#9b6a18', danger:'#a04432', info:'#2c5777', neutral:'var(--ink-2)' };
  const TONE_DOT = { ok:'#2d6a3f', warn:'#9b6a18', danger:'#a04432', info:'#2c5777', neutral:'#999' };

  // ─── Field offset helper (uses CwrDecode if loaded, with fallback) ──────
  function sliceAt(line, start, len) {
    return (line || '').slice(start, start + len).replace(/\s+$/, '');
  }

  // ─── Parser ─────────────────────────────────────────────────────────────
  function parse(text) {
    const errors = [];
    const lines = String(text || '').split(/\r?\n/).filter((l) => l.length > 0);
    if (!lines.length) {
      return { ok:false, errors:['Empty file'], header:null, groups:[], acks:[], summary:emptySummary() };
    }

    // First line should be HDR
    const hdrLine = lines[0];
    if (hdrLine.slice(0, 3) !== 'HDR') {
      return { ok:false, errors:['First record is not HDR'], header:null, groups:[], acks:[], summary:emptySummary() };
    }

    // Decode HDR — Sender Type (2), Sender ID (9), Sender Name (45), EDI std (5),
    // Creation Date (8), Creation Time (6), Transmission Date (8), Charset (2 v3+)
    const header = {
      raw: hdrLine,
      senderType: sliceAt(hdrLine, 3, 2),
      senderId:   sliceAt(hdrLine, 5, 9),
      senderName: sliceAt(hdrLine, 14, 45),
      ediStd:     sliceAt(hdrLine, 59, 5),
      created:    sliceAt(hdrLine, 64, 8),
      createdTime:sliceAt(hdrLine, 72, 6),
      transmitted:sliceAt(hdrLine, 78, 8),
    };
    // Try to identify the society from sender ID/name
    const matched = matchSociety(header);
    header.society = matched;

    // Walk records, building groups and ACK transactions
    const groups = [];
    const acks = [];
    let curGroup = null;
    let curAck = null;

    for (let i = 1; i < lines.length; i++) {
      const line = lines[i];
      const rt = line.slice(0, 3);
      if (rt === 'GRH') {
        curGroup = {
          line: i + 1,
          transactionType: sliceAt(line, 3, 3),
          groupId:         sliceAt(line, 6, 5),
          version:         sliceAt(line, 11, 5),
          batchRequest:    sliceAt(line, 16, 10),
          submissionFlag:  sliceAt(line, 26, 2),
          ackCount: 0,
        };
        groups.push(curGroup);
        curAck = null;
      } else if (rt === 'GRT') {
        if (curGroup) curGroup.recordCount = parseInt(sliceAt(line, 16, 8) || '0', 10);
        curAck = null;
      } else if (rt === 'TRL') {
        // transmission end
        curAck = null;
      } else if (rt === 'ACK') {
        // Per spec ACK fields after the standard 19-byte transaction header:
        //   Creation date 8, Creation time 6, Original group ID 5,
        //   Original transaction seq 8, Original transaction type 3,
        //   Creation title 60, Submitter creation # 20, Recipient creation # 20,
        //   Processing date 8, Transaction status 2
        const status = sliceAt(line, 19 + 8 + 6 + 5 + 8 + 3 + 60 + 20 + 20 + 8, 2) || 'AR';
        curAck = {
          line: i + 1,
          status,
          statusMeta: ACK_CODES[status] || { tone:'neutral', label:status, terminal:false },
          creationDate: sliceAt(line, 19, 8),
          creationTime: sliceAt(line, 27, 6),
          originalGroupId: sliceAt(line, 33, 5),
          originalTransactionSeq: sliceAt(line, 38, 8),
          originalTransactionType: sliceAt(line, 46, 3),
          title: sliceAt(line, 49, 60),
          submitterRef: sliceAt(line, 109, 20),
          recipientRef: sliceAt(line, 129, 20),
          processingDate: sliceAt(line, 149, 8),
          messages: [],
          societyWorkId: null, // populated later if any IPA/MSG carries it
          group: curGroup ? { transactionType: curGroup.transactionType, groupId: curGroup.groupId } : null,
        };
        acks.push(curAck);
        if (curGroup) curGroup.ackCount += 1;
      } else if (rt === 'MSG' && curAck) {
        // MSG fields after 19-byte hdr:
        //   Message type 1, Original record type 3, Message level 1,
        //   Validation # 3, Message text 150
        const messageType = sliceAt(line, 19, 1);     // F=field/G=record/T=transaction
        const recRef      = sliceAt(line, 20, 3);
        const level       = sliceAt(line, 23, 1);     // E=error, W=warning, etc
        const validation  = sliceAt(line, 24, 3);
        const messageText = sliceAt(line, 27, 150);
        curAck.messages.push({
          messageType, recordType: recRef, level, validation, text: messageText,
          severity: level === 'E' || level === 'F' ? 'error' : level === 'W' ? 'warning' : 'info',
        });
      } else if (curAck && rt === 'NWR') {
        // Some societies echo their assigned work ID inside the original NWR slot's "society work #" field
        // CWR 2.x NWR has Submitter Work # at offset 32 length 14. Society work # appears in v3 at later offset.
        // Best-effort: take chars 32-46 if non-empty as societyWorkId hint.
        const sw = sliceAt(line, 32, 14);
        if (sw && !curAck.societyWorkId) curAck.societyWorkId = sw;
      }
    }

    // Build summary
    const summary = emptySummary();
    summary.acks = acks.length;
    summary.groups = groups.length;
    summary.lines = lines.length;
    acks.forEach((a) => {
      summary.byStatus[a.status] = (summary.byStatus[a.status] || 0) + 1;
      const tone = a.statusMeta.tone;
      summary.byTone[tone] = (summary.byTone[tone] || 0) + 1;
      if (a.messages.length) summary.withMessages += 1;
    });

    return { ok:true, errors, header, groups, acks, summary };
  }

  function emptySummary() {
    return { acks:0, groups:0, lines:0, byStatus:{}, byTone:{ok:0,warn:0,danger:0,info:0,neutral:0}, withMessages:0 };
  }

  function matchSociety(header) {
    const list = window.SOCIETIES || [];
    const id = header.senderId || '';
    const nameLc = (header.senderName || '').toLowerCase();
    // Most societies use their CISAC 3-digit code as senderId (zero-padded)
    const byId = list.find((s) => s.cisacCode && (id === String(s.cisacCode) || id.endsWith(String(s.cisacCode))));
    if (byId) return { acronym: byId.acronym, name: byId.name, country: byId.country };
    const byName = list.find((s) =>
      nameLc.includes((s.acronym || '').toLowerCase()) ||
      nameLc.includes((s.name || '').toLowerCase().split(' ')[0])
    );
    if (byName) return { acronym: byName.acronym, name: byName.name, country: byName.country };
    return { acronym: '?', name: header.senderName || 'Unknown society', country: '' };
  }

  // ─── Apply: write to __REG_STATUS + match works by submitter ref ─────────
  function apply(parsed) {
    if (!parsed || !parsed.acks) return { applied:0, matched:0, unmatched:0 };
    window.__REG_STATUS = window.__REG_STATUS || {};
    const works = window.WORKS || [];
    const society = parsed.header?.society?.acronym || '?';
    let matched = 0, unmatched = 0;
    parsed.acks.forEach((a) => {
      const ref = a.submitterRef || a.title;
      const key = `${society}:${ref}`;
      // Try to bind to a real work
      const found = works.find((w) =>
        (w.submitterWorkId && w.submitterWorkId === ref) ||
        (w.id && w.id === ref) ||
        (w.title && a.title && w.title.toLowerCase() === a.title.toLowerCase())
      );
      if (found) matched += 1; else unmatched += 1;
      window.__REG_STATUS[key] = {
        society,
        status: a.status,
        statusLabel: a.statusMeta.label,
        tone: a.statusMeta.tone,
        terminal: !!a.statusMeta.terminal,
        societyWorkId: a.societyWorkId,
        title: a.title,
        submitterRef: ref,
        processingDate: a.processingDate,
        messages: a.messages,
        appliedAt: new Date().toISOString(),
        workId: found ? found.id : null,
      };
    });
    // Persist a small log entry
    window.__REG_STATUS_LOG = window.__REG_STATUS_LOG || [];
    window.__REG_STATUS_LOG.unshift({
      at: new Date().toISOString(),
      society,
      filename: parsed.header?.filename || 'pasted.txt',
      acks: parsed.acks.length, matched, unmatched,
      counts: parsed.summary.byStatus,
    });
    if (window.__REG_STATUS_LOG.length > 50) window.__REG_STATUS_LOG.length = 50;
    // Fire event so other screens can refresh
    try { window.dispatchEvent(new CustomEvent('astro-reg-status-updated', { detail: { society, count: parsed.acks.length } })); } catch (e) {}
    return { applied: parsed.acks.length, matched, unmatched };
  }

  // Lookup helper for other modules
  function statusFor(society, submitterRef) {
    const map = window.__REG_STATUS || {};
    return map[`${society}:${submitterRef}`] || null;
  }

  window.CwrAckImport = { parse, apply, statusFor, ACK_CODES };

  // ─── Synthetic sample for demo when user clicks "Try sample" ────────────
  function syntheticAckText() {
    const today = new Date();
    const yyyymmdd = today.toISOString().slice(0,10).replace(/-/g,'');
    const hhmmss = '143055';
    const pad = (s, n, c) => (c === 'L' ? String(s).padStart(n,' ').slice(-n) : String(s).padEnd(n,' ').slice(0, n));
    const hdr = 'HDR' + 'SO' + '000000101' + pad('ASCAP', 45) + '02.10' + yyyymmdd + hhmmss + yyyymmdd;
    const grh = 'GRH' + 'ACK' + '00001' + '02.10' + '          ' + '  ' + '             ';
    const lines = [hdr, grh];
    const samples = [
      { ref:'PLU-2026-00041', title:'Eres Tú',         status:'AS', msgs:[] },
      { ref:'PLU-2026-00042', title:'Volver a Vivir',  status:'AC', msgs:[
        { lvl:'W', val:'001', t:'Title modified by society — original capitalization restored' },
      ]},
      { ref:'PLU-2026-00043', title:'No Te Va Gustar', status:'CO', msgs:[
        { lvl:'E', val:'042', t:'Writer share conflict with prior registration · WID 123456' },
      ]},
      { ref:'PLU-2026-00044', title:'Última Llamada',  status:'IR', msgs:[
        { lvl:'E', val:'013', t:'Required field missing: writer IPI on SWR record 3' },
      ]},
      { ref:'PLU-2026-00045', title:'Mañana Será',     status:'NP', msgs:[
        { lvl:'W', val:'201', t:'Writer not represented by ASCAP territory' },
      ]},
      { ref:'PLU-2026-00046', title:'Caminos',         status:'DU', msgs:[
        { lvl:'W', val:'099', t:'Duplicate of prior submission · seq 00031' },
      ]},
      { ref:'PLU-2026-00047', title:'Lluvia y Sol',    status:'RJ', msgs:[
        { lvl:'F', val:'201', t:'Invalid IPI base check digit on writer 1' },
      ]},
      { ref:'PLU-2026-00048', title:'Calle Vieja',     status:'AS', msgs:[] },
    ];
    let seq = 1;
    samples.forEach((s) => {
      const recPrefix = 'ACK' + pad(String(seq).padStart(8,'0'),8) + '0000000001'; // txn seq + record seq
      // 19-byte header is RT(3) + txn(8) + rec(8). Already done above.
      const ack =
        recPrefix +
        yyyymmdd + hhmmss +
        '00001' +
        pad(String(seq).padStart(8,'0'),8) +
        'NWR' +
        pad(s.title, 60) +
        pad(s.ref, 20) +
        pad('A' + String(seq).padStart(8,'0'), 20) +
        yyyymmdd +
        s.status;
      lines.push(ack);
      let msgSeq = 1;
      s.msgs.forEach((m) => {
        const msgPrefix = 'MSG' + pad(String(seq).padStart(8,'0'),8) + pad(String(msgSeq).padStart(8,'0'),8);
        const msg = msgPrefix + 'F' + 'ACK' + m.lvl + m.val + pad(m.t, 150);
        lines.push(msg);
        msgSeq++;
      });
      seq++;
    });
    const grt = 'GRT' + '00001' + pad(String(samples.length).padStart(8,'0'),8) + pad(String(lines.length-2+1).padStart(8,'0'),8) + '   ' + '0000000000' + '          ';
    lines.push(grt);
    const trl = 'TRL' + '00001' + pad(String(samples.length).padStart(8,'0'),8) + pad(String(lines.length+1).padStart(7,'0'),7);
    lines.push(trl);
    return lines.join('\n');
  }

  // ─── UI: Import tab ──────────────────────────────────────────────────────
  function CwrAckImportTab() {
    const [text, setText] = useState('');
    const [filename, setFilename] = useState('');
    const [parsed, setParsed] = useState(null);
    const [applied, setApplied] = useState(null);
    const [activeAck, setActiveAck] = useState(0);
    const fileInput = useRef(null);
    const [history, setHistory] = useState(() => window.__REG_STATUS_LOG || []);

    useEffect(() => {
      const handler = () => setHistory([...(window.__REG_STATUS_LOG || [])]);
      window.addEventListener('astro-reg-status-updated', handler);
      return () => window.removeEventListener('astro-reg-status-updated', handler);
    }, []);

    const onText = (t, name) => {
      setText(t);
      setFilename(name || filename || 'pasted.txt');
      const p = parse(t);
      if (p.header) p.header.filename = name || filename || 'pasted.txt';
      setParsed(p);
      setActiveAck(0);
      setApplied(null);
    };

    const onFile = (file) => {
      if (!file) return;
      const r = new FileReader();
      r.onload = (e) => onText(String(e.target.result), file.name);
      r.readAsText(file);
    };

    const onDrop = (e) => {
      e.preventDefault();
      const f = e.dataTransfer.files && e.dataTransfer.files[0];
      if (f) onFile(f);
    };

    const trySample = () => onText(syntheticAckText(), 'sample-ASCAP.V21');

    const doApply = () => {
      if (!parsed) return;
      const r = apply(parsed);
      setApplied(r);
      setHistory([...(window.__REG_STATUS_LOG || [])]);
    };

    return (
      <div style={{ display:'grid', gridTemplateColumns: parsed ? '380px 1fr' : '1fr', gap:0, minHeight:'70vh', border:'1px solid var(--rule)' }}>
        {/* ─── LEFT: drop-zone + summary + history ─────────────────── */}
        <aside style={{ borderRight: parsed ? '1px solid var(--rule)' : '0', padding:'18px 18px 0', display:'flex', flexDirection:'column', gap:18, background:'var(--bg-2)' }}>
          {!parsed && (
            <div onDrop={onDrop} onDragOver={(e) => e.preventDefault()}
              style={{
                border:'2px dashed var(--ink-3)', padding:'40px 28px', textAlign:'center',
                background:'var(--paper)', display:'flex', flexDirection:'column', alignItems:'center', gap:12,
              }}>
              <div className="ff-display" style={{ fontSize:22, fontWeight:700, letterSpacing:'-0.02em' }}>Drop ACK file here</div>
              <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-3)' }}>
                Supports CWR 2.1 / 2.1r7 / 2.2 / 3.0 / 3.1<br/>.V21, .V22, .V30, .V31, .txt
              </div>
              <input ref={fileInput} type="file" accept=".V21,.V22,.V30,.V31,.txt" style={{ display:'none' }}
                onChange={(e) => onFile(e.target.files[0])} />
              <div style={{ display:'flex', gap:8, marginTop:6 }}>
                <button onClick={() => fileInput.current?.click()} className="ff-mono upper"
                  style={{ padding:'8px 14px', fontSize:11, fontWeight:600, letterSpacing:'.08em',
                    background:'var(--ink)', color:'var(--paper)', border:'1px solid var(--ink)', cursor:'pointer' }}>
                  Choose file
                </button>
                <button onClick={trySample} className="ff-mono upper"
                  style={{ padding:'8px 14px', fontSize:11, fontWeight:600, letterSpacing:'.08em',
                    background:'transparent', color:'var(--ink)', border:'1px solid var(--rule)', cursor:'pointer' }}>
                  Try sample
                </button>
              </div>
            </div>
          )}

          {!parsed && (
            <div>
              <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:8 }}>OR PASTE</div>
              <textarea value={text} onChange={(e) => onText(e.target.value, 'pasted.txt')}
                className="ff-mono"
                placeholder="HDR…&#10;GRH…&#10;ACK…"
                style={{ width:'100%', minHeight:120, padding:10, fontSize:11, lineHeight:1.5,
                  background:'var(--paper)', border:'1px solid var(--rule)', resize:'vertical' }} />
            </div>
          )}

          {parsed && (
            <>
              {/* file header */}
              <div>
                <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:6 }}>FILE</div>
                <div className="ff-mono" style={{ fontSize:13, fontWeight:600, marginBottom:2 }}>{filename}</div>
                <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-3)' }}>
                  {parsed.header?.society?.name} ({parsed.header?.society?.acronym}) · {parsed.summary.lines.toLocaleString()} lines
                </div>
                <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-3)' }}>
                  Created {fmtCwrDate(parsed.header?.created)} {fmtCwrTime(parsed.header?.createdTime)}
                </div>
              </div>

              {/* outcome bar */}
              <OutcomeBar summary={parsed.summary} />

              {/* status counts */}
              <div>
                <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:8 }}>OUTCOMES · {parsed.summary.acks}</div>
                <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
                  {Object.entries(parsed.summary.byStatus).sort((a,b) => b[1]-a[1]).map(([code, n]) => {
                    const meta = ACK_CODES[code] || { tone:'neutral', label:code };
                    return (
                      <div key={code} style={{ display:'flex', alignItems:'center', gap:10, padding:'5px 7px', background:'var(--paper)' }}>
                        <span className="ff-mono" style={{ fontSize:10, padding:'2px 7px', background:TONE_BG[meta.tone], color:TONE_FG[meta.tone], fontWeight:700, letterSpacing:'.04em' }}>{code}</span>
                        <span className="ff-mono" style={{ fontSize:11, color:'var(--ink-2)', flex:1 }}>{meta.label}</span>
                        <span className="ff-mono num" style={{ fontSize:13, fontWeight:600 }}>{n}</span>
                      </div>
                    );
                  })}
                </div>
              </div>

              {/* apply */}
              <div style={{ marginTop:'auto', paddingBottom:18 }}>
                {!applied ? (
                  <button onClick={doApply} className="ff-mono upper"
                    style={{ width:'100%', padding:'12px 14px', fontSize:11, fontWeight:600, letterSpacing:'.10em',
                      background:'var(--ink)', color:'var(--paper)', border:'1px solid var(--ink)', cursor:'pointer' }}>
                    Apply {parsed.summary.acks} status updates
                  </button>
                ) : (
                  <div style={{ padding:'10px 12px', background:'#e8f0e9', border:'1px solid #2d6a3f' }}>
                    <div className="ff-mono upper" style={{ fontSize:10, letterSpacing:'.10em', color:'#1e6b3f', fontWeight:700 }}>APPLIED</div>
                    <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-2)', marginTop:3 }}>
                      {applied.applied} statuses written · {applied.matched} matched to local works · {applied.unmatched} unmatched
                    </div>
                  </div>
                )}
                <button onClick={() => { setParsed(null); setText(''); setFilename(''); setApplied(null); }}
                  className="ff-mono upper"
                  style={{ width:'100%', marginTop:8, padding:'8px 14px', fontSize:10, fontWeight:600, letterSpacing:'.08em',
                    background:'transparent', color:'var(--ink-2)', border:'1px solid var(--rule)', cursor:'pointer' }}>
                  Import another
                </button>
              </div>
            </>
          )}
        </aside>

        {/* ─── RIGHT: ACK transactions table + detail ──────────────── */}
        {parsed && (
          <main style={{ display:'flex', flexDirection:'column', minWidth:0 }}>
            <div style={{ padding:'12px 16px', borderBottom:'1px solid var(--rule)', display:'flex', alignItems:'center', justifyContent:'space-between' }}>
              <div className="ff-mono upper" style={{ fontSize:10, letterSpacing:'.10em', color:'var(--ink-3)' }}>
                ACKNOWLEDGEMENTS · {parsed.acks.length}
              </div>
              <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)' }}>
                Click row to view messages + matched work
              </div>
            </div>
            <div style={{ flex:1, display:'grid', gridTemplateColumns:'1fr 360px', minHeight:0 }}>
              <AckList acks={parsed.acks} active={activeAck} onPick={setActiveAck} />
              <AckDetail ack={parsed.acks[activeAck]} society={parsed.header?.society?.acronym} />
            </div>
          </main>
        )}

        {/* ─── BOTTOM: history (when no file is loaded) ────────────── */}
        {!parsed && history.length > 0 && (
          <div style={{ gridColumn:'1 / -1', borderTop:'1px solid var(--rule)', padding:'16px 18px', background:'var(--paper)' }}>
            <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:8 }}>RECENT IMPORTS · {history.length}</div>
            <div style={{ display:'flex', flexDirection:'column', gap:4 }}>
              {history.slice(0, 10).map((h, i) => (
                <div key={i} style={{ display:'grid', gridTemplateColumns:'150px 60px 1fr 100px 100px', gap:14, padding:'7px 10px', background:'var(--bg-2)', alignItems:'center' }}>
                  <span className="ff-mono" style={{ fontSize:11, color:'var(--ink-3)' }}>{fmtTs(h.at)}</span>
                  <span className="ff-mono" style={{ fontSize:11, fontWeight:600 }}>{h.society}</span>
                  <span className="ff-mono" style={{ fontSize:11, color:'var(--ink-2)' }}>{h.filename}</span>
                  <span className="ff-mono num" style={{ fontSize:11 }}>{h.acks} acks</span>
                  <span className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)' }}>{h.matched}/{h.matched + h.unmatched} matched</span>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }

  function OutcomeBar({ summary }) {
    const total = summary.acks || 1;
    const segs = [
      { tone:'ok',     n: summary.byTone.ok || 0 },
      { tone:'warn',   n: summary.byTone.warn || 0 },
      { tone:'danger', n: summary.byTone.danger || 0 },
      { tone:'info',   n: summary.byTone.info || 0 },
    ];
    return (
      <div>
        <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:6 }}>OUTCOME MIX</div>
        <div style={{ display:'flex', height:10, border:'1px solid var(--rule)', overflow:'hidden' }}>
          {segs.map((s, i) => s.n > 0 && (
            <div key={i} title={`${s.n} ${s.tone}`} style={{ width: (s.n / total * 100) + '%', background: TONE_DOT[s.tone] }} />
          ))}
        </div>
      </div>
    );
  }

  function AckList({ acks, active, onPick }) {
    const [filter, setFilter] = useState('all');
    const filtered = useMemo(() => {
      if (filter === 'all') return acks;
      if (filter === 'errors') return acks.filter((a) => a.statusMeta.tone === 'danger');
      if (filter === 'warns') return acks.filter((a) => a.statusMeta.tone === 'warn');
      if (filter === 'ok') return acks.filter((a) => a.statusMeta.tone === 'ok');
      return acks;
    }, [acks, filter]);
    return (
      <div style={{ overflow:'auto', borderRight:'1px solid var(--rule)' }}>
        <div style={{ display:'flex', gap:4, padding:'10px 14px', borderBottom:'1px solid var(--rule)', background:'var(--bg-2)' }}>
          {[
            { v:'all', l:`all (${acks.length})` },
            { v:'errors', l:`errors (${acks.filter(a => a.statusMeta.tone === 'danger').length})` },
            { v:'warns', l:`warns (${acks.filter(a => a.statusMeta.tone === 'warn').length})` },
            { v:'ok', l:`ok (${acks.filter(a => a.statusMeta.tone === 'ok').length})` },
          ].map((t) => (
            <button key={t.v} onClick={() => setFilter(t.v)} className="ff-mono"
              style={{ padding:'4px 9px', fontSize:10, fontWeight:600,
                background: filter === t.v ? 'var(--ink)' : 'var(--paper)',
                color: filter === t.v ? 'var(--paper)' : 'var(--ink)',
                border:'1px solid var(--rule)', cursor:'pointer' }}>{t.l}</button>
          ))}
        </div>
        <div>
          {filtered.map((a, i) => {
            const realIdx = acks.indexOf(a);
            return (
              <div key={i} onClick={() => onPick(realIdx)}
                style={{
                  display:'grid', gridTemplateColumns:'80px 1fr 140px 80px', gap:12, padding:'9px 14px',
                  alignItems:'center', cursor:'pointer',
                  background: active === realIdx ? 'var(--bg-2)' : (i % 2 ? 'var(--bg)' : 'var(--paper)'),
                  borderLeft: active === realIdx ? '3px solid var(--ink)' : '3px solid transparent',
                  borderBottom:'1px solid var(--rule-soft)',
                }}>
                <span className="ff-mono" style={{ fontSize:10, padding:'3px 7px', background:TONE_BG[a.statusMeta.tone], color:TONE_FG[a.statusMeta.tone], fontWeight:700, textAlign:'center', justifySelf:'start' }}>
                  {a.status} · {a.statusMeta.label.split(' ')[0].toLowerCase()}
                </span>
                <div style={{ minWidth:0 }}>
                  <div style={{ fontSize:12, fontWeight:600, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{a.title || '(no title)'}</div>
                  <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', marginTop:2 }}>
                    ref {a.submitterRef || '—'} · {a.originalTransactionType}
                  </div>
                </div>
                <span className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)' }}>
                  {fmtCwrDate(a.processingDate)}
                </span>
                <span className="ff-mono" style={{ fontSize:10, color: a.messages.length ? 'var(--ink)' : 'var(--ink-4)', textAlign:'right' }}>
                  {a.messages.length} msg
                </span>
              </div>
            );
          })}
          {filtered.length === 0 && (
            <div className="ff-mono" style={{ padding:24, color:'var(--ink-3)', fontSize:11, textAlign:'center' }}>
              No acks match filter
            </div>
          )}
        </div>
      </div>
    );
  }

  function AckDetail({ ack, society }) {
    if (!ack) return <div style={{ padding:18, color:'var(--ink-3)' }} className="ff-mono">No selection</div>;
    const works = window.WORKS || [];
    const matched = works.find((w) =>
      (w.submitterWorkId && w.submitterWorkId === ack.submitterRef) ||
      (w.id && w.id === ack.submitterRef) ||
      (w.title && ack.title && w.title.toLowerCase() === ack.title.toLowerCase())
    );
    return (
      <div style={{ overflow:'auto', padding:18, display:'flex', flexDirection:'column', gap:18 }}>
        {/* status banner */}
        <div style={{ padding:'12px 14px', background: TONE_BG[ack.statusMeta.tone], border:`1px solid ${TONE_FG[ack.statusMeta.tone]}` }}>
          <div className="ff-mono upper" style={{ fontSize:10, letterSpacing:'.10em', color: TONE_FG[ack.statusMeta.tone], fontWeight:700 }}>
            {ack.status} · {ack.statusMeta.label}
          </div>
          <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-2)', marginTop:4 }}>
            {ack.statusMeta.terminal ? 'Terminal status — no further response expected.' : 'Action required — fix and resubmit.'}
          </div>
        </div>

        <Field label="Work title" value={ack.title} />
        <Field label="Our reference" value={ack.submitterRef} mono />
        <Field label="Society reference" value={ack.recipientRef || ack.societyWorkId || '—'} mono />
        <Field label="Original transaction" value={`${ack.originalTransactionType} · group ${ack.originalGroupId} · seq ${ack.originalTransactionSeq}`} mono />
        <Field label="Processing date" value={fmtCwrDate(ack.processingDate)} mono />

        {/* matched work */}
        <div>
          <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:6 }}>MATCHED CATALOG WORK</div>
          {matched ? (
            <div style={{ padding:'10px 12px', background:'var(--bg-2)', border:'1px solid var(--rule)' }}>
              <div style={{ fontSize:13, fontWeight:600 }}>{matched.title}</div>
              <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', marginTop:3 }}>
                {matched.id} · {(matched.writers || []).map((w) => w.name).slice(0, 3).join(' · ') || '—'}
              </div>
            </div>
          ) : (
            <div style={{ padding:'10px 12px', background:'#fff8ed', border:'1px solid #d68910' }}>
              <div className="ff-mono" style={{ fontSize:11, color:'#9b6a18' }}>No catalog work found for ref {ack.submitterRef}. Status will still be stored, but no work record updated.</div>
            </div>
          )}
        </div>

        {/* messages */}
        {ack.messages.length > 0 && (
          <div>
            <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:6 }}>VALIDATION MESSAGES · {ack.messages.length}</div>
            <div style={{ display:'flex', flexDirection:'column', gap:6 }}>
              {ack.messages.map((m, i) => (
                <div key={i} style={{ padding:'9px 11px', background:'var(--paper)', border:'1px solid var(--rule)' }}>
                  <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:4 }}>
                    <span className="ff-mono" style={{ fontSize:9, padding:'2px 6px',
                      background: m.severity === 'error' ? '#f4e3df' : m.severity === 'warning' ? '#f4ecd9' : '#e6eef5',
                      color: m.severity === 'error' ? '#a04432' : m.severity === 'warning' ? '#9b6a18' : '#2c5777',
                      fontWeight:700, letterSpacing:'.04em' }}>
                      {m.level} · {m.severity}
                    </span>
                    <span className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)' }}>
                      {m.recordType} · validation {m.validation}
                    </span>
                  </div>
                  <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-2)', lineHeight:1.4 }}>{m.text}</div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }

  function Field({ label, value, mono }) {
    return (
      <div>
        <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color:'var(--ink-3)', marginBottom:3 }}>{label}</div>
        <div className={mono ? 'ff-mono' : ''} style={{ fontSize: mono ? 11 : 13, fontWeight: mono ? 500 : 600 }}>{value || '—'}</div>
      </div>
    );
  }

  function fmtCwrDate(s) {
    if (!s || s.length !== 8) return s || '—';
    return `${s.slice(0,4)}-${s.slice(4,6)}-${s.slice(6,8)}`;
  }
  function fmtCwrTime(s) {
    if (!s || s.length !== 6) return '';
    return `${s.slice(0,2)}:${s.slice(2,4)}:${s.slice(4,6)}`;
  }
  function fmtTs(iso) {
    try {
      const d = new Date(iso);
      return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' });
    } catch (e) { return iso; }
  }

  window.CwrAckImportTab = CwrAckImportTab;
})();
