/**
 * Post-Quantum Signature Engine (ML-DSA-65)
 * NIST FIPS 204 compliant lattice-based digital signatures (Module-Lattice DSA / Dilithium).
 *
 * Companion to the ML-KEM-768 key-exchange engine (server/pqcEngine.ts): that engine does
 * post-quantum KEY EXCHANGE (FIPS 203); this one does post-quantum SIGNATURES (FIPS 204).
 *
 * Algorithm: ML-DSA-65 (Module-Lattice Digital Signature Algorithm)
 * Security Level: NIST Level 3 (192-bit / equivalent to AES-192)
 * Key & signature sizes (FIPS 204):
 *   - Public Key: 1952 bytes
 *   - Secret Key: 4032 bytes
 *   - Signature:  3309 bytes
 *   - Key seed (xi): 32 bytes
 *
 * Implementation status:
 *   - ML-DSA-65 sign/verify/keygen are REAL, FIPS 204-conformant via @noble/post-quantum
 *     (ml_dsa65), the same audited library that provides the real ML-KEM-768. keygen/sign/verify
 *     call the genuine lattice primitives — no HMAC/hash stand-in.
 *   - By default sign() is randomized (hedged, per FIPS 204). signDeterministic() disables the
 *     per-signature randomness (extraEntropy:false) for reproducible / known-answer signing.
 *   - Genuineness is proven by scripts/verify-mldsa65-engine.ts: byte-exact against the NIST ACVP
 *     FIPS-204 KAT vectors (keyGen + deterministic sigGen) THROUGH this engine's public API, plus
 *     sigVer expected-results and paired negative controls. Vectors + provenance in
 *     tests/fixtures/mldsa65/. Until that script is present+green, treat this as "real primitive,
 *     KAT-pending" — never claim conformance without the green KAT.
 *
 * Rule 1 (crypto hygiene): never log key or signature BYTES — only lengths, fingerprints, booleans.
 */

import * as crypto from 'crypto';
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
import { logger } from './lib/logger';

const dsaLogger = logger.child('pqc-sig');

// ML-DSA-65 constants (NIST FIPS 204). Asserted against ml_dsa65.lengths in selfTest().
export const ML_DSA_65 = {
  name: 'ML-DSA-65',
  publicKeySize: 1952,
  secretKeySize: 4032,
  signatureSize: 3309,
  seedSize: 32,
  securityLevel: 3,
  nistStandard: 'FIPS 204',
} as const;

export interface PQCSigKeyPair {
  publicKey: Buffer;
  secretKey: Buffer;
  algorithm: string;
  createdAt: Date;
  fingerprint: string;
}

export interface PQCSignatureResult {
  signature: Buffer;
  algorithm: string;
  deterministic: boolean;
  timestamp: Date;
  fingerprint: string; // fingerprint of the signing public key, for audit correlation
}

/**
 * ML-DSA-65 Digital Signature Engine.
 * Provides quantum-resistant signing and verification.
 *
 * Two surfaces:
 *   - Stateful instance surface (generateKeyPair → sign/verify with the engine's own key).
 *   - Stateless key-explicit surface (signWith / verifyWith) — used to sign arbitrary records
 *     with a caller-supplied key and to drive the NIST KAT with injected vector keys.
 */
export class MLDSAEngine {
  private keyPair: PQCSigKeyPair | null = null;

  // ── stateless, key-explicit core (also the KAT entry points) ────────────────

  /**
   * Real FIPS 204 ML-DSA-65 signature over `message` with an explicit secret key.
   * opts.deterministic disables per-signature randomness (extraEntropy:false) for reproducible /
   * known-answer signing; default is hedged randomized signing. opts.context is the optional
   * ML-DSA domain-separation context string (FIPS 204 §5.2; empty by default). This is the
   * external "pure" ML-DSA.Sign — the same path the NIST sigGen deterministic/external/pure KAT
   * exercises.
   */
  signWith(secretKey: Buffer, message: Buffer, opts: { deterministic?: boolean; context?: Buffer } = {}): Buffer {
    if (secretKey.length !== ML_DSA_65.secretKeySize) {
      throw new Error(`Invalid secret key size: expected ${ML_DSA_65.secretKeySize}, got ${secretKey.length}`);
    }
    const sigOpts: { context?: Uint8Array; extraEntropy?: false } = {};
    if (opts.context && opts.context.length > 0) sigOpts.context = opts.context;
    if (opts.deterministic) sigOpts.extraEntropy = false;
    return Buffer.from(ml_dsa65.sign(message, secretKey, sigOpts));
  }

  /**
   * Real FIPS 204 ML-DSA-65 verification of `signature` over `message` under `publicKey`.
   * Returns true only for a genuine signature; false for any tamper (message, signature, or key).
   */
  verifyWith(publicKey: Buffer, message: Buffer, signature: Buffer, context?: Buffer): boolean {
    if (publicKey.length !== ML_DSA_65.publicKeySize) {
      throw new Error(`Invalid public key size: expected ${ML_DSA_65.publicKeySize}, got ${publicKey.length}`);
    }
    // noble returns false (not throw) for well-formed-but-wrong signatures; a malformed
    // signature length can throw, so guard it to a clean false.
    try {
      const verOpts = context && context.length > 0 ? { context } : undefined;
      return ml_dsa65.verify(signature, message, publicKey, verOpts);
    } catch {
      return false;
    }
  }

  static fingerprintOf(publicKey: Buffer): string {
    return crypto.createHash('sha256').update(publicKey).digest('hex').substring(0, 16);
  }

  // ── stateful instance surface ───────────────────────────────────────────────

  /**
   * Generates a new ML-DSA-65 key pair (real FIPS 204 keygen via @noble/post-quantum).
   * keygen() draws a fresh 32-byte seed (xi) from the platform CSPRNG internally.
   */
  generateKeyPair(): PQCSigKeyPair {
    const kp = ml_dsa65.keygen();
    const publicKey = Buffer.from(kp.publicKey); // 1952 bytes
    const secretKey = Buffer.from(kp.secretKey); // 4032 bytes
    const fingerprint = MLDSAEngine.fingerprintOf(publicKey);

    this.keyPair = {
      publicKey,
      secretKey,
      algorithm: ML_DSA_65.name,
      createdAt: new Date(),
      fingerprint,
    };

    dsaLogger.info('Generated signature key pair', {
      metadata: {
        algorithm: ML_DSA_65.name,
        publicKeyBytes: publicKey.length,
        secretKeyBytes: secretKey.length,
        fingerprint,
      },
    });

    return this.keyPair;
  }

  /**
   * Deterministic FIPS 204 keygen from an explicit 32-byte seed (xi). Same key every time for a
   * given seed — used for the NIST keyGen KAT and for reproducible test keys. NOT for production
   * signing keys (those must use generateKeyPair()'s CSPRNG seed).
   */
  generateKeyPairFromSeed(seed: Buffer): PQCSigKeyPair {
    if (seed.length !== ML_DSA_65.seedSize) {
      throw new Error(`Invalid seed size: expected ${ML_DSA_65.seedSize}, got ${seed.length}`);
    }
    const kp = ml_dsa65.keygen(seed);
    const publicKey = Buffer.from(kp.publicKey);
    const secretKey = Buffer.from(kp.secretKey);
    this.keyPair = {
      publicKey,
      secretKey,
      algorithm: ML_DSA_65.name,
      createdAt: new Date(),
      fingerprint: MLDSAEngine.fingerprintOf(publicKey),
    };
    return this.keyPair;
  }

  /** Signs `message` with the engine's own secret key. */
  sign(message: Buffer, deterministic = false): PQCSignatureResult {
    if (!this.keyPair) throw new Error('No key pair generated. Call generateKeyPair() first.');
    const signature = this.signWith(this.keyPair.secretKey, message, { deterministic });
    dsaLogger.debug('Signed message', {
      metadata: { algorithm: ML_DSA_65.name, signatureBytes: signature.length, deterministic, fingerprint: this.keyPair.fingerprint },
    });
    return {
      signature,
      algorithm: ML_DSA_65.name,
      deterministic,
      timestamp: new Date(),
      fingerprint: this.keyPair.fingerprint,
    };
  }

  /** Verifies `signature` over `message` against a public key (defaults to the engine's own). */
  verify(message: Buffer, signature: Buffer, publicKey?: Buffer): boolean {
    const pk = publicKey ?? this.keyPair?.publicKey;
    if (!pk) throw new Error('No public key available. Call generateKeyPair() or pass a publicKey.');
    return this.verifyWith(pk, message, signature);
  }

  getPublicKey(): Buffer {
    if (!this.keyPair) throw new Error('No key pair generated. Call generateKeyPair() first.');
    return this.keyPair.publicKey;
  }

  getFingerprint(): string {
    if (!this.keyPair) throw new Error('No key pair generated. Call generateKeyPair() first.');
    return this.keyPair.fingerprint;
  }

  getAlgorithmInfo(): typeof ML_DSA_65 {
    return { ...ML_DSA_65 };
  }

  /**
   * Functional self-test: proves the primitive genuinely signs/verifies with real FIPS-204
   * shapes and detects tamper. This is NOT the NIST KAT (that is the byte-exact vector check in
   * scripts/verify-mldsa65-engine.ts) — it is a fast, dependency-free liveness/tamper gate.
   * Returns a structured result; never throws for a crypto failure (reports it instead).
   */
  selfTest(): { passed: boolean; checks: Array<{ name: string; ok: boolean }> } {
    const checks: Array<{ name: string; ok: boolean }> = [];
    const record = (name: string, ok: boolean) => checks.push({ name, ok });
    try {
      // Library-reported lengths match FIPS-204 constants.
      record('publicKey length == 1952', ml_dsa65.lengths.publicKey === ML_DSA_65.publicKeySize);
      record('secretKey length == 4032', ml_dsa65.lengths.secretKey === ML_DSA_65.secretKeySize);

      const kp = ml_dsa65.keygen();
      const pk = Buffer.from(kp.publicKey);
      const sk = Buffer.from(kp.secretKey);
      record('generated publicKey is 1952 bytes', pk.length === ML_DSA_65.publicKeySize);
      record('generated secretKey is 4032 bytes', sk.length === ML_DSA_65.secretKeySize);

      const msg = Buffer.from('AEGIS ML-DSA-65 self-test message', 'utf8');
      const sig = this.signWith(sk, msg, { deterministic: true });
      record('signature is 3309 bytes', sig.length === ML_DSA_65.signatureSize);

      // Deterministic signing is reproducible.
      const sig2 = this.signWith(sk, msg, { deterministic: true });
      record('deterministic signing reproducible', sig.equals(sig2));

      // Positive: genuine signature verifies.
      record('valid signature verifies (true)', this.verifyWith(pk, msg, sig) === true);

      // Negatives: tamper is rejected.
      const badMsg = Buffer.from('AEGIS ML-DSA-65 self-test messagX', 'utf8');
      record('tampered message rejected (false)', this.verifyWith(pk, badMsg, sig) === false);

      const badSig = Buffer.from(sig);
      badSig[100] ^= 0xff;
      record('tampered signature rejected (false)', this.verifyWith(pk, msg, badSig) === false);

      const otherPk = Buffer.from(ml_dsa65.keygen().publicKey);
      record('wrong public key rejected (false)', this.verifyWith(otherPk, msg, sig) === false);
    } catch (e: any) {
      record(`self-test threw: ${String(e?.message ?? e).slice(0, 80)}`, false);
    }
    return { passed: checks.every((c) => c.ok), checks };
  }
}

// Singleton instance (audit/attestation signing key for this node).
let dsaEngineInstance: MLDSAEngine | null = null;

export function getPQCSignatureEngine(): MLDSAEngine {
  if (!dsaEngineInstance) {
    dsaEngineInstance = new MLDSAEngine();
    dsaEngineInstance.generateKeyPair();
  }
  return dsaEngineInstance;
}
