/**
 * Artifact at-rest encryption — Phase 3 (RD-2).
 *
 * Per-org HKDF-derived Data Encryption Keys (DEKs) using the master key as IKM.
 * Disk format: [12-byte IV][16-byte GCM auth tag][ciphertext...]
 *
 * When MADARX_ARTIFACT_ENCRYPTION=1, createArtifact() writes encrypted blobs.
 * The `artifacts.encrypted` row flag (not the env var) governs decryption —
 * so mixed plaintext / encrypted rows coexist safely across env-var flips.
 *
 * Master key rotation is out of scope; documented as an operational procedure.
 */

import { createHash, hkdfSync, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';
import { resolveMasterKey } from './master-key.js';

const DEK_INFO = Buffer.from('madarx-artifact-dek-v1');

// Evaluated once at module load. Callers should use isEncryptionEnabled() rather
// than reading the env var directly so this module is the single source of truth.
const ENC_FLAG = process.env.MADARX_ARTIFACT_ENCRYPTION === '1';

/**
 * Returns true when MADARX_ARTIFACT_ENCRYPTION=1.
 * Only affects NEW writes — existing rows use their `encrypted` column flag.
 */
export function isEncryptionEnabled() {
  return ENC_FLAG;
}

/**
 * Derive a 32-byte AES-256 Data Encryption Key for the given org.
 *
 * Uses HKDF-SHA256:
 *   IKM  = master key (32 bytes, AES-256)
 *   salt = SHA-256(orgId) — domain-separates keys between orgs
 *   info = 'madarx-artifact-dek-v1' — version-pins the derivation
 *
 * @param {string} orgId - Organisation identifier (e.g. 'personal-org')
 * @returns {Buffer} 32-byte DEK
 */
export function deriveOrgDek(orgId) {
  if (!orgId) throw new Error('deriveOrgDek requires orgId');
  const salt = createHash('sha256').update(orgId).digest();
  const ikm = resolveMasterKey({ allowGenerate: true });
  // hkdfSync returns ArrayBuffer in Node — wrap in Buffer.
  return Buffer.from(hkdfSync('sha256', ikm, salt, DEK_INFO, 32));
}

/**
 * Encrypt a plaintext buffer or string with AES-256-GCM using the org DEK.
 *
 * @param {string} orgId
 * @param {Buffer|string} plaintext
 * @returns {{ ciphertext: Buffer, iv: Buffer, authTag: Buffer }}
 */
export function encryptArtifact(orgId, plaintext) {
  const dek = deriveOrgDek(orgId);
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', dek, iv);
  const buf = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext, 'utf8');
  const ciphertext = Buffer.concat([cipher.update(buf), cipher.final()]);
  return { ciphertext, iv, authTag: cipher.getAuthTag() };
}

/**
 * Decrypt ciphertext encrypted by encryptArtifact().
 *
 * @param {string} orgId
 * @param {Buffer} ciphertext
 * @param {Buffer} iv        12 bytes
 * @param {Buffer} authTag   16 bytes
 * @returns {Buffer} plaintext
 */
export function decryptArtifact(orgId, ciphertext, iv, authTag) {
  const dek = deriveOrgDek(orgId);
  const decipher = createDecipheriv('aes-256-gcm', dek, iv);
  decipher.setAuthTag(authTag);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

/**
 * Compose the canonical disk format and return a single Buffer:
 *   [12-byte IV][16-byte auth tag][ciphertext...]
 *
 * @param {string} orgId
 * @param {Buffer|string} plaintext
 * @returns {Buffer}
 */
export function encryptToDiskBuffer(orgId, plaintext) {
  const { ciphertext, iv, authTag } = encryptArtifact(orgId, plaintext);
  return Buffer.concat([iv, authTag, ciphertext]);
}

/**
 * Parse and decrypt a disk-format Buffer produced by encryptToDiskBuffer().
 *
 * @param {string} orgId
 * @param {Buffer} diskBuf
 * @returns {Buffer} plaintext
 * @throws {Error} on GCM auth-tag mismatch, wrong orgId, or corrupt data
 */
export function decryptFromDiskBuffer(orgId, diskBuf) {
  if (!Buffer.isBuffer(diskBuf) || diskBuf.length < 28) {
    throw new Error('Invalid encrypted artifact: too short');
  }
  const iv = diskBuf.subarray(0, 12);
  const authTag = diskBuf.subarray(12, 28);
  const ciphertext = diskBuf.subarray(28);
  return decryptArtifact(orgId, ciphertext, iv, authTag);
}
