//! Gemini API client for LLM-based extraction.
//!
//! Uses ureq (sync HTTP) consistent with other Aphoria HTTP clients
//! (corpus builders, hosted.rs).

use std::thread;
use std::time::Duration;

use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, warn};

use crate::config::LlmConfig;
use crate::AphoriaError;

/// Default initial delay for rate limit backoff (milliseconds).
const DEFAULT_RATE_LIMIT_INITIAL_DELAY_MS: u64 = 500;

/// Default maximum retries for rate limit errors.
const DEFAULT_RATE_LIMIT_MAX_RETRIES: usize = 5;

/// Result from an LLM API call.
#[derive(Debug, Clone)]
pub struct LlmResult {
    /// The response text content.
    pub response_text: String,
    /// Number of input tokens used.
    pub input_tokens: usize,
    /// Number of output tokens generated.
    pub output_tokens: usize,
}

/// Gemini API client.
#[derive(Debug)]
pub struct GeminiClient {
    /// API key for authentication.
    api_key: String,
    /// Model identifier (configured via `llm.model` in aphoria.toml).
    model: String,
    /// Timeout for API calls.
    timeout: Duration,
    /// Maximum tokens per file (used for max_tokens parameter).
    max_tokens_per_file: usize,
}

/// Request payload for Gemini generateContent API.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GenerateContentRequest {
    contents: Vec<Content>,
    system_instruction: Option<SystemInstruction>,
    generation_config: GenerationConfig,
}

/// System instruction wrapper.
#[derive(Debug, Serialize)]
struct SystemInstruction {
    parts: Vec<Part>,
}

/// Content in the request/response.
#[derive(Debug, Serialize, Deserialize)]
struct Content {
    #[serde(skip_serializing_if = "Option::is_none")]
    role: Option<String>,
    parts: Vec<Part>,
}

/// A part of content.
#[derive(Debug, Serialize, Deserialize)]
struct Part {
    text: String,
}

/// Generation configuration.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GenerationConfig {
    max_output_tokens: usize,
    temperature: f32,
}

/// Response from Gemini generateContent API.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GenerateContentResponse {
    candidates: Option<Vec<Candidate>>,
    usage_metadata: Option<UsageMetadata>,
}

/// A candidate response.
#[derive(Debug, Deserialize)]
struct Candidate {
    content: Content,
}

/// Token usage metadata.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UsageMetadata {
    prompt_token_count: Option<usize>,
    candidates_token_count: Option<usize>,
}

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

/// Error details.
#[derive(Debug, Deserialize)]
struct ApiError {
    message: String,
    status: Option<String>,
}

impl GeminiClient {
    /// Create a new Gemini client if LLM is configured and API key is available.
    ///
    /// Returns `Ok(None)` if LLM is disabled or API key is not set.
    /// Returns `Err` if configuration is invalid.
    pub fn new(config: &LlmConfig) -> Result<Option<Self>, AphoriaError> {
        if !config.enabled {
            return Ok(None);
        }

        // Get API key from environment
        let api_key = match std::env::var(&config.api_key_env) {
            Ok(key) if !key.is_empty() => key,
            Ok(_) => {
                warn!(
                    env_var = %config.api_key_env,
                    "LLM enabled but API key environment variable is empty"
                );
                return Ok(None);
            }
            Err(_) => {
                warn!(
                    env_var = %config.api_key_env,
                    "LLM enabled but API key environment variable not set"
                );
                return Ok(None);
            }
        };

        // Validate provider
        if config.provider != "gemini" {
            return Err(AphoriaError::LlmApi(format!(
                "Unsupported LLM provider '{}'. Only 'gemini' is supported.",
                config.provider
            )));
        }

        Ok(Some(Self {
            api_key,
            model: config.model.clone(),
            timeout: Duration::from_secs(config.timeout_secs),
            max_tokens_per_file: config.max_tokens_per_file,
        }))
    }

    /// Send a prompt to Gemini and get the response.
    ///
    /// Automatically retries with exponential backoff on rate limit (429) errors.
    #[instrument(skip(self, content), fields(model = %self.model, content_len = content.len()))]
    pub fn complete(&self, system_prompt: &str, content: &str) -> Result<LlmResult, AphoriaError> {
        self.complete_with_retry(
            system_prompt,
            content,
            DEFAULT_RATE_LIMIT_INITIAL_DELAY_MS,
            DEFAULT_RATE_LIMIT_MAX_RETRIES,
        )
    }

    /// Send a prompt with configurable retry parameters.
    ///
    /// Uses exponential backoff starting at `initial_delay_ms` and doubling
    /// on each retry up to `max_retries` attempts.
    pub fn complete_with_retry(
        &self,
        system_prompt: &str,
        content: &str,
        initial_delay_ms: u64,
        max_retries: usize,
    ) -> Result<LlmResult, AphoriaError> {
        let mut delay_ms = initial_delay_ms;

        for attempt in 0..=max_retries {
            match self.complete_once(system_prompt, content) {
                Ok(result) => return Ok(result),
                Err(e) if Self::is_rate_limit_error(&e) => {
                    if attempt == max_retries {
                        warn!(attempt, max_retries, "Rate limit exceeded after all retries");
                        return Err(e);
                    }
                    warn!(attempt, delay_ms, max_retries, "Rate limited (429), backing off");
                    thread::sleep(Duration::from_millis(delay_ms));
                    delay_ms = delay_ms.saturating_mul(2); // Exponential backoff
                }
                Err(e) => return Err(e),
            }
        }

        // This is unreachable because the loop either returns Ok, returns Err,
        // or continues. But Rust doesn't know that, so we need this.
        Err(AphoriaError::LlmApi("Unexpected retry loop exit".to_string()))
    }

    /// Check if an error is a rate limit error that should trigger retry.
    fn is_rate_limit_error(e: &AphoriaError) -> bool {
        match e {
            AphoriaError::LlmApi(msg) => {
                msg.contains("429")
                    || msg.contains("RESOURCE_EXHAUSTED")
                    || msg.contains("rate limit")
                    || msg.contains("Rate limit")
            }
            _ => false,
        }
    }

    /// Send a single prompt to Gemini without retry logic.
    fn complete_once(&self, system_prompt: &str, content: &str) -> Result<LlmResult, AphoriaError> {
        let request = GenerateContentRequest {
            contents: vec![Content {
                role: Some("user".to_string()),
                parts: vec![Part { text: content.to_string() }],
            }],
            system_instruction: Some(SystemInstruction {
                parts: vec![Part { text: system_prompt.to_string() }],
            }),
            generation_config: GenerationConfig {
                max_output_tokens: self.max_tokens_per_file,
                temperature: 0.1, // Low temperature for consistent extraction
            },
        };

        let body = serde_json::to_string(&request)
            .map_err(|e| AphoriaError::LlmApi(format!("Failed to serialize request: {}", e)))?;

        let url = format!(
            "https://generativelanguage.googleapis.com/v1beta/models/{}:generateContent?key={}",
            self.model, self.api_key
        );

        debug!(body_len = body.len(), "Sending request to Gemini API");

        let response = ureq::post(&url)
            .set("Content-Type", "application/json")
            .timeout(self.timeout)
            .send_string(&body)
            .map_err(|e| match e {
                ureq::Error::Status(status, response) => {
                    let body = response.into_string().unwrap_or_default();
                    if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(&body) {
                        AphoriaError::LlmApi(format!(
                            "API error ({}): {}",
                            error_response.error.status.unwrap_or_else(|| status.to_string()),
                            error_response.error.message
                        ))
                    } else {
                        AphoriaError::LlmApi(format!("HTTP {} - {}", status, body))
                    }
                }
                ureq::Error::Transport(transport) => {
                    AphoriaError::LlmApi(format!("Transport error: {}", transport))
                }
            })?;

        let response_body = response
            .into_string()
            .map_err(|e| AphoriaError::LlmApi(format!("Failed to read response: {}", e)))?;

        let response: GenerateContentResponse = serde_json::from_str(&response_body)
            .map_err(|e| AphoriaError::LlmParse(format!("Failed to parse response: {}", e)))?;

        // Extract text from candidates
        let response_text = response
            .candidates
            .unwrap_or_default()
            .into_iter()
            .flat_map(|c| c.content.parts)
            .map(|p| p.text)
            .collect::<Vec<_>>()
            .join("");

        let usage = response
            .usage_metadata
            .unwrap_or(UsageMetadata { prompt_token_count: None, candidates_token_count: None });

        debug!(
            input_tokens = usage.prompt_token_count.unwrap_or(0),
            output_tokens = usage.candidates_token_count.unwrap_or(0),
            response_len = response_text.len(),
            "Received response from Gemini API"
        );

        Ok(LlmResult {
            response_text,
            input_tokens: usage.prompt_token_count.unwrap_or(0),
            output_tokens: usage.candidates_token_count.unwrap_or(0),
        })
    }
}

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

    #[test]
    fn test_client_disabled_by_default() {
        let config = LlmConfig::default();
        assert!(!config.enabled);
        let client = GeminiClient::new(&config).expect("should not fail");
        assert!(client.is_none());
    }

    #[test]
    fn test_client_requires_api_key() {
        let config = LlmConfig {
            enabled: true,
            api_key_env: "NONEXISTENT_API_KEY_FOR_TEST".to_string(),
            ..Default::default()
        };
        let client = GeminiClient::new(&config).expect("should not fail");
        assert!(client.is_none());
    }

    #[test]
    fn test_client_rejects_unsupported_provider() {
        // Set a fake API key for this test
        std::env::set_var("TEST_LLM_API_KEY", "test-key");

        let config = LlmConfig {
            enabled: true,
            provider: "openai".to_string(),
            api_key_env: "TEST_LLM_API_KEY".to_string(),
            ..Default::default()
        };
        let result = GeminiClient::new(&config);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Unsupported LLM provider"));

        std::env::remove_var("TEST_LLM_API_KEY");
    }

    #[test]
    fn test_is_rate_limit_error_429() {
        let error = AphoriaError::LlmApi("HTTP 429 - Too Many Requests".to_string());
        assert!(GeminiClient::is_rate_limit_error(&error));
    }

    #[test]
    fn test_is_rate_limit_error_resource_exhausted() {
        let error =
            AphoriaError::LlmApi("API error (RESOURCE_EXHAUSTED): quota exceeded".to_string());
        assert!(GeminiClient::is_rate_limit_error(&error));
    }

    #[test]
    fn test_is_rate_limit_error_false_for_other_errors() {
        let error = AphoriaError::LlmApi("HTTP 500 - Internal Server Error".to_string());
        assert!(!GeminiClient::is_rate_limit_error(&error));

        let error = AphoriaError::LlmApi("Transport error: connection refused".to_string());
        assert!(!GeminiClient::is_rate_limit_error(&error));
    }
}
