use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
use tokio::sync::Mutex;

/// Online Synonym Expander
/// Fetches synonyms from LLM (llama.cpp server) on-demand and caches them.
///
/// Environment variables:
/// - `MOMENTRY_ONLINE_SYNONYM` - Enable online synonym expansion (default: false)
/// - `MOMENTRY_LLM_SYNONYM_URL` - LLM server URL (default: http://127.0.0.1:8081)
/// - `MOMENTRY_LLM_SYNONYM_MODEL` - Model name (default: gemma4)
/// - `MOMENTRY_LLM_SYNONYM_TIMEOUT` - Request timeout in seconds (default: 60)

#[derive(Debug, Deserialize)]
struct LlmResponse {
    choices: Vec<LlmChoice>,
}

#[derive(Debug, Deserialize)]
struct LlmChoice {
    message: LlmMessage,
}

#[derive(Debug, Deserialize)]
struct LlmMessage {
    content: String,
}

#[derive(Debug)]
pub struct OnlineSynonymExpander {
    /// Local synonym cache (loaded from file)
    local_map: HashMap<String, Vec<String>>,
    /// Runtime cache for LLM-fetched synonyms
    runtime_cache: Arc<Mutex<HashMap<String, Vec<String>>>>,
    /// LLM server URL
    api_url: String,
    /// Model name
    model: String,
    /// Request timeout
    timeout_secs: u64,
}

static SYSTEM_PROMPT: &str = r#"You are a synonym generation assistant. For each given word, provide 8-12 synonyms in the same language.
Rules:
1. Return ONLY a JSON array of strings, nothing else
2. Synonyms should be contextually relevant for video content search
3. Include common words, informal terms, and related concepts
4. Do NOT include the input word in the output
5. All synonyms must be in the SAME language as the input word
6. No explanations, no markdown, just the JSON array

Example input: "money"
Example output: ["cash", "dollar", "currency", "funds", "bucks", "greenbacks", "coins", "wealth", "payment"]"#;

impl OnlineSynonymExpander {
    pub fn new(local_file_path: Option<&str>) -> Self {
        let local_map = if let Some(path) = local_file_path {
            match Self::load_local_file(path) {
                Ok(map) => map,
                Err(e) => {
                    tracing::warn!("Failed to load local synonym file {}: {}", path, e);
                    HashMap::new()
                }
            }
        } else {
            HashMap::new()
        };

        let api_url = env::var("MOMENTRY_LLM_SYNONYM_URL")
            .unwrap_or_else(|_| "http://127.0.0.1:8081".to_string());
        let model = env::var("MOMENTRY_LLM_SYNONYM_MODEL").unwrap_or_else(|_| "gemma4".to_string());
        let timeout_secs = env::var("MOMENTRY_LLM_SYNONYM_TIMEOUT")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(60);

        Self {
            local_map,
            runtime_cache: Arc::new(Mutex::new(HashMap::new())),
            api_url,
            model,
            timeout_secs,
        }
    }

    fn load_local_file(path: &str) -> Result<HashMap<String, Vec<String>>> {
        let content = std::fs::read_to_string(path).context("Failed to read local synonym file")?;
        let map: HashMap<String, Vec<String>> =
            serde_json::from_str(&content).context("Failed to parse local synonym JSON")?;
        Ok(map)
    }

    /// Get synonyms for a word. Checks local map first, then runtime cache, then fetches from LLM.
    pub async fn expand_word(&self, word: &str) -> String {
        // 1. Check local map
        if let Some(syns) = self.local_map.get(word) {
            if !syns.is_empty() {
                let mut parts = vec![word.to_string()];
                parts.extend_from_slice(syns);
                return format!("({})", parts.join(" | "));
            }
        }

        // 2. Check runtime cache
        let mut cache = self.runtime_cache.lock().await;
        if let Some(syns) = cache.get(word) {
            if !syns.is_empty() {
                let mut parts = vec![word.to_string()];
                parts.extend_from_slice(syns);
                return format!("({})", parts.join(" | "));
            }
        }
        drop(cache);

        // 3. Fetch from LLM
        if let Ok(synonyms) = self.fetch_from_llm(word).await {
            if !synonyms.is_empty() {
                // Add to runtime cache
                let mut cache = self.runtime_cache.lock().await;
                cache.insert(word.to_string(), synonyms.clone());
                drop(cache);

                let mut parts = vec![word.to_string()];
                parts.extend_from_slice(&synonyms);
                return format!("({})", parts.join(" | "));
            }
        }

        // 4. Fallback: return original word
        word.to_string()
    }

    async fn fetch_from_llm(&self, word: &str) -> Result<Vec<String>> {
        let client = reqwest::Client::new();

        let prompt = format!(
            r#"Give synonyms for: "{}"
Return ONLY a JSON array of strings, nothing else. Do NOT include the input word."#,
            word
        );

        let payload = serde_json::json!({
            "model": self.model,
            "messages": [
                {
                    "role": "system",
                    "content": SYSTEM_PROMPT
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ],
            "temperature": 0.3,
            "stream": false,
            "max_tokens": 256,
        });

        let response = client
            .post(format!("{}/v1/chat/completions", self.api_url))
            .json(&payload)
            .timeout(std::time::Duration::from_secs(self.timeout_secs))
            .send()
            .await
            .context("LLM request failed")?;

        if !response.status().is_success() {
            anyhow::bail!("LLM request failed with status: {}", response.status());
        }

        let llm_resp: LlmResponse = response
            .json()
            .await
            .context("Failed to parse LLM response")?;

        let content = &llm_resp
            .choices
            .get(0)
            .context("No choices in LLM response")?
            .message
            .content;

        // Extract JSON from response (handle markdown code blocks)
        let json_str = if let Some(start) = content.find('[') {
            if let Some(end) = content.rfind(']') {
                &content[start..=end]
            } else {
                anyhow::bail!("No JSON array found in LLM response");
            }
        } else {
            anyhow::bail!("No JSON array found in LLM response");
        };

        let synonyms: Vec<String> =
            serde_json::from_str(json_str).context("Failed to parse LLM synonyms JSON")?;

        // Filter and normalize
        let cleaned: Vec<String> = synonyms
            .into_iter()
            .map(|s| s.trim().to_lowercase())
            .filter(|s| !s.is_empty() && !s.contains(' ')) // Filter out multi-word synonyms for to_tsquery compatibility
            .collect();

        if cleaned.is_empty() {
            anyhow::bail!("No valid synonyms returned");
        }

        tracing::info!(
            "LLM fetched {} synonyms for '{}': {:?}",
            cleaned.len(),
            word,
            cleaned.iter().take(5).collect::<Vec<_>>()
        );

        Ok(cleaned)
    }

    /// Get the number of cached synonyms
    pub async fn cache_size(&self) -> usize {
        self.runtime_cache.lock().await.len()
    }
}

/// Global online synonym expander (lazy-loaded)
static ONLINE_EXPANDER: Lazy<Option<OnlineSynonymExpander>> = Lazy::new(|| {
    if env::var("MOMENTRY_ONLINE_SYNONYM").is_ok() {
        let local_file = env::var("MOMENTRY_SYNONYM_FILE").ok();
        tracing::info!("Initializing online synonym expander");
        Some(OnlineSynonymExpander::new(local_file.as_deref()))
    } else {
        None
    }
});

/// Get the global online synonym expander (if enabled)
pub fn global_online_expander() -> Option<&'static OnlineSynonymExpander> {
    ONLINE_EXPANDER.as_ref()
}
