/* AuditSurface.jsx — Audit & Verification report surface (window.AuditSurface).

   READ-ONLY consumer of the live audit engine. Renders the migration cross-check
   report on the parent RAG model. NO write/remediation path ships here (remediation
   = OL-037, built last) — there are NO "Fix" buttons; the only POST enqueues a
   READ-ONLY audit pass on the execution spine (master §3 #15 — POST to enqueue +
   render op status, never fake completion).

   Honest by construction:
   - Status (report / section / finding) is the API rollup, rendered VERBATIM. The
     app NEVER recomputes RAG (invariant 9 — no parallel findings model).
   - Gray = honest-incomplete, rendered as a DISTINCT visual state (sunken card),
     never green / never red / never empty; carries its recommended_fix as the CTA.
   - Findings score renders score_label VERBATIM ("... NOT a compliance grade ...");
     never a letter grade, never a compliance %.
   - Attestation (attested_text + version_stamp + coverage_hash) shown verbatim;
     never any "compliant"/"certified" language (the server guards that).

   Role access (declared in BOTH places — mirrors the ScanHistory pattern):
     Shell.jsx   NAV_<ROLE>     — an 'audit' row in the Tools group of the surfaces
                 that own audit reports: NAV_CUSTOMER (→ customer + internal_partner)
                 + NAV_ADMIN + NAV_SA. DECLARED DENIED for marketing_admin/_jr,
                 dev_admin/_jr, support_agent/_admin (WPSB-internal personas — a
                 customer's audit reports are tier-2 owner-scoped; never cross-account
                 except SA via View-As).
     App.jsx     ALLOWED.<role> — 'audit' in customer, admin, super_admin (and
                 internal_partner via the derived = customer set).
   Nav visibility never widens API scope — the endpoints self-gate: read =
   requireJWT('wpsb-app') + requirePlan('growth') [Pro+]; run = + requirePlan('agency')
   + requireAgencyRole. A customer below the plan sees an honest upgrade prompt from
   the API 403, never a blocked-blank.

   Export: a print-to-PDF (clean-window) interim, mirroring ADAReport. The DURABLE
   asset-offload PDF (#25 — we generate, customer WP hosts via /wpsitebeam/v1/asset/
   import) is the FLAGGED FOLLOW-ON: no api report-export/offload endpoint exists yet
   (recon'd against live HEAD).

   Analytics: audit.* + migration.crosscheck.* are emitted SERVER-SIDE by the engine
   (POST + completion). This app-only surface does not double-emit (mirrors the
   ScanHistory app-PR analytics posture). */
(function () {
  'use strict';
  var A = window.WPSBAudit || {};
  var Icon = window.Icon || function () { return null; };
  var React = window.React;
  var useState = React.useState, useEffect = React.useEffect, useCallback = React.useCallback, useRef = React.useRef;

  /* ── Small shared bits ─────────────────────────────────────────────────── */
  function RagChip(props) {
    var m = A.ragMeta(props.status);
    var small = props.small;
    return (
      <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: small ? '1px 8px' : '3px 11px', borderRadius: 999,
        background: m.bg, color: m.color, border: '1px solid ' + m.border,
        fontFamily: 'var(--font-mono)', fontSize: small ? '.66rem' : '.72rem',
        fontWeight: 700, whiteSpace: 'nowrap', letterSpacing: '.04em' }}>
        <span aria-hidden="true" style={{ width: 7, height: 7, borderRadius: 999, background: m.color, flex: '0 0 auto' }} />
        {props.label || m.label}
      </span>
    );
  }

  /* THE ONE shared empty state (wiz-canon §4.4, design/ActivationPrimitives.jsx).
     Was a local fork until the L11 consolidation. ActivationPrimitives.jsx loads
     at index.html:645, ahead of this file (:698), so the binding is resolved.
     This fork was the only one with a `children` slot — the canonical was
     EXTENDED to carry it rather than the callers re-shaped to lose it. */
  var EmptyState = window.WPSB.Activation.EmptyState;

  /* Boxed v2 notice card (Edwiser/Moodle notice pattern — contained, tinted
     surface, icon + title + body). Used for the standalone empty/honest states so
     they read as a deliberate notice, not floating muted prose. Theme-flips. */
  function NoticeCard(props) {
    return (
      <div className="card" style={{ padding: '30px 22px', textAlign: 'center', background: 'var(--surface-2)', borderRadius: 'var(--r-lg, 14px)' }}>
        <div aria-hidden="true" style={{ marginBottom: 10, color: 'var(--dim)' }}><Icon name={props.icon || 'file-text'} size={26} /></div>
        <div style={{ color: 'var(--text)', fontWeight: 700, marginBottom: 6, fontFamily: 'var(--font-brand)' }}>{props.title}</div>
        <div style={{ fontSize: '.85rem', color: 'var(--muted)', maxWidth: 460, margin: '0 auto', lineHeight: 1.5 }}>{props.body}</div>
        {props.children}
      </div>
    );
  }

  function fetchJson(path, opts) {
    var token = A.authToken();
    var headers = Object.assign({ Authorization: 'Bearer ' + token }, (opts && opts.headers) || {});
    return fetch(A.apiBase() + path, Object.assign({}, opts, { headers: headers }));
  }

  /* Token-styled input (no global .input class exists). v2: sunken surface token
     (theme-flips), v2 radius. */
  var inputStyle = {
    flex: '1 1 240px', minWidth: 0, padding: '9px 11px', borderRadius: 'var(--r-sm, 8px)',
    border: '1px solid var(--border)', background: 'var(--surface-2)',
    color: 'var(--text)', fontSize: '.85rem',
  };

  /* ── Finding row (non-gray) ────────────────────────────────────────────── */
  function FindingRow(props) {
    var f = props.f;
    return (
      <div style={{ display: 'flex', gap: 12, padding: '11px 14px', borderTop: '1px solid var(--border)', alignItems: 'flex-start' }}>
        <div style={{ flex: '0 0 auto', marginTop: 1 }}><RagChip status={f.status} small={true} /></div>
        <div style={{ flex: '1 1 auto', minWidth: 0 }}>
          <div style={{ color: 'var(--text)', fontSize: '.85rem', lineHeight: 1.45 }}>{f.detail || f.check_id}</div>
          {f.fix_action ? (
            <div style={{ marginTop: 5, fontSize: '.78rem', color: 'var(--muted)' }}>
              <strong style={{ color: 'var(--text)' }}>Recommended: </strong>{f.fix_action}
            </div>
          ) : null}
          <div style={{ marginTop: 5, display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: '.68rem', color: 'var(--dim)' }}>
            <span className="tag">{f.check_id}</span>
            {f.tags && f.tags.wcag_criterion ? <span className="tag">WCAG {f.tags.wcag_criterion}</span> : null}
            <span className="tag" title="Provenance tier — 1 public/anonymous, 2 owner-authenticated">TIER {f.provenance_tier || 1}</span>
          </div>
        </div>
      </div>
    );
  }

  /* ── Gray card — honest-incomplete, distinct sunken state + recommended_fix CTA ─ */
  function GrayCard(props) {
    var f = props.f;
    var rec = f.recommended_fix || f.detail || 'Not measured.';
    /* A blocked/honest-incomplete finding whose unblock path is the plugin gets a
       real CTA; an out-of-phase ("later phase / manual path") card stays a muted
       note (no fake action). */
    var isConnectPrompt = /connect the .*plugin|tier-2 .*read|plugin-connected/i.test(rec) || /connect the .*plugin/i.test(f.recommended_fix || '');
    return (
      <div style={{ display: 'flex', gap: 12, padding: '12px 14px', borderTop: '1px solid var(--border)',
        background: 'var(--surface-2)', alignItems: 'flex-start' }}>
        <div style={{ flex: '0 0 auto', marginTop: 1 }}><RagChip status="gray" small={true} /></div>
        <div style={{ flex: '1 1 auto', minWidth: 0 }}>
          <div style={{ color: 'var(--muted)', fontSize: '.85rem', lineHeight: 1.45 }}>{f.detail}</div>
          <div style={{ marginTop: 7, display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
            {isConnectPrompt ? (
              <button className="btn btn-ghost btn-sm" onClick={function () { A.switchTab('sites'); }}
                title={rec}>
                <Icon name="link" size={12} /> Connect plugin for a tier-2 read
              </button>
            ) : (
              <span style={{ fontSize: '.78rem', color: 'var(--dim)', fontStyle: 'italic' }}>{rec}</span>
            )}
          </div>
        </div>
      </div>
    );
  }

  /* ── Section accordion ─────────────────────────────────────────────────── */
  function SectionAccordion(props) {
    var sec = props.section;
    var startOpen = props.startOpen;
    var openState = useState(!!startOpen);
    var open = openState[0], setOpen = openState[1];
    var rollup = sec.rollup || { status: 'gray', measured: 0, gray: 0, total: (sec.findings || []).length };
    var findings = sec.findings || [];
    return (
      <div className="card" style={{ marginBottom: 10 }}>
        <button onClick={function () { setOpen(!open); }}
          aria-expanded={open}
          style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', textAlign: 'left',
            background: 'transparent', border: 'none', cursor: 'pointer', padding: '14px 16px', color: 'var(--text)' }}>
          <Icon name={open ? 'chevron-down' : 'chevron-right'} size={16} />
          <span style={{ fontWeight: 700, fontSize: '.92rem', flex: '1 1 auto' }}>{A.sectionLabel(sec.section)}</span>
          <span style={{ fontSize: '.7rem', color: 'var(--dim)', whiteSpace: 'nowrap' }}>
            {rollup.measured} measured{rollup.gray ? ' · ' + rollup.gray + ' not measured' : ''}
          </span>
          <RagChip status={rollup.status} small={true} />
        </button>
        {open ? (
          <div>
            {findings.length === 0
              ? <div style={{ padding: '12px 16px', color: 'var(--dim)', fontSize: '.82rem', borderTop: '1px solid var(--border)' }}>No findings in this section.</div>
              : findings.map(function (f, i) {
                  return f.status === 'gray'
                    ? <GrayCard key={f.id || i} f={f} />
                    : <FindingRow key={f.id || i} f={f} />;
                })}
          </div>
        ) : null}
      </div>
    );
  }

  /* ── Findings score block — score_label VERBATIM, never a grade ─────────── */
  function ScoreBlock(props) {
    var report = props.report;
    var scoreLabel = props.scoreLabel;
    var hasScore = report.score != null;
    var m = A.ragMeta(report.status);
    return (
      <div className="card" style={{ padding: 16, display: 'flex', gap: 18, alignItems: 'center', flexWrap: 'wrap' }}>
        <div style={{ flex: '0 0 auto', width: 92, height: 92, borderRadius: 999, border: '4px solid ' + m.color,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: m.bg }}>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: '1.6rem', fontWeight: 800, color: m.color, lineHeight: 1 }}>
            {hasScore ? report.score : '—'}
          </div>
          <div style={{ fontFamily: 'var(--font-mono)', fontSize: '.6rem', color: 'var(--dim)', marginTop: 2, letterSpacing: '.04em' }}>{hasScore ? '/ 100' : 'no score'}</div>
        </div>
        <div style={{ flex: '1 1 240px', minWidth: 0 }}>
          <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 6 }}>
            <span style={{ fontWeight: 700, color: 'var(--text)' }}>Findings score</span>
            <RagChip status={report.status} />
          </div>
          {/* score_label rendered VERBATIM — the explicit not-a-compliance-grade copy. */}
          <div style={{ fontSize: '.78rem', color: 'var(--muted)', lineHeight: 1.5 }}>{scoreLabel}</div>
        </div>
      </div>
    );
  }

  /* ── Attestation block — attested_text + version_stamp + coverage_hash verbatim ─ */
  function AttestationBlock(props) {
    var att = props.attestation;
    var disclaimer = props.disclaimer;
    if (!att && !disclaimer) return null;
    return (
      <div className="card" style={{ padding: 16, marginTop: 14, background: 'var(--surface-2)' }}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
          <Icon name="shield" size={15} />
          <span style={{ fontWeight: 700, color: 'var(--text)', fontSize: '.9rem' }}>Attestation</span>
        </div>
        {att ? (
          <div>
            <div style={{ color: 'var(--text)', fontSize: '.85rem', lineHeight: 1.55 }}>{att.attested_text}</div>
            <div style={{ marginTop: 8, display: 'flex', gap: 8, flexWrap: 'wrap', fontSize: '.68rem', color: 'var(--dim)' }}>
              {att.version_stamp ? <span className="tag">{att.version_stamp}</span> : null}
              {att.coverage_hash ? <span className="tag">coverage {att.coverage_hash}</span> : null}
              {att.created_at ? <span className="tag">{A.fmtDateTime(att.created_at)}</span> : null}
            </div>
          </div>
        ) : null}
        {disclaimer ? <div style={{ marginTop: 10, fontSize: '.74rem', color: 'var(--muted)', fontStyle: 'italic' }}>{disclaimer}</div> : null}
      </div>
    );
  }

  /* ── Print-to-PDF (clean window) — interim export; durable offload is the follow-on ─ */
  function resolvePalette() {
    var cs = getComputedStyle(document.documentElement);
    function v(n, fb) { var x = cs.getPropertyValue(n); return (x && x.trim()) || fb; }
    return {
      green: v('--green', '#0a7a4a'), warn: v('--warn', '#946300'), red: v('--red', '#c53030'),
      dim: v('--dim', '#5878a0'), text: v('--text', '#1a2433'), border: v('--border', '#d4dde8'),
      muted: v('--muted', '#5a6b7d'),
    };
  }
  function ragHex(pal, s) { s = String(s || '').toLowerCase(); return s === 'green' ? pal.green : s === 'yellow' ? pal.warn : s === 'red' ? pal.red : pal.dim; }
  function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

  function exportReport(data) {
    var report = data.report, sections = data.sections || [], att = data.attestation;
    var pal = resolvePalette();
    var rows = sections.slice().sort(function (a, b) { return A.sectionOrder(a.section) - A.sectionOrder(b.section); }).map(function (sec) {
      var r = sec.rollup || {};
      var findings = (sec.findings || []).map(function (f) {
        return '<div style="border-top:1px solid ' + pal.border + ';padding:7px 0;">'
          + '<span style="color:' + ragHex(pal, f.status) + ';font-weight:700;font-size:11px;">' + esc(A.ragMeta(f.status).label).toUpperCase() + '</span> '
          + '<span style="font-size:12px;">' + esc(f.detail || f.check_id) + '</span>'
          + (f.fix_action ? '<div style="font-size:11px;color:' + pal.muted + ';margin-top:2px;">Recommended: ' + esc(f.fix_action) + '</div>' : '')
          + '</div>';
      }).join('');
      return '<div style="margin:14px 0;border:1px solid ' + pal.border + ';border-radius:6px;padding:12px;">'
        + '<div style="font-weight:700;font-size:14px;color:' + ragHex(pal, r.status) + ';">' + esc(A.sectionLabel(sec.section)) + ' — ' + esc(A.ragMeta(r.status).label) + '</div>'
        + findings + '</div>';
    }).join('');
    var html = '<!doctype html><html><head><meta charset="utf-8"><title>Audit Report</title></head>'
      + '<body style="font-family:system-ui,Arial,sans-serif;color:' + pal.text + ';max-width:760px;margin:24px auto;padding:0 16px;">'
      + '<h1 style="font-size:20px;">Audit &amp; Verification — ' + esc(A.modeLabel(report.mode)) + '</h1>'
      + '<div style="font-size:13px;color:' + pal.muted + ';">' + esc(A.prettyHost(report.source_url)) + ' &rarr; ' + esc(A.prettyHost(report.target_url)) + '</div>'
      + '<div style="font-size:13px;margin:8px 0;">Findings score: <strong>' + (report.score == null ? '—' : report.score) + ' / 100</strong> · Status: <strong style="color:' + ragHex(pal, report.status) + ';">' + esc(A.ragMeta(report.status).label) + '</strong></div>'
      + '<div style="font-size:11px;color:' + pal.muted + ';font-style:italic;">' + esc(data.scoreLabel) + '</div>'
      + rows
      + (att ? '<div style="margin-top:16px;border:1px solid ' + pal.border + ';border-radius:6px;padding:12px;font-size:12px;"><strong>Attestation</strong><div style="margin-top:6px;">' + esc(att.attested_text) + '</div><div style="margin-top:6px;color:' + pal.dim + ';font-size:11px;">' + esc(att.version_stamp) + ' · coverage ' + esc(att.coverage_hash) + '</div></div>' : '')
      + (data.disclaimer ? '<div style="margin-top:10px;font-size:11px;color:' + pal.muted + ';font-style:italic;">' + esc(data.disclaimer) + '</div>' : '')
      + '<script>window.onload=function(){window.print();}<\/script></body></html>';
    var w = window.open('', '_blank');
    if (!w) return;
    w.document.open(); w.document.write(html); w.document.close();
  }

  /* ── Report detail ─────────────────────────────────────────────────────── */
  function ReportDetail(props) {
    var data = props.data;        // { report, sections, attestation, score_label, disclaimer }
    var report = data.report;
    var sections = (data.sections || []).slice().sort(function (a, b) { return A.sectionOrder(a.section) - A.sectionOrder(b.section); });
    return (
      <div>
        <div style={{ display: 'flex', gap: 12, alignItems: 'center', marginBottom: 14, flexWrap: 'wrap' }}>
          <button className="btn btn-ghost btn-sm" onClick={props.onBack}><Icon name="chevron-left" size={12} /> All reports</button>
          <div style={{ flex: '1 1 auto', minWidth: 0 }}>
            <div style={{ fontWeight: 700, color: 'var(--text)' }}>
              {A.prettyHost(report.source_url)} <span style={{ color: 'var(--dim)' }}>&rarr;</span> {A.prettyHost(report.target_url)}
            </div>
            <div style={{ fontSize: '.72rem', color: 'var(--dim)' }}>
              {A.modeLabel(report.mode)}{report.sub_mode ? ' · ' + report.sub_mode : ''}
              {report.completed_at ? ' · ' + A.fmtDateTime(report.completed_at) : ''}
            </div>
          </div>
          <button className="btn btn-ghost btn-sm" onClick={function () { exportReport({ report: report, sections: data.sections, attestation: data.attestation, scoreLabel: data.score_label, disclaimer: data.disclaimer }); }}>
            <Icon name="printer" size={12} /> Save as PDF
          </button>
        </div>

        <ScoreBlock report={report} scoreLabel={data.score_label} />

        <div style={{ marginTop: 14 }}>
          {sections.map(function (sec, i) {
            var r = sec.rollup || {};
            return <SectionAccordion key={sec.section || i} section={sec} startOpen={r.status === 'red' || r.status === 'yellow'} />;
          })}
        </div>

        <AttestationBlock attestation={data.attestation} disclaimer={data.disclaimer} />

        <div style={{ marginTop: 10, fontSize: '.72rem', color: 'var(--dim)' }}>
          Durable PDF export to your site’s media library is coming soon — “Save as PDF” uses your browser’s print dialog for now.
        </div>
      </div>
    );
  }

  /* ── New migration cross-check (read-only enqueue + honest spine polling) ── */
  function NewRunForm(props) {
    var srcState = useState(''), tgtState = useState('');
    var src = srcState[0], setSrc = srcState[1], tgt = tgtState[0], setTgt = tgtState[1];
    var busyState = useState(false), errState = useState(null);
    var busy = busyState[0], setBusy = busyState[1], err = errState[0], setErr = errState[1];

    function run() {
      setErr(null);
      if (!src.trim() || !tgt.trim()) { setErr('Enter both the old (source) and new (target) site URLs.'); return; }
      setBusy(true);
      fetchJson('/audit/migration-crosscheck', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ source_url: src.trim(), target_url: tgt.trim(), sub_mode: 'plan' }),
      }).then(function (r) {
        return r.json().then(function (j) { return { ok: r.ok, status: r.status, j: j }; });
      }).then(function (res) {
        setBusy(false);
        if (!res.ok || !res.j || !res.j.ok) {
          var j = res.j || {};
          if (res.status === 403 && j.code === 'PLAN_UPGRADE_REQUIRED') { setErr('Migration cross-check is an Agency-plan feature. Upgrade to run it.'); return; }
          if (res.status === 403) { setErr(j.error || 'You don’t have permission to run a cross-check on this account.'); return; }
          if (res.status === 429) { setErr(j.error || 'An audit is already running for this account — try again when it finishes.'); return; }
          setErr(j.error || 'Could not start the cross-check. Try again shortly.');
          return;
        }
        props.onEnqueued(res.j.report_id, res.j.provenance);
      }).catch(function () { setBusy(false); setErr('Could not reach the audit service. Try again shortly.'); });
    }

    return (
      <div className="card" style={{ padding: 16, marginBottom: 16 }}>
        <div style={{ fontWeight: 700, color: 'var(--text)', marginBottom: 4 }}>New migration cross-check</div>
        <div style={{ fontSize: '.78rem', color: 'var(--dim)', marginBottom: 12 }}>
          Read-only. Compares an old site against its new build (Plan sub-mode) — URL parity, on-page SEO, indexability, links &amp; assets. Nothing is written to either site.
        </div>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
          <input type="url" placeholder="Old site URL (e.g. https://oldsite.com)" value={src}
            onChange={function (e) { setSrc(e.target.value); }} style={inputStyle} />
          <input type="url" placeholder="New site URL (e.g. https://newsite.com)" value={tgt}
            onChange={function (e) { setTgt(e.target.value); }} style={inputStyle} />
          <button className="btn btn-primary" onClick={run} disabled={busy}>
            {busy ? 'Starting…' : 'Run cross-check'}
          </button>
        </div>
        {err ? <div style={{ marginTop: 10, fontSize: '.8rem', color: 'var(--red)' }}>{err}</div> : null}
      </div>
    );
  }

  /* ── Reports list ──────────────────────────────────────────────────────── */
  function ReportList(props) {
    var reports = props.reports || [];
    if (reports.length === 0) {
      return <NoticeCard icon="file-text" title="No audit reports yet" body="Run a migration cross-check above to produce your first report. Reports are private to your account." />;
    }
    return (
      <div className="card" style={{ overflow: 'hidden' }}>
        {reports.map(function (rep, i) {
          var running = !rep.completed_at;
          return (
            <button key={rep.id || i} onClick={function () { props.onOpen(rep.id); }}
              style={{ display: 'flex', gap: 12, alignItems: 'center', width: '100%', textAlign: 'left',
                background: i % 2 ? 'var(--surface-2)' : 'transparent', border: 'none',
                borderTop: i ? '1px solid var(--border)' : 'none', cursor: 'pointer', padding: '12px 14px', color: 'var(--text)' }}>
              <RagChip status={running ? 'gray' : rep.status} small={true} label={running ? 'Processing…' : undefined} />
              <div style={{ flex: '1 1 auto', minWidth: 0 }}>
                <div style={{ fontSize: '.85rem', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {A.prettyHost(rep.source_url)} <span style={{ color: 'var(--dim)' }}>&rarr;</span> {A.prettyHost(rep.target_url)}
                </div>
                <div style={{ fontSize: '.7rem', color: 'var(--dim)' }}>
                  {A.modeLabel(rep.mode)}{rep.created_at ? ' · ' + A.fmtDateTime(rep.created_at) : ''}
                </div>
              </div>
              <div style={{ flex: '0 0 auto', textAlign: 'right' }}>
                <div style={{ fontSize: '.95rem', fontWeight: 800, color: 'var(--text)' }}>{rep.score == null ? '—' : rep.score}</div>
                <div style={{ fontSize: '.6rem', color: 'var(--dim)' }}>score</div>
              </div>
              <Icon name="chevron-right" size={14} />
            </button>
          );
        })}
      </div>
    );
  }

  /* ── Top-level surface ─────────────────────────────────────────────────── */
  function AuditSurface() {
    var phaseState = useState('loading');   // loading | signedout | error | list | detail
    var phase = phaseState[0], setPhase = phaseState[1];
    var reportsState = useState([]); var reports = reportsState[0], setReports = reportsState[1];
    var detailState = useState(null); var detail = detailState[0], setDetail = detailState[1];
    var pollState = useState(null); var polling = pollState[0], setPolling = pollState[1];  // report_id being polled
    var timerRef = useRef(null);

    var loadList = useCallback(function (attempt) {
      attempt = attempt || 0;
      var token = A.authToken();
      if (!token) {
        // Token not hydrated yet on first mount — retry briefly before declaring
        // signed-out (window.currentToken can lag the first render). Only a
        // genuinely-absent token after retries shows the sign-in state.
        if (attempt < 6) { setPhase('loading'); if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(function () { loadList(attempt + 1); }, 500); return; }
        setPhase('signedout'); return;
      }
      fetchJson('/audit/reports').then(function (r) {
        if (r.status === 401) { setPhase('signedout'); return null; }
        if (!r.ok) { setPhase('error'); return null; }
        return r.json();
      }).then(function (j) {
        if (!j) return;
        setReports((j && j.reports) || []);
        setPhase('list');
      }).catch(function () { setPhase('error'); });
    }, []);

    useEffect(function () { loadList(); return function () { if (timerRef.current) clearTimeout(timerRef.current); }; }, [loadList]);

    function openReport(id) {
      setPhase('detail'); setDetail(null);
      fetchJson('/audit/reports/' + encodeURIComponent(id)).then(function (r) {
        if (!r.ok) { setDetail({ _error: true }); return null; }
        return r.json();
      }).then(function (j) {
        if (!j) return;
        setDetail(j);
        if (j.report && !j.report.completed_at) startPoll(id);   // still processing
      }).catch(function () { setDetail({ _error: true }); });
    }

    function startPoll(id) {
      setPolling(id);
      var tick = function () {
        fetchJson('/audit/reports/' + encodeURIComponent(id)).then(function (r) { return r.ok ? r.json() : null; }).then(function (j) {
          if (!j) { timerRef.current = setTimeout(tick, 5000); return; }
          setDetail(j);
          if (j.report && j.report.completed_at) { setPolling(null); }
          else { timerRef.current = setTimeout(tick, 5000); }
        }).catch(function () { timerRef.current = setTimeout(tick, 6000); });
      };
      timerRef.current = setTimeout(tick, 4000);
    }

    function onEnqueued(reportId) {
      loadList();                 // refresh the list in the background
      openReport(reportId);       // jump straight to the (processing) detail
    }

    function backToList() {
      if (timerRef.current) clearTimeout(timerRef.current);
      setPolling(null); setDetail(null); setPhase('list'); loadList();
    }

    var header = (
      <div style={{ marginBottom: 16 }}>
        <h2 style={{ margin: 0, fontSize: '1.15rem', color: 'var(--text)' }}>Audit &amp; Verification</h2>
        <div style={{ fontSize: '.8rem', color: 'var(--dim)', marginTop: 2 }}>
          Read-only migration cross-check reports. Audited against WCAG 2.1 AA — findings, not a compliance certification.
        </div>
      </div>
    );

    if (phase === 'loading') {
      return <div>{header}<div className="card" style={{ padding: 18 }}><div style={{ height: 38, borderRadius: 'var(--r-sm, 8px)', background: 'var(--surface-2)', marginBottom: 10 }} /><div style={{ height: 38, borderRadius: 'var(--r-sm, 8px)', background: 'var(--surface-2)', opacity: .7 }} /><div style={{ marginTop: 10, fontSize: '.78rem', color: 'var(--dim)' }}>Loading audit reports…</div></div></div>;
    }
    if (phase === 'signedout') {
      return <div>{header}<EmptyState icon={<Icon name="lock" size={30} />} title="Sign in to view audit reports" body="Audit reports are private to your account." /></div>;
    }
    if (phase === 'error') {
      return <div>{header}<EmptyState icon={<Icon name="warn" size={30} />} title="Couldn’t load audit reports" body="The audit service couldn’t be reached just now. Try again shortly."><button className="btn btn-ghost btn-sm" onClick={loadList}>Retry</button></EmptyState></div>;
    }
    if (phase === 'detail') {
      if (!detail) {
        return <div>{header}<div className="card" style={{ padding: 22, textAlign: 'center', color: 'var(--dim)' }}><div style={{ fontSize: '.85rem' }}>Loading report…</div></div></div>;
      }
      if (detail._error) {
        return <div>{header}<EmptyState icon={<Icon name="warn" size={30} />} title="Couldn’t open this report" body="It couldn’t be read just now."><button className="btn btn-ghost btn-sm" onClick={backToList}>Back to reports</button></EmptyState></div>;
      }
      var processing = detail.report && !detail.report.completed_at;
      return (
        <div>
          {header}
          {processing ? (
            <div className="card" style={{ padding: 14, marginBottom: 12, display: 'flex', gap: 10, alignItems: 'center', background: 'var(--warn-dim)', border: '1px solid rgba(245,184,0,.3)' }}>
              <Icon name="clock" size={15} />
              <span style={{ fontSize: '.82rem', color: 'var(--text)' }}>Scanning both sites (read-only)… this refreshes automatically.</span>
            </div>
          ) : null}
          <ReportDetail data={detail} onBack={backToList} />
        </div>
      );
    }
    // list
    return (
      <div>
        {header}
        <NewRunForm onEnqueued={onEnqueued} />
        <ReportList reports={reports} onOpen={openReport} />
      </div>
    );
  }

  window.AuditSurface = AuditSurface;
})();
