//! Tool Call Execution Loop — Phase-83 T83.7
//!
//! Implements the automatic tool-calling loop:
//!
//! 1. Send messages + tool definitions to LLM via `LlmClient::chat()`
//! 2. If response has `tool_calls` (finish_reason == ToolCalls):
//!    a. Execute each tool via `ToolRegistry`
//!    b. Append assistant message (with tool_calls) + tool result messages
//!    c. Re-send to LLM
//! 3. Repeat until no more tool calls or max rounds reached
//! 4. Return the final text response with accumulated tool call history
//!
//! This module is the "glue" between the LLM Gateway and the ToolRegistry.

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

use tracing::{debug, info, warn};
use serde::{Serialize, Deserialize};

use super::client::{LlmClient, LlmMessage};
use super::types::{ChatConfig, ChatResponse, FinishReason, ToolCall, TokenUsage};
use super::error::{LlmError, LlmResult};
use super::tool_executor::{ToolRegistry, ToolResult};

// ============================================================================
// ToolCallRound — a single round of tool calling
// ============================================================================

/// A record of one round of tool calling.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRound {
    /// The tool calls requested by the LLM in this round.
    pub tool_calls: Vec<ToolCall>,

    /// The results of executing each tool call.
    pub results: Vec<ToolCallResult>,

    /// Round number (1-based).
    pub round: usize,
}

/// Result of a single tool call execution.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallResult {
    /// The tool call ID (matches `ToolCall.id`).
    pub call_id: String,

    /// The tool name.
    pub tool_name: String,

    /// The tool execution result.
    pub result: ToolResult,
}

// ============================================================================
// ToolCallLoopResult — final output of the loop
// ============================================================================

/// The final result of the tool call loop, including all intermediate rounds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallLoopResult {
    /// The final text response from the LLM.
    pub content: String,

    /// Thinking/reasoning content (if enabled).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thinking: Option<String>,

    /// Model that generated the response.
    pub model: String,

    /// Finish reason for the final response.
    pub finish_reason: FinishReason,

    /// All tool call rounds executed.
    pub rounds: Vec<ToolCallRound>,

    /// Total number of tool calls executed across all rounds.
    pub total_tool_calls: usize,

    /// Aggregated token usage across all LLM calls.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub total_usage: Option<TokenUsage>,

    /// Total wall-clock time for the entire loop (ms).
    pub total_duration_ms: u64,
}

impl ToolCallLoopResult {
    /// Whether any tools were called during the loop.
    pub fn had_tool_calls(&self) -> bool {
        self.total_tool_calls > 0
    }

    /// Number of rounds executed.
    pub fn round_count(&self) -> usize {
        self.rounds.len()
    }
}

// ============================================================================
// ToolCallLoop — the main execution engine
// ============================================================================

/// Executes the LLM ↔ Tool Call loop.
///
/// # Usage
///
/// ```ignore
/// let loop_engine = ToolCallLoop::new(client, registry);
/// let result = loop_engine.run(messages, config).await?;
/// println!("Final response: {}", result.content);
/// println!("Tool calls executed: {}", result.total_tool_calls);
/// ```
pub struct ToolCallLoop {
    /// The LLM client to use for chat.
    client: Arc<dyn LlmClient>,

    /// The tool registry for executing tool calls.
    registry: Arc<ToolRegistry>,
}

impl ToolCallLoop {
    /// Create a new tool call loop.
    pub fn new(client: Arc<dyn LlmClient>, registry: Arc<ToolRegistry>) -> Self {
        Self { client, registry }
    }

    /// Run the tool call loop.
    ///
    /// Takes an initial set of messages and a chat config, then runs the
    /// LLM ↔ tool call loop until the LLM produces a final text response
    /// or the maximum number of rounds is reached.
    ///
    /// # Arguments
    /// * `messages` - Initial conversation messages (system + user + history)
    /// * `config` - Chat configuration (model, temperature, etc.)
    ///
    /// # Returns
    /// A `ToolCallLoopResult` with the final response and all tool call history.
    pub async fn run(
        &self,
        messages: Vec<LlmMessage>,
        config: &ChatConfig,
    ) -> LlmResult<ToolCallLoopResult> {
        let start = Instant::now();
        let max_rounds = self.registry.max_tool_rounds();

        // Build config with tool definitions if not already present
        let mut config = config.clone();
        if config.tools.is_empty() {
            config.tools = self.registry.tool_definitions();
        }

        let mut conversation = messages;
        let mut rounds: Vec<ToolCallRound> = Vec::new();
        let mut total_usage: Option<TokenUsage> = None;
        let mut final_response: Option<ChatResponse> = None;

        for round_num in 1..=max_rounds {
            debug!(round = round_num, max = max_rounds, "Tool call loop: sending to LLM");

            // Send to LLM
            let response = self.client.chat(&conversation, &config).await?;

            // Accumulate token usage
            if let Some(usage) = &response.usage {
                total_usage = Some(merge_usage(total_usage.as_ref(), usage));
            }

            // Check if the LLM wants to call tools
            if response.finish_reason == FinishReason::ToolCalls && !response.tool_calls.is_empty()
            {
                info!(
                    round = round_num,
                    tool_count = response.tool_calls.len(),
                    "LLM requested tool calls"
                );

                // Execute all tool calls
                let mut results = Vec::with_capacity(response.tool_calls.len());
                for tc in &response.tool_calls {
                    debug!(tool = %tc.name, call_id = %tc.id, "Executing tool call");

                    let tool_result = self.registry.execute(&tc.name, &tc.arguments).await?;

                    results.push(ToolCallResult {
                        call_id: tc.id.clone(),
                        tool_name: tc.name.clone(),
                        result: tool_result,
                    });
                }

                // Record the round
                rounds.push(ToolCallRound {
                    tool_calls: response.tool_calls.clone(),
                    results: results.clone(),
                    round: round_num,
                });

                // Build messages for the next LLM call:
                // 1. Add the assistant message (with tool_calls)
                let mut assistant_msg = LlmMessage::assistant(&response.content);
                assistant_msg.tool_calls = response.tool_calls.clone();
                conversation.push(assistant_msg);

                // 2. Add tool result messages (one per tool call)
                for result in &results {
                    let content = if result.result.success {
                        result.result.output.clone()
                    } else {
                        format!(
                            "Error: {}",
                            result.result.error.as_deref().unwrap_or("Unknown error")
                        )
                    };
                    conversation.push(LlmMessage::tool(&result.call_id, content));
                }

                // Continue the loop — the LLM will process the tool results
                continue;
            }

            // No tool calls — we have the final response
            final_response = Some(response);
            break;
        }

        // If we exhausted all rounds without a final response, use the last response
        let response = match final_response {
            Some(r) => r,
            None => {
                warn!(
                    max_rounds,
                    "Tool call loop reached max rounds without final response"
                );
                // Make one more call without tools to force a text response
                config.tools.clear();
                self.client.chat(&conversation, &config).await?
            }
        };

        let total_tool_calls = rounds.iter().map(|r| r.tool_calls.len()).sum();
        let total_duration_ms = start.elapsed().as_millis() as u64;

        info!(
            rounds = rounds.len(),
            total_tool_calls,
            total_duration_ms,
            "Tool call loop completed"
        );

        Ok(ToolCallLoopResult {
            content: response.content,
            thinking: response.thinking,
            model: response.model,
            finish_reason: response.finish_reason,
            rounds,
            total_tool_calls,
            total_usage,
            total_duration_ms,
        })
    }
}

/// Merge two `TokenUsage` values by summing their fields.
fn merge_usage(existing: Option<&TokenUsage>, new: &TokenUsage) -> TokenUsage {
    match existing {
        Some(e) => TokenUsage {
            prompt_tokens: e.prompt_tokens + new.prompt_tokens,
            completion_tokens: e.completion_tokens + new.completion_tokens,
            total_tokens: e.total_tokens + new.total_tokens,
            reasoning_tokens: merge_opt_u32(e.reasoning_tokens, new.reasoning_tokens),
            cost: merge_opt_f64(e.cost, new.cost),
            cached_tokens: merge_opt_u32(e.cached_tokens, new.cached_tokens),
        },
        None => new.clone(),
    }
}

fn merge_opt_u32(a: Option<u32>, b: Option<u32>) -> Option<u32> {
    match (a, b) {
        (Some(x), Some(y)) => Some(x + y),
        (Some(x), None) => Some(x),
        (None, Some(y)) => Some(y),
        (None, None) => None,
    }
}

fn merge_opt_f64(a: Option<f64>, b: Option<f64>) -> Option<f64> {
    match (a, b) {
        (Some(x), Some(y)) => Some(x + y),
        (Some(x), None) => Some(x),
        (None, Some(y)) => Some(y),
        (None, None) => None,
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::llm::types::TokenUsage;

    #[test]
    fn test_merge_usage() {
        let a = TokenUsage {
            prompt_tokens: 10,
            completion_tokens: 20,
            total_tokens: 30,
            reasoning_tokens: Some(5),
            cost: Some(0.001),
            cached_tokens: None,
        };
        let b = TokenUsage {
            prompt_tokens: 15,
            completion_tokens: 25,
            total_tokens: 40,
            reasoning_tokens: Some(10),
            cost: Some(0.002),
            cached_tokens: None,
        };
        let merged = merge_usage(Some(&a), &b);
        assert_eq!(merged.prompt_tokens, 25);
        assert_eq!(merged.completion_tokens, 45);
        assert_eq!(merged.total_tokens, 70);
        assert_eq!(merged.reasoning_tokens, Some(15));
        assert_eq!(merged.cost, Some(0.003));
    }

    #[test]
    fn test_merge_usage_none_existing() {
        let b = TokenUsage {
            prompt_tokens: 15,
            completion_tokens: 25,
            total_tokens: 40,
            reasoning_tokens: None,
            cost: None,
            cached_tokens: None,
        };
        let merged = merge_usage(None, &b);
        assert_eq!(merged.prompt_tokens, 15);
        assert_eq!(merged.total_tokens, 40);
    }

    #[test]
    fn test_tool_call_loop_result_helpers() {
        let result = ToolCallLoopResult {
            content: "Final answer".to_string(),
            thinking: None,
            model: "gpt-4".to_string(),
            finish_reason: FinishReason::Stop,
            rounds: vec![],
            total_tool_calls: 0,
            total_usage: None,
            total_duration_ms: 100,
        };
        assert!(!result.had_tool_calls());
        assert_eq!(result.round_count(), 0);

        let result2 = ToolCallLoopResult {
            content: "After tools".to_string(),
            thinking: None,
            model: "gpt-4".to_string(),
            finish_reason: FinishReason::Stop,
            rounds: vec![ToolCallRound {
                tool_calls: vec![ToolCall {
                    id: "c1".to_string(),
                    name: "test".to_string(),
                    arguments: "{}".to_string(),
                }],
                results: vec![],
                round: 1,
            }],
            total_tool_calls: 1,
            total_usage: None,
            total_duration_ms: 200,
        };
        assert!(result2.had_tool_calls());
        assert_eq!(result2.round_count(), 1);
    }
}
