// Shared helpers for the verification scripts in ../scripts.
//
// All paths are resolved relative to this file, so the scripts can be run
// from any working directory. They load the repo-pinned snarkjs from the
// workspace node_modules, so `yarn install --frozen-lockfile` must have been
// run first.

const MINIMUM_NODE_MAJOR = 18; // for global fetch() and util.parseArgs()

if (Number(process.versions.node.split(".")[0]) < MINIMUM_NODE_MAJOR) {
  console.error(
    `Node.js >= ${MINIMUM_NODE_MAJOR} is required. Current: ${process.version}`
  );
  process.exit(1);
}

const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const util = require("node:util");
const { Readable } = require("node:stream");
const { pipeline } = require("node:stream/promises");

const REPO_ROOT = path.join(__dirname, "../..");
const CIRCUITS_PACKAGE = path.join(REPO_ROOT, "packages/circuits");
const ARTIFACTS_DIR = path.join(REPO_ROOT, "verification/artifacts");

const CIRCUIT_NAMES = ["commitment", "withdraw"];

// The two phase-1 powers-of-tau files used by this flow, keyed by the set of
// zkeys they check (the "kind"):
//
//   final - the checked-in trusted-setup final keys were produced by the
//           Privacy Pools p0tion ceremony, which runs on top of Perpetual
//           Powers of Tau contribution #80 (see README, "Phase 1").
//   build - the checked-in dev keys under packages/circuits/build were
//           generated locally from the Hermez file wired into the `setup:*`
//           scripts of packages/circuits/package.json.
//
// Neither upstream publishes hashes for these files; the values below were
// observed on 2026-07-03 (see README).
const PTAU_FILES = {
  final: {
    name: "ppot_0080_16.ptau",
    url: "https://pse-trusted-setup-ppot.s3.eu-central-1.amazonaws.com/pot28_0080/ppot_0080_16.ptau",
    sha256: "ed3622a7c79b0b49aadd134ebbc5b77df8c8c59bccebdfd0d9bf2c1a51561cf9",
    blake2b512:
      "9532c6c04a21335577713724b6d46c266a93aa621b78882b8b64b26f3080a8f0d974aded00c4d781adbdf493a45c51db455108f7aeedb49971569d57a56971c3",
  },
  build: {
    name: "powersOfTau28_hez_final_16.ptau",
    url: "https://storage.googleapis.com/zkevm/ptau/powersOfTau28_hez_final_16.ptau",
    sha256: "1c401abb57c9ce531370f3015c3e75c0892e0f32b8b1e94ace0f6682d9695922",
    blake2b512:
      "6a6277a2f74e1073601b4f9fed6e1e55226917efb0f0db8a07d98ab01df1ccf43eb0e8c3159432acd4960e2f29fe84a4198501fa54c8dad9e43297453efec125",
  },
};

const paths = {
  repoRoot: REPO_ROOT,
  artifacts: ARTIFACTS_DIR,
  regeneratedVkeys: path.join(ARTIFACTS_DIR, "regenerated-vkeys"),
  build: path.join(CIRCUITS_PACKAGE, "build"),
  finalKeys: path.join(CIRCUITS_PACKAGE, "trusted-setup/final-keys"),
  beaconHash: path.join(CIRCUITS_PACKAGE, "trusted-setup/beacon_hash"),
};

// Local path where a fetched ptau file of the given kind is stored.
function ptauPath(kind) {
  return path.join(ARTIFACTS_DIR, PTAU_FILES[kind].name);
}

function relativeToRepo(filePath) {
  return path.relative(REPO_ROOT, filePath);
}

// Parses process.argv against `options` (a util.parseArgs spec). Prints
// `usage` and exits on --help or on an unknown/malformed option.
function parseCli(usage, options = {}) {
  try {
    const { values } = util.parseArgs({
      options: { ...options, help: { type: "boolean" } },
      strict: true,
    });
    if (values.help) {
      console.log(usage);
      process.exit(0);
    }
    return values;
  } catch (error) {
    console.error(`${error.message}\n`);
    console.error(usage);
    process.exit(1);
  }
}

// Validates an optional comma-separated circuit subset ("commitment,withdraw").
function selectCircuits(raw) {
  if (!raw) return CIRCUIT_NAMES;
  const selected = raw.split(",").map((name) => name.trim());
  const unknown = selected.filter((name) => !CIRCUIT_NAMES.includes(name));
  if (unknown.length) {
    console.error(`Unknown circuit names: ${unknown.join(", ")}`);
    console.error(`Expected a subset of: ${CIRCUIT_NAMES.join(",")}`);
    process.exit(1);
  }
  return selected;
}

// Streams a file through SHA-256 and BLAKE2b-512 in one pass.
async function hashFile(filePath) {
  const sha256 = crypto.createHash("sha256");
  const blake2b512 = crypto.createHash("blake2b512");
  for await (const chunk of fs.createReadStream(filePath)) {
    sha256.update(chunk);
    blake2b512.update(chunk);
  }
  return { sha256: sha256.digest("hex"), blake2b512: blake2b512.digest("hex") };
}

async function downloadFile(url, destination) {
  const response = await fetch(url); // follows redirects
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} while downloading ${url}`);
  }
  await pipeline(
    Readable.fromWeb(response.body),
    fs.createWriteStream(destination)
  );
}

// Serializes a JSON value with object keys sorted, so two documents compare
// equal exactly when they are semantically equal.
function canonicalJson(value) {
  if (Array.isArray(value)) {
    return `[${value.map(canonicalJson).join(",")}]`;
  }
  if (value && typeof value === "object") {
    const entries = Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`);
    return `{${entries.join(",")}}`;
  }
  return JSON.stringify(value);
}

// --- minimal .zkey transcript reader ---------------------------------------
//
// A snarkjs .zkey is a sectioned binary container; section 10 holds the MPC
// transcript: a 64-byte circuit hash, a uint32 contribution count, and one
// record per contribution. Each record is 384 bytes of fixed-size curve
// points and hashes, a uint32 contribution type (0 = regular, 1 = beacon),
// and a tag-length-value block with the contributor name and, for beacons,
// the beacon parameters. We only need the count and the trailing record.
function readZkeyContributions(filePath) {
  const buf = fs.readFileSync(filePath);
  if (buf.toString("ascii", 0, 4) !== "zkey") {
    throw new Error(`${relativeToRepo(filePath)} is not a snarkjs zkey file`);
  }

  // Locate section 10. Container layout: magic(4) version(4) nSections(4),
  // then per section: type(4) size(8) data(size).
  const nSections = buf.readUInt32LE(8);
  let pos = 12;
  let section = null;
  for (let i = 0; i < nSections; i++) {
    const type = buf.readUInt32LE(pos);
    const size = Number(buf.readBigUInt64LE(pos + 4));
    pos += 12;
    if (type === 10) section = { start: pos, size };
    pos += size;
  }
  if (!section) {
    throw new Error(`${relativeToRepo(filePath)} has no MPC transcript section`);
  }

  pos = section.start + 64; // skip circuit hash
  const count = buf.readUInt32LE(pos);
  pos += 4;

  let last = null;
  for (let i = 0; i < count; i++) {
    const contribution = { type: buf.readUInt32LE(pos + 384) };
    const paramsEnd = pos + 392 + buf.readUInt32LE(pos + 388);
    let p = pos + 392;
    while (p < paramsEnd) {
      const tag = buf[p++];
      if (tag === 1) {
        const length = buf[p++];
        contribution.name = buf.toString("utf8", p, p + length);
        p += length;
      } else if (tag === 2) {
        contribution.numIterationsExp = buf[p++]; // single byte, no length
      } else if (tag === 3) {
        const length = buf[p++];
        contribution.beaconHash = buf.toString("hex", p, p + length);
        p += length;
      } else {
        throw new Error(
          `unknown contribution parameter tag ${tag} in ${relativeToRepo(filePath)}`
        );
      }
    }
    pos = paramsEnd;
    last = contribution;
  }
  return { count, last };
}

module.exports = {
  CIRCUIT_NAMES,
  PTAU_FILES,
  canonicalJson,
  downloadFile,
  hashFile,
  parseCli,
  paths,
  ptauPath,
  readZkeyContributions,
  relativeToRepo,
  selectCircuits,
};
