//! HTTP API Client for External LLM Services
//!
//! Provides HTTP-based integration with:
//! - Ollama (local LLM)
//! - OpenAI API
//! - Azure OpenAI
//! - Any OpenAI-compatible API

use std::time::Duration;

use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{Deserialize, Serialize};

use super::config::ReasoningConfig;
use super::error::ReasoningError;
use super::traits::{Message, MessageRole, ReasoningEngine, ReasoningResult, TokenUsage};

// ============================================================================
// API Request/Response Types
// ============================================================================

/// OpenAI-compatible chat message
#[derive(Debug, Serialize, Deserialize)]
struct ApiMessage {
    role: String,
    content: String,
}

impl From<&Message> for ApiMessage {
    fn from(msg: &Message) -> Self {
        Self {
            role: match msg.role {
                MessageRole::System => "system".to_string(),
                MessageRole::User => "user".to_string(),
                MessageRole::Assistant => "assistant".to_string(),
            },
            content: msg.content.clone(),
        }
    }
}

/// Chat completion request
#[derive(Debug, Serialize)]
struct ChatRequest {
    model: String,
    messages: Vec<ApiMessage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_tokens: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    stream: Option<bool>,
}

/// Chat completion response
#[derive(Debug, Deserialize)]
struct ChatResponse {
    choices: Vec<ChatChoice>,
    #[serde(default)]
    usage: Option<ApiUsage>,
    #[serde(default)]
    model: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ChatChoice {
    message: ApiMessage,
    #[serde(default)]
    finish_reason: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ApiUsage {
    prompt_tokens: usize,
    completion_tokens: usize,
    total_tokens: usize,
}

/// Embedding request
#[derive(Debug, Serialize)]
struct EmbeddingRequest {
    model: String,
    input: EmbeddingInput,
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
enum EmbeddingInput {
    Single(String),
    Batch(Vec<String>),
}

/// Embedding response
#[derive(Debug, Deserialize)]
struct EmbeddingResponse {
    data: Vec<EmbeddingData>,
    #[serde(default)]
    usage: Option<ApiUsage>,
}

#[derive(Debug, Deserialize)]
struct EmbeddingData {
    embedding: Vec<f32>,
    index: usize,
}

/// API error response
#[derive(Debug, Deserialize)]
struct ApiError {
    error: ApiErrorDetail,
}

#[derive(Debug, Deserialize)]
struct ApiErrorDetail {
    message: String,
    #[serde(default)]
    r#type: Option<String>,
    #[serde(default)]
    code: Option<String>,
}

// ============================================================================
// HTTP API Client
// ============================================================================

/// HTTP client for OpenAI-compatible APIs
pub struct HttpApiClient {
    client: reqwest::Client,
    config: ReasoningConfig,
    base_url: String,
}

impl HttpApiClient {
    /// Create from ReasoningConfig
    pub fn from_config(config: &ReasoningConfig) -> Result<Self, ReasoningError> {
        let mut headers = HeaderMap::new();
        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));

        // Add API key if provided
        if let Some(ref api_key) = config.api_key {
            let auth_value = format!("Bearer {}", api_key);
            headers.insert(
                AUTHORIZATION,
                HeaderValue::from_str(&auth_value)
                    .map_err(|e| ReasoningError::AuthError(e.to_string()))?,
            );
        }

        // Add extra headers
        for (key, value) in &config.extra_headers {
            headers.insert(
                reqwest::header::HeaderName::from_bytes(key.as_bytes())
                    .map_err(|e| ReasoningError::RequestError(e.to_string()))?,
                HeaderValue::from_str(value)
                    .map_err(|e| ReasoningError::RequestError(e.to_string()))?,
            );
        }

        let client = reqwest::Client::builder()
            .default_headers(headers)
            .timeout(Duration::from_secs(config.timeout_secs))
            .build()
            .map_err(|e| ReasoningError::ConnectionError(e.to_string()))?;

        // Normalize endpoint URL
        let base_url = config.endpoint.trim_end_matches('/').to_string();

        Ok(Self {
            client,
            config: config.clone(),
            base_url,
        })
    }

    /// Create a client for Ollama
    pub fn ollama(model: &str) -> Result<Self, ReasoningError> {
        Self::from_config(&ReasoningConfig::ollama(model))
    }

    /// Create a client for OpenAI
    pub fn openai(api_key: &str, model: &str) -> Result<Self, ReasoningError> {
        Self::from_config(&ReasoningConfig::openai(api_key, model))
    }

    /// Send a chat completion request
    pub async fn chat_async(&self, messages: &[Message]) -> Result<ReasoningResult, ReasoningError> {
        let url = format!("{}/chat/completions", self.base_url);

        let request = ChatRequest {
            model: self.config.model.clone(),
            messages: messages.iter().map(ApiMessage::from).collect(),
            max_tokens: self.config.max_tokens,
            temperature: self.config.temperature,
            stream: Some(false),
        };

        let response = self
            .client
            .post(&url)
            .json(&request)
            .send()
            .await
            .map_err(|e| {
                if e.is_timeout() {
                    ReasoningError::Timeout {
                        timeout_secs: self.config.timeout_secs,
                    }
                } else if e.is_connect() {
                    ReasoningError::ConnectionError(e.to_string())
                } else {
                    ReasoningError::RequestError(e.to_string())
                }
            })?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            if let Ok(api_error) = serde_json::from_str::<ApiError>(&error_text) {
                return Err(ReasoningError::ApiError {
                    status: status.as_u16(),
                    message: api_error.error.message,
                });
            }
            return Err(ReasoningError::ApiError {
                status: status.as_u16(),
                message: error_text,
            });
        }

        let chat_response: ChatResponse = response
            .json()
            .await
            .map_err(|e| ReasoningError::InvalidResponse(e.to_string()))?;

        let content = chat_response
            .choices
            .first()
            .map(|c| c.message.content.clone())
            .unwrap_or_default();

        let usage = chat_response.usage.map(|u| TokenUsage {
            prompt_tokens: u.prompt_tokens,
            completion_tokens: u.completion_tokens,
            total_tokens: u.total_tokens,
        });

        Ok(ReasoningResult {
            content,
            usage,
            model: chat_response.model,
        })
    }

    /// Send an embedding request
    pub async fn embed_async(&self, text: &str) -> Result<Vec<f32>, ReasoningError> {
        let embeddings = self.embed_batch_async(&[text.to_string()]).await?;
        embeddings
            .into_iter()
            .next()
            .ok_or_else(|| ReasoningError::InvalidResponse("No embedding returned".to_string()))
    }

    /// Send a batch embedding request
    pub async fn embed_batch_async(&self, texts: &[String]) -> Result<Vec<Vec<f32>>, ReasoningError> {
        let url = format!("{}/embeddings", self.base_url);

        let request = EmbeddingRequest {
            model: self.config.model.clone(),
            input: if texts.len() == 1 {
                EmbeddingInput::Single(texts[0].clone())
            } else {
                EmbeddingInput::Batch(texts.to_vec())
            },
        };

        let response = self
            .client
            .post(&url)
            .json(&request)
            .send()
            .await
            .map_err(|e| ReasoningError::RequestError(e.to_string()))?;

        let status = response.status();
        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            return Err(ReasoningError::ApiError {
                status: status.as_u16(),
                message: error_text,
            });
        }

        let embedding_response: EmbeddingResponse = response
            .json()
            .await
            .map_err(|e| ReasoningError::InvalidResponse(e.to_string()))?;

        // Sort by index and extract embeddings
        let mut data = embedding_response.data;
        data.sort_by_key(|d| d.index);
        Ok(data.into_iter().map(|d| d.embedding).collect())
    }

    /// Blocking chat completion (for sync contexts)
    pub fn chat_blocking(&self, messages: &[Message]) -> Result<ReasoningResult, ReasoningError> {
        // Create a runtime for blocking execution
        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .map_err(|e| ReasoningError::RequestError(e.to_string()))?;

        rt.block_on(self.chat_async(messages))
    }

    /// Check if the API is reachable
    pub async fn health_check(&self) -> bool {
        // Try to get models list (common endpoint)
        let url = format!("{}/models", self.base_url);
        match self.client.get(&url).send().await {
            Ok(response) => response.status().is_success(),
            Err(_) => false,
        }
    }
}

impl ReasoningEngine for HttpApiClient {
    fn check_logic_consistency(&self, fact_a: &str, fact_b: &str) -> Result<bool, ReasoningError> {
        let prompt = format!(
            "Determine if these two statements are logically consistent with each other. \
             Reply with only 'true' if they are consistent, or 'false' if they contradict each other.\n\n\
             Statement A: {}\n\
             Statement B: {}\n\n\
             Answer (true/false):",
            fact_a, fact_b
        );

        let messages = vec![
            Message::system("You are a logic analyzer. Reply only with 'true' or 'false'."),
            Message::user(prompt),
        ];

        let result = self.chat_blocking(&messages)?;
        let answer = result.content.trim().to_lowercase();
        Ok(answer.contains("true"))
    }

    fn understand_intent(&self, query: &str) -> Result<String, ReasoningError> {
        let messages = vec![
            Message::system(
                "You are an intent analyzer. Analyze the user's query and describe their intent \
                 in a clear, structured way. Be concise.",
            ),
            Message::user(query),
        ];

        let result = self.chat_blocking(&messages)?;
        Ok(result.content)
    }

    fn summarize(&self, content: &str, max_tokens: usize) -> Result<String, ReasoningError> {
        let messages = vec![
            Message::system(format!(
                "You are a summarizer. Create a concise summary in approximately {} words or less.",
                max_tokens / 2 // Rough estimate: 1 token ≈ 0.75 words
            )),
            Message::user(format!("Summarize the following:\n\n{}", content)),
        ];

        let result = self.chat_blocking(&messages)?;
        Ok(result.content)
    }

    fn chat(&self, messages: &[Message]) -> Result<ReasoningResult, ReasoningError> {
        self.chat_blocking(messages)
    }

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

    fn is_available(&self) -> bool {
        // Create a runtime for blocking execution
        if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
        {
            rt.block_on(self.health_check())
        } else {
            false
        }
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_api_message_conversion() {
        let msg = Message::user("Hello");
        let api_msg: ApiMessage = (&msg).into();
        assert_eq!(api_msg.role, "user");
        assert_eq!(api_msg.content, "Hello");
    }

    #[test]
    fn test_client_creation() {
        let config = ReasoningConfig::ollama("llama3.2");
        let client = HttpApiClient::from_config(&config);
        assert!(client.is_ok());
    }

    // Integration tests (require running Ollama)
    #[tokio::test]
    #[ignore]
    async fn test_ollama_chat() {
        let client = HttpApiClient::ollama("llama3.2").unwrap();
        
        if !client.health_check().await {
            eprintln!("Ollama not available, skipping test");
            return;
        }

        let messages = vec![Message::user("Say hello in one word.")];
        let result = client.chat_async(&messages).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    #[ignore]
    async fn test_ollama_embedding() {
        let config = ReasoningConfig::ollama("nomic-embed-text");
        let client = HttpApiClient::from_config(&config).unwrap();

        if !client.health_check().await {
            eprintln!("Ollama not available, skipping test");
            return;
        }

        let embedding = client.embed_async("Hello, world!").await;
        assert!(embedding.is_ok());
        assert!(!embedding.unwrap().is_empty());
    }
}
