//! OpenAI-Compatible LLM Client (Phase-78: T78.5)
//!
//! Implements the `LlmClient` trait for the OpenAI chat/completions protocol.
//! This single client covers **80%+** of providers:
//!
//! - **OpenAI** (GPT-4o, o1, etc.)
//! - **DeepSeek** (DeepSeek-V3, DeepSeek-R1)
//! - **Ollama** (local, any model)
//! - **Qwen** (Alibaba, via DashScope)
//! - **OpenRouter** (aggregator)
//! - **Azure OpenAI** (with API version)
//! - Any other OpenAI-compatible endpoint
//!
//! # Streaming
//!
//! Uses SSE (Server-Sent Events) via `POST /v1/chat/completions` with `stream: true`.
//! The SSE stream is parsed line-by-line and converted to `StreamEvent` values.
//!
//! # Special handling
//!
//! - **DeepSeek thinking**: Extracts `reasoning_content` field → `ThinkingDelta`
//! - **Vision**: Converts `ContentPart::Image` → OpenAI content array format
//! - **Function calling**: Converts `ToolDefinition` → OpenAI tools format

use std::pin::Pin;
use std::sync::Arc;
use std::time::Instant;

use async_trait::async_trait;
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tracing::{debug, warn, trace};

use super::client::{ContentPart, LlmClient, LlmMessage};
use super::error::{LlmError, LlmResult};
use super::types::{
    ChatConfig, ChatResponse, FinishReason, LlmProvider, ProviderBrand,
    StreamEvent, TokenUsage, ToolCall,
};

// ============================================================================
// OpenAI API Request/Response types
// ============================================================================

#[derive(Debug, Serialize)]
struct OpenAiRequest {
    model: String,
    messages: Vec<OpenAiMessage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    temperature: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    max_tokens: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_p: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    frequency_penalty: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    presence_penalty: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    seed: Option<u64>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    stop: Vec<String>,
    stream: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    tools: Option<Vec<Value>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    response_format: Option<Value>,
    /// Stream options (request usage in stream mode)
    #[serde(skip_serializing_if = "Option::is_none")]
    stream_options: Option<Value>,
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
enum OpenAiMessage {
    Text {
        role: String,
        content: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        name: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        tool_call_id: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        tool_calls: Option<Vec<Value>>,
    },
    Multimodal {
        role: String,
        content: Vec<Value>,
    },
}

#[derive(Debug, Deserialize)]
struct OpenAiResponse {
    #[allow(dead_code)]
    id: Option<String>,
    model: Option<String>,
    choices: Vec<OpenAiChoice>,
    usage: Option<OpenAiUsage>,
}

#[derive(Debug, Deserialize)]
struct OpenAiChoice {
    message: Option<OpenAiResponseMessage>,
    delta: Option<OpenAiDelta>,
    finish_reason: Option<String>,
    #[allow(dead_code)]
    index: Option<usize>,
}

#[derive(Debug, Deserialize)]
struct OpenAiResponseMessage {
    #[allow(dead_code)]
    role: Option<String>,
    content: Option<String>,
    reasoning_content: Option<String>,
    tool_calls: Option<Vec<OpenAiToolCall>>,
}

#[derive(Debug, Deserialize)]
struct OpenAiDelta {
    #[allow(dead_code)]
    role: Option<String>,
    content: Option<String>,
    reasoning_content: Option<String>,
    tool_calls: Option<Vec<OpenAiToolCallDelta>>,
}

#[derive(Debug, Deserialize)]
struct OpenAiToolCall {
    id: String,
    function: OpenAiFunction,
}

#[derive(Debug, Deserialize)]
struct OpenAiToolCallDelta {
    index: Option<usize>,
    id: Option<String>,
    function: Option<OpenAiFunctionDelta>,
}

#[derive(Debug, Deserialize)]
struct OpenAiFunction {
    name: String,
    arguments: String,
}

#[derive(Debug, Deserialize)]
struct OpenAiFunctionDelta {
    name: Option<String>,
    arguments: Option<String>,
}

#[derive(Debug, Deserialize)]
struct OpenAiUsage {
    prompt_tokens: u32,
    completion_tokens: u32,
    total_tokens: u32,
}

#[derive(Debug, Deserialize)]
struct OpenAiErrorResponse {
    error: Option<OpenAiErrorDetail>,
}

#[derive(Debug, Deserialize)]
struct OpenAiErrorDetail {
    message: String,
    #[allow(dead_code)]
    r#type: Option<String>,
    #[allow(dead_code)]
    code: Option<String>,
}

// ============================================================================
// OpenAiCompatClient
// ============================================================================

/// OpenAI-compatible LLM client.
///
/// Handles the OpenAI chat/completions protocol and its variants used by
/// DeepSeek, Ollama, Qwen, OpenRouter, Azure OpenAI, and custom endpoints.
pub struct OpenAiCompatClient {
    provider: LlmProvider,
    http: HttpClient,
}

impl OpenAiCompatClient {
    /// Create a new client for the given provider.
    pub fn new(provider: LlmProvider) -> LlmResult<Self> {
        let mut builder = HttpClient::builder()
            .timeout(std::time::Duration::from_secs(provider.timeout_secs as u64));

        // Add custom headers if any
        if !provider.custom_headers.is_empty() {
            let mut headers = reqwest::header::HeaderMap::new();
            for (k, v) in &provider.custom_headers {
                if let (Ok(name), Ok(val)) = (
                    reqwest::header::HeaderName::from_bytes(k.as_bytes()),
                    reqwest::header::HeaderValue::from_str(v),
                ) {
                    headers.insert(name, val);
                }
            }
            builder = builder.default_headers(headers);
        }

        let http = builder.build().map_err(|e| {
            LlmError::ConfigError(format!("Failed to create HTTP client: {e}"))
        })?;

        Ok(Self { provider, http })
    }

    /// Build the chat completions URL.
    fn chat_url(&self) -> String {
        let base = self.provider.base_url.trim_end_matches('/');

        match self.provider.brand {
            ProviderBrand::AzureOpenAI => {
                // Azure: {base}/openai/deployments/{model}/chat/completions?api-version={version}
                // The model is passed separately, so use a generic endpoint
                let version = self
                    .provider
                    .api_version
                    .as_deref()
                    .unwrap_or("2024-02-15-preview");
                format!("{base}/openai/chat/completions?api-version={version}")
            }
            ProviderBrand::Ollama => {
                format!("{base}/v1/chat/completions")
            }
            _ => {
                // Standard OpenAI-compatible endpoint
                if base.ends_with("/chat/completions") {
                    base.to_string()
                } else if base.ends_with("/v1") {
                    format!("{base}/chat/completions")
                } else {
                    format!("{base}/v1/chat/completions")
                }
            }
        }
    }

    /// Build authorization header.
    fn auth_header(&self) -> Option<String> {
        match &self.provider.brand {
            ProviderBrand::AzureOpenAI => {
                self.provider.api_key.as_ref().map(|k| format!("api-key:{k}"))
            }
            ProviderBrand::Ollama | ProviderBrand::MicroGpt => None,
            _ => self.provider.api_key.as_ref().map(|k| format!("Bearer {k}")),
        }
    }

    /// Convert LlmMessages to OpenAI format.
    fn convert_messages(&self, messages: &[LlmMessage]) -> Vec<OpenAiMessage> {
        messages
            .iter()
            .map(|msg| {
                if msg.is_multimodal() {
                    // Multi-modal: use content array
                    let parts: Vec<Value> = msg
                        .content_parts
                        .iter()
                        .map(|p| match p {
                            ContentPart::Text { text } => {
                                json!({"type": "text", "text": text})
                            }
                            ContentPart::Image { url, .. } => {
                                json!({
                                    "type": "image_url",
                                    "image_url": { "url": url }
                                })
                            }
                            ContentPart::Audio { url, .. } => {
                                json!({
                                    "type": "input_audio",
                                    "input_audio": { "data": url }
                                })
                            }
                        })
                        .collect();
                    OpenAiMessage::Multimodal {
                        role: msg.role.to_string(),
                        content: parts,
                    }
                } else {
                    // Text-only message
                    let tool_calls = if msg.has_tool_calls() {
                        Some(
                            msg.tool_calls
                                .iter()
                                .map(|tc| {
                                    json!({
                                        "id": tc.id,
                                        "type": "function",
                                        "function": {
                                            "name": tc.name,
                                            "arguments": tc.arguments,
                                        }
                                    })
                                })
                                .collect(),
                        )
                    } else {
                        None
                    };

                    OpenAiMessage::Text {
                        role: msg.role.to_string(),
                        content: msg.content.clone(),
                        name: msg.name.clone(),
                        tool_call_id: msg.tool_call_id.clone(),
                        tool_calls,
                    }
                }
            })
            .collect()
    }

    /// Build the request body.
    fn build_request(
        &self,
        model_name: &str,
        messages: &[LlmMessage],
        config: &ChatConfig,
        stream: bool,
    ) -> OpenAiRequest {
        let tools = if config.tools.is_empty() {
            None
        } else {
            Some(
                config
                    .tools
                    .iter()
                    .map(|t| {
                        json!({
                            "type": t.tool_type,
                            "function": {
                                "name": t.function.name,
                                "description": t.function.description,
                                "parameters": t.function.parameters,
                            }
                        })
                    })
                    .collect(),
            )
        };

        let response_format = config
            .response_format
            .as_ref()
            .map(|f| json!({"type": f}));

        let stream_options = if stream {
            Some(json!({"include_usage": true}))
        } else {
            None
        };

        OpenAiRequest {
            model: model_name.to_string(),
            messages: self.convert_messages(messages),
            temperature: config.temperature,
            max_tokens: config.max_tokens,
            top_p: config.top_p,
            frequency_penalty: config.frequency_penalty,
            presence_penalty: config.presence_penalty,
            seed: config.seed,
            stop: config.stop.clone(),
            stream,
            tools,
            response_format,
            stream_options,
        }
    }

    /// Send the HTTP request.
    async fn send_request(
        &self,
        body: &OpenAiRequest,
    ) -> LlmResult<reqwest::Response> {
        let url = self.chat_url();
        debug!(url = %url, model = %body.model, stream = body.stream, "Sending OpenAI request");

        let mut req = self.http.post(&url).json(body);

        // Auth
        if let Some(auth) = self.auth_header() {
            if auth.starts_with("api-key:") {
                req = req.header("api-key", &auth[8..]);
            } else {
                req = req.header("Authorization", &auth);
            }
        }

        // Organization header
        if let Some(org) = &self.provider.organization_id {
            req = req.header("OpenAI-Organization", org);
        }

        let resp = req.send().await.map_err(|e| {
            if e.is_timeout() {
                LlmError::RequestError(format!("Request timeout ({}s): {e}", self.provider.timeout_secs))
            } else if e.is_connect() {
                LlmError::ProviderUnhealthy(format!("Connection failed to {}: {e}", url))
            } else {
                LlmError::RequestError(format!("HTTP error: {e}"))
            }
        })?;

        let status = resp.status();
        if !status.is_success() {
            let body_text = resp.text().await.unwrap_or_default();
            // Try to parse OpenAI error format
            if let Ok(err_resp) = serde_json::from_str::<OpenAiErrorResponse>(&body_text) {
                if let Some(detail) = err_resp.error {
                    return match status.as_u16() {
                        401 => Err(LlmError::AuthError(detail.message)),
                        429 => Err(LlmError::RateLimitExceeded(detail.message)),
                        _ => Err(LlmError::RequestError(format!(
                            "HTTP {status}: {}", detail.message
                        ))),
                    };
                }
            }
            return Err(LlmError::RequestError(format!("HTTP {status}: {body_text}")));
        }

        Ok(resp)
    }

    /// Parse a non-streaming response.
    fn parse_response(&self, resp: OpenAiResponse, duration_ms: u64) -> LlmResult<ChatResponse> {
        let choice = resp
            .choices
            .into_iter()
            .next()
            .ok_or_else(|| LlmError::ResponseParseError("No choices in response".into()))?;

        let msg = choice
            .message
            .ok_or_else(|| LlmError::ResponseParseError("No message in choice".into()))?;

        let tool_calls: Vec<ToolCall> = msg
            .tool_calls
            .unwrap_or_default()
            .into_iter()
            .map(|tc| ToolCall {
                id: tc.id,
                name: tc.function.name,
                arguments: tc.function.arguments,
            })
            .collect();

        let finish_reason = match choice.finish_reason.as_deref() {
            Some("stop") | None => FinishReason::Stop,
            Some("length") => FinishReason::Length,
            Some("tool_calls") | Some("function_call") => FinishReason::ToolCalls,
            Some("content_filter") => FinishReason::ContentFilter,
            Some(other) => FinishReason::Other(other.to_string()),
        };

        let usage = resp.usage.map(|u| TokenUsage {
            prompt_tokens: u.prompt_tokens,
            completion_tokens: u.completion_tokens,
            total_tokens: u.total_tokens,
            ..Default::default()
        });

        Ok(ChatResponse {
            content: msg.content.unwrap_or_default(),
            thinking: msg.reasoning_content,
            tool_calls,
            usage,
            finish_reason,
            model: resp.model.unwrap_or_default(),
            duration_ms: Some(duration_ms),
        })
    }
}

#[async_trait]
impl LlmClient for OpenAiCompatClient {
    async fn chat(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<ChatResponse> {
        // Determine model name — use first message's context or default
        let model_name = &self.provider.id; // Will be overridden by router
        let body = self.build_request(model_name, messages, config, false);
        let start = Instant::now();
        let resp = self.send_request(&body).await?;
        let duration_ms = start.elapsed().as_millis() as u64;

        let resp_body: OpenAiResponse = resp.json().await.map_err(|e| {
            LlmError::ResponseParseError(format!("Failed to parse response: {e}"))
        })?;

        self.parse_response(resp_body, duration_ms)
    }

    async fn chat_stream(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<Pin<Box<dyn futures_core::Stream<Item = LlmResult<StreamEvent>> + Send>>> {
        let model_name = &self.provider.id;
        let body = self.build_request(model_name, messages, config, true);
        let resp = self.send_request(&body).await?;

        // Create an mpsc channel to bridge the SSE parsing to a Stream
        let (tx, rx) = mpsc::channel::<LlmResult<StreamEvent>>(64);

        // Spawn a task to parse the SSE stream
        tokio::spawn(async move {
            use tokio_stream::StreamExt;

            let mut byte_stream = resp.bytes_stream();
            let mut buffer = String::new();

            // Track accumulated tool call data
            let mut tool_call_names: std::collections::HashMap<usize, String> =
                std::collections::HashMap::new();

            while let Some(chunk_result) = byte_stream.next().await {
                let chunk = match chunk_result {
                    Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
                        Ok(s) => s,
                        Err(e) => {
                            let _ = tx
                                .send(Err(LlmError::StreamError(format!(
                                    "Invalid UTF-8: {e}"
                                ))))
                                .await;
                            break;
                        }
                    },
                    Err(e) => {
                        let _ = tx
                            .send(Err(LlmError::StreamError(format!("Stream read error: {e}"))))
                            .await;
                        break;
                    }
                };

                buffer.push_str(&chunk);

                // Process complete SSE events (separated by \n\n)
                while let Some(pos) = buffer.find("\n\n") {
                    let event_text = buffer[..pos].to_string();
                    buffer = buffer[pos + 2..].to_string();

                    // Parse SSE lines
                    for line in event_text.lines() {
                        let line = line.trim();
                        if line.is_empty() || line.starts_with(':') {
                            continue;
                        }

                        if let Some(data) = line.strip_prefix("data: ") {
                            let data = data.trim();
                            if data == "[DONE]" {
                                // Stream complete; Done event was already sent via finish_reason
                                continue;
                            }

                            match serde_json::from_str::<OpenAiResponse>(data) {
                                Ok(resp) => {
                                    for choice in &resp.choices {
                                        if let Some(delta) = &choice.delta {
                                            // Thinking content (DeepSeek R1)
                                            if let Some(thinking) = &delta.reasoning_content {
                                                if !thinking.is_empty() {
                                                    let _ = tx
                                                        .send(Ok(StreamEvent::ThinkingDelta {
                                                            content: thinking.clone(),
                                                        }))
                                                        .await;
                                                }
                                            }

                                            // Text content
                                            if let Some(content) = &delta.content {
                                                if !content.is_empty() {
                                                    let _ = tx
                                                        .send(Ok(StreamEvent::TextDelta {
                                                            content: content.clone(),
                                                        }))
                                                        .await;
                                                }
                                            }

                                            // Tool calls
                                            if let Some(tool_calls) = &delta.tool_calls {
                                                for tc in tool_calls {
                                                    let idx =
                                                        tc.index.unwrap_or(0);
                                                    if let Some(func) = &tc.function {
                                                        // Track name
                                                        if let Some(name) = &func.name {
                                                            tool_call_names
                                                                .insert(idx, name.clone());
                                                        }

                                                        let name = tool_call_names
                                                            .get(&idx)
                                                            .cloned()
                                                            .unwrap_or_default();

                                                        let args = func
                                                            .arguments
                                                            .as_deref()
                                                            .unwrap_or("")
                                                            .to_string();

                                                        if !args.is_empty()
                                                            || tc.id.is_some()
                                                        {
                                                            let _ = tx
                                                                .send(Ok(
                                                                    StreamEvent::ToolCallDelta {
                                                                        id: tc
                                                                            .id
                                                                            .clone()
                                                                            .unwrap_or_default(),
                                                                        name,
                                                                        arguments_delta: args,
                                                                    },
                                                                ))
                                                                .await;
                                                        }
                                                    }
                                                }
                                            }
                                        }

                                        // Finish reason
                                        if let Some(reason_str) = &choice.finish_reason {
                                            let reason = match reason_str.as_str() {
                                                "stop" => FinishReason::Stop,
                                                "length" => FinishReason::Length,
                                                "tool_calls" | "function_call" => {
                                                    FinishReason::ToolCalls
                                                }
                                                "content_filter" => {
                                                    FinishReason::ContentFilter
                                                }
                                                other => {
                                                    FinishReason::Other(other.to_string())
                                                }
                                            };
                                            let _ = tx
                                                .send(Ok(StreamEvent::Done {
                                                    message_id: String::new(),
                                                    finish_reason: reason,
                                                }))
                                                .await;
                                        }
                                    }

                                    // Usage (included in last chunk when stream_options.include_usage = true)
                                    if let Some(usage) = resp.usage {
                                        let _ = tx
                                            .send(Ok(StreamEvent::Usage(TokenUsage {
                                                prompt_tokens: usage.prompt_tokens,
                                                completion_tokens: usage.completion_tokens,
                                                total_tokens: usage.total_tokens,
                                                ..Default::default()
                                            })))
                                            .await;
                                    }
                                }
                                Err(e) => {
                                    trace!(data = data, err = %e, "Failed to parse SSE chunk (non-fatal)");
                                }
                            }
                        }
                    }
                }
            }
        });

        // Convert mpsc::Receiver into a Stream
        let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
        Ok(Box::pin(stream))
    }

    fn provider(&self) -> &LlmProvider {
        &self.provider
    }

    async fn health_check(&self) -> LlmResult<bool> {
        let base = self.provider.base_url.trim_end_matches('/');
        let url = match self.provider.brand {
            ProviderBrand::Ollama => format!("{base}/api/tags"),
            _ => format!("{base}/v1/models"),
        };

        let mut req = self.http.get(&url);
        if let Some(auth) = self.auth_header() {
            if auth.starts_with("api-key:") {
                req = req.header("api-key", &auth[8..]);
            } else {
                req = req.header("Authorization", &auth);
            }
        }

        match req
            .timeout(std::time::Duration::from_secs(10))
            .send()
            .await
        {
            Ok(resp) => {
                let healthy = resp.status().is_success();
                debug!(
                    provider = %self.provider.id,
                    status = %resp.status(),
                    healthy,
                    "Health check"
                );
                Ok(healthy)
            }
            Err(e) => {
                warn!(provider = %self.provider.id, err = %e, "Health check failed");
                Ok(false)
            }
        }
    }
}

impl std::fmt::Debug for OpenAiCompatClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("OpenAiCompatClient")
            .field("provider_id", &self.provider.id)
            .field("brand", &self.provider.brand)
            .field("base_url", &self.provider.base_url)
            .finish()
    }
}

// ============================================================================
// ModelAwareClient — wraps OpenAiCompatClient with model name
// ============================================================================

/// An OpenAI-compatible client bound to a specific model.
///
/// The `ProviderRouter` creates these to route requests to the correct
/// provider + model combination.
pub struct ModelAwareClient {
    inner: Arc<OpenAiCompatClient>,
    model_name: String,
}

impl ModelAwareClient {
    /// Create a model-aware wrapper.
    pub fn new(client: Arc<OpenAiCompatClient>, model_name: String) -> Self {
        Self {
            inner: client,
            model_name,
        }
    }
}

#[async_trait]
impl LlmClient for ModelAwareClient {
    async fn chat(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<ChatResponse> {
        let body = self.inner.build_request(&self.model_name, messages, config, false);
        let start = Instant::now();
        let resp = self.inner.send_request(&body).await?;
        let duration_ms = start.elapsed().as_millis() as u64;

        let resp_body: OpenAiResponse = resp.json().await.map_err(|e| {
            LlmError::ResponseParseError(format!("Failed to parse response: {e}"))
        })?;

        self.inner.parse_response(resp_body, duration_ms)
    }

    async fn chat_stream(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<Pin<Box<dyn futures_core::Stream<Item = LlmResult<StreamEvent>> + Send>>> {
        let body = self.inner.build_request(&self.model_name, messages, config, true);
        let resp = self.inner.send_request(&body).await?;

        let (tx, rx) = mpsc::channel::<LlmResult<StreamEvent>>(64);

        tokio::spawn(async move {
            use tokio_stream::StreamExt;

            let mut byte_stream = resp.bytes_stream();
            let mut buffer = String::new();
            let mut tool_call_names: std::collections::HashMap<usize, String> =
                std::collections::HashMap::new();

            while let Some(chunk_result) = byte_stream.next().await {
                let chunk = match chunk_result {
                    Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
                        Ok(s) => s,
                        Err(e) => {
                            let _ = tx.send(Err(LlmError::StreamError(format!("Invalid UTF-8: {e}")))).await;
                            break;
                        }
                    },
                    Err(e) => {
                        let _ = tx.send(Err(LlmError::StreamError(format!("Stream error: {e}")))).await;
                        break;
                    }
                };

                buffer.push_str(&chunk);

                while let Some(pos) = buffer.find("\n\n") {
                    let event_text = buffer[..pos].to_string();
                    buffer = buffer[pos + 2..].to_string();

                    for line in event_text.lines() {
                        let line = line.trim();
                        if line.is_empty() || line.starts_with(':') {
                            continue;
                        }
                        if let Some(data) = line.strip_prefix("data: ") {
                            let data = data.trim();
                            if data == "[DONE]" {
                                continue;
                            }
                            if let Ok(resp) = serde_json::from_str::<OpenAiResponse>(data) {
                                for choice in &resp.choices {
                                    if let Some(delta) = &choice.delta {
                                        if let Some(thinking) = &delta.reasoning_content {
                                            if !thinking.is_empty() {
                                                let _ = tx.send(Ok(StreamEvent::ThinkingDelta { content: thinking.clone() })).await;
                                            }
                                        }
                                        if let Some(content) = &delta.content {
                                            if !content.is_empty() {
                                                let _ = tx.send(Ok(StreamEvent::TextDelta { content: content.clone() })).await;
                                            }
                                        }
                                        if let Some(tool_calls) = &delta.tool_calls {
                                            for tc in tool_calls {
                                                let idx = tc.index.unwrap_or(0);
                                                if let Some(func) = &tc.function {
                                                    if let Some(name) = &func.name {
                                                        tool_call_names.insert(idx, name.clone());
                                                    }
                                                    let name = tool_call_names.get(&idx).cloned().unwrap_or_default();
                                                    let args = func.arguments.as_deref().unwrap_or("").to_string();
                                                    if !args.is_empty() || tc.id.is_some() {
                                                        let _ = tx.send(Ok(StreamEvent::ToolCallDelta { id: tc.id.clone().unwrap_or_default(), name, arguments_delta: args })).await;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                    if let Some(reason_str) = &choice.finish_reason {
                                        let reason = match reason_str.as_str() {
                                            "stop" => FinishReason::Stop,
                                            "length" => FinishReason::Length,
                                            "tool_calls" | "function_call" => FinishReason::ToolCalls,
                                            "content_filter" => FinishReason::ContentFilter,
                                            other => FinishReason::Other(other.to_string()),
                                        };
                                        let _ = tx.send(Ok(StreamEvent::Done { message_id: String::new(), finish_reason: reason })).await;
                                    }
                                }
                                if let Some(usage) = resp.usage {
                                    let _ = tx.send(Ok(StreamEvent::Usage(TokenUsage { prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens, total_tokens: usage.total_tokens, ..Default::default() }))).await;
                                }
                            }
                        }
                    }
                }
            }
        });

        let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
        Ok(Box::pin(stream))
    }

    fn provider(&self) -> &LlmProvider {
        self.inner.provider()
    }

    async fn health_check(&self) -> LlmResult<bool> {
        self.inner.health_check().await
    }
}

impl std::fmt::Debug for ModelAwareClient {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ModelAwareClient")
            .field("provider_id", &self.inner.provider.id)
            .field("model_name", &self.model_name)
            .finish()
    }
}
