import { readFileSync } from 'node:fs';
import { createPrivateKey, type KeyObject } from 'node:crypto';

/**
 * Boot-time validators for the BFF's JWKS publishing material, per
 * [ADR-0014](../../../../docs/decisions/0014-downstream-api-access-obo-pattern.md)
 * §"Service strategy (non-Entra downstreams)":
 *
 *   "The downstream verifies the signature against the BFF's
 *    published JWKS at `/.well-known/jwks.json`"
 *
 * Two env vars together describe the BFF's signing identity:
 *
 *   - `BFF_JWKS_PRIVATE_KEY_PATH` — filesystem path to a PEM-encoded
 *     private key (RSA-2048 or stronger, or EC P-256 / P-384). The
 *     file is read once at boot; the BFF refuses to start if it's
 *     missing, unreadable, or the key is weak.
 *   - `BFF_JWKS_KID` — stable identifier published in the JWKS and
 *     emitted in the JWT header so a downstream can pick the right
 *     key when rotation introduces a second entry. URL-safe charset
 *     (`[A-Za-z0-9_-]+`) so it lives unescaped in JWT headers.
 *
 * Returns the parsed `KeyObject` (private) + kid so the application
 * layer never re-reads the file at request time — a perf wart given
 * how often `X-User-Assertion` JWTs get minted on a busy downstream
 * call site.
 */

const KID_RE = /^[A-Za-z0-9_-]{4,128}$/;

export interface JwksConfig {
  /** PEM-parsed private key, ready for `jose.SignJWT` consumption. */
  readonly privateKey: KeyObject;
  /** Stable identifier published in the JWKS + the JWT `kid` header. */
  readonly kid: string;
  /**
   * JOSE algorithm derived from the key type. RSA → RS256;
   * EC → ES256 / ES384 depending on curve. Kept on the config so
   * the strategy doesn't have to re-derive it on every sign call.
   */
  readonly alg: 'RS256' | 'ES256' | 'ES384';
}

export function assertJwksConfig(): JwksConfig {
  const path = process.env['BFF_JWKS_PRIVATE_KEY_PATH'];
  if (path === undefined || path === '') {
    throw new Error(
      `BFF_JWKS_PRIVATE_KEY_PATH is not set. Generate a key with ` +
        `"openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out apps/portal-bff/.secrets/jwks.pem" ` +
        `and set BFF_JWKS_PRIVATE_KEY_PATH to its path. Required for the ADR-0014 ` +
        `signed-assertion strategy.`,
    );
  }

  let pem: string;
  try {
    pem = readFileSync(path, 'utf8');
  } catch (err) {
    throw new Error(
      `BFF_JWKS_PRIVATE_KEY_PATH could not be read at "${path}": ` +
        `${err instanceof Error ? err.message : String(err)}`,
    );
  }

  let privateKey: KeyObject;
  try {
    privateKey = createPrivateKey(pem);
  } catch (err) {
    throw new Error(
      `BFF_JWKS_PRIVATE_KEY_PATH ("${path}") is not a valid PEM private key: ` +
        `${err instanceof Error ? err.message : String(err)}`,
    );
  }

  const alg = deriveAlg(privateKey);
  if (alg === null) {
    throw new Error(
      `BFF_JWKS_PRIVATE_KEY_PATH ("${path}") uses an unsupported key type ` +
        `("${privateKey.asymmetricKeyType ?? 'unknown'}"). Use RSA-2048+ or EC ` +
        `(P-256, P-384). Symmetric / Ed25519 keys are not accepted in v1.`,
    );
  }

  const rawKid = process.env['BFF_JWKS_KID'];
  if (rawKid === undefined || rawKid === '') {
    throw new Error(
      `BFF_JWKS_KID is not set. Pick a stable identifier for the key (per ADR-0014 ` +
        `§"Configuration") — e.g. "bff-2026-05" — and put it in apps/portal-bff/.env. ` +
        `URL-safe charset only ([A-Za-z0-9_-]).`,
    );
  }
  if (!KID_RE.test(rawKid)) {
    throw new Error(
      `BFF_JWKS_KID must match ${KID_RE.source} (URL-safe, 4–128 chars). Got: ${rawKid}`,
    );
  }

  return { privateKey, kid: rawKid, alg };
}

function deriveAlg(key: KeyObject): JwksConfig['alg'] | null {
  if (key.asymmetricKeyType === 'rsa') {
    // RSA modulus length is in bits. `asymmetricKeyDetails.modulusLength` is
    // the canonical accessor on Node 16+.
    const modulus = key.asymmetricKeyDetails?.modulusLength;
    if (modulus === undefined || modulus < 2048) {
      // Weak key — refuse rather than silently sign with it.
      return null;
    }
    return 'RS256';
  }
  if (key.asymmetricKeyType === 'ec') {
    const curve = key.asymmetricKeyDetails?.namedCurve;
    if (curve === 'prime256v1' || curve === 'P-256') return 'ES256';
    if (curve === 'secp384r1' || curve === 'P-384') return 'ES384';
    return null;
  }
  return null;
}
