//! Per-project configuration.

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

use serde::{Deserialize, Serialize};

use super::{AppearanceOverride, ConfigError};

/// Persisted zoom and scroll state for a single PDF tab.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct PdfTabState {
    pub zoom: f64,
    pub scroll_top: f64,
}

/// A PDF tab to restore on next launch.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct PdfTabEntry {
    pub ref_id: String,
    pub title: String,
}

/// Session state restored when a project is next opened.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SessionConfig {
    /// PDF tabs to reopen, in order (Library tab is implicit).
    pub open_tabs: Vec<PdfTabEntry>,
    /// Index into [Library, ...open_tabs] of the tab that was active.
    pub active_tab: usize,
    /// Per-reference zoom and scroll position, keyed by ref_id.
    pub pdf_states: HashMap<String, PdfTabState>,
}

/// Per-project configuration, stored at `{repo}/.brittle/config.toml`.
///
/// This file should be in the project's `.gitignore` — it holds local session
/// state and optional appearance overrides that are personal to this machine.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ProjectConfig {
    /// Optional per-project appearance overrides.  `None` means "use globals."
    pub appearance: Option<AppearanceOverride>,
    pub session: SessionConfig,
}

impl ProjectConfig {
    /// Load from `{repo_root}/.brittle/config.toml`.
    ///
    /// Returns `Default` if the file does not yet exist.
    pub fn load(repo_root: &Path) -> Result<Self, ConfigError> {
        Self::load_from(&project_config_path(repo_root))
    }

    /// 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 `{repo_root}/.brittle/config.toml`.
    pub fn save(&self, repo_root: &Path) -> Result<(), ConfigError> {
        self.save_to(&project_config_path(repo_root))
    }

    /// 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(())
    }
}

/// Returns the canonical path for a project's config file.
pub fn project_config_path(repo_root: &Path) -> PathBuf {
    repo_root.join(".brittle").join("config.toml")
}

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

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

    #[test]
    fn project_config_defaults() {
        let cfg = ProjectConfig::default();
        assert!(cfg.appearance.is_none());
        assert!(cfg.session.open_tabs.is_empty());
        assert_eq!(cfg.session.active_tab, 0);
        assert!(cfg.session.pdf_states.is_empty());
    }

    #[test]
    fn project_config_round_trips() {
        let cfg = ProjectConfig {
            appearance: Some(AppearanceOverride {
                theme: Some(Theme::Light),
                font_size: Some(16),
            }),
            session: SessionConfig {
                open_tabs: vec![PdfTabEntry {
                    ref_id: "abc-123".to_string(),
                    title: "smith2024".to_string(),
                }],
                active_tab: 1,
                pdf_states: [("abc-123".to_string(), PdfTabState { zoom: 1.5, scroll_top: 320.0 })]
                    .into_iter().collect(),
            },
        };
        let s = toml::to_string_pretty(&cfg).unwrap();
        let parsed: ProjectConfig = toml::from_str(&s).unwrap();
        assert_eq!(parsed, cfg);
    }

    #[test]
    fn empty_toml_uses_defaults() {
        let cfg: ProjectConfig = toml::from_str("").unwrap();
        assert_eq!(cfg, ProjectConfig::default());
    }

    #[test]
    fn partial_appearance_override_keeps_none_fields() {
        // Only theme specified; font_size should remain None.
        let toml = "[appearance]\ntheme = \"light\"\n";
        let cfg: ProjectConfig = toml::from_str(toml).unwrap();
        let ov = cfg.appearance.unwrap();
        assert_eq!(ov.theme, Some(Theme::Light));
        assert!(ov.font_size.is_none());
    }

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

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

        let original = ProjectConfig {
            appearance: Some(AppearanceOverride {
                theme: Some(Theme::Dark),
                font_size: None,
            }),
            session: SessionConfig {
                open_tabs: vec![PdfTabEntry { ref_id: "ref-1".to_string(), title: "ref2024".to_string() }],
                ..Default::default()
            },
        };
        original.save_to(&path).unwrap();

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

    #[test]
    fn load_and_save_use_brittle_subdir() {
        let tmp = TempDir::new().unwrap();
        let repo = tmp.path();

        let original = ProjectConfig {
            session: SessionConfig {
                open_tabs: vec![PdfTabEntry { ref_id: "x".to_string(), title: "x2024".to_string() }],
                ..Default::default()
            },
            ..Default::default()
        };
        original.save(repo).unwrap();

        assert!(repo.join(".brittle").join("config.toml").exists());

        let loaded = ProjectConfig::load(repo).unwrap();
        assert_eq!(loaded, original);
    }
}
