use std::{fmt, fs, path::PathBuf, str::FromStr};

use serde::{Deserialize, Serialize};

use crate::{
    agent::CompactionConfig,
    credentials::{
        load_web_search_api_key, save_web_search_api_key, CredentialStore, OsCredentialStore,
        WebSearchCredential,
    },
    model::favorites::{favorite_model_values, normalized_favorite_models},
    paths,
    reasoning::ReasoningLevel,
};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
    pub provider: String,
    pub model: String,
    pub max_output_bytes: usize,
    pub max_tool_output_lines: usize,
    pub auth: String,
    pub reasoning: ReasoningLevel,
    pub show_reasoning_output: bool,
    pub auto_compact: bool,
    pub compact_threshold_percent: u8,
    pub compact_target_percent: u8,
    pub title_provider: Option<String>,
    pub title_model: Option<String>,
    pub title_auth: Option<String>,
    pub favorite_models: Vec<String>,
    pub web_search_provider: SearchProvider,
    pub check_for_updates: bool,
    #[serde(flatten)]
    pub(crate) legacy_web_search_credentials: LegacyWebSearchCredentials,
    pub rtk: bool,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            provider: "openai".into(),
            model: "gpt-5.5".into(),
            max_output_bytes: 12000,
            max_tool_output_lines: 10,
            auth: "api-key".into(),
            reasoning: ReasoningLevel::Medium,
            show_reasoning_output: true,
            auto_compact: false,
            compact_threshold_percent: 85,
            compact_target_percent: 50,
            title_provider: None,
            title_model: None,
            title_auth: None,
            favorite_models: Vec::new(),
            web_search_provider: SearchProvider::Auto,
            check_for_updates: true,
            legacy_web_search_credentials: LegacyWebSearchCredentials::default(),
            rtk: true,
        }
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SearchProvider {
    #[default]
    Auto,
    OpenAi,
    Exa,
    Brave,
    Parallel,
    Tavily,
    Perplexity,
    Gemini,
    Disabled,
}

impl SearchProvider {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Auto => "auto",
            Self::OpenAi => "openai",
            Self::Exa => "exa",
            Self::Brave => "brave",
            Self::Parallel => "parallel",
            Self::Tavily => "tavily",
            Self::Perplexity => "perplexity",
            Self::Gemini => "gemini",
            Self::Disabled => "disabled",
        }
    }

    pub fn from_config_value(value: &str) -> Self {
        match value.trim().to_ascii_lowercase().as_str() {
            "auto" => Self::Auto,
            "openai" => Self::OpenAi,
            "exa" => Self::Exa,
            "brave" => Self::Brave,
            "disabled" => Self::Disabled,
            _ => Self::Auto,
        }
    }

    pub const fn next_configurable(self) -> Self {
        match self {
            Self::Auto => Self::OpenAi,
            Self::OpenAi => Self::Exa,
            Self::Exa => Self::Brave,
            Self::Brave => Self::Disabled,
            Self::Disabled | Self::Parallel | Self::Tavily | Self::Perplexity | Self::Gemini => {
                Self::Auto
            }
        }
    }
}

impl fmt::Display for SearchProvider {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for SearchProvider {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.trim().to_ascii_lowercase().as_str() {
            "auto" => Ok(Self::Auto),
            "openai" => Ok(Self::OpenAi),
            "exa" => Ok(Self::Exa),
            "brave" => Ok(Self::Brave),
            "parallel" => Ok(Self::Parallel),
            "tavily" => Ok(Self::Tavily),
            "perplexity" => Ok(Self::Perplexity),
            "gemini" => Ok(Self::Gemini),
            "disabled" => Ok(Self::Disabled),
            other => Err(format!("unknown search provider: {other}")),
        }
    }
}

impl Serialize for SearchProvider {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for SearchProvider {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(serde::de::Error::custom)
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
pub(crate) struct LegacyWebSearchCredentials {
    #[serde(
        default,
        rename = "web_search_openai_api_key",
        skip_serializing_if = "Option::is_none"
    )]
    openai: Option<String>,
    #[serde(
        default,
        rename = "web_search_exa_api_key",
        skip_serializing_if = "Option::is_none"
    )]
    exa: Option<String>,
    #[serde(
        default,
        rename = "web_search_brave_api_key",
        skip_serializing_if = "Option::is_none"
    )]
    brave: Option<String>,
}

impl LegacyWebSearchCredentials {
    fn get(&self, credential: WebSearchCredential) -> Option<&str> {
        match credential {
            WebSearchCredential::OpenAi => self.openai.as_deref(),
            WebSearchCredential::Exa => self.exa.as_deref(),
            WebSearchCredential::Brave => self.brave.as_deref(),
        }
    }

    fn clear(&mut self, credential: WebSearchCredential) {
        match credential {
            WebSearchCredential::OpenAi => self.openai = None,
            WebSearchCredential::Exa => self.exa = None,
            WebSearchCredential::Brave => self.brave = None,
        }
    }
}

fn write_config(path: &PathBuf, config: &Config) -> anyhow::Result<()> {
    fs::write(path, toml::to_string_pretty(config)?)?;
    Ok(())
}

impl Config {
    pub fn default_path() -> anyhow::Result<PathBuf> {
        Ok(paths::rho_dir()?.join("config.toml"))
    }

    pub fn load(path: Option<PathBuf>) -> anyhow::Result<Self> {
        let path = path.map(Ok).unwrap_or_else(Self::default_path)?;
        Self::load_with_store(path, &OsCredentialStore)
    }

    fn load_with_store(path: PathBuf, store: &dyn CredentialStore) -> anyhow::Result<Self> {
        if !path.exists() {
            Config::default().save_with_store(path.clone(), store)?;
        }

        let mut cfg = Config::default();
        let text = fs::read_to_string(&path)?;
        let file: PartialConfig = toml::from_str(&text)?;
        if let Some(v) = file.provider {
            cfg.provider = v;
        }
        if let Some(v) = file.model {
            cfg.model = v;
        }
        if let Some(v) = file.max_output_bytes {
            cfg.max_output_bytes = v;
        }
        if let Some(v) = file.max_tool_output_lines {
            cfg.max_tool_output_lines = v.max(1);
        }
        if let Some(v) = file.auth {
            cfg.auth = v;
        }
        if let Some(v) = file.reasoning {
            cfg.reasoning = v;
        } else if let Some(v) = file.reasoning_effort {
            cfg.reasoning = v.parse()?;
        }
        if let Some(v) = file.show_reasoning_output {
            cfg.show_reasoning_output = v;
        }
        if let Some(v) = file.auto_compact {
            cfg.auto_compact = v;
        }
        if let Some(v) = file.compact_threshold_percent {
            cfg.set_compact_threshold_percent(v);
        }
        if let Some(v) = file.compact_target_percent {
            cfg.set_compact_target_percent(v);
        }
        if let Some(v) = file.title_provider {
            cfg.title_provider = Some(v);
        }
        if let Some(v) = file.title_model {
            cfg.title_model = Some(v);
        }
        if let Some(v) = file.title_auth {
            cfg.title_auth = Some(v);
        }
        if let Some(v) = file.favorite_models {
            cfg.favorite_models = favorite_model_values(&normalized_favorite_models(&v));
        }
        if let Some(v) = file.web_search_provider {
            cfg.web_search_provider = SearchProvider::from_config_value(&v);
        }
        if let Some(v) = file.check_for_updates {
            cfg.check_for_updates = v;
        }
        cfg.legacy_web_search_credentials = LegacyWebSearchCredentials {
            openai: file.web_search_openai_api_key.and_then(non_empty_secret),
            exa: file.web_search_exa_api_key.and_then(non_empty_secret),
            brave: file.web_search_brave_api_key.and_then(non_empty_secret),
        };
        if let Some(v) = file.rtk {
            cfg.rtk = v;
        }
        if matches!(cfg.migrate_legacy_web_search_credentials(store), Ok(true)) {
            let _cleanup_result = write_config(&path, &cfg);
        }
        Ok(cfg)
    }

    pub fn save(&self, path: Option<PathBuf>) -> anyhow::Result<()> {
        let path = path.map(Ok).unwrap_or_else(Self::default_path)?;
        self.save_with_store(path, &OsCredentialStore)
    }

    fn save_with_store(&self, path: PathBuf, store: &dyn CredentialStore) -> anyhow::Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let mut config = self.clone();
        config.normalize_compaction_percentages();
        config.favorite_models =
            favorite_model_values(&normalized_favorite_models(&config.favorite_models));
        let _migration_result = config.migrate_legacy_web_search_credentials(store);
        write_config(&path, &config)?;
        Ok(())
    }

    pub fn set_compact_threshold_percent(&mut self, value: u8) {
        self.compact_threshold_percent = clamp_percent(value);
        self.normalize_compaction_percentages();
    }

    pub fn set_compact_target_percent(&mut self, value: u8) {
        self.compact_target_percent = clamp_percent(value);
        self.normalize_compaction_percentages();
    }

    pub(crate) fn legacy_web_search_api_key(
        &self,
        credential: WebSearchCredential,
    ) -> Option<&str> {
        self.legacy_web_search_credentials.get(credential)
    }

    fn migrate_legacy_web_search_credentials(
        &mut self,
        store: &dyn CredentialStore,
    ) -> crate::credentials::CredentialResult<bool> {
        let mut changed = false;
        for credential in WebSearchCredential::ALL {
            let Some(secret) = self
                .legacy_web_search_credentials
                .get(credential)
                .map(str::to_string)
            else {
                continue;
            };
            if load_web_search_api_key(store, credential)?.is_none() {
                save_web_search_api_key(store, credential, &secret)?;
            }
            self.legacy_web_search_credentials.clear(credential);
            changed = true;
        }
        Ok(changed)
    }

    fn normalize_compaction_percentages(&mut self) {
        self.compact_threshold_percent = clamp_percent(self.compact_threshold_percent);
        self.compact_target_percent = normalized_compact_target_percent(
            self.compact_threshold_percent,
            self.compact_target_percent,
        );
    }
}

impl From<&Config> for CompactionConfig {
    fn from(config: &Config) -> Self {
        Self {
            auto_compact: config.auto_compact,
            threshold_percent: config.compact_threshold_percent,
            target_percent: config.compact_target_percent,
        }
    }
}

#[derive(Deserialize)]
struct PartialConfig {
    provider: Option<String>,
    model: Option<String>,
    max_output_bytes: Option<usize>,
    max_tool_output_lines: Option<usize>,
    auth: Option<String>,
    reasoning: Option<ReasoningLevel>,
    reasoning_effort: Option<String>,
    show_reasoning_output: Option<bool>,
    auto_compact: Option<bool>,
    compact_threshold_percent: Option<u8>,
    compact_target_percent: Option<u8>,
    #[allow(dead_code)]
    compact_recent_messages: Option<usize>,
    title_provider: Option<String>,
    title_model: Option<String>,
    title_auth: Option<String>,
    favorite_models: Option<Vec<String>>,
    web_search_provider: Option<String>,
    check_for_updates: Option<bool>,
    web_search_openai_api_key: Option<String>,
    web_search_exa_api_key: Option<String>,
    web_search_brave_api_key: Option<String>,
    rtk: Option<bool>,
}

fn non_empty_secret(secret: String) -> Option<String> {
    let secret = secret.trim().to_string();
    (!secret.is_empty()).then_some(secret)
}

fn clamp_percent(value: u8) -> u8 {
    value.clamp(1, 100)
}

fn normalized_compact_target_percent(threshold_percent: u8, target_percent: u8) -> u8 {
    let threshold_percent = clamp_percent(threshold_percent);
    let target_percent = clamp_percent(target_percent);
    if threshold_percent == 1 {
        1
    } else {
        target_percent.min(threshold_percent - 1)
    }
}

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

    #[test]
    fn default_shows_reasoning_output() {
        assert!(Config::default().show_reasoning_output);
    }

    #[test]
    fn loads_reasoning_output_visibility() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(&path, "show_reasoning_output = false\n").unwrap();

        let config = Config::load(Some(path)).unwrap();

        assert!(!config.show_reasoning_output);
    }

    #[test]
    fn loads_check_for_updates() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(&path, "check_for_updates = false\n").unwrap();

        let config = Config::load(Some(path)).unwrap();

        assert!(!config.check_for_updates);
    }

    #[test]
    fn loads_and_normalizes_compaction_percentages() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(
            &path,
            "auto_compact = true\ncompact_threshold_percent = 80\ncompact_target_percent = 95\n",
        )
        .unwrap();

        let config = Config::load(Some(path)).unwrap();

        assert!(config.auto_compact);
        assert_eq!(config.compact_threshold_percent, 80);
        assert_eq!(config.compact_target_percent, 79);
    }

    #[test]
    fn loads_rtk_toggle() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(&path, "rtk = false\n").unwrap();

        let config = Config::load(Some(path)).unwrap();

        assert!(!config.rtk);
    }

    #[test]
    fn loads_and_saves_favorite_models() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(
            &path,
            r#"
favorite_models = [
  " openai/gpt-5.5 ",
  "missing-separator",
  "openai/gpt-5.5",
  "anthropic/claude-sonnet-4-5",
]
"#,
        )
        .unwrap();

        let config = Config::load(Some(path.clone())).unwrap();

        assert_eq!(
            config.favorite_models,
            vec!["openai/gpt-5.5", "anthropic/claude-sonnet-4-5"]
        );

        config.save(Some(path.clone())).unwrap();
        let saved = std::fs::read_to_string(path).unwrap();
        assert!(saved.contains("favorite_models"), "{saved}");
        assert!(saved.contains("openai/gpt-5.5"), "{saved}");
        assert!(!saved.contains("missing-separator"), "{saved}");
    }

    #[test]
    fn unsupported_web_search_config_providers_fall_back_to_auto() {
        for provider in ["parallel", "tavily", "perplexity", "gemini", "unknown"] {
            assert_eq!(
                super::SearchProvider::from_config_value(provider),
                super::SearchProvider::Auto
            );
        }
    }

    #[test]
    fn supported_web_search_config_provider_is_preserved() {
        assert_eq!(
            super::SearchProvider::from_config_value(" brave "),
            super::SearchProvider::Brave
        );
    }

    #[test]
    fn save_preserves_legacy_web_search_keys_when_credentials_are_unavailable() {
        struct UnavailableCredentialStore;

        impl crate::credentials::CredentialStore for UnavailableCredentialStore {
            fn get_secret(
                &self,
                _account: &str,
            ) -> crate::credentials::CredentialResult<Option<String>> {
                Err(crate::credentials::CredentialError::StoreUnavailable(
                    "test store unavailable".into(),
                ))
            }

            fn set_secret(
                &self,
                _account: &str,
                _secret: &str,
            ) -> crate::credentials::CredentialResult<()> {
                Err(crate::credentials::CredentialError::StoreUnavailable(
                    "test store unavailable".into(),
                ))
            }

            fn delete_secret(&self, _account: &str) -> crate::credentials::CredentialResult<bool> {
                Err(crate::credentials::CredentialError::StoreUnavailable(
                    "test store unavailable".into(),
                ))
            }
        }

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        let config = Config {
            rtk: false,
            legacy_web_search_credentials: super::LegacyWebSearchCredentials {
                openai: Some("sk-test".into()),
                exa: None,
                brave: None,
            },
            ..Config::default()
        };

        config
            .save_with_store(path.clone(), &UnavailableCredentialStore)
            .unwrap();

        let saved = std::fs::read_to_string(path).unwrap();
        assert!(
            saved.contains("web_search_openai_api_key = \"sk-test\""),
            "{saved}"
        );
        assert!(saved.contains("rtk = false"), "{saved}");
    }

    #[cfg(unix)]
    #[test]
    fn load_succeeds_when_migrated_credential_cleanup_cannot_be_written() {
        use std::os::unix::fs::PermissionsExt;

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        std::fs::write(&path, "web_search_openai_api_key = \"sk-test\"\n").unwrap();
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444)).unwrap();
        let store = crate::credentials::MemoryCredentialStore::default();

        let config = Config::load_with_store(path.clone(), &store).unwrap();

        assert_eq!(
            crate::credentials::load_web_search_api_key(
                &store,
                crate::credentials::WebSearchCredential::OpenAi
            )
            .unwrap()
            .as_deref(),
            Some("sk-test")
        );
        assert_eq!(
            config.legacy_web_search_api_key(crate::credentials::WebSearchCredential::OpenAi),
            None
        );
        let saved = std::fs::read_to_string(&path).unwrap();
        assert!(saved.contains("web_search_openai_api_key"), "{saved}");

        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
    }

    #[test]
    fn migrates_legacy_web_search_keys_to_credentials() {
        let store = crate::credentials::MemoryCredentialStore::default();
        let mut config = Config {
            legacy_web_search_credentials: super::LegacyWebSearchCredentials {
                openai: Some("sk-test".into()),
                exa: Some("exa-test".into()),
                brave: Some("BSA-test".into()),
            },
            ..Config::default()
        };

        assert!(config
            .migrate_legacy_web_search_credentials(&store)
            .unwrap());
        assert_eq!(
            crate::credentials::load_web_search_api_key(
                &store,
                crate::credentials::WebSearchCredential::OpenAi
            )
            .unwrap()
            .as_deref(),
            Some("sk-test")
        );
        assert_eq!(
            config.legacy_web_search_api_key(crate::credentials::WebSearchCredential::OpenAi),
            None
        );
    }

    #[test]
    fn saved_config_omits_migrated_web_search_secrets() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("config.toml");
        let config = Config::default();

        super::write_config(&path, &config).unwrap();

        let saved = std::fs::read_to_string(path).unwrap();
        assert!(!saved.contains("web_search_openai_api_key"), "{saved}");
        assert!(!saved.contains("web_search_exa_api_key"), "{saved}");
        assert!(!saved.contains("web_search_brave_api_key"), "{saved}");
    }
}
