#!/usr/bin/env node
// Standalone Mesh ledger verifier — the trust root.
//
// It imports NOTHING from mesh-core or any app: it re-implements the published spec
// from scratch, so verifying the ledger requires no trust in the vendor's code paths.
// Dependency-light: node's built-in crypto plus the RFC 8785 `canonicalize` package
// (independently vetted; the same spec any JCS implementation follows).
//
//   node verify-mesh-ledger.mjs --base https://mesh.example.com --chain <chain-id>
//   node verify-mesh-ledger.mjs --file dump.json [--anchors anchors.json]
//
// Specs supported (dispatched on the export's `scheme` field):
//   vaspera-mesh-ledger-v1 — single-chain (legacy dumps)
//   vaspera-mesh-ledger-v2 — multi-tenant: `chainId` is INSIDE every hashed preimage,
//     so a chain served under the wrong identity fails verification even if its hashes
//     are internally consistent (anti-substitution).
//
// Exit code 0 only if the chain is fully intact (and, when anchors are supplied,
// consistent with every externally witnessed head).
import { createHash } from "node:crypto"
import { readFile } from "node:fs/promises"
import canonicalize from "canonicalize"

const sha256 = (s) => createHash("sha256").update(s).digest("hex")

// Re-derive a row's canonical preimage from its exported fields alone.
function buildPreimage(scheme, r) {
  const fields = {
    seq: r.seq,
    prevHash: r.prevHash,
    agent: r.agent,
    action: r.action,
    model: r.model,
    inputs: r.inputs ?? null,
    sources: r.sources ?? null,
    outputs: r.outputs ?? null,
    confidence: r.confidence ?? null,
    decision: r.decision,
    decisionReason: r.decisionReason,
    policyVersion: r.policyVersion,
    createdAt: r.createdAt,
  }
  if (scheme === "vaspera-mesh-ledger-v2") fields.chainId = r.chainId
  return `${scheme}\n` + canonicalize(fields)
}

function argVal(args, key) {
  const i = args.indexOf(key)
  return i >= 0 ? args[i + 1] : null
}

function withParams(url, params) {
  const joiner = url.includes("?") ? "&" : "?"
  return url + joiner + params
}

async function loadAnchors(arg) {
  let text
  if (/^https?:\/\//.test(arg)) {
    const res = await fetch(arg)
    if (!res.ok) throw new Error(`anchors HTTP ${res.status}`)
    text = await res.text()
  } else {
    text = await readFile(arg, "utf8")
  }
  return JSON.parse(text).anchors ?? []
}

async function loadExport({ base, file, chain }) {
  if (file) {
    return JSON.parse(await readFile(file, "utf8"))
  }
  const rows = []
  let scheme = null
  let afterSeq = 0
  for (;;) {
    let url = `${base.replace(/\/$/, "")}/api/ledger/export`
    if (chain) url = withParams(url, `chain=${encodeURIComponent(chain)}`)
    url = withParams(url, `afterSeq=${afterSeq}&limit=500`)
    const res = await fetch(url)
    if (!res.ok) throw new Error(`export HTTP ${res.status}`)
    const page = await res.json()
    scheme = page.scheme ?? scheme
    rows.push(...(page.rows ?? []))
    if (page.nextAfterSeq == null) break
    afterSeq = page.nextAfterSeq
  }
  return { scheme, rows }
}

// Returns the first broken row (or null), stopping there — the chain is worthless past
// its first break.
function verify(scheme, rows, expectedChain) {
  let prevHash = null
  let expectedSeq = 1
  for (const r of rows) {
    const fail = (error) => ({ seq: r.seq, error })
    if (scheme === "vaspera-mesh-ledger-v2") {
      if (typeof r.chainId !== "string" || r.chainId.length === 0) return fail("missing chainId")
      if (expectedChain && r.chainId !== expectedChain)
        return fail(`chain identity: row belongs to chain ${r.chainId}, expected ${expectedChain}`)
      if (rows[0] && r.chainId !== rows[0].chainId) return fail("chain identity: rows from mixed chains")
    }
    if (r.seq !== expectedSeq) return fail(`dense-ordering break: expected seq ${expectedSeq}, got ${r.seq}`)
    if (typeof r.preimage !== "string" || typeof r.rowHash !== "string") return fail("missing preimage/rowHash")
    if (sha256(r.preimage) !== r.rowHash) return fail("integrity: rowHash != sha256(preimage)")
    if (buildPreimage(scheme, r) !== r.preimage) return fail("re-derivation: preimage does not match the exported fields")
    if (r.seq > 1 && r.prevHash !== prevHash) return fail("linkage: prevHash != previous row's rowHash")
    prevHash = r.rowHash
    expectedSeq += 1
  }
  return null
}

async function main() {
  const args = process.argv.slice(2)
  const base = argVal(args, "--base")
  const file = argVal(args, "--file")
  const chain = argVal(args, "--chain")
  // With --base, cross-check the public OpenTimestamps anchors automatically.
  let anchorsArg = argVal(args, "--anchors")
  if (!anchorsArg && base) {
    anchorsArg = `${base.replace(/\/$/, "")}/api/ledger/anchors`
    if (chain) anchorsArg = withParams(anchorsArg, `chain=${encodeURIComponent(chain)}`)
  }
  if (!base && !file) {
    console.error(
      "usage: node verify-mesh-ledger.mjs (--base <url> [--chain <chain-id>] | --file <dump.json>) [--anchors <url|file>]",
    )
    process.exit(2)
  }

  const exported = await loadExport({ base, file, chain })
  const rows = exported.rows ?? []
  const scheme = exported.scheme ?? "vaspera-mesh-ledger-v2"
  if (!["vaspera-mesh-ledger-v1", "vaspera-mesh-ledger-v2"].includes(scheme)) {
    console.error(`unknown ledger scheme: ${scheme}`)
    process.exit(2)
  }
  if (rows.length === 0) {
    console.log("ledger: 0 chained rows (pre-genesis — nothing to verify yet).")
    process.exit(0)
  }

  const broken = verify(scheme, rows, chain)
  const head = rows[rows.length - 1]
  if (broken) {
    console.error(`FAIL — chain broken at seq ${broken.seq}: ${broken.error}`)
    console.error(`  verified ${broken.seq - 1}/${rows.length} rows before the break.`)
    process.exit(1)
  }
  console.log(`PASS — ${rows.length} rows, chain intact (${scheme}).`)
  if (scheme === "vaspera-mesh-ledger-v2") console.log(`  chain: ${head.chainId}`)
  console.log(`  head: seq ${head.seq}, rowHash ${head.rowHash}`)

  // Cross-check against the external witness: every OpenTimestamps-anchored head must
  // still match the chain. A mismatch means the history was rewritten after being
  // witnessed. The .ots receipts themselves verify against Bitcoin with any OTS client —
  // this checks that what was witnessed is what's in the chain.
  if (anchorsArg) {
    let anchors = null
    try {
      anchors = await loadAnchors(anchorsArg)
    } catch (e) {
      console.log(`  anchors: unavailable (${e?.message ?? e}).`)
    }
    if (anchors && anchors.length) {
      const bySeq = new Map(rows.map((r) => [r.seq, r.rowHash]))
      for (const a of anchors) {
        const rh = bySeq.get(a.headSeq)
        if (rh === undefined) {
          console.error(`FAIL — anchor for seq ${a.headSeq} is beyond the exported chain (head behind a witnessed anchor).`)
          process.exit(1)
        }
        if (rh !== a.headHash) {
          console.error(`FAIL — chain diverges from the external witness at seq ${a.headSeq}:`)
          console.error(`  witnessed ${a.headHash}`)
          console.error(`  chain     ${rh}`)
          process.exit(1)
        }
      }
      const confirmed = anchors.filter((a) => a.proofStatus === "confirmed")
      const latest = anchors[0] // endpoint returns newest head first
      console.log(`  anchors: ${anchors.length} checked, all consistent (${confirmed.length} Bitcoin-confirmed).`)
      if (latest) {
        const desc =
          latest.proofStatus === "confirmed"
            ? `witnessed on Bitcoin block ${latest.bitcoinHeight}${latest.witnessedAt ? ` at ${latest.witnessedAt}` : ""}`
            : "pending Bitcoin confirmation (~hours)"
        console.log(`  latest anchor: head seq ${latest.headSeq} ${desc}. Verify the .ots receipt with any OpenTimestamps client.`)
      }
    } else if (anchors) {
      console.log("  anchors: none yet (external witness not submitted).")
    }
  }
  process.exit(0)
}

main().catch((e) => {
  console.error(`verifier error: ${e?.message ?? e}`)
  process.exit(2)
})
