// MVG v40.8 — Attachment bundle export (offline)
// No uploads. No network calls. CSP connect-src 'none'.
// Goal: help procurement/audit attachables become deterministic, replayable artifacts.

(() => {
  'use strict';

  const TOOL_BUILD = 'v40.8.0';

  const el = (id) => document.getElementById(id);

  const els = {
    dz: el('dz'),
    pdfAudit: el('pdfAudit'),
    jsonCountersigned: el('jsonCountersigned'),
    jsonKit: el('jsonKit'),

    ticketSystem: el('ticketSystem'),
    ticketId: el('ticketId'),

    btnPrepare: el('btnPrepare'),
    btnReset: el('btnReset'),
    btnDownloadJson: el('btnDownloadJson'),
    btnDownloadZip: el('btnDownloadZip'),
    btnCopyNote: el('btnCopyNote'),
    btnDownloadManifest: el('btnDownloadManifest'),
    btnDownloadNote: el('btnDownloadNote'),

    tagStatus: el('tagStatus'),
    statusDetail: el('statusDetail'),

    outApprovalShort: el('outApprovalShort'),
    outApprovalId: el('outApprovalId'),
    outTicket: el('outTicket'),
    outBundleHash: el('outBundleHash'),
    outNames: el('outNames'),
    outManifest: el('outManifest')
  };

  let prepared = false;
  let preparedData = null; // { names, manifest, note, files:[{role, suggestedName, originalName, bytes(Uint8Array), sha256Hex, bytesLen, internalIds?}] }

  // ----------- UI helpers -----------

  function setText(node, txt) {
    if (!node) return;
    node.textContent = (txt === undefined || txt === null) ? '' : String(txt);
  }

  function setStatus(level, detail) {
    if (!els.tagStatus) return;
    const t = els.tagStatus;
    t.classList.remove('pass', 'fail', 'hold', 'neutral');
    if (level === 'PASS') t.classList.add('pass');
    else if (level === 'FAIL') t.classList.add('fail');
    else if (level === 'HOLD') t.classList.add('hold');
    else t.classList.add('neutral');
    setText(t, level);
    setText(els.statusDetail, detail || '');
  }

  function setButtons(enabled) {
    const en = !!enabled;
    if (els.btnDownloadJson) els.btnDownloadJson.disabled = !en;
    if (els.btnDownloadZip) els.btnDownloadZip.disabled = !en;
    if (els.btnCopyNote) els.btnCopyNote.disabled = !en;
    if (els.btnDownloadManifest) els.btnDownloadManifest.disabled = !en;
    if (els.btnDownloadNote) els.btnDownloadNote.disabled = !en;
  }

  function resetOutputs() {
    prepared = false;
    preparedData = null;
    setButtons(false);
    setStatus('—', 'Select files, then click “Prepare bundle”.');
    setText(els.outApprovalShort, '—');
    setText(els.outApprovalId, '—');
    setText(els.outTicket, '—');
    setText(els.outBundleHash, '—');
    setText(els.outNames, '—');
    setText(els.outManifest, '—');
  }

  // ----------- bytes/hashes -----------

  function bytesToHex(bytes) {
    const out = [];
    for (let i = 0; i < bytes.length; i++) out.push(bytes[i].toString(16).padStart(2, '0'));
    return out.join('');
  }

  async function sha256Hex(bytes) {
    if (!crypto || !crypto.subtle || !crypto.subtle.digest) throw new Error('WEBCRYPTO_UNAVAILABLE');
    const digest = await crypto.subtle.digest('SHA-256', bytes);
    return bytesToHex(new Uint8Array(digest));
  }

  function shortCodeFromHex(hex) {
    const up = String(hex || '').toUpperCase();
    const core = up.slice(0, 16);
    return core.match(/.{1,4}/g)?.join('-') || core;
  }

  function shortCodeFromSha256Id(id) {
    const s = String(id || '').trim();
    const m = s.match(/^sha256:([0-9a-fA-F]{32,})$/);
    if (!m) return null;
    return shortCodeFromHex(m[1]);
  }

  function safeCode(code) {
    const c = String(code || '').trim().replace(/[^A-Za-z0-9-]/g, '');
    return c || 'Approval';
  }

  async function readFileBytes(file) {
    const buf = await file.arrayBuffer();
    return new Uint8Array(buf);
  }

  async function readFileJson(file) {
    const txt = await file.text();
    return JSON.parse(txt);
  }

  function canonicalize(obj) {
    if (obj === null || obj === undefined) return obj;
    if (Array.isArray(obj)) return obj.map(canonicalize);
    if (typeof obj === 'object') {
      const out = {};
      for (const k of Object.keys(obj).sort()) out[k] = canonicalize(obj[k]);
      return out;
    }
    return obj;
  }

  function canonicalJson(obj) {
    return JSON.stringify(canonicalize(obj), null, 0);
  }

  // ----------- extractors (best-effort; schema tolerant) -----------

  function extractTicket(obj) {
    if (!obj || typeof obj !== 'object') return null;

    const t = obj.ticket || obj.ticket_binding || obj.ticketBinding || null;
    if (t && typeof t === 'object') {
      const system = String(t.system || t.provider || t.type || '').trim();
      const id = String(t.id || t.ticket_id || t.ticketId || '').trim();
      if (system && id) return { system, id };
    }

    // Sometimes nested (e.g., inside an approval/certificate)
    const a = obj.approval || obj.certificate || obj.org_approval || null;
    if (a && typeof a === 'object') {
      const tt = a.ticket || a.ticket_binding || null;
      if (tt && typeof tt === 'object') {
        const system = String(tt.system || '').trim();
        const id = String(tt.id || '').trim();
        if (system && id) return { system, id };
      }
    }

    return null;
  }

  function extractApproval(obj) {
    if (!obj || typeof obj !== 'object') return { id: null, short: null };

    // Preferred: explicit approval object
    if (obj.approval && typeof obj.approval === 'object') {
      const id = obj.approval.id ? String(obj.approval.id) : null;
      const short = obj.approval.short_code ? String(obj.approval.short_code) : (id ? shortCodeFromSha256Id(id) : null);
      if (short || id) return { id, short };
    }

    // Org approval certificate schema (countersigned)
    const orgId = obj.org_approval_id || obj.approval_id || obj.approvalId || null;
    if (orgId) {
      const id = String(orgId);
      const short = shortCodeFromSha256Id(id);
      return { id, short };
    }

    // Fallback: countersigned receipt id
    const cs = obj.countersigned_receipt_id || obj.countersignedReceiptId || null;
    if (cs) {
      const id = String(cs);
      const short = shortCodeFromSha256Id(id);
      return { id, short };
    }

    return { id: null, short: null };
  }

  function classifyJsonRole(obj) {
    const sid = String((obj && obj.schema_id) || '').trim();
    if (sid === 'mvg.airgapped_verifier_kit.verification_summary@1') return 'kit_receipt';
    if (sid === 'mvg.org_approval_certificate@1') return 'countersigned_approval';
    // Be tolerant
    if (sid.includes('airgapped_verifier_kit')) return 'kit_receipt';
    if (sid.includes('org_approval')) return 'countersigned_approval';
    if (sid.includes('countersigned')) return 'countersigned_approval';
    return 'unknown';
  }

  function formatTicket(t) {
    if (!t) return '—';
    return `${t.system}:${t.id}`;
  }

  async function computeBundleManifestHash(primaryApprovalId, ticket) {
    const id = String(primaryApprovalId || '').trim();
    const m = id.match(/^sha256:([0-9a-fA-F]{64})$/);
    if (!m) return null;
    const basis = {
      schema_id: 'mvg.attachment_bundle_manifest_hash_basis@1',
      primary_approval_id: 'sha256:' + m[1].toLowerCase(),
      ticket: ticket ? { system: String(ticket.system || ''), id: String(ticket.id || '') } : null
    };
    const hex = await sha256Hex(new TextEncoder().encode(canonicalJson(basis)));
    return 'sha256:' + hex;
  }

  // ----------- suggested filenames -----------

  function suggestedNames(approvalShort) {
    const t = safeCode(approvalShort);
    return {
      audit_summary_pdf: `MVG_AuditSummary_${t}.pdf`,
      countersigned_approval_receipt_json: `MVG_CountersignedApproval_${t}.json`,
      kit_supply_chain_receipt_json: `MVG_KitSupplyChainReceipt_${t}.json`,
      bundle_manifest_json: `MVG_AttachmentBundle_Manifest_${t}.json`,
      ticket_note_txt: `MVG_TicketNote_${t}.txt`,
      bundle_zip: `MVG_AttachmentBundle_${t}.zip`,
      readme_txt: `MVG_AttachmentBundle_README_${t}.txt`
    };
  }

  // ----------- downloads -----------

  function downloadBytes(filename, bytes, mime) {
    const blob = new Blob([bytes], { type: mime || 'application/octet-stream' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.rel = 'noopener';
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      URL.revokeObjectURL(url);
      a.remove();
    }, 2000);
  }

  function downloadText(filename, text, mime) {
    downloadBytes(filename, new TextEncoder().encode(String(text || '')), mime || 'text/plain');
  }

  async function copyToClipboard(text) {
    const s = String(text || '');
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(s);
      return true;
    }
    // Fallback
    const ta = document.createElement('textarea');
    ta.value = s;
    ta.setAttribute('readonly', '');
    ta.style.position = 'fixed';
    ta.style.left = '-1000px';
    ta.style.top = '-1000px';
    document.body.appendChild(ta);
    ta.select();
    const ok = document.execCommand('copy');
    ta.remove();
    return ok;
  }

  // ----------- ZIP (store) builder (no external deps) -----------

  // CRC32 implementation (table-based)
  const CRC_TABLE = (() => {
    const table = new Uint32Array(256);
    for (let i = 0; i < 256; i++) {
      let c = i;
      for (let k = 0; k < 8; k++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
      table[i] = c >>> 0;
    }
    return table;
  })();

  function crc32(bytes) {
    let c = 0xFFFFFFFF;
    for (let i = 0; i < bytes.length; i++) {
      c = CRC_TABLE[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8);
    }
    return (c ^ 0xFFFFFFFF) >>> 0;
  }

  function dosDateTime(d) {
    const dt = d instanceof Date ? d : new Date();
    const year = Math.max(1980, dt.getFullYear());
    const month = dt.getMonth() + 1;
    const day = dt.getDate();
    const hours = dt.getHours();
    const minutes = dt.getMinutes();
    const seconds = Math.floor(dt.getSeconds() / 2); // 2-sec resolution
    const dosTime = (hours << 11) | (minutes << 5) | seconds;
    const dosDate = ((year - 1980) << 9) | (month << 5) | day;
    return { dosTime, dosDate };
  }

  function u16le(n) {
    const b = new Uint8Array(2);
    b[0] = n & 0xFF;
    b[1] = (n >>> 8) & 0xFF;
    return b;
  }
  function u32le(n) {
    const b = new Uint8Array(4);
    b[0] = n & 0xFF;
    b[1] = (n >>> 8) & 0xFF;
    b[2] = (n >>> 16) & 0xFF;
    b[3] = (n >>> 24) & 0xFF;
    return b;
  }

  function concatBytes(chunks) {
    const total = chunks.reduce((s, c) => s + (c ? c.length : 0), 0);
    const out = new Uint8Array(total);
    let off = 0;
    for (const c of chunks) {
      if (!c) continue;
      out.set(c, off);
      off += c.length;
    }
    return out;
  }

  function zipStore(fileEntries) {
    // fileEntries: [{name, bytes(Uint8Array), mtime(Date)}]
    const localParts = [];
    const centralParts = [];
    let offset = 0;

    const dt = dosDateTime(new Date());

    for (const f of fileEntries) {
      const nameBytes = new TextEncoder().encode(f.name);
      const data = f.bytes || new Uint8Array();
      const crc = crc32(data);
      const size = data.length;

      // Local file header
      const localHeader = concatBytes([
        u32le(0x04034b50),     // signature
        u16le(20),             // version needed
        u16le(0),              // flags
        u16le(0),              // compression (store)
        u16le(dt.dosTime),
        u16le(dt.dosDate),
        u32le(crc),
        u32le(size),
        u32le(size),
        u16le(nameBytes.length),
        u16le(0),              // extra len
        nameBytes
      ]);
      localParts.push(localHeader, data);

      // Central directory header
      const centralHeader = concatBytes([
        u32le(0x02014b50),     // signature
        u16le(20),             // version made by
        u16le(20),             // version needed
        u16le(0),              // flags
        u16le(0),              // compression
        u16le(dt.dosTime),
        u16le(dt.dosDate),
        u32le(crc),
        u32le(size),
        u32le(size),
        u16le(nameBytes.length),
        u16le(0),              // extra
        u16le(0),              // comment
        u16le(0),              // disk start
        u16le(0),              // internal attrs
        u32le(0),              // external attrs
        u32le(offset),         // local header offset
        nameBytes
      ]);
      centralParts.push(centralHeader);

      offset += localHeader.length + size;
    }

    const centralDir = concatBytes(centralParts);
    const centralOffset = offset;
    const end = concatBytes([
      u32le(0x06054b50), // end signature
      u16le(0),
      u16le(0),
      u16le(fileEntries.length),
      u16le(fileEntries.length),
      u32le(centralDir.length),
      u32le(centralOffset),
      u16le(0) // comment len
    ]);

    return concatBytes([...localParts, centralDir, end]);
  }

  // ----------- ticket comment template -----------

  function buildTicketNote(manifest, names) {
    const approvalId = (manifest && manifest.approval && manifest.approval.primary_id) ? manifest.approval.primary_id : '—';
    const approvalShort = (manifest && manifest.approval && manifest.approval.short_code) ? manifest.approval.short_code : '—';
    const bundleId = manifest && manifest.bundle_id ? manifest.bundle_id : '—';
    const bundleShort = manifest && manifest.bundle_short_code ? manifest.bundle_short_code : '—';
    const bundleManifestHash = manifest && manifest.attachment_bundle_manifest_sha256 ? manifest.attachment_bundle_manifest_sha256 : '—';
    const ticket = manifest && manifest.ticket ? `${manifest.ticket.system}:${manifest.ticket.id}` : '—';

    const cs = manifest && manifest.sources ? manifest.sources.countersigned_approval : null;
    const kit = manifest && manifest.sources ? manifest.sources.kit_supply_chain : null;

    const lines = [];
    lines.push('MVG attachment bundle (offline‑verifiable)');
    lines.push(`Approval: ${approvalShort} (${approvalId})`);
    lines.push(`Bundle: ${bundleShort} (${bundleId})`);
    lines.push(`Bundle manifest hash: ${bundleManifestHash}`);
    lines.push(`Ticket: ${ticket}`);
    lines.push('');
    lines.push('Attachments:');
    lines.push(`- ${names.audit_summary_pdf}  (save from verifier print)`);
    lines.push(`- ${names.countersigned_approval_receipt_json}`);
    lines.push(`- ${names.kit_supply_chain_receipt_json}`);
    lines.push(`- ${names.bundle_manifest_json}`);
    lines.push('');
    lines.push('Summary (non‑binding):');
    if (cs) lines.push(`- Countersigned approval: ${cs.decision || '—'} • org_approval_id=${cs.org_approval_id || '—'}`);
    if (kit) lines.push(`- Kit supply‑chain: ${kit.verdict || '—'} • kit_sha256=${kit.kit_sha256 || '—'} • keyring_snapshot=${kit.keyring_snapshot_sha256 || '—'}`);
    lines.push('');
    lines.push('Boundary conditions:');
    lines.push('- Non‑binding unless signed. Not legal advice. Not a compliance certification.');
    lines.push('- No patent license by publication.');
    lines.push('- Hashes in the bundle manifest are replayable offline.');
    return lines.join('\n');
  }

  function buildReadme(names) {
    return [
      'MVG Attachment Bundle',
      '',
      'This ZIP is a convenience wrapper for ticket/audit attachments.',
      '',
      'Recommended attachments:',
      `- ${names.audit_summary_pdf} (save via browser print from a verifier page)`,
      `- ${names.countersigned_approval_receipt_json}`,
      `- ${names.kit_supply_chain_receipt_json}`,
      `- ${names.bundle_manifest_json}`,
      '',
      'Boundary conditions:',
      '- Non-binding unless signed. Not legal advice. Not a compliance certification.',
      '- No patent license by publication.',
      '',
      `Tool build: ${TOOL_BUILD}`,
      ''
    ].join('\n');
  }

  // ----------- core prepare flow -----------

  async function prepareBundle() {
    resetOutputs();
    setStatus('HOLD', 'Preparing…');

    try {

    const fPdf = (els.pdfAudit && els.pdfAudit.files && els.pdfAudit.files[0]) ? els.pdfAudit.files[0] : null;
    const fCs = (els.jsonCountersigned && els.jsonCountersigned.files && els.jsonCountersigned.files[0]) ? els.jsonCountersigned.files[0] : null;
    const fKit = (els.jsonKit && els.jsonKit.files && els.jsonKit.files[0]) ? els.jsonKit.files[0] : null;

    if (!fCs && !fKit) {
      setStatus('HOLD', 'Select at least one JSON receipt (countersigned approval and/or kit supply‑chain receipt).');
      return;
    }

    let csObj = null, kitObj = null;
    const warnings = [];

    try {
      if (fCs) csObj = await readFileJson(fCs);
    } catch (e) {
      setStatus('FAIL', `Countersigned JSON parse error: ${String(e && (e.message || e) || e)}`);
      return;
    }

    try {
      if (fKit) kitObj = await readFileJson(fKit);
    } catch (e) {
      setStatus('FAIL', `Kit receipt JSON parse error: ${String(e && (e.message || e) || e)}`);
      return;
    }

    // Auto-fill ticket fields if possible
    const extractedTicket = csObj ? extractTicket(csObj) : null;
    if (extractedTicket) {
      if (els.ticketSystem && !String(els.ticketSystem.value || '').trim()) els.ticketSystem.value = extractedTicket.system;
      if (els.ticketId && !String(els.ticketId.value || '').trim()) els.ticketId.value = extractedTicket.id;
    }

    const ticketSystem = String(els.ticketSystem ? els.ticketSystem.value : '').trim();
    const ticketId = String(els.ticketId ? els.ticketId.value : '').trim();
    const ticket = (ticketSystem && ticketId) ? { system: ticketSystem, id: ticketId } : extractedTicket;

    // Approval extraction
    const csApproval = csObj ? extractApproval(csObj) : { id: null, short: null };
    const kitApproval = kitObj ? extractApproval(kitObj) : { id: null, short: null };

    const primary = (csApproval && (csApproval.short || csApproval.id)) ? csApproval : ((kitApproval && (kitApproval.short || kitApproval.id)) ? kitApproval : null);
    if (!primary) {
      setStatus('FAIL', 'Unable to derive Approval ID/short code from provided receipts.');
      return;
    }

    const approvalId = primary.id || '—';
    const approvalShort = safeCode(primary.short || (primary.id ? shortCodeFromSha256Id(primary.id) : null) || 'Approval');
    const bundleManifestHash = await computeBundleManifestHash(approvalId, ticket);

    // Warnings about mismatched internal IDs (expected across different artifacts)
    if (csApproval && csApproval.id && kitApproval && kitApproval.id && csApproval.id !== kitApproval.id) {
      warnings.push('Receipts contain different internal approval ids (expected if artifacts come from different verifiers). Manifest captures both.');
    }

    const names = suggestedNames(approvalShort);

    // Prepare file entries
    const files = [];

    // PDF
    if (fPdf) {
      const bytes = await readFileBytes(fPdf);
      const h = await sha256Hex(bytes);
      files.push({
        role: 'audit_summary_pdf',
        originalName: fPdf.name,
        suggestedName: names.audit_summary_pdf,
        bytes,
        sha256: 'sha256:' + h,
        bytesLen: bytes.length
      });
    }

    // Countersigned approval JSON
    if (fCs) {
      const bytes = await readFileBytes(fCs);
      const h = await sha256Hex(bytes);
      files.push({
        role: 'countersigned_approval_json',
        originalName: fCs.name,
        suggestedName: names.countersigned_approval_receipt_json,
        bytes,
        sha256: 'sha256:' + h,
        bytesLen: bytes.length,
        schema_id: String(csObj && csObj.schema_id || ''),
        internal_approval_id: csApproval.id || null,
        decision: csObj && (csObj.decision || csObj.result || csObj.verdict) ? String(csObj.decision || csObj.result || csObj.verdict) : null
      });
    }

    // Kit supply-chain receipt JSON
    if (fKit) {
      const bytes = await readFileBytes(fKit);
      const h = await sha256Hex(bytes);
      files.push({
        role: 'kit_supply_chain_receipt_json',
        originalName: fKit.name,
        suggestedName: names.kit_supply_chain_receipt_json,
        bytes,
        sha256: 'sha256:' + h,
        bytesLen: bytes.length,
        schema_id: String(kitObj && kitObj.schema_id || ''),
        internal_approval_id: kitApproval.id || null,
        verdict: kitObj && kitObj.verdict ? String(kitObj.verdict) : null,
        kit_sha256: kitObj && kitObj.computed ? String(kitObj.computed.sha256_hex || '') : (kitObj && kitObj.kit && kitObj.kit.sha256_hex ? String(kitObj.kit.sha256_hex) : null),
        keyring_snapshot_sha256: kitObj && kitObj.keyring && kitObj.keyring.snapshot_sha256 ? String(kitObj.keyring.snapshot_sha256) : (kitObj && kitObj.keyring_payload_sha256 ? String(kitObj.keyring_payload_sha256) : null)
      });
    }

    // Bundle id basis (deterministic; excludes generated_utc)
    const basis = {
      schema_id: 'mvg.attachment_bundle_id_basis@1',
      approval_short_code: approvalShort,
      ticket: ticket || null,
      files: files.map(f => ({ role: f.role, suggested_name: f.suggestedName, sha256: f.sha256, bytes: f.bytesLen }))
    };
    const basisHex = await sha256Hex(new TextEncoder().encode(canonicalJson(basis)));
    const bundleId = 'sha256:' + basisHex;
    const bundleShort = shortCodeFromHex(basisHex);

    // Build manifest (replay aid)
    const manifest = {
      schema_id: 'mvg.attachment_bundle_manifest@1',
      bundle_manifest_version: '1.0.0',
      tool_build: TOOL_BUILD,
      generated_utc: new Date().toISOString(),
      bundle_id: bundleId,
      bundle_short_code: bundleShort,
      attachment_bundle_manifest_sha256: bundleManifestHash || undefined,
      approval: {
        primary_id: approvalId,
        short_code: approvalShort
      },
      ticket: ticket || undefined,
      sources: {
        countersigned_approval: csObj ? {
          schema_id: String(csObj.schema_id || ''),
          org_approval_id: String(csObj.org_approval_id || csApproval.id || ''),
          decision: String(csObj.decision || csObj.result || csObj.verdict || ''),
          ticket: extractTicket(csObj) || undefined
        } : undefined,
        kit_supply_chain: kitObj ? {
          schema_id: String(kitObj.schema_id || ''),
          verdict: String(kitObj.verdict || ''),
          approval_id: (kitApproval && kitApproval.id) ? String(kitApproval.id) : '',
          kit_sha256: (kitObj && kitObj.computed && kitObj.computed.sha256_hex) ? String(kitObj.computed.sha256_hex) : '',
          keyring_snapshot_sha256: (kitObj && kitObj.keyring && kitObj.keyring.snapshot_sha256) ? String(kitObj.keyring.snapshot_sha256) : (kitObj && kitObj.keyring_payload_sha256 ? String(kitObj.keyring_payload_sha256) : ''),
        } : undefined
      },
      files: files.map(f => ({
        role: f.role,
        original_name: f.originalName,
        suggested_name: f.suggestedName,
        sha256: f.sha256,
        bytes: f.bytesLen
      })),
      boundary: {
        non_binding_unless_signed: true,
        no_patent_license_by_publication: true,
        not_legal_advice: true,
        not_a_compliance_certification: true,
        offline_tool_no_network_calls: true,
        note: 'This manifest is an attachment reconciliation aid; it does not modify signed receipts.'
      }
    };

    const note = buildTicketNote(manifest, names);

    // Update UI outputs
    setText(els.outApprovalShort, approvalShort);
    setText(els.outApprovalId, approvalId);
    setText(els.outTicket, ticket ? formatTicket(ticket) : '—');
    setText(els.outBundleHash, bundleManifestHash || '—');

    setText(els.outNames, [
      names.audit_summary_pdf,
      names.countersigned_approval_receipt_json,
      names.kit_supply_chain_receipt_json,
      names.bundle_manifest_json
    ].join('\n'));

    setText(els.outManifest, JSON.stringify(manifest, null, 2));

    prepared = true;
    preparedData = { names, manifest, note, files, bundleManifestHash };

    setButtons(true);

    if (warnings.length > 0) setStatus('HOLD', warnings.join(' '));
    else setStatus('PASS', 'Bundle prepared.');
    } catch (e) {
      const msg = String((e && (e.message || e)) || e);
      if (msg.includes('WEBCRYPTO_UNAVAILABLE')) {
        setStatus('FAIL', 'WebCrypto SHA‑256 is unavailable. Open this page over https:// or http://localhost, or use a modern browser.');
      } else {
        setStatus('FAIL', 'Bundle preparation failed: ' + msg);
      }
    }

  }

  // ----------- actions -----------

  function downloadJsonAttachments() {
    if (!prepared || !preparedData) return;
    const { files } = preparedData;
    for (const f of files) {
      if (f.role === 'countersigned_approval_json' || f.role === 'kit_supply_chain_receipt_json') {
        downloadBytes(f.suggestedName, f.bytes, 'application/json');
      }
    }
  }

  function downloadManifest() {
    if (!prepared || !preparedData) return;
    const { names, manifest } = preparedData;
    downloadText(names.bundle_manifest_json, JSON.stringify(manifest, null, 2), 'application/json');
  }

  function downloadTicketNote() {
    if (!prepared || !preparedData) return;
    const { names, note } = preparedData;
    downloadText(names.ticket_note_txt, note, 'text/plain');
  }

  async function handleCopyNote() {
    if (!prepared || !preparedData) return;
    try {
      await copyToClipboard(preparedData.note);
      setStatus('PASS', 'Ticket comment copied.');
      setTimeout(() => setStatus('PASS', 'Bundle prepared.'), 1600);
    } catch (e) {
      setStatus('HOLD', 'Copy failed. You can download the ticket note instead.');
    }
  }

  function downloadZip() {
    if (!prepared || !preparedData) return;
    const { names, manifest, note, files } = preparedData;

    const entries = [];

    for (const f of files) {
      // Include all user-provided artifacts, renamed.
      entries.push({ name: f.suggestedName, bytes: f.bytes });
    }

    entries.push({ name: names.bundle_manifest_json, bytes: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) });
    entries.push({ name: names.ticket_note_txt, bytes: new TextEncoder().encode(note) });
    entries.push({ name: names.readme_txt, bytes: new TextEncoder().encode(buildReadme(names)) });

    const zipBytes = zipStore(entries);
    downloadBytes(names.bundle_zip, zipBytes, 'application/zip');
  }

  // ----------- drag/drop assignment -----------

  function setInputFile(input, file) {
    if (!input) return;
    // Programmatic set via DataTransfer (best effort; some browsers may restrict).
    try {
      const dt = new DataTransfer();
      dt.items.add(file);
      input.files = dt.files;
    } catch (_e) {
      // ignore
    }
  }

  async function tryAssignJson(file) {
    try {
      const obj = JSON.parse(await file.text());
      const role = classifyJsonRole(obj);
      if (role === 'kit_receipt') {
        setInputFile(els.jsonKit, file);
      } else if (role === 'countersigned_approval') {
        setInputFile(els.jsonCountersigned, file);
      } else {
        // Fallback: first empty slot
        if (els.jsonCountersigned && (!els.jsonCountersigned.files || els.jsonCountersigned.files.length === 0)) setInputFile(els.jsonCountersigned, file);
        else if (els.jsonKit && (!els.jsonKit.files || els.jsonKit.files.length === 0)) setInputFile(els.jsonKit, file);
      }
    } catch (_e) {
      // Fallback: first empty slot
      if (els.jsonCountersigned && (!els.jsonCountersigned.files || els.jsonCountersigned.files.length === 0)) setInputFile(els.jsonCountersigned, file);
      else if (els.jsonKit && (!els.jsonKit.files || els.jsonKit.files.length === 0)) setInputFile(els.jsonKit, file);
    }
  }

  function assignFileToInput(file) {
    const name = String(file && file.name || '').toLowerCase();
    const isPdf = (file && file.type === 'application/pdf') || name.endsWith('.pdf');
    const isJson = (file && file.type === 'application/json') || name.endsWith('.json');

    if (isPdf) {
      setInputFile(els.pdfAudit, file);
      return;
    }
    if (isJson) {
      // async classify
      tryAssignJson(file);
      return;
    }
  }

  function wireDragDrop() {
    const dz = els.dz;
    if (!dz) return;
    const prevent = (e) => { e.preventDefault(); e.stopPropagation(); };
    dz.addEventListener('dragenter', (e) => { prevent(e); dz.classList.add('drag'); });
    dz.addEventListener('dragover', (e) => { prevent(e); dz.classList.add('drag'); });
    dz.addEventListener('dragleave', (e) => { prevent(e); dz.classList.remove('drag'); });
    dz.addEventListener('drop', (e) => {
      prevent(e);
      dz.classList.remove('drag');
      const files = e.dataTransfer && e.dataTransfer.files ? Array.from(e.dataTransfer.files) : [];
      for (const f of files) assignFileToInput(f);
      setStatus('—', 'Files staged. Click “Prepare bundle”.');
    });
  }

  // ----------- wiring -----------

  function wire() {
    resetOutputs();
    wireDragDrop();

    if (els.btnPrepare) els.btnPrepare.addEventListener('click', () => { prepareBundle(); });

    if (els.btnReset) els.btnReset.addEventListener('click', () => {
      if (els.pdfAudit) els.pdfAudit.value = '';
      if (els.jsonCountersigned) els.jsonCountersigned.value = '';
      if (els.jsonKit) els.jsonKit.value = '';
      if (els.ticketSystem) els.ticketSystem.value = '';
      if (els.ticketId) els.ticketId.value = '';
      resetOutputs();
    });

    if (els.btnDownloadJson) els.btnDownloadJson.addEventListener('click', () => { downloadJsonAttachments(); });
    if (els.btnDownloadZip) els.btnDownloadZip.addEventListener('click', () => { downloadZip(); });
    if (els.btnCopyNote) els.btnCopyNote.addEventListener('click', () => { handleCopyNote(); });
    if (els.btnDownloadManifest) els.btnDownloadManifest.addEventListener('click', () => { downloadManifest(); });
    if (els.btnDownloadNote) els.btnDownloadNote.addEventListener('click', () => { downloadTicketNote(); });
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
  else wire();
})();
