import { randomBytes } from 'crypto';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { resolve } from 'path';
import Logger from '#utils/logger';

/**
 * OIDC Signing Key Rotation Job
 *
 * Rotates RSA key pairs used by oidc-provider for signing ID tokens and JWT access tokens.
 * Implements zero-downtime rotation using a two-phase approach:
 *
 *   Phase 1 (Day 0): Add new key to keystore
 *     → New tokens signed with new key, old tokens still verifiable with old key
 *   Phase 2 (Day 1): Remove old key from keystore
 *     → All tokens now signed with new key
 *
 * Key rotation is triggered via a REST endpoint or cron.
 * The job stores keys in a file so they persist across restarts.
 *
 * RATIONALE:
 * - oidc-provider generates ephemeral keys by default (lost on restart)
 * - Storing keys in Redis allows sharing across multiple instances (HA)
 * - This job supports both file-based (single instance) and Redis-based (HA) storage
 */

const KEYS_DIR = resolve(process.cwd(), 'config', 'keys');
const KEY_FILE = resolve(KEYS_DIR, 'keystore.json');

interface KeyEntry {
	kid: string;
	alg: string;
	use: string;
	kty: string;
	n: string;
	e: string;
	d: string | undefined;
	p: string | undefined;
	q: string | undefined;
	dp: string | undefined;
	dq: string | undefined;
	qi: string | undefined;
		createdAt: string;
		rotatedAt?: string;
}

interface KeystoreData {
	keys: KeyEntry[];
	activeKid: string;
	pendingRetirementKid?: string;
	retirementDeadline?: string;
}

// ── Helpers ──────────────────────────────────────────────────────────────────

function ensureKeysDir(): void {
	if (!existsSync(KEYS_DIR)) {
		mkdirSync(KEYS_DIR, { recursive: true });
		Logger.info(`[SecretRotation] Created keys directory: ${KEYS_DIR}`);
	}
}

function loadKeystore(): KeystoreData {
	ensureKeysDir();
	if (!existsSync(KEY_FILE)) {
		return { keys: [], activeKid: '' } as KeystoreData;
	}
	try {
		return JSON.parse(readFileSync(KEY_FILE, 'utf8')) as KeystoreData;
	} catch {
		Logger.warn('[SecretRotation] Corrupted keystore — starting fresh');
		return { keys: [], activeKid: '' } as KeystoreData;
	}
}

function saveKeystore(data: KeystoreData): void {
	ensureKeysDir();
	writeFileSync(KEY_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
}

/** Generate a new RSA key pair and return it as a JWK */
function generateRsaKeyPair(): KeyEntry {
	const { publicKey, privateKey } = require('crypto').generateKeyPairSync('rsa', {
		modulusLength: 2048, // 2048-bit minimum; 4096 recommended for production
		publicKeyEncoding: { type: 'spki', format: 'jwk' },
		privateKeyEncoding: { type: 'pkcs8', format: 'jwk' },
	});

	const pub = publicKey as KeyEntry;
	const priv = privateKey as KeyEntry;
	const kid = randomBytes(8).toString('hex');

	// JWK private components are strings when generated correctly; assert non-null
	return {
		kid,
		alg: 'RS256',
		use: 'sig',
		kty: pub.kty,
		n: pub.n,
		e: pub.e,
		d: priv.d ?? undefined,
		p: priv.p ?? undefined,
		q: priv.q ?? undefined,
		dp: priv.dp ?? undefined,
		dq: priv.dq ?? undefined,
		qi: priv.qi ?? undefined,
		createdAt: new Date().toISOString(),
	};
}

// ── Core rotation logic ───────────────────────────────────────────────────────

/**
 * Rotate to a new signing key.
 *
 * If no key exists: generates and activates the first key (no old key to retire).
 * If a key already exists: adds new key, marks old key for retirement after GRACE_PERIOD.
 * The retirement is only executed when retireOldKeys() is called.
 */
export async function rotateSigningKey(): Promise<{ newKid: string; retiredKid: string | undefined }> {
	const data = loadKeystore();
	const now = new Date();
	const GRACE_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours

	// ── Generate new key ────────────────────────────────────────────────────────
	const newKey = generateRsaKeyPair();
	const newKid = newKey.kid;
	data.keys.push(newKey);

	// ── Retire old key (if any) ────────────────────────────────────────────────
	let retiredKid: string | undefined;
	if (data.activeKid && data.activeKid !== newKid) {
		const oldKey = data.keys.find((k) => k.kid === data.activeKid);
		if (oldKey) {
			oldKey.rotatedAt = now.toISOString();
			// Mark as pending retirement — will be removed after grace period
			data.pendingRetirementKid = oldKey.kid;
			data.retirementDeadline = new Date(now.getTime() + GRACE_PERIOD_MS).toISOString();
			Logger.info(`[SecretRotation] Key ${oldKey.kid} marked for retirement at ${data.retirementDeadline}`);
		}
		retiredKid = data.activeKid;
	}

	data.activeKid = newKid;
	saveKeystore(data);

	Logger.info(`[SecretRotation] New signing key active: kid=${newKid}`);
	return { newKid, retiredKid };
}

/**
 * Remove keys that have passed their retirement deadline.
 * Should be called periodically (e.g., every hour via cron).
 * Only removes keys that have been rotated AND have passed the grace period.
 */
export async function retireOldKeys(): Promise<string[]> {
	const data = loadKeystore();
	const now = new Date();
	const removed: string[] = [];

	if (!data.pendingRetirementKid || !data.retirementDeadline) {
		return removed;
	}

	const deadline = new Date(data.retirementDeadline);
	if (now >= deadline) {
		const kid = data.pendingRetirementKid;
		const idx = data.keys.findIndex((k) => k.kid === kid);
		if (idx !== -1) {
			data.keys.splice(idx, 1);
			removed.push(kid);
			Logger.info(`[SecretRotation] Retired old signing key: kid=${kid}`);
		}
		delete data.pendingRetirementKid;
		delete data.retirementDeadline;
		saveKeystore(data);
	}

	return removed;
}

/**
 * Get the current active key in JWK format.
 * oidc-provider uses this to load the signing key from disk on startup.
 */
export function getActiveKey(): KeyEntry | null {
	const data = loadKeystore();
	return data.keys.find((k) => k.kid === data.activeKid) || null;
}

/**
 * Get all valid keys (active + pending retirement) for the JWKS endpoint.
 * Clients use these to verify token signatures.
 */
export function getValidKeys(): KeyEntry[] {
	const data = loadKeystore();
	return data.keys.filter((k) => {
		if (k.kid === data.activeKid) return true;
		if (k.kid === data.pendingRetirementKid) return true;
		return false;
	});
}

/**
 * Check if a key is still valid for signature verification.
 */
export function isKeyValid(kid: string): boolean {
	return getValidKeys().some((k) => k.kid === kid);
}

// ── Rotation schedule summary ─────────────────────────────────────────────────
//
// Recommended cron setup:
//   rotate-signing-key: monthly or on-demand via admin API
//   retire-old-keys:    every hour (safe to run — only removes keys past deadline)
//
// Flow when rotation is triggered:
//   T+0h:  rotateSigningKey() adds new key, marks old key for retirement (deadline +24h)
//   T+1h:  retireOldKeys() runs — old key still valid (deadline not reached)
//   T+24h: retireOldKeys() runs — old key now past deadline, removed
//
// This gives all clients 24 hours to fetch the new JWKS and update their verification keys.
