//! Configuration types for Brittle.
//!
//! Two levels of config exist:
//! - [`GlobalConfig`] — stored at `~/.config/brittle/config.toml`; applies to all projects.
//! - [`ProjectConfig`] — stored at `{repo}/.brittle/config.toml`; overrides globals per project.
//!
//! Use [`MergedConfig::merge`] to produce the effective config the app should use.

mod global;
mod project;

pub use global::GlobalConfig;
pub use project::{PdfTabEntry, PdfTabState, ProjectConfig, SessionConfig};

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

// ── Shared types ──────────────────────────────────────────────────────────────

/// Application colour theme.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
    #[default]
    Dark,
    Light,
}

/// Appearance settings (font size, theme).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct AppearanceConfig {
    pub theme: Theme,
    pub font_size: u32,
}

impl Default for AppearanceConfig {
    fn default() -> Self {
        Self {
            theme: Theme::Dark,
            font_size: 14,
        }
    }
}

/// Partial appearance override from a project config.
/// Only `Some` fields replace the global value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct AppearanceOverride {
    pub theme: Option<Theme>,
    pub font_size: Option<u32>,
}

/// Pane layout proportions and constraints.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct LayoutConfig {
    /// Initial fraction of the window width for the left pane (0..1).
    pub left_pane_fraction: f32,
    /// Initial fraction of the window width for the right pane (0..1).
    pub right_pane_fraction: f32,
    /// Minimum width of the left pane in pixels.
    pub left_pane_min: i32,
    /// Maximum width of the left pane in pixels.
    pub left_pane_max: i32,
    /// Minimum width of the right pane in pixels.
    pub right_pane_min: i32,
    /// Maximum width of the right pane in pixels.
    pub right_pane_max: i32,
    /// Minimum width of the center pane in pixels.
    pub center_pane_min: i32,
}

impl Default for LayoutConfig {
    fn default() -> Self {
        Self {
            left_pane_fraction: 0.20,
            right_pane_fraction: 0.35,
            left_pane_min: 120,
            left_pane_max: 600,
            right_pane_min: 150,
            right_pane_max: 600,
            center_pane_min: 200,
        }
    }
}

/// Keybinding overrides — maps action name to key combo string.
///
/// Only actions the user wants to rebind need an entry here; everything else
/// falls back to the built-in defaults defined in `keymap`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct KeybindingsConfig(pub HashMap<String, String>);

// ── Merged config ─────────────────────────────────────────────────────────────

/// The effective config the application uses at runtime, produced by merging
/// global defaults with per-project overrides.
#[derive(Debug, Clone)]
#[allow(dead_code)] // consumed in Phase 3 when AppState is wired up
pub struct MergedConfig {
    pub appearance: AppearanceConfig,
    pub layout: LayoutConfig,
    pub keybindings: KeybindingsConfig,
    /// Tabs that should be restored on launch.
    pub open_tabs: Vec<PdfTabEntry>,
}

impl MergedConfig {
    #[allow(dead_code)] // consumed in Phase 3 when AppState is wired up
    /// Merge `global` with an optional `project` override.
    ///
    /// Project values take precedence for appearance; layout and keybindings
    /// are always taken from the global config (per-project overrides for those
    /// are intentionally not supported — it would be confusing).
    pub fn merge(global: &GlobalConfig, project: Option<&ProjectConfig>) -> Self {
        let appearance = match project.and_then(|p| p.appearance.as_ref()) {
            Some(ov) => AppearanceConfig {
                theme: ov
                    .theme
                    .clone()
                    .unwrap_or_else(|| global.appearance.theme.clone()),
                font_size: ov.font_size.unwrap_or(global.appearance.font_size),
            },
            None => global.appearance.clone(),
        };

        Self {
            appearance,
            layout: global.layout.clone(),
            keybindings: global.keybindings.clone(),
            open_tabs: project
                .map(|p| p.session.open_tabs.clone())
                .unwrap_or_default(),
        }
    }
}

// ── Error type ────────────────────────────────────────────────────────────────

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    #[error("no config directory found on this platform")]
    NoConfigDir,
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),
    #[error("TOML parse error: {0}")]
    Parse(#[from] toml::de::Error),
    #[error("TOML serialize error: {0}")]
    Serialize(#[from] toml::ser::Error),
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::project::{PdfTabEntry, SessionConfig};

    // ── AppearanceConfig ──────────────────────────────────────────────────────

    #[test]
    fn appearance_defaults_are_dark_14pt() {
        let a = AppearanceConfig::default();
        assert_eq!(a.theme, Theme::Dark);
        assert_eq!(a.font_size, 14);
    }

    #[test]
    fn appearance_round_trips() {
        let original = AppearanceConfig {
            theme: Theme::Light,
            font_size: 16,
        };
        let s = toml::to_string_pretty(&original).unwrap();
        let parsed: AppearanceConfig = toml::from_str(&s).unwrap();
        assert_eq!(parsed, original);
    }

    #[test]
    fn appearance_missing_fields_use_defaults() {
        // Only theme specified; font_size should fall back to 14.
        let parsed: AppearanceConfig = toml::from_str("theme = \"light\"").unwrap();
        assert_eq!(parsed.theme, Theme::Light);
        assert_eq!(parsed.font_size, 14);
    }

    // ── LayoutConfig ─────────────────────────────────────────────────────────

    #[test]
    fn layout_defaults() {
        let l = LayoutConfig::default();
        assert!((l.left_pane_fraction - 0.20).abs() < f32::EPSILON);
        assert!((l.right_pane_fraction - 0.35).abs() < f32::EPSILON);
    }

    #[test]
    fn layout_round_trips() {
        let original = LayoutConfig {
            left_pane_fraction: 0.25,
            right_pane_fraction: 0.40,
            ..LayoutConfig::default()
        };
        let s = toml::to_string_pretty(&original).unwrap();
        let parsed: LayoutConfig = toml::from_str(&s).unwrap();
        assert_eq!(parsed, original);
    }

    // ── MergedConfig ─────────────────────────────────────────────────────────

    #[test]
    fn merge_without_project_uses_globals() {
        let global = GlobalConfig {
            appearance: AppearanceConfig {
                theme: Theme::Light,
                font_size: 16,
            },
            ..Default::default()
        };
        let merged = MergedConfig::merge(&global, None);
        assert_eq!(merged.appearance.theme, Theme::Light);
        assert_eq!(merged.appearance.font_size, 16);
        assert!(merged.open_tabs.is_empty());
    }

    #[test]
    fn merge_project_overrides_theme() {
        let global = GlobalConfig {
            appearance: AppearanceConfig {
                theme: Theme::Dark,
                font_size: 14,
            },
            ..Default::default()
        };
        let project = ProjectConfig {
            appearance: Some(AppearanceOverride {
                theme: Some(Theme::Light),
                font_size: None,
            }),
            ..Default::default()
        };
        let merged = MergedConfig::merge(&global, Some(&project));
        assert_eq!(merged.appearance.theme, Theme::Light);
        // font_size not overridden — inherits from global
        assert_eq!(merged.appearance.font_size, 14);
    }

    #[test]
    fn merge_project_overrides_font_size_only() {
        let global = GlobalConfig {
            appearance: AppearanceConfig {
                theme: Theme::Dark,
                font_size: 14,
            },
            ..Default::default()
        };
        let project = ProjectConfig {
            appearance: Some(AppearanceOverride {
                theme: None,
                font_size: Some(18),
            }),
            ..Default::default()
        };
        let merged = MergedConfig::merge(&global, Some(&project));
        assert_eq!(merged.appearance.theme, Theme::Dark);
        assert_eq!(merged.appearance.font_size, 18);
    }

    #[test]
    fn merge_project_open_tabs_are_included() {
        let global = GlobalConfig::default();
        let project = ProjectConfig {
            session: SessionConfig {
                open_tabs: vec![
                    PdfTabEntry { ref_id: "tab-a".to_string(), title: "a2024".to_string() },
                    PdfTabEntry { ref_id: "tab-b".to_string(), title: "b2024".to_string() },
                ],
                ..Default::default()
            },
            ..Default::default()
        };
        let merged = MergedConfig::merge(&global, Some(&project));
        assert_eq!(merged.open_tabs.len(), 2);
        assert_eq!(merged.open_tabs[0].ref_id, "tab-a");
        assert_eq!(merged.open_tabs[1].ref_id, "tab-b");
    }

    #[test]
    fn merge_layout_always_from_global() {
        let global = GlobalConfig {
            layout: LayoutConfig {
                left_pane_fraction: 0.30,
                right_pane_fraction: 0.40,
                ..LayoutConfig::default()
            },
            ..Default::default()
        };
        // Project config has no layout override capability.
        let merged = MergedConfig::merge(&global, None);
        assert!((merged.layout.left_pane_fraction - 0.30).abs() < f32::EPSILON);
    }
}
