/* ScanHistory.jsx — per-site Scan History viewer.

   Closes _TROUBLESHOOTING-MATRIX.md §2.36 (no surface to view historical scan
   findings; the "Full report" / "View Report" buttons were toast-only dead stubs)
   per _SCAN-HISTORY-SURFACE-SPEC.md. App-only PR — NO api/server.js changes.

   ── Read-of-record (recon §1.1, resolved against live HEAD) ──────────────────
   This surface reads the EXISTING saved-scan substrate via already-deployed,
   account-scoped endpoints:
     GET /scans/saved            → the account's saved scans. UNIQUE(account_id,
                                   site) dedup keeps the LATEST scan per site
                                   (+ 90-day expiry), so the list is one row per
                                   site, reverse-chronological.
     GET /scans/saved/by-site    → the single latest full-report blob for a site:
                                   { found, scan_data, scanned_at, is_stale,
                                     source, provenance_tier }.
   `saved_scans` IS the read-of-record. The `scan_history` table is NOT wired —
   it has 0 rows and no app-facing endpoint; wiring to it would be the exact
   AUDIT-UNWIRED defect class this surface closes, so it is deliberately avoided.

   ── Honest by construction (Radical Transparency) ────────────────────────────
   - Four tab states: loading / loaded / cached-stale (served-from-store + an
     as-of stamp) / honestly-empty ("No scans recorded for this site yet…",
     never a seeded/example row).
   - NULL-safe scores: a missing *_score renders an em dash with a "not yet
     scored" tooltip — NEVER 0 (a 0 score is a false claim; the score producers
     are unbuilt by design, _SCANNER-SCORE-PRODUCERS-SPEC.md). Lights up
     automatically once scores ship.
   - No fabricated trend: the thin time-series snapshot table is not exposed to
     the app and saved_scans retains one row per site, so a multi-point trend
     cannot be drawn honestly. The trend strip shows an honest "needs >=2 recorded
     scans" state rather than inventing a line from a single point.
   - Provenance enforced on READ: each row renders the provenance_tier the server
     returned (a column compare, not an inference). Default-deny — an absent tier
     is treated as tier-2 (private/owner). This file is a read consumer; it never
     widens tier (the server scopes every read by account_id).
   - Opening a report reads stored findings (no re-scan). A row whose stored
     report is missing shows the honest-empty detail — never a fake "report
     opened" toast (the same anti-pattern as the ContentSEO fake-success fix).

   ── Role access (declared in BOTH places; mirrors the Site Scanner surface) ───
     Shell.jsx   NAV_<ROLE>     — a "Scan History" row beneath every 'scanner' row.
     App.jsx     ALLOWED.<role> — 'scanhistory' added to every set that carries
                 'scanner' (customer, marketing_admin, dev_admin, admin,
                 support_agent, support_admin, super_admin); dev_jr / marketing_jr
                 / internal_partner inherit via the derived sets.
   Nav visibility never widens API scope — the endpoints self-gate per account
   (requireJWT('wpsb-app'), account_id-scoped). Staff see only their own account
   scope; cross-account history is SA-only via View-As impersonation (the read
   reflects whatever the write-path attributed — it does not re-attribute, per
   spec §6).

   ── Analytics (spec §7) ──────────────────────────────────────────────────────
   scan_history.viewed / scan_history.report_opened are reserved for server-side
   trackEvent wiring on the api stream — intentionally NOT emitted from this
   app-only PR.
*/
(function () {
  'use strict';

  const { useState, useEffect, useCallback } = React;
  const tx = (k, v, f) => (window.WPSB && window.WPSB.t) ? window.WPSB.t(k, v, f) : (f != null ? f : k);

  /* 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 (:686), so the binding is resolved.
     The fork hardcoded its 'clock' illustration and inlined a 'scanner' glyph in
     the CTA; both are now explicit props (icon node + primary.icon). */
  const EmptyState = window.WPSB.Activation.EmptyState;

  const FOCUS_KEY = 'wpsb-scan-history-site';   /* set by the dead-button wiring */
  const SCANNER_SESSION_KEY = 'wpsb-scan-current'; /* Scanner hydrates from this on mount */

  function apiBase() {
    /* Canonical base resolution (matches AuditData/BillingInfo/Estimator/etc):
       window.WPSBD.apiBase is set at boot (index.html); the hardcoded origin is
       the belt-and-suspenders fallback. The prior '' fallback sent the fetch to
       the app's OWN origin (app.wpsitebeam.io) → Vercel 404 when the globals
       weren't set — never resolve to empty. */
    return (window.WPSBD && window.WPSBD.apiBase)
      || (window.WPSB_CONFIG && window.WPSB_CONFIG.RAILWAY_URL)
      || window.RAILWAY_URL
      || 'https://api.wpsitebeam.io';
  }
  function authToken() {
    return (typeof window !== 'undefined' && window.WPSB && window.WPSB.getToken && window.WPSB.getToken()) || null;
  }
  function switchTab(tab) {
    if (window.WPSBD && window.WPSBD.switchTab) window.WPSBD.switchTab(tab);
  }

  /* Human "as-of" date stamp. */
  function asOf(iso) {
    if (!iso) return null;
    try { return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); }
    catch (_) { return null; }
  }
  function asOfTime(iso) {
    if (!iso) return null;
    try { return new Date(iso).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }
    catch (_) { return null; }
  }
  /* Display host (www-stripped) for a site/url. */
  function prettyHost(u) {
    const raw = String(u || '');
    try { return new URL(/^https?:\/\//.test(raw) ? raw : 'https://' + raw).hostname.replace(/^www\./, ''); }
    catch (_) { return raw.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/[\/?#].*$/, ''); }
  }

  /* Defensive readers — the saved_scans rows are snake_case server-side; tolerate
     either casing so a future field rename can't silently blank the surface. */
  function pick(o, keys) {
    for (let i = 0; i < keys.length; i++) {
      if (o && o[keys[i]] != null) return o[keys[i]];
    }
    return null;
  }
  function rowSite(it)  { return pick(it, ['site', 'host', 'domain']) || prettyHost(pick(it, ['site_url', 'siteUrl', 'url'])); }
  function rowUrl(it)   { return pick(it, ['site_url', 'siteUrl', 'url']) || rowSite(it); }
  function rowWhen(it)  { return pick(it, ['updated_at', 'updatedAt', 'scanned_at', 'scannedAt', 'created_at', 'createdAt']); }
  function rowPages(it) { return pick(it, ['page_count', 'pageCount', 'pages']); }
  function rowImgs(it)  { return pick(it, ['image_count', 'imageCount', 'images']); }
  /* Default-deny: an absent provenance tier is treated as tier-2 (private/owner). */
  function rowTier(it)  { const t = pick(it, ['provenance_tier', 'provenanceTier']); return t == null ? 2 : Number(t); }

  /* ── Small shared bits ─────────────────────────────────────────────────── */
  function ProvBadge({ tier }) {
    /* tier-1 = public/anonymous; tier-2 = owner-authenticated (private). */
    if (Number(tier) === 1) {
      return <span className="tag" title={tx('page.scanhistory.prov1_title', null, 'Tier 1 — public/anonymous (only what an anonymous crawler legitimately sees)')}>{tx('page.scanhistory.prov1_badge', null, 'TIER 1 · PUBLIC')}</span>;
    }
    return <span className="tag ok" title={tx('page.scanhistory.prov2_title', null, 'Tier 2 — owner-authenticated (private to your account)')}>{tx('page.scanhistory.prov2_badge', null, 'TIER 2 · OWNER')}</span>;
  }

  /* NULL-safe metric cell — renders an em dash + tooltip when the value is absent,
     never a fabricated 0. */
  function Metric({ label, value, tone }) {
    const has = value != null && value !== '';
    return (
      <div className="stat" style={{ minWidth: 120 }}>
        <div className="stat-lbl">{label}</div>
        <div className={'stat-val' + (has && tone ? ' ' + tone : '')}
             style={{ color: has ? undefined : 'var(--dim)' }}
             title={has ? undefined : tx('page.scanhistory.not_yet_scored', null, 'Not yet scored')}>
          {has ? value : '—'}
        </div>
      </div>
    );
  }

  function Skeleton() {
    return (
      <div className="card">
        <div className="card-body">
          {[0, 1, 2, 3].map(i => (
            <div key={i} aria-hidden="true"
                 style={{ height: 38, borderRadius: 'var(--r-sm, 8px)', marginBottom: 10,
                          background: 'var(--surface-2)', opacity: 1 - i * 0.18 }} />
          ))}
          <div style={{ fontSize: '.78rem', color: 'var(--dim)' }}>{tx('page.scanhistory.loading', null, 'Loading scan history…')}</div>
        </div>
      </div>
    );
  }

  /* ── Full-report detail (drill-in) ─────────────────────────────────────── */
  function ReportDetail({ site, onClose }) {
    const [phase, setPhase] = useState('loading'); /* loading | loaded | empty | error | signedout */
    const [report, setReport] = useState(null);    /* { scan_data, scanned_at, is_stale, source, provenance_tier } */

    const load = useCallback(async () => {
      const token = authToken();
      if (!token) { setPhase('signedout'); return; }
      setPhase('loading');
      try {
        const r = await fetch(apiBase() + '/scans/saved/by-site?consumer=history&site=' + encodeURIComponent(site),
          { headers: { Authorization: 'Bearer ' + token } });
        if (r.status === 404) { setPhase('empty'); return; }
        if (!r.ok) { setPhase('error'); return; }
        const j = await r.json();
        if (j && j.found && j.scan_data) { setReport(j); setPhase('loaded'); }
        else { setPhase('empty'); }
      } catch (_) {
        setPhase('error');
      }
    }, [site]);

    useEffect(() => { load(); }, [load]);

    function openInScanner() {
      /* Genuine hand-off: write the stored blob to the Scanner's session key so it
         hydrates the FULL tabbed report on its next mount — then navigate. This is a
         navigation, not a success claim; no toast asserts anything that didn't run. */
      try {
        if (report && report.scan_data && typeof sessionStorage !== 'undefined') {
          sessionStorage.setItem(SCANNER_SESSION_KEY, JSON.stringify(report.scan_data));
        }
      } catch (_) { /* quota / unavailable — navigation still proceeds */ }
      switchTab('scanner');
    }

    const head = (
      <div className="card-head">
        <div>
          <h2 className="card-title">{tx('page.scanhistory.full_report_host', { host: prettyHost(site) }, 'Full report · {host}')}</h2>
          {phase === 'loaded' && report && (
            <div style={{ fontSize: '.7rem', color: 'var(--dim)', marginTop: 3 }}>
              {report.is_stale ? tx('page.scanhistory.served_from_saved', null, 'Served from saved scan') : tx('page.scanhistory.latest_saved_scan', null, 'Latest saved scan')}
              {asOfTime(report.scanned_at) ? tx('page.scanhistory.as_of', { when: asOfTime(report.scanned_at) }, ' · as of {when}') : ''}
              {report.source ? tx('page.scanhistory.source', { source: report.source }, ' · source: {source}') : ''}
            </div>
          )}
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          {phase === 'loaded' && report && <ProvBadge tier={report.provenance_tier == null ? 2 : report.provenance_tier} />}
          <button className="btn btn-ghost btn-sm" onClick={onClose}><Icon name="x" size={12} />{tx('page.scanhistory.close', null, 'Close')}</button>
        </div>
      </div>
    );

    if (phase === 'loading') {
      return <div className="card">{head}<div className="card-body"><Skeleton /></div></div>;
    }
    if (phase === 'signedout') {
      return <div className="card">{head}<div className="card-body"><EmptyState icon={<Icon name="clock" size={30} />} title={tx('page.scanhistory.report_signedout_title', null, 'Sign in to view this report')} body={tx('page.scanhistory.report_signedout_body', null, 'Saved scan reports are private to your account.')} /></div></div>;
    }
    if (phase === 'error') {
      return <div className="card">{head}<div className="card-body"><EmptyState icon={<Icon name="clock" size={30} />} title={tx('page.scanhistory.report_error_title', null, 'Couldn’t load this report')} body={tx('page.scanhistory.report_error_body', null, 'The saved scan couldn’t be read just now. Try again shortly.')} /></div></div>;
    }
    if (phase === 'empty' || !report) {
      /* Honest-empty — the button's target scan has no stored report (e.g. an old
         pre-substrate scan). Never a fabricated success. */
      return (
        <div className="card">{head}
          <div className="card-body">
            <EmptyState
              icon={<Icon name="clock" size={30} />}
              title={tx('page.scanhistory.report_empty_title', null, 'No stored report for this site yet')}
              body={tx('page.scanhistory.report_empty_body', null, 'This site has no saved scan on record. Run a scan in the Site Scanner to record its first report.')}
              primary={{ icon: <Icon name="scanner" size={15} />, label: tx('page.scanhistory.open_scanner', null, 'Open Site Scanner'), onClick: () => switchTab('scanner') }} />
          </div>
        </div>
      );
    }

    /* loaded — render a NULL-safe summary of the stored findings. */
    const d = report.scan_data || {};
    const pages = pick(d, ['page_count', 'pageCount']) != null
      ? pick(d, ['page_count', 'pageCount'])
      : (Array.isArray(d.pages) ? d.pages.length : (typeof d.pages === 'number' ? d.pages : null));
    const images = pick(d, ['image_count', 'imageCount']) != null
      ? pick(d, ['image_count', 'imageCount'])
      : (Array.isArray(d.images) ? d.images.length : (typeof d.images === 'number' ? d.images : null));
    const ada = pick(d, ['ada_score', 'adaScore']);
    const seo = pick(d, ['seo_score', 'seoScore']);
    const sec = pick(d, ['security_score', 'securityScore']);
    const health = pick(d, ['health_score', 'healthScore']);
    const broken = pick(d, ['broken_link_count', 'brokenLinkCount', 'broken_links']);
    const altMissing = pick(d, ['alt_missing_count', 'altMissingCount', 'missing_alt']);

    return (
      <div className="card">{head}
        <div className="card-body">
          <div className="grid grid-4" style={{ marginBottom: 14 }}>
            <Metric label={tx('page.scanhistory.metric_pages', null, 'Pages')} value={pages} />
            <Metric label={tx('page.scanhistory.metric_images', null, 'Images')} value={images} />
            <Metric label={tx('page.scanhistory.metric_broken_links', null, 'Broken links')} value={broken} tone={broken > 0 ? 'warn' : undefined} />
            <Metric label={tx('page.scanhistory.metric_missing_alt', null, 'Missing alt text')} value={altMissing} tone={altMissing > 0 ? 'warn' : undefined} />
          </div>
          <div className="grid grid-4" style={{ marginBottom: 14 }}>
            <Metric label={tx('page.scanhistory.metric_ada_score', null, 'ADA score')} value={ada} />
            <Metric label={tx('page.scanhistory.metric_seo_score', null, 'SEO score')} value={seo} />
            <Metric label={tx('page.scanhistory.metric_security_score', null, 'Security score')} value={sec} />
            <Metric label={tx('page.scanhistory.metric_health_score', null, 'Health score')} value={health} />
          </div>
          <div style={{ fontSize: '.72rem', color: 'var(--dim)', lineHeight: 1.5, marginBottom: 14 }}>
            {tx('page.scanhistory.scores_read_prefix', null, 'Scores read ')}<strong>—</strong>{tx('page.scanhistory.scores_read_note', null, ' until the score producers ship — an em dash is honest (a 0 would be a false claim). This summary is read from the stored scan; no re-scan ran.')}
          </div>
          <button className="btn btn-ghost btn-sm" onClick={openInScanner}>
            <Icon name="scanner" size={13} />{tx('page.scanhistory.open_full_findings', null, 'Open full findings in Site Scanner →')}
          </button>
        </div>
      </div>
    );
  }

  /* ── Trend strip — honest-empty (no fabricated trend from a single point) ── */
  function TrendStrip() {
    return (
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-head"><h2 className="card-title">{tx('page.scanhistory.trend_title', null, 'Trend')}</h2><span className="tag">{tx('page.scanhistory.trend_tag', null, 'NEEDS ≥2 SCANS')}</span></div>
        <div className="card-body" style={{ fontSize: '.8rem', color: 'var(--dim)', lineHeight: 1.5 }}>
          {tx('page.scanhistory.trend_body', null, 'A trend appears once a site has two or more recorded scans. Only the latest scan per site is retained today, so there is no multi-point series to chart yet — this strip will light up automatically when the scan-snapshot time-series is available (it is never drawn from a single point).')}
        </div>
      </div>
    );
  }

  /* ── History list ──────────────────────────────────────────────────────── */
  function HistoryList({ items, selectedSite, onOpen }) {
    return (
      <div className="card">
        <div className="card-head">
          <h2 className="card-title">{items.length === 1
            ? tx('page.scanhistory.saved_scans_count_one', { n: items.length }, 'Saved scans · {n} site')
            : tx('page.scanhistory.saved_scans_count_many', { n: items.length }, 'Saved scans · {n} sites')}</h2>
          <span className="tag" title={tx('page.scanhistory.latest_per_site_title', null, 'The latest recorded scan per site (one row per site).')}>{tx('page.scanhistory.latest_per_site_tag', null, 'LATEST PER SITE')}</span>
        </div>
        <div className="card-body" style={{ padding: 0, overflowX: 'auto' }}>
          <table className="table" style={{ minWidth: 640 }}>
            <thead><tr>
              <th scope="col">{tx('page.scanhistory.col_site', null, 'Site')}</th>
              <th scope="col">{tx('page.scanhistory.col_recorded', null, 'Recorded')}</th>
              <th scope="col" style={{ textAlign: 'right' }}>{tx('page.scanhistory.col_pages', null, 'Pages')}</th>
              <th scope="col" style={{ textAlign: 'right' }}>{tx('page.scanhistory.col_images', null, 'Images')}</th>
              <th scope="col">{tx('page.scanhistory.col_provenance', null, 'Provenance')}</th>
              <th scope="col"></th>
            </tr></thead>
            <tbody>
              {items.map((it, i) => {
                const site = rowSite(it);
                const when = rowWhen(it);
                const pages = rowPages(it);
                const imgs = rowImgs(it);
                const active = selectedSite && prettyHost(selectedSite) === prettyHost(site);
                return (
                  <tr key={(it.id || site || i)} style={active ? { background: 'var(--surface-2)' } : undefined}>
                    <td style={{ fontWeight: 600 }}>{prettyHost(site)}</td>
                    <td className="mono" style={{ fontSize: '.78rem', color: 'var(--dim)' }}>{asOf(when) || '—'}</td>
                    <td className="mono" style={{ textAlign: 'right' }}>{pages != null ? pages : '—'}</td>
                    <td className="mono" style={{ textAlign: 'right' }}>{imgs != null ? imgs : '—'}</td>
                    <td><ProvBadge tier={rowTier(it)} /></td>
                    <td style={{ textAlign: 'right' }}>
                      <button className="btn btn-ghost btn-sm" onClick={() => onOpen(rowUrl(it) || site)}>
                        {tx('page.scanhistory.view_report', null, 'View report →')}
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </div>
    );
  }

  /* ── Surface root ──────────────────────────────────────────────────────── */
  function ScanHistory() {
    const [phase, setPhase] = useState('loading');  /* loading | loaded | empty | error | signedout */
    const [items, setItems] = useState([]);
    const [openSite, setOpenSite] = useState(null);

    const load = useCallback(async () => {
      const token = authToken();
      if (!token) { setPhase('signedout'); return; }
      setPhase('loading');
      try {
        const r = await fetch(apiBase() + '/scans/saved', { headers: { Authorization: 'Bearer ' + token } });
        /* A non-2xx (incl. 404) or a network throw is a LOAD ERROR — never the
           empty state. A failed load is not "zero scans" (Radical Transparency).
           Empty is ONLY a 200 with an empty list. */
        if (!r.ok) { setPhase('error'); return; }
        const j = await r.json();
        /* API contract: { scans: [...] } (server.js GET /scans/saved). Keep
           `items` as a back-compat alias so a shape change can't silently blank. */
        const list = Array.isArray(j.scans) ? j.scans : (Array.isArray(j.items) ? j.items : []);
        /* Reverse-chronological by recorded time. */
        list.sort((a, b) => {
          const ta = new Date(rowWhen(a) || 0).getTime();
          const tb = new Date(rowWhen(b) || 0).getTime();
          return tb - ta;
        });
        setItems(list);
        setPhase(list.length ? 'loaded' : 'empty');
      } catch (_) {
        setPhase('error');
      }
    }, []);

    useEffect(() => { load(); }, [load]);

    /* Pick up a focused site handed off by a dead-button wiring (Pages1 site
       detail / Site Scanner). Opens that site's report immediately. */
    useEffect(() => {
      try {
        const focus = typeof sessionStorage !== 'undefined' && sessionStorage.getItem(FOCUS_KEY);
        if (focus) {
          sessionStorage.removeItem(FOCUS_KEY);
          setOpenSite(focus);
        }
      } catch (_) { /* sessionStorage unavailable — no focus, list-only is fine */ }
    }, []);

    const header = (
      <PageHead crumb={tx('page.scanhistory.crumb', null, 'Tools')} title={tx('page.scanhistory.title', null, 'Scan History')}
        sub={tx('page.scanhistory.sub', null, 'Historical scan findings per site, read from your saved scans. Open any row to view its stored full report — no re-scan.')}
        actions={
          <button className="btn btn-ghost btn-sm" onClick={() => switchTab('scanner')}>
            <Icon name="scanner" size={13} />{tx('page.scanhistory.site_scanner_link', null, 'Site Scanner →')}
          </button>
        } />
    );

    let body;
    if (phase === 'loading') {
      body = <Skeleton />;
    } else if (phase === 'signedout') {
      body = <EmptyState icon={<Icon name="clock" size={30} />} title={tx('page.scanhistory.list_signedout_title', null, 'Sign in to view scan history')} body={tx('page.scanhistory.list_signedout_body', null, 'Saved scans are private to your account.')} />;
    } else if (phase === 'error') {
      body = <EmptyState icon={<Icon name="clock" size={30} />} title={tx('page.scanhistory.list_error_title', null, 'Couldn’t load scan history')} body={tx('page.scanhistory.list_error_body', null, 'The saved-scan service couldn’t be reached just now — this is a load error, not an empty history. Retry below.')} primary={{ label: tx('page.scanhistory.retry', null, 'Retry'), onClick: load }} />;
    } else if (phase === 'empty') {
      body = (
        <EmptyState
          icon={<Icon name="clock" size={30} />}
          title={tx('page.scanhistory.list_empty_title', null, 'No scans recorded yet')}
          body={tx('page.scanhistory.list_empty_body', null, 'Run a scan in the Site Scanner to start a site’s history. Saved scans appear here, newest first.')}
          primary={{ icon: <Icon name="scanner" size={15} />, label: tx('page.scanhistory.open_scanner', null, 'Open Site Scanner'), onClick: () => switchTab('scanner') }} />
      );
    } else {
      body = (
        <>
          <TrendStrip />
          {openSite && (
            <div style={{ marginBottom: 14 }}>
              <ReportDetail site={openSite} onClose={() => setOpenSite(null)} />
            </div>
          )}
          <HistoryList items={items} selectedSite={openSite} onOpen={(site) => setOpenSite(site)} />
        </>
      );
    }

    return <div>{header}<div style={{ marginTop: 16 }}>{body}</div></div>;
  }

  window.ScanHistory = ScanHistory;
})();
