use crate::llm::{self, AppState, ChatMessage, ChatResponse, GenerateRequest, LlmEvent, OllamaChatResponse};
use serde_json::json;
use tauri::ipc::Channel;

// ─── Simple (non-streaming) generate ─────────────────────────

#[tauri::command]
pub async fn generate(state: tauri::State<'_, AppState>, req: GenerateRequest) -> Result<String, String> {
    let config = {
        let guard = state.config.lock().map_err(|e| e.to_string())?;
        guard.clone()
    };

    let client = reqwest::Client::new();
    let messages = llm::build_messages(req.system.as_deref(), &req.prompt);
    let temperature = req.temperature.unwrap_or(config.temperature);
    let max_tokens = req.max_tokens.unwrap_or(config.max_tokens);

    // Inject retrieved lore into the system prompt so every generator stays
    // consistent with the user's world bible. Shared path = every caller.
    let messages = inject_lore(&state, &client, &config, messages, &req.rag_query).await;

    if llm::is_ollama(&config.api_url) {
        call_ollama(&client, &config, &messages, temperature, max_tokens).await
    } else {
        call_openai(&client, &config, &messages, temperature, max_tokens).await
    }
}

// ─── Streaming generate via Tauri Channel ─────────────────────

#[tauri::command]
pub async fn generate_stream(
    state: tauri::State<'_, AppState>,
    req: GenerateRequest,
    channel: Channel<LlmEvent>,
) -> Result<(), String> {
    let config = state.config.lock().map_err(|e| e.to_string())?.clone();

    let client = reqwest::Client::new();
    let messages = llm::build_messages(req.system.as_deref(), &req.prompt);
    let temperature = req.temperature.unwrap_or(config.temperature);
    let max_tokens = req.max_tokens.unwrap_or(config.max_tokens);
    let messages = inject_lore(&state, &client, &config, messages, &req.rag_query).await;

    tauri::async_runtime::spawn(async move {
        // For now, we do a non-streaming call and emit the full response as one token
        // Real SSE streaming from Ollama/OpenAI can be added later
        let result = if llm::is_ollama(&config.api_url) {
            call_ollama(&client, &config, &messages, temperature, max_tokens).await
        } else {
            call_openai(&client, &config, &messages, temperature, max_tokens).await
        };

        match result {
            Ok(text) => {
                let _ = channel.send(LlmEvent::Token(text.clone()));
                let _ = channel.send(LlmEvent::Done(text));
            }
            Err(e) => {
                let _ = channel.send(LlmEvent::Error(e));
            }
        }
    });

    Ok(())
}

// ─── Get / Set LLM Config ────────────────────────────────────

#[tauri::command]
pub fn get_llm_config(state: tauri::State<'_, AppState>) -> Result<crate::llm::LlmConfig, String> {
    let config = state.config.lock().map_err(|e| e.to_string())?;
    Ok(config.clone())
}

#[tauri::command]
pub fn set_llm_config(state: tauri::State<'_, AppState>, config: crate::llm::LlmConfig) -> Result<(), String> {
    let mut lock = state.config.lock().map_err(|e| e.to_string())?;
    *lock = config;
    Ok(())
}

// ─── Connection test: list models from the configured endpoint ──
// ponytail: Ollama exposes GET /api/tags, OpenAI-compatible exposes
// GET /v1/models. Returns the model names so Settings can show a list and
// a green/red pill. Best-effort — surface the error string on failure.
#[derive(serde::Serialize)]
pub struct ConnectionTest {
    pub ok: bool,
    pub models: Vec<String>,
    pub error: String,
}

#[tauri::command]
pub async fn test_connection(state: tauri::State<'_, AppState>) -> Result<ConnectionTest, String> {
    let config = state.config.lock().map_err(|e| e.to_string())?.clone();
    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(8))
        .build()
        .map_err(|e| e.to_string())?;

    if llm::is_ollama(&config.api_url) {
        let url = format!("{}/api/tags", config.api_url.trim_end_matches('/'));
        let res = client.get(&url).send().await.map_err(|e| e.to_string())?;
        if !res.status().is_success() {
            return Ok(ConnectionTest {
                ok: false,
                models: vec![],
                error: format!("HTTP {}", res.status()),
            });
        }
        let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?;
        let models = body["models"]
            .as_array()
            .map(|a| {
                a.iter()
                    .filter_map(|m| m["name"].as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();
        Ok(ConnectionTest { ok: true, models, error: String::new() })
    } else {
        let url = format!("{}/v1/models", config.api_url.trim_end_matches('/'));
        let mut req = client.get(&url);
        if !config.api_key.is_empty() {
            req = req.bearer_auth(&config.api_key);
        }
        let res = req.send().await.map_err(|e| e.to_string())?;
        if !res.status().is_success() {
            return Ok(ConnectionTest {
                ok: false,
                models: vec![],
                error: format!("HTTP {}", res.status()),
            });
        }
        let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?;
        let models = body["data"]
            .as_array()
            .map(|a| {
                a.iter()
                    .filter_map(|m| m["id"].as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default();
        Ok(ConnectionTest { ok: true, models, error: String::new() })
    }
}

// ─── Lore RAG injection (shared by generate + generate_stream) ─

/// Best-effort: if `rag_query` is set, retrieve top lore chunks and fold them
/// into the system message so generation is grounded in the user's world bible.
/// Silently degrades to the original messages on any retrieval error.
async fn inject_lore(
    state: &tauri::State<'_, AppState>,
    _client: &reqwest::Client,
    config: &crate::llm::LlmConfig,
    mut messages: Vec<ChatMessage>,
    rag_query: &Option<String>,
) -> Vec<ChatMessage> {
    let Some(query) = rag_query.as_ref() else { return messages; };
    if query.trim().is_empty() { return messages; }

    let Ok(hits) = state.rag.search(config, query, 4).await else { return messages; };
    if hits.is_empty() { return messages; }

    let lore = hits.iter().map(|(t, _s, _sc)| t.clone()).collect::<Vec<_>>().join("\n\n---\n\n");
    let injection = format!(
        "Relevant lore from the campaign bible — stay consistent with it:\n\n{lore}"
    );
    if let Some(first) = messages.first_mut() {
        if first.role == "system" {
            first.content = format!("{}\n\n{}", first.content, injection);
            return messages;
        }
    }
    messages.insert(0, ChatMessage { role: "system".into(), content: injection });
    messages
}

// ─── Ollama API ───────────────────────────────────────────────

async fn call_ollama(
    client: &reqwest::Client,
    config: &crate::llm::LlmConfig,
    messages: &[ChatMessage],
    temperature: f32,
    max_tokens: u32,
) -> Result<String, String> {
    let body = json!({
        "model": config.model,
        "messages": messages,
        "stream": false,
        "options": {
            "temperature": temperature,
            "num_predict": max_tokens,
        }
    });

    let url = format!("{}/api/chat", config.api_url.trim_end_matches('/'));

    let res = client
        .post(&url)
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("Ollama request failed: {e}"))?;

    if !res.status().is_success() {
        let status = res.status();
        let text = res.text().await.unwrap_or_default();
        return Err(format!("Ollama error {status}: {text}"));
    }

    let response: OllamaChatResponse = res
        .json()
        .await
        .map_err(|e| format!("Ollama parse error: {e}"))?;

    Ok(response.message.content)
}

// ─── OpenAI-compatible API ────────────────────────────────────

async fn call_openai(
    client: &reqwest::Client,
    config: &crate::llm::LlmConfig,
    messages: &[ChatMessage],
    temperature: f32,
    max_tokens: u32,
) -> Result<String, String> {
    let body = json!({
        "model": config.model,
        "messages": messages,
        "temperature": temperature,
        "max_tokens": max_tokens,
    });

    let url = format!("{}/v1/chat/completions", config.api_url.trim_end_matches('/'));

    let mut req = client.post(&url);
    if !config.api_key.is_empty() {
        req = req.bearer_auth(&config.api_key);
    }

    let res = req
        .json(&body)
        .send()
        .await
        .map_err(|e| format!("API request failed: {e}"))?;

    if !res.status().is_success() {
        let status = res.status();
        let text = res.text().await.unwrap_or_default();
        return Err(format!("API error {status}: {text}"));
    }

    let response: ChatResponse = res
        .json()
        .await
        .map_err(|e| format!("API parse error: {e}"))?;

    response
        .choices
        .first()
        .map(|c| c.message.content.clone())
        .ok_or_else(|| "No response from API".to_string())
}