use reqwest::{Client, Error as ReqwestError};
use serde::{Deserialize, Serialize};
use std::time::Duration;

const OLLAMA_URL: &str = "http://nxtgauge-ai-assistant:11434";
const DEFAULT_MODEL: &str = "gemma3:270m";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);

#[derive(Debug, Clone)]
pub struct OllamaClient {
    http_client: Client,
    base_url: String,
    model: String,
}

#[derive(Debug, Serialize)]
struct GenerateRequest {
    model: String,
    prompt: String,
    stream: bool,
    options: Option<GenerationOptions>,
}

#[derive(Debug, Serialize, Default)]
struct GenerationOptions {
    temperature: Option<f32>,
    top_p: Option<f32>,
    top_k: Option<i32>,
    num_predict: Option<i32>,
}

#[derive(Debug, Deserialize)]
pub struct GenerateResponse {
    pub model: String,
    pub created_at: String,
    pub response: String,
    pub done: bool,
    pub context: Option<Vec<i32>>,
    pub total_duration: Option<u64>,
    pub load_duration: Option<u64>,
    pub prompt_eval_count: Option<i32>,
    pub prompt_eval_duration: Option<u64>,
    pub eval_count: Option<i32>,
    pub eval_duration: Option<u64>,
}

#[derive(Debug, Deserialize)]
struct OllamaErrorResponse {
    error: String,
}

#[derive(Debug, thiserror::Error)]
pub enum OllamaError {
    #[error("HTTP request failed: {0}")]
    RequestFailed(#[from] ReqwestError),
    
    #[error("Ollama API error: {0}")]
    ApiError(String),
    
    #[error("Failed to parse response: {0}")]
    ParseError(String),
    
    #[error("Connection timeout")]
    Timeout,
    
    #[error("Model not found: {0}")]
    ModelNotFound(String),
}

impl OllamaClient {
    pub fn new() -> Self {
        let http_client = Client::builder()
            .timeout(REQUEST_TIMEOUT)
            .build()
            .expect("Failed to create HTTP client");
        
        Self {
            http_client,
            base_url: OLLAMA_URL.to_string(),
            model: DEFAULT_MODEL.to_string(),
        }
    }
    
    pub fn with_url(base_url: impl Into<String>) -> Self {
        let http_client = Client::builder()
            .timeout(REQUEST_TIMEOUT)
            .build()
            .expect("Failed to create HTTP client");
        
        Self {
            http_client,
            base_url: base_url.into(),
            model: DEFAULT_MODEL.to_string(),
        }
    }
    
    pub fn with_model(mut self, model: impl Into<String>) -> Self {
        self.model = model.into();
        self
    }
    
    /// Generate text using Ollama API
    pub async fn generate(&self, prompt: &str) -> Result<GenerateResponse, OllamaError> {
        let url = format!("{}/api/generate", self.base_url);
        
        let request_body = GenerateRequest {
            model: self.model.clone(),
            prompt: prompt.to_string(),
            stream: false,
            options: Some(GenerationOptions {
                temperature: Some(0.7),
                top_p: Some(0.9),
                num_predict: Some(512),
            }),
        };
        
        let response = self.http_client
            .post(&url)
            .json(&request_body)
            .send()
            .await?;
        
        let status = response.status();
        
        if !status.is_success() {
            let error_text = response.text().await.unwrap_or_default();
            if let Ok(err) = serde_json::from_str::<OllamaErrorResponse>(&error_text) {
                return Err(OllamaError::ApiError(err.error));
            }
            return Err(OllamaError::ApiError(format!(
                "HTTP {}: {}",
                status.as_u16(),
                error_text
            )));
        }
        
        let generate_response = response
            .json::<GenerateResponse>()
            .await
            .map_err(|e| OllamaError::ParseError(e.to_string()))?;
        
        Ok(generate_response)
    }
    
    /// Generate a job description from requirements
    pub async fn generate_job_description(&self, requirements: &str) -> Result<String, OllamaError> {
        let prompt = format!(
            r#"You are an expert recruitment professional. Create a professional, engaging job description based on the following requirements:

Requirements: {}

Generate a complete job description that includes:
1. Job Title (suggested)
2. Company Overview section
3. Job Summary
4. Key Responsibilities
5. Required Qualifications
6. Preferred Qualifications (if applicable)
7. Benefits/Perks (optional)
8. Application Instructions

Make it ATS-friendly and compelling. Output only the job description, no extra commentary."#,
            requirements
        );
        
        let response = self.generate(&prompt).await?;
        Ok(response.response)
    }
    
    /// Check if Ollama is reachable and model is available
    pub async fn health_check(&self) -> Result<(), OllamaError> {
        let url = format!("{}/api/tags", self.base_url);
        
        let response = self.http_client
            .get(&url)
            .send()
            .await;
        
        match response {
            Ok(resp) if resp.status().is_success() => Ok(()),
            Ok(resp) => Err(OllamaError::ApiError(format!(
                "Health check failed with status: {}",
                resp.status().as_u16()
            ))),
            Err(e) if e.is_timeout() => Err(OllamaError::Timeout),
            Err(e) => Err(OllamaError::RequestFailed(e)),
        }
    }
    
    /// Pull a model if not already available
    pub async fn pull_model(&self) -> Result<(), OllamaError> {
        let url = format!("{}/api/pull", self.base_url);
        
        #[derive(Serialize)]
        struct PullRequest {
            name: String,
            stream: bool,
        }
        
        let request_body = PullRequest {
            name: self.model.clone(),
            stream: false,
        };
        
        let response = self.http_client
            .post(&url)
            .json(&request_body)
            .send()
            .await?;
        
        if response.status().is_success() {
            Ok(())
        } else {
            Err(OllamaError::ModelNotFound(self.model.clone()))
        }
    }
}

impl Default for OllamaClient {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    // These tests require a running Ollama instance
    // Run with: cargo test -- --ignored
    
    #[tokio::test]
    #[ignore = "Requires Ollama server"]
    async fn test_generate() {
        let client = OllamaClient::new();
        let result = client.generate("Hello, world!").await;
        assert!(result.is_ok());
    }
    
    #[tokio::test]
    #[ignore = "Requires Ollama server"]
    async fn test_generate_job_description() {
        let client = OllamaClient::new();
        let requirements = "Senior Rust Developer with 5+ years experience, Actix-web knowledge required";
        let result = client.generate_job_description(requirements).await;
        assert!(result.is_ok());
        let jd = result.unwrap();
        assert!(!jd.is_empty());
    }
}