import {
  isInsideContainer,
  getPreimageManager,
  getAccountsProvider,
  getTruApi,
} from "@parity/product-sdk-host";
import { cryptoWaitReady, signatureVerify } from "@polkadot/util-crypto";
import { hexToU8a, u8aToHex } from "@polkadot/util";
import { CID } from "multiformats/cid";
import { create as createMultihashDigest } from "multiformats/hashes/digest";
import { blake2b } from "@noble/hashes/blake2b";

// Bulletin's CID convention: CIDv1 + raw codec + blake2b-256 multihash.
const IPFS_CODEC_RAW = 0x55;
const IPFS_HASH_BLAKE2B_256 = 0xb220;

// ----- DOM refs ----------------------------------------------------------

const $receiptView = document.getElementById("receipt-view");
const $stateView = document.getElementById("state-view");

// Receipt skeleton
const $sealWrap = document.getElementById("seal-wrap");
const $headline = document.getElementById("headline");
const $subline = document.getElementById("subline");
const $chainSec = document.getElementById("chain-sec");
const $chain = document.getElementById("chain");
const $chainMeta = document.getElementById("chain-meta");
const $more = document.getElementById("more");
const $rows = document.getElementById("rows");
const $actions = document.getElementById("actions");
const $recruit = document.getElementById("recruit");
const $tamperLink = document.getElementById("tamper-link");
const $btnBuild = document.getElementById("btn-build");

// Uploader footer
const $footer = document.querySelector(".page-footer");
const $btnToggle = document.getElementById("btn-upload-toggle");
const $panel = document.getElementById("uploader");
const $jsonTextarea = document.getElementById("json");
const $btnSubmit = document.getElementById("btn-upload-submit");
const $btnExample = document.getElementById("btn-upload-example");
const $statusEl = document.getElementById("upload-status");
const $outputEl = document.getElementById("upload-output");
const $outputCid = document.getElementById("upload-cid");
const $btnCopy = document.getElementById("btn-upload-copy");
const $openReceipt = document.getElementById("upload-open");
const $errorEl = document.getElementById("upload-error");
const $logEl = document.getElementById("upload-log");

// ----- Inline SVG icons --------------------------------------------------

const I = {
  check:
    '<svg viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>',
  x: '<svg viewBox="0 0 24 24" fill="none" stroke="#fb7185" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>',
  link: '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round"><path d="M9 12h6M10 8H8a4 4 0 000 8h2M14 8h2a4 4 0 010 8h-2"/></svg>',
  clock:
    '<svg viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
  file: '<svg viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2"><path d="M14 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V8z"/><path d="M14 3v5h5"/></svg>',
  user: '<svg viewBox="0 0 24 24" fill="none" stroke="#34d399" stroke-width="2"><path d="M16 21v-2a4 4 0 00-4-4H7a4 4 0 00-4 4v2"/><circle cx="9.5" cy="7" r="4"/><path d="M17 11l2 2 4-4"/></svg>',
  info: '<svg viewBox="0 0 24 24" fill="none" stroke="#a0a0aa" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>',
  chev: '<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>',
};

// ----- Host bridge -------------------------------------------------------

let preimageManager = null;
let truApi = null;
let hostReady = false;
let bulletinAllowanceGranted = false;
let _hostInitPromise = null;

function ensureHost() {
  if (!_hostInitPromise) {
    _hostInitPromise = (async () => {
      try {
        const inside = await isInsideContainer();
        if (!inside) return false;
        const accountsProvider = await getAccountsProvider();
        if (accountsProvider?.getLegacyAccounts) {
          const r = await accountsProvider.getLegacyAccounts();
          if (r?.match)
            r.match(
              () => {},
              () => {},
            );
        }
        preimageManager = await getPreimageManager();
        truApi = await getTruApi();
        hostReady = !!preimageManager;
        return hostReady;
      } catch {
        return false;
      }
    })();
  }
  return _hostInitPromise;
}

// ----- CID + lookup ------------------------------------------------------

function cidFromHash() {
  const h = window.location.hash || "";
  if (!h.startsWith("#/")) return null;
  const raw = h.slice(2).trim();
  return raw || null;
}

function preimageHashFromCid(cidStr) {
  const cid = CID.parse(cidStr);
  return u8aToHex(cid.multihash.digest);
}

function lookupPreimage(cidStr, timeoutMs = 8_000) {
  const hash = preimageHashFromCid(cidStr);
  return new Promise((resolve) => {
    let done = false;
    let sub;
    const finish = (value) => {
      if (done) return;
      done = true;
      try {
        sub?.unsubscribe?.();
      } catch {}
      resolve(value);
    };
    try {
      sub = preimageManager.lookup(hash, (preimage) =>
        finish(preimage ?? null),
      );
    } catch {
      finish(null);
      return;
    }
    setTimeout(() => finish(null), timeoutMs);
  });
}

async function fetchArtifact(cid) {
  if (!hostReady) {
    throw new Error(
      "Polkadot host bridge unavailable — open this page inside the Polkadot app.",
    );
  }
  const bytes = await lookupPreimage(cid);
  if (!bytes)
    throw new Error(`Preimage not found for CID ${cid.slice(0, 18)}…`);
  try {
    return JSON.parse(new TextDecoder().decode(bytes));
  } catch (err) {
    throw new Error(`Preimage was not valid JSON: ${err.message}`);
  }
}

// ----- Signature verification --------------------------------------------

// Canonical signing convention: strip sig/pubkey/sig_alg, sort top-level keys,
// JSON.stringify, UTF-8 encode. MUST stay byte-identical to the deck's
// implementation in components/ProofPanelFull.vue — drift here breaks verification.
function canonicalSigningBytes(artifact) {
  const { sig, pubkey, sig_alg, ...rest } = artifact;
  const sorted = {};
  for (const k of Object.keys(rest).sort()) sorted[k] = rest[k];
  return new TextEncoder().encode(JSON.stringify(sorted));
}

async function verify(artifact) {
  if (!artifact || typeof artifact !== "object")
    return { ok: false, reason: "artifact is not an object" };
  if (!artifact.sig || !artifact.pubkey) {
    return { ok: false, reason: "artifact missing sig or pubkey" };
  }
  await cryptoWaitReady();
  const msg = canonicalSigningBytes(artifact);
  try {
    // signatureVerify auto-detects the crypto scheme (sr25519/ed25519/ecdsa)
    // and whether the message was wrapped in <Bytes>…</Bytes> before signing,
    // which Polkadot signers (including the host's legacy signer) do by default.
    const result = signatureVerify(
      msg,
      hexToU8a(artifact.sig),
      hexToU8a(artifact.pubkey),
    );
    if (!result.isValid) {
      return {
        ok: false,
        reason: `signature does not match (crypto: ${result.crypto}, wrapped: ${result.isWrapped})`,
      };
    }
    if (
      artifact.sig_alg &&
      artifact.sig_alg !== result.crypto &&
      result.crypto !== "none"
    ) {
      return {
        ok: false,
        reason: `sig_alg mismatch: artifact says ${artifact.sig_alg}, signature is ${result.crypto}`,
      };
    }
    return { ok: true };
  } catch (err) {
    return { ok: false, reason: `verify threw: ${err.message}` };
  }
}

// ----- Formatting helpers ------------------------------------------------

function fmtWindow(startIso, endIso) {
  if (!startIso) return "";
  try {
    const s = new Date(startIso);
    const e = endIso ? new Date(endIso) : null;
    const pad = (n) => String(n).padStart(2, "0");
    const fmt = (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`;
    return e ? `${fmt(s)}–${fmt(e)}` : fmt(s);
  } catch {
    return "";
  }
}

function fmtDate(iso) {
  if (!iso) return "";
  try {
    return new Date(iso).toLocaleDateString(undefined, {
      weekday: "long",
      month: "long",
      day: "numeric",
    });
  } catch {
    return "";
  }
}

// ----- Reveal cascade ----------------------------------------------------

function reveal(seq) {
  let d = 0;
  for (const el of seq) {
    if (!el) continue;
    const target = el;
    setTimeout(() => target.classList.add("in"), d);
    d += 160;
  }
}

function show(el) {
  if (!el) return null;
  el.style.display = "";
  return el;
}

// ----- Chain rendering ---------------------------------------------------

function clearChildren(el) {
  while (el.firstChild) el.removeChild(el.firstChild);
}

function escape(s) {
  return String(s ?? "").replace(
    /[&<>"']/g,
    (c) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
        c
      ],
  );
}

function buildChain(artifact) {
  if (!Array.isArray(artifact.chain) || artifact.chain.length === 0) {
    $chainSec.style.display = "none";
    return false;
  }

  // Reset (keep the .spine element which is the first child of #chain)
  clearChildren($chain);
  const spine = document.createElement("div");
  spine.className = "spine";
  $chain.appendChild(spine);

  for (const item of artifact.chain) {
    if (item.s) {
      const d = document.createElement("div");
      d.className = "sent";
      d.innerHTML = `<span class="dot"></span><p>${escape(item.s)}</p>`;
      $chain.appendChild(d);
    } else if (item.blk || item.h) {
      const d = document.createElement("div");
      d.className = "rivet" + (item.fresh ? " fresh" : "");
      const explorer = artifact.explorer_url || "#";
      d.innerHTML =
        (item.fresh ? '<span class="pz"></span>' : "") +
        `<span class="node">${I.link}</span>` +
        `<span class="chip">
          <span class="blk">block ${escape(item.blk || "")}</span>
          <span class="hash">${escape(item.h || "")}</span>
          <span class="time">${escape(item.time || "")}${item.fresh ? " · newest" : ""}</span>
          <span class="chip-detail">${escape(item.full || item.h || "")}<br><a href="${escape(explorer)}">open in explorer →</a></span>
        </span>`;
      d.querySelector(".chip").addEventListener("click", (e) => {
        e.currentTarget.classList.toggle("open");
      });
      $chain.appendChild(d);
    }
  }

  const totalSentences =
    artifact.total_sentences ?? artifact.chain.filter((c) => c.s).length;
  const totalBlocks =
    artifact.total_blocks ?? artifact.chain.filter((c) => c.blk || c.h).length;
  $chainMeta.textContent = `${totalSentences} sentences · ${totalBlocks} blocks`;
  $more.textContent = `⋮  showing the last few of ${totalSentences} — the full record is signed below`;
  $chainSec.style.display = "";
  return true;
}

// ----- Rows --------------------------------------------------------------

function buildRows(rows) {
  clearChildren($rows);
  for (const r of rows) {
    const div = document.createElement("div");
    div.className = "row" + (r.warn ? " warn" : "");
    div.innerHTML =
      `<div class="row-head"><span class="row-ic">${I[r.ic]}</span>` +
      `<span class="row-title">${escape(r.t)}</span>${I.chev}</div>` +
      `<div class="row-body"><div class="row-body-inner">${r.body}</div></div>`;
    div.querySelector(".row-head").addEventListener("click", () => {
      const open = div.classList.toggle("open");
      const b = div.querySelector(".row-body");
      b.style.maxHeight = open
        ? b.firstElementChild.scrollHeight + 24 + "px"
        : "0";
    });
    $rows.appendChild(div);
  }
}

function rowsForVerified(artifact) {
  const speaker = escape(
    artifact.speaker || artifact.dotns || "one verified human",
  );
  const anchorBlock = escape(artifact.anchor_block || "—");
  const totalSentences =
    artifact.total_sentences ??
    (Array.isArray(artifact.chain)
      ? artifact.chain.filter((c) => c.s).length
      : "—");
  const totalBlocks =
    artifact.total_blocks ??
    (Array.isArray(artifact.chain)
      ? artifact.chain.filter((c) => c.blk || c.h).length
      : "—");
  const explorer = escape(artifact.explorer_url || "#");

  return [
    {
      ic: "clock",
      t: "Happened today",
      body:
        `Each sentence is interleaved with the hash of the freshest Polkadot block, so this couldn’t exist before tonight. ` +
        `The whole record was then anchored on-chain at block <b>${anchorBlock}</b>, so it couldn’t be made later either.` +
        `<br><br><a href="${explorer}">View the anchor transaction →</a>`,
    },
    {
      ic: "file",
      t: "These exact words, untouched",
      body:
        `All ${totalSentences} sentences are hash-chained to the ${totalBlocks} blocks between them. ` +
        `Cut, splice, or reorder a single line and the chain breaks. What you heard is what was said.`,
    },
    {
      ic: "user",
      t: "One verified human behind them",
      body:
        `Signed by a key that lives in exactly one verified human — not an account, not a paid badge. ` +
        `A unique person stands behind every word and cannot deny it. (Signer: <b>${speaker}</b>.)`,
    },
    {
      ic: "info",
      t: "What this doesn’t prove",
      warn: true,
      body:
        `It does <b>not</b> prove the face in the video is real — nobody can promise that anymore. ` +
        `It proves accountability, not pixels.<br><br>` +
        `It does not prove the speaker wasn’t coerced, and someone who rents out their key could fake it. ` +
        `We raised the cost of pretending. We didn’t reach zero — nothing does.`,
    },
  ];
}

const TAMPERED_ROWS = [
  {
    ic: "info",
    t: "Why this happens",
    warn: true,
    body:
      `Any change — a face-swap, a clip, even an innocent re-upload that re-compresses the file — ` +
      `breaks the cryptographic match. Only the original, unaltered recording verifies.`,
  },
];

// ----- Receipt rendering -------------------------------------------------

function showReceipt() {
  $stateView.hidden = true;
  $stateView.innerHTML = "";
  $receiptView.hidden = false;
}

function showState(html, kind = "") {
  $receiptView.hidden = true;
  $stateView.hidden = false;
  $stateView.innerHTML = `<div class="state-line ${kind === "err" ? "state-err" : ""}">${html}</div>`;
}

function showStateRich(html) {
  $receiptView.hidden = true;
  $stateView.hidden = false;
  $stateView.innerHTML = html;
}

function clearRevealClasses() {
  for (const el of [
    $headline,
    $subline,
    $chainSec,
    $rows,
    $actions,
    $recruit,
    $tamperLink,
  ]) {
    if (el) el.classList.remove("in");
  }
  // The seal is rebuilt each time, no need to reset.
}

function renderVerified(artifact) {
  clearRevealClasses();
  showReceipt();

  $sealWrap.innerHTML = `<div class="seal ok r" id="seal"><div class="pulse"></div><div class="ring"></div>${I.check}</div>`;
  $headline.textContent = "This talk is real.";
  const speaker = escape(
    artifact.speaker || artifact.dotns || "one verified human",
  );
  const win = escape(
    artifact.window || fmtWindow(artifact.started_at, artifact.ended_at),
  );
  const winText = win ? `, today, ${win}` : "";
  $subline.innerHTML = `One verified human — <b>${speaker}</b> — said these words live${winText}.`;

  const hasChain = buildChain(artifact);
  buildRows(rowsForVerified(artifact));

  const seal = document.getElementById("seal");
  reveal([
    seal,
    $headline,
    $subline,
    hasChain ? show($chainSec) : null,
    $rows,
    show($actions),
    show($recruit),
  ]);
}

function renderTampered(reason) {
  clearRevealClasses();
  showReceipt();

  $sealWrap.innerHTML = `<div class="seal bad r" id="seal"><div class="pulse"></div><div class="ring"></div>${I.x}</div>`;
  $headline.textContent = "This copy doesn’t match.";
  $subline.innerHTML =
    `This receipt could not be verified against its signer. ` +
    `It is <b>not</b> the original record. That doesn’t mean the talk was fake — ` +
    `it means <b>this copy</b> can’t be trusted.<br><br>` +
    `<span style="font-family: var(--mono); font-size: 11px; color: var(--text-3); word-break:break-all">${escape(reason || "")}</span>`;

  $chainSec.style.display = "none";
  buildRows(TAMPERED_ROWS);
  $actions.style.display = "none";
  $recruit.style.display = "none";

  const seal = document.getElementById("seal");
  reveal([seal, $headline, $subline, $rows]);
}

// ----- Actions -----------------------------------------------------------

$btnBuild?.addEventListener("click", async () => {
  await ensureHost();
  await truApi?.navigateTo({ tag: "v1", value: "playground.dot" });
});

// ----- Main flow ---------------------------------------------------------

async function main() {
  const cid = cidFromHash();
  if ($footer) $footer.hidden = !!cid;

  if (!cid) {
    showStateRich(`
      <div class="state-line">Scan a Proof of Talk QR to verify a receipt.</div>
      <div class="state-sub">Or upload a proof below to test the round-trip.</div>
    `);
    return;
  }

  await ensureHost();

  showState("Fetching receipt&hellip;");

  let artifact;
  try {
    artifact = await fetchArtifact(cid);
  } catch (err) {
    showStateRich(`
      <div class="state-line state-err">Could not load receipt.</div>
      <pre class="state-sub state-err">${escape(err.message)}</pre>
    `);
    return;
  }

  showState("Verifying signature&hellip;");
  const verification = await verify(artifact);

  if (verification.ok) {
    renderVerified(artifact);
  } else {
    renderTampered(verification.reason);
  }
}

window.addEventListener("hashchange", main);
main();

// ----- Uploader ----------------------------------------------------------

const EXAMPLE_ARTIFACT = {
  v: 1,
  speaker: "Karim",
  dotns: "karim.dot",
  title: "People don't use technology, they use products",
  venue: "Web3 Summit 2026",
  started_at: "2026-06-14T19:42:00Z",
  ended_at: "2026-06-14T20:04:00Z",
  window: "19:42–20:04",
  anchor_block: "24,118,907",
  explorer_url: "https://paseo.subscan.io",
  canonical_url: "",
  total_sentences: 247,
  total_blocks: 246,
  chain: [
    {
      s: "Some of you are here with me. A lot of you are watching a recording.",
    },
    {
      h: "0x7a3f…c2e1",
      blk: "24,118,901",
      time: "20:03:58",
      full: "0x7a3f9e08bb1042d6e7c4f3a190d8b4e3c2e1",
    },
    {
      s: "You have no way of knowing I’m real — that this is my face, my voice.",
    },
    {
      h: "0x91b0…4d7a",
      blk: "24,118,903",
      time: "20:04:10",
      full: "0x91b0c5172af6e88130bd49aa6c33e1f24d7a",
    },
    { s: "So while I talk, that screen is going to do something about it." },
    {
      h: "0x3c5e…a71e",
      blk: "24,118,907",
      time: "20:04:23",
      full: "0x3c5e7d4419fa0b62c8e1d05530af7c92a71e",
      fresh: true,
    },
    { s: "Don’t worry about it yet. Just know it’s running." },
  ],
  pubkey: "0x00000000000000000000000000000000000000000000000000000000000000aa",
  sig: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
  sig_alg: "sr25519",
};

function cidForBytes(bytes) {
  const digest = blake2b(bytes, { dkLen: 32 });
  const mh = createMultihashDigest(IPFS_HASH_BLAKE2B_256, digest);
  return CID.createV1(IPFS_CODEC_RAW, mh).toString();
}

function uploadLog(msg) {
  const stamp = new Date().toLocaleTimeString();
  $logEl.textContent += `[${stamp}] ${msg}\n`;
}

function setUploadStatus(text, kind = "") {
  $statusEl.textContent = text;
  $statusEl.className = `upload-status${kind ? ` upload-status-${kind}` : ""}`;
}

function setUploadError(text) {
  if (!text) {
    $errorEl.hidden = true;
    $errorEl.textContent = "";
    return;
  }
  $errorEl.hidden = false;
  $errorEl.textContent = text;
}

async function ensureBulletinAllowance() {
  if (bulletinAllowanceGranted) return;
  if (!truApi) throw new Error("truApi unavailable — host bridge not ready");
  uploadLog("requesting BulletinAllowance…");
  const res = await truApi.requestResourceAllocation({
    tag: "v1",
    value: [{ tag: "BulletinAllowance", value: undefined }],
  });
  if (res?.match) {
    let thrown = null;
    res.match(
      () => uploadLog("BulletinAllowance granted"),
      (err) => {
        thrown = new Error(`BulletinAllowance: ${err?.value?.name ?? err}`);
      },
    );
    if (thrown) throw thrown;
  }
  bulletinAllowanceGranted = true;
}

$btnToggle?.addEventListener("click", () => {
  const opening = $panel.hidden;
  $panel.hidden = !opening;
  $btnToggle.textContent = opening ? "Hide uploader" : "Upload a proof";
  if (opening && !$jsonTextarea.value) {
    $jsonTextarea.value = JSON.stringify(EXAMPLE_ARTIFACT, null, 2);
  }
});

$btnExample?.addEventListener("click", () => {
  $jsonTextarea.value = JSON.stringify(EXAMPLE_ARTIFACT, null, 2);
});

$btnSubmit?.addEventListener("click", async () => {
  setUploadError("");
  $outputEl.hidden = true;

  await ensureHost();
  if (!hostReady) {
    setUploadError(
      "Polkadot host bridge unavailable — open this page inside the Polkadot app.",
    );
    return;
  }

  let json;
  try {
    json = JSON.parse($jsonTextarea.value);
  } catch (err) {
    setUploadError(`Invalid JSON: ${err.message}`);
    return;
  }

  const bytes = new TextEncoder().encode(JSON.stringify(json));
  const cid = cidForBytes(bytes);
  uploadLog(`bytes: ${bytes.length}`);
  uploadLog(`local CID (blake2b-256): ${cid}`);

  $btnSubmit.disabled = true;
  const original = $btnSubmit.textContent;
  $btnSubmit.textContent = "Uploading…";
  setUploadStatus("uploading…");

  try {
    await ensureBulletinAllowance();
    uploadLog("preimageManager.submit()…");
    const hash = await preimageManager.submit(bytes);
    uploadLog(`bulletin returned: ${hash}`);
    $outputCid.textContent = cid;
    $openReceipt.href = `#/${cid}`;
    $outputEl.hidden = false;
    setUploadStatus("uploaded", "ok");
  } catch (err) {
    setUploadError(`Upload failed: ${err.message}`);
    setUploadStatus("failed", "err");
  } finally {
    $btnSubmit.textContent = original;
    $btnSubmit.disabled = false;
  }
});

$btnCopy?.addEventListener("click", async () => {
  try {
    await navigator.clipboard.writeText($outputCid.textContent);
    $btnCopy.textContent = "Copied!";
    setTimeout(() => {
      $btnCopy.textContent = "Copy CID";
    }, 1500);
  } catch {}
});
