//! Global (user-wide) configuration.

use std::collections::HashMap;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use super::{AppearanceConfig, ConfigError, KeybindingsConfig, LayoutConfig};

/// Record of recently opened repositories, stored in the global config.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ProjectsState {
    pub recent: Vec<PathBuf>,
    pub last_opened: Option<PathBuf>,
}

/// User-wide configuration, stored at `~/.config/brittle/config.toml`.
///
/// All fields are optional in the file; missing fields fall back to their
/// `Default` implementations.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct GlobalConfig {
    pub appearance: AppearanceConfig,
    pub layout: LayoutConfig,
    pub projects: ProjectsState,
    /// Flat map of action name → key combo string for user-defined overrides.
    #[serde(with = "keybindings_map")]
    pub keybindings: KeybindingsConfig,
}

impl GlobalConfig {
    /// Load from the standard platform config directory.
    ///
    /// Returns `Default` if the file does not yet exist.
    pub fn load() -> Result<Self, ConfigError> {
        Self::load_from(&global_config_path()?)
    }

    /// Load from an explicit path.
    ///
    /// Returns `Default` if the file does not exist.
    pub fn load_from(path: &Path) -> Result<Self, ConfigError> {
        if !path.exists() {
            return Ok(Self::default());
        }
        let content = std::fs::read_to_string(path)?;
        Ok(toml::from_str(&content)?)
    }

    /// Save to the standard platform config directory,
    /// creating parent directories as needed.
    pub fn save(&self) -> Result<(), ConfigError> {
        self.save_to(&global_config_path()?)
    }

    /// Save to an explicit path, creating parent directories as needed.
    pub fn save_to(&self, path: &Path) -> Result<(), ConfigError> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(path, toml::to_string_pretty(self)?)?;
        Ok(())
    }
}

fn global_config_path() -> Result<PathBuf, ConfigError> {
    dirs::config_dir()
        .ok_or(ConfigError::NoConfigDir)
        .map(|d| d.join("brittle").join("config.toml"))
}

/// Custom serde module so `KeybindingsConfig` round-trips as a flat TOML table.
///
/// Stored in the file as:
/// ```toml
/// [keybindings]
/// focus_left = "H"
/// tab_next   = "gt"
/// ```
mod keybindings_map {
    use super::*;
    use serde::{Deserializer, Serializer};

    pub fn serialize<S: Serializer>(kc: &KeybindingsConfig, s: S) -> Result<S::Ok, S::Error> {
        kc.0.serialize(s)
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<KeybindingsConfig, D::Error> {
        HashMap::<String, String>::deserialize(d).map(KeybindingsConfig)
    }
}

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

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

    #[test]
    fn global_config_defaults() {
        let cfg = GlobalConfig::default();
        assert_eq!(cfg.appearance.theme, Theme::Dark);
        assert_eq!(cfg.appearance.font_size, 14);
        assert!(cfg.projects.recent.is_empty());
        assert!(cfg.keybindings.0.is_empty());
    }

    #[test]
    fn global_config_round_trips() {
        let mut cfg = GlobalConfig::default();
        cfg.appearance.theme = Theme::Light;
        cfg.appearance.font_size = 16;
        cfg.keybindings
            .0
            .insert("focus_left".to_string(), "C-h".to_string());

        let s = toml::to_string_pretty(&cfg).unwrap();
        let parsed: GlobalConfig = toml::from_str(&s).unwrap();
        assert_eq!(parsed, cfg);
    }

    #[test]
    fn empty_toml_uses_all_defaults() {
        let cfg: GlobalConfig = toml::from_str("").unwrap();
        assert_eq!(cfg.appearance.font_size, 14);
        assert!((cfg.layout.left_pane_fraction - 0.20).abs() < f32::EPSILON);
    }

    #[test]
    fn partial_toml_uses_defaults_for_missing_sections() {
        let toml = "[appearance]\ntheme = \"light\"\n";
        let cfg: GlobalConfig = toml::from_str(toml).unwrap();
        assert_eq!(cfg.appearance.theme, Theme::Light);
        // layout not specified — should be default
        assert!((cfg.layout.left_pane_fraction - 0.20).abs() < f32::EPSILON);
    }

    #[test]
    fn load_from_nonexistent_path_returns_default() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join("does_not_exist.toml");
        let cfg = GlobalConfig::load_from(&path).unwrap();
        assert_eq!(cfg, GlobalConfig::default());
    }

    #[test]
    fn save_to_and_load_from_round_trip() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join("config.toml");

        let mut original = GlobalConfig::default();
        original.appearance.theme = Theme::Light;
        original.appearance.font_size = 18;
        original.save_to(&path).unwrap();

        let loaded = GlobalConfig::load_from(&path).unwrap();
        assert_eq!(loaded, original);
    }

    #[test]
    fn save_to_creates_parent_directories() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join("nested").join("dirs").join("config.toml");
        GlobalConfig::default().save_to(&path).unwrap();
        assert!(path.exists());
    }

    #[test]
    fn keybinding_overrides_round_trip() {
        let mut cfg = GlobalConfig::default();
        cfg.keybindings
            .0
            .insert("tab_next".to_string(), "C-Right".to_string());

        let s = toml::to_string_pretty(&cfg).unwrap();
        let parsed: GlobalConfig = toml::from_str(&s).unwrap();
        assert_eq!(parsed.keybindings.0["tab_next"], "C-Right");
    }
}
