/**
 * Node.js Crypto Provider
 *
 * Implementation using Node.js native crypto module.
 */

import crypto from 'node:crypto';

import { isRsaPssAlg, jwtAlgToNodeAlg } from './jwt-alg';
import type { CryptoProvider } from './types';

export { isRsaPssAlg, jwtAlgToNodeAlg } from './jwt-alg';

/**
 * Convert Node.js Buffer to Uint8Array.
 */
function toUint8Array(buf: Buffer): Uint8Array {
  return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
}

/**
 * Normalize input to Buffer for Node.js crypto operations.
 */
function toBuffer(data: string | Uint8Array): Buffer {
  if (typeof data === 'string') {
    return Buffer.from(data, 'utf8');
  }
  return Buffer.from(data);
}

/**
 * Node.js crypto provider implementation.
 */
export const nodeCrypto: CryptoProvider = {
  randomUUID(): string {
    return crypto.randomUUID();
  },

  randomBytes(length: number): Uint8Array {
    return toUint8Array(crypto.randomBytes(length));
  },

  sha256(data: string | Uint8Array): Uint8Array {
    const hash = crypto.createHash('sha256').update(toBuffer(data)).digest();
    return toUint8Array(hash);
  },

  sha256Hex(data: string | Uint8Array): string {
    return crypto.createHash('sha256').update(toBuffer(data)).digest('hex');
  },

  hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
    const hmac = crypto.createHmac('sha256', Buffer.from(key)).update(Buffer.from(data)).digest();
    return toUint8Array(hmac);
  },

  hkdfSha256(ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, length: number): Uint8Array {
    // Implement HKDF-SHA256 (RFC 5869)
    const ikmBuf = Buffer.from(ikm);
    const saltBuf = salt.length > 0 ? Buffer.from(salt) : Buffer.alloc(32); // Default salt if empty

    // Extract phase: PRK = HMAC-Hash(salt, IKM)
    const prk = crypto.createHmac('sha256', saltBuf).update(ikmBuf).digest();

    // Expand phase
    const hashLen = 32; // SHA-256 output length
    const n = Math.ceil(length / hashLen);
    const chunks: Buffer[] = [];
    let prev: Buffer = Buffer.alloc(0);

    for (let i = 1; i <= n; i++) {
      prev = crypto
        .createHmac('sha256', prk)
        .update(Buffer.concat([prev, Buffer.from(info), Buffer.from([i])]))
        .digest();
      chunks.push(prev);
    }

    return toUint8Array(Buffer.concat(chunks).subarray(0, length));
  },

  encryptAesGcm(key: Uint8Array, plaintext: Uint8Array, iv: Uint8Array): { ciphertext: Uint8Array; tag: Uint8Array } {
    const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key), Buffer.from(iv));
    const encrypted = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
    const tag = cipher.getAuthTag();

    return {
      ciphertext: toUint8Array(encrypted),
      tag: toUint8Array(tag),
    };
  },

  decryptAesGcm(key: Uint8Array, ciphertext: Uint8Array, iv: Uint8Array, tag: Uint8Array): Uint8Array {
    const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(key), Buffer.from(iv));
    decipher.setAuthTag(Buffer.from(tag));
    const decrypted = Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]);

    return toUint8Array(decrypted);
  },

  timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
    if (a.length !== b.length) return false;
    return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
  },
};

/** Alias for conditional import resolution via `#crypto-provider`. */
export { nodeCrypto as cryptoProvider };

// ═══════════════════════════════════════════════════════════════════
// RSA KEY UTILITIES (Node.js only)
// ═══════════════════════════════════════════════════════════════════

/**
 * RSA JWK structure for public keys
 */
export interface RsaJwk {
  kty: 'RSA';
  kid: string;
  alg: string;
  use: 'sig';
  n: string;
  e: string;
}

/**
 * RSA key pair structure
 */
export interface RsaKeyPair {
  /** Private key for signing */
  privateKey: crypto.KeyObject;
  /** Public key for verification */
  publicKey: crypto.KeyObject;
  /** Public key in JWK format */
  publicJwk: RsaJwk;
}

/**
 * Generate an RSA key pair with the specified modulus length
 *
 * @param modulusLength - Key size in bits (default: 2048, suitable for short-lived OAuth/JWT verification; use 3072+ for longer-term keys)
 * @param alg - JWT algorithm (default: 'RS256')
 * @returns RSA key pair with private, public keys and JWK
 */
export function generateRsaKeyPair(modulusLength = 2048, alg = 'RS256'): RsaKeyPair {
  const kid = `rsa-key-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;

  const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength,
  });

  const exported = publicKey.export({ format: 'jwk' }) as {
    n: string;
    e: string;
  };
  const publicJwk: RsaJwk = {
    ...exported,
    kid,
    alg,
    use: 'sig',
    kty: 'RSA',
  };

  return { privateKey, publicKey, publicJwk };
}

/**
 * Sign data using RSA with the specified algorithm.
 *
 * For RSA-PSS (PS256/PS384/PS512), callers must pass appropriate padding/saltLength options.
 */
export function rsaSign(
  algorithm: string,
  data: Buffer,
  privateKey: crypto.KeyObject,
  options?: Omit<crypto.SignKeyObjectInput, 'key'>,
): Buffer {
  const signingKey: crypto.KeyObject | crypto.SignKeyObjectInput = options
    ? { key: privateKey, ...options }
    : privateKey;
  return crypto.sign(algorithm, data, signingKey);
}

export function rsaVerify(jwtAlg: string, data: Buffer, publicJwk: JsonWebKey, signature: Buffer): boolean {
  const publicKey = crypto.createPublicKey({ key: publicJwk as crypto.JsonWebKey, format: 'jwk' });
  const nodeAlgorithm = jwtAlgToNodeAlg(jwtAlg);
  const verifyKey: crypto.KeyObject | crypto.VerifyKeyObjectInput = isRsaPssAlg(jwtAlg)
    ? {
        key: publicKey,
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
        saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
      }
    : publicKey;
  return crypto.verify(nodeAlgorithm, data, verifyKey, signature);
}

/**
 * Sign data with an RSA private JWK and return a base64url-encoded signature.
 *
 * Mirror of {@link rsaVerify} on the signing side: takes a JWT alg name
 * (`RS256`/`RS384`/`RS512`/`PS256`/`PS384`/`PS512`), a private key in JWK
 * format, and the data bytes; returns the detached signature as a base64url
 * string suitable for inline JSON envelopes (e.g. tamper-evident audit
 * records that re-use the same key registry as bundle signing).
 *
 * Node-only — `crypto.createPrivateKey({ format: 'jwk' })` is not exposed in
 * WebCrypto with the same shape, so audit signing is gated to Node runtimes.
 *
 * @param jwtAlg - JWT algorithm identifier (e.g. 'RS256', 'PS256')
 * @param data - Bytes to sign
 * @param privateJwk - Private key in JWK format
 * @returns Base64url-encoded signature
 */
export function rsaSignBase64Url(jwtAlg: string, data: Buffer | Uint8Array, privateJwk: JsonWebKey): string {
  const privateKey = crypto.createPrivateKey({ key: privateJwk as crypto.JsonWebKey, format: 'jwk' });
  const nodeAlgorithm = jwtAlgToNodeAlg(jwtAlg);
  const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
  const sig = isRsaPssAlg(jwtAlg)
    ? rsaSign(nodeAlgorithm, buf, privateKey, {
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
        saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
      })
    : rsaSign(nodeAlgorithm, buf, privateKey);
  return sig.toString('base64url');
}

/**
 * Verify an RSA / RSA-PSS / EdDSA signature synchronously.
 *
 * Node-only — mirrors {@link rsaSignBase64Url}'s sync, JWK-first surface so
 * server-side callers (audit signer verifier, bundle signature verifier)
 * have a single utility to call instead of touching `node:crypto` directly.
 *
 * - RS256/RS384/RS512 / PS256/PS384/PS512: pass `jwtAlg` and an RSA JWK.
 * - EdDSA: pass `'EdDSA'` and an Ed25519 JWK (`kty: 'OKP'`, `crv: 'Ed25519'`).
 *
 * Returns `false` (never throws) for malformed keys / unsupported algs so
 * callers can surface a single "verification failed" reason instead of
 * branching on every internal crypto error.
 *
 * @param jwtAlg - JWT algorithm identifier (`'RS256'`, `'PS256'`, `'EdDSA'`, ...)
 * @param data - Bytes that were signed
 * @param publicJwk - Public key in JWK format
 * @param signature - Signature bytes (raw, NOT base64url)
 */
export function rsaVerifySync(
  jwtAlg: string,
  data: Buffer | Uint8Array,
  publicJwk: JsonWebKey,
  signature: Buffer | Uint8Array,
): boolean {
  try {
    const publicKey = crypto.createPublicKey({ key: publicJwk as crypto.JsonWebKey, format: 'jwk' });
    const dataBuf = Buffer.isBuffer(data) ? data : Buffer.from(data);
    const sigBuf = Buffer.isBuffer(signature) ? signature : Buffer.from(signature);
    if (jwtAlg === 'EdDSA') {
      // Ed25519 / Ed448: Node uses `null` as the algorithm name; the digest is
      // internal to the curve operation.
      return crypto.verify(null, dataBuf, publicKey, sigBuf);
    }
    const nodeAlgorithm = jwtAlgToNodeAlg(jwtAlg);
    const verifyKey: crypto.KeyObject | crypto.VerifyKeyObjectInput = isRsaPssAlg(jwtAlg)
      ? {
          key: publicKey,
          padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
          saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
        }
      : publicKey;
    return crypto.verify(nodeAlgorithm, dataBuf, verifyKey, sigBuf);
  } catch {
    return false;
  }
}

/**
 * Convert a PEM-encoded public key (SPKI) to a JWK.
 *
 * Node-only. Used by host code that has historically configured trust roots
 * as PEMs (bundle-signature trust list, audit verifier trust list) so the
 * verifier path can normalize to JWK before calling {@link rsaVerifySync}.
 *
 * Throws if the PEM cannot be parsed — callers should catch and translate
 * into a structured "malformed_public_key" reason where appropriate.
 */
export function pemToPublicJwk(pem: string): JsonWebKey {
  const key = crypto.createPublicKey({ key: pem, format: 'pem' });
  return key.export({ format: 'jwk' }) as JsonWebKey;
}

/**
 * Create a JWT signed with an RSA key
 *
 * @param payload - JWT payload
 * @param privateKey - RSA private key
 * @param kid - Key ID for the JWT header
 * @param alg - JWT algorithm (default: 'RS256')
 * @returns Signed JWT string
 */
export function createSignedJwt(
  payload: Record<string, unknown>,
  privateKey: crypto.KeyObject,
  kid: string,
  alg = 'RS256',
): string {
  const header = { alg, typ: 'JWT', kid };
  const headerB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
  const payloadB64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
  const signatureInput = `${headerB64}.${payloadB64}`;

  const nodeAlgorithm = jwtAlgToNodeAlg(alg);
  const signature = rsaSign(
    nodeAlgorithm,
    Buffer.from(signatureInput),
    privateKey,
    isRsaPssAlg(alg)
      ? {
          padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
          saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
        }
      : undefined,
  );
  const signatureB64 = signature.toString('base64url');

  return `${headerB64}.${payloadB64}.${signatureB64}`;
}
