/**
 * AES-256-GCM at-rest encryption for sensitive settings (API keys, SMTP
 * credentials, CRM auth tokens).
 *
 * Key source: `APP_ENCRYPTION_KEY` env-var, must be a 64-character hex string
 * (= 32 bytes). Generate via: `openssl rand -hex 32`.
 *
 * Storage format (after base64-encoding):
 *   [12-byte IV][16-byte authTag][ciphertext]
 *
 * GCM gives us authenticated encryption — any tampering produces a decryption
 * error. We surface that as `EncryptionError('integrity')` so callers can log
 * and refuse to use the value rather than silently returning garbage.
 */

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const TAG_LENGTH = 16;

export type EncryptionErrorKind = 'config' | 'integrity' | 'format';

export class EncryptionError extends Error {
  readonly kind: EncryptionErrorKind;
  constructor(kind: EncryptionErrorKind, message: string) {
    super(message);
    this.kind = kind;
    this.name = 'EncryptionError';
  }
}

let cachedKey: Buffer | null = null;

function getKey(): Buffer {
  if (cachedKey) return cachedKey;
  const hex = process.env.APP_ENCRYPTION_KEY?.trim();
  if (!hex) {
    throw new EncryptionError(
      'config',
      'APP_ENCRYPTION_KEY is not set — generate via `openssl rand -hex 32` and put it in .env.local',
    );
  }
  if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
    throw new EncryptionError(
      'config',
      'APP_ENCRYPTION_KEY must be exactly 64 hex characters (= 32 bytes). Got ' + hex.length + ' chars.',
    );
  }
  cachedKey = Buffer.from(hex, 'hex');
  return cachedKey;
}

export function encrypt(plaintext: string): string {
  const key = getKey();
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return Buffer.concat([iv, authTag, ciphertext]).toString('base64');
}

export function decrypt(payload: string): string {
  const key = getKey();
  let buf: Buffer;
  try {
    buf = Buffer.from(payload, 'base64');
  } catch {
    throw new EncryptionError('format', 'payload is not valid base64');
  }
  if (buf.length < IV_LENGTH + TAG_LENGTH + 1) {
    throw new EncryptionError('format', 'payload too short to contain IV + authTag + ciphertext');
  }
  const iv = buf.subarray(0, IV_LENGTH);
  const authTag = buf.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
  const ciphertext = buf.subarray(IV_LENGTH + TAG_LENGTH);

  const decipher = createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  try {
    const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
    return plaintext.toString('utf8');
  } catch (err) {
    throw new EncryptionError(
      'integrity',
      'decryption failed — APP_ENCRYPTION_KEY may have changed, or the value was tampered with. ' +
        (err instanceof Error ? err.message : String(err)),
    );
  }
}

/**
 * Test if encryption is available (env-var present and valid). Used by the
 * Settings UI to decide whether to even show secret-input fields.
 */
export function isEncryptionAvailable(): boolean {
  try {
    getKey();
    return true;
  } catch {
    return false;
  }
}
