/**
 * Read-only Base Sepolia V3 rehearsal binding. The SDK accepts READY only when
 * a strict canonical manifest and a receipt emitted by the Python artifact
 * validator agree. It exposes no deployment, signing, or custody write path.
 */
import { createHash } from 'crypto';
import { ethers } from 'ethers';

export const BASE_SEPOLIA_V3_CHAIN_ID = 84532;
/** Manifest metadata version; it is intentionally unrelated to on-chain terms. */
export const OBJECTIVE_RULE_SCHEMA_VERSION = 1;
/** The only allowed V3Safe on-chain terms sentinel is rule class/version (0,0). */
export const V3SAFE_NO_OBJECTIVE_RULE_SCHEMA_VERSION = 0;
export const READY_FOR_REHEARSAL = 'READY_FOR_REHEARSAL' as const;
/** No production auditor/release public key is approved in this candidate. READY is fail-closed. */
const PRODUCTION_RELEASE_ATTESTER: string | undefined = undefined;

export type CanonicalContractName =
  | 'MultisigGovernanceV3' | 'BaseSepoliaRehearsalAccessControllerV3'
  | 'ThresholdConsensusV3' | 'DisputeRegistryV3' | 'SafeBondFactoryV3'
  | 'PartnershipBondFactoryV3' | 'AccountabilityBondFactoryV3';
export type BaseSepoliaV3ContractName =
  | 'governance' | 'controller' | 'consensus' | 'disputeRegistry'
  | 'safeBondFactory' | 'partnershipBondFactory' | 'accountabilityBondFactory';

export enum ObjectiveRuleClassV3 { None = 0 }

export interface CanonicalContractArtifact {
  address: string | null;
  creationCodeHash: string | null;
  runtimeCodeHash: string | null;
  constructorArguments: string | null;
}
export interface BaseSepoliaV3ArtifactEvidenceAttestation {
  schema: 'vaultfire.base-sepolia-v3-artifact-attestation.v2';
  validator: 'validate_v3_candidate_manifest.py';
  manifestArtifactHash: string;
  artifactEvidenceBundleSha256: string;
  validatorSourceSha256: string;
  validatorTranscriptSha256: string;
  deploymentVerificationStatementSha256: string;
  domain: 'vaultfire.base-sepolia-v3.ready-attestation.v2';
  chainId: 84532;
  keyId: string;
  algorithm: 'Ed25519';
  signature: string;
}
export interface BaseSepoliaV3RehearsalManifest {
  schema: 'vaultfire.base-sepolia-v3-rehearsal-manifest.v1';
  status: typeof READY_FOR_REHEARSAL | 'BLOCKED_UNTIL_HUMAN_ROLES_AND_EXACT_024_ARTIFACTS';
  network: { name: string; chainId: 84532; productionFundsProhibited: true; mainnetRejected: true; rpcPolicy: 'READ_ONLY_BASE_SEPOLIA_ONLY' };
  candidate: {
    sourceCommit: string | null; preRemediationContractsTree: string | null;
    currentContractsSourceSha256: string | null; sourceArchiveSha256: string | null;
    compiler: 'solc 0.8.24+commit.e11b9ed9'; foundry: string | null;
    settings: { viaIR: true; optimizerRuns: 200; evmVersion: 'paris'; bytecodeHash: 'none' };
    artifactEvidenceBundleSha256: string | null;
  };
  objectiveRuleSchema: {
    version: 1; allowedClasses: []; prohibitedCategories: string[];
    opaquePolicyHashesRejected: true; mutualConsentSettlementOnly: true; disputesChangeCustody: false;
  };
  contracts: Record<CanonicalContractName, CanonicalContractArtifact>;
  roles: { governanceSigners: unknown[]; guardian: unknown; designPartner: unknown; treasury: unknown };
  capsAtomic: { global: unknown; perPosition: unknown; partnershipPerParty: unknown; partnershipPerBond: unknown; accountabilityPerBond: unknown };
  orderedHumanAuthorizedCalls: string[]; enabledSurface: string[]; excludedSurface: string[];
  artifactHash: string;
}

const CONTRACT_MAPPING: Record<BaseSepoliaV3ContractName, CanonicalContractName> = {
  governance: 'MultisigGovernanceV3', controller: 'BaseSepoliaRehearsalAccessControllerV3',
  consensus: 'ThresholdConsensusV3', disputeRegistry: 'DisputeRegistryV3',
  safeBondFactory: 'SafeBondFactoryV3', partnershipBondFactory: 'PartnershipBondFactoryV3',
  accountabilityBondFactory: 'AccountabilityBondFactoryV3',
};
const CANONICAL_CONTRACTS = Object.values(CONTRACT_MAPPING) as CanonicalContractName[];
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const SHA256 = /^[0-9a-f]{64}$/i;
const COMMIT = /^[0-9a-f]{40}$/i;
const HASH32 = /^0x[0-9a-f]{64}$/i;
const ABI_BYTES = /^0x(?:[0-9a-f]{2})*$/i;
const ASCII_IDENTIFIER = /^[A-Za-z][A-Za-z0-9_]*$/;
const PROHIBITED = ['behavioral', 'flourishing', 'reputation', 'loyalty', 'quality', 'belief', 'biometric', 'profiling', 'surveillance'];
const EXCLUDED = ['rewards', 'x402', 'StreetCred', 'mandate', 'validation', 'ERC-8004', 'escrow', 'Solana', 'cross-chain'];
const ROOT_KEYS = ['schema', 'status', 'network', 'candidate', 'objectiveRuleSchema', 'contracts', 'roles', 'capsAtomic', 'orderedHumanAuthorizedCalls', 'enabledSurface', 'excludedSurface', 'artifactHash'];
const NETWORK_KEYS = ['name', 'chainId', 'productionFundsProhibited', 'mainnetRejected', 'rpcPolicy'];
const CANDIDATE_KEYS = ['sourceCommit', 'preRemediationContractsTree', 'currentContractsSourceSha256', 'sourceArchiveSha256', 'compiler', 'foundry', 'settings', 'artifactEvidenceBundleSha256'];
const SETTINGS_KEYS = ['viaIR', 'optimizerRuns', 'evmVersion', 'bytecodeHash'];
const RULE_KEYS = ['version', 'allowedClasses', 'prohibitedCategories', 'opaquePolicyHashesRejected', 'mutualConsentSettlementOnly', 'disputesChangeCustody'];
const CONTRACT_KEYS = ['address', 'creationCodeHash', 'runtimeCodeHash', 'constructorArguments'];
const ROLE_KEYS = ['governanceSigners', 'guardian', 'designPartner', 'treasury'];
const CAP_KEYS = ['global', 'perPosition', 'partnershipPerParty', 'partnershipPerBond', 'accountabilityPerBond'];
const ATTESTATION_KEYS = ['schema', 'validator', 'domain', 'chainId', 'manifestArtifactHash', 'artifactEvidenceBundleSha256', 'validatorSourceSha256', 'validatorTranscriptSha256', 'deploymentVerificationStatementSha256', 'keyId', 'algorithm', 'signature'];

function record(value: unknown, label: string): Record<string, unknown> {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object`);
  return value as Record<string, unknown>;
}
function assertAsciiKeys(value: unknown): void {
  if (Array.isArray(value)) { value.forEach(assertAsciiKeys); return; }
  if (value !== null && typeof value === 'object') {
    for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
      if (!ASCII_IDENTIFIER.test(key)) throw new Error('canonical JSON object keys must be ASCII identifiers');
      assertAsciiKeys(child);
    }
  }
}
function assertExactKeys(value: unknown, expected: string[], label: string): Record<string, unknown> {
  const actual = record(value, label); const keys = Object.keys(actual).sort(); const wanted = [...expected].sort();
  if (keys.length !== wanted.length || keys.some((key, index) => key !== wanted[index])) throw new Error(`${label} keys must exactly match canonical schema`);
  return actual;
}
function exactStrings(value: unknown, expected: string[], label: string): void {
  if (!Array.isArray(value) || value.length !== expected.length || new Set(value).size !== value.length
    || !value.every((item) => typeof item === 'string') || [...value].sort().some((item, index) => item !== [...expected].sort()[index])) {
    throw new Error(`${label} must exactly match canonical policy set`);
  }
}
function canonicalize(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(canonicalize);
  if (value !== null && typeof value === 'object') {
    const input = value as Record<string, unknown>;
    return Object.fromEntries(Object.keys(input).sort().map((key) => [key, canonicalize(input[key])]));
  }
  return value;
}
function digest(value: Record<string, unknown>, nullableField: string): string {
  assertAsciiKeys(value);
  const copy = JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
  copy[nullableField] = null;
  return createHash('sha256').update(JSON.stringify(canonicalize(copy)), 'utf8').digest('hex');
}
/** Exact v2 Ed25519 message shared with the Python test-only producer. */
export function canonicalAttestationV2SigningBytes(attestation: BaseSepoliaV3ArtifactEvidenceAttestation): Uint8Array {
  assertAsciiKeys(attestation);
  assertExactKeys(attestation, ATTESTATION_KEYS, 'artifact evidence attestation');
  const unsigned = JSON.parse(JSON.stringify(attestation)) as Record<string, unknown>;
  unsigned.signature = null;
  return Buffer.from(JSON.stringify(canonicalize(unsigned)), 'utf8');
}
/** Matches Python's sorted compact UTF-8 JSON exactly after strict key checks. */
export function canonicalManifestContentHash(manifest: BaseSepoliaV3RehearsalManifest): string {
  return digest(manifest as unknown as Record<string, unknown>, 'artifactHash');
}
export function canonicalArtifactEvidenceAttestationHash(attestation: BaseSepoliaV3ArtifactEvidenceAttestation): string {
  // Legacy helper retained for deterministic diagnostic fingerprints only; it is
  // never a trust proof and cannot authorize READY.
  return digest(attestation as unknown as Record<string, unknown>, 'signature');
}
/** Parse raw canonical evidence before digesting: duplicate members are rejected. */
export function parseCanonicalEvidenceJson(raw: string, label = 'evidence'): Record<string, unknown> {
  if (typeof raw !== 'string' || /[\uD800-\uDFFF]/.test(raw)) throw new Error(`${label} must be UTF-8 JSON without surrogate code points`);
  const stack: { keys: Set<string>; expectingKey: boolean }[] = []; let i = 0;
  const ws = () => { while (/\s/.test(raw[i] ?? '')) i += 1; };
  const stringToken = (): string => { const begin = i++; let escaped = false; while (i < raw.length) { const c = raw[i++]; if (escaped) { escaped = false; continue; } if (c === '\\') { escaped = true; continue; } if (c === '"') return raw.slice(begin, i); } throw new Error(`${label} has unterminated string`); };
  // Detect every object key directly in raw input before JSON.parse applies last-key-wins.
  while (i < raw.length) { ws(); if (raw[i] === '"') { const token = stringToken(); ws(); if (raw[i] === ':') { const key = JSON.parse(token) as string; const frame = stack[stack.length - 1]; if (!frame || frame.keys.has(key)) throw new Error(`${label} has duplicate JSON object member: ${key}`); frame.keys.add(key); } continue; } if (raw[i] === '{') { stack.push({ keys: new Set(), expectingKey: true }); } else if (raw[i] === '}') { stack.pop(); } i += 1; }
  let parsed: unknown; try { parsed = JSON.parse(raw); } catch { throw new Error(`${label} is not valid JSON`); }
  assertAsciiKeys(parsed);
  if (JSON.stringify(canonicalize(parsed)) !== raw) throw new Error(`${label} must use byte-exact compact canonical JSON`);
  return record(parsed, label);
}

function assertPopulatedAddress(label: string, value: string | null): void {
  if (typeof value !== 'string' || !ethers.isAddress(value) || value.toLowerCase() === ZERO_ADDRESS) throw new Error(`Base Sepolia V3 manifest has no permitted ${label} address`);
}
/** Terms sentinel is independent of manifest version and must be exactly (0,0). */
export function assertObjectiveRuleV3(ruleClass: number, schemaVersion: number): void {
  if (ruleClass !== ObjectiveRuleClassV3.None || schemaVersion !== V3SAFE_NO_OBJECTIVE_RULE_SCHEMA_VERSION) {
    throw new Error('Base Sepolia V3Safe terms permit only sentinel (0,0): mutual-consent custody has no objective evidence class');
  }
}
function assertStrictSemanticManifest(manifest: BaseSepoliaV3RehearsalManifest): void {
  assertAsciiKeys(manifest); const root = assertExactKeys(manifest, ROOT_KEYS, 'manifest');
  if (root.schema !== 'vaultfire.base-sepolia-v3-rehearsal-manifest.v1') throw new Error('invalid Base Sepolia V3 manifest schema');
  const network = assertExactKeys(root.network, NETWORK_KEYS, 'network');
  if (network.chainId !== BASE_SEPOLIA_V3_CHAIN_ID || network.mainnetRejected !== true || network.productionFundsProhibited !== true || network.rpcPolicy !== 'READ_ONLY_BASE_SEPOLIA_ONLY') throw new Error('V3 rehearsal manifest must be pinned to read-only Base Sepolia (84532)');
  const candidate = assertExactKeys(root.candidate, CANDIDATE_KEYS, 'candidate'); assertExactKeys(candidate.settings, SETTINGS_KEYS, 'candidate.settings');
  if (!COMMIT.test(String(candidate.sourceCommit ?? '')) || !SHA256.test(String(candidate.currentContractsSourceSha256 ?? '')) || !SHA256.test(String(candidate.sourceArchiveSha256 ?? '')) || !SHA256.test(String(candidate.artifactEvidenceBundleSha256 ?? '')) || candidate.compiler !== 'solc 0.8.24+commit.e11b9ed9' || (candidate.settings as Record<string, unknown>).viaIR !== true || (candidate.settings as Record<string, unknown>).optimizerRuns !== 200 || (candidate.settings as Record<string, unknown>).evmVersion !== 'paris' || (candidate.settings as Record<string, unknown>).bytecodeHash !== 'none') throw new Error('V3 rehearsal manifest lacks canonical committed-source and exact 0.8.24 artifact binding');
  const rules = assertExactKeys(root.objectiveRuleSchema, RULE_KEYS, 'objectiveRuleSchema');
  if (rules.version !== OBJECTIVE_RULE_SCHEMA_VERSION || !Array.isArray(rules.allowedClasses) || rules.allowedClasses.length !== 0 || rules.opaquePolicyHashesRejected !== true || rules.mutualConsentSettlementOnly !== true || rules.disputesChangeCustody !== false) throw new Error('V3 rehearsal manifest must disable generic evidence and dispute custody decisions');
  exactStrings(rules.prohibitedCategories, PROHIBITED, 'objectiveRuleSchema.prohibitedCategories'); exactStrings(root.excludedSurface, EXCLUDED, 'excludedSurface');
  const contracts = assertExactKeys(root.contracts, CANONICAL_CONTRACTS, 'contracts');
  for (const name of CANONICAL_CONTRACTS) {
    const artifact = assertExactKeys(contracts[name], CONTRACT_KEYS, name);
    assertPopulatedAddress(name, artifact.address as string | null);
    if (!HASH32.test(String(artifact.creationCodeHash ?? '')) || !HASH32.test(String(artifact.runtimeCodeHash ?? '')) || !ABI_BYTES.test(String(artifact.constructorArguments ?? ''))) throw new Error(`V3 rehearsal manifest has invalid exact artifact fields for ${name}`);
  }
  assertExactKeys(root.roles, ROLE_KEYS, 'roles'); assertExactKeys(root.capsAtomic, CAP_KEYS, 'capsAtomic');
}
function validateAttestation(manifest: BaseSepoliaV3RehearsalManifest, attestation: BaseSepoliaV3ArtifactEvidenceAttestation | undefined): void {
  // Deliberately before any provider call. An arbitrary self-generated SHA receipt
  // is never an attestation; production cannot opt into a caller-supplied key.
  if (!PRODUCTION_RELEASE_ATTESTER) throw new Error('SDK refuses READY: no independently pinned production release-attester key exists');
  if (!attestation) throw new Error('SDK refuses READY manifest without authenticated deployment evidence attestation');
  assertAsciiKeys(attestation); const receipt = assertExactKeys(attestation, ATTESTATION_KEYS, 'artifact evidence attestation');
  if (receipt.schema !== 'vaultfire.base-sepolia-v3-artifact-attestation.v2' || receipt.validator !== 'validate_v3_candidate_manifest.py' || !SHA256.test(String(receipt.manifestArtifactHash ?? '')) || !SHA256.test(String(receipt.artifactEvidenceBundleSha256 ?? '')) || !SHA256.test(String(receipt.validatorSourceSha256 ?? '')) || !SHA256.test(String(receipt.validatorTranscriptSha256 ?? '')) || !SHA256.test(String(receipt.deploymentVerificationStatementSha256 ?? '')) || receipt.manifestArtifactHash !== manifest.artifactHash || receipt.artifactEvidenceBundleSha256 !== manifest.candidate.artifactEvidenceBundleSha256 || receipt.domain !== 'vaultfire.base-sepolia-v3.ready-attestation.v2' || receipt.chainId !== BASE_SEPOLIA_V3_CHAIN_ID || receipt.algorithm !== 'Ed25519' || receipt.keyId !== PRODUCTION_RELEASE_ATTESTER || !ethers.isHexString(receipt.signature)) throw new Error('SDK authenticated deployment evidence attestation is absent, invalid, or does not bind this canonical manifest');
  throw new Error('SDK Ed25519 verifier is intentionally unavailable until an independently pinned production key and audited browser-compatible verifier are added');
}
/** Validate exact manifest semantics and the Python-produced READY receipt before mapping names. */
export function validateBaseSepoliaV3ManifestJson(manifestRaw: string, attestationRaw?: string): void {
  const manifest = parseCanonicalEvidenceJson(manifestRaw, 'manifest') as unknown as BaseSepoliaV3RehearsalManifest;
  const attestation = attestationRaw === undefined ? undefined : parseCanonicalEvidenceJson(attestationRaw, 'attestation') as unknown as BaseSepoliaV3ArtifactEvidenceAttestation;
  validateBaseSepoliaV3Manifest(manifest, attestation);
}
export function validateBaseSepoliaV3Manifest(manifest: BaseSepoliaV3RehearsalManifest, attestation?: BaseSepoliaV3ArtifactEvidenceAttestation): void {
  assertStrictSemanticManifest(manifest);
  if (manifest.status !== READY_FOR_REHEARSAL) throw new Error(`Refusing non-ready Base Sepolia V3 manifest status ${manifest.status}`);
  if (!SHA256.test(manifest.artifactHash) || canonicalManifestContentHash(manifest) !== manifest.artifactHash) throw new Error('V3 rehearsal manifest content hash is missing or tampered');
  validateAttestation(manifest, attestation);
}
function deepFreezeValue<T>(value: T): T {
  if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) { for (const child of Object.values(value as Record<string, unknown>)) deepFreezeValue(child); Object.freeze(value); }
  return value;
}
function deepFreezeManifest(manifest: BaseSepoliaV3RehearsalManifest): Readonly<BaseSepoliaV3RehearsalManifest> {
  const snapshot = JSON.parse(JSON.stringify(manifest)) as BaseSepoliaV3RehearsalManifest;
  return deepFreezeValue(snapshot) as Readonly<BaseSepoliaV3RehearsalManifest>;
}
/** A read-only binding to a validated and deeply frozen canonical manifest. */
export class BaseSepoliaV3Rehearsal {
  private constructor(public readonly provider: ethers.Provider, public readonly manifest: Readonly<BaseSepoliaV3RehearsalManifest>) {}
  static async connect(provider: ethers.Provider, manifest: BaseSepoliaV3RehearsalManifest, attestation?: BaseSepoliaV3ArtifactEvidenceAttestation): Promise<BaseSepoliaV3Rehearsal> {
    validateBaseSepoliaV3Manifest(manifest, attestation);
    const network = await provider.getNetwork();
    if (network.chainId !== BigInt(BASE_SEPOLIA_V3_CHAIN_ID)) throw new Error(`Refusing non-Base-Sepolia provider chain ${network.chainId.toString()}`);
    return new BaseSepoliaV3Rehearsal(provider, deepFreezeManifest(manifest));
  }
  contractAddress(name: BaseSepoliaV3ContractName): string { return this.manifest.contracts[CONTRACT_MAPPING[name]].address as string; }
}
