/**
 * Shared encryption primitives for at-rest credential storage.
 *
 * Used by:
 *   - services/apiKeyManager.js   (api-keys.enc)
 *   - services/authCacheStore.js  (auth-cache.enc)
 *
 * Both stores derive the same machine-specific AES-256-GCM key from a
 * persistent salt at `<userDataDir>/.key-salt` plus os-derived data.
 * Sharing the cipher primitives keeps the byte layout consistent and
 * means a single review covers credential-at-rest for the whole app.
 *
 * On-disk format (per ciphertext, base64-encoded):
 *   [ IV (16B) | authTag (16B) | ciphertext (variable) ]
 *
 * KEY_DERIVATION_DOMAIN must stay constant — changing it invalidates
 * every existing `api-keys.enc` on every user's machine.
 */

import crypto from 'crypto';
import os from 'os';
import path from 'path';
import { promises as fs } from 'fs';

export const ALGORITHM = 'aes-256-gcm';
export const KEY_LENGTH = 32;        // 256 bits
export const IV_LENGTH = 16;
export const AUTH_TAG_LENGTH = 16;
export const SALT_LENGTH = 32;
export const PBKDF2_ITERATIONS = 100000;
export const SALT_FILENAME = '.key-salt';

/**
 * Domain-separation string mixed into the key derivation input. Must
 * not change without a migration — existing on-disk files would
 * become undecryptable.
 */
export const KEY_DERIVATION_DOMAIN = 'loxia-api-key-encryption-v1';

/**
 * Read or create the per-machine salt and derive the AES key from it
 * combined with stable machine identifiers.
 *
 * @param {string} settingsDir - directory holding `.key-salt`
 * @param {Object} [logger]
 * @returns {Promise<Buffer>} 32-byte AES key
 */
export async function getOrCreateEncryptionKey(settingsDir, logger) {
  const saltFile = path.join(settingsDir, SALT_FILENAME);
  let salt;
  try {
    salt = await fs.readFile(saltFile);
  } catch {
    salt = crypto.randomBytes(SALT_LENGTH);
    await fs.writeFile(saltFile, salt, { mode: 0o600 });
    logger?.info?.('[secretsCipher] Generated new encryption salt');
  }

  const machineData = [
    os.hostname(),
    os.homedir(),
    os.userInfo().username,
    process.platform,
    KEY_DERIVATION_DOMAIN,
  ].join(':');

  return crypto.pbkdf2Sync(machineData, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
}

/**
 * Encrypt a UTF-8 plaintext to a base64 string carrying IV+authTag.
 *
 * @param {string} plaintext
 * @param {Buffer} encryptionKey
 * @returns {string} base64
 */
export function encrypt(plaintext, encryptionKey) {
  if (!encryptionKey) throw new Error('Encryption key not initialized');

  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(ALGORITHM, encryptionKey, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'base64');
  encrypted += cipher.final('base64');

  const authTag = cipher.getAuthTag();
  const combined = Buffer.concat([
    iv,
    authTag,
    Buffer.from(encrypted, 'base64'),
  ]);
  return combined.toString('base64');
}

/**
 * Decrypt a base64 IV+authTag+ciphertext blob back to UTF-8.
 *
 * @param {string} encryptedData base64
 * @param {Buffer} encryptionKey
 * @returns {string} plaintext
 */
export function decrypt(encryptedData, encryptionKey) {
  if (!encryptionKey) throw new Error('Encryption key not initialized');

  const combined = Buffer.from(encryptedData, 'base64');
  const iv = combined.subarray(0, IV_LENGTH);
  const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
  const encrypted = combined.subarray(IV_LENGTH + AUTH_TAG_LENGTH);

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

  let decrypted = decipher.update(encrypted.toString('base64'), 'base64', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}
