#!/usr/bin/env node

import assert from "node:assert/strict";
import {
  existsSync,
  lstatSync,
  readFileSync,
  readdirSync,
  statSync,
} from "node:fs";
import { basename, extname, resolve } from "node:path";
import { execFileSync } from "node:child_process";
import {
  EXPECTED,
  PROTECTED_INPUT_CLASSES,
  readManifest,
  root,
  sourceProgramNames,
  supportedPrograms,
  verifyCanonicalManifest,
  programByName,
} from "./release-policy.mjs";
import {
  assertIdlInterface,
  extractSourceInterface,
} from "./interface-policy.mjs";

const EXPECTED_PACKAGE_FILES = [
  "Anchor.toml",
  "Cargo.toml",
  "Cargo.lock",
  "v3-program-dispositions.json",
  "MAINNET_READINESS.json",
  "V3_SECURITY_RELEASE.json",
  "README.md",
  "PARTNER_LOCAL_REHEARSAL.md",
  "PROGRAM_IDS.md",
  "SECURITY_OPERABILITY_NOTES.md",
  "rehearsal-versions.env",
  "Containerfile.rehearsal",
  "scripts/",
  "programs/ai_accountability_bonds/",
  "programs/ai_partnership_bonds/",
  "programs/identity_registry/",
  "programs/multisig_governance/",
  "programs/privacy_guarantees/",
  "programs/vaultfire_adapter/",
];

function filesUnder(directory) {
  if (!existsSync(directory)) return [];
  const files = [];
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
    const path = resolve(directory, entry.name);
    if (entry.isDirectory()) files.push(...filesUnder(path));
    if (entry.isFile()) files.push(path);
  }
  return files;
}

function repositoryFiles(directory = root) {
  const files = [];
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
    if (directory === root && [".git", "node_modules"].includes(entry.name)) continue;
    const path = resolve(directory, entry.name);
    const stat = lstatSync(path);
    assertNotSymlink(stat.isSymbolicLink(), path);
    if (stat.isDirectory()) files.push(...repositoryFiles(path));
    if (stat.isFile()) files.push(path);
  }
  return files;
}

export function assertNotSymlink(isSymbolicLink, path) {
  assert.equal(isSymbolicLink, false, `symlink is prohibited: ${path}`);
}

export function workspaceMembers(cargoToml) {
  const block = cargoToml.match(/\[workspace\]([\s\S]*?)(?:\n\[|$)/);
  assert.ok(block, "Cargo workspace section is required");
  const members = block[1].match(/members\s*=\s*\[([\s\S]*?)\]/);
  assert.ok(members, "Cargo workspace members must be explicit");
  assert.doesNotMatch(members[1], /\*/, "workspace glob membership is prohibited");
  return [...members[1].matchAll(/"programs\/([^"]+)"/g)]
    .map((match) => match[1])
    .sort();
}

export function assertProgramIsolation(name, source, dependencyBlock, allPrograms) {
  const cpiPattern = /\binvoke_signed\s*\(|\binvoke\s*\(|\bcpi::/;
  assert.match(dependencyBlock, /anchor-lang\s*=\s*"=0\.32\.1"/);
  assert.doesNotMatch(dependencyBlock, /\bpath\s*=/, `${name} path dependency`);
  for (const other of allPrograms.filter((candidate) => candidate !== name)) {
    assert.doesNotMatch(
      `${dependencyBlock}\n${source}`,
      new RegExp(`\\b${other}\\b`),
      `${name} references program ${other}`,
    );
  }
  assert.doesNotMatch(source, cpiPattern, `${name} has prohibited raw/program CPI`);
  if (name === "ai_accountability_bonds") {
    assert.match(source, /system_program::transfer/);
    assert.match(source, /CpiContext::new/);
  } else {
    assert.doesNotMatch(source, /\bCpiContext\b/, `${name} has prohibited CPI`);
  }
}

function assertNoProgramDependencies(allPrograms) {
  for (const name of allPrograms) {
    const cargo = readFileSync(resolve(root, "programs", name, "Cargo.toml"), "utf8");
    const source = readFileSync(
      resolve(root, "programs", name, "src", "lib.rs"),
      "utf8",
    );
    const dependencies = cargo.match(/\[dependencies\]([\s\S]*?)(?:\n\[|$)/);
    assert.ok(dependencies, `${name} dependencies section is required`);
    assertProgramIsolation(name, source, dependencies[1], allPrograms);
  }
}

function assertProtectedInputBoundary(manifest, allowed) {
  for (const name of allowed) {
    const program = programByName(manifest, name);
    const source = readFileSync(resolve(root, program.sourcePath), "utf8");
    for (const inputClass of PROTECTED_INPUT_CLASSES) {
      assert.doesNotMatch(
        source,
        new RegExp(`\\b${inputClass.replace("-", "[-_]")}\\b`, "i"),
        `${name} introduces protected input class ${inputClass}`,
      );
    }
  }
}

export function assertNoExcludedJsonActivation(value, excluded, path = "document") {
  const canonicalProgramToken = (entry) =>
    typeof entry === "string"
      ? entry
          .trim()
          .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
          .toLowerCase()
          .replace(/[^a-z0-9]+/g, "_")
          .replace(/^_|_$/g, "")
      : "";
  const containsProgram = (entry, name) => {
    if (typeof entry === "string") return canonicalProgramToken(entry).includes(name);
    if (Array.isArray(entry)) return entry.some((item) => containsProgram(item, name));
    if (!entry || typeof entry !== "object") return false;
    return Object.entries(entry).some(
      ([key, item]) => containsProgram(key, name) || containsProgram(item, name),
    );
  };
  for (const name of excluded) {
    if (!containsProgram(value, name)) continue;
    assert.fail(
      `${path} mentions excluded program ${name} outside the canonical disposition manifest`,
    );
  }
}

function packagedFiles(entries) {
  return entries.flatMap((entry) => {
    const absolute = resolve(root, entry);
    if (entry.endsWith("/")) return filesUnder(absolute);
    assert.ok(existsSync(absolute), `packaged path is missing: ${entry}`);
    return [absolute];
  });
}

function assertPackagedReleaseSurface(manifest, excluded) {
  const packageJson = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
  assert.deepEqual(packageJson.files, EXPECTED_PACKAGE_FILES);
  for (const path of packageJson.files) {
    assert.doesNotMatch(path, /^historical\/|^TEST_RESULTS\.md$/);
    for (const name of excluded) {
      assert.doesNotMatch(path, new RegExp(`programs/${name}/`));
    }
  }

  for (const path of filesUnder(resolve(root, "historical", "tests"))) {
    if (basename(path) === "README.md") continue;
    const tombstone = readFileSync(path, "utf8");
    assert.match(tombstone, /NON-EXECUTABLE EVIDENCE TOMBSTONE/);
    assert.doesNotMatch(
      tombstone,
      /\b(?:import|require|describe|test|it)\s*\(|Keypair|sendAndConfirm|requestAirdrop/,
      `${path} contains executable historical test code`,
    );
  }

  const canonicalManifest = resolve(root, "v3-program-dispositions.json");
  const docsAndManifests = [
    resolve(root, "package.json"),
    ...packagedFiles(packageJson.files),
  ].filter((path) => {
    const file = basename(path);
    return (
      /\.(?:md|json|toml|lock|env)$/i.test(file) ||
      file === "Containerfile.rehearsal"
    );
  });
  for (const path of docsAndManifests) {
    if (path === canonicalManifest) continue;
    const relative = path.slice(root.length + 1).replaceAll("\\", "/");
    const contents = readFileSync(path, "utf8");
    assertNoExcludedReleaseClaims(contents, excluded, relative, false);
    if (extname(path) === ".json") {
      assertNoExcludedJsonActivation(JSON.parse(contents), excluded, relative);
    }
    for (const program of manifest.programs.filter((entry) => excluded.includes(entry.name))) {
      assert.doesNotMatch(
        contents,
        new RegExp(program.programId),
        `${relative} republishes excluded identity ${program.name}`,
      );
    }
  }

  const identityDoc = readFileSync(resolve(root, "PROGRAM_IDS.md"), "utf8");
  for (const name of supportedPrograms(manifest)) {
    const program = programByName(manifest, name);
    assert.match(
      identityDoc,
      new RegExp("`" + name + "` \\| `" + program.programId + "`"),
      `${name} partner identity mismatch`,
    );
  }
}

export function assertGeneratedOutputAllowed(file, contents, allowed, excluded) {
  const extension = extname(file);
  const programName = file.replace(/(?:-keypair)?\.(?:json|so)$/, "");
  if ([".so", ".json"].includes(extension)) {
    assert.ok(allowed.includes(programName), `generated output is not allowlisted: ${file}`);
  }
  for (const retired of excluded) {
    assert.doesNotMatch(
      `${file}\n${contents}`,
      new RegExp(`\\b${retired}\\b`),
      `excluded program entered generated output: ${retired}`,
    );
  }
  assert.doesNotMatch(file, /keypair\.json$/, "generated signer files are prohibited");
}

export function assertLockfileScope(lockfile, allowed, excluded) {
  const packageNames = [
    ...lockfile.matchAll(/^\[\[package\]\]\nname = "([^"]+)"/gm),
  ].map((match) => match[1]);
  for (const name of allowed) {
    assert.equal(
      packageNames.filter((candidate) => candidate === name).length,
      1,
      `${name} must appear exactly once in Cargo.lock`,
    );
  }
  for (const name of excluded) {
    assert.equal(
      packageNames.includes(name),
      false,
      `excluded package remains in Cargo.lock: ${name}`,
    );
  }
}

export function assertNoExcludedReleaseClaims(contents, excluded, file = "document") {
  const containsAlias = (line, name) => {
    const words = name.split("_").map((word) => word.toLowerCase());
    const compact = words.join("");
    const separated = line
      .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
      .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
      .toLowerCase()
      .split(/[^a-z0-9]+/)
      .filter(Boolean);
    const hasWordSequence = separated.some(
      (_, index) =>
        index + words.length <= separated.length &&
        words.every((word, offset) => separated[index + offset] === word),
    );
    const compactIdentifiers = line
      .split(/[^a-zA-Z0-9_-]+/)
      .filter(Boolean)
      .map((identifier) => identifier.replace(/[^a-zA-Z0-9]/g, "").toLowerCase());
    return hasWordSequence || compactIdentifiers.includes(compact);
  };
  let inFence = false;
  for (const [index, line] of contents.split("\n").entries()) {
    if (/^\s*```/.test(line)) {
      inFence = !inFence;
      continue;
    }
    if (inFence) continue;
    for (const name of excluded) {
      if (!containsAlias(line, name)) continue;
      const positive =
        /\b(?:release[- ]eligible|supported[- ]candidate|build(?:\/rehearsal)?[- ]eligible|deployed|fully functional|approved for (?:release|deployment)|production[- ]ready)\b/i.exec(
          line,
        );
      const safelyNegated =
        positive === null ||
        /\b(?:not|never|no|excluded from|prohibited from|blocked from|disabled from)\b[^.;]{0,48}$/i.test(
          line.slice(0, positive.index),
        );
      assert.equal(
        positive !== null && !safelyNegated,
        false,
        `${file}:${index + 1} makes a prohibited release claim for ${name}`,
      );
    }
  }
}

function containsSecretKeyShape(value) {
  if (Array.isArray(value)) {
    if (
      [32, 64].includes(value.length) &&
      value.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)
    ) {
      return true;
    }
    return value.some(containsSecretKeyShape);
  }
  if (!value || typeof value !== "object") return false;
  for (const [key, entry] of Object.entries(value)) {
    if (
      /^(?:secret.?key|private.?key|keypair|mnemonic|seed.?phrase|wallet)$/i.test(key) &&
      entry !== null &&
      entry !== false &&
      entry !== ""
    ) {
      return true;
    }
    if (containsSecretKeyShape(entry)) return true;
  }
  return false;
}

function containsTextualSecretArray(contents) {
  const candidate = /\[((?:\s*\d{1,3}\s*,){31,63}\s*\d{1,3}\s*)\]/g;
  for (const match of contents.matchAll(candidate)) {
    const bytes = match[1].split(",").map((entry) => Number(entry.trim()));
    if ([32, 64].includes(bytes.length) && bytes.every((byte) => byte >= 0 && byte <= 255)) {
      return true;
    }
  }
  return false;
}

export function assertNoKeyMaterial(relative, parsed, contents = "") {
  const file = basename(relative);
  const keyNamed =
    /(?:wallet|authority|signer|deployer|keypair|secret|mnemonic|seed)(?:[-_.]|$)/i.test(file) ||
    /(?:^|\/)id\.json$/i.test(relative) ||
    /\.(?:pem|key)$/i.test(file);
  assert.equal(keyNamed, false, `key material filename is prohibited: ${relative}`);
  if (parsed !== undefined) {
    assert.equal(
      containsSecretKeyShape(parsed),
      false,
      `Solana secret-key-shaped JSON is prohibited: ${relative}`,
    );
  }
  assert.doesNotMatch(
    contents,
    /-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----/,
    `private-key material is prohibited: ${relative}`,
  );
  assert.equal(
    containsTextualSecretArray(contents),
    false,
    `Solana secret-key-shaped integer array is prohibited: ${relative}`,
  );
}

export function assertReadinessCommitProvenance(readiness) {
  assert.equal(
    readiness.asOfCommit,
    null,
    "source-only readiness must not claim a commit before clean CI creates executable evidence",
  );
  assert.equal(readiness.asOfCommitReason, "unavailable-until-clean-committed-candidate");
  assert.equal(readiness.executableEvidenceAvailable, false);
}

export function verifyCommittedCandidate(expectedCommit = process.env.EXPECTED_CANDIDATE_COMMIT) {
  const command = (args) =>
    execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
  const head = command(["rev-parse", "HEAD"]);
  if (expectedCommit) assert.equal(head, expectedCommit, "candidate commit mismatch");
  const requiredTracked = [
    "v3-program-dispositions.json",
    "Cargo.toml",
    "Cargo.lock",
    ".github/workflows/ci.yml",
    "package.json",
    "scripts/interface-policy.mjs",
    "scripts/release-policy.mjs",
    "scripts/verify-release-scope.mjs",
    "scripts/verify-artifacts.mjs",
    "tests/v3-disposition-gates.test.mjs",
  ];
  for (const path of requiredTracked) {
    execFileSync("git", ["ls-files", "--error-unmatch", "--", path], {
      cwd: root,
      stdio: "ignore",
    });
  }
  assert.equal(
    command(["status", "--porcelain", "--untracked-files=all", "--", "."]),
    "",
    "committed candidate must have a clean Solana subtree",
  );
  return head;
}

export function assertGeneratedPathAllowed(
  relative,
  parsed,
  manifest,
  allowed,
  excluded,
  seenArtifacts = new Set(),
  seenIdls = new Set(),
) {
  const file = basename(relative);
  const extension = extname(file);
  const canonicalEvidenceFiles = new Set([
    "artifacts/rehearsal/local-validator-evidence.json",
    "checksums/sbf-manifest.json",
    "checksums/sbf-sha256.txt",
  ]);
  assertNoKeyMaterial(relative, parsed);

  if (relative.startsWith("artifacts/")) {
    const canonicalSbf =
      relative.startsWith("artifacts/sbf/") &&
      relative.split("/").length === 3 &&
      extension === ".so";
    assert.ok(
      canonicalSbf || canonicalEvidenceFiles.has(relative),
      `unexpected artifact file: ${relative}`,
    );
  }
  if (relative.startsWith("checksums/")) {
    assert.ok(canonicalEvidenceFiles.has(relative), `unexpected checksum file: ${relative}`);
  }
  if (relative.startsWith("target/")) {
    assert.ok(
      relative.startsWith("target/idl/") && relative.split("/").length === 3,
      `unexpected target file: ${relative}`,
    );
  }
  if (/checksum|sha(?:256|512)/i.test(file) && !canonicalEvidenceFiles.has(relative)) {
    assert.fail(`checksum outside canonical path: ${relative}`);
  }

  if (extension === ".so") {
    assert.ok(relative.startsWith("artifacts/sbf/"), `SBF outside canonical path: ${relative}`);
    assert.equal(relative.split("/").length, 3, `nested SBF path prohibited: ${relative}`);
    const name = file.slice(0, -3);
    assert.ok(allowed.includes(name), `non-allowlisted SBF: ${relative}`);
    assert.equal(seenArtifacts.has(name), false, `duplicate SBF for ${name}`);
    seenArtifacts.add(name);
  }

  const pathLooksLikeIdl = /(?:^|\/)(?:idl|idls|generated-idls)(?:\/|$)/i.test(relative);
  const contentLooksLikeIdl =
    parsed &&
    Array.isArray(parsed.instructions) &&
    (typeof parsed.address === "string" || typeof parsed.name === "string" || parsed.metadata);
  if (pathLooksLikeIdl || contentLooksLikeIdl) {
    assert.ok(
      relative.startsWith("target/idl/") && relative.split("/").length === 3,
      `IDL outside canonical path: ${relative}`,
    );
    const name = file.replace(/\.json$/, "");
    assert.ok(allowed.includes(name), `non-allowlisted IDL: ${relative}`);
    assert.equal(seenIdls.has(name), false, `duplicate IDL for ${name}`);
    seenIdls.add(name);
    const expected = programByName(manifest, name);
    assert.equal(parsed?.address, expected.programId, `${name} IDL program ID mismatch`);
    const source = readFileSync(resolve(root, expected.sourcePath), "utf8");
    assertIdlInterface(parsed, expected, extractSourceInterface(source));
  }

  for (const name of excluded) {
    if (extension === ".so" || pathLooksLikeIdl || contentLooksLikeIdl) {
      assert.doesNotMatch(
        `${relative}\n${JSON.stringify(parsed ?? {})}`,
        new RegExp(`\\b${name}\\b`),
      );
    }
  }
}

function assertRecursiveGeneratedOutputPolicy(manifest, allowed, excluded) {
  const seenArtifacts = new Set();
  const seenIdls = new Set();
  for (const path of repositoryFiles()) {
    const relative = path.slice(root.length + 1).replaceAll("\\", "/");
    const contents = readFileSync(path);
    let parsed;
    if (extname(path) === ".json") {
      try {
        parsed = JSON.parse(contents.toString("utf8"));
      } catch {
        parsed = undefined;
      }
    }
    assertNoKeyMaterial(relative, parsed, contents.toString("utf8"));
    assertGeneratedPathAllowed(
      relative,
      parsed,
      manifest,
      allowed,
      excluded,
      seenArtifacts,
      seenIdls,
    );
  }
}

function assertGeneratedOutputsAllowlisted(allowed, excluded) {
  const generated = [
    ...filesUnder(resolve(root, "artifacts", "sbf")),
    ...filesUnder(resolve(root, "target", "idl")),
    ...filesUnder(resolve(root, "target", "deploy")),
  ];
  for (const path of generated) {
    const file = basename(path);
    const extension = extname(file);
    const contents = extension === ".json" ? readFileSync(path, "utf8") : "";
    assertGeneratedOutputAllowed(file, contents, allowed, excluded);
    assert.ok(statSync(path).isFile());
  }
}

const manifest = verifyCanonicalManifest(readManifest());
const allPrograms = sourceProgramNames();
const allowed = supportedPrograms(manifest);
const excluded = [...EXPECTED.retired, ...EXPECTED["redesign-required"]];
const cargoToml = readFileSync(resolve(root, "Cargo.toml"), "utf8");
const readiness = JSON.parse(readFileSync(resolve(root, "MAINNET_READINESS.json"), "utf8"));

assertReadinessCommitProvenance(readiness);
assert.deepEqual(workspaceMembers(cargoToml), allowed);
for (const name of excluded) {
  assert.match(cargoToml, new RegExp(`"programs/${name}"`), `${name} must be excluded`);
}

const anchorToml = readFileSync(resolve(root, "Anchor.toml"), "utf8");
assert.doesNotMatch(anchorToml, /^\[programs\./m, "Anchor deployment maps are prohibited");
for (const prohibitedDirectory of ["deployments", ".anchor"]) {
  assert.equal(
    filesUnder(resolve(root, prohibitedDirectory)).length,
    0,
    `${prohibitedDirectory} deployment metadata is prohibited`,
  );
}
for (const prohibitedFile of ["deployment-map.json", "program-map.json"]) {
  assert.equal(
    existsSync(resolve(root, prohibitedFile)),
    false,
    `${prohibitedFile} is prohibited`,
  );
}
const migration = readFileSync(resolve(root, "migrations", "deploy.ts"), "utf8");
assert.match(migration, /throw new Error/);
assert.doesNotMatch(migration, /setProvider|\.rpc\s*\(/);
for (const name of excluded) {
  assert.doesNotMatch(anchorToml, new RegExp(`\\b${name}\\b`));
  assert.doesNotMatch(migration, new RegExp(`\\b${name}\\b`));
}

assertNoProgramDependencies(allPrograms);
assertProtectedInputBoundary(manifest, allowed);
assertLockfileScope(
  readFileSync(resolve(root, "Cargo.lock"), "utf8"),
  allowed,
  excluded,
);
assertGeneratedOutputsAllowlisted(allowed, excluded);
assertRecursiveGeneratedOutputPolicy(manifest, allowed, excluded);
assertPackagedReleaseSurface(manifest, excluded);

if (
  process.argv.includes("--require-committed-candidate") ||
  process.env.REQUIRE_COMMITTED_CANDIDATE === "1"
) {
  verifyCommittedCandidate();
}

console.log(
  `V3 release scope verified: ${allowed.length} supported candidates; ` +
    `${excluded.length} source-only excluded programs; no deployment map or program CPI.`,
);
