//! Configuration loader with two-layer merge and env overrides.
//!
//! The loading process:
//! 1. Read global config from `~/.config/agentic/agentic.toml`
//! 2. Read local config from `./agentic.toml`
//! 3. Deep merge at TOML Value level (tables merge, arrays/scalars replace)
//! 4. Deserialize once into typed `AgenticConfig`
//! 5. Apply env var overrides (highest precedence)
//! 6. Run advisory validation

use crate::merge::deep_merge;
use crate::types::AgenticConfig;
use crate::validation::AdvisoryWarning;
use anyhow::Context;
use anyhow::Result;
use std::path::Path;
use std::path::PathBuf;

/// Filename for local config (TOML format).
pub const LOCAL_FILE: &str = "agentic.toml";

/// Directory name under `config_dir` for global config.
pub const GLOBAL_DIR: &str = "agentic";

/// Filename for global config (TOML format).
pub const GLOBAL_FILE: &str = "agentic.toml";

/// Resolved paths for config files.
#[derive(Debug, Clone)]
pub struct AgenticConfigPaths {
    /// Path to local config (./agentic.toml).
    pub local: PathBuf,

    /// Path to global config (~/.config/agentic/agentic.toml).
    pub global: PathBuf,
}

/// Result of loading configuration.
#[derive(Debug)]
pub struct LoadedAgenticConfig {
    /// The loaded and merged configuration.
    pub config: AgenticConfig,

    /// Advisory warnings from validation.
    pub warnings: Vec<AdvisoryWarning>,

    /// Resolved config file paths.
    pub paths: AgenticConfigPaths,
}

/// Get the global config file path.
///
/// Returns `~/.config/agentic/agentic.toml` on Unix-like systems.
///
/// # Test Hook
///
/// Set `__AGENTIC_CONFIG_DIR_FOR_TESTS` to override the base config directory.
/// This is handled by [`crate::paths::xdg_config_home()`] which implements XDG
/// path resolution with test hook support.
pub fn global_config_path() -> Result<PathBuf> {
    let base = crate::paths::xdg_config_home()?;
    Ok(base.join(GLOBAL_DIR).join(GLOBAL_FILE))
}

/// Get the local config file path for a given directory.
pub fn local_config_path(local_dir: &Path) -> PathBuf {
    local_dir.join(LOCAL_FILE)
}

/// Load and merge configuration from global and local files.
///
/// # Precedence (lowest to highest)
/// 1. Default values
/// 2. Global config (`~/.config/agentic/agentic.toml`)
/// 3. Local config (`./agentic.toml`)
/// 4. Environment variables
pub fn load_merged(local_dir: &Path) -> Result<LoadedAgenticConfig> {
    let global_path = global_config_path()?;
    let local_path = local_config_path(local_dir);

    // Collect warnings
    let mut warnings = Vec::new();

    // Read configs as TOML Values
    let global_v = read_toml_table_or_empty(&global_path)?;
    let local_v = read_toml_table_or_empty(&local_path)?;

    // Merge: global as base, local as patch
    let merged = deep_merge(global_v, local_v);

    // Detect unknown top-level keys
    warnings.extend(crate::validation::detect_unknown_top_level_keys_toml(
        &merged,
    ));

    // Detect deprecated keys from merged config (before deserialization)
    warnings.extend(crate::validation::detect_deprecated_keys_toml(&merged));

    // Deserialize to typed config using serde_path_to_error for better error messages
    let cfg: AgenticConfig = {
        let deserializer = merged;
        serde_path_to_error::deserialize(deserializer)
            .with_context(|| "Failed to deserialize merged agentic config")?
    };

    // Apply env var overrides (highest precedence)
    let mut cfg = cfg;
    apply_env_overrides(&mut cfg);

    // Run advisory validation and add to warnings
    warnings.extend(crate::validation::validate(&cfg));

    Ok(LoadedAgenticConfig {
        config: cfg,
        warnings,
        paths: AgenticConfigPaths {
            local: local_path,
            global: global_path,
        },
    })
}

/// Apply environment variable overrides to the config.
fn apply_env_overrides(cfg: &mut AgenticConfig) {
    // --- Service URLs ---
    if let Some(v) = env_trimmed("ANTHROPIC_BASE_URL") {
        cfg.services.anthropic.base_url = v;
    }
    if let Some(v) = env_trimmed("EXA_BASE_URL") {
        cfg.services.exa.base_url = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_BASE_URL") {
        cfg.services.linear.base_url = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_SERVICES_GITHUB_BASE_URL") {
        cfg.services.github.base_url = v;
    }

    // --- Subagents model overrides ---
    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_LOCATOR_MODEL") {
        cfg.subagents.locator_model = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_ANALYZER_MODEL") {
        cfg.subagents.analyzer_model = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_SUBAGENTS_RUNTIME_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.subagents.runtime_timeout_secs = n;
    }

    // --- Reasoning model overrides ---
    if let Some(v) = env_trimmed("AGENTIC_REASONING_OPTIMIZER_MODEL") {
        cfg.reasoning.optimizer_model = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_EXECUTOR_MODEL") {
        cfg.reasoning.executor_model = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_EFFORT") {
        cfg.reasoning.reasoning_effort = Some(v);
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_API_BASE_URL") {
        cfg.reasoning.api_base_url = Some(v);
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_MAX_INPUT_TOKENS")
        && let Ok(n) = v.parse()
    {
        cfg.reasoning.max_input_tokens = Some(n);
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_MAX_COMPLETION_TOKENS")
        && let Ok(n) = v.parse()
    {
        cfg.reasoning.max_completion_tokens = Some(n);
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_EXECUTOR_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.reasoning.executor_timeout_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_EMPTY_RESPONSE_NO_RETRY_AFTER_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.reasoning.empty_response_no_retry_after_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_REASONING_STREAM_HEARTBEAT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.reasoning.stream_heartbeat_secs = n;
    }

    // --- CLI tools overrides ---
    if let Some(v) = env_trimmed("AGENTIC_CLI_TOOLS_JUST_EXECUTE_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.cli_tools.just_execute_timeout_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_CLI_TOOLS_JUST_SEARCH_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.cli_tools.just_search_timeout_secs = n;
    }

    // --- Service timeout overrides ---
    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_CONNECT_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.services.linear.connect_timeout_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_SERVICES_LINEAR_REQUEST_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.services.linear.request_timeout_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_SERVICES_GITHUB_TOTAL_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.services.github.total_timeout_secs = n;
    }

    // --- Review/thoughts overrides ---
    if let Some(v) = env_trimmed("AGENTIC_REVIEW_RUN_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.review.run_timeout_secs = n;
    }
    if let Some(v) = env_trimmed("AGENTIC_THOUGHTS_ADD_REFERENCE_TIMEOUT_SECS")
        && let Ok(n) = v.parse()
    {
        cfg.thoughts.add_reference_timeout_secs = n;
    }

    // --- Logging overrides ---
    if let Some(v) = env_trimmed("AGENTIC_LOG_LEVEL") {
        cfg.logging.level = v;
    }
    if let Some(v) = env_trimmed("AGENTIC_LOG_JSON") {
        cfg.logging.json = v.to_lowercase() == "true" || v == "1";
    }
}

/// Helper to read and normalize an env var (trim + filter empty).
fn env_trimmed(name: &str) -> Option<String> {
    std::env::var(name)
        .ok()
        .map(|v| v.trim().to_string())
        .filter(|v| !v.is_empty())
}

/// Read a TOML file as a Value, returning empty table if file doesn't exist.
fn read_toml_table_or_empty(path: &Path) -> Result<toml::Value> {
    if !path.exists() {
        return Ok(toml::Value::Table(Default::default()));
    }

    let raw = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config file {}", path.display()))?;

    let v: toml::Value =
        toml::from_str(&raw).with_context(|| format!("Invalid TOML in {}", path.display()))?;

    match v {
        toml::Value::Table(_) => Ok(v),
        _ => anyhow::bail!("Config root must be a TOML table: {}", path.display()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_support::EnvGuard;
    use serial_test::serial;
    use tempfile::TempDir;

    /// Env var name for test isolation of global config path.
    const CONFIG_DIR_TEST_VAR: &str = "__AGENTIC_CONFIG_DIR_FOR_TESTS";

    #[test]
    #[serial]
    fn test_load_no_files_returns_defaults() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        let loaded = load_merged(temp.path()).unwrap();

        // Should get default values
        assert_eq!(
            loaded.config.services.anthropic.base_url,
            "https://api.anthropic.com"
        );
        assert_eq!(loaded.config.orchestrator.session_deadline_secs, 3600);
        assert_eq!(loaded.config.subagents.runtime_timeout_secs, 3600);
        assert_eq!(loaded.config.cli_tools.just_execute_timeout_secs, 1800);
        assert_eq!(loaded.config.review.run_timeout_secs, 1800);
        assert!(loaded.warnings.is_empty());
    }

    #[test]
    #[serial]
    fn test_load_local_only() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        let local_path = temp.path().join(LOCAL_FILE);
        std::fs::write(
            &local_path,
            r"
[orchestrator]
session_deadline_secs = 7200
",
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.orchestrator.session_deadline_secs, 7200);
        // Other fields should be defaults
        assert_eq!(loaded.config.orchestrator.inactivity_timeout_secs, 300);
    }

    #[test]
    #[serial]
    fn test_local_overrides_global() {
        let temp = TempDir::new().unwrap();

        // Point global config to our temp directory
        let global_base = temp.path().join("global_config");
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, &global_base);

        // Create global config
        let global_dir = global_base.join(GLOBAL_DIR);
        std::fs::create_dir_all(&global_dir).unwrap();
        std::fs::write(
            global_dir.join(GLOBAL_FILE),
            r#"
[subagents]
locator_model = "global-model"
analyzer_model = "global-analyzer"
"#,
        )
        .unwrap();

        // Create local config that overrides locator_model but not analyzer_model
        let local_dir = temp.path().join("local_repo");
        std::fs::create_dir_all(&local_dir).unwrap();
        std::fs::write(
            local_dir.join(LOCAL_FILE),
            r#"
[subagents]
locator_model = "local-model"
"#,
        )
        .unwrap();

        let loaded = load_merged(&local_dir).unwrap();
        // Local wins for locator_model
        assert_eq!(loaded.config.subagents.locator_model, "local-model");
        // Global value preserved for analyzer_model
        assert_eq!(loaded.config.subagents.analyzer_model, "global-analyzer");
    }

    #[test]
    #[serial]
    fn test_env_overrides_files() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r#"
[reasoning]
optimizer_model = "file-model"
"#,
        )
        .unwrap();

        // Set env var via EnvGuard (RAII cleanup)
        let _env_guard = EnvGuard::set("AGENTIC_REASONING_OPTIMIZER_MODEL", "env-model");

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.reasoning.optimizer_model, "env-model");
    }

    #[test]
    #[serial]
    fn test_reasoning_defaults_include_streaming_recovery_fields() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        let loaded = load_merged(temp.path()).unwrap();

        assert_eq!(loaded.config.reasoning.executor_timeout_secs, 2700);
        assert_eq!(
            loaded.config.reasoning.empty_response_no_retry_after_secs,
            600
        );
        assert_eq!(loaded.config.reasoning.stream_heartbeat_secs, 30);
    }

    #[test]
    #[serial]
    fn test_reasoning_streaming_env_overrides_apply() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
        let _g1 = EnvGuard::set("AGENTIC_REASONING_EXECUTOR_TIMEOUT_SECS", "123");
        let _g2 = EnvGuard::set("AGENTIC_REASONING_EMPTY_RESPONSE_NO_RETRY_AFTER_SECS", "45");
        let _g3 = EnvGuard::set("AGENTIC_REASONING_STREAM_HEARTBEAT_SECS", "9");

        let loaded = load_merged(temp.path()).unwrap();

        assert_eq!(loaded.config.reasoning.executor_timeout_secs, 123);
        assert_eq!(
            loaded.config.reasoning.empty_response_no_retry_after_secs,
            45
        );
        assert_eq!(loaded.config.reasoning.stream_heartbeat_secs, 9);
    }

    #[test]
    #[serial]
    fn test_timeout_env_overrides_apply() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
        let _g1 = EnvGuard::set("AGENTIC_SUBAGENTS_RUNTIME_TIMEOUT_SECS", "123");
        let _g2 = EnvGuard::set("AGENTIC_CLI_TOOLS_JUST_EXECUTE_TIMEOUT_SECS", "456");
        let _g3 = EnvGuard::set("AGENTIC_CLI_TOOLS_JUST_SEARCH_TIMEOUT_SECS", "0");
        let _g4 = EnvGuard::set("AGENTIC_SERVICES_LINEAR_CONNECT_TIMEOUT_SECS", "11");
        let _g5 = EnvGuard::set("AGENTIC_SERVICES_LINEAR_REQUEST_TIMEOUT_SECS", "22");
        let _g6 = EnvGuard::set("AGENTIC_SERVICES_GITHUB_TOTAL_TIMEOUT_SECS", "33");
        let _g7 = EnvGuard::set("AGENTIC_REVIEW_RUN_TIMEOUT_SECS", "44");
        let _g8 = EnvGuard::set("AGENTIC_THOUGHTS_ADD_REFERENCE_TIMEOUT_SECS", "55");

        let loaded = load_merged(temp.path()).unwrap();

        assert_eq!(loaded.config.subagents.runtime_timeout_secs, 123);
        assert_eq!(loaded.config.cli_tools.just_execute_timeout_secs, 456);
        assert_eq!(loaded.config.cli_tools.just_search_timeout_secs, 0);
        assert_eq!(loaded.config.services.linear.connect_timeout_secs, 11);
        assert_eq!(loaded.config.services.linear.request_timeout_secs, 22);
        assert_eq!(loaded.config.services.github.total_timeout_secs, 33);
        assert_eq!(loaded.config.review.run_timeout_secs, 44);
        assert_eq!(loaded.config.thoughts.add_reference_timeout_secs, 55);
    }

    #[test]
    #[serial]
    fn test_reasoning_token_limit_toml_is_ignored_without_warnings() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r"
[reasoning]
token_limit = 12345
",
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.reasoning.max_input_tokens, None);
        assert_eq!(loaded.config.reasoning.max_completion_tokens, Some(128_000));
        assert!(loaded.warnings.is_empty());
    }

    #[test]
    #[serial]
    fn test_reasoning_max_input_tokens_wins_over_token_limit_in_same_toml_layer() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r"
[reasoning]
max_input_tokens = 111
token_limit = 222
",
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.reasoning.max_input_tokens, Some(111));
        assert!(loaded.warnings.is_empty());
    }

    #[test]
    #[serial]
    fn test_deprecated_env_token_limit_is_ignored_and_new_env_still_wins() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        {
            let _g_old = EnvGuard::set("AGENTIC_REASONING_TOKEN_LIMIT", "222");

            let loaded = load_merged(temp.path()).unwrap();
            assert_eq!(loaded.config.reasoning.max_input_tokens, None);
            assert!(loaded.warnings.is_empty());
        }

        let _g_old = EnvGuard::set("AGENTIC_REASONING_TOKEN_LIMIT", "222");
        let _g_new = EnvGuard::set("AGENTIC_REASONING_MAX_INPUT_TOKENS", "111");

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.reasoning.max_input_tokens, Some(111));
        assert!(loaded.warnings.is_empty());
    }

    #[test]
    #[serial]
    fn test_env_trimmed_ignores_whitespace() {
        // Use EnvGuard for RAII cleanup
        let _g1 = EnvGuard::set("TEST_AGENTIC_TRIM", "  value  ");
        let result = env_trimmed("TEST_AGENTIC_TRIM");
        assert_eq!(result, Some("value".to_string()));

        let _g2 = EnvGuard::set("TEST_AGENTIC_EMPTY", "   ");
        let result = env_trimmed("TEST_AGENTIC_EMPTY");
        assert_eq!(result, None);
    }

    #[test]
    #[serial]
    fn test_invalid_toml_errors() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());
        std::fs::write(temp.path().join(LOCAL_FILE), "not valid toml [[[").unwrap();

        let result = load_merged(temp.path());
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Invalid TOML"));
    }

    #[test]
    #[serial]
    fn test_local_value_overrides_struct_default() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        // Create local config that overrides a nested value
        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r"
[web_retrieval]
request_timeout_secs = 60
",
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert_eq!(loaded.config.web_retrieval.request_timeout_secs, 60);
    }

    #[test]
    #[serial]
    fn test_paths_are_set() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        let loaded = load_merged(temp.path()).unwrap();

        assert_eq!(loaded.paths.local, temp.path().join(LOCAL_FILE));
        // Global path should point to our isolated temp dir
        assert_eq!(
            loaded.paths.global,
            temp.path().join(GLOBAL_DIR).join(GLOBAL_FILE)
        );
    }

    #[test]
    #[serial]
    fn test_warns_on_unknown_top_level_key() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r#"
typo = 1
unknown_section = "value"
"#,
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert!(
            loaded
                .warnings
                .iter()
                .any(|w| w.code == "config.unknown_top_level_key" && w.message.contains("typo"))
        );
        assert!(
            loaded
                .warnings
                .iter()
                .any(|w| w.code == "config.unknown_top_level_key"
                    && w.message.contains("unknown_section"))
        );
    }

    #[test]
    #[serial]
    fn test_warns_on_deprecated_thoughts_section() {
        let temp = TempDir::new().unwrap();
        let _guard = EnvGuard::set(CONFIG_DIR_TEST_VAR, temp.path());

        std::fs::write(
            temp.path().join(LOCAL_FILE),
            r"
[thoughts]
mount_dirs = {}
",
        )
        .unwrap();

        let loaded = load_merged(temp.path()).unwrap();
        assert!(
            loaded
                .warnings
                .iter()
                .any(|w| w.code == "config.deprecated.thoughts.mount_dirs")
        );
        assert!(
            !loaded
                .warnings
                .iter()
                .any(|w| w.code == "config.unknown_top_level_key" && w.message.contains("thoughts"))
        );
    }
}
