'use strict';

import type { Db } from '../types/db';

const crypto = require('crypto');

const ALGORITHM = 'aes-256-gcm';
const IV_BYTES = 12;
const TAG_BYTES = 16;
// Domain-separation label for HKDF. Changing this invalidates all stored secrets.
const HKDF_INFO = 'bill-tracker-token-encryption-v1';

// Ciphertext prefixes identify which key was used to encrypt.
// e2: = env key (TOKEN_ENCRYPTION_KEY)
// v2: = db key (HKDF derivation)
// no prefix = legacy db key (SHA-256 — pre-v0.78 only)
const ENV_PREFIX = 'e2:';
const V2_PREFIX = 'v2:';

// Returns the env-provided IKM, or null if the var is not set.
function getEnvIkm(): Buffer | null {
  const k = process.env.TOKEN_ENCRYPTION_KEY?.trim();
  return k ? Buffer.from(k, 'utf8') : null;
}

// Returns the auto-generated DB IKM, creating it on first call.
function getDbIkm(): Buffer {
  const { getSetting, setSetting } = require('../db/database.cts');
  let stored = getSetting('_auto_encryption_key');
  if (!stored) {
    stored = crypto.randomBytes(48).toString('hex');
    setSetting('_auto_encryption_key', stored);
  }
  return Buffer.from(stored, 'utf8');
}

function deriveKey(ikm: Buffer): Buffer {
  return Buffer.from(crypto.hkdfSync('sha256', ikm, /* salt */ '', HKDF_INFO, 32));
}

// Legacy derivation — used only to decrypt pre-v0.78 ciphertext (no prefix).
function deriveLegacyKey(ikm: Buffer): Buffer {
  return crypto.createHash('sha256').update(ikm).digest();
}

// True when TOKEN_ENCRYPTION_KEY is present in the environment.
function isEnvKeyActive(): boolean {
  return !!process.env.TOKEN_ENCRYPTION_KEY?.trim();
}

// A non-reversible fingerprint of the ACTIVE key (env if set, else the stored DB
// key). Lets an operator verify which key is live and that a saved backup
// matches — without ever exposing the key. Domain-separated so it shares nothing
// with the cipher key derivation, and it never force-creates a key (returns null
// when none exists yet). A 16-hex prefix of SHA-256 over a 48-byte random key is
// not reversible or guessable.
function keyFingerprint(): string | null {
  let ikm = getEnvIkm();
  if (!ikm) {
    const { getSetting } = require('../db/database.cts');
    const stored = getSetting('_auto_encryption_key');
    if (!stored) return null;
    ikm = Buffer.from(stored, 'utf8');
  }
  return crypto
    .createHash('sha256')
    .update('bill-tracker-key-fingerprint-v1')
    .update(ikm)
    .digest('hex')
    .slice(0, 16);
}

// No-op kept for call-site compatibility.
function assertEncryptionReady(): void {}

// Encrypts with the env key (e2: prefix) when TOKEN_ENCRYPTION_KEY is set,
// otherwise with the DB key (v2: prefix).
function encryptSecret(plaintext: string): string {
  const envIkm = getEnvIkm();
  const ikm = envIkm ?? getDbIkm();
  const prefix = envIkm ? ENV_PREFIX : V2_PREFIX;
  const key = deriveKey(ikm);
  const iv = crypto.randomBytes(IV_BYTES);
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
  const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return `${prefix}${iv.toString('hex')}:${tag.toString('hex')}:${ct.toString('hex')}`;
}

// Decrypts both e2: (env key), v2: (db key HKDF), and legacy (db key SHA-256) ciphertext.
function decryptSecret(stored: string): string {
  const isEnv = stored.startsWith(ENV_PREFIX);
  const isV2 = !isEnv && stored.startsWith(V2_PREFIX);
  const payload = stored.slice(isEnv ? ENV_PREFIX.length : isV2 ? V2_PREFIX.length : 0);

  const parts = payload.split(':');
  if (parts.length !== 3) throw new Error('Invalid encrypted secret format');
  const [ivHex, tagHex, ctHex] = parts;

  let key: Buffer;
  if (isEnv) {
    const envIkm = getEnvIkm();
    if (!envIkm)
      throw new Error('TOKEN_ENCRYPTION_KEY is required to decrypt this secret but is not set');
    key = deriveKey(envIkm);
  } else if (isV2) {
    key = deriveKey(getDbIkm());
  } else {
    key = deriveLegacyKey(getDbIkm());
  }

  const iv = Buffer.from(ivHex, 'hex');
  const tag = Buffer.from(tagHex, 'hex');
  const ct = Buffer.from(ctHex, 'hex');
  const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
}

// Re-encrypts all DB-key-encrypted secrets with the env key.
// Called at startup when TOKEN_ENCRYPTION_KEY is present — idempotent.
// Already-migrated (e2:) values are skipped.
function reEncryptWithEnvKey(db: Db): void {
  if (!isEnvKeyActive()) return;

  // All table+column pairs that store encrypted values.
  // Table names are hardcoded — never user-supplied.
  const TABLE_COLS = [
    { table: 'data_sources', col: 'encrypted_secret' },
    { table: 'users', col: 'totp_secret' },
    { table: 'users', col: 'totp_recovery_codes' },
    { table: 'users', col: 'push_url' },
    { table: 'users', col: 'push_token' },
    { table: 'user_login_history', col: 'ip_address' },
    { table: 'user_login_history', col: 'user_agent' },
    { table: 'user_login_history', col: 'location_city' },
    { table: 'user_login_history', col: 'location_country' },
    { table: 'user_login_history', col: 'location_region' },
    { table: 'user_login_history', col: 'location_isp' },
  ];
  const SETTINGS_KEYS = ['notify_smtp_password', 'oidc_client_secret'];

  function reEnc(val: string | null): string | null {
    if (!val || val.startsWith(ENV_PREFIX)) return val; // null or already migrated
    try {
      return encryptSecret(decryptSecret(val)); // decrypt with old key, encrypt with new
    } catch {
      return val; // corrupted or unknown format — leave as-is
    }
  }

  let count = 0;

  db.transaction(() => {
    for (const { table, col } of TABLE_COLS) {
      // Guard against schema differences across upgrade paths
      const existing = db
        .prepare(`PRAGMA table_info(${table})`)
        .all()
        .map((r) => r.name);
      if (!existing.includes(col)) continue;

      const rows = db.prepare(`SELECT id, ${col} FROM ${table} WHERE ${col} IS NOT NULL`).all();
      for (const row of rows) {
        const migrated = reEnc(row[col]);
        if (migrated !== row[col]) {
          db.prepare(`UPDATE ${table} SET ${col} = ? WHERE id = ?`).run(migrated, row.id);
          count++;
        }
      }
    }

    for (const key of SETTINGS_KEYS) {
      const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
      if (!row?.value) continue;
      const migrated = reEnc(row.value);
      if (migrated !== row.value) {
        db.prepare(
          "INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, datetime('now'))",
        ).run(key, migrated);
        count++;
      }
    }
  })();

  if (count > 0) {
    console.log(
      `[encryption] Migrated ${count} secret(s) from db-key to env-key (TOKEN_ENCRYPTION_KEY)`,
    );
  }
}

module.exports = {
  assertEncryptionReady,
  encryptSecret,
  decryptSecret,
  isEnvKeyActive,
  keyFingerprint,
  reEncryptWithEnvKey,
};
