import crypto from 'node:crypto';
import { count, createDb, desc, eq, encryptionKeys, secrets, sql } from '@bmm/db';
import { config } from '../config.js';

const ALGO = 'aes-256-gcm';
const db = createDb();

/**
 * Envelope encryption.
 *
 *   KEK  — Key Encryption Key. 32 bytes from env (SECRETS_ENCRYPTION_KEY).
 *          Never stored in the database. The root of trust.
 *   DEK  — Data Encryption Key. 32 random bytes, generated by us, stored in
 *          the encryption_keys table *wrapped* (AES-256-GCM encrypted) with
 *          the KEK. Secrets are encrypted with the DEK.
 *
 * Rotation mints a fresh DEK and re-encrypts every secret under it, so a
 * suspected DEK compromise is recoverable without ever touching the KEK.
 * Legacy secrets (keyId = null) were encrypted directly with the KEK before
 * envelope encryption existed; decryptSecret handles them, and the first
 * rotation migrates them onto a DEK.
 */

function getKEK(): Buffer {
  const buf = Buffer.from(config.SECRETS_ENCRYPTION_KEY, 'hex');
  if (buf.length !== 32) {
    throw new Error('SECRETS_ENCRYPTION_KEY must be 32 bytes (64 hex chars)');
  }
  return buf;
}

// Low-level AES-256-GCM with an explicit key. Payload: iv.tag.ciphertext (base64).
function aesEncrypt(key: Buffer, plaintext: string): string {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv(ALGO, key, iv);
  const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `${iv.toString('base64')}.${tag.toString('base64')}.${enc.toString('base64')}`;
}

function aesDecrypt(key: Buffer, payload: string): string {
  const [ivB64, tagB64, encB64] = payload.split('.');
  if (!ivB64 || !tagB64 || !encB64) throw new Error('malformed_ciphertext');
  const decipher = crypto.createDecipheriv(ALGO, key, Buffer.from(ivB64, 'base64'));
  decipher.setAuthTag(Buffer.from(tagB64, 'base64'));
  return Buffer.concat([decipher.update(Buffer.from(encB64, 'base64')), decipher.final()]).toString(
    'utf8',
  );
}

// In-memory DEK cache: encryption_keys.id -> raw 32-byte DEK.
const dekCache = new Map<string, Buffer>();
let activeKeyId: string | null = null;

async function loadKeys(): Promise<void> {
  const rows = await db.select().from(encryptionKeys);
  const kek = getKEK();
  dekCache.clear();
  activeKeyId = null;
  for (const row of rows) {
    // wrappedDek decrypts to the base64 of the 32-byte DEK
    const dek = Buffer.from(aesDecrypt(kek, row.wrappedDek), 'base64');
    if (dek.length !== 32) throw new Error(`corrupt DEK for key version ${row.version}`);
    dekCache.set(row.id, dek);
    if (row.active) activeKeyId = row.id;
  }
}

/**
 * Boot-time: load existing keys and, if there is no active key yet, create
 * version 1. Must run before any encryptSecret call. Fail-closed: if this
 * throws, the API must not start.
 */
export async function ensureActiveKey(): Promise<void> {
  await loadKeys();
  if (activeKeyId) return;
  const dek = crypto.randomBytes(32);
  const wrapped = aesEncrypt(getKEK(), dek.toString('base64'));
  const [row] = await db
    .insert(encryptionKeys)
    .values({ version: 1, wrappedDek: wrapped, active: true })
    .returning();
  if (!row) throw new Error('failed to create initial encryption key');
  dekCache.set(row.id, dek);
  activeKeyId = row.id;
}

export interface EncryptResult {
  value: string;
  keyId: string;
}

export function encryptSecret(plaintext: string): EncryptResult {
  if (!activeKeyId) {
    throw new Error('encryption not initialized — ensureActiveKey() must run at boot');
  }
  const dek = dekCache.get(activeKeyId);
  if (!dek) throw new Error('active DEK missing from cache');
  return { value: aesEncrypt(dek, plaintext), keyId: activeKeyId };
}

export function decryptSecret(value: string, keyId: string | null): string {
  if (!keyId) {
    // Legacy: encrypted directly with the KEK before envelope encryption.
    return aesDecrypt(getKEK(), value);
  }
  const dek = dekCache.get(keyId);
  if (!dek) throw new Error(`unknown encryption key id: ${keyId}`);
  return aesDecrypt(dek, value);
}

export interface RotateResult {
  newVersion: number;
  reEncrypted: number;
}

/**
 * Mint a fresh DEK and re-encrypt every secret under it in a single
 * transaction. Legacy (KEK-direct) secrets are migrated in the same pass.
 */
export async function rotateKeys(rotatedBy: string): Promise<RotateResult> {
  await loadKeys();

  const newDek = crypto.randomBytes(32);
  const wrapped = aesEncrypt(getKEK(), newDek.toString('base64'));
  const [{ maxV } = { maxV: 0 }] = await db
    .select({ maxV: sql<number>`coalesce(max(${encryptionKeys.version}), 0)` })
    .from(encryptionKeys);
  const nextVersion = Number(maxV) + 1;

  const reEncrypted = await db.transaction(async (tx) => {
    const [newKey] = await tx
      .insert(encryptionKeys)
      .values({ version: nextVersion, wrappedDek: wrapped, active: false, rotatedBy })
      .returning();
    if (!newKey) throw new Error('failed to insert new encryption key');

    const all = await tx.select().from(secrets);
    for (const s of all) {
      const plain = decryptSecret(s.encryptedValue, s.keyId);
      await tx
        .update(secrets)
        .set({ encryptedValue: aesEncrypt(newDek, plain), keyId: newKey.id })
        .where(eq(secrets.id, s.id));
    }

    await tx
      .update(encryptionKeys)
      .set({ active: false, retiredAt: new Date() })
      .where(eq(encryptionKeys.active, true));
    await tx.update(encryptionKeys).set({ active: true }).where(eq(encryptionKeys.id, newKey.id));

    return { count: all.length, keyId: newKey.id };
  });

  // Commit succeeded — update the in-memory cache.
  dekCache.set(reEncrypted.keyId, newDek);
  activeKeyId = reEncrypted.keyId;

  return { newVersion: nextVersion, reEncrypted: reEncrypted.count };
}

export interface EncryptionStatus {
  activeVersion: number | null;
  keyCount: number;
  secretCount: number;
  legacySecretCount: number;
  keys: {
    version: number;
    active: boolean;
    createdAt: Date;
    retiredAt: Date | null;
  }[];
}

export async function encryptionStatus(): Promise<EncryptionStatus> {
  const keys = await db.select().from(encryptionKeys).orderBy(desc(encryptionKeys.version));
  const [{ c: secretCount } = { c: 0 }] = await db.select({ c: count() }).from(secrets);
  const [{ c: legacy } = { c: 0 }] = await db
    .select({ c: count() })
    .from(secrets)
    .where(sql`${secrets.keyId} is null`);
  return {
    activeVersion: keys.find((k) => k.active)?.version ?? null,
    keyCount: keys.length,
    secretCount: Number(secretCount),
    legacySecretCount: Number(legacy),
    keys: keys.map((k) => ({
      version: k.version,
      active: k.active,
      createdAt: k.createdAt,
      retiredAt: k.retiredAt,
    })),
  };
}
