/**
 * Master key resolver for the encrypted secrets vault.
 *
 * Resolution order:
 *   1. MADARX_MASTER_KEY env var (32 raw bytes, base64-encoded) — primary
 *   2. On first install (empty secrets table): generate a random key, print a
 *      one-time instruction, and write it to .env.local as a fallback.
 *
 * Decision (2026-05-13): no macOS Keychain integration — keeps install identical
 * across macOS and Linux and avoids Keychain prompt loops.
 */

import { randomBytes } from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';

const KEY_LENGTH = 32; // bytes → 256-bit AES key

// Resolved once per process; reset in tests via resetMasterKey().
let _resolvedKey = null;

function envFilePath() {
  return process.env.MADARX_ENV_FILE || path.join(process.cwd(), '.env.local');
}

/**
 * Parse all `MADARX_MASTER_KEY=` lines out of an env file's contents.
 * Returns an array of raw values (in file order). Empty array if none.
 *
 * Comments and quoted values are tolerated; we only match lines that begin
 * with `MADARX_MASTER_KEY=` (whitespace-trimmed).
 */
function extractMasterKeyLines(content) {
  if (!content) return [];
  return content
    .split('\n')
    .map(line => line.trim())
    .filter(line => line.startsWith('MADARX_MASTER_KEY='))
    .map(line => line.slice('MADARX_MASTER_KEY='.length).replace(/^["']|["']$/g, '').trim());
}

function writeKeyToEnvLocal(b64) {
  const file = envFilePath();
  let content = '';
  try {
    content = fs.readFileSync(file, 'utf8');
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }

  // Idempotent: refuse to append a second MADARX_MASTER_KEY entry. Previously this
  // function blindly appended every time it ran, producing .env.local files with
  // multiple conflicting keys. dotenv + Node --env-file both use first-wins, so a
  // restart could bind a different key than the one that encrypted vault rows,
  // making secrets silently undecryptable.
  const existing = extractMasterKeyLines(content);
  if (existing.length > 0) {
    throw new Error(
      `MADARX_MASTER_KEY is already present in ${file} (${existing.length} entr${existing.length === 1 ? 'y' : 'ies'} found). ` +
      'Refusing to append a second key — that would corrupt the vault. ' +
      'Either export MADARX_MASTER_KEY in your shell before starting, or fix .env.local by hand so only one MADARX_MASTER_KEY= line remains.'
    );
  }

  const warning = [
    '',
    '# ============================================================',
    '# WARNING: MADARX_MASTER_KEY is the encryption key for all',
    '# integration credentials in the secrets vault. Keep this safe.',
    '# Copy it to your shell rc (e.g. ~/.zshrc) and remove it from',
    '# this file once your shell is configured. Anyone with this key',
    '# can decrypt all stored secrets.',
    '# ============================================================',
    `MADARX_MASTER_KEY=${b64}`,
    '',
  ].join('\n');

  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, content + warning, { mode: 0o600 });
  try { fs.chmodSync(file, 0o600); } catch {}
}

/**
 * Scan repo root (and ~/.madarx when present) for .env*.bak* / .env*.backup*
 * files with mode > 600. Returns an array of { file, mode } for offenders.
 * Read-only — never auto-chmods. Surfaces the issue so the operator can act.
 */
export function findInsecureEnvBackups({ extraDirs = [] } = {}) {
  const offenders = [];
  const dirs = [process.cwd(), path.join(process.env.HOME || '', '.madarx'), ...extraDirs]
    .filter(Boolean);
  const pattern = /^\.env(\.[^/]+)?\.(bak|backup)(-.*)?$/;
  for (const dir of dirs) {
    let entries;
    try { entries = fs.readdirSync(dir); } catch { continue; }
    for (const name of entries) {
      if (!pattern.test(name)) continue;
      const full = path.join(dir, name);
      let st;
      try { st = fs.statSync(full); } catch { continue; }
      if (!st.isFile()) continue;
      const mode = st.mode & 0o777;
      if (mode > 0o600) offenders.push({ file: full, mode });
    }
  }
  return offenders;
}

/**
 * Detect a corrupt env-file state where multiple MADARX_MASTER_KEY entries
 * disagree. Returns null when fine, or { keys: string[] } when corrupt.
 * Read-only — never mutates the file. Safe to call on every boot.
 */
export function detectMasterKeyConflict(envFile = envFilePath()) {
  let content = '';
  try { content = fs.readFileSync(envFile, 'utf8'); }
  catch (err) { if (err.code !== 'ENOENT') throw err; return null; }
  const lines = extractMasterKeyLines(content);
  if (lines.length <= 1) return null;
  const unique = new Set(lines);
  if (unique.size === 1) return null;
  return { file: envFile, keys: lines };
}

function generateAndPersistKey() {
  const raw = randomBytes(KEY_LENGTH);
  const b64 = raw.toString('base64');

  writeKeyToEnvLocal(b64);

  console.log('\n' + '='.repeat(70));
  console.log('MADARX SECRETS VAULT — FIRST INSTALL');
  console.log('='.repeat(70));
  console.log('A new master encryption key has been generated and written to .env.local.');
  console.log('');
  console.log('Add this to your shell rc file (~/.zshrc or ~/.bash_profile) so the key');
  console.log('is available to all processes (Next.js server, daemons, scripts):');
  console.log('');
  console.log(`  export MADARX_MASTER_KEY=${b64}`);
  console.log('');
  console.log('After adding it to your shell rc, reload it:');
  console.log('  source ~/.zshrc');
  console.log('');
  console.log('The .env.local fallback will be used until your shell is configured.');
  console.log('='.repeat(70) + '\n');

  // Set it in the current process so callers can use it immediately.
  process.env.MADARX_MASTER_KEY = b64;

  return raw;
}

/**
 * Check whether the secrets table has any rows, without importing lib/db.js
 * directly (to avoid circular imports). We accept the db handle as a parameter.
 */
export function hasPersistentSecrets(db) {
  try {
    const row = db.prepare('SELECT id FROM secrets WHERE status = ? LIMIT 1').get('active');
    return Boolean(row);
  } catch {
    // Table doesn't exist yet — treat as empty.
    return false;
  }
}

/**
 * Resolve the master key as a 32-byte Buffer.
 *
 * @param {{ db?: object, allowGenerate?: boolean }} [opts]
 *   db             — pass the DatabaseSync handle so we can check for existing
 *                    secrets before generating a new key. If omitted, generation
 *                    is allowed unconditionally (used in tests).
 *   allowGenerate  — override: if false, never generate (fail instead).
 * @returns {Buffer} 32-byte AES-256 master key
 */
export function resolveMasterKey({ db = null, allowGenerate = true } = {}) {
  if (_resolvedKey) return _resolvedKey;

  // Surface a startup-time warning if .env.local has multiple disagreeing keys.
  // dotenv / Node --env-file both pick first-wins, so silent decrypt failures
  // are otherwise impossible to diagnose without trial-and-error.
  try {
    const conflict = detectMasterKeyConflict();
    if (conflict) {
      console.warn(
        `[secrets] WARNING: ${conflict.file} contains ${conflict.keys.length} different MADARX_MASTER_KEY lines. ` +
        'Only one will be loaded (first-wins) and vault rows encrypted with any other key will silently fail to decrypt. ' +
        'Fix the file so only one MADARX_MASTER_KEY= line remains.'
      );
    }
  } catch { /* never let diagnostics break startup */ }

  // Surface .env*.bak / .env*.backup files with mode > 600. These usually
  // contain previous credentials; world-readable perms leak them to any
  // local process. Do not auto-chmod — operator must decide whether to
  // chmod 600 or delete the file outright.
  try {
    const insecure = findInsecureEnvBackups();
    for (const { file, mode } of insecure) {
      console.warn(
        `[secrets] WARNING: ${file} has mode ${mode.toString(8)} (expected 600). ` +
        'This backup likely contains credentials and is readable by other local users. ' +
        `Fix: chmod 600 "${file}" — or delete the file if no longer needed.`
      );
    }
  } catch { /* never let diagnostics break startup */ }

  const envValue = process.env.MADARX_MASTER_KEY;

  if (envValue) {
    const raw = Buffer.from(envValue, 'base64');
    if (raw.length !== KEY_LENGTH) {
      throw new Error(
        `MADARX_MASTER_KEY must be a base64-encoded 32-byte key (got ${raw.length} bytes). ` +
        'Generate a new key with: node -e "console.log(require(\'node:crypto\').randomBytes(32).toString(\'base64\'))"'
      );
    }
    _resolvedKey = raw;
    return _resolvedKey;
  }

  // Env var not set. Check if there are existing secrets that would be
  // unreadable without the key.
  if (db && hasPersistentSecrets(db)) {
    throw new Error(
      'MADARX_MASTER_KEY is not set but the secrets vault has stored credentials.\n' +
      'Set the env var to your master key before starting:\n' +
      '  export MADARX_MASTER_KEY=<your-base64-key>\n\n' +
      'If you have lost the key, the vault contents are unrecoverable. You will need to\n' +
      'delete all rows from the secrets table and re-add credentials from scratch.'
    );
  }

  if (!allowGenerate) {
    throw new Error(
      'MADARX_MASTER_KEY is not set. Set it before using the secrets vault.'
    );
  }

  // Fresh install — generate and persist.
  _resolvedKey = generateAndPersistKey();
  return _resolvedKey;
}

/** Reset the cached key (used in tests to isolate state). */
export function resetMasterKey() {
  _resolvedKey = null;
}
