//! LLM provider implementations for real API calls
//!
//! Integrates with Claude, OpenAI, Gemini, and Ollama APIs.

use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::time::Duration;

/// LLM request parameters
#[derive(Debug, Clone, Serialize)]
pub struct LLMRequest {
    /// System prompt
    pub system: String,
    /// User message
    pub message: String,
    /// Temperature (0.0-1.0)
    pub temperature: f32,
    /// Max tokens
    pub max_tokens: usize,
}

/// LLM response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMResponse {
    /// Generated text
    pub text: String,
    /// Provider used
    pub provider: String,
    /// Tokens used
    pub tokens_used: Option<usize>,
    /// Cost in USD
    pub cost: Option<f64>,
}

/// Claude API client (Anthropic)
#[derive(Debug, Clone)]
pub struct ClaudeClient {
    /// API key for Anthropic
    api_key: String,
    /// HTTP client
    client: reqwest::Client,
    /// Model identifier
    model: String,
}

impl ClaudeClient {
    /// Create new Claude client
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(60))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            model: "claude-sonnet-4.5".to_string(),
        }
    }

    /// Call Claude API
    pub async fn complete(&self, req: &LLMRequest) -> anyhow::Result<LLMResponse> {
        let url = "https://api.anthropic.com/v1/messages";

        let payload = serde_json::json!({
            "model": self.model,
            "max_tokens": req.max_tokens,
            "system": req.system,
            "messages": [
                {
                    "role": "user",
                    "content": req.message
                }
            ],
            "temperature": req.temperature
        });

        let response = self
            .client
            .post(url)
            .header("x-api-key", &self.api_key)
            .header("anthropic-version", "2023-06-01")
            .json(&payload)
            .send()
            .await?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            tracing::error!("Claude API error: {} - {}", status, error_text);
            return Err(anyhow!("Claude API error: {}", status));
        }

        let data = response.json::<serde_json::Value>().await?;

        let text = data
            .get("content")
            .and_then(|c| c.get(0))
            .and_then(|c| c.get("text"))
            .and_then(|t| t.as_str())
            .unwrap_or("No response")
            .to_string();

        let tokens_used = data
            .get("usage")
            .and_then(|u| u.get("output_tokens"))
            .and_then(|t| t.as_u64())
            .map(|t| t as usize);

        // Rough cost estimation: $3 per 1M input tokens, $15 per 1M output tokens
        let cost = tokens_used.map(|t| (t as f64 / 1_000_000.0) * 15.0);

        Ok(LLMResponse {
            text,
            provider: "claude".to_string(),
            tokens_used,
            cost,
        })
    }
}

/// OpenAI API client
#[derive(Debug, Clone)]
pub struct OpenAIClient {
    /// API key for OpenAI
    api_key: String,
    /// HTTP client
    client: reqwest::Client,
    /// Model identifier
    model: String,
}

impl OpenAIClient {
    /// Create new OpenAI client
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(60))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            model: "gpt-4-turbo".to_string(),
        }
    }

    /// Call OpenAI API
    pub async fn complete(&self, req: &LLMRequest) -> anyhow::Result<LLMResponse> {
        let url = "https://api.openai.com/v1/chat/completions";

        let payload = serde_json::json!({
            "model": self.model,
            "max_tokens": req.max_tokens,
            "temperature": req.temperature,
            "messages": [
                {
                    "role": "system",
                    "content": req.system
                },
                {
                    "role": "user",
                    "content": req.message
                }
            ]
        });

        let response = self
            .client
            .post(url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .json(&payload)
            .send()
            .await?;

        if !response.status().is_success() {
            let status = response.status();
            tracing::error!("OpenAI API error: {}", status);
            return Err(anyhow!("OpenAI API error: {}", status));
        }

        let data = response.json::<serde_json::Value>().await?;

        let text = data
            .get("choices")
            .and_then(|c| c.get(0))
            .and_then(|c| c.get("message"))
            .and_then(|m| m.get("content"))
            .and_then(|c| c.as_str())
            .unwrap_or("No response")
            .to_string();

        let tokens_used = data
            .get("usage")
            .and_then(|u| u.get("completion_tokens"))
            .and_then(|t| t.as_u64())
            .map(|t| t as usize);

        // Cost: $15 per 1M input, $45 per 1M output tokens (GPT-4)
        let cost = tokens_used.map(|t| (t as f64 / 1_000_000.0) * 45.0);

        Ok(LLMResponse {
            text,
            provider: "openai".to_string(),
            tokens_used,
            cost,
        })
    }
}

/// Google Gemini API client
#[derive(Debug, Clone)]
pub struct GeminiClient {
    /// API key for Google
    api_key: String,
    /// HTTP client
    client: reqwest::Client,
}

impl GeminiClient {
    /// Create new Gemini client
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(60))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
        }
    }

    /// Call Gemini API
    pub async fn complete(&self, req: &LLMRequest) -> anyhow::Result<LLMResponse> {
        let url = format!(
            "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={}",
            self.api_key
        );

        let payload = serde_json::json!({
            "contents": [
                {
                    "parts": [
                        {
                            "text": format!("System: {}\n\nUser: {}", req.system, req.message)
                        }
                    ]
                }
            ],
            "generationConfig": {
                "temperature": req.temperature,
                "maxOutputTokens": req.max_tokens
            }
        });

        let response = self.client.post(&url).json(&payload).send().await?;

        if !response.status().is_success() {
            let status = response.status();
            tracing::error!("Gemini API error: {}", status);
            return Err(anyhow!("Gemini API error: {}", status));
        }

        let data = response.json::<serde_json::Value>().await?;

        let text = data
            .get("candidates")
            .and_then(|c| c.get(0))
            .and_then(|c| c.get("content"))
            .and_then(|c| c.get("parts"))
            .and_then(|p| p.get(0))
            .and_then(|p| p.get("text"))
            .and_then(|t| t.as_str())
            .unwrap_or("No response")
            .to_string();

        // Cost: ~$2.50 per 1M tokens (Gemini Pro)
        let cost = Some(0.0025);

        Ok(LLMResponse {
            text,
            provider: "gemini".to_string(),
            tokens_used: None,
            cost,
        })
    }
}

/// Ollama (local model) client
#[derive(Debug, Clone)]
pub struct OllamaClient {
    /// Base URL for Ollama service
    base_url: String,
    /// HTTP client
    client: reqwest::Client,
    /// Model identifier
    model: String,
}

impl OllamaClient {
    /// Create new Ollama client (local)
    pub fn new(base_url: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(120))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            model: "mistral".to_string(),
        }
    }

    /// Call Ollama API (local)
    pub async fn complete(&self, req: &LLMRequest) -> anyhow::Result<LLMResponse> {
        let url = format!("{}/api/generate", self.base_url);

        let payload = serde_json::json!({
            "model": self.model,
            "prompt": format!("System: {}\n\nUser: {}", req.system, req.message),
            "stream": false,
            "temperature": req.temperature
        });

        let response = self.client.post(&url).json(&payload).send().await?;

        if !response.status().is_success() {
            let status = response.status();
            tracing::error!("Ollama error: {}", status);
            return Err(anyhow!("Ollama error: {}", status));
        }

        let data = response.json::<serde_json::Value>().await?;

        let text = data
            .get("response")
            .and_then(|r| r.as_str())
            .unwrap_or("No response")
            .to_string();

        Ok(LLMResponse {
            text,
            provider: "ollama".to_string(),
            tokens_used: None,
            cost: Some(0.0), // Local - no cost
        })
    }
}

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

    #[test]
    fn test_llm_request() {
        let req = LLMRequest {
            system: "You are helpful".to_string(),
            message: "Hello".to_string(),
            temperature: 0.7,
            max_tokens: 100,
        };

        assert_eq!(req.temperature, 0.7);
        assert_eq!(req.max_tokens, 100);
    }

    #[test]
    fn test_llm_response() {
        let resp = LLMResponse {
            text: "Hello, world!".to_string(),
            provider: "claude".to_string(),
            tokens_used: Some(50),
            cost: Some(0.0001),
        };

        assert_eq!(resp.provider, "claude");
        assert!(resp.cost.is_some());
    }

    #[test]
    fn test_claude_client_creation() {
        let _client = ClaudeClient::new("fake-key");
        // Would make API call in integration test
    }

    #[test]
    fn test_openai_client_creation() {
        let _client = OpenAIClient::new("fake-key");
        // Would make API call in integration test
    }

    #[test]
    fn test_gemini_client_creation() {
        let _client = GeminiClient::new("fake-key");
        // Would make API call in integration test
    }

    #[test]
    fn test_ollama_client_creation() {
        let _client = OllamaClient::new("http://localhost:11434");
        // Would make API call in integration test
    }
}
