//! User configuration — `~/.p3ak/config.toml`.
//!
//! The config file is optional. All values can be overridden by CLI flags or
//! environment variables.  Resolution order (last wins):
//!
//! 1. Built-in defaults
//! 2. `~/.p3ak/config.toml` (or `$XDG_CONFIG_HOME/p3ak-vault/config.toml`)
//! 3. `P3AK_VAULT_PATH` / `P3AK_VAULT_PASSPHRASE` environment variables
//! 4. `--path` / `--passphrase` CLI flags
//!
//! # Example config
//!
//! ```toml
//! [vault]
//! path = "/Users/alice/.p3ak/work.vault"
//! passphrase_env = "VAULT_PP"   # reads passphrase from this env-var name
//!
//! [search]
//! mode  = "hybrid"     # flat | pageindex | hybrid
//! limit = 5
//! ```

use anyhow::Result;
use serde::Deserialize;
use std::path::{Path, PathBuf};

// ─── Schema ───────────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
    #[serde(default)]
    pub vault: VaultConfig,
    // Search defaults will be wired to --mode / --limit in a future commit.
    #[serde(default)]
    #[allow(dead_code)]
    pub search: SearchConfig,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct VaultConfig {
    /// Default vault path (absolute or `~`-prefixed).
    // Used by resolve_vault_path (wired to CLI in a future commit).
    #[allow(dead_code)]
    pub path: Option<String>,
    /// Name of the environment variable that holds the passphrase.
    /// If set, `std::env::var(passphrase_env)` is used when no passphrase is
    /// supplied on the command line.
    pub passphrase_env: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SearchConfig {
    /// Default search mode.  Overridden by `--mode` on the CLI.
    // Used by CLI search/accuracy-test in a future commit.
    #[allow(dead_code)]
    #[serde(default = "default_search_mode")]
    pub mode: String,
    /// Default result limit.  Overridden by `--limit`.
    #[allow(dead_code)]
    #[serde(default = "default_limit")]
    pub limit: usize,
}

impl Default for SearchConfig {
    fn default() -> Self {
        Self {
            mode: default_search_mode(),
            limit: default_limit(),
        }
    }
}

fn default_search_mode() -> String {
    "flat".to_string()
}
fn default_limit() -> usize {
    10
}

// ─── Location ─────────────────────────────────────────────────────────────────

/// Return the path to the user config file.
///
/// Respects `$XDG_CONFIG_HOME` (Linux/macOS) and `$APPDATA` (Windows).
/// Falls back to `~/.p3ak/config.toml`.
pub fn config_path() -> PathBuf {
    // Allow tests / CI to override
    if let Ok(p) = std::env::var("P3AK_VAULT_CONFIG") {
        return PathBuf::from(p);
    }

    #[cfg(target_os = "windows")]
    {
        if let Ok(appdata) = std::env::var("APPDATA") {
            return PathBuf::from(appdata)
                .join("p3ak-vault")
                .join("config.toml");
        }
    }

    if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
        return PathBuf::from(xdg).join("p3ak-vault").join("config.toml");
    }

    home_dir().join(".p3ak").join("config.toml")
}

/// Best-effort home directory (falls back to `.` if unresolvable).
fn home_dir() -> PathBuf {
    #[allow(deprecated)] // std::env::home_dir is deprecated but still the most portable option
    std::env::home_dir().unwrap_or_else(|| PathBuf::from("."))
}

// ─── Load ─────────────────────────────────────────────────────────────────────

/// Load the config file, returning defaults if it doesn't exist or is empty.
///
/// Parsing errors are surfaced so the user can see a clear message.
pub fn load() -> Result<Config> {
    let path = config_path();
    load_from(&path)
}

/// Load from a specific path (used in tests).
pub fn load_from(path: &Path) -> Result<Config> {
    if !path.exists() {
        return Ok(Config::default());
    }
    let raw = std::fs::read_to_string(path)?;
    if raw.trim().is_empty() {
        return Ok(Config::default());
    }
    let config: Config = toml::from_str(&raw)?;
    Ok(config)
}

/// Resolve the vault path by checking (in order):
/// 1. Explicit `--path` / `$P3AK_VAULT_PATH` (already merged by clap into `cli_path`)
/// 2. `config.vault.path`
///
/// Returns `None` if none of the above is set.
pub fn resolve_vault_path(cli_path: Option<PathBuf>, config: &Config) -> Option<PathBuf> {
    if let Some(p) = cli_path {
        return Some(p);
    }
    config.vault.path.as_deref().map(expand_tilde)
}

/// Like [`resolve_vault_path`] but returns an error instead of `None`.
///
/// Used by CLI subcommands that require a vault path.
pub fn resolve_required_path(
    cli_path: Option<PathBuf>,
    config: &Config,
) -> anyhow::Result<PathBuf> {
    resolve_vault_path(cli_path, config).ok_or_else(|| {
        anyhow::anyhow!(
            "no vault path specified\n\
             \n\
             Options (in priority order):\n\
               1. --path /path/to/my.vault\n\
               2. export P3AK_VAULT_PATH=/path/to/my.vault\n\
               3. Set vault.path in ~/.p3ak/config.toml"
        )
    })
}

/// Resolve the passphrase by checking (in order):
/// 1. Explicit `--passphrase` CLI flag / `P3AK_VAULT_PASSPHRASE` env (already merged by clap)
/// 2. `config.vault.passphrase_env` — reads the *value* of that env-var
///
/// Writes a resolved value into `cache` so the returned `&str` stays alive.
pub fn resolve_passphrase(
    cli_pp: Option<String>,
    config: &Config,
    cache: &mut Option<String>,
) -> Option<String> {
    if cli_pp.is_some() {
        return cli_pp;
    }
    // `P3AK_VAULT_PASSPHRASE` is handled by clap's `env` attribute so it
    // arrives as `cli_pp`. We handle the *indirection* case here:
    // `passphrase_env = "MY_KEY"` → read `$MY_KEY` at runtime.
    if let Some(env_name) = &config.vault.passphrase_env {
        if let Ok(val) = std::env::var(env_name) {
            if !val.is_empty() {
                *cache = Some(val.clone());
                return Some(val);
            }
        }
    }
    None
}

/// Expand a leading `~` to the home directory.
#[allow(dead_code)]
fn expand_tilde(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/") {
        home_dir().join(rest)
    } else if path == "~" {
        home_dir()
    } else {
        PathBuf::from(path)
    }
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    fn write_config(dir: &TempDir, content: &str) -> PathBuf {
        let p = dir.path().join("config.toml");
        std::fs::write(&p, content).unwrap();
        p
    }

    #[test]
    fn test_empty_file_returns_defaults() {
        let dir = TempDir::new().unwrap();
        let p = write_config(&dir, "");
        let cfg = load_from(&p).unwrap();
        assert_eq!(cfg.search.mode, "flat");
        assert_eq!(cfg.search.limit, 10);
        assert!(cfg.vault.path.is_none());
    }

    #[test]
    fn test_missing_file_returns_defaults() {
        let cfg = load_from(std::path::Path::new("/no/such/config.toml")).unwrap();
        assert!(cfg.vault.path.is_none());
        assert_eq!(cfg.search.mode, "flat");
    }

    #[test]
    fn test_vault_path_and_mode_parsed() {
        let dir = TempDir::new().unwrap();
        let p = write_config(
            &dir,
            r#"
[vault]
path = "/home/alice/work.vault"
passphrase_env = "VAULT_KEY"

[search]
mode  = "hybrid"
limit = 3
"#,
        );
        let cfg = load_from(&p).unwrap();
        assert_eq!(cfg.vault.path.as_deref(), Some("/home/alice/work.vault"));
        assert_eq!(cfg.vault.passphrase_env.as_deref(), Some("VAULT_KEY"));
        assert_eq!(cfg.search.mode, "hybrid");
        assert_eq!(cfg.search.limit, 3);
    }

    #[test]
    fn test_partial_config_fills_defaults() {
        let dir = TempDir::new().unwrap();
        let p = write_config(
            &dir,
            r#"
[vault]
path = "~/my.vault"
"#,
        );
        let cfg = load_from(&p).unwrap();
        assert!(cfg.vault.path.is_some());
        // search section is missing — should use defaults
        assert_eq!(cfg.search.mode, "flat");
        assert_eq!(cfg.search.limit, 10);
    }

    #[test]
    fn test_invalid_toml_returns_error() {
        let dir = TempDir::new().unwrap();
        let p = write_config(&dir, "this is not valid toml!!!@#$");
        let result = load_from(&p);
        assert!(result.is_err());
    }

    #[test]
    fn test_expand_tilde_home() {
        let p = expand_tilde("~/documents/vault.vault");
        let home = home_dir();
        assert!(p.starts_with(&home));
        assert!(p.to_string_lossy().ends_with("documents/vault.vault"));
    }

    #[test]
    fn test_expand_tilde_no_prefix() {
        let p = expand_tilde("/absolute/path.vault");
        assert_eq!(p, PathBuf::from("/absolute/path.vault"));
    }

    #[test]
    fn test_resolve_vault_path_cli_wins() {
        let cfg = Config::default();
        let result = resolve_vault_path(Some(PathBuf::from("/cli/vault.vault")), &cfg);
        assert_eq!(result, Some(PathBuf::from("/cli/vault.vault")));
    }

    #[test]
    fn test_resolve_vault_path_config_fallback() {
        let mut cfg = Config::default();
        cfg.vault.path = Some("/config/vault.vault".to_string());
        let result = resolve_vault_path(None, &cfg);
        assert_eq!(result, Some(PathBuf::from("/config/vault.vault")));
    }

    #[test]
    fn test_resolve_required_path_error_is_actionable() {
        let cfg = Config::default();
        let err = resolve_required_path(None, &cfg).unwrap_err();
        let msg = err.to_string();
        assert!(msg.contains("--path"), "should hint at --path flag");
        assert!(msg.contains("config.toml"), "should hint at config file");
    }

    /// RAII guard that restores an env variable after the test.
    struct EnvGuard {
        key: &'static str,
        original: Option<String>,
    }
    impl EnvGuard {
        fn clear(key: &'static str) -> Self {
            let original = std::env::var(key).ok();
            std::env::remove_var(key);
            Self { key, original }
        }
    }
    impl Drop for EnvGuard {
        fn drop(&mut self) {
            match &self.original {
                Some(v) => std::env::set_var(self.key, v),
                None => std::env::remove_var(self.key),
            }
        }
    }
}
