// ── Veritee · live Veritee Ledger data ─────────────────────────────────────
// Fetches the Midnight preprod attestation contract via the backend API and
// shapes it into window.VERITEE_DATA. The site is served by the Express
// backend, so /api/* is same-origin.
//
// The verification stream is high-volume (thousands of records/day), so it is
// never loaded wholesale: the initial load fetches registrations plus the
// server-side aggregates (GET /api/summary — totals, per-app rollup,
// per-registration activity), and the Explorer pages through verifications on
// demand via window.VERITEE_API.fetchVerifications (GET /api/verifications
// with cursor pagination and server-side filtering).
//
// The on-chain Attestation struct carries: version, attestation (an MAA token
// / JWT claims or a raw AMD SEV-SNP report), extra_pcrs, hashes, TLS routing
// metadata, issued_at, and app_details. Any UI field with no on-chain source
// is rendered as "n/a". See backend/src/transform.ts + aggregate.ts.
(function () {
  const NA = "n/a";

  // PCR label table for detail view (static — describes the TPM/PCR slots).
  const PCR_LABELS = {
    "0": "UEFI firmware measurement",
    "1": "UEFI configuration",
    "2": "UEFI drivers & boot apps",
    "3": "UEFI boot manager code",
    "4": "Boot loader",
    "12": "Reserved / custom",
    "13": "Reserved / custom",
  };

  // PCR slots the Explorer renders, in order (PCR15 is on-chain but not shown).
  const PCR_ORDER = ["0", "1", "2", "3", "4", "12", "13"];

  // Synchronous placeholder so the first render can't crash while we fetch.
  window.VERITEE_DATA = {
    records: [],
    stats: { total: NA, registrations: NA, verifications: NA, dapps: NA, applications: NA },
    // Server-side aggregates (see backend/src/aggregate.ts):
    //   apps          — per-app rollup rows for the verifications view
    //   registrations — per-registration {verifications, lastVerifiedAt}
    summary: { apps: [], registrations: {} },
    measurementApps: [],
    staging: null,
    DAPPS: [],
    VK: { x: NA, y: NA },
    PCR_LABELS,
    contract: NA,
    network: "Midnight · preprod",
    // Raw /api/status payload (chainInfo, syncedAt, pollIntervalMs) — used by
    // the Production Stats tab.
    status: null,
    loading: true,
    error: false,
  };

  // Strip a leading "0x" and lower-case so hashes match the mono styling.
  const cleanHex = (s) => (typeof s === "string" ? s.replace(/^0x/i, "") : NA);

  // Fallback for backend responses that predate attestationFormat. Mirrors
  // backend/src/transform.ts: SNP evidence is a JSON envelope marked
  // attestation.mode === "raw_sev_snp"; MAA is an envelope with maa_resp, a
  // normalized MAA policy object, or a bare JWT. Anything else is unknown.
  function inferAttestationFormat(s) {
    if (!s || typeof s !== "string") return "unknown";
    try {
      const obj = JSON.parse(s);
      if (obj && obj.attestation && obj.attestation.mode === "raw_sev_snp") return "sev-snp-report";
      if (obj && typeof obj === "object" && "maa_resp" in obj) return "maa-jwt";
      if (obj && obj["attestation-type"] === "sevsnpvm" && typeof obj["compliance-status"] === "string") return "maa-jwt";
      return "unknown";
    } catch (e) { /* not plain JSON — try JWT below */ }
    if (/^[\w-]+\.[\w-]+\.[\w-]+$/.test(s)) return "maa-jwt";
    return "unknown";
  }

  // Map one backend AttestationRecord → the UI record shape (type 0 —
  // a measurement registration in the `attestations` ledger map).
  // Map one backend OsReleaseRecord / OsReleaseLink → UI shape. The on-chain
  // os_releases entry stores digests only; the signed manifest (image ref,
  // source commit) stays in the Warden's archive.
  function toOsRelease(o) {
    return {
      os_key: o.osKey ? cleanHex(o.osKey) : NA,
      release_version: o.releaseVersion || NA,
      raw_disk_sha256: o.rawDiskSha256 ? cleanHex(o.rawDiskSha256) : NA,
      pcr4: o.pcr4 ? cleanHex(o.pcr4) : NA,
      pcr4_bank: o.pcr4Bank || NA,
      pcr4_profile: o.pcr4Profile || NA,
      attestation_mode: o.attestationMode || NA,
      acceptance: o.acceptance || "unknown",
      evidence_digest: o.evidenceDigest ? cleanHex(o.evidenceDigest) : NA,
      pcr4_matches: o.pcr4Matches === true,
      registration_ids: o.registrationIds || [],
    };
  }

  function toRecord(a, environment, contractAddress) {
    const pcrs = {};
    PCR_ORDER.forEach((k) => {
      const v = a.pcrs ? a.pcrs["pcr" + k] : null;
      pcrs[k] = v ? cleanHex(v) : NA;
    });
    const issuedMs = a.issuedAt ? Date.parse(a.issuedAt) : NaN;
    return {
      id: a.id || NA,
      key: environment === "staging" ? "staging:" + a.id : a.id,
      environment,
      contract: contractAddress || NA,
      type: 0, // all on-chain attestations = TEE identity registration
      dapp: {
        id: NA, // no dapp-id field on-chain (app_hash identifies the app — see app_hash)
        name: a.appName || NA, // from on-chain app_details JSON
        desc: a.appDescription || NA, // from on-chain app_details JSON
        url: a.appUrl || NA, // from on-chain app_details JSON
      },
      issued_at: Number.isFinite(issuedMs) ? issuedMs : NaN,
      version: a.onChainVersion != null ? a.onChainVersion : NA,
      pcrs,
      app_root_hash: a.applicationRootHash ? cleanHex(a.applicationRootHash) : NA,
      attestation_url: a.attestationUrl || NA,
      tls_mode: a.tlsMode || NA,
      app_hash: a.appHash ? cleanHex(a.appHash) : NA,
      // application_id = SHA256(temenos_app_key); groups policies of one app.
      application_id: a.applicationId ? cleanHex(a.applicationId) : NA,
      // On-chain attestation evidence (Bytes<16384>), committed to by
      // maa_hash: an MAA token (text) or a raw AMD SEV-SNP report (hex).
      // The backend classifies it; older responses lack the field, so infer.
      attestation: a.attestation || null,
      attestation_format: a.attestationFormat || inferAttestationFormat(a.attestation),
      maa_hash: a.maaHash ? cleanHex(a.maaHash) : NA,
      call_id: null, // verification-only; unused for registrations
      result_hash: null,
      // Set when the registration has an entry in the on-chain revocations
      // map. Forward-only: verifications recorded before revoked_at stay valid.
      revoked: Boolean(a.revoked),
      revoked_at: a.revokedAt ? Date.parse(a.revokedAt) : NaN,
      // Base OS release this image was admitted against (OS release registry
      // ledgers only; null on legacy ledgers). See toOsRelease.
      os_release: a.osRelease ? toOsRelease(a.osRelease) : null,
      // Per-record blockchain provenance is not exposed by the indexer.
      chain: {
        seq: NA,
        tx_hash: NA,
        block_height: NA,
        block_hash: NA,
        block_timestamp: NA,
        block_author: NA,
      },
    };
  }

  // Map one backend VerificationRecord → the UI record shape (type 1 — a
  // commitment-only runtime verification). The contract guarantees the parent
  // registration exists, so the app identity is inherited from it.
  function toVerificationRecord(v, registration, environment) {
    const verifiedMs = v.verifiedAt ? Date.parse(v.verifiedAt) : NaN;
    return {
      id: v.id || NA,
      key: environment === "staging" ? "staging:" + v.id : v.id,
      environment,
      type: 1,
      dapp: registration ? registration.dapp : { id: NA, name: NA, desc: NA, url: NA },
      issued_at: Number.isFinite(verifiedMs) ? verifiedMs : NaN,
      version: v.onChainVersion != null ? v.onChainVersion : NA,
      decision_status: v.status || "approved",
      application_id: registration ? registration.application_id : NA,
      registration_id: v.registrationId ? cleanHex(v.registrationId) : NA,
      attestation_hash: v.attestationHash ? cleanHex(v.attestationHash) : NA,
      call_id: v.callId ? cleanHex(v.callId) : NA,
      result_hash: v.resultHash ? cleanHex(v.resultHash) : NA,
      diagnostics: v.diagnostics || null,
      batch_prefix: v.batchPrefix || NA,
      revoked: false,
    };
  }

  // Registrations by id — set on load, used to give fetched verification
  // records their parent app identity.
  let registrationsById = new Map();

  function buildData(attestations, summary, status, staging) {
    const registrations = (Array.isArray(attestations) ? attestations : [])
      .map((a) => toRecord(a, "production", status && status.contractAddress));
    const stagingRecords = staging && staging.enabled && staging.syncedAt
      ? (staging.attestations || []).map((a) => toRecord(a, "staging", staging.contractAddress)) : [];
    registrations.push(...stagingRecords);
    // Scope parent lookups to the archive that supplied each verification.
    registrationsById = new Map(registrations.map((r) => [r.key, r]));
    // Newest first, matching the previous behavior.
    registrations.sort((a, b) => (b.issued_at || 0) - (a.issued_at || 0));

    const totals = (summary && summary.totals) || {};
    const count = (n) => (typeof n === "number" ? n : NA);
    const stagingArchiveReady = Boolean(staging && staging.syncedAt
      && ["azure-table", "fixture"].includes(staging.verificationSource));
    const stagingVerifications = stagingArchiveReady && staging.summary ? staging.summary.totals.verifications : 0;
    const stagingApps = staging && staging.summary ? staging.summary.apps.map((app) => ({
      ...app,
      key: "staging:" + app.key,
      environment: "staging",
      registrationIds: app.registrationIds.map((id) => "staging:" + id),
      verifications: stagingArchiveReady ? app.verifications : null,
      lastVerifiedAt: stagingArchiveReady ? app.lastVerifiedAt : null,
    })) : [];
    const apps = [...((summary && summary.apps) || []), ...stagingApps];
    const stagingActivity = stagingArchiveReady && staging.summary
      ? Object.fromEntries(Object.entries(staging.summary.registrations).map(([id, activity]) => ["staging:" + id, activity])) : {};

    return {
      records: registrations,
      stats: {
        total: typeof totals.registrations === "number" && typeof totals.verifications === "number"
          ? totals.registrations + stagingRecords.length + totals.verifications + stagingVerifications : NA,
        registrations: typeof totals.registrations === "number" ? totals.registrations + stagingRecords.length : NA,
        verifications: typeof totals.verifications === "number" ? totals.verifications + stagingVerifications : NA,
        dapps: count(totals.dapps),
        applications: typeof totals.applications === "number"
          ? totals.applications + (staging && staging.summary ? staging.summary.totals.applications : 0) : NA,
      },
      summary: {
        apps,
        registrations: { ...((summary && summary.registrations) || {}), ...stagingActivity },
      },
      measurementApps: apps,
      staging,
      stagingArchiveReady,
      DAPPS: [],
      VK: { x: NA, y: NA },
      PCR_LABELS,
      contract: (status && status.contractAddress) || NA,
      network: "Midnight · preprod",
      status: status || null,
      toOsRelease,
      loading: false,
      error: false,
    };
  }

  // ── On-demand verification stream (paged, server-filtered) ────────────────
  // opts: { app, registrationId, q, before, limit } — see backend GET
  // /api/verifications. Resolves to { items (UI records), total, nextBefore }.
  async function fetchVerifications(opts) {
    const o = opts || {};
    const params = new URLSearchParams();
    const isStagingApp = o.app && o.app.startsWith("staging:");
    const environment = isStagingApp || o.environment === "staging" ? "staging" : "production";
    if (environment === "staging") params.set("environment", "staging");
    if (o.limit) params.set("limit", String(o.limit));
    if (o.before) params.set("before", o.before);
    if (o.registrationId) params.set("registration_id", o.registrationId);
    if (o.app) params.set("app", isStagingApp ? o.app.slice("staging:".length) : o.app);
    if (o.q) params.set("q", o.q);
    const res = await fetch("/api/verifications?" + params.toString());
    if (!res.ok) throw new Error("verifications HTTP " + res.status);
    const page = await res.json();
    return {
      items: (page.items || []).map((v) => toVerificationRecord(v,
        registrationsById.get((environment === "staging" ? "staging:" : "") + cleanHex(v.registrationId)), environment)),
      total: page.total || 0,
      nextBefore: page.nextBefore || null,
    };
  }

  window.VERITEE_API = { fetchVerifications };

  // Live refresh: the backend re-syncs the contract every POLL_INTERVAL
  // seconds, so the page re-fetches on the same cadence (status.pollIntervalMs,
  // floored at 15s) and dispatches `veritee:data` again. Polling pauses while
  // the tab is hidden and catches up as soon as it is visible again. A failed
  // refresh keeps the last good data on screen instead of blanking the page.
  const DEFAULT_REFRESH_MS = 60000;
  let lastLoadedAt = 0;
  let inFlight = false;

  async function load() {
    if (inFlight) return;
    inFlight = true;
    try {
      const results = await Promise.allSettled(
        ["/api/attestations", "/api/summary", "/api/status", "/api/staging"].map(async (url) => {
          const res = await fetch(url, { cache: "no-store" });
          if (!res.ok) throw new Error(url + " HTTP " + res.status);
          return res.json();
        })
      );
      const [attestations, summary, status, staging] = results.map((r) => r.status === "fulfilled" ? r.value : null);
      results.forEach((r) => { if (r.status === "rejected") console.error("[veritee] failed to load:", r.reason); });
      const failed = !attestations || !summary;
      // After the first successful load, a failed refresh leaves the page as is.
      if (failed && lastLoadedAt) return;
      window.VERITEE_DATA = buildData(attestations, summary, status, staging || { error: true });
      window.VERITEE_DATA.error = failed;
      if (!failed) lastLoadedAt = Date.now();
      window.dispatchEvent(new Event("veritee:data"));
    } finally {
      inFlight = false;
    }
  }

  const refreshMs = () => {
    const s = window.VERITEE_DATA.status;
    return Math.max(15000, (s && s.pollIntervalMs) || DEFAULT_REFRESH_MS);
  };
  function schedule() {
    setTimeout(async () => {
      if (!document.hidden) await load();
      schedule();
    }, refreshMs());
  }
  document.addEventListener("visibilitychange", () => {
    if (!document.hidden && Date.now() - lastLoadedAt >= refreshMs()) load();
  });

  load().then(schedule);
})();
