use std::{
    env, fs,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AppPreferences {
    pub theme: String,
    pub show_command_bar: bool,
    pub show_rule_info: bool,
    pub show_carriage_returns: bool,
    pub item_marker: String,
    pub autosave_minutes: u64,
    pub confirm_destructive: bool,
    pub date_format: String,
    pub clock_24h: bool,
    pub decimal_separator: String,
    pub thousands_separator: String,
}

impl Default for AppPreferences {
    fn default() -> Self {
        Self {
            theme: "classic".into(),
            show_command_bar: true,
            show_rule_info: true,
            show_carriage_returns: false,
            item_marker: "•".into(),
            autosave_minutes: 5,
            confirm_destructive: true,
            date_format: "iso".into(),
            clock_24h: true,
            decimal_separator: ".".into(),
            thousands_separator: ",".into(),
        }
    }
}

impl AppPreferences {
    pub fn path() -> Result<PathBuf> {
        let home =
            env::var_os("HOME").context("HOME is not set; cannot locate user preferences")?;
        Ok(PathBuf::from(home)
            .join(".config")
            .join("rogue-agenda")
            .join("preferences.toml"))
    }

    pub fn load() -> Result<(Self, PathBuf)> {
        let path = Self::path()?;
        let preferences = if path.exists() {
            Self::load_from(&path)?
        } else {
            Self::default()
        };
        Ok((preferences, path))
    }

    pub fn load_from(path: &Path) -> Result<Self> {
        let source = fs::read_to_string(path)
            .with_context(|| format!("could not read preferences from {}", path.display()))?;
        let value: Self = toml::from_str(&source)
            .with_context(|| format!("invalid TOML in {}", path.display()))?;
        value.validate()?;
        Ok(value)
    }

    pub fn save_to(&self, path: &Path) -> Result<()> {
        self.validate()?;
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("could not create {}", parent.display()))?;
        }
        let body = format!(
            "# Rogue Agenda user preferences\n{}",
            toml::to_string_pretty(self).context("could not serialize user preferences")?
        );
        let temporary = path.with_extension("toml.tmp");
        fs::write(&temporary, body)
            .with_context(|| format!("could not write {}", temporary.display()))?;
        fs::rename(&temporary, path)
            .with_context(|| format!("could not replace {}", path.display()))?;
        Ok(())
    }

    pub fn validate(&self) -> Result<()> {
        if !matches!(
            self.theme.as_str(),
            "classic" | "mono" | "amber" | "greenscreen" | "nord" | "catppuccin-mocha"
        ) {
            bail!(
                "theme must be classic, mono, amber, greenscreen, nord, or \
                 catppuccin-mocha"
            )
        }
        if !matches!(
            self.date_format.as_str(),
            "iso" | "us" | "european" | "long"
        ) {
            bail!("date format must be iso, us, european, or long")
        }
        if self.item_marker.chars().count() != 1 {
            bail!("item marker must be exactly one character")
        }
        if self.autosave_minutes > 60 {
            bail!("autosave interval must be between 0 and 60 minutes")
        }
        for (name, separator) in [
            ("decimal separator", &self.decimal_separator),
            ("thousands separator", &self.thousands_separator),
        ] {
            if separator.chars().count() != 1 {
                bail!("{name} must be exactly one character")
            }
        }
        if self.decimal_separator == self.thousands_separator {
            bail!("decimal and thousands separators must be different")
        }
        Ok(())
    }
}

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

    #[test]
    fn preferences_roundtrip_in_user_config_format() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("preferences.toml");
        let preferences = AppPreferences {
            theme: "amber".into(),
            item_marker: ">".into(),
            autosave_minutes: 12,
            date_format: "long".into(),
            ..Default::default()
        };
        preferences.save_to(&path).unwrap();
        assert_eq!(AppPreferences::load_from(&path).unwrap(), preferences);
    }

    #[test]
    fn preferences_reject_unknown_and_ambiguous_values() {
        let directory = tempdir().unwrap();
        let path = directory.path().join("preferences.toml");
        fs::write(&path, "unknown = true\n").unwrap();
        assert!(AppPreferences::load_from(&path).is_err());
        let invalid = AppPreferences {
            decimal_separator: ",".into(),
            thousands_separator: ",".into(),
            ..Default::default()
        };
        assert!(invalid.validate().is_err());
    }

    #[test]
    fn preferences_accept_all_bundled_themes() {
        for theme in [
            "classic",
            "mono",
            "amber",
            "greenscreen",
            "nord",
            "catppuccin-mocha",
        ] {
            let preferences = AppPreferences {
                theme: theme.into(),
                ..Default::default()
            };
            preferences.validate().unwrap();
        }
    }
}
