import {
	createPrivateKey,
	createSign,
	randomUUID,
	X509Certificate,
	type KeyObject,
} from 'node:crypto';

import { formatPemBlock } from './format-pem-block';

// private_key_jwt (RFC 7521/7523): the client proves its identity with a JWT
// signed by its private key instead of a shared secret. The `x5t` header (SHA-1
// thumbprint of the certificate) tells the server which public key verifies it.
export const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';

const ASSERTION_TTL_SECONDS = 300;

function base64url(input: Buffer | string): string {
	return Buffer.from(input).toString('base64url');
}

function certificateThumbprint(certificate: string): string {
	let parsed: X509Certificate;
	try {
		parsed = new X509Certificate(formatPemBlock(certificate));
	} catch (error) {
		throw new Error(
			'The Certificate field must contain a PEM certificate (-----BEGIN CERTIFICATE-----).',
			{ cause: error },
		);
	}
	return Buffer.from(parsed.fingerprint.replace(/:/g, ''), 'hex').toString('base64url');
}

export interface BuildClientAssertionOptions {
	clientId: string;
	/** Token endpoint; used as the JWT `aud`. */
	accessTokenUri: string;
	/** RSA private key (PEM). Signing is RS256-only; EC/Ed25519 keys are not supported. */
	privateKey: string;
	certificate: string;
}

export function buildClientAssertion(options: BuildClientAssertionOptions): string {
	const now = Math.floor(Date.now() / 1000);
	const header = { alg: 'RS256', typ: 'JWT', x5t: certificateThumbprint(options.certificate) };
	const payload = {
		aud: options.accessTokenUri,
		iss: options.clientId,
		sub: options.clientId,
		jti: randomUUID(),
		iat: now,
		nbf: now,
		exp: now + ASSERTION_TTL_SECONDS,
	};

	let privateKey: KeyObject;
	try {
		privateKey = createPrivateKey(formatPemBlock(options.privateKey));
	} catch (error) {
		throw new Error(
			'The Private Key field must contain a PEM private key (-----BEGIN PRIVATE KEY-----).',
			{ cause: error },
		);
	}

	// `createSign('RSA-SHA256')` also signs EC/Ed25519 keys, producing a signature
	// that contradicts the pinned `alg: RS256` header. Reject non-RSA keys up front.
	if (privateKey.asymmetricKeyType !== 'rsa') {
		throw new Error('Certificate authentication requires an RSA private key');
	}

	const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
	const signature = createSign('RSA-SHA256').update(signingInput).sign(privateKey);
	return `${signingInput}.${base64url(signature)}`;
}
