//! Implements [`LlmClient`] for [`motosan_ai::Client`] so that the
//! motosan-ai multi-provider client can drive an [`AgentLoop`] directly.
//!
//! Enabled via the `motosan-ai` feature flag.

use std::pin::Pin;

use futures::Stream;

use crate::llm::{ChatOutput, LlmClient, LlmResponse, StreamChunk, TokenUsage, ToolCallItem};
use crate::message::{Message as LoopMessage, Role as LoopRole};
use crate::AgentError;
use motosan_agent_tool::ToolDef;

use motosan_ai::types::{
    ChatRequest, Message as AiMessage, StreamEventType, ToolCall as AiToolCall,
};
use motosan_ai::Client;

/// Build a [`ChatRequest`] from loop messages and tool definitions.
fn build_chat_request(messages: &[LoopMessage], tools: &[ToolDef]) -> crate::Result<ChatRequest> {
    let ai_messages: Vec<AiMessage> = messages
        .iter()
        .map(loop_message_to_ai_message)
        .collect::<crate::Result<Vec<_>>>()?;

    let mut req = ChatRequest::builder().messages(ai_messages);
    if !tools.is_empty() {
        req = req.tool_defs(tools);
    }
    Ok(req.build())
}

/// Convert a [`motosan_ai::types::ChatResponse`] into an [`LlmResponse`].
fn chat_response_to_llm_response(content: String, tool_calls: Vec<ToolCallItem>) -> LlmResponse {
    if tool_calls.is_empty() {
        LlmResponse::Message(content)
    } else {
        LlmResponse::ToolCalls(tool_calls)
    }
}

#[async_trait::async_trait]
impl LlmClient for Client {
    async fn chat(&self, messages: &[LoopMessage], tools: &[ToolDef]) -> crate::Result<ChatOutput> {
        let req = build_chat_request(messages, tools)?;

        let response = self
            .chat_with(req)
            .await
            .map_err(|e| AgentError::Llm(e.to_string()))?;

        let usage = TokenUsage {
            input_tokens: response.usage.input_tokens as u64,
            output_tokens: response.usage.output_tokens as u64,
        };

        let llm_response = if !response.tool_calls.is_empty() {
            let items: Vec<ToolCallItem> = response
                .tool_calls
                .into_iter()
                .map(|tc| ToolCallItem {
                    id: tc.id,
                    name: tc.name,
                    args: tc.input,
                })
                .collect();
            LlmResponse::ToolCalls(items)
        } else {
            LlmResponse::Message(response.content)
        };

        Ok(ChatOutput::with_usage(llm_response, usage))
    }

    fn chat_stream<'a>(
        &'a self,
        messages: &'a [LoopMessage],
        tools: &'a [ToolDef],
    ) -> Pin<Box<dyn Stream<Item = crate::Result<StreamChunk>> + Send + 'a>> {
        Box::pin(async_stream::stream! {
            let req = build_chat_request(messages, tools)?;

            let mut stream = self
                .stream_with(req)
                .await
                .map_err(|e| AgentError::Llm(e.to_string()))?;

            use futures::StreamExt;

            let mut accumulated_text = String::new();
            let mut tool_calls: Vec<ToolCallItem> = Vec::new();
            let mut current_tc_id = String::new();
            let mut current_tc_name = String::new();
            let mut current_tc_args = String::new();

            while let Some(event) = stream.next().await {
                if event.done {
                    let response = chat_response_to_llm_response(
                        std::mem::take(&mut accumulated_text),
                        std::mem::take(&mut tool_calls),
                    );
                    yield Ok(StreamChunk::Done(response));
                    break;
                }

                match event.event_type {
                    StreamEventType::Text => {
                        if !event.content.is_empty() {
                            accumulated_text.push_str(&event.content);
                            yield Ok(StreamChunk::TextDelta(event.content));
                        }
                    }
                    StreamEventType::ToolCallStart => {
                        current_tc_id = event.tool_call_id.unwrap_or_default();
                        current_tc_name = event.tool_call_name.unwrap_or_default();
                        current_tc_args.clear();
                    }
                    StreamEventType::ToolCallArgs => {
                        if let Some(delta) = event.tool_call_args_delta {
                            current_tc_args.push_str(&delta);
                        }
                    }
                    StreamEventType::ToolCallEnd => {
                        let input: serde_json::Value =
                            serde_json::from_str(&current_tc_args)
                                .unwrap_or_else(|_| serde_json::json!({}));
                        tool_calls.push(ToolCallItem {
                            id: std::mem::take(&mut current_tc_id),
                            name: std::mem::take(&mut current_tc_name),
                            args: input,
                        });
                        current_tc_args.clear();
                    }
                    _ => {}
                }
            }
        })
    }
}

fn loop_message_to_ai_message(msg: &LoopMessage) -> crate::Result<AiMessage> {
    match msg.role {
        LoopRole::User => Ok(AiMessage::user(&msg.content)),
        LoopRole::System => Ok(AiMessage::system(&msg.content)),
        LoopRole::Tool => {
            let tool_call_id = msg
                .tool_call_id
                .as_ref()
                .ok_or_else(|| AgentError::Llm("Tool message missing tool_call_id".into()))?;
            Ok(AiMessage::tool_result(tool_call_id.clone(), &msg.content))
        }
        LoopRole::Assistant => {
            if msg.tool_calls.is_empty() {
                Ok(AiMessage::assistant(&msg.content))
            } else {
                let tcs: Vec<AiToolCall> = msg
                    .tool_calls
                    .iter()
                    .map(|tc| AiToolCall {
                        id: tc.id.clone(),
                        name: tc.name.clone(),
                        input: tc.args.clone(),
                    })
                    .collect();
                Ok(AiMessage::assistant_with_tool_calls(&msg.content, tcs))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::message::ToolCallRef;
    use motosan_ai::types::Role as AiRole;
    use std::sync::Arc;

    #[test]
    fn convert_user_message() {
        let msg = LoopMessage::user("hello");
        let ai = loop_message_to_ai_message(&msg).unwrap();
        assert_eq!(ai.role, AiRole::User);
        assert_eq!(ai.content, "hello");
    }

    #[test]
    fn convert_system_message() {
        let msg = LoopMessage::system("you are helpful");
        let ai = loop_message_to_ai_message(&msg).unwrap();
        assert_eq!(ai.role, AiRole::System);
        assert_eq!(ai.content, "you are helpful");
    }

    #[test]
    fn convert_assistant_message() {
        let msg = LoopMessage::assistant("sure thing");
        let ai = loop_message_to_ai_message(&msg).unwrap();
        assert_eq!(ai.role, AiRole::Assistant);
        assert_eq!(ai.content, "sure thing");
        assert!(ai.tool_calls.is_empty());
    }

    #[test]
    fn convert_assistant_with_tool_calls() {
        let calls = vec![
            ToolCallRef {
                id: "call_1".into(),
                name: "search".into(),
                args: serde_json::json!({"q": "rust"}),
            },
            ToolCallRef {
                id: "call_2".into(),
                name: "fetch".into(),
                args: serde_json::json!({"url": "http://example.com"}),
            },
        ];
        let msg = LoopMessage::assistant_with_tool_calls("let me check", calls);
        let ai = loop_message_to_ai_message(&msg).unwrap();
        assert_eq!(ai.role, AiRole::Assistant);
        assert_eq!(ai.content, "let me check");
        assert_eq!(ai.tool_calls.len(), 2);
        assert_eq!(ai.tool_calls[0].id, "call_1");
        assert_eq!(ai.tool_calls[0].name, "search");
        assert_eq!(ai.tool_calls[0].input, serde_json::json!({"q": "rust"}));
        assert_eq!(ai.tool_calls[1].id, "call_2");
        assert_eq!(ai.tool_calls[1].name, "fetch");
        assert_eq!(
            ai.tool_calls[1].input,
            serde_json::json!({"url": "http://example.com"})
        );
    }

    #[test]
    fn convert_tool_message() {
        let msg = LoopMessage::tool_result("call_abc", "result data");
        let ai = loop_message_to_ai_message(&msg).unwrap();
        assert_eq!(ai.role, AiRole::Tool);
        assert_eq!(ai.content, "result data");
        assert_eq!(ai.tool_call_id.as_deref(), Some("call_abc"));
    }

    #[test]
    fn tool_message_missing_id_returns_error() {
        let msg = LoopMessage {
            role: LoopRole::Tool,
            content: "result".into(),
            tool_call_id: None,
            tool_calls: vec![],
        };
        let result = loop_message_to_ai_message(&msg);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("tool_call_id"));
    }

    /// Compile-time check: `Client` must satisfy `Arc<dyn LlmClient>`.
    #[allow(dead_code)]
    fn assert_client_is_llm_client(_c: Arc<dyn LlmClient>) {}

    #[allow(dead_code)]
    fn assert_client_coerces(client: Client) {
        let _: Arc<dyn LlmClient> = Arc::new(client);
    }

    /// Integration test: streams a real response from an Anthropic-compatible
    /// API.  Requires `ANTHROPIC_API_KEY` to be set.  Run with:
    ///
    /// ```sh
    /// cargo test --features motosan-ai chat_stream_integration -- --ignored
    /// ```
    #[tokio::test]
    #[ignore]
    async fn chat_stream_integration() {
        use crate::llm::StreamChunk;
        use futures::StreamExt;
        use motosan_ai::providers::Provider;

        let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");

        let client = Client::builder()
            .provider(Provider::Anthropic)
            .api_key(api_key)
            .model("claude-haiku-4-5-20251001")
            .build()
            .expect("client build");

        let messages = vec![LoopMessage::user("Say hello in exactly three words.")];
        let tools: Vec<ToolDef> = vec![];

        let mut stream = client.chat_stream(&messages, &tools);
        let mut got_text_delta = false;
        let mut got_done = false;

        while let Some(result) = stream.next().await {
            let chunk = result.expect("stream chunk");
            match chunk {
                StreamChunk::TextDelta(text) => {
                    assert!(!text.is_empty());
                    got_text_delta = true;
                }
                StreamChunk::Done(response) => {
                    match response {
                        LlmResponse::Message(text) => {
                            assert!(!text.is_empty(), "final message should not be empty");
                        }
                        _ => panic!("expected Message response, got tool calls"),
                    }
                    got_done = true;
                }
                StreamChunk::Usage(_) => {}
            }
        }

        assert!(
            got_text_delta,
            "should have received at least one TextDelta"
        );
        assert!(got_done, "should have received Done");
    }
}
