//! Configuration loading and utility functions.
//!
//! This module provides functions to load configuration from YAML files,
//! resolve secrets from environment variables, and filter models by
//! various criteria.

use super::types::{GatewayConfig, Modality, ModelConfig};
use anyhow::{Context, Result};
use enact_config::load_dotenv_from_home;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Once;

// Ensure .env from ENACT_HOME is loaded only once
static DOTENV_INIT: Once = Once::new();

fn ensure_dotenv_loaded() {
    DOTENV_INIT.call_once(|| {
        let _ = load_dotenv_from_home();
    });
}

/// Load gateway configuration from a YAML file.
///
/// # Arguments
///
/// * `path` - Path to the YAML configuration file.
///
/// # Returns
///
/// The parsed configuration or an error if loading/parsing fails.
///
/// # Example
///
/// ```no_run
/// use enact_providers::config::load_config;
///
/// let config = load_config("providers.yaml").unwrap();
/// println!("Loaded {} models", config.model_count());
/// ```
pub fn load_config(path: impl AsRef<Path>) -> Result<GatewayConfig> {
    let path = path.as_ref();
    let content = fs::read_to_string(path)
        .with_context(|| format!("Failed to read config file: {:?}", path))?;

    let config: GatewayConfig =
        serde_yaml::from_str(&content).with_context(|| "Failed to parse YAML configuration")?;

    Ok(config)
}

/// Path to providers.yaml in ENACT_HOME (e.g. ~/.enact/providers.yaml).
fn providers_home_path() -> PathBuf {
    enact_config::enact_home().join("providers.yaml")
}

/// Load configuration from the default path or environment overrides.
///
/// Resolution order: ENACT_PROVIDERS_CONFIG_PATH → cwd/providers.yaml → ~/.enact/providers.yaml.
/// Also ensures .env from ~/.enact is loaded before reading environment variables.
pub fn load_default_config() -> Result<GatewayConfig> {
    ensure_dotenv_loaded();
    if let Ok(path) = env::var("ENACT_PROVIDERS_CONFIG_PATH") {
        return load_config(&path);
    }
    let cwd_path = PathBuf::from("providers.yaml");
    if cwd_path.exists() {
        return load_config(&cwd_path);
    }
    let home_path = providers_home_path();
    if home_path.exists() {
        return load_config(&home_path);
    }
    anyhow::bail!(
        "providers.yaml not found. Place providers.yaml in cwd or copy to {:?}",
        home_path
    )
}

/// Resolve a secret from an environment variable.
///
/// This function ensures .env file is loaded before reading environment variables.
///
/// # Arguments
///
/// * `env_var_name` - Name of the environment variable containing the secret.
///
/// # Returns
///
/// The secret value or an error if the environment variable is not set.
///
/// # Example
///
/// ```no_run
/// use enact_providers::config::resolve_secret;
///
/// let api_key = resolve_secret("AZURE_API_KEY").unwrap();
/// ```
pub fn resolve_secret(env_var_name: &str) -> Result<String> {
    ensure_dotenv_loaded();
    env::var(env_var_name)
        .with_context(|| format!("Environment variable '{}' not set", env_var_name))
}

/// Get models filtered by input or output modality.
///
/// Returns models that support the given modality as either input or output.
///
/// # Arguments
///
/// * `config` - The gateway configuration.
/// * `modality` - The modality to filter by.
///
/// # Returns
///
/// A HashMap of model IDs to model configurations that support the modality.
pub fn get_models_by_modality<'a>(
    config: &'a GatewayConfig,
    modality: &Modality,
) -> HashMap<String, &'a ModelConfig> {
    config
        .models
        .iter()
        .filter(|(_, model)| {
            model.input_modality.contains(modality) || model.output_modality.contains(modality)
        })
        .map(|(k, v)| (k.clone(), v))
        .collect()
}

/// Get models filtered by provider name.
///
/// # Arguments
///
/// * `config` - The gateway configuration.
/// * `provider` - The provider name to filter by (e.g., "azure-foundry", "openrouter").
///
/// # Returns
///
/// A HashMap of model IDs to model configurations from the specified provider.
pub fn get_models_by_provider<'a>(
    config: &'a GatewayConfig,
    provider: &str,
) -> HashMap<String, &'a ModelConfig> {
    config
        .models
        .iter()
        .filter(|(_, model)| model.provider_name == provider)
        .map(|(k, v)| (k.clone(), v))
        .collect()
}

/// Get models that support tool calling.
pub fn get_models_with_tools(config: &GatewayConfig) -> HashMap<String, &ModelConfig> {
    config
        .models
        .iter()
        .filter(|(_, model)| model.capabilities.tool_calls)
        .map(|(k, v)| (k.clone(), v))
        .collect()
}

/// Get models that support reasoning.
pub fn get_models_with_reasoning(config: &GatewayConfig) -> HashMap<String, &ModelConfig> {
    config
        .models
        .iter()
        .filter(|(_, model)| model.capabilities.reasoning)
        .map(|(k, v)| (k.clone(), v))
        .collect()
}

/// Get active models (lifecycle_status == Active).
pub fn get_active_models(config: &GatewayConfig) -> HashMap<String, &ModelConfig> {
    use super::types::LifecycleStatus;
    config
        .models
        .iter()
        .filter(|(_, model)| model.governance.lifecycle_status == LifecycleStatus::Active)
        .map(|(k, v)| (k.clone(), v))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn create_test_config() -> String {
        r#"
models:
  test:text:model-1:
    display_name: Test Model 1
    provider_name: azure-foundry
    input_modality:
      - text
    output_modality:
      - text
    runtime:
      base_url: "https://example.com"
      full_url: "https://example.com/v1/chat"
      protocol: azure_sdk
      api_version: "2024-01-01"
      use_max_completion_tokens: false
      has_temperature_supported: true
    auth:
      type: header
    secrets:
      type: environment
      name: TEST_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:image:model-2:
    display_name: Test Model 2
    provider_name: openrouter
    input_modality:
      - text
      - image
    output_modality:
      - image
    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: false
    auth:
      type: bearer
    secrets:
      type: environment
      name: OPENROUTER_API_KEY
    capabilities:
      tool_calls: false
      reasoning: false
    governance:
      is_use_approve: true
      approved_by: admin
      approval_date: '2024-01-01T00:00:00Z'
      lifecycle_status: active
    cost:
      provider: openrouter
      currency: USD
      cost_per_1m_input: 0.0005
      cost_per_1m_output: 0.001
"#
        .to_string()
    }

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

        let config = load_config(file.path()).unwrap();
        assert_eq!(config.model_count(), 2);
    }

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

        let config = load_config(file.path()).unwrap();
        let model = config.get_model("test:text:model-1");
        assert!(model.is_some());
        assert_eq!(model.unwrap().display_name, "Test Model 1");
    }

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

        let config = load_config(file.path()).unwrap();
        let text_models = get_models_by_modality(&config, &Modality::Text);
        assert_eq!(text_models.len(), 2); // Both models have text input
    }

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

        let config = load_config(file.path()).unwrap();
        let image_models = get_models_by_modality(&config, &Modality::Image);
        assert_eq!(image_models.len(), 1);
    }

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

        let config = load_config(file.path()).unwrap();
        let azure_models = get_models_by_provider(&config, "azure-foundry");
        assert_eq!(azure_models.len(), 1);
    }

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

        let config = load_config(file.path()).unwrap();
        let openrouter_models = get_models_by_provider(&config, "openrouter");
        assert_eq!(openrouter_models.len(), 1);
    }

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

        let config = load_config(file.path()).unwrap();
        let tool_models = get_models_with_tools(&config);
        assert_eq!(tool_models.len(), 1);
    }

    #[test]
    fn test_resolve_secret_missing() {
        let result = resolve_secret("NONEXISTENT_SECRET_VAR_12345");
        assert!(result.is_err());
    }

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

        let config = load_config(file.path()).unwrap();
        let model = config.get_model("test:text:model-1").unwrap();

        assert!(model.supports_input_modality(&Modality::Text));
        assert!(!model.supports_input_modality(&Modality::Image));
        assert!(model.supports_tools());
        assert!(!model.supports_reasoning());
        assert_eq!(model.api_key_env_var(), "TEST_API_KEY");
    }

    #[test]
    fn test_load_actual_config_file() {
        // Test loading the actual providers.yaml file
        let config_path = "providers.yaml";
        if std::path::Path::new(config_path).exists() {
            let result = load_config(config_path);
            match result {
                Ok(config) => {
                    println!(
                        "Loaded {} models from {}",
                        config.model_count(),
                        config_path
                    );
                    assert!(config.model_count() > 0, "Config should have models");
                }
                Err(e) => {
                    println!("Failed to load config: {}", e);
                    // Print more details about the error
                    if let Some(source) = e.source() {
                        println!("Caused by: {}", source);
                    }
                    panic!("Failed to load providers.yaml: {}", e);
                }
            }
        } else {
            println!("providers.yaml not found, skipping test");
        }
    }
}
