//! Application configuration loading and management.
//!
//! This module handles the main mi6 configuration file (`<mi6_dir>/config.toml`).
//! Since issue #271, both core and TUI settings are stored in a single config.toml file.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::Duration;

use super::dir;
use super::history::{DEFAULT_HISTORY, default_history_string, parse_history};
use super::hooks::HooksConfig;
use super::otel::OtelConfig;
use super::tui::TuiConfig;
use crate::model::error::ConfigError;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Explicit machine identifier. If None, uses hostname.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub machine_id: Option<String>,

    #[serde(default)]
    pub hooks: HooksConfig,

    /// Data history period for garbage collection. Default: 1000 days.
    /// Supports suffixes: 'd' (days), 'h' (hours), 'm' (minutes), 's' (seconds), or raw seconds.
    #[serde(default = "default_history_string")]
    pub history: String,

    /// OpenTelemetry server configuration.
    #[serde(default)]
    pub otel: OtelConfig,

    /// TUI configuration (columns, theme, animations, etc.).
    #[serde(default, skip_serializing_if = "TuiConfig::is_empty")]
    pub tui: TuiConfig,

    /// Where the config was loaded from (not serialized)
    #[serde(skip)]
    source: ConfigSource,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            machine_id: None,
            hooks: HooksConfig::default(),
            history: default_history_string(),
            otel: OtelConfig::default(),
            tui: TuiConfig::default(),
            source: ConfigSource::Default,
        }
    }
}

/// Where the config was loaded from
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConfigSource {
    /// User config file (<mi6_dir>/config.toml)
    UserConfig,
    /// No config file found, using defaults
    #[default]
    Default,
}

impl Config {
    /// Load config with source information.
    ///
    /// Config resolution order:
    /// 1. `<mi6_dir>/config.toml` (if exists)
    /// 2. Built-in defaults
    ///
    /// The mi6 directory is determined by `MI6_DIR_PATH` env var or defaults to `~/.mi6`.
    pub fn load_with_source() -> Result<(Self, ConfigSource), ConfigError> {
        // Check user config at <mi6_dir>/config.toml
        let config_path = dir::core_config_path()?;
        if config_path.exists() {
            let contents = std::fs::read_to_string(&config_path)?;
            let mut config: Config = toml::from_str(&contents)?;
            config.source = ConfigSource::UserConfig;
            return Ok((config, ConfigSource::UserConfig));
        }

        // Use defaults
        Ok((Config::default(), ConfigSource::Default))
    }

    /// Load config (convenience method that discards source)
    pub fn load() -> Result<Self, ConfigError> {
        Ok(Self::load_with_source()?.0)
    }

    /// Get the database path.
    ///
    /// Returns `<mi6_dir>/mi6.db`.
    pub fn db_path() -> Result<PathBuf, ConfigError> {
        dir::db_path()
    }

    /// Get the source from which this config was loaded.
    pub fn source(&self) -> ConfigSource {
        self.source
    }

    /// Get the mi6 directory path.
    ///
    /// Returns `MI6_DIR_PATH` if set, otherwise `~/.mi6`.
    pub fn mi6_dir() -> Result<PathBuf, ConfigError> {
        dir::mi6_dir()
    }

    /// Get the machine ID.
    ///
    /// Resolution order:
    /// 1. `machine_id` from config file (if set)
    /// 2. Hostname
    /// 3. "unknown" fallback
    pub fn machine_id(&self) -> String {
        // 1. Config file setting
        if let Some(ref id) = self.machine_id
            && !id.is_empty()
        {
            return id.clone();
        }

        // 2. Hostname
        if let Some(hostname) = hostname::get().ok().and_then(|h| h.into_string().ok())
            && !hostname.is_empty()
        {
            return hostname;
        }

        // 3. Fallback
        "unknown".to_string()
    }

    /// Get the history period as a Duration.
    ///
    /// Falls back to default if the history string is invalid.
    pub fn history_duration(&self) -> Duration {
        match parse_history(&self.history) {
            Ok(duration) => duration,
            Err(e) => {
                eprintln!(
                    "mi6: warning: failed to parse history '{}': {}. Using default.",
                    self.history, e
                );
                DEFAULT_HISTORY
            }
        }
    }

    /// Save the configuration to disk.
    ///
    /// Writes to `<mi6_dir>/config.toml`, ensuring the mi6 directory exists.
    pub fn save(&self) -> Result<(), ConfigError> {
        // Ensure mi6 directory exists
        let _ = dir::ensure_initialized();

        let config_path = dir::core_config_path()?;
        let contents =
            toml::to_string_pretty(self).map_err(|e| ConfigError::TomlSerialize(e.to_string()))?;
        std::fs::write(&config_path, contents)?;
        Ok(())
    }

    /// Get a mutable reference to the TUI config section.
    pub fn tui_mut(&mut self) -> &mut TuiConfig {
        &mut self.tui
    }
}

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

    #[test]
    fn test_config_default_history() {
        let config = Config::default();
        assert_eq!(config.history, "1000d");
        assert_eq!(config.history_duration(), DEFAULT_HISTORY);
    }

    #[test]
    fn test_config_custom_history() -> Result<(), String> {
        let toml_str = r#"
            history = "30d"
        "#;

        let config: Config = toml::from_str(toml_str).map_err(|e| e.to_string())?;
        assert_eq!(config.history, "30d");
        assert_eq!(
            config.history_duration(),
            std::time::Duration::from_secs(30 * 24 * 60 * 60)
        );
        Ok(())
    }

    #[test]
    fn test_config_history_fallback_on_invalid() {
        let config = Config {
            history: "invalid".to_string(),
            ..Default::default()
        };
        // Should fall back to DEFAULT_HISTORY
        assert_eq!(config.history_duration(), DEFAULT_HISTORY);
    }

    #[test]
    fn test_config_default_source() {
        let config = Config::default();
        assert_eq!(config.source(), ConfigSource::Default);
    }

    #[test]
    fn test_config_source_cached_from_load() -> Result<(), String> {
        // When loading config, the source should be cached in the struct
        let (config, source) = Config::load_with_source().map_err(|e| e.to_string())?;
        assert_eq!(config.source(), source);
        Ok(())
    }

    #[test]
    fn test_db_path_consistent() -> Result<(), String> {
        // Calling db_path() multiple times should return consistent results
        let path1 = Config::db_path().map_err(|e| e.to_string())?;
        let path2 = Config::db_path().map_err(|e| e.to_string())?;
        assert_eq!(path1, path2);
        Ok(())
    }

    #[test]
    fn test_db_path_ends_with_mi6_db() -> Result<(), String> {
        // db_path() should return a path ending in mi6.db
        let path = Config::db_path().map_err(|e| e.to_string())?;
        assert!(path.ends_with("mi6.db"));
        Ok(())
    }

    #[test]
    fn test_config_source_default_is_default() {
        // Verify ConfigSource::Default is the default variant
        assert_eq!(ConfigSource::default(), ConfigSource::Default);
    }

    #[test]
    fn test_machine_id_returns_nonempty() {
        // machine_id() should always return a non-empty string
        let config = Config::default();
        let machine_id = config.machine_id();
        // Should be non-empty (config, hostname, or "unknown")
        assert!(!machine_id.is_empty());
    }

    #[test]
    fn test_config_machine_id_from_toml() -> Result<(), String> {
        let toml_str = r#"
            machine_id = "test-machine"
        "#;

        let config: Config = toml::from_str(toml_str).map_err(|e| e.to_string())?;
        assert_eq!(config.machine_id, Some("test-machine".to_string()));
        Ok(())
    }

    #[test]
    fn test_config_machine_id_optional_in_toml() -> Result<(), String> {
        // machine_id should be optional in TOML
        let toml_str = r#"
            history = "30d"
        "#;

        let config: Config = toml::from_str(toml_str).map_err(|e| e.to_string())?;
        assert_eq!(config.machine_id, None);
        Ok(())
    }

    #[test]
    fn test_machine_id_default_is_none() {
        // Default config should have machine_id as None
        let config = Config::default();
        assert!(config.machine_id.is_none());
    }

    #[test]
    fn test_mi6_dir_returns_path() -> Result<(), String> {
        // mi6_dir() should return a valid path
        let path = Config::mi6_dir().map_err(|e| e.to_string())?;
        assert!(path.ends_with(".mi6") || std::env::var("MI6_DIR_PATH").is_ok());
        Ok(())
    }
}
