import { createHash } from 'node:crypto';

export type V3EvidenceKind = 'deployment' | 'listing' | 'durable-store' | 'finality-verifier' | 'audit-anchor' | 'payee';

export interface V3CapabilityAuthorization {
  schemaVersion: '1.0.0';
  bundleId: string;
  environment: 'production';
  routeId: string;
  routeTemplate: string;
  concreteResource: string;
  method: 'GET' | 'POST';
  network: `eip155:${number}`;
  chainId: number;
  deploymentAddress: `0x${string}`;
  deploymentCodeDigest: string;
  asset: `0x${string}`;
  payee: `0x${string}`;
  amount: string;
  scheme: 'exact';
  requiredFinality: 'soft' | 'safe' | 'finalized';
  minConfirmations: number;
  validAfter: number;
  expiresAt: number;
  paymentRequirementDigest: string;
  listingReceiptId: string;
  deploymentEvidenceId: string;
  evidenceDigests: Readonly<Record<V3EvidenceKind, string>>;
  issuer: string;
  signature: string;
  bundleDigest: string;
}

export interface V3CapabilityEvidence {
  bundle: V3CapabilityAuthorization | null;
}

/** A verifier must authenticate the one signed bundle and return its immutable authorization. */
export type V3EvidenceVerifier = (
  bundle: Readonly<V3CapabilityAuthorization>,
  canonicalPayload: string,
) => Readonly<V3CapabilityAuthorization> | null;

export type V3CapabilityExpectation = Pick<V3CapabilityAuthorization,
  'schemaVersion' | 'routeId' | 'routeTemplate' | 'concreteResource' | 'method' |
  'network' | 'chainId' | 'deploymentAddress' | 'deploymentCodeDigest' | 'asset' |
  'payee' | 'amount' | 'scheme' | 'requiredFinality' | 'minConfirmations' |
  'validAfter' | 'expiresAt' | 'paymentRequirementDigest'>;

export const UNAVAILABLE_V3_CAPABILITIES: V3CapabilityEvidence = Object.freeze({ bundle: null });

const ID = /^[a-z0-9][a-z0-9._:-]{15,159}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const ISSUER = /^[a-z0-9][a-z0-9._:@/-]{7,159}$/;
const SIGNATURE = /^[A-Za-z0-9_-]{43,512}$/;
const ADDRESS = /^0x[0-9a-f]{40}$/;
const NETWORK = /^eip155:[1-9]\d{0,14}$/;
const ROUTE_ID = /^[a-z0-9][a-z0-9-]{1,79}$/;
const RESOURCE = /^\/api\/v3\/[A-Za-z0-9:_./-]+$/;
const MAX_EVIDENCE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const EVIDENCE_KINDS: readonly V3EvidenceKind[] =
  ['deployment', 'listing', 'durable-store', 'finality-verifier', 'audit-anchor', 'payee'];

export function canonicalJson(value: unknown): string {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
  const record = value as Record<string, unknown>;
  return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
}

export function canonicalEvidencePayload(bundle: V3CapabilityAuthorization): string {
  const { signature: _signature, bundleDigest: _bundleDigest, ...payload } = bundle;
  return canonicalJson(payload);
}

export function paymentRequirementDigest(value: Pick<V3CapabilityAuthorization,
  'schemaVersion' | 'routeId' | 'routeTemplate' | 'concreteResource' | 'method' |
  'network' | 'chainId' | 'deploymentAddress' | 'deploymentCodeDigest' | 'asset' |
  'payee' | 'amount' | 'scheme' | 'requiredFinality' | 'minConfirmations' |
  'validAfter' | 'expiresAt'>): string {
  return createHash('sha256').update(canonicalJson({
    schemaVersion: value.schemaVersion,
    routeId: value.routeId,
    routeTemplate: value.routeTemplate,
    concreteResource: value.concreteResource,
    method: value.method,
    network: value.network,
    chainId: value.chainId,
    deploymentAddress: value.deploymentAddress,
    deploymentCodeDigest: value.deploymentCodeDigest,
    asset: value.asset,
    payee: value.payee,
    amount: value.amount,
    scheme: value.scheme,
    requiredFinality: value.requiredFinality,
    minConfirmations: value.minConfirmations,
    validAfter: value.validAfter,
    expiresAt: value.expiresAt,
  })).digest('hex');
}

function hasExactKeys(value: object, expected: readonly string[]): boolean {
  const actual = Object.keys(value).sort();
  return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
}

function validBundleShape(bundle: V3CapabilityAuthorization, now: number): boolean {
  const evidence = bundle.evidenceDigests;
  return (
    hasExactKeys(bundle, [
      'schemaVersion', 'bundleId', 'environment', 'routeId', 'routeTemplate', 'concreteResource',
      'method', 'network', 'chainId', 'deploymentAddress', 'deploymentCodeDigest', 'asset',
      'payee', 'amount', 'scheme', 'requiredFinality', 'minConfirmations', 'validAfter',
      'expiresAt', 'paymentRequirementDigest', 'listingReceiptId', 'deploymentEvidenceId',
      'evidenceDigests', 'issuer', 'signature', 'bundleDigest',
    ]) &&
    bundle.schemaVersion === '1.0.0' &&
    ID.test(bundle.bundleId) &&
    bundle.environment === 'production' &&
    ROUTE_ID.test(bundle.routeId) &&
    RESOURCE.test(bundle.routeTemplate) &&
    RESOURCE.test(bundle.concreteResource) &&
    ['GET', 'POST'].includes(bundle.method) &&
    NETWORK.test(bundle.network) &&
    Number.isSafeInteger(bundle.chainId) &&
    bundle.network === `eip155:${bundle.chainId}` &&
    ADDRESS.test(bundle.deploymentAddress) &&
    DIGEST.test(bundle.deploymentCodeDigest) &&
    ADDRESS.test(bundle.asset) &&
    ADDRESS.test(bundle.payee) &&
    /^[1-9]\d*$/.test(bundle.amount) &&
    bundle.scheme === 'exact' &&
    ['soft', 'safe', 'finalized'].includes(bundle.requiredFinality) &&
    Number.isSafeInteger(bundle.minConfirmations) &&
    bundle.minConfirmations >= 1 &&
    bundle.minConfirmations <= 10000 &&
    Number.isSafeInteger(bundle.validAfter) &&
    Number.isSafeInteger(bundle.expiresAt) &&
    bundle.validAfter < bundle.expiresAt &&
    now >= bundle.validAfter &&
    now < bundle.expiresAt &&
    now - bundle.validAfter <= MAX_EVIDENCE_AGE_MS &&
    DIGEST.test(bundle.paymentRequirementDigest) &&
    ID.test(bundle.listingReceiptId) &&
    ID.test(bundle.deploymentEvidenceId) &&
    Boolean(evidence) &&
    hasExactKeys(evidence, EVIDENCE_KINDS) &&
    EVIDENCE_KINDS.every((kind) => DIGEST.test(evidence[kind])) &&
    new Set(EVIDENCE_KINDS.map((kind) => evidence[kind])).size === EVIDENCE_KINDS.length &&
    ISSUER.test(bundle.issuer) &&
    SIGNATURE.test(bundle.signature) &&
    DIGEST.test(bundle.bundleDigest)
  );
}

function matchesExpectation(bundle: V3CapabilityAuthorization, expected?: V3CapabilityExpectation): boolean {
  if (!expected) return true;
  return (Object.keys(expected) as Array<keyof V3CapabilityExpectation>)
    .every((key) => canonicalJson(bundle[key]) === canonicalJson(expected[key]));
}

export function evaluateV3CapabilityEvidence(
  evidence: V3CapabilityEvidence = UNAVAILABLE_V3_CAPABILITIES,
  verify?: V3EvidenceVerifier,
  now = Date.now(),
  expected?: V3CapabilityExpectation,
) {
  const bundle = evidence.bundle;
  let authenticated = false;
  if (bundle && verify && validBundleShape(bundle, now)) {
    const payload = canonicalEvidencePayload(bundle);
    const digestMatches = createHash('sha256').update(payload).digest('hex') === bundle.bundleDigest;
    const requirementMatches = paymentRequirementDigest(bundle) === bundle.paymentRequirementDigest;
    const verified = digestMatches && requirementMatches ? verify(Object.freeze({ ...bundle }), payload) : null;
    authenticated = Boolean(
      verified &&
      canonicalJson(verified) === canonicalJson(bundle) &&
      matchesExpectation(bundle, expected),
    );
  }
  return {
    durableReceiptStore: authenticated,
    deploymentVerified: authenticated,
    payeeVerified: authenticated,
    finalityVerifier: authenticated,
    auditIntegrity: authenticated,
    publicationEvidence: authenticated,
    coherentBundle: authenticated,
    authenticatedBundleCount: authenticated ? 1 : 0,
    satisfied: authenticated,
  };
}

export function evaluateV3BazaarPublicationGate(
  evidence: V3CapabilityEvidence = UNAVAILABLE_V3_CAPABILITIES,
  verify?: V3EvidenceVerifier,
  now = Date.now(),
) {
  const capability = evaluateV3CapabilityEvidence(evidence, verify, now);
  const bundle = capability.satisfied ? evidence.bundle : null;
  return {
    schema: 'vaultfire.v3.x402.publication-gate.v1',
    schemaVersion: '1.0.0',
    candidateCount: 0,
    eligible: false,
    listingReceiptIds: bundle ? [bundle.listingReceiptId] : [],
    deploymentEvidenceIds: bundle ? [bundle.deploymentEvidenceId] : [],
    required: ['objective-listing-receipt-id', 'verified-v3-deployment-evidence'],
    capabilityEvidenceSatisfied: capability.satisfied,
    coherentBundleRequired: true,
  } as const;
}

/** Parses JSON only after a recursive token walk rejects duplicate object keys. */
export function parseDuplicateSafeEvidenceJson(text: string): unknown {
  let index = 0;
  const whitespace = () => { while (/\s/.test(text[index] ?? '')) index += 1; };
  const stringToken = (): string => {
    const start = index;
    if (text[index++] !== '"') throw new Error('expected JSON string');
    while (index < text.length) {
      if (text[index] === '\\') { index += 2; continue; }
      if (text[index++] === '"') return JSON.parse(text.slice(start, index));
    }
    throw new Error('unterminated JSON string');
  };
  const value = (): void => {
    whitespace();
    if (text[index] === '{') {
      index += 1; whitespace();
      const keys = new Set<string>();
      if (text[index] === '}') { index += 1; return; }
      while (true) {
        whitespace(); const key = stringToken();
        if (keys.has(key)) throw new Error(`duplicate JSON key: ${key}`);
        keys.add(key); whitespace();
        if (text[index++] !== ':') throw new Error('expected colon');
        value(); whitespace();
        const delimiter = text[index++];
        if (delimiter === '}') return;
        if (delimiter !== ',') throw new Error('expected object delimiter');
      }
    }
    if (text[index] === '[') {
      index += 1; whitespace();
      if (text[index] === ']') { index += 1; return; }
      while (true) {
        value(); whitespace();
        const delimiter = text[index++];
        if (delimiter === ']') return;
        if (delimiter !== ',') throw new Error('expected array delimiter');
      }
    }
    if (text[index] === '"') { stringToken(); return; }
    const match = text.slice(index).match(/^(?:-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/);
    if (!match) throw new Error('invalid JSON value');
    index += match[0].length;
  };
  value(); whitespace();
  if (index !== text.length) throw new Error('trailing JSON data');
  return JSON.parse(text);
}
