// MVG v40.7 — Air‑gapped verifier kit self‑verifier (offline)
// No network calls (CSP connect-src 'none').
(() => {
  'use strict';

  const VERIFIER_BUILD = 'v40.7.0';

  const ROOT_TRUST_ANCHORS = [{"kid":"ta.mvg.airgapped.kit.root.2026-02-12.v1.demo.d9f8e47adc","kty":"OKP","crv":"Ed25519","x_b64u":"S25f-wqW5N0-gXz378uyZyeXnW7Fdncqm3raIQqsz6E","status":"deprecated","grace_until_utc":"2026-06-01T00:00:00Z"},{"kid":"ta.mvg.airgapped.kit.root.2026-02-12.v2.demo.19a29d194d","kty":"OKP","crv":"Ed25519","x_b64u":"wPqhvU26S7aC1QA7tXifiltldtqKEPt6IEc2--xeclI","status":"active"}];
  const DEFAULT_KEYRING_DSSE = {"payload":" ","payloadType":"application/vnd.mvg.airgapped_verifier_kit.keyring_snapshot+json","signatures":[{"keyid":"ta.mvg.airgapped.kit.root.2026-02-12.v2.demo.19a29d194d","sig":"65t3qD-3jUnK_OcH94xspUvdM8KZB2SOWb3-KLVnyXvh4Ds_-UHYrYKkFnzf1v3LJxZmEXIix8VWP0Xn2sNYDg"}]};

  const ROLE_KIT = 'airgap_kit.release.dsse.sign';
  const ROLE_COMMIT = 'airgap_kit.commitments.dsse.sign';
  const ROLE_HEADCHAIN = 'airgap_kit.headchain.dsse.sign';

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

  const els = {
    kitZip: el('kitZip'),
    kitDsse: el('kitDsse'),
    keyringDsse: el('keyringDsse'),

    chkRequireLog: el('chkRequireLog'),
    chkRequireHeadchain: el('chkRequireHeadchain'),

    releaseReceiptDsse: el('releaseReceiptDsse'),
    commitLogJson: el('commitLogJson'),
    commitLogHeadDsse: el('commitLogHeadDsse'),

    headChainJson: el('headChainJson'),
    headChainHeadDsse: el('headChainHeadDsse'),

    btnVerify: el('btnVerify'),
    btnReset: el('btnReset'),
    btnPrintSummary: el('btnPrintSummary'),
    btnDownloadReceipt: el('btnDownloadReceipt'),
    btnCopyNote: el('btnCopyNote'),
    printArea: el('printArea'),
    copyStatus: el('copyStatus'),

    verdictTag: el('verdictTag'),
    verdictDetail: el('verdictDetail'),

    computedSha: el('computedSha'),
    computedBytes: el('computedBytes'),
    approvalId: el('approvalId'),
    approvalShort: el('approvalShort'),
    suggestedNames: el('suggestedNames'),
    dsseKeyId: el('dsseKeyId'),
    keyStatus: el('keyStatus'),
    keyringInfo: el('keyringInfo'),
    keyringRoot: el('keyringRoot'),
    keyringSnapshotSha: el('keyringSnapshotSha'),
    payloadSchema: el('payloadSchema'),
    payloadSha: el('payloadSha'),
    payloadBytes: el('payloadBytes'),

    computedCommitment: el('computedCommitment'),
    receiptCommitment: el('receiptCommitment'),
    commitLogVerdict: el('commitLogVerdict'),
    commitLogHeadEntry: el('commitLogHeadEntry'),
    headChainVerdict: el('headChainVerdict'),
  };

  let lastVerification = null;

  // ---------- small utilities ----------

  function setText(node, txt) { if (node) node.textContent = txt; }

  function setVerdict(level, detail) {
    const tag = els.verdictTag;
    tag.classList.remove('pass','fail','hold','neutral');
    if (level === 'PASS') tag.classList.add('pass');
    else if (level === 'FAIL') tag.classList.add('fail');
    else if (level === 'HOLD') tag.classList.add('hold');
    else tag.classList.add('neutral');
    setText(tag, level);
    setText(els.verdictDetail, detail || '');
  }

  function resetOutputs() {
    setVerdict('—', '');
    lastVerification = null;
    if (els.btnPrintSummary) els.btnPrintSummary.disabled = true;
    if (els.btnDownloadReceipt) els.btnDownloadReceipt.disabled = true;
    if (els.btnCopyNote) els.btnCopyNote.disabled = true;
    if (els.copyStatus) els.copyStatus.textContent = '';
    for (const k of ['computedSha','computedBytes','approvalId','approvalShort','suggestedNames','dsseKeyId','keyStatus','keyringInfo','keyringRoot','keyringSnapshotSha','payloadSchema','payloadSha','payloadBytes','computedCommitment','receiptCommitment','commitLogVerdict','commitLogHeadEntry','headChainVerdict']) {
      setText(els[k], '—');
    }
  }

  function escapeHtml(s){
    return String(s ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  function downloadJson(filename, obj){
    const blob = new Blob([JSON.stringify(obj, null, 2)], {type:'application/json'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(()=>{ try{ URL.revokeObjectURL(url); }catch(_e){} }, 1000);
  }



  function suggestedAttachmentNames(approvalShort, fallbackTag) {
    const raw = String(approvalShort || '').trim();
    const fb = String(fallbackTag || 'Approval').trim();
    const tag = raw && raw !== '—' ? raw : fb;
    const safe = tag.replace(/[^A-Za-z0-9-]/g, '');
    const t = safe || 'Approval';
    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`,
      spec: '/downloads2/MVG_Attachment_Naming_Convention_Spec_v1.0.0.md'
    };
  }

  async function copyToClipboard(text) {
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(text);
        return true;
      }
    } catch (_e) {}

    // Fallback for non-secure contexts / hardened environments.
    try {
      const ta = document.createElement('textarea');
      ta.value = String(text);
      ta.setAttribute('readonly', '');
      ta.style.position = 'fixed';
      ta.style.top = '-1000px';
      ta.style.left = '-1000px';
      document.body.appendChild(ta);
      ta.focus();
      ta.select();
      const ok = document.execCommand('copy');
      ta.remove();
      return !!ok;
    } catch (_e) {
      return false;
    }
  }

  function collectSummary(){
    const get = (n)=> (n && n.textContent !== undefined) ? String(n.textContent).trim() : '—';
    const nowUtc = new Date().toISOString();

    const kitSha = get(els.computedSha);
    const kitBytes = get(els.computedBytes);

    return {
      schema_id: 'mvg.airgapped_verifier_kit.verification_summary@1',
      verifier_build: VERIFIER_BUILD,
      generated_utc: nowUtc,
      verdict: get(els.verdictTag),
      detail: get(els.verdictDetail),
      approval: {
        id: get(els.approvalId),
        short_code: get(els.approvalShort)
      },
      suggested_attachments: suggestedAttachmentNames(get(els.approvalShort), kitSha && kitSha !== '—' ? kitSha.slice(0,16) : 'Approval'),
      kit: {
        sha256_hex: kitSha,
        bytes: kitBytes ? Number(kitBytes) : null
      },
      dsse: {
        keyid: get(els.dsseKeyId),
        key_status: get(els.keyStatus)
      },
      keyring: {
        info: get(els.keyringInfo),
        root_kid: get(els.keyringRoot),
        snapshot_sha256: get(els.keyringSnapshotSha)
      },
      payload: {
        schema_id: get(els.payloadSchema),
        sha256_hex: get(els.payloadSha),
        bytes: get(els.payloadBytes)
      },
      supply_chain: {
        computed_commitment_sha256_hex: get(els.computedCommitment),
        receipt_commitment_sha256_hex: get(els.receiptCommitment),
        commitment_log: get(els.commitLogVerdict),
        commitment_log_head_entry_sha256_hex: get(els.commitLogHeadEntry),
        headchain: get(els.headChainVerdict)
      },
      boundary: {
        non_binding_unless_signed: true,
        no_patent_license_by_publication: true,
        not_legal_advice: true,
        not_a_compliance_certification: true,
        offline_verifier_no_network_calls: true
      }
    };
  }

  function renderAndPrintSummary(){
    if(!els.printArea) return;
    const s = collectSummary();
    lastVerification = s;

    const context = 'Air‑gapped verifier kit — Audit summary';
    const verdict = (s.verdict || '—').toUpperCase();
    const detail = s.detail || '';
    const detailHtml = detail ? `<p class="muted" style="margin-top:10px">${escapeHtml(detail)}</p>` : '';
    const kitSha = s.kit && s.kit.sha256_hex ? s.kit.sha256_hex : '—';
    const kitBytes = (s.kit && s.kit.bytes !== null && s.kit.bytes !== undefined) ? String(s.kit.bytes) : '—';

    const keyringHash = (s.keyring && s.keyring.snapshot_sha256) ? s.keyring.snapshot_sha256 : '—';

    const approvalId = (s.approval && s.approval.id) ? s.approval.id : '—';
    const approvalShort = (s.approval && s.approval.short_code) ? s.approval.short_code : '—';

    const names = suggestedAttachmentNames(approvalShort !== '—' ? approvalShort : '', kitSha !== '—' ? kitSha.slice(0,16) : 'Approval');

    els.printArea.innerHTML = `
      <div style="font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">
        <div class="muted" style="border:1px solid #d7d7d7; padding:6px 8px; border-radius:12px; margin-bottom:10px; font-size:10.5pt;">
          <b>Attachment cover:</b> Reviewer-generated audit summary for internal procurement/security/legal workflows.<br/>
          <span class="muted">Suggested filename:</span> <span class="mono">${escapeHtml(names.audit_summary_pdf)}</span> Non‑binding unless signed; no patent license by publication; not legal advice; not a compliance certification.
        </div>

        <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:16px;">
          <div>
            <h1>${escapeHtml(context)}</h1>
            <div class="muted">Generated (UTC): <span class="mono">${escapeHtml(s.generated_utc)}</span> • Offline verifier (no uploads, no network calls)</div>
          </div>
          <div style="text-align:right">
            <div class="pill"><b>FINAL: ${escapeHtml(verdict)}</b></div>
          </div>
        </div>

        ${detailHtml}

        <h2>Identifiers</h2>
        <table>
          <tbody>
            <tr><th style="width:34%">Kit sha256</th><td class="mono">${escapeHtml(kitSha)}</td></tr>
            <tr><th>Kit bytes</th><td class="mono">${escapeHtml(kitBytes)}</td></tr>
            <tr><th>Approval ID</th><td class="mono">${escapeHtml(approvalId)}${approvalShort !== '—' ? ' <span class=\"muted\">(' + escapeHtml(approvalShort) + ')</span>' : ''}</td></tr>
            <tr><th>DSSE keyid</th><td class="mono">${escapeHtml(s.dsse.keyid || '—')}</td></tr>
            <tr><th>Key status</th><td class="mono">${escapeHtml(s.dsse.key_status || '—')}</td></tr>
            <tr><th>Keyring snapshot hash</th><td class="mono">${escapeHtml(keyringHash)}</td></tr>
            <tr><th>Keyring</th><td class="mono">${escapeHtml(s.keyring.info || '—')}</td></tr>
            <tr><th>Commitment (computed)</th><td class="mono">${escapeHtml(s.supply_chain.computed_commitment_sha256_hex || '—')}</td></tr>
            <tr><th>Commitment (receipt)</th><td class="mono">${escapeHtml(s.supply_chain.receipt_commitment_sha256_hex || '—')}</td></tr>
            <tr><th>Commitment log</th><td class="mono">${escapeHtml(s.supply_chain.commitment_log || '—')}</td></tr>
            <tr><th>Headchain</th><td class="mono">${escapeHtml(s.supply_chain.headchain || '—')}</td></tr>
          </tbody>
        </table>

        <h2>Attachments (recommended naming)</h2>
        <table>
          <tbody>
            <tr><th style="width:34%">Audit summary (this PDF)</th><td class="mono">${escapeHtml(names.audit_summary_pdf)}</td></tr>
            <tr><th>Kit supply‑chain receipt (JSON)</th><td class="mono">${escapeHtml(names.kit_supply_chain_receipt_json)}</td></tr>
            <tr><th>Countersigned approval receipt (JSON)</th><td class="mono">${escapeHtml(names.countersigned_approval_receipt_json)} <span class="muted">(optional)</span></td></tr>
          </tbody>
        </table>

        <h2>Boundary conditions</h2>
        <ul class="muted" style="margin:0; padding-left: 18px;">
          <li>Reviewer‑generated summary. Intended for internal procurement/security/legal workflows.</li>
          <li>Non‑binding unless signed. Not legal advice. Not a compliance certification.</li>
          <li>No patent license by publication.</li>
          <li>Approval ID is deterministic and recomputable offline; use it to associate this PDF with its JSON receipt(s).</li>
          <li>Keyring snapshot hash is deterministic and recomputable offline from the signed keyring snapshot.</li>
        </ul>
      </div>
    `;
    window.print();
  }


  function padB64u(s) {
    const m = s.length % 4;
    return m === 0 ? s : (s + '='.repeat(4 - m));
  }

  function b64uToBytes(s) {
    const bin = atob(padB64u(s).replace(/-/g,'+').replace(/_/g,'/'));
    const u8 = new Uint8Array(bin.length);
    for (let i=0;i<bin.length;i++) u8[i] = bin.charCodeAt(i);
    return u8;
  }

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

  async function sha256Hex(bytes) {
    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;
  }

  async function computeApprovalId(kitShaHex, kitBytesLen, keyringSnapshotShaHex) {
    // Deterministic, recomputable offline.
    // Binds the kit fingerprint and the verifying keyring snapshot (supply-chain governance).
    const basis = {
      schema_id: 'mvg.airgapped_verifier_kit.approval_id_basis@1',
      kit_sha256_hex: String(kitShaHex || ''),
      kit_bytes: Number(kitBytesLen || 0),
      keyring_snapshot_sha256_hex: keyringSnapshotShaHex ? String(keyringSnapshotShaHex) : null
    };
    const enc = new TextEncoder();
    const idHex = await sha256Hex(enc.encode(canonicalJson(basis)));
    return {
      approval_id: 'sha256:' + idHex,
      approval_short_code: shortCodeFromHex(idHex),
      basis
    };
  }

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

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

  function dssePAE(payloadType, payloadBytes) {
    const enc = new TextEncoder();
    const pt = enc.encode(payloadType);
    const lenPt = enc.encode(String(pt.length));
    const lenPl = enc.encode(String(payloadBytes.length));
    const prefix = enc.encode('DSSEv1 ');
    const space = enc.encode(' ');
    const out = new Uint8Array(prefix.length + lenPt.length + 1 + pt.length + 1 + lenPl.length + 1 + payloadBytes.length);
    let o=0;
    out.set(prefix,o); o+=prefix.length;
    out.set(lenPt,o); o+=lenPt.length;
    out.set(space,o); o+=1;
    out.set(pt,o); o+=pt.length;
    out.set(space,o); o+=1;
    out.set(lenPl,o); o+=lenPl.length;
    out.set(space,o); o+=1;
    out.set(payloadBytes,o);
    return out;
  }

  async function importEd25519PublicKey(rawBytes) {
    return crypto.subtle.importKey('raw', rawBytes, { name: 'Ed25519' }, false, ['verify']);
  }

  async function verifyEd25519Sig(pubKey, msgBytes, sigBytes) {
    return crypto.subtle.verify({ name: 'Ed25519' }, pubKey, sigBytes, msgBytes);
  }

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

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

  function parseUtc(iso) {
    if (!iso || typeof iso !== 'string') return null;
    const t = Date.parse(iso);
    return Number.isFinite(t) ? t : null;
  }

  function evaluateKeyLifecycle(key, nowMs) {
    if (!key) return { verdict: 'HOLD', note: 'missing_key' };
    const status = (key.status || '').toLowerCase();
    if (status === 'revoked') return { verdict: 'FAIL', note: 'revoked' };
    const nb = parseUtc(key.not_before_utc);
    const na = parseUtc(key.not_after_utc);
    if (nb !== null && nowMs < nb) return { verdict: 'HOLD', note: 'not_before' };
    if (na !== null && nowMs > na) return { verdict: 'HOLD', note: 'expired' };

    if (status === 'deprecated') {
      const gu = parseUtc(key.grace_until_utc);
      if (gu !== null && nowMs <= gu) return { verdict: 'PASS', note: 'deprecated_within_grace' };
      return { verdict: 'HOLD', note: 'deprecated_out_of_grace' };
    }

    if (status === 'active') return { verdict: 'PASS', note: 'active' };

    return { verdict: 'HOLD', note: status || 'unknown' };
  }

  function keyringKeyByKid(keyringPayload, kid) {
    const keys = (keyringPayload && Array.isArray(keyringPayload.keys)) ? keyringPayload.keys : [];
    for (const k of keys) {
      if (k && k.kid === kid) return k;
    }
    return null;
  }

  function keyHasRole(key, role) {
    const roles = (key && Array.isArray(key.roles)) ? key.roles : [];
    return roles.includes(role);
  }

  async function verifyDsseEnvelope(env, expectedPayloadType, pubKeyRawBytes) {
    if (!env || typeof env !== 'object') throw new Error('DSSE_INVALID');
    if (env.payloadType !== expectedPayloadType) throw new Error('DSSE_PAYLOADTYPE_MISMATCH');
    const sig0 = (env.signatures && env.signatures[0]) ? env.signatures[0] : null;
    if (!sig0 || typeof sig0.sig !== 'string') throw new Error('DSSE_SIGNATURE_MISSING');
    const payloadBytes = b64uToBytes(env.payload);
    const sigBytes = b64uToBytes(sig0.sig);
    const msg = dssePAE(env.payloadType, payloadBytes);
    const pubKey = await importEd25519PublicKey(pubKeyRawBytes);
    const ok = await verifyEd25519Sig(pubKey, msg, sigBytes);
    if (!ok) throw new Error('DSSE_SIGNATURE_INVALID');
    const payloadObj = JSON.parse(new TextDecoder().decode(payloadBytes));
    return { keyid: sig0.keyid || '—', payloadObj, payloadBytes };
  }

  async function loadKeyring() {
    // Use user-provided keyring DSSE if present; otherwise use built-in.
    const overrideFile = els.keyringDsse && els.keyringDsse.files && els.keyringDsse.files[0] ? els.keyringDsse.files[0] : null;
    const env = overrideFile ? await readFileJson(overrideFile) : DEFAULT_KEYRING_DSSE;

    const sig0 = (env.signatures && env.signatures[0]) ? env.signatures[0] : null;
    if (!sig0) throw new Error('KEYRING_SIGNATURE_MISSING');
    const root = (ROOT_TRUST_ANCHORS || []).find(k => k && k.kid === sig0.keyid);
    if (!root) throw new Error('KEYRING_ROOT_KID_MISMATCH');

    const rootPub = b64uToBytes(root.x_b64u);
    const verified = await verifyDsseEnvelope(env, 'application/vnd.mvg.airgapped_verifier_kit.keyring_snapshot+json', rootPub);

    const kr = verified.payloadObj;
    if (!kr || kr.schema_id !== 'mvg.airgapped_verifier_kit.keyring@1') throw new Error('KEYRING_SCHEMA_INVALID');

    const src = overrideFile ? ('override:' + overrideFile.name) : 'built-in';
    const payloadSha = 'sha256:' + (await sha256Hex(verified.payloadBytes));
    return { keyring: kr, source: src, keyring_payload_sha256: payloadSha, root_kid: root.kid, root_status: (root.status||'').toLowerCase() || 'unknown' };
  }

  async function verifyWithKeyring(env, expectedPayloadType, requiredRole, keyringPayload) {
    if (!env || typeof env !== 'object') throw new Error('DSSE_INVALID');
    if (env.payloadType !== expectedPayloadType) throw new Error('DSSE_PAYLOADTYPE_MISMATCH');
    const sig0 = (env.signatures && env.signatures[0]) ? env.signatures[0] : null;
    if (!sig0) throw new Error('DSSE_SIGNATURE_MISSING');
    const kid = sig0.keyid;
    const key = keyringKeyByKid(keyringPayload, kid);
    if (!key) throw new Error('SIGNING_KEY_NOT_IN_KEYRING');
    if (!keyHasRole(key, requiredRole)) throw new Error('SIGNING_KEY_ROLE_MISMATCH');

    const nowMs = Date.now();
    const life = evaluateKeyLifecycle(key, nowMs);
    if (life.verdict === 'FAIL') throw new Error('SIGNING_KEY_REVOKED');
    // HOLD still allows crypto verification, but overall result becomes HOLD.

    const pubRaw = b64uToBytes(key.x_b64u);
    const verified = await verifyDsseEnvelope(env, expectedPayloadType, pubRaw);
    return { kid, key, life, payloadObj: verified.payloadObj, payloadBytes: verified.payloadBytes };
  }

  function computeCommitment(kitShaHex, nonceB64u) {
    const enc = new TextEncoder();
    const kitShaBytes = enc.encode(kitShaHex);
    const nonce = b64uToBytes(nonceB64u);
    const buf = new Uint8Array(kitShaBytes.length + nonce.length);
    buf.set(kitShaBytes,0);
    buf.set(nonce, kitShaBytes.length);
    return sha256Hex(buf);
  }

  function computeEntrySha(entry) {
    const core = {};
    const keys = Object.keys(entry || {}).sort();
    for (const k of keys) {
      if (k === 'entry_sha256') continue;
      core[k] = entry[k];
    }
    const s = JSON.stringify(core, null, 0);
    const enc = new TextEncoder();
    return sha256Hex(enc.encode(s));
  }

  async function verifyCommitmentLog(logObj, expectedCommitmentSha) {
    if (!logObj || logObj.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log@1') throw new Error('LOG_SCHEMA');
    const entries = Array.isArray(logObj.entries) ? logObj.entries : [];
    if (entries.length < 1) throw new Error('LOG_EMPTY');

    // Chain + entry hashes
    let prev = '0'.repeat(64);
    for (let i=0;i<entries.length;i++) {
      const e = entries[i];
      if (!e || e.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log_entry@1') throw new Error('LOG_ENTRY_SCHEMA');
      if (e.prev_entry_sha256 !== prev) throw new Error('LOG_CHAIN');
      const calc = await computeEntrySha(e);
      if (calc !== e.entry_sha256) throw new Error('LOG_ENTRY_HASH');
      prev = e.entry_sha256;
    }

    // Inclusion
    const found = entries.some(e => e.public_commitment_sha256 === expectedCommitmentSha);
    if (!found) throw new Error('LOG_NOT_FOUND');

    const enc = new TextEncoder();
    const logSha = await sha256Hex(enc.encode(canonicalJson(logObj)));
    return { logSha, headEntry: entries[entries.length-1].entry_sha256, entryCount: entries.length };
  }

  async function verifyHeadchain(headchainLog, headchainHead, commitmentLogHeadEnv) {
    if (!headchainLog || headchainLog.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log_headchain@1') throw new Error('HEADCHAIN_SCHEMA');
    const entries = Array.isArray(headchainLog.entries) ? headchainLog.entries : [];
    if (entries.length < 1) throw new Error('HEADCHAIN_EMPTY');

    // Verify chain
    let prev = '0'.repeat(64);
    for (let i=0;i<entries.length;i++) {
      const e = entries[i];
      if (!e || e.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log_headchain_entry@1') throw new Error('HEADCHAIN_ENTRY_SCHEMA');
      if (e.prev_entry_sha256 !== prev) throw new Error('HEADCHAIN_CHAIN');
      const calc = await computeEntrySha(e);
      if (calc !== e.entry_sha256) throw new Error('HEADCHAIN_ENTRY_HASH');
      prev = e.entry_sha256;
    }

    const enc = new TextEncoder();
    const logSha = await sha256Hex(enc.encode(canonicalJson(headchainLog)));
    if (headchainHead.log_sha256 !== logSha) throw new Error('HEADCHAIN_LOG_SHA');
    if (headchainHead.entry_count !== entries.length) throw new Error('HEADCHAIN_COUNT');
    if (headchainHead.head_entry_sha256 !== entries[entries.length-1].entry_sha256) throw new Error('HEADCHAIN_HEAD_ENTRY');

    // Inclusion check: the commitment log head DSSE (envelope) sha256 is listed in headchain
    const headEnvSha = await sha256Hex(enc.encode(canonicalJson(commitmentLogHeadEnv)));
    const found = entries.some(e => e.commitment_log_head_dsse_sha256 === headEnvSha);
    if (!found) throw new Error('HEADCHAIN_NOT_FOUND');

    return { logSha, headEntry: entries[entries.length-1].entry_sha256, entryCount: entries.length };
  }

  async function run() {
    resetOutputs();

    // Basic inputs
    const kitFile = els.kitZip.files && els.kitZip.files[0] ? els.kitZip.files[0] : null;
    const dsseFile = els.kitDsse.files && els.kitDsse.files[0] ? els.kitDsse.files[0] : null;

    if (!kitFile || !dsseFile) {
      setVerdict('HOLD', 'Select a kit ZIP and a DSSE signature JSON.');
      return;
    }

    // Load keyring
    let keyringInfo = null;
    try {
      const kr = await loadKeyring();
      keyringInfo = kr;
      setText(els.keyringInfo, `${kr.keyring.keyring_version || '—'} (${kr.source})`);
      setText(els.keyringRoot, kr.root_kid || '—');
      setText(els.keyringSnapshotSha, kr.keyring_payload_sha256 || '—');
    } catch (e) {
      setVerdict('HOLD', 'Keyring verification failed: ' + (e && e.message ? e.message : String(e)));
      setText(els.keyringInfo, 'HOLD');
      setText(els.keyringRoot, kr.root_kid || '—');
      setText(els.keyringSnapshotSha, '—');
      return;
    }

    // Compute kit sha/bytes
    const kitBytes = await readFileBytes(kitFile);
    const kitSha = await sha256Hex(kitBytes);
    setText(els.computedSha, kitSha);
    setText(els.computedBytes, String(kitBytes.length));

    // Approval ID: one identifier to match JSON receipt ↔ printed PDF summary.
    try {
      const a = await computeApprovalId(kitSha, kitBytes.length, keyringInfo && keyringInfo.keyring_payload_sha256 ? keyringInfo.keyring_payload_sha256 : null);
      setText(els.approvalId, a.approval_id);
      setText(els.approvalShort, a.approval_short_code);
    } catch (e) {
      // If crypto fails here, treat as HOLD for procurement-grade workflows.
      setText(els.approvalId, '—');
      setText(els.approvalShort, '—');
      const names = suggestedAttachmentNames('', kitSha ? kitSha.slice(0,16) : 'Approval');
      if (els.suggestedNames) els.suggestedNames.textContent = names.audit_summary_pdf + '\n' + names.countersigned_approval_receipt_json + '\n' + names.kit_supply_chain_receipt_json;
    }

    // Suggested attachment names (for ticket/audit attachment reconciliation)
    try {
      const short = (els.approvalShort && els.approvalShort.textContent) ? String(els.approvalShort.textContent).trim() : '';
      const names = suggestedAttachmentNames(short !== '—' ? short : '', kitSha ? kitSha.slice(0,16) : 'Approval');
      if (els.suggestedNames) els.suggestedNames.textContent = names.audit_summary_pdf + '\n' + names.countersigned_approval_receipt_json + '\n' + names.kit_supply_chain_receipt_json;
    } catch (_e) { /* ignore */ }

    // Enable outputs after a final verdict is computed.


    // Verify kit DSSE via keyring
    let dsseEnv = null;
    try {
      dsseEnv = await readFileJson(dsseFile);
      const out = await verifyWithKeyring(dsseEnv, 'application/vnd.mvg.airgapped_verifier_kit+json', ROLE_KIT, keyringInfo.keyring);

      setText(els.dsseKeyId, out.kid);
      setText(els.keyStatus, `${out.key.status || '—'} / ${out.life.note}`);

      const payload = out.payloadObj;
      setText(els.payloadSchema, payload.schema_id || '—');
      const enc = new TextEncoder();
      const payloadSha = await sha256Hex(enc.encode(canonicalJson(payload)));
      setText(els.payloadSha, payloadSha);
      setText(els.payloadBytes, payload.kit && payload.kit.bytes ? String(payload.kit.bytes) : '—');

      // Compare
      const expSha = payload.kit && payload.kit.sha256 ? String(payload.kit.sha256) : '';
      const expBytes = payload.kit && payload.kit.bytes ? Number(payload.kit.bytes) : NaN;

      if (expSha !== kitSha || expBytes !== kitBytes.length) {
        setVerdict('FAIL', 'Kit ZIP does not match DSSE payload (sha/bytes mismatch).');
        if (els.btnPrintSummary) els.btnPrintSummary.disabled = false;
        if (els.btnDownloadReceipt) els.btnDownloadReceipt.disabled = false;
        if (els.btnCopyNote) els.btnCopyNote.disabled = false;
        return;
      }

      // If key policy produced HOLD, overall HOLD (still cryptographically valid)
      if (out.life.verdict === 'HOLD') {
        setVerdict('HOLD', 'Signature is valid, but key policy indicates HOLD: ' + out.life.note);
      } else {
        setVerdict('PASS', 'Kit integrity verified (DSSE + sha/bytes).');
      }

      if (els.btnPrintSummary) els.btnPrintSummary.disabled = false;
      if (els.btnDownloadReceipt) els.btnDownloadReceipt.disabled = false;
      if (els.btnCopyNote) els.btnCopyNote.disabled = false;

    } catch (e) {
      setVerdict('HOLD', 'Verification error: ' + (e && e.message ? e.message : String(e)));
      if (els.btnPrintSummary) els.btnPrintSummary.disabled = false;
      if (els.btnDownloadReceipt) els.btnDownloadReceipt.disabled = false;
      if (els.btnCopyNote) els.btnCopyNote.disabled = false;
      return;
    }

    // Supply-chain mode
    if (!els.chkRequireLog || !els.chkRequireLog.checked) return;

    try {
      const receiptFile = els.releaseReceiptDsse.files && els.releaseReceiptDsse.files[0] ? els.releaseReceiptDsse.files[0] : null;
      const logFile = els.commitLogJson.files && els.commitLogJson.files[0] ? els.commitLogJson.files[0] : null;
      const headFile = els.commitLogHeadDsse.files && els.commitLogHeadDsse.files[0] ? els.commitLogHeadDsse.files[0] : null;

      if (!receiptFile || !logFile || !headFile) {
        setVerdict('HOLD', 'Supply-chain mode requires receipt DSSE + log JSON + signed head DSSE.');
        return;
      }

      const receiptEnv = await readFileJson(receiptFile);
      const receiptOut = await verifyWithKeyring(receiptEnv, 'application/vnd.mvg.airgapped_verifier_kit.private_receipt+json', ROLE_COMMIT, keyringInfo.keyring);

      const receipt = receiptOut.payloadObj;
      const nonce = receipt.nonce_b64u;
      const expectedCommit = await computeCommitment(kitSha, nonce);
      setText(els.computedCommitment, expectedCommit);
      setText(els.receiptCommitment, receipt.public_commitment_sha256 || '—');

      if (receipt.kit_sha256 !== kitSha || Number(receipt.kit_bytes) !== kitBytes.length) {
        setVerdict('FAIL', 'Receipt does not match the selected kit ZIP (sha/bytes).');
        return;
      }
      if (receipt.public_commitment_sha256 !== expectedCommit) {
        setVerdict('FAIL', 'Receipt commitment does not match computed commitment.');
        return;
      }

      // Verify log head DSSE
      const logObj = await readFileJson(logFile);
      const headEnv = await readFileJson(headFile);
      const headOut = await verifyWithKeyring(headEnv, 'application/vnd.mvg.airgapped_verifier_kit.commitment_log_head+json', ROLE_COMMIT, keyringInfo.keyring);

      const headPayload = headOut.payloadObj;
      if (!headPayload || headPayload.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log_head@1') throw new Error('LOG_HEAD_SCHEMA');

      const enc = new TextEncoder();
      const logSha = await sha256Hex(enc.encode(canonicalJson(logObj)));
      if (headPayload.log_sha256 !== logSha) throw new Error('LOG_SHA_MISMATCH');

      // Verify log integrity + inclusion
      const logVerd = await verifyCommitmentLog(logObj, expectedCommit);
      if (logVerd.logSha !== logSha) throw new Error('LOG_SHA_INTERNAL');
      if (headPayload.head_entry_sha256 !== logVerd.headEntry) throw new Error('LOG_HEAD_ENTRY_MISMATCH');

      setText(els.commitLogVerdict, 'PASS (' + logVerd.entryCount + ' entries)');
      setText(els.commitLogHeadEntry, logVerd.headEntry);

      // Optional headchain verification
      if (els.chkRequireHeadchain && els.chkRequireHeadchain.checked) {
        const hcLogFile = els.headChainJson.files && els.headChainJson.files[0] ? els.headChainJson.files[0] : null;
        const hcHeadFile = els.headChainHeadDsse.files && els.headChainHeadDsse.files[0] ? els.headChainHeadDsse.files[0] : null;
        if (!hcLogFile || !hcHeadFile) {
          setVerdict('HOLD', 'Headchain required: provide headchain log JSON + signed headchain head DSSE.');
          return;
        }
        const hcLog = await readFileJson(hcLogFile);
        const hcHeadEnv = await readFileJson(hcHeadFile);
        const hcHeadOut = await verifyWithKeyring(hcHeadEnv, 'application/vnd.mvg.airgapped_verifier_kit.commitment_log_headchain_head+json', ROLE_HEADCHAIN, keyringInfo.keyring);
        const hcHead = hcHeadOut.payloadObj;
        if (!hcHead || hcHead.schema_id !== 'mvg.airgapped_verifier_kit.commitment_log_headchain_head@1') throw new Error('HEADCHAIN_HEAD_SCHEMA');

        const hcVerd = await verifyHeadchain(hcLog, hcHead, headEnv);
        setText(els.headChainVerdict, 'PASS (' + hcVerd.entryCount + ' entries)');
      } else {
        setText(els.headChainVerdict, 'SKIP');
      }

      // Final verdict: if base was PASS, remain PASS; if base was HOLD due to key policy, keep HOLD.
      const base = els.verdictTag.textContent;
      if (base === 'PASS') setVerdict('PASS', 'Kit verified + supply-chain evidence verified.');
      else if (base === 'HOLD') setVerdict('HOLD', 'Kit verified, but key policy HOLD; supply-chain evidence verified.');

    } catch (e) {
      setVerdict('HOLD', 'Supply-chain verification error: ' + (e && e.message ? e.message : String(e)));
      setText(els.commitLogVerdict, 'HOLD');
      return;
    }
  }

  els.btnVerify.addEventListener('click', () => { run(); });
  els.btnReset.addEventListener('click', () => {
    if (els.kitZip) els.kitZip.value = '';
    if (els.kitDsse) els.kitDsse.value = '';
    if (els.keyringDsse) els.keyringDsse.value = '';
    if (els.releaseReceiptDsse) els.releaseReceiptDsse.value = '';
    if (els.commitLogJson) els.commitLogJson.value = '';
    if (els.commitLogHeadDsse) els.commitLogHeadDsse.value = '';
    if (els.headChainJson) els.headChainJson.value = '';
    if (els.headChainHeadDsse) els.headChainHeadDsse.value = '';
    if (els.chkRequireLog) els.chkRequireLog.checked = false;
    if (els.chkRequireHeadchain) els.chkRequireHeadchain.checked = false;
    resetOutputs();
  });

  els.btnDownloadReceipt && els.btnDownloadReceipt.addEventListener('click', () => {
    const s = collectSummary();
    lastVerification = s;
    const short = String(s.approval && s.approval.short_code ? s.approval.short_code : '').trim();
    const sha = String(s.kit && s.kit.sha256_hex ? s.kit.sha256_hex : '').trim();
    const fallback = sha ? sha.slice(0,16) : 'Approval';
    const names = suggestedAttachmentNames(short, fallback);
    downloadJson(names.kit_supply_chain_receipt_json, s);
  });



  els.btnCopyNote && els.btnCopyNote.addEventListener('click', async () => {
    if (els.btnCopyNote.disabled) return;
    const s = collectSummary();
    lastVerification = s;

    const verdict = String(s.verdict || '—').toUpperCase();
    const approvalId = (s.approval && s.approval.id) ? s.approval.id : '—';
    const short = (s.approval && s.approval.short_code) ? String(s.approval.short_code).trim() : '';
    const kitSha = (s.kit && s.kit.sha256_hex) ? String(s.kit.sha256_hex) : '—';
    const kitBytes = (s.kit && s.kit.bytes !== null && s.kit.bytes !== undefined) ? String(s.kit.bytes) : '—';
    const names = s.suggested_attachments ? s.suggested_attachments : suggestedAttachmentNames(short, kitSha !== '—' ? kitSha.slice(0,16) : 'Approval');

    const lines = [
      'MVG air‑gapped kit verification: ' + verdict,
      'Approval ID: ' + approvalId + (short ? ' (' + short + ')' : ''),
      'Kit sha256: ' + kitSha,
      'Kit bytes: ' + kitBytes,
      'Suggested attachments:',
      '- ' + names.audit_summary_pdf,
      '- ' + names.kit_supply_chain_receipt_json,
      '- ' + names.countersigned_approval_receipt_json + ' (optional)',
      'Boundary: non‑binding unless signed; no patent license by publication; not legal advice; not a compliance certification.'
    ];

    const ok = await copyToClipboard(lines.join('\n'));
    if (els.copyStatus) {
      els.copyStatus.textContent = ok ? 'Copied ticket note.' : 'Copy not available — select manually.';
      setTimeout(() => { if (els.copyStatus) els.copyStatus.textContent = ''; }, 6000);
    }
  });

  els.btnPrintSummary && els.btnPrintSummary.addEventListener('click', () => {
    if (els.btnPrintSummary.disabled) return;
    renderAndPrintSummary();
  });


  resetOutputs();
})();
