// vapora-llm-router: LLM Provider implementations
// Phase 3: Real providers via typedialog-ai

use std::sync::Arc;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ProviderError {
    #[error("Request failed: {0}")]
    RequestFailed(String),

    #[error("Invalid response: {0}")]
    InvalidResponse(String),

    #[error("Rate limit exceeded")]
    RateLimitExceeded,

    #[error("Authentication failed: {0}")]
    AuthenticationFailed(String),

    #[error("Provider unavailable: {0}")]
    Unavailable(String),

    #[error("Configuration error: {0}")]
    ConfigError(String),

    #[error("HTTP error: {0}")]
    HttpError(#[from] reqwest::Error),

    #[error("JSON parsing error: {0}")]
    JsonError(#[from] serde_json::Error),

    #[error("LLM error: {0}")]
    LlmError(String),
}

/// LLM Provider trait for unified interface
#[async_trait]
pub trait LLMClient: Send + Sync {
    /// Send completion request to LLM
    async fn complete(
        &self,
        prompt: String,
        context: Option<String>,
    ) -> Result<CompletionResponse, ProviderError>;

    /// Stream completion response
    async fn stream(
        &self,
        prompt: String,
    ) -> Result<tokio::sync::mpsc::Receiver<String>, ProviderError>;

    /// Get cost per 1k tokens (combined input/output estimate)
    fn cost_per_1k_tokens(&self) -> f64;

    /// Get average latency in milliseconds
    fn latency_ms(&self) -> u32;

    /// Check if provider is available
    fn available(&self) -> bool;

    /// Get provider name
    fn provider_name(&self) -> String;

    /// Get model name
    fn model_name(&self) -> String;

    /// Calculate cost for token usage
    fn calculate_cost(&self, input_tokens: u64, output_tokens: u64) -> u32;
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionResponse {
    pub text: String,
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub finish_reason: String,
}

// ============================================================================
// Claude Provider (via typedialog-ai)
// ============================================================================

pub struct ClaudeClient {
    adapter: TypeDialogAdapter,
}

impl ClaudeClient {
    pub fn new(
        api_key: String,
        model: String,
        _max_tokens: usize,
        _temperature: f32,
        cost_per_1m_input: f64,
        cost_per_1m_output: f64,
    ) -> Result<Self, ProviderError> {
        #[cfg(feature = "anthropic")]
        {
            use typedialog_ai::llm::providers::AnthropicProvider;
            let provider = AnthropicProvider::new(&api_key, &model)
                .map_err(|e| ProviderError::LlmError(e.to_string()))?;
            let adapter = TypeDialogAdapter::new(
                Arc::new(provider),
                "claude".to_string(),
                cost_per_1m_input,
                cost_per_1m_output,
            );
            Ok(Self { adapter })
        }
        #[cfg(not(feature = "anthropic"))]
        {
            Err(ProviderError::ConfigError(
                "Claude provider requires 'anthropic' feature".to_string(),
            ))
        }
    }
}

#[async_trait]
impl LLMClient for ClaudeClient {
    async fn complete(
        &self,
        prompt: String,
        context: Option<String>,
    ) -> Result<CompletionResponse, ProviderError> {
        self.adapter.complete(prompt, context).await
    }

    async fn stream(
        &self,
        prompt: String,
    ) -> Result<tokio::sync::mpsc::Receiver<String>, ProviderError> {
        self.adapter.stream(prompt).await
    }

    fn cost_per_1k_tokens(&self) -> f64 {
        self.adapter.cost_per_1k_tokens()
    }

    fn latency_ms(&self) -> u32 {
        self.adapter.latency_ms()
    }

    fn available(&self) -> bool {
        self.adapter.available()
    }

    fn provider_name(&self) -> String {
        self.adapter.provider_name()
    }

    fn model_name(&self) -> String {
        self.adapter.model_name()
    }

    fn calculate_cost(&self, input_tokens: u64, output_tokens: u64) -> u32 {
        self.adapter.calculate_cost(input_tokens, output_tokens)
    }
}

// ============================================================================
// OpenAI Provider (via typedialog-ai)
// ============================================================================

pub struct OpenAIClient {
    adapter: TypeDialogAdapter,
}

impl OpenAIClient {
    pub fn new(
        api_key: String,
        model: String,
        _max_tokens: usize,
        _temperature: f32,
        cost_per_1m_input: f64,
        cost_per_1m_output: f64,
    ) -> Result<Self, ProviderError> {
        #[cfg(feature = "openai")]
        {
            use typedialog_ai::llm::providers::OpenAiProvider;
            let provider = OpenAiProvider::new(&api_key, &model)
                .map_err(|e| ProviderError::LlmError(e.to_string()))?;
            let adapter = TypeDialogAdapter::new(
                Arc::new(provider),
                "openai".to_string(),
                cost_per_1m_input,
                cost_per_1m_output,
            );
            Ok(Self { adapter })
        }
        #[cfg(not(feature = "openai"))]
        {
            Err(ProviderError::ConfigError(
                "OpenAI provider requires 'openai' feature".to_string(),
            ))
        }
    }
}

#[async_trait]
impl LLMClient for OpenAIClient {
    async fn complete(
        &self,
        prompt: String,
        context: Option<String>,
    ) -> Result<CompletionResponse, ProviderError> {
        self.adapter.complete(prompt, context).await
    }

    async fn stream(
        &self,
        prompt: String,
    ) -> Result<tokio::sync::mpsc::Receiver<String>, ProviderError> {
        self.adapter.stream(prompt).await
    }

    fn cost_per_1k_tokens(&self) -> f64 {
        self.adapter.cost_per_1k_tokens()
    }

    fn latency_ms(&self) -> u32 {
        self.adapter.latency_ms()
    }

    fn available(&self) -> bool {
        self.adapter.available()
    }

    fn provider_name(&self) -> String {
        self.adapter.provider_name()
    }

    fn model_name(&self) -> String {
        self.adapter.model_name()
    }

    fn calculate_cost(&self, input_tokens: u64, output_tokens: u64) -> u32 {
        self.adapter.calculate_cost(input_tokens, output_tokens)
    }
}

// Note: Gemini provider not available in typedialog-ai yet
// Can be added as separate provider in future if needed

// ============================================================================
// Ollama Provider (Local)
// ============================================================================

pub struct OllamaClient {
    adapter: TypeDialogAdapter,
}

impl OllamaClient {
    pub fn new(
        _endpoint: String,
        model: String,
        _max_tokens: usize,
        _temperature: f32,
    ) -> Result<Self, ProviderError> {
        #[cfg(feature = "ollama")]
        {
            use typedialog_ai::llm::providers::OllamaProvider;
            let provider =
                OllamaProvider::new(&model).map_err(|e| ProviderError::LlmError(e.to_string()))?;
            let adapter = TypeDialogAdapter::new(
                Arc::new(provider),
                "ollama".to_string(),
                0.0, // Local models have no cost
                0.0,
            );
            Ok(Self { adapter })
        }
        #[cfg(not(feature = "ollama"))]
        {
            Err(ProviderError::ConfigError(
                "Ollama provider requires 'ollama' feature".to_string(),
            ))
        }
    }
}

#[async_trait]
impl LLMClient for OllamaClient {
    async fn complete(
        &self,
        prompt: String,
        context: Option<String>,
    ) -> Result<CompletionResponse, ProviderError> {
        self.adapter.complete(prompt, context).await
    }

    async fn stream(
        &self,
        prompt: String,
    ) -> Result<tokio::sync::mpsc::Receiver<String>, ProviderError> {
        self.adapter.stream(prompt).await
    }

    fn cost_per_1k_tokens(&self) -> f64 {
        0.0 // Local models have no cost
    }

    fn latency_ms(&self) -> u32 {
        self.adapter.latency_ms()
    }

    fn available(&self) -> bool {
        self.adapter.available()
    }

    fn provider_name(&self) -> String {
        self.adapter.provider_name()
    }

    fn model_name(&self) -> String {
        self.adapter.model_name()
    }

    fn calculate_cost(&self, _input_tokens: u64, _output_tokens: u64) -> u32 {
        0 // Local models have no cost
    }
}

// ============================================================================
// TypeDialog AI Adapter (Private module, breaks circular dependency)
// ============================================================================

/// Adapter wrapping typedialog-ai LlmProvider for VAPORA LLMClient trait
/// PRIVATE: Only used internally by provider implementations
pub(crate) struct TypeDialogAdapter {
    provider: Arc<dyn typedialog_ai::llm::LlmProvider>,
    provider_name: String,
    cost_per_1m_input: f64,
    cost_per_1m_output: f64,
}

impl TypeDialogAdapter {
    /// Create new adapter wrapping a typedialog-ai provider
    pub fn new(
        provider: Arc<dyn typedialog_ai::llm::LlmProvider>,
        provider_name: String,
        cost_per_1m_input: f64,
        cost_per_1m_output: f64,
    ) -> Self {
        Self {
            provider,
            provider_name,
            cost_per_1m_input,
            cost_per_1m_output,
        }
    }

    /// Estimate tokens from text (fallback for providers without token
    /// counting)
    fn estimate_tokens(text: &str) -> u64 {
        // Rough estimate: 4 characters ≈ 1 token (works well for English/code)
        (text.len() as u64).div_ceil(4)
    }

    /// Build message list from prompt and optional context
    fn build_messages(prompt: &str, context: Option<&str>) -> Vec<typedialog_ai::llm::Message> {
        use typedialog_ai::llm::{Message, Role};
        let mut messages = Vec::new();

        if let Some(ctx) = context {
            messages.push(Message {
                role: Role::System,
                content: ctx.to_string(),
            });
        }

        messages.push(Message {
            role: Role::User,
            content: prompt.to_string(),
        });

        messages
    }
}

#[async_trait]
impl LLMClient for TypeDialogAdapter {
    /// Send completion request to underlying LLM provider
    async fn complete(
        &self,
        prompt: String,
        context: Option<String>,
    ) -> Result<CompletionResponse, ProviderError> {
        use tracing::error;
        let messages = Self::build_messages(&prompt, context.as_deref());

        // Create default generation options
        let options = typedialog_ai::llm::GenerationOptions {
            temperature: 0.7,
            max_tokens: Some(4096),
            stop_sequences: vec![],
            top_p: None,
            top_k: None,
            presence_penalty: None,
            frequency_penalty: None,
        };

        let text = self
            .provider
            .generate(&messages, &options)
            .await
            .map_err(|e| {
                error!("LLM generation failed: {}", e);
                ProviderError::RequestFailed(e.to_string())
            })?;

        let input_tokens = Self::estimate_tokens(&prompt);
        let output_tokens = Self::estimate_tokens(&text);

        Ok(CompletionResponse {
            text,
            input_tokens,
            output_tokens,
            finish_reason: "stop".to_string(),
        })
    }

    /// Stream completion response token-by-token
    async fn stream(
        &self,
        prompt: String,
    ) -> Result<tokio::sync::mpsc::Receiver<String>, ProviderError> {
        use futures::StreamExt;
        use tracing::error;
        let messages = Self::build_messages(&prompt, None);

        let options = typedialog_ai::llm::GenerationOptions {
            temperature: 0.7,
            max_tokens: Some(4096),
            stop_sequences: vec![],
            top_p: None,
            top_k: None,
            presence_penalty: None,
            frequency_penalty: None,
        };

        let mut stream = self
            .provider
            .stream(&messages, &options)
            .await
            .map_err(|e| {
                error!("LLM stream failed: {}", e);
                ProviderError::RequestFailed(e.to_string())
            })?;

        let (tx, rx) = tokio::sync::mpsc::channel(100);

        tokio::spawn(async move {
            while let Some(token_result) = stream.next().await {
                match token_result {
                    Ok(token) => {
                        if tx.send(token).await.is_err() {
                            // Receiver dropped, stop streaming
                            break;
                        }
                    }
                    Err(e) => {
                        error!("Stream error: {}", e);
                        break;
                    }
                }
            }
        });

        Ok(rx)
    }

    /// Cost per 1k tokens (combined estimate)
    fn cost_per_1k_tokens(&self) -> f64 {
        (self.cost_per_1m_input + self.cost_per_1m_output) / 1000.0
    }

    /// Average latency in milliseconds
    fn latency_ms(&self) -> u32 {
        match self.provider_name.as_str() {
            "claude" => 300,
            "openai" => 250,
            "gemini" => 400,
            "ollama" => 150, // Local, typically faster
            _ => 250,
        }
    }

    /// Check if provider is available
    fn available(&self) -> bool {
        true // typedialog-ai providers handle availability internally
    }

    /// Get provider name
    fn provider_name(&self) -> String {
        self.provider_name.clone()
    }

    /// Get model name
    fn model_name(&self) -> String {
        self.provider.model().to_string()
    }

    /// Calculate cost for token usage
    fn calculate_cost(&self, input_tokens: u64, output_tokens: u64) -> u32 {
        let input_cost = (input_tokens as f64 / 1_000_000.0) * self.cost_per_1m_input;
        let output_cost = (output_tokens as f64 / 1_000_000.0) * self.cost_per_1m_output;
        ((input_cost + output_cost) * 100.0) as u32 // Convert to cents
    }
}

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

    #[test]
    fn test_llm_client_trait_exists() {
        // Tests compile-time verification that LLMClient trait is properly
        // defined with required methods for all implementations
    }

    #[test]
    fn test_token_estimation() {
        assert_eq!(TypeDialogAdapter::estimate_tokens("hello"), 2); // ~4 chars per token
        assert_eq!(TypeDialogAdapter::estimate_tokens("hello world"), 3);
        assert_eq!(TypeDialogAdapter::estimate_tokens("a"), 1);
    }

    #[test]
    fn test_cost_calculation() {
        let adapter = TypeDialogAdapter::new(
            Arc::new(MockProvider),
            "test".to_string(),
            0.80, // $0.80 per 1M input tokens
            1.60, // $1.60 per 1M output tokens
        );

        // 1000 input tokens + 1000 output tokens
        let cost = adapter.calculate_cost(1000, 1000);
        // (1000 / 1M * 0.80) + (1000 / 1M * 1.60) = 0.0008 + 0.0016 = 0.0024 = 0.24
        // cents
        assert_eq!(cost, 0);
    }

    #[derive(Debug)]
    struct MockProvider;

    #[async_trait]
    impl typedialog_ai::llm::LlmProvider for MockProvider {
        async fn generate(
            &self,
            _messages: &[typedialog_ai::llm::Message],
            _options: &typedialog_ai::llm::GenerationOptions,
        ) -> typedialog_ai::llm::Result<String> {
            Ok("mock response".to_string())
        }

        async fn stream(
            &self,
            _messages: &[typedialog_ai::llm::Message],
            _options: &typedialog_ai::llm::GenerationOptions,
        ) -> typedialog_ai::llm::Result<typedialog_ai::llm::StreamResponse> {
            use futures::stream;
            let stream = stream::iter(vec![Ok("mock".to_string())]);
            Ok(Box::pin(stream))
        }

        fn name(&self) -> &str {
            "mock"
        }

        fn model(&self) -> &str {
            "mock-model"
        }

        async fn is_available(&self) -> bool {
            true
        }
    }
}
