use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::subcommand::{SubcommandSet, TomlSubcommand};
use crate::{AliasDetail, AliasName, AliasSet, TomlAlias};

const CONFIG_FILE: &str = "config.toml";

#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
pub struct ShellsTomlConfig {
    pub fish: Option<FishConfig>,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct FishConfig {
    #[serde(default)]
    pub use_abbr: bool,
}

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Config {
    #[serde(default)]
    pub aliases: AliasSet,
    #[serde(default, skip_serializing_if = "SubcommandSet::is_empty")]
    pub subcommands: SubcommandSet,
    #[serde(default, skip_serializing_if = "crate::vars::VarSet::is_empty")]
    pub vars: crate::vars::VarSet,
    #[serde(default)]
    pub shell: ShellsTomlConfig,
    #[serde(default)]
    pub logging: LoggingConfig,
    #[serde(default)]
    pub update: UpdateConfig,
}

#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub struct UpdateConfig {
    /// Whether the listing commands should check crates.io for a newer
    /// release. Defaults to `true`. Set to `false` to disable entirely.
    #[serde(default = "default_update_check")]
    pub check: bool,
}

impl Default for UpdateConfig {
    fn default() -> Self {
        Self {
            check: default_update_check(),
        }
    }
}

fn default_update_check() -> bool {
    true
}

#[derive(Debug, Default, Deserialize, Serialize, Clone)]
pub struct LoggingConfig {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project_loading: Option<LogVerbosity>,

    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project_unloading: Option<LogVerbosity>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum LogVerbosity {
    Off,
    Short,
    Verbose,
}

impl Config {
    pub fn load_from(config_dir: &Path) -> crate::Result<Self> {
        let path = config_dir.join(CONFIG_FILE);
        if !path.exists() {
            return Ok(Self::default());
        }
        let data = std::fs::read_to_string(path)?;
        let config = toml::from_str(&data)?;
        Ok(config)
    }

    pub fn save_to(&self, config_dir: &Path) -> crate::Result<()> {
        if !config_dir.exists() {
            std::fs::create_dir_all(config_dir)?;
        }
        let path = config_dir.join(CONFIG_FILE);
        let data = toml::to_string(self)?;
        std::fs::write(path, data)?;
        Ok(())
    }

    pub fn load() -> crate::Result<Self> {
        Self::load_from(&crate::dirs::config_dir())
    }

    pub fn add_alias(
        &mut self,
        name: String,
        command: String,
        raw: bool,
        description: Option<String>,
    ) {
        let key: AliasName = name.into();
        let alias = if description.is_some() || raw {
            TomlAlias::Detailed(AliasDetail {
                command,
                description,
                raw,
            })
        } else {
            TomlAlias::Command(command)
        };
        self.aliases.insert(key, alias);
    }

    pub fn merge_aliases(&mut self, incoming: AliasSet) {
        for (name, alias) in incoming.iter() {
            self.aliases.insert(name.clone(), alias.clone());
        }
    }

    pub fn remove_alias(&mut self, name: &str) -> crate::Result<()> {
        let key: AliasName = name.into();
        self.aliases
            .remove(&key)
            .ok_or_else(|| anyhow::anyhow!("Global alias '{name}' not found"))?;
        Ok(())
    }

    pub fn add_subcommand(
        &mut self,
        key: String,
        long_subcommands: Vec<String>,
        description: Option<String>,
    ) {
        let value = match description {
            Some(d) => TomlSubcommand::Detailed(crate::subcommand::SubcommandDetail {
                expansions: long_subcommands,
                description: Some(d),
            }),
            None => TomlSubcommand::Expansion(long_subcommands),
        };
        self.subcommands.as_mut().insert(key, value);
    }

    pub fn remove_subcommand(&mut self, key: &str) -> crate::Result<()> {
        self.subcommands
            .as_mut()
            .remove(key)
            .ok_or_else(|| anyhow::anyhow!("Subcommand alias '{key}' not found"))?;
        Ok(())
    }

    pub fn set_var(&mut self, name: crate::vars::VarName, value: String) {
        self.vars.insert(name, value);
    }

    pub fn unset_var(&mut self, name: &crate::vars::VarName) -> Option<String> {
        self.vars.remove(name)
    }
}

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

    #[test]
    fn test_default_config_when_no_file() {
        let dir = tempfile::tempdir().unwrap();
        let config = Config::load_from(dir.path()).unwrap();
        assert!(config.aliases.is_empty());
    }

    #[test]
    fn test_save_and_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let mut config = Config {
            aliases: AliasSet::default(),
            ..Default::default()
        };
        config.add_alias("ll".to_string(), "ls -lha".to_string(), false, None);
        config.save_to(dir.path()).unwrap();

        let loaded = Config::load_from(dir.path()).unwrap();
        assert_eq!(loaded.aliases.iter().count(), 1);
    }

    #[test]
    fn test_add_and_remove_global_alias() {
        let mut config = Config::default();
        config.add_alias("ll".to_string(), "ls -lha".to_string(), false, None);
        assert_eq!(config.aliases.iter().count(), 1);

        config.remove_alias("ll").unwrap();
        assert!(config.aliases.is_empty());
    }

    #[test]
    fn test_remove_nonexistent_global_alias() {
        let mut config = Config::default();
        let err = config.remove_alias("nope").unwrap_err();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn test_merge_aliases_into_global() {
        let mut config = Config::default();
        config.add_alias("ll".into(), "ls -lha".into(), false, None);
        let mut incoming = AliasSet::default();
        incoming.insert("gs".into(), TomlAlias::Command("git status".into()));
        incoming.insert("ll".into(), TomlAlias::Command("ls -la".into()));
        config.merge_aliases(incoming);
        assert_eq!(config.aliases.len(), 2);
        assert_eq!(
            config
                .aliases
                .get(&AliasName::from("ll"))
                .unwrap()
                .command(),
            "ls -la"
        );
    }

    #[test]
    fn test_config_with_subcommands_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let mut config = Config::default();
        config.subcommands.as_mut().insert(
            "jj:ab".into(),
            TomlSubcommand::Expansion(vec!["abandon".into()]),
        );
        config.save_to(dir.path()).unwrap();

        let loaded = Config::load_from(dir.path()).unwrap();
        assert_eq!(loaded.subcommands.as_ref().len(), 1);
        assert_eq!(
            loaded.subcommands.as_ref()["jj:ab"],
            TomlSubcommand::Expansion(vec!["abandon".to_string()])
        );
    }

    #[test]
    fn test_add_and_remove_subcommand() {
        let mut config = Config::default();
        config.add_subcommand("jj:ab".into(), vec!["abandon".into()], None);
        assert_eq!(config.subcommands.as_ref().len(), 1);

        config.remove_subcommand("jj:ab").unwrap();
        assert!(config.subcommands.is_empty());
    }

    #[test]
    fn test_remove_nonexistent_subcommand() {
        let mut config = Config::default();
        let err = config.remove_subcommand("jj:nope").unwrap_err();
        assert!(err.to_string().contains("not found"));
    }

    #[test]
    fn test_fish_use_abbr_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let config = Config {
            shell: ShellsTomlConfig {
                fish: Some(FishConfig { use_abbr: true }),
            },
            ..Default::default()
        };
        config.save_to(dir.path()).unwrap();
        let loaded = Config::load_from(dir.path()).unwrap();
        assert!(loaded.shell.fish.unwrap().use_abbr);
    }

    #[test]
    fn test_default_config_has_no_shell_config() {
        let dir = tempfile::tempdir().unwrap();
        let config = Config::load_from(dir.path()).unwrap();
        assert!(config.shell.fish.is_none());
    }

    #[test]
    fn test_logging_config_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let toml_str = r#"
[logging]
project_loading = "short"
project_unloading = "off"
"#;
        std::fs::write(dir.path().join("config.toml"), toml_str).unwrap();
        let config = Config::load_from(dir.path()).unwrap();
        assert!(matches!(
            config.logging.project_loading,
            Some(LogVerbosity::Short)
        ));
        assert!(matches!(
            config.logging.project_unloading,
            Some(LogVerbosity::Off)
        ));
    }

    #[test]
    fn test_logging_config_defaults_to_none() {
        let dir = tempfile::tempdir().unwrap();
        let toml_str = "[aliases]\n";
        std::fs::write(dir.path().join("config.toml"), toml_str).unwrap();
        let config = Config::load_from(dir.path()).unwrap();
        assert!(config.logging.project_loading.is_none());
        assert!(config.logging.project_unloading.is_none());
    }

    #[test]
    fn config_vars_default_empty_and_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let mut config = Config::default();
        assert!(config.vars.is_empty());
        config.set_var(
            crate::vars::VarName::parse("path").unwrap(),
            "/opt/v1".into(),
        );
        config.save_to(dir.path()).unwrap();
        let loaded = Config::load_from(dir.path()).unwrap();
        assert_eq!(loaded.vars.len(), 1);
        assert_eq!(
            loaded
                .vars
                .get(&crate::vars::VarName::parse("path").unwrap())
                .map(String::as_str),
            Some("/opt/v1")
        );
    }

    #[test]
    fn config_unset_var_returns_value() {
        let mut config = Config::default();
        config.set_var(crate::vars::VarName::parse("x").unwrap(), "y".into());
        let prev = config.unset_var(&crate::vars::VarName::parse("x").unwrap());
        assert_eq!(prev.as_deref(), Some("y"));
        assert!(config.vars.is_empty());
    }

    #[test]
    fn config_unset_unknown_var_returns_none() {
        let mut config = Config::default();
        let prev = config.unset_var(&crate::vars::VarName::parse("nope").unwrap());
        assert!(prev.is_none());
    }

    #[test]
    fn add_alias_with_description_stores_detailed_form() {
        let mut config = Config::default();
        config.add_alias(
            "gs".into(),
            "git status".into(),
            false,
            Some("git status short".into()),
        );
        let stored = config.aliases.get(&"gs".into()).unwrap();
        match stored {
            TomlAlias::Detailed(d) => {
                assert_eq!(d.description.as_deref(), Some("git status short"));
                assert_eq!(d.command, "git status");
            }
            _ => panic!("expected Detailed"),
        }
    }

    #[test]
    fn add_alias_without_description_stores_simple_form() {
        let mut config = Config::default();
        config.add_alias("ll".into(), "ls -lha".into(), false, None);
        let stored = config.aliases.get(&"ll".into()).unwrap();
        assert!(matches!(stored, TomlAlias::Command(_)));
    }

    #[test]
    fn add_subcommand_with_description_stores_detailed_form() {
        let mut config = Config::default();
        config.add_subcommand(
            "jj:b:l".into(),
            vec!["branch".into(), "list".into()],
            Some("list branches".into()),
        );
        match config.subcommands.as_ref().get("jj:b:l").unwrap() {
            crate::subcommand::TomlSubcommand::Detailed(d) => {
                assert_eq!(d.description.as_deref(), Some("list branches"));
                assert_eq!(d.expansions, vec!["branch", "list"]);
            }
            _ => panic!("expected Detailed"),
        }
    }

    #[test]
    fn add_subcommand_without_description_stores_simple_form() {
        let mut config = Config::default();
        config.add_subcommand("jj:ab".into(), vec!["abandon".into()], None);
        assert!(matches!(
            config.subcommands.as_ref().get("jj:ab").unwrap(),
            crate::subcommand::TomlSubcommand::Expansion(_)
        ));
    }
}
