//! OpenRouter provider
//!
//! OpenRouter provides a unified API for accessing multiple LLM providers.
//! It uses an OpenAI-compatible API format with Bearer token authentication.
//!
//! ## Features
//!
//! - Access to 100+ models from various providers
//! - OpenAI-compatible API format
//! - Automatic failover and load balancing
//!
//! ## Configuration
//!
//! Configure via config.yml with provider_name: "openrouter".
//! Requires OPENROUTER_API_KEY environment variable.

use crate::config::{Modality, ModelConfig};
use crate::http::{post_json, HttpRequestSpec};
use async_trait::async_trait;
use enact_core::providers::{
    ChatRequest, ChatResponse, EmbeddingData, EmbeddingRequest, EmbeddingResponse, EmbeddingUsage,
    ImageData, ImageGenerationRequest, ImageGenerationResponse, ModelCapabilities, ModelProvider,
};
use reqwest::Client;
use serde_json::Value;
use std::collections::HashSet;
use std::time::Duration;

/// OpenRouter provider for accessing multiple LLM models.
///
/// OpenRouter aggregates models from various providers (OpenAI, Anthropic, Google, etc.)
/// through a single, unified API.
pub struct OpenRouter {
    client: Client,
    /// Model identifier from config (e.g., "openrouter:text:arcee-ai-trinity-large-preview-free")
    model_id: String,
    /// The actual model name to pass to OpenRouter (e.g., "arcee-ai/trinity-large-preview:free")
    openrouter_model: String,
    /// API key for authentication
    api_key: String,
    /// Full URL for chat completions
    chat_url: String,
    /// Full URL for embeddings (if applicable)
    embeddings_url: Option<String>,
    /// Full URL for image generation (if applicable)
    images_url: Option<String>,
    /// Request timeout
    timeout: Duration,
    /// Whether temperature is supported
    has_temperature_supported: bool,
    /// Whether tool calls are supported
    supports_tool_calls: bool,
    /// Whether reasoning is supported
    supports_reasoning: bool,
    /// Default max tokens (from config runtime.max_tokens_default)
    max_tokens_default: u32,
    /// Cost per 1K input tokens (from config cost.cost_per_1m_input)
    cost_per_1m_input: Option<f64>,
    /// Cost per 1K output tokens (from config cost.cost_per_1m_output)
    cost_per_1m_output: Option<f64>,
    /// Cost per 1K pixels for image (from config cost.cost_per_1m_pixels)
    cost_per_1m_pixels: Option<f64>,
    /// Output modalities supported by this model
    output_modalities: HashSet<Modality>,
}

impl OpenRouter {
    /// Create a new OpenRouter provider from model configuration.
    ///
    /// # Arguments
    ///
    /// * `model_id` - The model identifier (e.g., "openrouter:text:arcee-ai-trinity-large-preview-free")
    /// * `config` - The model configuration from config.yml
    /// * `api_key` - The resolved API key
    ///
    /// # Example
    ///
    /// ```no_run
    /// use enact_providers::config::{load_config, resolve_secret};
    /// use enact_providers::OpenRouter;
    ///
    /// let config = load_config("config.yml").unwrap();
    /// let model_config = config.get_model("openrouter:text:arcee-ai-trinity-large-preview-free").unwrap();
    /// let api_key = resolve_secret(model_config.api_key_env_var()).unwrap();
    /// let provider = OpenRouter::from_model_config("openrouter:text:arcee-ai-trinity-large-preview-free", model_config, &api_key).unwrap();
    /// ```
    pub fn from_model_config(
        model_id: &str,
        config: &ModelConfig,
        api_key: &str,
    ) -> anyhow::Result<Self> {
        // Extract the actual OpenRouter model name from cost.provider_key
        // This is where the actual model identifier is stored (e.g., "arcee-ai/trinity-large-preview:free")
        let openrouter_model = config
            .cost
            .provider_key
            .clone()
            .unwrap_or_else(|| model_id.to_string());

        // Collect output modalities
        let output_modalities: HashSet<Modality> = config.output_modality.iter().cloned().collect();

        // Determine URLs based on modality or explicit URL
        let is_embeddings = config.runtime.full_url.contains("/embeddings");
        let is_images = config.runtime.full_url.contains("/images");

        let (chat_url, embeddings_url, images_url) = if is_embeddings {
            (
                config
                    .runtime
                    .full_url
                    .replace("/embeddings", "/chat/completions"),
                Some(config.runtime.full_url.clone()),
                None,
            )
        } else if is_images {
            (
                config
                    .runtime
                    .full_url
                    .replace("/images/generations", "/chat/completions"),
                None,
                Some(config.runtime.full_url.clone()),
            )
        } else {
            // For standard chat endpoints, check if modalities suggest other capabilities
            let base_url = config.runtime.base_url.clone();
            let embeddings = if output_modalities.contains(&Modality::Embedding) {
                Some(format!("{}/embeddings", base_url))
            } else {
                None
            };
            let images = if output_modalities.contains(&Modality::Image) {
                Some(format!("{}/images/generations", base_url))
            } else {
                None
            };
            (config.runtime.full_url.clone(), embeddings, images)
        };

        Ok(Self {
            client: Client::new(),
            model_id: model_id.to_string(),
            openrouter_model,
            api_key: api_key.to_string(),
            chat_url,
            embeddings_url,
            images_url,
            timeout: Duration::from_secs(config.runtime.timeout_seconds),
            has_temperature_supported: config.runtime.has_temperature_supported,
            supports_tool_calls: config.capabilities.tool_calls,
            supports_reasoning: config.capabilities.reasoning,
            max_tokens_default: config.runtime.max_tokens_default,
            cost_per_1m_input: config.cost.cost_per_1m_input,
            cost_per_1m_output: config.cost.cost_per_1m_output,
            cost_per_1m_pixels: config.cost.cost_per_1m_pixels,
            output_modalities,
        })
    }

    /// Create with explicit configuration (for cases where config.yml is not used).
    pub fn new(api_key: impl Into<String>, model: impl Into<String>) -> Self {
        Self {
            client: Client::new(),
            model_id: "openrouter".to_string(),
            openrouter_model: model.into(),
            api_key: api_key.into(),
            chat_url: "https://openrouter.ai/api/v1/chat/completions".to_string(),
            embeddings_url: Some("https://openrouter.ai/api/v1/embeddings".to_string()),
            images_url: Some("https://openrouter.ai/api/v1/images/generations".to_string()),
            timeout: Duration::from_secs(60),
            has_temperature_supported: true,
            supports_tool_calls: true,
            supports_reasoning: false,
            max_tokens_default: 4096,
            cost_per_1m_input: None,
            cost_per_1m_output: None,
            cost_per_1m_pixels: None,
            output_modalities: HashSet::from([Modality::Text]),
        }
    }

    /// Build headers for OpenRouter API requests.
    fn build_headers(&self) -> Vec<(String, String)> {
        vec![
            ("Content-Type".to_string(), "application/json".to_string()),
            (
                "Authorization".to_string(),
                format!("Bearer {}", self.api_key),
            ),
            // Optional: Add site identification for better rate limits
            // ("HTTP-Referer".to_string(), "https://your-site.com".to_string()),
            // ("X-Title".to_string(), "Your App Name".to_string()),
        ]
    }

    /// Transform the request for OpenRouter API.
    ///
    /// OpenRouter requires the model name in the request body.
    fn transform_request(&self, mut request: ChatRequest) -> anyhow::Result<Value> {
        // Handle temperature support
        if !self.has_temperature_supported {
            request.temperature = None;
        }

        // Convert to JSON
        let mut body = serde_json::to_value(&request)?;

        // Add model name to request body (required by OpenRouter)
        if let Some(obj) = body.as_object_mut() {
            obj.insert(
                "model".to_string(),
                Value::String(self.openrouter_model.clone()),
            );
        }

        Ok(body)
    }
}

#[async_trait]
impl ModelProvider for OpenRouter {
    fn name(&self) -> &str {
        "openrouter"
    }

    fn model(&self) -> &str {
        &self.model_id
    }

    fn capabilities(&self) -> ModelCapabilities {
        ModelCapabilities {
            max_tokens: self.max_tokens_default,
            max_output_tokens: self.max_tokens_default,
            supports_streaming: true,
            supports_tools: self.supports_tool_calls,
            supports_reasoning: self.supports_reasoning,
            supports_vision: false,
            supports_json_mode: true,
            supports_embeddings: self.embeddings_url.is_some()
                || self.output_modalities.contains(&Modality::Embedding),
            supports_image_generation: self.images_url.is_some()
                || self.output_modalities.contains(&Modality::Image),
            supports_audio_transcription: self.output_modalities.contains(&Modality::Audio),
            supports_speech: self.output_modalities.contains(&Modality::Audio),
            supports_video_generation: self.output_modalities.contains(&Modality::Video),
            pii_safe: false,
            cost_per_1m_input: self.cost_per_1m_input,
            cost_per_1m_output: self.cost_per_1m_output,
            cost_per_1m_pixels: self.cost_per_1m_pixels,
        }
    }

    async fn chat(&self, request: ChatRequest) -> anyhow::Result<ChatResponse> {
        // Transform request (adds model name)
        let body = self.transform_request(request)?;

        // Build HTTP request spec
        let http_req = HttpRequestSpec {
            url: self.chat_url.clone(),
            headers: self.build_headers(),
            body,
            timeout: self.timeout,
        };

        // Execute request using HTTP helper
        let json_response = post_json(&self.client, http_req).await?;

        // Parse response (OpenRouter uses OpenAI-compatible format)
        let chat_response: ChatResponse = serde_json::from_value(json_response)?;
        Ok(chat_response)
    }

    async fn embed(&self, request: EmbeddingRequest) -> anyhow::Result<EmbeddingResponse> {
        let embeddings_url = self
            .embeddings_url
            .as_ref()
            .ok_or_else(|| anyhow::anyhow!("Embeddings not supported for this model"))?;

        // Build request body (OpenAI-compatible format with model)
        let mut body_map = serde_json::Map::new();
        body_map.insert(
            "model".to_string(),
            Value::String(self.openrouter_model.clone()),
        );
        body_map.insert("input".to_string(), Value::String(request.input.clone()));
        let body = Value::Object(body_map);

        // Build HTTP request spec
        let http_req = HttpRequestSpec {
            url: embeddings_url.clone(),
            headers: self.build_headers(),
            body,
            timeout: self.timeout,
        };

        // Execute request
        let json_response = post_json(&self.client, http_req).await?;

        // Parse response - OpenRouter returns OpenAI-compatible format
        let data = json_response
            .get("data")
            .and_then(|d| d.as_array())
            .ok_or_else(|| anyhow::anyhow!("Missing 'data' array in embedding response"))?;

        let embeddings: Vec<EmbeddingData> = data
            .iter()
            .enumerate()
            .filter_map(|(i, item)| {
                let embedding = item.get("embedding")?.as_array()?;
                let vec: Vec<f32> = embedding
                    .iter()
                    .filter_map(|v| v.as_f64().map(|f| f as f32))
                    .collect();
                Some(EmbeddingData {
                    embedding: vec,
                    index: i as u32,
                })
            })
            .collect();

        let usage = json_response.get("usage").and_then(|u| {
            Some(EmbeddingUsage {
                prompt_tokens: u.get("prompt_tokens")?.as_u64()? as u32,
                total_tokens: u.get("total_tokens")?.as_u64()? as u32,
            })
        });

        Ok(EmbeddingResponse {
            data: embeddings,
            model: json_response
                .get("model")
                .and_then(|m| m.as_str())
                .unwrap_or(&self.model_id)
                .to_string(),
            usage,
        })
    }

    async fn generate_image(
        &self,
        request: ImageGenerationRequest,
    ) -> anyhow::Result<ImageGenerationResponse> {
        // Check if image generation is supported
        if !self.output_modalities.contains(&Modality::Image) {
            anyhow::bail!("Image generation not supported by model {}", self.model_id);
        }

        // OpenRouter uses chat completions for image generation
        // Build request body with image generation prompt as a message
        // The modalities field enables image output
        let body = serde_json::json!({
            "model": self.openrouter_model,
            "messages": [{
                "role": "user",
                "content": request.prompt
            }],
            "modalities": ["image", "text"],
            "n": request.n.unwrap_or(1)
        });

        // Build HTTP request spec - use chat_url, not images_url
        let http_req = HttpRequestSpec {
            url: self.chat_url.clone(),
            headers: self.build_headers(),
            body,
            timeout: self.timeout,
        };

        // Execute request
        let json_response = post_json(&self.client, http_req).await?;

        // Parse response - OpenRouter returns images in choices[0].message.images
        let created = json_response
            .get("created")
            .and_then(|c| c.as_u64())
            .unwrap_or_else(|| {
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map(|d| d.as_secs())
                    .unwrap_or(0)
            });

        // OpenRouter format: choices[0].message.images[].image_url.url
        let images = json_response
            .get("choices")
            .and_then(|c| c.as_array())
            .and_then(|arr| arr.first())
            .and_then(|choice| choice.get("message"))
            .and_then(|msg| msg.get("images"))
            .and_then(|i| i.as_array());

        let data: Vec<ImageData> = if let Some(images_arr) = images {
            images_arr
                .iter()
                .filter_map(|img| {
                    let url = img
                        .get("image_url")
                        .and_then(|iu| iu.get("url"))
                        .and_then(|u| u.as_str())?;

                    // If it's a data URI, extract the base64 part
                    let b64_json = if url.starts_with("data:") {
                        // Remove "data:image/xxx;base64," prefix
                        url.find(",").map(|idx| url[idx + 1..].to_string())
                    } else {
                        None
                    };

                    Some(ImageData {
                        url: if b64_json.is_none() {
                            Some(url.to_string())
                        } else {
                            None
                        },
                        b64_json,
                        revised_prompt: None,
                    })
                })
                .collect()
        } else {
            vec![]
        };

        if data.is_empty() {
            anyhow::bail!("OpenRouter returned no images in response");
        }

        Ok(ImageGenerationResponse { created, data })
    }
}

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

    fn create_test_config(provider_key: &str, is_embedding: bool) -> ModelConfig {
        let full_url = if is_embedding {
            "https://openrouter.ai/api/v1/embeddings".to_string()
        } else {
            "https://openrouter.ai/api/v1/chat/completions".to_string()
        };

        ModelConfig {
            display_name: "Test OpenRouter Model".to_string(),
            provider_name: "openrouter".to_string(),
            input_modality: vec![Modality::Text],
            output_modality: vec![Modality::Text],
            runtime: RuntimeConfig {
                base_url: "https://openrouter.ai/api/v1".to_string(),
                full_url,
                protocol: Protocol::OpenrouterSdk,
                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: "OPENROUTER_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 {
                provider: Some("openrouter".to_string()),
                provider_key: Some(provider_key.to_string()),
                currency: Some("USD".to_string()),
                cost_per_1m_input: Some(0.0),
                cost_per_1m_output: Some(0.0),
                cost_per_1m_cached_input: None,
                cost_per_1m_cached_output: None,
                cost_per_1m_pixels: None,
            },
            fallbacks: vec![],
        }
    }

    #[test]
    fn test_from_model_config() {
        let config = create_test_config("arcee-ai/trinity-large-preview:free", false);
        let provider =
            OpenRouter::from_model_config("openrouter:text:test", &config, "test-key").unwrap();

        assert_eq!(provider.name(), "openrouter");
        assert_eq!(provider.model(), "openrouter:text:test");
        assert_eq!(
            provider.openrouter_model,
            "arcee-ai/trinity-large-preview:free"
        );
    }

    #[test]
    fn test_bearer_auth_headers() {
        let config = create_test_config("test/model", false);
        let provider = OpenRouter::from_model_config("test", &config, "my-api-key").unwrap();

        let headers = provider.build_headers();
        assert!(headers
            .iter()
            .any(|(k, v)| k == "Authorization" && v == "Bearer my-api-key"));
    }

    #[test]
    fn test_request_includes_model() {
        let config = create_test_config("anthropic/claude-3", false);
        let provider = OpenRouter::from_model_config("test", &config, "key").unwrap();

        let request = ChatRequest {
            messages: vec![],
            max_tokens: Some(100),
            temperature: Some(0.5),
            tools: None,
            tool_choice: None,
        };

        let body = provider.transform_request(request).unwrap();
        let obj = body.as_object().unwrap();

        assert_eq!(obj.get("model").unwrap(), "anthropic/claude-3");
    }

    #[test]
    fn test_request_with_tools_includes_tools_and_tool_choice() {
        use enact_core::providers::{ChatTool, ChatToolFunction, ToolChoice};

        let config = create_test_config("test/model", false);
        let provider = OpenRouter::from_model_config("test", &config, "key").unwrap();

        let request = ChatRequest {
            messages: vec![],
            max_tokens: Some(100),
            temperature: Some(0.5),
            tools: Some(vec![ChatTool {
                tool_type: "function".to_string(),
                function: ChatToolFunction {
                    name: "get_weather".to_string(),
                    description: "Get weather".to_string(),
                    parameters: serde_json::json!({"type": "object"}),
                },
            }]),
            tool_choice: Some(ToolChoice::String("auto".to_string())),
        };

        let body = provider.transform_request(request).unwrap();
        let obj = body.as_object().unwrap();

        assert!(obj.contains_key("tools"), "body should contain 'tools' key");
        assert!(
            obj.contains_key("tool_choice"),
            "body should contain 'tool_choice' key"
        );
    }

    #[test]
    fn test_embeddings_url_detection() {
        let config = create_test_config("qwen/qwen3-embedding-8b", true);
        let provider = OpenRouter::from_model_config("test", &config, "key").unwrap();

        assert!(provider.embeddings_url.is_some());
        assert_eq!(
            provider.embeddings_url.unwrap(),
            "https://openrouter.ai/api/v1/embeddings"
        );
        assert_eq!(
            provider.chat_url,
            "https://openrouter.ai/api/v1/chat/completions"
        );
    }

    #[test]
    fn test_capabilities_from_config() {
        let config = create_test_config("test/model", false);
        let provider = OpenRouter::from_model_config("test", &config, "key").unwrap();

        let caps = provider.capabilities();
        assert!(caps.supports_tools);
    }

    #[test]
    fn test_new_constructor() {
        let provider = OpenRouter::new("api-key", "openai/gpt-4");

        assert_eq!(provider.openrouter_model, "openai/gpt-4");
        assert_eq!(
            provider.chat_url,
            "https://openrouter.ai/api/v1/chat/completions"
        );
    }
}
