//! Provider factory for creating providers from configuration.
//!
//! This module provides a factory pattern for creating provider instances
//! from model configurations loaded from config.yml.

use crate::config::{
    load_config, load_default_config, resolve_secret, GatewayConfig, ModelConfig, Protocol,
};
use anyhow::{anyhow, Context, Result};
use enact_config::Config;
use enact_core::providers::ModelProvider;
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, RwLock};

#[cfg(feature = "provider-anthropic")]
use crate::Anthropic;
#[cfg(feature = "provider-azure")]
use crate::AzureOpenAI;
use crate::AzureSpeech;
#[cfg(feature = "provider-openai-compatible")]
use crate::OpenAICompatible;
#[cfg(feature = "provider-openrouter")]
use crate::OpenRouter;

/// Factory for creating provider instances from model configuration.
pub struct ProviderFactory;

impl ProviderFactory {
    /// Create a provider instance from model configuration.
    ///
    /// # Arguments
    ///
    /// * `model_id` - The model identifier (e.g., "azure_foundry:text:gpt-5-nano")
    /// * `config` - The model configuration
    ///
    /// # Returns
    ///
    /// A boxed provider instance or an error if the provider type is unknown
    /// or the API key cannot be resolved.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use enact_providers::config::load_config;
    /// use enact_providers::factory::ProviderFactory;
    ///
    /// let config = load_config("config.yml").unwrap();
    /// let model_config = config.get_model("azure_foundry:text:gpt-5-nano").unwrap();
    /// let provider = ProviderFactory::create("azure_foundry:text:gpt-5-nano", model_config).unwrap();
    /// ```
    pub fn create(
        model_id: &str,
        config: &ModelConfig,
    ) -> Result<Box<dyn ModelProvider + Send + Sync>> {
        // Resolve the API key from environment
        let api_key = resolve_secret(&config.secrets.name)
            .with_context(|| format!("Failed to resolve API key for model '{}'", model_id))?;

        Self::create_with_key(model_id, config, &api_key)
    }

    /// Create a provider instance with an explicitly provided API key.
    ///
    /// Use this method when you want to provide the API key directly
    /// instead of resolving it from environment variables.
    ///
    /// Routing is based on `runtime.protocol` to determine which provider
    /// implementation to use for the API call.
    pub fn create_with_key(
        model_id: &str,
        config: &ModelConfig,
        api_key: &str,
    ) -> Result<Box<dyn ModelProvider + Send + Sync>> {
        match config.runtime.protocol {
            #[cfg(feature = "provider-azure")]
            Protocol::AzureSdk => {
                let provider = AzureOpenAI::from_model_config(model_id, config, api_key)?;
                Ok(Box::new(provider))
            }
            #[cfg(feature = "provider-openrouter")]
            Protocol::OpenrouterSdk => {
                let provider = OpenRouter::from_model_config(model_id, config, api_key)?;
                Ok(Box::new(provider))
            }
            #[cfg(feature = "provider-anthropic")]
            Protocol::AnthropicSdk => {
                let provider = Anthropic::from_model_config(model_id, config, api_key)?;
                Ok(Box::new(provider))
            }
            #[cfg(feature = "provider-openai-compatible")]
            Protocol::OpenAICompatible => {
                let provider = OpenAICompatible::from_model_config(model_id, config, api_key)?;
                Ok(Box::new(provider))
            }
            Protocol::AzureSpeech => {
                // Azure Cognitive Speech Services (SSML-based TTS, raw audio STT)
                let provider = AzureSpeech::from_model_config(model_id, config, api_key)?;
                Ok(Box::new(provider))
            }
            #[allow(unreachable_patterns)]
            protocol => Err(anyhow!(
                "Unsupported protocol '{:?}' for model '{}'. Enable the appropriate provider feature.",
                protocol,
                model_id
            )),
        }
    }
}

/// Provider registry for caching and managing provider instances.
///
/// The registry loads configuration from a YAML file and creates
/// provider instances on demand, caching them for reuse.
pub struct ProviderRegistry {
    config: GatewayConfig,
    providers: RwLock<HashMap<String, Arc<dyn ModelProvider + Send + Sync>>>,
}

impl ProviderRegistry {
    /// Create a new provider registry from a configuration.
    pub fn new(config: GatewayConfig) -> Self {
        Self {
            config,
            providers: RwLock::new(HashMap::new()),
        }
    }

    /// Create a new provider registry by loading configuration from a file.
    pub fn from_config_file(path: impl AsRef<Path>) -> Result<Self> {
        let config = load_config(path)?;
        Ok(Self::new(config))
    }

    /// Get or create a provider for the given model ID.
    ///
    /// If the provider has already been created, returns the cached instance.
    /// Otherwise, creates a new provider and caches it.
    pub fn get_provider(&self, model_id: &str) -> Result<Arc<dyn ModelProvider + Send + Sync>> {
        // Check cache first
        {
            let cache = self.providers.read().unwrap();
            if let Some(provider) = cache.get(model_id) {
                return Ok(Arc::clone(provider));
            }
        }

        // Get model configuration
        let model_config = self
            .config
            .get_model(model_id)
            .ok_or_else(|| anyhow!("Model '{}' not found in configuration", model_id))?;

        // Create provider
        let provider = ProviderFactory::create(model_id, model_config)?;
        let provider: Arc<dyn ModelProvider + Send + Sync> = Arc::from(provider);

        // Cache it
        {
            let mut cache = self.providers.write().unwrap();
            cache.insert(model_id.to_string(), Arc::clone(&provider));
        }

        Ok(provider)
    }

    /// List all available model IDs.
    pub fn list_models(&self) -> Vec<&String> {
        self.config.list_models()
    }

    /// Get the number of models in the configuration.
    pub fn model_count(&self) -> usize {
        self.config.model_count()
    }

    /// Get the number of cached provider instances.
    pub fn cached_provider_count(&self) -> usize {
        self.providers.read().unwrap().len()
    }

    /// Clear the provider cache.
    pub fn clear_cache(&self) {
        self.providers.write().unwrap().clear();
    }

    /// Get the underlying configuration.
    pub fn config(&self) -> &GatewayConfig {
        &self.config
    }
}

/// Create the default provider from config (config-driven).
///
/// Model selection order: `default_model_id` from ~/.enact/config.yaml, then
/// first text-capable model from providers, then first model.
///
/// # Example
///
/// ```no_run
/// use enact_providers::factory::create_default_provider;
///
/// let provider = create_default_provider().unwrap();
/// ```
pub fn create_default_provider() -> Result<Arc<dyn ModelProvider + Send + Sync>> {
    let config = load_default_config()?;
    let model_id = Config::load_from_home()
        .ok()
        .and_then(|c| c.default_model_id)
        .filter(|s| !s.is_empty())
        .or_else(|| {
            let mut ids: Vec<String> = config.list_models().into_iter().cloned().collect();
            ids.sort();
            // Prefer text-capable models when no explicit default is set
            ids.iter()
                .find(|id| id.contains(":text:"))
                .or_else(|| ids.first())
                .cloned()
        })
        .unwrap_or_default();
    if model_id.is_empty() {
        anyhow::bail!(
            "No default model. Set default_model_id in ~/.enact/config.yaml, or add models to providers.yaml."
        );
    }
    let model_config = config
        .get_model(&model_id)
        .ok_or_else(|| anyhow::anyhow!("Model '{}' not found in configuration", model_id))?;
    let _api_key = resolve_secret(&model_config.secrets.name)
        .with_context(|| format!("Failed to resolve API key for default model '{}'", model_id))?;
    let provider = ProviderFactory::create(&model_id, model_config)?;
    Ok(Arc::from(provider))
}

/// Create a provider for a specific model ID.
///
/// Used by agents that specify a model override in their agent.yaml.
/// Loads the model configuration from the default providers config.
///
/// # Arguments
///
/// * `model_id` - The model identifier (e.g., "azure_foundry:text:gpt-4-1-mini")
///
/// # Example
///
/// ```no_run
/// use enact_providers::factory::create_provider_for_model;
///
/// let provider = create_provider_for_model("azure_foundry:text:gpt-4-1-mini").unwrap();
/// ```
pub fn create_provider_for_model(model_id: &str) -> Result<Arc<dyn ModelProvider + Send + Sync>> {
    let config = load_default_config()?;
    let model_config = config
        .get_model(model_id)
        .ok_or_else(|| anyhow::anyhow!("Model '{}' not found in configuration", model_id))?;
    let provider = ProviderFactory::create(model_id, model_config)?;
    Ok(Arc::from(provider))
}

/// Create a provider registry from the default config (ENACT_PROVIDERS_CONFIG_PATH or cwd/~/.enact/providers.yaml).
///
/// # Example
///
/// ```no_run
/// use enact_providers::factory::create_default_registry;
///
/// let registry = create_default_registry().unwrap();
/// let provider = registry.get_provider("azure_foundry:text:gpt-5-nano").unwrap();
/// ```
pub fn create_default_registry() -> Result<ProviderRegistry> {
    let config = load_default_config()?;
    Ok(ProviderRegistry::new(config))
}

/// Convenience function to create a provider from a config file.
///
/// # Arguments
///
/// * `model_id` - The model identifier
/// * `config_path` - Path to the configuration file
///
/// # Example
///
/// ```no_run
/// use enact_providers::factory::create_provider;
///
/// let provider = create_provider("azure_foundry:text:gpt-5-nano", "config.yml").unwrap();
/// ```
pub fn create_provider(
    model_id: &str,
    config_path: impl AsRef<Path>,
) -> Result<Box<dyn ModelProvider + Send + Sync>> {
    let config = load_config(config_path)?;
    let model_config = config
        .get_model(model_id)
        .ok_or_else(|| anyhow!("Model '{}' not found in configuration", model_id))?;
    ProviderFactory::create(model_id, model_config)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{
        AuthConfig, AuthType, CapabilitiesConfig, CostConfig, GovernanceConfig, LifecycleStatus,
        Modality, Protocol, RuntimeConfig, SecretType, SecretsConfig,
    };
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn create_test_config_yaml() -> String {
        r#"
models:
  test:text:azure-model:
    display_name: Azure Test Model
    provider_name: azure-foundry
    input_modality:
      - text
    output_modality:
      - text
    runtime:
      base_url: "https://test.azure.com"
      full_url: "https://test.azure.com/openai/deployments/test/chat/completions?api-version=2024-05-01"
      protocol: azure_sdk
      api_version: "2024-05-01"
      use_max_completion_tokens: false
      has_temperature_supported: true
    auth:
      type: header
    secrets:
      type: environment
      name: TEST_AZURE_API_KEY
    capabilities:
      tool_calls: true
      reasoning: false
    governance:
      is_use_approve: true
      approved_by: admin
      approval_date: '2024-01-01T00:00:00Z'
      lifecycle_status: active
    cost:
      provider: portkey
      currency: USD
      cost_per_1m_input: 0.001
      cost_per_1m_output: 0.002
  test:text:openrouter-model:
    display_name: OpenRouter Test Model
    provider_name: openrouter
    input_modality:
      - text
    output_modality:
      - text
    runtime:
      base_url: "https://openrouter.ai/api/v1"
      full_url: "https://openrouter.ai/api/v1/chat/completions"
      protocol: openrouter_sdk
      api_version: v1
      use_max_completion_tokens: false
      has_temperature_supported: true
    auth:
      type: bearer
    secrets:
      type: environment
      name: TEST_OPENROUTER_API_KEY
    capabilities:
      tool_calls: true
      reasoning: false
    governance:
      is_use_approve: true
      approved_by: admin
      approval_date: '2024-01-01T00:00:00Z'
      lifecycle_status: active
    cost:
      provider: openrouter
      provider_key: "test/model"
      currency: USD
      cost_per_1m_input: 0.0005
      cost_per_1m_output: 0.001
"#
        .to_string()
    }

    #[allow(dead_code)] // test helper for configs that only need OpenAI-compatible protocol
    fn create_mock_model_config(provider_name: &str) -> ModelConfig {
        create_mock_model_config_with_protocol(provider_name, Protocol::OpenAICompatible)
    }

    fn create_mock_model_config_with_protocol(
        provider_name: &str,
        protocol: Protocol,
    ) -> ModelConfig {
        ModelConfig {
            display_name: "Test Model".to_string(),
            provider_name: provider_name.to_string(),
            input_modality: vec![Modality::Text],
            output_modality: vec![Modality::Text],
            runtime: RuntimeConfig {
                base_url: "https://test.example.com".to_string(),
                full_url: "https://test.example.com/v1/chat/completions".to_string(),
                protocol,
                api_version: "v1".to_string(),
                use_max_completion_tokens: false,
                has_temperature_supported: true,
                additional_notes: vec![],
                model_param: false,
                timeout_seconds: 60,
                max_tokens_default: 4096,
                temperature_default: 0.7,
            },
            auth: AuthConfig {
                auth_type: AuthType::Bearer,
            },
            secrets: SecretsConfig {
                secret_type: SecretType::Environment,
                name: "TEST_API_KEY".to_string(),
            },
            capabilities: CapabilitiesConfig {
                tool_calls: true,
                reasoning: false,
            },
            governance: GovernanceConfig {
                is_use_approve: true,
                approved_by: Some("admin".to_string()),
                approval_date: Some("2024-01-01".to_string()),
                lifecycle_status: LifecycleStatus::Active,
            },
            cost: CostConfig::default(),
            fallbacks: vec![],
        }
    }

    #[test]
    #[cfg(feature = "provider-azure")]
    fn test_factory_creates_azure_provider() {
        let config = create_mock_model_config_with_protocol("azure-foundry", Protocol::AzureSdk);
        let provider =
            ProviderFactory::create_with_key("test:text:azure", &config, "test-key").unwrap();
        assert_eq!(provider.name(), "azure-foundry");
    }

    #[test]
    #[cfg(feature = "provider-openrouter")]
    fn test_factory_creates_openrouter_provider() {
        let config = create_mock_model_config_with_protocol("openrouter", Protocol::OpenrouterSdk);
        let provider =
            ProviderFactory::create_with_key("test:text:openrouter", &config, "test-key").unwrap();
        assert_eq!(provider.name(), "openrouter");
    }

    #[test]
    fn test_factory_fails_for_unknown_provider() {
        let config = create_mock_model_config_with_protocol("unknown-provider", Protocol::VertexAi);
        let result = ProviderFactory::create_with_key("test", &config, "key");
        assert!(result.is_err());
        let err = result.err().unwrap();
        assert!(err.to_string().contains("Unsupported protocol"));
    }

    #[test]
    fn test_registry_from_config_file() {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(create_test_config_yaml().as_bytes())
            .unwrap();

        let registry = ProviderRegistry::from_config_file(file.path()).unwrap();
        assert_eq!(registry.model_count(), 2);
    }

    #[test]
    fn test_registry_list_models() {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(create_test_config_yaml().as_bytes())
            .unwrap();

        let registry = ProviderRegistry::from_config_file(file.path()).unwrap();
        let models = registry.list_models();
        assert_eq!(models.len(), 2);
    }

    #[test]
    #[cfg(feature = "provider-azure")]
    fn test_registry_caches_providers() {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(create_test_config_yaml().as_bytes())
            .unwrap();

        // Set environment variable for test
        std::env::set_var("TEST_AZURE_API_KEY", "test-key");

        let registry = ProviderRegistry::from_config_file(file.path()).unwrap();

        // First call creates provider
        let provider1 = registry.get_provider("test:text:azure-model").unwrap();
        assert_eq!(registry.cached_provider_count(), 1);

        // Second call returns cached
        let provider2 = registry.get_provider("test:text:azure-model").unwrap();
        assert_eq!(registry.cached_provider_count(), 1);

        // Should be same Arc
        assert!(Arc::ptr_eq(&provider1, &provider2));

        // Cleanup
        std::env::remove_var("TEST_AZURE_API_KEY");
    }

    #[test]
    fn test_registry_clear_cache() {
        let mut file = NamedTempFile::new().unwrap();
        file.write_all(create_test_config_yaml().as_bytes())
            .unwrap();

        std::env::set_var("TEST_AZURE_API_KEY", "test-key");

        let registry = ProviderRegistry::from_config_file(file.path()).unwrap();
        let _ = registry.get_provider("test:text:azure-model");
        assert_eq!(registry.cached_provider_count(), 1);

        registry.clear_cache();
        assert_eq!(registry.cached_provider_count(), 0);

        std::env::remove_var("TEST_AZURE_API_KEY");
    }
}
