use std::pin::Pin;
use std::time::Duration;
use async_openai::config::OpenAIConfig;
use async_openai::Client;
use async_openai::types::chat::{
    ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessage,
    ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestMessage,
    ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, ChatCompletionTool,
    ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse,
    CreateChatCompletionStreamResponse, FinishReason, ReasoningEffort, ToolChoiceOptions,
};
use futures::StreamExt;
use models::projects::project_skill;
use models::rooms::room_ai;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use uuid::Uuid;

use super::context::RoomMessageContext;
use super::{AiRequest, AiStreamChunk, Mention, StreamCallback};
use crate::client::AiClientConfig;
use crate::compact::{CompactConfig, CompactService};
use crate::embed::EmbedService;
use crate::error::{AgentError, Result};
use crate::perception::{PerceptionService, SkillEntry, ToolCallEvent};
use crate::react::{ReactAgent, ReactConfig, DEFAULT_SYSTEM_PROMPT};
use crate::tool::{ToolCall, ToolContext, ToolExecutor, ToolResult, registry::ToolRegistry};

/// Service for handling AI chat requests in rooms.
pub struct ChatService {
    openai_client: Client<OpenAIConfig>,
    ai_client_config: Option<AiClientConfig>,
    compact_service: Option<CompactService>,
    embed_service: Option<EmbedService>,
    perception_service: PerceptionService,
    tool_registry: Option<ToolRegistry>,
}

impl ChatService {
    pub fn new(openai_client: Client<OpenAIConfig>) -> Self {
        Self {
            openai_client,
            ai_client_config: None,
            compact_service: None,
            embed_service: None,
            perception_service: PerceptionService::default(),
            tool_registry: None,
        }
    }

    /// Set the AI client config (required for `process_react`).
    pub fn with_ai_client_config(mut self, config: AiClientConfig) -> Self {
        self.ai_client_config = Some(config);
        self
    }

    pub fn with_compact_service(mut self, compact_service: CompactService) -> Self {
        self.compact_service = Some(compact_service);
        self
    }

    pub fn with_embed_service(mut self, embed_service: EmbedService) -> Self {
        self.embed_service = Some(embed_service);
        self
    }

    pub fn with_perception_service(mut self, perception_service: PerceptionService) -> Self {
        self.perception_service = perception_service;
        self
    }

    pub fn with_tool_registry(mut self, registry: ToolRegistry) -> Self {
        self.tool_registry = Some(registry);
        self
    }

    /// Returns all registered tools as OpenAI tool definitions.
    pub fn tools(&self) -> Vec<ChatCompletionTool> {
        self.tool_registry
            .as_ref()
            .map(|r| r.to_openai_tools())
            .unwrap_or_default()
    }

    #[allow(deprecated)]
    pub async fn process(&self, request: AiRequest) -> Result<String> {
        let tools: Vec<ChatCompletionTool> = request.tools.clone().unwrap_or_default();
        let tools_enabled = !tools.is_empty();
        let tool_choice = tools_enabled.then(|| {
            async_openai::types::chat::ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto)
        });
        let think = request.think;
        let max_tool_depth = request.max_tool_depth;
        let top_p = request.top_p;
        let frequency_penalty = request.frequency_penalty;
        let presence_penalty = request.presence_penalty;
        let temperature_f = request.temperature;
        let max_tokens_i = request.max_tokens;

        let mut messages = self.build_messages(&request).await?;

        let room_ai = room_ai::Entity::find()
            .filter(room_ai::Column::Room.eq(request.room.id))
            .filter(room_ai::Column::Model.eq(request.model.id))
            .one(&request.db)
            .await?;

        let model_name = request.model.name.clone();
        let temperature = room_ai
            .as_ref()
            .and_then(|r| r.temperature.map(|v| v as f32))
            .unwrap_or(temperature_f as f32);
        let max_tokens = room_ai
            .as_ref()
            .and_then(|r| r.max_tokens.map(|v| v as u32))
            .unwrap_or(max_tokens_i as u32);
        let mut tool_depth = 0;

        loop {
            let req = CreateChatCompletionRequest {
                model: model_name.clone(),
                messages: messages.clone(),
                temperature: Some(temperature),
                max_completion_tokens: Some(max_tokens),
                top_p: Some(top_p as f32),
                frequency_penalty: Some(frequency_penalty as f32),
                presence_penalty: Some(presence_penalty as f32),
                stream: Some(false),
                reasoning_effort: if think { Some(ReasoningEffort::High) } else { None },
                tools: if tools_enabled {
                    Some(
                        tools
                            .iter()
                            .map(|t| ChatCompletionTools::Function(t.clone()))
                            .collect(),
                    )
                } else {
                    None
                },
                tool_choice: tool_choice.clone(),
                ..Default::default()
            };

            let response: CreateChatCompletionResponse = self
                .openai_client
                .chat()
                .create(req)
                .await
                .map_err(|e| AgentError::OpenAi(e.to_string()))?;

            let choice = response
                .choices
                .into_iter()
                .next()
                .ok_or_else(|| AgentError::Internal("no choice in response".into()))?;

            if tools_enabled {
                if let Some(ref tool_calls) = choice.message.tool_calls {
                    if !tool_calls.is_empty() {
                        messages.push(ChatCompletionRequestMessage::Assistant(
                            ChatCompletionRequestAssistantMessage {
                                content: choice
                                    .message
                                    .content
                                    .clone()
                                    .map(ChatCompletionRequestAssistantMessageContent::Text),
                                name: None,
                                refusal: None,
                                audio: None,
                                tool_calls: Some(tool_calls.clone()),
                                function_call: None,
                            },
                        ));

                        let calls: Vec<ToolCall> = tool_calls
                            .iter()
                            .filter_map(|tc| {
                                if let ChatCompletionMessageToolCalls::Function(
                                    async_openai::types::chat::ChatCompletionMessageToolCall {
                                        id,
                                        function,
                                    },
                                ) = tc
                                {
                                    Some(ToolCall {
                                        id: id.clone(),
                                        name: function.name.clone(),
                                        arguments: function.arguments.clone(),
                                    })
                                } else {
                                    None
                                }
                            })
                            .collect();

                        if !calls.is_empty() {
                            let tool_messages = self.execute_tool_calls(calls, &request).await?;
                            messages.extend(tool_messages);

                            tool_depth += 1;
                            if tool_depth >= max_tool_depth {
                                return Ok(String::new());
                            }
                            continue;
                        }
                    }
                }
            }

            let text = choice.message.content.unwrap_or_default();
            return Ok(text);
        }
    }

    #[allow(deprecated)]
    pub async fn process_stream(&self, request: AiRequest, on_chunk: StreamCallback) -> Result<()> {
        let tools: Vec<ChatCompletionTool> = request.tools.clone().unwrap_or_default();
        let tools_enabled = !tools.is_empty();
        let tool_choice = tools_enabled.then(|| {
            async_openai::types::chat::ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto)
        });
        let think = request.think;
        let max_tool_depth = request.max_tool_depth;
        let top_p = request.top_p;
        let frequency_penalty = request.frequency_penalty;
        let presence_penalty = request.presence_penalty;
        let temperature_f = request.temperature;
        let max_tokens_i = request.max_tokens;

        let mut messages = self.build_messages(&request).await?;

        let room_ai = room_ai::Entity::find()
            .filter(room_ai::Column::Room.eq(request.room.id))
            .filter(room_ai::Column::Model.eq(request.model.id))
            .one(&request.db)
            .await?;

        let model_name = request.model.name.clone();
        let temperature = room_ai
            .as_ref()
            .and_then(|r| r.temperature.map(|v| v as f32))
            .unwrap_or(temperature_f as f32);
        let max_tokens = room_ai
            .as_ref()
            .and_then(|r| r.max_tokens.map(|v| v as u32))
            .unwrap_or(max_tokens_i as u32);
        let mut tool_depth = 0;

        loop {
            let req = CreateChatCompletionRequest {
                model: model_name.clone(),
                messages: messages.clone(),
                temperature: Some(temperature),
                max_completion_tokens: Some(max_tokens),
                top_p: Some(top_p as f32),
                frequency_penalty: Some(frequency_penalty as f32),
                presence_penalty: Some(presence_penalty as f32),
                stream: Some(true),
                reasoning_effort: if think { Some(ReasoningEffort::High) } else { None },
                tools: if tools_enabled {
                    Some(
                        tools
                            .iter()
                            .map(|t| ChatCompletionTools::Function(t.clone()))
                            .collect(),
                    )
                } else {
                    None
                },
                tool_choice: tool_choice.clone(),
                ..Default::default()
            };

            let mut stream = self
                .openai_client
                .chat()
                .create_stream(req)
                .await
                .map_err(|e| AgentError::OpenAi(e.to_string()))?;

            let mut text_accumulated = String::new();
            let mut tool_call_chunks: Vec<ToolCallChunkAccum> = Vec::new();
            let mut finish_reason: Option<FinishReason> = None;

            while let Some(chunk_result) = stream.next().await {
                let chunk: CreateChatCompletionStreamResponse =
                    chunk_result.map_err(|e| AgentError::OpenAi(e.to_string()))?;

                let choice = match chunk.choices.first() {
                    Some(c) => c,
                    None => continue,
                };

                // Track finish reason
                if let Some(ref fr) = choice.finish_reason {
                    finish_reason = Some(fr.clone());
                }

                // Text delta
                if let Some(content) = &choice.delta.content {
                    text_accumulated.push_str(content);
                    on_chunk(AiStreamChunk {
                        content: text_accumulated.clone(),
                        done: false,
                    })
                    .await;
                }

                // Tool call deltas
                if let Some(ref tool_chunks) = choice.delta.tool_calls {
                    for tc in tool_chunks {
                        let idx = tc.index as usize;
                        if tool_call_chunks.len() <= idx {
                            tool_call_chunks.resize(idx + 1, ToolCallChunkAccum::default());
                        }
                        if let Some(ref id) = tc.id {
                            tool_call_chunks[idx].id = Some(id.clone());
                        }
                        if let Some(ref fc) = tc.function {
                            if let Some(ref name) = fc.name {
                                tool_call_chunks[idx].name.push_str(name);
                            }
                            if let Some(ref args) = fc.arguments {
                                tool_call_chunks[idx].arguments.push_str(args);
                            }
                        }
                    }
                }
            }

            let has_tool_calls = matches!(
                finish_reason,
                Some(FinishReason::ToolCalls) | Some(FinishReason::FunctionCall)
            );

            if has_tool_calls && tools_enabled {
                // Build ToolCall list from accumulated chunks
                let tool_calls: Vec<_> = tool_call_chunks
                    .into_iter()
                    .filter(|c| !c.name.is_empty())
                    .map(|c| ToolCall {
                        id: c.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
                        name: c.name,
                        arguments: c.arguments,
                    })
                    .collect();

                if !tool_calls.is_empty() {
                    // Capture thinking text, send it as a completed chunk, then clear for the next turn
                    let thinking_text = text_accumulated.clone();
                    if !thinking_text.is_empty() {
                        on_chunk(AiStreamChunk {
                            content: thinking_text.clone(),
                            done: true,
                        })
                        .await;
                    }
                    text_accumulated.clear();

                    // Append assistant message with tool calls to history
                    messages.push(ChatCompletionRequestMessage::Assistant(
                        ChatCompletionRequestAssistantMessage {
                            content: Some(
                                ChatCompletionRequestAssistantMessageContent::Text(
                                    thinking_text,
                                ),
                            ),
                            name: None,
                            refusal: None,
                            audio: None,
                            tool_calls: Some(
                                tool_calls
                                    .iter()
                                    .map(|tc| {
                                        ChatCompletionMessageToolCalls::Function(
                                            async_openai::types::chat::ChatCompletionMessageToolCall {
                                                id: tc.id.clone(),
                                                function: async_openai::types::chat::FunctionCall {
                                                    name: tc.name.clone(),
                                                    arguments: tc.arguments.clone(),
                                                },
                                            },
                                        )
                                    })
                                    .collect(),
                            ),
                            function_call: None,
                        },
                    ));

                    let tool_messages = self.execute_tool_calls(tool_calls, &request).await?;
                    messages.extend(tool_messages);

                    tool_depth += 1;
                    if tool_depth >= max_tool_depth {
                        return Ok(());
                    }
                    continue;
                }
            }

            on_chunk(AiStreamChunk {
                content: text_accumulated,
                done: true,
            })
            .await;
            return Ok(());
        }
    }

    /// Executes a batch of tool calls and returns the tool result messages.
    async fn execute_tool_calls(
        &self,
        calls: Vec<ToolCall>,
        request: &AiRequest,
    ) -> Result<Vec<ChatCompletionRequestMessage>> {
        let mut ctx = ToolContext::new(
            request.db.clone(),
            request.cache.clone(),
            request.config.clone(),
            request.room.id,
            Some(request.sender.uid),
        )
        .with_project(request.project.id);

        // Add pre-configured tool registry if available
        if let Some(ref registry) = self.tool_registry {
            ctx.registry_mut().merge(registry.clone());
        }

        let executor = ToolExecutor::new();

        let results = executor
            .execute_batch(calls, &mut ctx)
            .await
            .map_err(|e| AgentError::Internal(e.to_string()))?;

        Ok(ToolExecutor::to_tool_messages(&results))
    }

    async fn build_messages(
        &self,
        request: &AiRequest,
    ) -> Result<Vec<ChatCompletionRequestMessage>> {
        let mut messages = Vec::new();

        let mut processed_history = Vec::new();
        if let Some(compact_service) = &self.compact_service {
            // Auto-compact: only compresses when token count exceeds threshold
            let config = CompactConfig::default();
            match compact_service
                .compact_room_auto(request.room.id, Some(request.user_names.clone()), config)
                .await
            {
                Ok(compact_summary) => {
                    if !compact_summary.summary.is_empty() {
                        messages.push(ChatCompletionRequestMessage::System(
                            ChatCompletionRequestSystemMessage {
                                content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
                                    format!("Conversation summary:\n{}", compact_summary.summary),
                                ),
                                ..Default::default()
                            },
                        ));
                    }
                    processed_history = compact_summary.retained;
                }
                Err(e) => {
                    let _ = e;
                }
            }
        }

        if !processed_history.is_empty() {
            for msg_summary in processed_history {
                let ctx = RoomMessageContext::from(msg_summary);
                messages.push(ctx.to_message());
            }
        } else {
            for msg in &request.history {
                let ctx = RoomMessageContext::from_model_with_names(msg, &request.user_names);
                messages.push(ctx.to_message());
            }
        }

        if let Some(embed_service) = &self.embed_service {
            for mention in &request.mention {
                match mention {
                    Mention::Repo(repo) => {
                        let query = format!(
                            "{} {}",
                            repo.repo_name,
                            repo.description.as_deref().unwrap_or_default()
                        );
                        match embed_service.search_issues(&query, 5).await {
                            Ok(issues) if !issues.is_empty() => {
                                let context = format!(
                                    "Related issues:\n{}",
                                    issues
                                        .iter()
                                        .map(|i| format!("- {}", i.payload.text))
                                        .collect::<Vec<_>>()
                                        .join("\n")
                                );
                                messages.push(ChatCompletionRequestMessage::System(
                                    ChatCompletionRequestSystemMessage {
                                        content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
                                            context,
                                        ),
                                        ..Default::default()
                                    },
                                ));
                            }
                            Err(e) => {
                                let _ = e;
                            }
                            _ => {}
                        }
                        match embed_service.search_repos(&query, 3).await {
                            Ok(repos) if !repos.is_empty() => {
                                let context = format!(
                                    "Related repositories:\n{}",
                                    repos
                                        .iter()
                                        .map(|r| format!("- {}", r.payload.text))
                                        .collect::<Vec<_>>()
                                        .join("\n")
                                );
                                messages.push(ChatCompletionRequestMessage::System(
                                    ChatCompletionRequestSystemMessage {
                                        content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
                                            context,
                                        ),
                                        ..Default::default()
                                    },
                                ));
                            }
                            Err(e) => {
                                let _ = e;
                            }
                            _ => {}
                        }
                    }
                    Mention::User(user) => {
                        let mut profile_parts = vec![format!("Username: {}", user.username)];
                        if let Some(ref display_name) = user.display_name {
                            profile_parts.push(format!("Display name: {}", display_name));
                        }
                        if let Some(ref org) = user.organization {
                            profile_parts.push(format!("Organization: {}", org));
                        }
                        if let Some(ref website) = user.website_url {
                            profile_parts.push(format!("Website: {}", website));
                        }
                        messages.push(ChatCompletionRequestMessage::System(
                            ChatCompletionRequestSystemMessage {
                                content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
                                    format!("Mentioned user profile:\n{}", profile_parts.join("\n")),
                                ),
                                ..Default::default()
                            },
                        ));
                    }
                }
            }
        }

        // Inject relevant skills via the perception system (auto + active + passive).
        let skill_contexts = self.build_skill_context(request).await;
        for ctx in skill_contexts {
            messages.push(ctx.to_system_message() as ChatCompletionRequestMessage);
        }

        // Inject relevant past conversation memories via vector similarity.
        let memories = self.build_memory_context(request).await;
        for mem in memories {
            messages.push(mem.to_system_message());
        }

        // Inject project context so the AI knows which project it is operating in.
        let project_info = format!(
            "Current Project:\n{}\nDescription: {}\nPublic: {}",
            request.project.display_name,
            request.project.description.as_deref().unwrap_or("(none)"),
            if request.project.is_public { "yes" } else { "no" }
        );
        messages.push(ChatCompletionRequestMessage::System(
            ChatCompletionRequestSystemMessage {
                content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(project_info),
                ..Default::default()
            },
        ));

        // Inject sender info so the AI knows who is asking.
        let mut sender_parts = vec![format!("**Sender:** {}", request.sender.username)];
        if let Some(ref display_name) = request.sender.display_name {
            sender_parts.push(display_name.clone());
        }
        if let Some(ref org) = request.sender.organization {
            sender_parts.push(format!("({})", org));
        }
        let sender_display = sender_parts.join(" ");
        messages.push(ChatCompletionRequestMessage::System(
            ChatCompletionRequestSystemMessage {
                content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
                    format!("The person sending the next message:\n{}", sender_display),
                ),
                ..Default::default()
            },
        ));

        messages.push(ChatCompletionRequestMessage::User(
            ChatCompletionRequestUserMessage {
                content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
                    request.input.clone(),
                ),
                ..Default::default()
            },
        ));

        Ok(messages)
    }

    /// Fetch enabled skills for the current project and run them through the
    /// perception system (auto + active + passive) to inject relevant context.
    async fn build_skill_context(
        &self,
        request: &AiRequest,
    ) -> Vec<crate::perception::SkillContext> {
        // Fetch enabled skills for this project.
        let skills: Vec<SkillEntry> = match project_skill::Entity::find()
            .filter(project_skill::Column::ProjectUuid.eq(request.project.id))
            .filter(project_skill::Column::Enabled.eq(true))
            .all(&request.db)
            .await
        {
            Ok(models) => models
                .into_iter()
                .map(|s| SkillEntry {
                    slug: s.slug,
                    name: s.name,
                    description: s.description,
                    content: s.content,
                })
                .collect(),
            Err(_) => return Vec::new(),
        };

        if skills.is_empty() {
            return Vec::new();
        }

        // Build history text for auto-awareness scoring.
        let history_texts: Vec<String> = request
            .history
            .iter()
            .rev()
            .take(10)
            .map(|msg| msg.content.clone())
            .collect();

        // Active + passive + auto perception (keyword-based).
        let tool_events: Vec<ToolCallEvent> = Vec::new(); // Tool calls tracked in loop via process()
        let keyword_skills = self
            .perception_service
            .inject_skills(&request.input, &history_texts, &tool_events, &skills)
            .await;

        // Vector-aware active perception: semantic search for skills via Qdrant.
        let mut vector_skills = Vec::new();
        if let Some(embed_service) = &self.embed_service {
            let awareness = crate::perception::VectorActiveAwareness::default();
            vector_skills = awareness
                .detect(embed_service, &request.input, &request.project.id.to_string())
                .await;
        }

        // Merge: deduplicate by label, preferring vector results (higher signal).
        let mut seen = std::collections::HashSet::new();
        let mut result = Vec::new();
        for ctx in vector_skills {
            if seen.insert(ctx.label.clone()) {
                result.push(ctx);
            }
        }
        for ctx in keyword_skills {
            if seen.insert(ctx.label.clone()) {
                result.push(ctx);
            }
        }

        result
    }

    /// Inject relevant past conversation memories via vector similarity search.
    async fn build_memory_context(
        &self,
        request: &AiRequest,
    ) -> Vec<crate::perception::vector::MemoryContext> {
        let embed_service = match &self.embed_service {
            Some(s) => s,
            None => return Vec::new(),
        };

        // Search memories by current input semantic similarity.
        let awareness = crate::perception::VectorPassiveAwareness::default();
        awareness
            .detect(embed_service, &request.input, &request.room.id.to_string())
            .await
    }

    /// Returns true if the error message indicates a transient failure that can be retried.
    fn is_retryable_tool_error(msg: &str) -> bool {
        let msg_lower = msg.to_lowercase();
        // Transient errors: network, timeouts, rate limits, permission issues that may be temporary
        msg_lower.contains("connection")
            || msg_lower.contains("timeout")
            || msg_lower.contains("timed out")
            || msg_lower.contains("rate limit")
            || msg_lower.contains("too many")
            || msg_lower.contains("unavailable")
            || msg_lower.contains("service unavailable")
            || msg_lower.contains("temporarily")
            || msg_lower.contains("refused")
            || msg_lower.contains("reset")
            || msg_lower.contains("broken pipe")
            || msg_lower.contains("deadline exceeded")
            || msg_lower.contains("try again")
            || msg_lower.contains("not found") // DB/Redis transient not-found
            || msg_lower.contains("permission denied")
            || msg_lower.contains("access denied")
    }

    /// Process a request using the ReAct (Reasoning + Acting) agent.
    ///
    /// Unlike the simple loop in `process`, the ReAct agent performs multi-step
    /// tool-augmented reasoning: it thinks, calls tools, observes results, and
    /// iterates until it has enough information to answer.
    ///
    /// The `on_chunk` callback receives each step event (Thought, Action,
    /// Observation, Answer).
    ///
    /// Requires `with_ai_client_config` to have been called. Returns an error if
    /// the agent failed or exceeded `max_steps`.
    pub async fn process_react<C>(
        &self,
        request: &AiRequest,
        mut on_chunk: C,
    ) -> Result<String>
    where
        C: FnMut(crate::react::ReactStep) + Send,
    {
        let client_config = self.ai_client_config.as_ref().ok_or_else(|| {
            AgentError::Internal("AiClientConfig not set — call with_ai_client_config first".into())
        })?;
        let Some(registry) = &self.tool_registry else {
            return Err(AgentError::Internal("no tool registry registered".into()));
        };

        // Build a ToolExecutor that wraps the request-scoped registry.
        let db = request.db.clone();
        let cache = request.cache.clone();
        let config = request.config.clone();
        let room_id = request.room.id;
        let project_id = Some(request.project.id);
        let sender_uid = Some(request.sender.uid);
        let registry = registry.clone();

        let executor: std::sync::Arc<
            dyn Fn(String, serde_json::Value) -> Pin<Box<dyn Future<Output = std::result::Result<serde_json::Value, String>> + Send>>
                + Send
                + Sync,
        > = std::sync::Arc::new(move |name: String, args: serde_json::Value| {
            let db = db.clone();
            let cache = cache.clone();
            let config = config.clone();
            let room_id = room_id;
            let project_id = project_id;
            let sender_uid = sender_uid;
            let registry = registry.clone();

            Box::pin(async move {
                let max_retries = 3;
                let mut last_err = String::new();

                for attempt in 0..=max_retries {
                    let mut ctx = ToolContext::new(db.clone(), cache.clone(), config.clone(), room_id, sender_uid);
                    if let Some(pid) = project_id {
                        ctx = ctx.with_project(pid);
                    }
                    ctx.registry_mut().merge(registry.clone());

                    let tool_executor = ToolExecutor::new();
                    let call = ToolCall {
                        id: Uuid::new_v4().to_string(),
                        name: name.clone(),
                        arguments: serde_json::to_string(&args).unwrap_or_else(|_| "{}".into()),
                    };

                    match tool_executor.execute_batch(vec![call], &mut ctx).await {
                        Ok(results) => {
                            let result = results.into_iter().next()
                                .ok_or_else(|| "no tool result returned".to_string())?;
                            match result.result {
                                ToolResult::Ok(v) => return Ok(v),
                                ToolResult::Error(msg) => {
                                    // Check if error is retryable
                                    if attempt < max_retries && Self::is_retryable_tool_error(&msg) {
                                        last_err = msg;
                                        let backoff_ms = 100u64.saturating_mul(2u64.pow(attempt as u32));
                                        tracing::warn!(
                                            tool = %name,
                                            attempt = attempt + 1,
                                            backoff_ms = backoff_ms,
                                            error = %last_err,
                                            "tool_execute_retry"
                                        );
                                        tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
                                        continue;
                                    }
                                    // Non-retryable or exhausted retries — pass error to AI as observation
                                    return Err(msg);
                                }
                            }
                        }
                        Err(e) => {
                            last_err = e.to_string();
                            if attempt < max_retries && Self::is_retryable_tool_error(&last_err) {
                                let backoff_ms = 100u64.saturating_mul(2u64.pow(attempt as u32));
                                tracing::warn!(
                                    tool = %name,
                                    attempt = attempt + 1,
                                    backoff_ms = backoff_ms,
                                    error = %last_err,
                                    "tool_execute_retry"
                                );
                                tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
                                continue;
                            }
                            return Err(last_err);
                        }
                    }
                }

                // Should not reach here, but just in case
                Err(last_err)
            }) as Pin<Box<dyn std::future::Future<Output = std::result::Result<serde_json::Value, String>> + Send>>
        });

        let tools = self.tools();
        let config = ReactConfig {
            max_steps: 20,
            stop_sequences: Vec::new(),
            tool_executor: Some(executor),
        };

        let mut agent = ReactAgent::new(DEFAULT_SYSTEM_PROMPT, tools, config);
        agent.add_user_message(&request.input);

        agent.run(&request.model.name, client_config, |step| {
            on_chunk(step);
        })
        .await
    }
}
#[derive(Clone, Debug, Default)]
struct ToolCallChunkAccum {
    id: Option<String>,
    name: String,
    arguments: String,
}
