#!/usr/bin/env node

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import {
  decodeBase58,
  programByName,
  readManifest,
  supportedPrograms,
  verifyCanonicalManifest,
} from "./release-policy.mjs";
import { verifyCommittedCandidate } from "./verify-release-scope.mjs";

const root = resolve(import.meta.dirname, "..");
const artifactDir = join(root, "artifacts", "sbf");
const checksumDir = join(root, "checksums");
const dispositionManifest = verifyCanonicalManifest(readManifest());
const sourceCommit = verifyCommittedCandidate();
const programs = supportedPrograms(dispositionManifest);
const expectedArtifacts = programs.map((name) => `${name}.so`).sort();
const ephemeralProgramKeypairs = programs
  .map((name) => `${name}-keypair.json`)
  .sort();
const command = (name, args, options = {}) =>
  execFileSync(name, args, { cwd: root, encoding: "utf8", ...options }).trim();
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");

assert.equal(programs.length, 6, "expected exactly six supported candidates");
mkdirSync(artifactDir, { recursive: true });
assert.deepEqual(
  readdirSync(artifactDir),
  [],
  "artifact destination must be empty before verifier-owned builds",
);

const buildRoots = [
  mkdtempSync(join(tmpdir(), "vaultfire-solana-owned-build-a-")),
  mkdtempSync(join(tmpdir(), "vaultfire-solana-owned-build-b-")),
];
const buildInvocations = [];

function runOwnedBuild(pass, buildRoot) {
  const output = join(buildRoot, "sbf");
  const target = join(buildRoot, "target");
  mkdirSync(output);
  for (const name of programs) {
    const program = programByName(dispositionManifest, name);
    const args = [
      "build-sbf",
      "--manifest-path",
      join(root, program.cargoManifestPath),
      "--sbf-out-dir",
      output,
      "--",
      "--locked",
    ];
    buildInvocations.push({
      pass,
      program: name,
      command: "cargo",
      args: [
        "build-sbf",
        "--manifest-path",
        program.cargoManifestPath,
        "--sbf-out-dir",
        `<verifier-owned-pass-${pass}>`,
        "--",
        "--locked",
      ],
      cargoTargetDir: `<verifier-owned-target-${pass}>`,
    });
    execFileSync("cargo", args, {
      cwd: root,
      stdio: "inherit",
      env: { ...process.env, CARGO_TARGET_DIR: target },
    });
  }
  assert.deepEqual(
    readdirSync(output).sort(),
    [...expectedArtifacts, ...ephemeralProgramKeypairs].sort(),
    `verifier-owned build pass ${pass} produced an unexpected artifact set`,
  );
  for (const file of ephemeralProgramKeypairs) {
    rmSync(join(output, file), { force: true });
  }
  assert.deepEqual(
    readdirSync(output).sort(),
    expectedArtifacts,
    `verifier-owned build pass ${pass} retained keypair or unexpected bytes`,
  );
  // The reproducibility decision only needs the isolated output binaries.
  // Discard this pass's compiler cache before starting the next pass so the
  // verifier does not require space for two complete Cargo target trees.
  rmSync(target, { recursive: true, force: true });
  return output;
}

try {
  const firstOutput = runOwnedBuild(1, buildRoots[0]);
  verifyCommittedCandidate(sourceCommit);
  const secondOutput = runOwnedBuild(2, buildRoots[1]);
  verifyCommittedCandidate(sourceCommit);

  const artifactBytes = new Map();
  for (const file of expectedArtifacts) {
    const first = readFileSync(join(firstOutput, file));
    const second = readFileSync(join(secondOutput, file));
    assert.equal(
      first.equals(second),
      true,
      `verifier-owned clean builds differ for ${file}`,
    );
    artifactBytes.set(file, first);
  }

  const cargoLockSha256 = sha256(readFileSync(join(root, "Cargo.lock")));
  const supportedSourceHasher = createHash("sha256");
  for (const name of programs) {
    const program = programByName(dispositionManifest, name);
    supportedSourceHasher.update(`${name}\0${program.programId}\0`);
    supportedSourceHasher.update(readFileSync(join(root, program.sourcePath)));
    supportedSourceHasher.update("\0");
    supportedSourceHasher.update(readFileSync(join(root, program.cargoManifestPath)));
    supportedSourceHasher.update("\0");
  }
  const supportedSourceManifestSha256 = supportedSourceHasher.digest("hex");
  const rehearsalContractSha256 = sha256(
    Buffer.concat([
      readFileSync(join(root, "Containerfile.rehearsal")),
      Buffer.from("\0"),
      readFileSync(join(root, "rehearsal-versions.env")),
      Buffer.from("\0"),
      Buffer.from(JSON.stringify(buildInvocations)),
    ]),
  );

  const lines = [];
  const manifest = {
    schemaVersion: 2,
    purpose: "ephemeral-local-validator-rehearsal-only",
    evidenceScope: "verifier-owned-locally-built-executable-artifacts-only",
    v3DispositionEvidence: "source-only",
    candidateProgramsOnly: true,
    deployableRelease: false,
    deploymentAuthorized: false,
    upgradeAuthority: null,
    sourceCommit,
    sourceTreeDirty: false,
    committedCandidateVerified: true,
    reproducibilityMethod: "verifier-owned-two-clean-builds-byte-comparison",
    twoCleanBuildsByteIdentical: true,
    ephemeralProgramKeypairsDeletedBeforePublication: true,
    buildInvocations,
    rehearsalContractSha256,
    dispositionManifestSha256: sha256(
      readFileSync(join(root, "v3-program-dispositions.json")),
    ),
    cargoLockSha256,
    supportedSourceManifestSha256,
    toolchain: {
      anchor: command("anchor", ["--version"]),
      solana: command("solana", ["--version"]),
      rustc: command("rustc", ["--version"]),
      cargo: command("cargo", ["--version"]),
      node: process.version,
    },
    artifacts: [],
  };

  for (const file of expectedArtifacts) {
    const bytes = artifactBytes.get(file);
    const programName = file.slice(0, -3);
    const program = programByName(dispositionManifest, programName);
    const programIdBytes = decodeBase58(program.programId);
    assert.ok(bytes.length > 0, `${file} must not be empty`);
    assert.equal(bytes[0], 0x7f, `${file} must start with ELF magic`);
    assert.equal(bytes.subarray(1, 4).toString("ascii"), "ELF", `${file} must be ELF`);
    assert.notEqual(
      bytes.indexOf(programIdBytes),
      -1,
      `${file} does not contain its pinned program ID bytes`,
    );
    const artifactSha256 = sha256(bytes);
    lines.push(`${artifactSha256}  artifacts/sbf/${file}`);
    manifest.artifacts.push({
      file,
      programId: program.programId,
      sourcePath: program.sourcePath,
      sourceSha256: sha256(readFileSync(join(root, program.sourcePath))),
      cargoManifestPath: program.cargoManifestPath,
      cargoManifestSha256: sha256(readFileSync(join(root, program.cargoManifestPath))),
      bytes: bytes.length,
      sha256: artifactSha256,
      pass1Sha256: artifactSha256,
      pass2Sha256: sha256(readFileSync(join(secondOutput, file))),
    });
  }

  verifyCommittedCandidate(sourceCommit);
  for (const file of expectedArtifacts) copyFileSync(join(firstOutput, file), join(artifactDir, file));
  mkdirSync(checksumDir, { recursive: true });
  writeFileSync(join(checksumDir, "sbf-sha256.txt"), `${lines.join("\n")}\n`);
  writeFileSync(
    join(checksumDir, "sbf-manifest.json"),
    `${JSON.stringify(manifest, null, 2)}\n`,
  );
  console.log(`Built twice, compared, and checksummed ${expectedArtifacts.length} SBF artifacts.`);
} finally {
  for (const buildRoot of buildRoots) {
    if (existsSync(buildRoot)) rmSync(buildRoot, { recursive: true, force: true });
  }
}
