import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync, readdirSync } from "node:fs";
import { resolve } from "node:path";
import {
  assertNoProtectedInterfaceDrift,
  extractSourceInterface,
  sourceInterfaceDigest,
} from "./interface-policy.mjs";

export const root = resolve(import.meta.dirname, "..");
export const manifestPath = resolve(root, "v3-program-dispositions.json");
export const PROTECTED_INPUT_CLASSES = Object.freeze([
  "behavioral",
  "belief",
  "biometric",
  "flourishing",
  "loyalty",
  "profiling",
  "quality",
  "reputation",
  "surveillance",
]);
export const PROTECTED_DECISIONS = Object.freeze([
  "access",
  "custody",
  "disputes",
  "eligibility",
  "pricing",
  "routing",
  "settlement",
  "slashing",
  "value-transfer",
]);

export const EXPECTED = Object.freeze({
  "supported-candidate": [
    "ai_accountability_bonds",
    "ai_partnership_bonds",
    "identity_registry",
    "multisig_governance",
    "privacy_guarantees",
    "vaultfire_adapter",
  ],
  "redesign-required": [
    "anti_surveillance",
    "dilithium_attestor",
    "mission_enforcement",
    "validation_registry",
  ],
  retired: [
    "belief_attestation_verifier",
    "flourishing_metrics_oracle",
    "production_belief_verifier",
    "reputation_registry",
    "trust_data_bridge",
  ],
});

export function readManifest() {
  return JSON.parse(readFileSync(manifestPath, "utf8"));
}

export function namesFor(manifest, disposition) {
  return manifest.programs
    .filter((program) => program.disposition === disposition)
    .map((program) => program.name)
    .sort();
}

export function sourceProgramNames() {
  return readdirSync(resolve(root, "programs"), { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name)
    .sort();
}

export function verifyCanonicalManifest(manifest = readManifest()) {
  assert.equal(manifest.schemaVersion, 2);
  assert.equal(manifest.releaseLine, "Vaultfire Solana V3");
  assert.equal(manifest.canonical, true);
  assert.equal(manifest.evidenceScope, "source-only");
  assert.equal(manifest.executableEvidenceAvailable, false);
  assert.equal(manifest.deploymentAuthorized, false);
  assert.equal(manifest.policy.buildOnlySupportedCandidates, true);
  assert.equal(manifest.policy.generatedIdlsOnlyForSupportedCandidates, true);
  assert.equal(manifest.policy.artifactsOnlyForSupportedCandidates, true);
  assert.equal(manifest.policy.deploymentMapsProhibited, true);
  assert.equal(manifest.policy.crossProgramInvocationProhibited, true);
  assert.equal(manifest.policy.protectedDecisionUseProhibited, true);
  assert.equal(manifest.policy.rehearsalUpgradeAuthority, null);
  assert.equal(manifest.policy.committedCandidateRequiredForExecutableEvidence, true);
  assert.deepEqual(manifest.policy.protectedInputClasses, PROTECTED_INPUT_CLASSES);
  assert.deepEqual(manifest.policy.protectedDecisions, PROTECTED_DECISIONS);
  assert.equal(manifest.programs.length, 15);

  const names = manifest.programs.map((program) => program.name);
  assert.equal(new Set(names).size, 15, "program names must be unique");
  assert.deepEqual([...names].sort(), sourceProgramNames());

  for (const [disposition, expected] of Object.entries(EXPECTED)) {
    assert.deepEqual(namesFor(manifest, disposition), [...expected].sort());
  }
  assert.equal(
    namesFor(manifest, "supported-candidate").length,
    manifest.policy.supportedCandidateCount,
  );
  assert.equal(
    namesFor(manifest, "redesign-required").length,
    manifest.policy.redesignRequiredCount,
  );
  assert.equal(namesFor(manifest, "retired").length, manifest.policy.retiredCount);

  for (const program of manifest.programs) {
    assert.equal(
      program.sourcePath,
      `programs/${program.name}/src/lib.rs`,
      `${program.name} source path`,
    );
    assert.equal(
      program.cargoManifestPath,
      `programs/${program.name}/Cargo.toml`,
      `${program.name} Cargo path`,
    );
    const source = readFileSync(resolve(root, program.sourcePath), "utf8");
    const declared = source.match(/declare_id!\("([^"]+)"\)/);
    assert.ok(declared, `${program.name} must declare an ID`);
    assert.equal(declared[1], program.programId, `${program.name} pinned ID mismatch`);
    assert.equal(decodeBase58(program.programId).length, 32, `${program.name} ID length`);
    const supported = program.disposition === "supported-candidate";
    assert.equal(program.buildEligible, supported, `${program.name} build eligibility`);
    assert.equal(
      program.rehearsalEligible,
      supported,
      `${program.name} rehearsal eligibility`,
    );
    assert.equal(program.protectedDecisionUse, "prohibited");
    if (supported) {
      assert.equal(
        createHash("sha256").update(source).digest("hex"),
        program.sourceSha256,
        `${program.name} source allowlist drift`,
      );
      assert.match(program.sourceSha256, /^[0-9a-f]{64}$/);
      const sourceInterface = extractSourceInterface(source);
      assert.deepEqual(
        sourceInterface.instructions.map((instruction) => instruction.name),
        program.allowedInstructions,
        `${program.name} instruction allowlist drift`,
      );
      assert.equal(
        sourceInterfaceDigest(source),
        program.sourceInterfaceSha256,
        `${program.name} account/event/instruction interface drift`,
      );
      assert.match(program.sourceInterfaceSha256, /^[0-9a-f]{64}$/);
      assertNoProtectedInterfaceDrift(sourceInterface, sourceInterface, program.name);
    } else {
      assert.equal(program.allowedInstructions, undefined);
      assert.equal(program.sourceSha256, undefined);
      assert.equal(program.sourceInterfaceSha256, undefined);
    }
    assert.ok(program.reason.length >= 30, `${program.name} requires a reason`);
  }

  const adapter = manifest.programs.find(
    (program) => program.name === "vaultfire_adapter",
  );
  assert.equal(adapter.mode, "passive-unverified-adapter");
  assert.equal(
    new Set(manifest.programs.map((program) => program.programId)).size,
    15,
    "program IDs must be globally unique",
  );
  return manifest;
}

export function supportedPrograms(manifest = verifyCanonicalManifest()) {
  return namesFor(manifest, "supported-candidate");
}

export function programByName(manifest, name) {
  const program = manifest.programs.find((entry) => entry.name === name);
  assert.ok(program, `unknown canonical program ${name}`);
  return program;
}

export function decodeBase58(value) {
  const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
  let number = 0n;
  for (const character of value) {
    const digit = alphabet.indexOf(character);
    assert.notEqual(digit, -1, `invalid base58 character ${character}`);
    number = number * 58n + BigInt(digit);
  }
  const bytes = [];
  while (number > 0n) {
    bytes.unshift(Number(number & 255n));
    number >>= 8n;
  }
  for (let index = 0; index < value.length && value[index] === "1"; index += 1) {
    bytes.unshift(0);
  }
  return Buffer.from(bytes);
}
