//! LLM Gateway — Unified LLM Client Trait (Phase-78: T78.4)
//!
//! Defines the `LlmClient` async trait — the unified abstraction layer for
//! interacting with any LLM provider (OpenAI, Gemini, Anthropic, local, etc.).
//!
//! # Architecture
//!
//! ```text
//! ┌──────────────────────────────────────────────────────┐
//! │          async trait LlmClient                       │
//! │  ├─ chat()          → ChatResponse                   │
//! │  ├─ chat_stream()   → Stream<StreamEvent>            │
//! │  └─ health_check()  → bool                           │
//! ├──────────────────────────────────────────────────────┤
//! │ OpenAiCompatClient │ GeminiClient │ AnthropicClient  │
//! └──────────────────────────────────────────────────────┘
//! ```
//!
//! # Message Types
//!
//! `LlmMessage` is the input message type used with `LlmClient`. It extends
//! the RAG module's simple `ChatMessage` with multi-modal content, tool calls,
//! and tool results.

use std::pin::Pin;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use super::error::LlmResult;
use super::types::{
    ChatConfig, ChatResponse, LlmProvider, StreamEvent, ToolCall,
};

// ============================================================================
// Message Role (extended for LLM Gateway)
// ============================================================================

/// Role of a message participant.
///
/// Extends the RAG module's `ChatRole` with `Tool` for function calling.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
    /// System instructions / prompt.
    System,
    /// User query (human).
    User,
    /// Assistant (AI model) response.
    Assistant,
    /// Tool/function result (sent back to model after function calling).
    Tool,
}

impl std::fmt::Display for MessageRole {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::System => write!(f, "system"),
            Self::User => write!(f, "user"),
            Self::Assistant => write!(f, "assistant"),
            Self::Tool => write!(f, "tool"),
        }
    }
}

// ============================================================================
// Content Part (multi-modal)
// ============================================================================

/// A content part within a message (supports multi-modal).
///
/// When a message is text-only, use a single `ContentPart::Text`.
/// For multi-modal messages (image + text, audio + text), use multiple parts.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
    /// Plain text content.
    Text {
        text: String,
    },
    /// Image content (URL or base64).
    Image {
        /// Image URL or base64-encoded data URI.
        url: String,
        /// MIME type (e.g., "image/png", "image/jpeg").
        #[serde(default, skip_serializing_if = "Option::is_none")]
        mime_type: Option<String>,
    },
    /// Audio content (URL or base64).
    Audio {
        /// Audio URL or base64-encoded data URI.
        url: String,
        /// Audio format (e.g., "wav", "mp3").
        #[serde(default, skip_serializing_if = "Option::is_none")]
        format: Option<String>,
    },
}

// ============================================================================
// LlmMessage (input message for LlmClient)
// ============================================================================

/// A message in a conversation (input to `LlmClient`).
///
/// Supports text, multi-modal content, tool calls, and tool results.
///
/// # Usage patterns
///
/// ```
/// use aifastdb_core::llm::client::{LlmMessage, MessageRole};
///
/// // Simple text message
/// let msg = LlmMessage::user("What is Rust?");
///
/// // System prompt
/// let sys = LlmMessage::system("You are a helpful assistant.");
///
/// // Tool result
/// let tool = LlmMessage::tool("call_123", r#"{"result": "42"}"#);
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmMessage {
    /// Message role.
    pub role: MessageRole,

    /// Text content (primary). May be empty for tool-call-only assistant messages.
    #[serde(default)]
    pub content: String,

    /// Multi-modal content parts (alternative to plain `content`).
    ///
    /// When non-empty, the provider client should prefer this over `content`.
    /// For text-only messages, this is empty and `content` is used.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub content_parts: Vec<ContentPart>,

    /// Tool calls requested by the assistant (only for role == Assistant).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_calls: Vec<ToolCall>,

    /// Tool call ID this message is responding to (only for role == Tool).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_call_id: Option<String>,

    /// Optional name (for multi-participant conversations).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

impl LlmMessage {
    /// Create a system message.
    pub fn system(content: impl Into<String>) -> Self {
        Self {
            role: MessageRole::System,
            content: content.into(),
            content_parts: Vec::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            name: None,
        }
    }

    /// Create a user message.
    pub fn user(content: impl Into<String>) -> Self {
        Self {
            role: MessageRole::User,
            content: content.into(),
            content_parts: Vec::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            name: None,
        }
    }

    /// Create an assistant message.
    pub fn assistant(content: impl Into<String>) -> Self {
        Self {
            role: MessageRole::Assistant,
            content: content.into(),
            content_parts: Vec::new(),
            tool_calls: Vec::new(),
            tool_call_id: None,
            name: None,
        }
    }

    /// Create a tool result message.
    pub fn tool(call_id: impl Into<String>, result: impl Into<String>) -> Self {
        Self {
            role: MessageRole::Tool,
            content: result.into(),
            content_parts: Vec::new(),
            tool_calls: Vec::new(),
            tool_call_id: Some(call_id.into()),
            name: None,
        }
    }

    /// Create a user message with multi-modal content.
    pub fn user_multimodal(parts: Vec<ContentPart>) -> Self {
        Self {
            role: MessageRole::User,
            content: String::new(),
            content_parts: parts,
            tool_calls: Vec::new(),
            tool_call_id: None,
            name: None,
        }
    }

    /// Whether this message has multi-modal content.
    pub fn is_multimodal(&self) -> bool {
        !self.content_parts.is_empty()
    }

    /// Whether this message has tool calls.
    pub fn has_tool_calls(&self) -> bool {
        !self.tool_calls.is_empty()
    }

    /// Get the text content, falling back to first text part if multi-modal.
    pub fn text_content(&self) -> &str {
        if !self.content.is_empty() {
            return &self.content;
        }
        for part in &self.content_parts {
            if let ContentPart::Text { text } = part {
                return text.as_str();
            }
        }
        ""
    }
}

// ============================================================================
// LlmClient Trait
// ============================================================================

/// Unified LLM Client trait — the core abstraction for all LLM providers.
///
/// Implementations handle protocol-specific details (request/response format,
/// streaming SSE parsing, error handling) while exposing a uniform interface.
///
/// # Implementations
///
/// - `OpenAiCompatClient`: OpenAI-compatible protocol (covers 80% of providers)
/// - `GeminiClient`: Google Gemini native protocol
/// - `AnthropicClient`: Anthropic Messages API
///
/// # Streaming
///
/// `chat_stream()` returns a `Pin<Box<dyn Stream<...>>>` that yields
/// `StreamEvent` items. The caller can consume these events one by one
/// to build a progressive response (SSE, gRPC stream, etc.).
///
/// # Error handling
///
/// All methods return `LlmResult<T>`, with `LlmError` covering:
/// - Network errors (timeout, connection refused)
/// - Auth errors (invalid API key)
/// - Rate limiting
/// - Parsing errors (unexpected response format)
/// - Provider-specific errors
#[async_trait]
pub trait LlmClient: Send + Sync {
    /// Non-streaming chat completion.
    ///
    /// Sends all messages to the provider and waits for the full response.
    async fn chat(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<ChatResponse>;

    /// Streaming chat completion.
    ///
    /// Returns a stream of `StreamEvent` items that the caller can
    /// consume progressively. The stream ends with a `StreamEvent::Done`
    /// or `StreamEvent::Error`.
    async fn chat_stream(
        &self,
        messages: &[LlmMessage],
        config: &ChatConfig,
    ) -> LlmResult<Pin<Box<dyn futures_core::Stream<Item = LlmResult<StreamEvent>> + Send>>>;

    /// Get the provider this client is connected to.
    fn provider(&self) -> &LlmProvider;

    /// Health check — verify the provider is reachable.
    ///
    /// Implementations should make a lightweight request (e.g., list models)
    /// and return `true` if the provider responds within timeout.
    async fn health_check(&self) -> LlmResult<bool>;

    /// Get the provider ID for this client.
    fn provider_id(&self) -> &str {
        &self.provider().id
    }

    /// Get the provider brand for this client.
    fn provider_brand(&self) -> &super::types::ProviderBrand {
        &self.provider().brand
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_message_constructors() {
        let sys = LlmMessage::system("You are helpful.");
        assert_eq!(sys.role, MessageRole::System);
        assert_eq!(sys.content, "You are helpful.");

        let user = LlmMessage::user("Hello!");
        assert_eq!(user.role, MessageRole::User);
        assert!(!user.is_multimodal());

        let asst = LlmMessage::assistant("Hi there!");
        assert_eq!(asst.role, MessageRole::Assistant);

        let tool = LlmMessage::tool("call_1", r#"{"answer": 42}"#);
        assert_eq!(tool.role, MessageRole::Tool);
        assert_eq!(tool.tool_call_id.as_deref(), Some("call_1"));
    }

    #[test]
    fn test_multimodal_message() {
        let parts = vec![
            ContentPart::Text {
                text: "Describe this image".into(),
            },
            ContentPart::Image {
                url: "https://example.com/cat.jpg".into(),
                mime_type: Some("image/jpeg".into()),
            },
        ];
        let msg = LlmMessage::user_multimodal(parts);
        assert!(msg.is_multimodal());
        assert_eq!(msg.text_content(), "Describe this image");
    }

    #[test]
    fn test_message_serde() {
        let msg = LlmMessage::user("test");
        let json = serde_json::to_string(&msg).unwrap();
        let msg2: LlmMessage = serde_json::from_str(&json).unwrap();
        assert_eq!(msg2.role, MessageRole::User);
        assert_eq!(msg2.content, "test");
    }

    #[test]
    fn test_role_display() {
        assert_eq!(MessageRole::System.to_string(), "system");
        assert_eq!(MessageRole::User.to_string(), "user");
        assert_eq!(MessageRole::Assistant.to_string(), "assistant");
        assert_eq!(MessageRole::Tool.to_string(), "tool");
    }
}
