use anyhow::Context;
use serde::{Deserialize, Deserializer};
use std::fmt;

/// Supported LLM providers.
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Provider {
    #[default]
    OpenAI,
    Anthropic,
    Gemini,
    Ollama,
    OpenRouter,
    Xai,
    DeepSeek,
    Groq,
    Together,
    Perplexity,
    Mistral,
    Cohere,
    /// Generic OpenAI-compatible endpoint (custom URL required).
    #[serde(alias = "open_ai_compatible", alias = "openaicompatible")]
    OpenAiCompatible,
}

impl fmt::Display for Provider {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(match self {
            Self::OpenAI => "OpenAI",
            Self::Anthropic => "Anthropic",
            Self::Gemini => "Gemini",
            Self::Ollama => "Ollama",
            Self::OpenRouter => "OpenRouter",
            Self::Xai => "xAI",
            Self::DeepSeek => "DeepSeek",
            Self::Groq => "Groq",
            Self::Together => "Together",
            Self::Perplexity => "Perplexity",
            Self::Mistral => "Mistral",
            Self::Cohere => "Cohere",
            Self::OpenAiCompatible => "OpenAI-compatible",
        })
    }
}

#[derive(Debug)]
pub struct Config {
    pub model: ModelConfig,
    /// Diff command as a list of arguments. Use "$pre" and "$post" as
    /// placeholders for the pre/post change temp file paths.
    /// Example: ["difft", "--color", "always", "$pre", "$post"]
    /// When unset, auto-detects difft, delta, or diff.
    pub diff_tool: Option<Vec<String>>,
    /// Enable personality mode. When enabled, Rik will be more chatty about his work.
    pub personality: bool,
    /// Write question answers back into the source file instead of only printing them.
    pub write_answers: bool,
    /// Internal edit boundary policy.
    pub edition_constraints: EditionConstraints,
}

#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum EditionConstraints {
    /// Allow edits anywhere in the target file.
    None,
    /// Enforce marker vicinity before each edit_file call.
    VicinityBefore,
    /// Let the model edit freely, then keep only marker-vicinity chunks.
    VicinityAfter,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            model: ModelConfig::default(),
            diff_tool: None,
            personality: false,
            write_answers: false,
            edition_constraints: EditionConstraints::VicinityAfter,
        }
    }
}

#[derive(Deserialize)]
struct RawConfig {
    pub model: ModelConfig,
    #[serde(default)]
    pub diff_tool: Option<Vec<String>>,
    #[serde(default)]
    pub personality: bool,
    #[serde(default, rename = "write-answers", alias = "write_answers")]
    pub write_answers: bool,
    #[serde(default, rename = "edition-constraints", alias = "edition_constraints")]
    pub edition_constraints: Option<EditionConstraints>,
    #[serde(default)]
    pub marker_limits_edition_range: Option<bool>,
}

impl<'de> Deserialize<'de> for Config {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw = RawConfig::deserialize(deserializer)?;
        let edition_constraints = raw.edition_constraints.unwrap_or_else(|| {
            if raw.marker_limits_edition_range.unwrap_or(true) {
                EditionConstraints::VicinityAfter
            } else {
                EditionConstraints::None
            }
        });
        Ok(Self {
            model: raw.model,
            diff_tool: raw.diff_tool,
            personality: raw.personality,
            write_answers: raw.write_answers,
            edition_constraints,
        })
    }
}

#[derive(Deserialize, Debug, Default)]
pub struct ModelConfig {
    /// Which provider to use.
    ///
    /// Supported values: openai, anthropic, gemini, ollama, openrouter, xai,
    /// deepseek, groq, together, perplexity, mistral, cohere, openaicompatible.
    pub provider: Provider,

    /// API base URL. Optional — when omitted the provider default is used.
    /// Required for `openaicompatible`.
    ///
    /// Examples:
    /// ```text
    /// openai:           "https://api.openai.com/v1"                 (default)
    /// anthropic:        "https://api.anthropic.com"                  (default)
    /// gemini:           "https://generativelanguage.googleapis.com" (default)
    /// ollama:           "http://localhost:11434"                     (default)
    /// openaicompatible: "<your-endpoint>"                            (required)
    /// ```
    pub url: Option<String>,

    /// API key. Read from environment variable when omitted.
    ///
    /// Environment variables checked per provider:
    ///   openai:          OPENAI_API_KEY
    ///   anthropic:       ANTHROPIC_API_KEY
    ///   gemini:          GEMINI_API_KEY
    ///   ollama:          (no key needed, ignored)
    ///   openrouter:      OPENROUTER_API_KEY
    ///   xai:             XAI_API_KEY
    ///   deepseek:        DEEPSEEK_API_KEY
    ///   groq:            GROQ_API_KEY
    ///   together:        TOGETHER_API_KEY
    ///   perplexity:      PERPLEXITY_API_KEY
    ///   mistral:         MISTRAL_API_KEY
    ///   cohere:          COHERE_API_KEY
    ///   openaicompatible: OPENAI_API_KEY
    pub api_key: Option<String>,

    /// Model name (e.g. "gpt-4o", "claude-sonnet-4-20250514", "gemini-2.5-pro").
    pub model: String,
}

fn find_config_path() -> Option<std::path::PathBuf> {
    // Try ~/.config/rik/rik.toml first (Linux-style, works everywhere)
    if let Some(home) = dirs::home_dir() {
        let linux_style = home.join(".config").join("rik").join("rik.toml");
        if linux_style.exists() {
            return Some(linux_style);
        }
    }

    // Fall back to platform-specific config dir
    dirs::config_dir().map(|d| d.join("rik").join("rik.toml"))
}

pub fn load(model: Option<&str>) -> anyhow::Result<Config> {
    let config_path = find_config_path()
        .ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?;
    let contents = std::fs::read_to_string(&config_path)
        .with_context(|| format!("Failed to read config at {}", config_path.display()))?;
    parse(&contents, model).context("Failed to parse config.toml")
}

fn parse(contents: &str, model: Option<&str>) -> anyhow::Result<Config> {
    let mut config: toml::Value = toml::from_str(contents)?;

    let default_model = ["default_model", "default-model"]
        .into_iter()
        .find_map(|key| config.get(key).and_then(toml::Value::as_str));
    if let Some(model) = model.or(default_model) {
        let selected = select_model(&config, model)?;
        config
            .as_table_mut()
            .context("Config root must be a TOML table")?
            .insert("model".to_owned(), selected);
    }

    config.try_into().map_err(Into::into)
}

fn select_model(config: &toml::Value, name: &str) -> anyhow::Result<toml::Value> {
    let path: Vec<_> = name.split('.').collect();
    if path.is_empty() || path.iter().any(|part| part.is_empty()) {
        anyhow::bail!("Invalid model profile '{name}'");
    }

    for root in ["models", "model"] {
        if let Some(selected) = select_model_from_root(config, root, &path) {
            return Ok(selected);
        }
    }

    anyhow::bail!("Model profile '{name}' not found under [models] or [model]");
}

fn select_model_from_root(config: &toml::Value, root: &str, path: &[&str]) -> Option<toml::Value> {
    let mut current = config.get(root)?.as_table()?;
    let mut selected = toml::map::Map::new();

    inherit_model_fields(&mut selected, current);
    for part in path {
        current = current.get(*part)?.as_table()?;
        inherit_model_fields(&mut selected, current);
    }

    Some(toml::Value::Table(selected))
}

fn inherit_model_fields(
    selected: &mut toml::map::Map<String, toml::Value>,
    current: &toml::map::Map<String, toml::Value>,
) {
    for (key, value) in current {
        if !value.is_table() {
            selected.insert(key.clone(), value.clone());
        }
    }
}

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

    #[test]
    fn parses_existing_single_model_config() {
        let config = parse(
            r#"
                personality = true
                [model]
                provider = "anthropic"
                model = "claude"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::Anthropic);
        assert_eq!(config.model.model, "claude");
        assert!(config.personality);
        assert!(!config.write_answers);
        assert_eq!(
            config.edition_constraints,
            EditionConstraints::VicinityAfter
        );
    }

    #[test]
    fn parses_write_answers_config() {
        let config = parse(
            r#"
                write-answers = true
                [model]
                provider = "openai"
                model = "gpt"
            "#,
            None,
        )
        .unwrap();

        assert!(config.write_answers);
    }

    #[test]
    fn default_edition_constraints_are_vicinity_after() {
        assert_eq!(
            Config::default().edition_constraints,
            EditionConstraints::VicinityAfter
        );
    }

    #[test]
    fn parses_internal_edition_constraints() {
        let config = parse(
            r#"
                edition-constraints = "vicinity-after"

                [model]
                provider = "openai"
                model = "gpt"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(
            config.edition_constraints,
            EditionConstraints::VicinityAfter
        );
    }

    #[test]
    fn legacy_marker_limit_false_maps_to_no_constraints() {
        let config = parse(
            r#"
                marker_limits_edition_range = false

                [model]
                provider = "openai"
                model = "gpt"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(config.edition_constraints, EditionConstraints::None);
    }

    #[test]
    fn explicit_edition_constraints_override_legacy_marker_limit() {
        let config = parse(
            r#"
                marker_limits_edition_range = false
                edition-constraints = "vicinity-before"

                [model]
                provider = "openai"
                model = "gpt"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(
            config.edition_constraints,
            EditionConstraints::VicinityBefore
        );
    }

    #[test]
    fn selects_nested_model_and_inherits_parent_settings() {
        let config = parse(
            r#"
                [models.openrouter]
                provider = "openrouter"
                api_key = "aaaa"

                [models.openrouter.gpt120]
                model = "gpt-120:turbo"
            "#,
            Some("openrouter.gpt120"),
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::OpenRouter);
        assert_eq!(config.model.api_key.as_deref(), Some("aaaa"));
        assert_eq!(config.model.model, "gpt-120:turbo");
    }

    #[test]
    fn child_model_settings_override_parent_settings() {
        let config = parse(
            r#"
                [models.openrouter]
                provider = "openrouter"
                api_key = "parent"
                model = "default"

                [models.openrouter.fast]
                api_key = "child"
                model = "fast"
            "#,
            Some("openrouter.fast"),
        )
        .unwrap();

        assert_eq!(config.model.api_key.as_deref(), Some("child"));
        assert_eq!(config.model.model, "fast");
    }

    #[test]
    fn supports_named_profiles_under_model_table() {
        let config = parse(
            r#"
                [model.zai]
                provider = "openai"
                model = "zai-model"
            "#,
            Some("zai"),
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::OpenAI);
        assert_eq!(config.model.model, "zai-model");
    }

    #[test]
    fn selects_top_level_default_model_profile() {
        let config = parse(
            r#"
                default_model = "openrouter.mercury"

                [model.openrouter]
                provider = "openrouter"

                [model.openrouter.mercury]
                model = "mercury"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::OpenRouter);
        assert_eq!(config.model.model, "mercury");
    }

    #[test]
    fn supports_hyphenated_default_model_and_ignores_unselected_model_table() {
        let config = parse(
            r#"
                default-model = "openrouter.mercury"

                [model]

                [model.unselected]
                model = "missing-provider"

                [model.openrouter]
                provider = "openrouter"

                [model.openrouter.mercury]
                model = "mercury"
            "#,
            None,
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::OpenRouter);
        assert_eq!(config.model.model, "mercury");
    }

    #[test]
    fn explicit_model_overrides_top_level_default_model() {
        let config = parse(
            r#"
                default_model = "openrouter.mercury"

                [model.openrouter]
                provider = "openrouter"

                [model.openrouter.mercury]
                model = "mercury"

                [model.openrouter.fast]
                model = "fast"
            "#,
            Some("openrouter.fast"),
        )
        .unwrap();

        assert_eq!(config.model.provider, Provider::OpenRouter);
        assert_eq!(config.model.model, "fast");
    }

    #[test]
    fn reports_missing_model_profile() {
        let error = parse(
            "[models.openrouter]\napi_key = \"aaaa\"",
            Some("openrouter.missing"),
        )
        .unwrap_err();

        assert!(
            error
                .to_string()
                .contains("Model profile 'openrouter.missing' not found")
        );
    }

    #[test]
    fn requires_provider_in_selected_model_profile() {
        let error = parse(
            r#"
                [models.zai]
                model = "zai-model"
            "#,
            Some("zai"),
        )
        .unwrap_err();

        assert!(error.to_string().contains("missing field `provider`"));
    }
}
