use serde::Deserialize;
use thiserror::Error;
use tracing::{debug, info, warn};

use crate::capability::{CapabilitySpec, CustomCapability};
use crate::registry::{CapabilityError, CapabilityRegistry};

#[derive(Debug, Error)]
pub enum LoaderError {
    #[error("Failed to read file '{path}': {source}")]
    Io {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to parse TOML: {0}")]
    Parse(#[from] toml::de::Error),

    #[error("Registry error applying capabilities: {0}")]
    Registry(#[from] CapabilityError),
}

/// Partial override applied on top of an existing built-in capability.
///
/// Only fields set in TOML are applied; unset fields keep their default value.
/// The `id` field identifies which built-in to patch.
#[derive(Debug, Deserialize)]
pub struct CapabilityOverride {
    /// ID of the built-in capability to override (e.g., `"code-reviewer"`).
    pub id: String,
    pub system_prompt: Option<String>,
    pub preferred_provider: Option<String>,
    pub preferred_model: Option<String>,
    pub max_tokens: Option<u32>,
    pub temperature: Option<f32>,
    pub priority: Option<u32>,
    pub mcp_tools: Option<Vec<String>>,
}

/// Configuration file structure for capability customization.
///
/// ```toml
/// # Patch a built-in: use Claude Sonnet instead of Opus for code-reviewer
/// [[override]]
/// id = "code-reviewer"
/// preferred_model = "claude-sonnet-4-6"
/// max_tokens = 16384
///
/// # Add a custom capability not shipped as a built-in
/// [[custom]]
/// id = "db-optimizer"
/// display_name = "Database Optimizer"
/// description = "Analyzes and optimizes SQL queries and schema"
/// agent_role = "db_optimizer"
/// task_types = ["db_optimization", "query_review"]
/// system_prompt = "You are a database performance expert..."
/// mcp_tools = ["file_read", "code_search"]
/// preferred_provider = "claude"
/// preferred_model = "claude-sonnet-4-6"
/// max_tokens = 4096
/// temperature = 0.2
/// priority = 70
/// parallelizable = true
/// ```
#[derive(Debug, Deserialize)]
pub struct CapabilityConfig {
    /// Patches applied on top of existing built-in capabilities.
    #[serde(default, rename = "override")]
    pub overrides: Vec<CapabilityOverride>,
    /// Entirely new capabilities not shipped as built-ins.
    #[serde(default)]
    pub custom: Vec<CapabilitySpec>,
}

/// Loads capability overrides and custom capabilities from TOML configuration.
pub struct CapabilityLoader;

impl CapabilityLoader {
    /// Parse capability configuration from a TOML string.
    ///
    /// # Errors
    ///
    /// Returns [`LoaderError::Parse`] if the TOML is malformed.
    pub fn parse(toml_content: &str) -> Result<CapabilityConfig, LoaderError> {
        let config: CapabilityConfig = toml::from_str(toml_content)?;
        Ok(config)
    }

    /// Parse capability configuration from a file on disk.
    ///
    /// # Errors
    ///
    /// Returns [`LoaderError::Io`] if the file cannot be read,
    /// or [`LoaderError::Parse`] if the content is not valid TOML.
    pub fn from_file(path: &str) -> Result<CapabilityConfig, LoaderError> {
        let content = std::fs::read_to_string(path).map_err(|e| LoaderError::Io {
            path: path.to_string(),
            source: e,
        })?;
        Self::parse(&content)
    }

    /// Apply a parsed [`CapabilityConfig`] to a [`CapabilityRegistry`].
    ///
    /// - Overrides: existing built-in specs are patched with the non-None
    ///   fields. If the ID does not exist in the registry it is skipped with a
    ///   warning.
    /// - Custom: new capabilities are added. If the ID already exists it is
    ///   replaced (allowing idempotent re-application of config).
    ///
    /// # Errors
    ///
    /// Returns [`LoaderError::Registry`] only when a custom capability spec has
    /// an empty ID.
    pub fn apply(
        config: CapabilityConfig,
        registry: &CapabilityRegistry,
    ) -> Result<(), LoaderError> {
        let override_count = config.overrides.len();
        let custom_count = config.custom.len();

        // Apply overrides to existing capabilities
        for ov in config.overrides {
            if !registry.contains(&ov.id) {
                warn!(
                    "Capability override '{}' not found in registry — skipping",
                    ov.id
                );
                continue;
            }

            let base_arc = registry
                .get(&ov.id)
                .expect("contains check above ensures presence");
            let mut spec = base_arc.spec();

            if let Some(prompt) = ov.system_prompt {
                spec.system_prompt = prompt;
            }
            if let Some(provider) = ov.preferred_provider {
                spec.preferred_provider = provider;
            }
            if let Some(model) = ov.preferred_model {
                spec.preferred_model = model;
            }
            if let Some(max_tokens) = ov.max_tokens {
                spec.max_tokens = max_tokens;
            }
            if let Some(temperature) = ov.temperature {
                spec.temperature = temperature;
            }
            if let Some(priority) = ov.priority {
                spec.priority = priority;
            }
            if let Some(tools) = ov.mcp_tools {
                spec.mcp_tools = tools;
            }

            debug!("Applied override to capability '{}'", ov.id);
            registry.register_or_replace(CustomCapability(spec));
        }

        // Register custom capabilities (replace if already present)
        for spec in config.custom {
            if spec.id.is_empty() {
                return Err(LoaderError::Registry(CapabilityError::InvalidSpec(
                    "custom capability id must not be empty".to_string(),
                )));
            }
            debug!("Registering custom capability '{}'", spec.id);
            registry.register_or_replace(CustomCapability(spec));
        }

        info!(
            "Applied capability config: {} overrides, {} custom",
            override_count, custom_count
        );
        Ok(())
    }

    /// Convenience: parse from file and apply to registry in one call.
    pub fn load_and_apply(path: &str, registry: &CapabilityRegistry) -> Result<(), LoaderError> {
        let config = Self::from_file(path)?;
        Self::apply(config, registry)
    }
}

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

    #[test]
    fn parse_empty_config_succeeds() {
        let config = CapabilityLoader::parse("").unwrap();
        assert!(config.overrides.is_empty());
        assert!(config.custom.is_empty());
    }

    #[test]
    fn parse_override_only() {
        let toml = r#"
            [[override]]
            id = "code-reviewer"
            preferred_model = "claude-sonnet-4-6"
            max_tokens = 16384
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        assert_eq!(config.overrides.len(), 1);
        assert_eq!(config.overrides[0].id, "code-reviewer");
        assert_eq!(
            config.overrides[0].preferred_model.as_deref(),
            Some("claude-sonnet-4-6")
        );
        assert_eq!(config.overrides[0].max_tokens, Some(16384));
        assert!(config.overrides[0].system_prompt.is_none());
    }

    #[test]
    fn parse_custom_capability() {
        let toml = r#"
            [[custom]]
            id = "db-optimizer"
            display_name = "Database Optimizer"
            description = "Optimizes SQL queries"
            agent_role = "db_optimizer"
            task_types = ["db_optimization"]
            system_prompt = "You are a database expert."
            mcp_tools = ["file_read"]
            preferred_provider = "claude"
            preferred_model = "claude-sonnet-4-6"
            max_tokens = 2048
            temperature = 0.2
            priority = 65
            parallelizable = true
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        assert_eq!(config.custom.len(), 1);
        assert_eq!(config.custom[0].id, "db-optimizer");
        assert_eq!(config.custom[0].agent_role, "db_optimizer");
        assert_eq!(config.custom[0].max_tokens, 2048);
    }

    #[test]
    fn apply_override_changes_model() {
        let registry = CapabilityRegistry::with_built_ins();
        let original_model = registry
            .get("code-reviewer")
            .unwrap()
            .spec()
            .preferred_model
            .clone();

        let toml = r#"
            [[override]]
            id = "code-reviewer"
            preferred_model = "claude-sonnet-4-6"
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        CapabilityLoader::apply(config, &registry).unwrap();

        let updated = registry.activate("code-reviewer").unwrap();
        assert_eq!(updated.llm_model, "claude-sonnet-4-6");
        // The original is different (it was opus), so this proves the override took
        // effect
        assert_ne!(updated.llm_model, original_model);
    }

    #[test]
    fn apply_override_preserves_unset_fields() {
        let registry = CapabilityRegistry::with_built_ins();
        let original_spec = registry.get("code-reviewer").unwrap().spec();

        let toml = r#"
            [[override]]
            id = "code-reviewer"
            max_tokens = 4096
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        CapabilityLoader::apply(config, &registry).unwrap();

        let updated_spec = registry.get("code-reviewer").unwrap().spec();
        assert_eq!(updated_spec.max_tokens, 4096);
        // System prompt and provider should be unchanged
        assert_eq!(updated_spec.system_prompt, original_spec.system_prompt);
        assert_eq!(
            updated_spec.preferred_provider,
            original_spec.preferred_provider
        );
    }

    #[test]
    fn apply_unknown_override_id_is_skipped() {
        let registry = CapabilityRegistry::with_built_ins();
        let initial_count = registry.len();

        let toml = r#"
            [[override]]
            id = "nonexistent-cap"
            max_tokens = 999
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        // Should succeed without error — unknown override is skipped with a warning
        CapabilityLoader::apply(config, &registry).unwrap();

        // Registry count unchanged
        assert_eq!(registry.len(), initial_count);
    }

    #[test]
    fn apply_custom_adds_new_capability() {
        let registry = CapabilityRegistry::with_built_ins();
        let initial_count = registry.len();

        let toml = r#"
            [[custom]]
            id = "test-custom"
            display_name = "Test Custom"
            description = "A test custom capability"
            agent_role = "custom_tester"
            task_types = ["custom_task"]
            system_prompt = "You are a custom test agent."
            mcp_tools = []
            preferred_provider = "ollama"
            preferred_model = "llama3.2"
            max_tokens = 2048
            temperature = 0.5
            priority = 40
            parallelizable = true
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        CapabilityLoader::apply(config, &registry).unwrap();

        assert_eq!(registry.len(), initial_count + 1);
        assert!(registry.contains("test-custom"));

        let def = registry.activate("test-custom").unwrap();
        assert_eq!(def.role, "custom_tester");
        assert_eq!(def.llm_provider, "ollama");
        assert_eq!(def.system_prompt.unwrap(), "You are a custom test agent.");
    }

    #[test]
    fn apply_custom_replaces_existing_idempotently() {
        let registry = CapabilityRegistry::with_built_ins();

        let toml = r#"
            [[custom]]
            id = "replaceable"
            display_name = "V1"
            description = "Version 1"
            agent_role = "role_v1"
            task_types = ["task_v1"]
            system_prompt = "V1 prompt"
            mcp_tools = []
            preferred_provider = "claude"
            preferred_model = "claude-sonnet-4-6"
            max_tokens = 1024
            temperature = 0.5
            priority = 50
            parallelizable = true
        "#;

        let config = CapabilityLoader::parse(toml).unwrap();
        CapabilityLoader::apply(config, &registry).unwrap();

        let toml2 = r#"
            [[custom]]
            id = "replaceable"
            display_name = "V2"
            description = "Version 2"
            agent_role = "role_v2"
            task_types = ["task_v2"]
            system_prompt = "V2 prompt"
            mcp_tools = []
            preferred_provider = "claude"
            preferred_model = "claude-opus-4-6"
            max_tokens = 2048
            temperature = 0.3
            priority = 60
            parallelizable = false
        "#;

        let config2 = CapabilityLoader::parse(toml2).unwrap();
        CapabilityLoader::apply(config2, &registry).unwrap();

        let spec = registry.get("replaceable").unwrap().spec();
        assert_eq!(spec.agent_role, "role_v2");
        assert_eq!(spec.preferred_model, "claude-opus-4-6");
    }

    #[test]
    fn from_file_nonexistent_returns_io_error() {
        let err = CapabilityLoader::from_file("/nonexistent/path/capabilities.toml").unwrap_err();
        assert!(matches!(err, LoaderError::Io { .. }));
    }
}
