// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * config.js — YAML config reader/writer for NERF CLI.
 *
 * Mirrors the Python gateway/config.py load_config() format and precedence:
 *   1. Environment variables (LLM_BACKEND, ANTHROPIC_API_KEY)   — highest
 *   2. Project-root config.yaml (where cli/ directory lives)
 *   3. ~/.config/nerf/config.yaml                             — lowest
 *
 * Produces YAML that Python's yaml.safe_load() can parse identically.
 *
 * @module config
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { homedir } from 'node:os';
import { parse, stringify } from 'yaml';

// ---------------------------------------------------------------------------
// Path resolution
// ---------------------------------------------------------------------------

/**
 * Walk up from `startDir` looking for cli/ directory — same logic as Python's
 * _find_project_root(). Returns the directory containing cli/ directory, or null.
 *
 * @param {string} [startDir=process.cwd()]
 * @returns {string | null}
 */
function findProjectRoot(startDir = process.cwd()) {
  let current = resolve(startDir);
  for (let i = 0; i < 20; i++) {
    if (existsSync(join(current, 'cli'))) {
      return current;
    }
    const parent = dirname(current);
    if (parent === current) break; // filesystem root
    current = parent;
  }
  return null;
}

/**
 * Return the two config file paths NERF checks.
 *
 * @param {{ home?: string, cwd?: string }} [opts] — overrides for testing
 * @returns {{ userConfig: string, projectConfig: string }}
 */
export function getConfigPaths(opts = {}) {
  const home = opts.home || homedir();
  const startDir = opts.cwd || process.cwd();
  const userConfig = join(home, '.config', 'nerf', 'config.yaml');
  const projectRoot = findProjectRoot(startDir);
  const projectConfig = projectRoot
    ? join(projectRoot, 'config.yaml')
    : join(startDir, 'config.yaml');
  return { userConfig, projectConfig };
}

/**
 * Check whether any config file exists (user-home or project-root).
 *
 * @param {{ home?: string, cwd?: string }} [opts] — overrides for testing
 * @returns {boolean}
 */
export function configExists(opts) {
  const { userConfig, projectConfig } = getConfigPaths(opts);
  return existsSync(userConfig) || existsSync(projectConfig);
}

// ---------------------------------------------------------------------------
// YAML file I/O
// ---------------------------------------------------------------------------

/**
 * Safely load a YAML file. Returns empty object if missing or empty.
 * Wraps parse errors with the file path for diagnostics.
 *
 * @param {string} filePath
 * @returns {Record<string, any>}
 */
/**
 * Substitute ${VAR} and ${VAR:-default} patterns with environment variables.
 * @param {string} text
 * @returns {string}
 */
export function substituteEnvVars(text) {
  // ${VAR:-default} first (greedy match on var name, lazy on default)
  text = text.replace(/\$\{(\w+):-([^}]*)\}/g, (_, varName, defaultVal) => {
    return process.env[varName] || defaultVal;
  });
  // ${VAR} (no default)
  text = text.replace(/\$\{(\w+)\}/g, (_, varName) => {
    return process.env[varName] || '';
  });
  return text;
}

function loadYamlFile(filePath) {
  if (!existsSync(filePath)) return {};
  try {
    let raw = readFileSync(filePath, 'utf-8');
    raw = substituteEnvVars(raw);
    const data = parse(raw);
    return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
  } catch (err) {
    // Warn on malformed config but fall back to empty object rather than
    // crashing the process. A typo in config.yaml should not prevent startup.
    // CWE-755: Improper Handling of Exceptional Conditions prevention.
    process.stderr.write(
      `[nerf-config] WARNING: Failed to parse config at ${filePath}: ${err.message}\n` +
      '[nerf-config] Falling back to defaults. Fix the YAML syntax to restore custom settings.\n',
    );
    return {};
  }
}

// ---------------------------------------------------------------------------
// Deep merge helper
// ---------------------------------------------------------------------------

/**
 * Deep-merge `source` into `target`. Source values override target.
 * Only merges plain objects — arrays and primitives are replaced wholesale.
 *
 * @param {Record<string, any>} target
 * @param {Record<string, any>} source
 * @returns {Record<string, any>}
 */
function deepMerge(target, source) {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    if (
      source[key] &&
      typeof source[key] === 'object' &&
      !Array.isArray(source[key]) &&
      target[key] &&
      typeof target[key] === 'object' &&
      !Array.isArray(target[key])
    ) {
      result[key] = deepMerge(target[key], source[key]);
    } else {
      result[key] = source[key];
    }
  }
  return result;
}

// ---------------------------------------------------------------------------
// Load config
// ---------------------------------------------------------------------------

/**
 * Load configuration from YAML files and environment variable overrides.
 *
 * Precedence (highest → lowest):
 *   1. Environment variables
 *   2. Project-root config.yaml
 *   3. ~/.config/nerf/config.yaml
 *
 * @param {{ home?: string, cwd?: string }} [opts] — overrides for testing
 * @returns {Record<string, any>} Plain config object matching the Python shape.
 */
export function loadConfig(opts) {
  const { userConfig, projectConfig } = getConfigPaths(opts);

  // Load both files — project-root overrides user-home via deep merge
  const userData = loadYamlFile(userConfig);
  const projectData = loadYamlFile(projectConfig);
  const merged = deepMerge(userData, projectData);

  // Apply env var overrides (highest precedence)
  if (process.env.LLM_BACKEND) {
    merged.llm_backend = process.env.LLM_BACKEND;
  }

  // API key loading precedence: env var > Docker secret > config file
  const anthropicKey = _loadSecret('ANTHROPIC_API_KEY', '/run/secrets/anthropic_api_key');
  if (anthropicKey) {
    if (!merged.claude || typeof merged.claude !== 'object') {
      merged.claude = {};
    }
    merged.claude.api_key = anthropicKey;
  }

  const openaiKey = _loadSecret('OPENAI_API_KEY', '/run/secrets/openai_api_key');

  // Defaults for v2.0 fields
  if (!merged.auto_supervisor) {
    merged.auto_supervisor = { soft_timeout_minutes: 20, idle_timeout_minutes: 10, hard_timeout_minutes: 30 };
  }
  if (merged.budget_ceiling === undefined) merged.budget_ceiling = 50.00;
  if (!merged.verification_commands) merged.verification_commands = [];
  if (merged.verification_auto_fix === undefined) merged.verification_auto_fix = true;
  if (merged.verification_max_retries === undefined) merged.verification_max_retries = 2;
  if (!merged.token_profile) merged.token_profile = 'balanced';
  if (merged.unique_engagement_ids === undefined) merged.unique_engagement_ids = true;
  if (merged.auto_report === undefined) merged.auto_report = true;
  if (!merged.models) merged.models = {};

  return merged;
}

/**
 * Load a secret from env var, Docker secret file, or --api-key-file path.
 * Clears the env var after reading to limit exposure window.
 * @param {string} envVar - Environment variable name
 * @param {string} dockerSecretPath - Docker secret file path
 * @returns {string|null}
 */
function _loadSecret(envVar, dockerSecretPath) {
  // 1. Environment variable
  const envValue = process.env[envVar];
  if (envValue) {
    // Clear from environment after reading (limit exposure window)
    delete process.env[envVar];
    return envValue.trim();
  }

  // 2. Docker secret
  try {
    if (existsSync(dockerSecretPath)) {
      return readFileSync(dockerSecretPath, 'utf-8').trim();
    }
  } catch { /* not in Docker or no secret mounted */ }

  return null;
}

// ---------------------------------------------------------------------------
// Write config
// ---------------------------------------------------------------------------

/**
 * Write a config object as YAML. Creates parent directories if needed.
 *
 * @param {Record<string, any>} config — config object to write
 * @param {string} [targetPath] — defaults to ~/.config/nerf/config.yaml
 * @returns {string} The path that was written.
 */
export function writeConfig(config, targetPath) {
  const dest = targetPath || join(homedir(), '.config', 'nerf', 'config.yaml');
  const dir = dirname(dest);

  try {
    mkdirSync(dir, { recursive: true });
  } catch (err) {
    throw new Error(
      `Failed to create config directory ${dir}: ${err.code || err.message}`
    );
  }

  const yamlStr = stringify(config, {
    lineWidth: 0,       // no line wrapping — matches Python's default_flow_style=False
    nullStr: '""',      // empty strings stay as "" not null
  });

  // Add the standard header comment
  const header = [
    '# NERF Gateway Configuration',
    `# Written by nerf setup — ${new Date().toISOString()}`,
    '#',
    '# Precedence: env vars > project-root config.yaml > ~/.config/nerf/config.yaml',
    '',
  ].join('\n');

  try {
    writeFileSync(dest, header + yamlStr, 'utf-8');
  } catch (err) {
    throw new Error(
      `Failed to write config to ${dest}: ${err.code || err.message}`
    );
  }

  return dest;
}
