'use strict';

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() {
  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() {
  const { getSetting, setSetting } = require('../db/database');
  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) {
  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) {
  return crypto.createHash('sha256').update(ikm).digest();
}

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

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

// Encrypts with the env key (e2: prefix) when TOKEN_ENCRYPTION_KEY is set,
// otherwise with the DB key (v2: prefix).
function encryptSecret(plaintext) {
  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) {
  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;
  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) {
  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) {
    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, reEncryptWithEnvKey };
