use super::{ProviderResponse, ProviderStreamChunk};
use crate::errors::AppError;
use crate::models::{ContentPart, UnifiedMessage, UnifiedRequest};
use futures::stream::{BoxStream, StreamExt};
use serde_json::Value;

/// Convert messages to OpenAI-compatible JSON, resolving images asynchronously.
///
/// This avoids the deadlock caused by `futures::executor::block_on` inside a
/// Tokio async context. All image base64 conversions are awaited properly.
pub async fn messages_to_openai_json(messages: &[UnifiedMessage]) -> Result<Vec<serde_json::Value>, AppError> {
    let mut result = Vec::new();
    for m in messages {
        let mut parts = Vec::new();
        for p in &m.content {
            match p {
                ContentPart::Text { text } => {
                    parts.push(serde_json::json!({ "type": "text", "text": text }));
                }
                ContentPart::Image(image_input) => {
                    let (base64_data, mime_type) = image_input
                        .to_base64()
                        .await
                        .map_err(|e| AppError::MultimodalError(e.to_string()))?;
                    parts.push(serde_json::json!({
                        "type": "image_url",
                        "image_url": { "url": format!("data:{};base64,{}", mime_type, base64_data) }
                    }));
                }
            }
        }
        result.push(serde_json::json!({
            "role": m.role,
            "content": parts
        }));
    }
    Ok(result)
}

/// Convert messages to OpenAI-compatible JSON, but replace images with a
/// text placeholder "[Image]". Useful for providers that don't support
/// multimodal in streaming mode or at all.
pub async fn messages_to_openai_json_text_only(
    messages: &[UnifiedMessage],
) -> Result<Vec<serde_json::Value>, AppError> {
    let mut result = Vec::new();
    for m in messages {
        let mut parts = Vec::new();
        for p in &m.content {
            match p {
                ContentPart::Text { text } => {
                    parts.push(serde_json::json!({ "type": "text", "text": text }));
                }
                ContentPart::Image(_) => {
                    parts.push(serde_json::json!({ "type": "text", "text": "[Image]" }));
                }
            }
        }
        result.push(serde_json::json!({
            "role": m.role,
            "content": parts
        }));
    }
    Ok(result)
}

/// Build an OpenAI-compatible request body from a UnifiedRequest and pre-converted messages.
pub fn build_openai_body(
    request: &UnifiedRequest,
    messages_json: Vec<serde_json::Value>,
    stream: bool,
) -> serde_json::Value {
    let mut body = serde_json::json!({
        "model": request.model,
        "messages": messages_json,
        "stream": stream,
    });

    if let Some(temp) = request.temperature {
        body["temperature"] = serde_json::json!(temp);
    }
    if let Some(max_tokens) = request.max_tokens {
        body["max_tokens"] = serde_json::json!(max_tokens);
    }

    body
}

/// Parse an OpenAI-compatible chat completion response JSON into a ProviderResponse.
pub fn parse_openai_response(resp_json: &Value, model: String) -> Result<ProviderResponse, AppError> {
    let choice = resp_json["choices"]
        .get(0)
        .ok_or_else(|| AppError::ProviderError("No choices in response".to_string()))?;
    let message = &choice["message"];

    let content = message["content"].as_str().unwrap_or_default().to_string();
    let reasoning_content = message["reasoning_content"].as_str().map(|s| s.to_string());

    let usage = &resp_json["usage"];
    let prompt_tokens = usage["prompt_tokens"].as_u64().unwrap_or(0) as u32;
    let completion_tokens = usage["completion_tokens"].as_u64().unwrap_or(0) as u32;
    let total_tokens = usage["total_tokens"].as_u64().unwrap_or(0) as u32;

    Ok(ProviderResponse {
        content,
        reasoning_content,
        prompt_tokens,
        completion_tokens,
        total_tokens,
        model,
    })
}

/// Create an SSE stream that parses OpenAI-compatible streaming chunks.
///
/// The optional `reasoning_field` allows overriding the field name for
/// reasoning content (e.g., "thought" for Ollama).
pub fn create_openai_stream(
    es: reqwest_eventsource::EventSource,
    model: String,
    reasoning_field: Option<&'static str>,
) -> BoxStream<'static, Result<ProviderStreamChunk, AppError>> {
    use reqwest_eventsource::Event;

    let stream = async_stream::try_stream! {
        let mut es = es;
        while let Some(event) = es.next().await {
            match event {
                Ok(Event::Message(msg)) => {
                    if msg.data == "[DONE]" {
                        break;
                    }

                    let chunk: Value = serde_json::from_str(&msg.data)
                        .map_err(|e| AppError::ProviderError(format!("Failed to parse stream chunk: {}", e)))?;

                    if let Some(choice) = chunk["choices"].get(0) {
                        let delta = &choice["delta"];
                        let content = delta["content"].as_str().unwrap_or_default().to_string();
                        let reasoning_content = delta["reasoning_content"]
                            .as_str()
                            .or_else(|| reasoning_field.and_then(|f| delta[f].as_str()))
                            .map(|s| s.to_string());
                        let finish_reason = choice["finish_reason"].as_str().map(|s| s.to_string());

                        yield ProviderStreamChunk {
                            content,
                            reasoning_content,
                            finish_reason,
                            model: model.clone(),
                        };
                    }
                }
                Ok(_) => continue,
                Err(e) => {
                    Err(AppError::ProviderError(format!("Stream error: {}", e)))?;
                }
            }
        }
    };

    Box::pin(stream)
}

/// Calculate cost using the model registry first, then falling back to provider pricing config.
pub fn calculate_cost_with_registry(
    model: &str,
    prompt_tokens: u32,
    completion_tokens: u32,
    registry: &crate::models::registry::ModelRegistry,
    pricing: &[crate::config::ModelPricing],
    default_prompt_rate: f64,
    default_completion_rate: f64,
) -> f64 {
    if let Some(metadata) = registry.find_model(model)
        && let Some(cost) = &metadata.cost
    {
        return (prompt_tokens as f64 * cost.input / 1_000_000.0)
            + (completion_tokens as f64 * cost.output / 1_000_000.0);
    }

    let (prompt_rate, completion_rate) = pricing
        .iter()
        .find(|p| model.contains(&p.model))
        .map(|p| (p.prompt_tokens_per_million, p.completion_tokens_per_million))
        .unwrap_or((default_prompt_rate, default_completion_rate));

    (prompt_tokens as f64 * prompt_rate / 1_000_000.0) + (completion_tokens as f64 * completion_rate / 1_000_000.0)
}
