"""
Tool Executor System

ToolExecutor with ContextComponent output and ScaffoldManager integration.
Executes tools and scaffolds, produces ContextComponents for context tree.
Enhanced with ToolExecutionHooks support for lifecycle management.
"""

import asyncio
import threading
import time
from pydantic import BaseModel
from typing import Optional, Dict, Any, List, Union
from datetime import datetime
import logging
import difflib

from egregore.core.tool_calling.tool_registry import ToolRegistry
from egregore.core.tool_calling.context_components import ToolResult, ScaffoldResult
from egregore.core.messaging import ProviderToolCall
from egregore.core.context_management.pact.components.core import PACTCore as ContextComponent
from egregore.core.agent.hooks.execution import ToolExecutionHooks, HookType
from egregore.core.agent.hooks.execution_contexts import ContextFactory, BaseExecContext, OperationExecContext

logger = logging.getLogger(__name__)


class ToolExecutor(BaseModel):
    """V2 tool executor that produces ContextComponents for context tree"""

    model_config = {"arbitrary_types_allowed": True}

    registry: ToolRegistry
    context: Optional[Any] = None  # Context for registry population - avoid circular import
    context_history: Optional[Any] = None  # ContextHistory - avoid circular import

    # Hook system integration
    hooks: Optional[ToolExecutionHooks] = None

    # Concurrency control
    _execution_semaphore: Optional[threading.Semaphore] = None
    _max_concurrent_executions: int = 5

    # Agent context for hooks
    _agent_id: Optional[str] = None
    _execution_id: Optional[str] = None

    # NEW: Agent reference for enhanced hook context
    agent: Optional[Any] = None
    
    def __init__(self, **data):
        """Initialize ToolExecutor with optional hooks and concurrency control."""
        super().__init__(**data)
        
        # Initialize semaphore for concurrency control
        if self._execution_semaphore is None:
            self._execution_semaphore = threading.Semaphore(self._max_concurrent_executions)
    
    # Hook System Configuration
    
    def set_hooks(self, hooks: ToolExecutionHooks) -> None:
        """Set the hook system for tool execution lifecycle management."""
        self.hooks = hooks
        logger.debug("ToolExecutionHooks configured for ToolExecutor")
    
    def set_agent_context(self, agent_id: str, execution_id: Optional[str] = None) -> None:
        """Set agent context for hook execution."""
        self._agent_id = agent_id
        self._execution_id = execution_id
        logger.debug(f"Agent context set: {agent_id}, execution: {execution_id}")
    
    def set_concurrency_limit(self, max_concurrent: int) -> None:
        """Set maximum concurrent tool executions."""
        self._max_concurrent_executions = max_concurrent
        self._execution_semaphore = threading.Semaphore(max_concurrent)
        logger.debug(f"Concurrency limit set to {max_concurrent}")

    # Unified Hook Firing (Canonical Hooks Only)

    async def _fire_unified_tool_start_hooks(self, context) -> None:
        """
        Fire tool start hook.

        Args:
            context: Tool execution context
        """
        if self.hooks:
            await self.hooks.execute_hooks_async(HookType.TOOL_START, context)

    async def _fire_unified_tool_done_hooks(self, context) -> None:
        """
        Fire tool done hook.

        Args:
            context: Tool execution context
        """
        if self.hooks:
            await self.hooks.execute_hooks_async(HookType.TOOL_DONE, context)

    async def _fire_unified_tool_error_hooks(self, context) -> None:
        """
        Fire tool error hook.

        Args:
            context: Tool execution context
        """
        if self.hooks:
            await self.hooks.execute_hooks_async(HookType.TOOL_ERROR, context)

    # Enhanced Tool Execution with Hooks
    
    def execute_tools_with_hooks(
        self,
        tool_calls: List[ProviderToolCall],
        agent_id: Optional[str] = None,
        execution_id: Optional[str] = None,
        cycle_id: Optional[str] = None,
        snapshot_id: Optional[str] = None,
        provider_name: Optional[str] = None
    ) -> List[ContextComponent]:
        """
        Execute multiple tools with full hook lifecycle management AND history tracking.

        This is the unified tool execution method - combines hooks + tracking.

        Args:
            tool_calls: List of tool calls to execute
            agent_id: Agent identifier for hook context
            execution_id: Execution identifier for hook context
            cycle_id: Optional message cycle identifier for history tracking
            snapshot_id: Optional snapshot ID that triggered execution (for history)
            provider_name: Optional provider name for history tracking

        Returns:
            List of tool execution results (ToolResult or ScaffoldResult)

        Note:
            - If cycle_id is provided and context_history is available, execution will be tracked
            - Tracking is opt-in via cycle_id parameter (backward compatible)
        """
        # Use provided context or fall back to instance context
        effective_agent_id = agent_id or self._agent_id or "unknown"
        effective_execution_id = execution_id or self._execution_id
        
        # Create hook context for the overall execution
        execution_context = BaseExecContext(
            agent_id=effective_agent_id,
            execution_id=effective_execution_id,
            agent=self.agent,
            metadata={
                "tool_count": len(tool_calls),
                "tool_names": [call.tool_name for call in tool_calls]
            }
        )
        
        # Execute before tool execution hooks
        if self.hooks:
            self.hooks.execute_hooks(HookType.BEFORE_TOOL_EXECUTION, execution_context)
        
        results = []
        execution_start_time = datetime.now()
        
        try:
            # Execute each tool with individual hooks
            for tool_call in tool_calls:
                try:
                    result = self._execute_single_tool_with_hooks(
                        tool_call, effective_agent_id, effective_execution_id
                    )
                    results.append(result)
                except Exception as e:
                    logger.error(f"Tool execution failed for {tool_call.tool_name}: {e}")
                    # Create error result
                    error_result = ToolResult(
                        tool_call_id=tool_call.tool_call_id,
                        tool_name=tool_call.tool_name,
                        content=f"Execution failed: {str(e)}",
                        success=False,
                        execution_time=datetime.now(),
                        error_message=str(e)
                    )
                    results.append(error_result)
            
            # Update context with results
            md = execution_context.metadata or {}
            md.update({
                "execution_duration": (datetime.now() - execution_start_time).total_seconds(),
                "success_count": sum(1 for r in results if getattr(r, 'success', False)),
                "failure_count": sum(1 for r in results if not getattr(r, 'success', True))
            })
            execution_context.metadata = md
            
            # Execute after tool execution hooks
            if self.hooks:
                self.hooks.execute_hooks(HookType.AFTER_TOOL_EXECUTION, execution_context)

            # Track execution if history is available and cycle_id provided
            if self.context_history and cycle_id and results:
                # Extract ToolResult instances for tracking (filter out ScaffoldResult)
                tool_results = [r for r in results if isinstance(r, ToolResult)]

                if tool_results:
                    self._track_tool_execution(
                        tool_calls=tool_calls,
                        results=tool_results,
                        snapshot_id=snapshot_id or f"snapshot_{cycle_id}",
                        cycle_id=cycle_id,
                        provider_name=provider_name
                    )

            return results
            
        except Exception as e:
            # Execute error hooks
            execution_context.error = e
            if self.hooks:
                self.hooks.execute_hooks(HookType.TOOL_ERROR, execution_context)
            raise
    
    def _execute_single_tool_with_hooks(
        self,
        tool_call: ProviderToolCall,
        agent_id: str,
        execution_id: Optional[str]
    ) -> ContextComponent:
        """Execute a single tool with hooks and concurrency control."""
        
        # Acquire semaphore for concurrency control
        semaphore = self._execution_semaphore
        if semaphore is None:
            raise RuntimeError("Execution semaphore not initialized")
        with semaphore:
            # Get tool declaration to determine type
            tool_decl = self.registry.get_tool(tool_call.tool_name)
            from egregore.core.tool_calling.tool_declaration import ScaffoldOpDeclaration
            is_scaffold = isinstance(tool_decl, ScaffoldOpDeclaration)
            
            # Create operation context for individual tool call (handles both tools and scaffolds)
            call_context = ContextFactory.create_operation_context(
                agent_id=agent_id,
                tool_declaration=tool_decl,
                tool_params=tool_call.parameters,
                agent=self.agent,
                execution_id=execution_id,
                metadata={
                    'tool_call_id': getattr(tool_call, 'tool_call_id', None),
                    'execution_start': time.time()
                }
            )
            
            # Apply injection patterns if hooks are available
            if self.hooks:
                modified_params = self.hooks.apply_injection_patterns(
                    tool_call.tool_name,
                    tool_call.parameters,
                    call_context
                )
                # Create modified tool call if parameters were injected
                if modified_params != tool_call.parameters:
                    tool_call = ProviderToolCall(
                        tool_call_id=tool_call.tool_call_id,
                        tool_name=tool_call.tool_name,
                        parameters=modified_params
                    )
                    call_context.tool_params = modified_params
            
            # Execute before tool call hooks
            if self.hooks:
                self.hooks.execute_hooks(HookType.TOOL_START, call_context)
            
            # Execute call intercept hooks (PRE-EXECUTION PHASE)
            if self.hooks:
                self.hooks.execute_hooks(HookType.CALL_INTERCEPT, call_context)

                # Check if any hook rejected the call
                if call_context.validation_rejected:
                    return self._create_rejection_response(
                        tool_call,
                        call_context.rejection_reason or "Tool execution blocked",
                        call_context.rejection_message
                    )
            
            try:
                # Execute the actual tool (already format-aware in execute_tool)
                result = self.execute_tool(tool_call)
                
                # Update context with result for POST-EXECUTION PHASE
                call_context.tool_result = result
                
                # Execute call intercept hooks again (POST-EXECUTION PHASE)
                if self.hooks:
                    self.hooks.execute_hooks(HookType.CALL_INTERCEPT, call_context)
                
                # Execute after tool call hooks
                if self.hooks:
                    self.hooks.execute_hooks(HookType.TOOL_DONE, call_context)
                
                # Return potentially modified result
                return call_context.tool_result
                
            except Exception as e:
                # Execute error hooks for individual tool call
                call_context.error = e
                if self.hooks:
                    self.hooks.execute_hooks(HookType.TOOL_ERROR, call_context)
                raise
    
    def _create_rejection_response(self, tool_call: ProviderToolCall, reason: str, message: Optional[str] = None) -> ToolResult:
        """Create standardized rejection response for blocked tool calls.

        Args:
            tool_call: The tool call that was rejected
            reason: Short technical reason for rejection
            message: Full formatted message for LLM (from PolicyResult.message), if available

        Returns:
            ToolResult with rejection information
        """
        from egregore.core.tool_calling.context_components import ToolResult
        from datetime import datetime

        # Use full policy message if available, otherwise use short reason
        content = message if message else f"Tool execution blocked: {reason}"

        return ToolResult(
            tool_call_id=tool_call.tool_call_id,
            tool_name=tool_call.tool_name,
            content=content,
            success=False,
            execution_time=datetime.now(),
            error_message=f"Rejected: {reason}",
            metadata={"rejected": True, "reason": reason}
        )

    async def _execute_single_tool_with_hooks_async(
        self,
        tool_call: ProviderToolCall,
        agent_id: str,
        execution_id: Optional[str]
    ) -> ContextComponent:
        """Async version of single tool execution with hooks."""
        
        # Get tool declaration to determine type (async version)
        tool_decl = self.registry.get_tool(tool_call.tool_name)
        from egregore.core.tool_calling.tool_declaration import ScaffoldOpDeclaration
        is_scaffold = isinstance(tool_decl, ScaffoldOpDeclaration)
        
        # Create operation context for individual tool call (handles both tools and scaffolds)
        call_context = ContextFactory.create_operation_context(
            agent_id=agent_id,
            tool_declaration=tool_decl,
            tool_params=tool_call.parameters,
            agent=self.agent,
            execution_id=execution_id,
            metadata={
                'tool_call_id': getattr(tool_call, 'tool_call_id', None),
                'execution_start': time.time()
            }
        )
        
        # Apply injection patterns if hooks are available
        if self.hooks:
            modified_params = self.hooks.apply_injection_patterns(
                tool_call.tool_name,
                tool_call.parameters,
                call_context
            )
            # Create modified tool call if parameters were injected
            if modified_params != tool_call.parameters:
                tool_call = ProviderToolCall(
                    tool_call_id=tool_call.tool_call_id,
                    tool_name=tool_call.tool_name,
                    parameters=modified_params
                )
                call_context.tool_params = modified_params
        
        # Fire unified tool start hooks (async)
        await self._fire_unified_tool_start_hooks(call_context)

        # Execute call intercept hooks (PRE-EXECUTION PHASE) - async
        if self.hooks:
            await self.hooks.execute_hooks_async(HookType.CALL_INTERCEPT, call_context)

            # Check if any hook rejected the call
            if call_context.validation_rejected:
                return self._create_rejection_response(
                    tool_call,
                    call_context.rejection_reason or "Tool execution blocked",
                    call_context.rejection_message
                )

        try:
            # Execute the actual tool (sync - tools are typically sync)
            result = self.execute_tool(tool_call)

            # Update context with result for POST-EXECUTION PHASE
            call_context.tool_result = result

            # Execute call intercept hooks again (POST-EXECUTION PHASE) - async
            if self.hooks:
                await self.hooks.execute_hooks_async(HookType.CALL_INTERCEPT, call_context)

            # Fire unified tool done hooks (async)
            await self._fire_unified_tool_done_hooks(call_context)

            # Return potentially modified result
            return call_context.tool_result

        except Exception as e:
            # Fire unified tool error hooks (async)
            call_context.error = e
            await self._fire_unified_tool_error_hooks(call_context)
            raise

    def execute_tool(self, tool_call: Union[ProviderToolCall, "ToolCall"]) -> ContextComponent:
        """Execute tool and return ContextComponent for context tree (Phase 3: Union type support)

        Args:
            tool_call: Either ProviderToolCall (legacy) or ToolCall component (Phase 3)

        Returns:
            Either ToolResult or ScaffoldResult
        """
        # Phase 3: Extract component ID if available (for registry population)
        from .context_components import ToolCall

        call_component_id = None
        if isinstance(tool_call, ToolCall):
            # New Phase 3 flow: ToolCall component from context
            call_component_id = tool_call.id
            tool_call_id = tool_call.tool_call_id
            tool_name = tool_call.tool_name

            # Convert ToolCall → ProviderToolCall for internal execution (backward compat)
            provider_tool_call = ProviderToolCall(
                tool_name=tool_call.tool_name,
                tool_call_id=tool_call.tool_call_id,
                parameters=tool_call.parameters
            )
        else:
            # Legacy flow: ProviderToolCall from response parsing
            provider_tool_call = tool_call
            tool_call_id = tool_call.tool_call_id
            tool_name = tool_call.tool_name

        # Get tool declaration from registry
        tool_decl = self.registry.get_tool(tool_name)
        if not tool_decl:
            return self._create_tool_not_found_error(provider_tool_call)

        # Use type-based detection instead of pattern matching
        from egregore.core.tool_calling.tool_declaration import ScaffoldOpDeclaration

        # Execute tool with ToolCall component for TTL synchronization
        # Pass tool_call component if available (Phase 3) for TTL sync
        tool_call_component_for_sync = tool_call if isinstance(tool_call, ToolCall) else None

        if isinstance(tool_decl, ScaffoldOpDeclaration):
            # Route to scaffold execution - tool already has scaffold metadata
            logger.debug(f"Executing ScaffoldOpDeclaration: {tool_name}")
            result = tool_decl.execute(provider_tool_call, tool_call_component=tool_call_component_for_sync)
        else:
            # Route to regular tool execution
            logger.debug(f"Executing regular ToolDeclaration: {tool_name}")
            result = tool_decl.execute(provider_tool_call, tool_call_component=tool_call_component_for_sync)

        # Note: Tool pair registration primarily handled automatically via CONTEXT_AFTER_CHANGE hook
        # in Agent._register_tool_registration_hook() when components are inserted into context.
        # However, for Phase 3 flow where execute_tool is called directly with ToolCall component,
        # we need to complete the pair registration here since the result may not be inserted immediately.
        if call_component_id and hasattr(result, 'id'):
            # Phase 3: Complete the tool pair registration
            self.context._registry.complete_tool_pair(tool_call_id, result.id)

        return result

    def _create_tool_not_found_error(self, tool_call: ProviderToolCall) -> ToolResult:
        """Create error ToolResult for unknown tool with helpful suggestions"""
        
        # Get available tools for suggestions
        available_tools = self.registry.get_all_tools()
        available_names = [tool.name for tool in available_tools]
        
        # Check if this might be a scaffold tool format issue
        scaffold_suggestions = self._generate_scaffold_suggestions(tool_call.tool_name, available_names)
        
        # Generate helpful error message
        if scaffold_suggestions:
            content = f"Tool '{tool_call.tool_name}' not found. {scaffold_suggestions}"
            error_message = f"Tool '{tool_call.tool_name}' not registered. {scaffold_suggestions}"
        else:
            # Find similar tool names
            similar_tools = self._find_similar_tools(tool_call.tool_name, available_names)
            if similar_tools:
                suggestions = f"Did you mean: {', '.join(similar_tools[:3])}?"
                content = f"Tool '{tool_call.tool_name}' not found. {suggestions}"
                error_message = f"Tool '{tool_call.tool_name}' not registered. {suggestions}"
            else:
                if available_names:
                    available_list = ', '.join(available_names[:5])
                    if len(available_names) > 5:
                        available_list += f" (and {len(available_names) - 5} more)"
                    content = f"Tool '{tool_call.tool_name}' not found. Available tools: {available_list}"
                    error_message = f"Tool '{tool_call.tool_name}' not registered. Available: {available_list}"
                else:
                    content = f"Tool '{tool_call.tool_name}' not found. No tools are currently registered."
                    error_message = f"Tool '{tool_call.tool_name}' not registered. No tools available."
        
        result = ToolResult(
            tool_call_id=tool_call.tool_call_id,
            tool_name=tool_call.tool_name,
            content=content,
            success=False,
            execution_time=datetime.now(),
            error_message=error_message
        )
        return result
    
    def _generate_scaffold_suggestions(self, tool_name: str, available_tools: List[str]) -> str:
        """Generate suggestions for scaffold tool format issues"""
        
        # Check if this looks like a scaffold tool pattern
        if '_' in tool_name:
            # Looks like distinct format: scaffoldname_operation
            parts = tool_name.split('_', 1)
            if len(parts) == 2:
                scaffold_name, operation = parts
                
                # Look for unified format alternative
                if scaffold_name in available_tools:
                    return f"Did you mean '{scaffold_name}' with action parameter? Try: '{scaffold_name}' with action='{operation}'"
                
                # Look for similar scaffold names
                scaffold_matches = [t for t in available_tools if t.startswith(scaffold_name)]
                if scaffold_matches:
                    return f"Found similar scaffold tools: {', '.join(scaffold_matches[:3])}"
        else:
            # Might be unified format but scaffold not available
            # Look for distinct format alternatives
            distinct_matches = [t for t in available_tools if t.startswith(f"{tool_name}_")]
            if distinct_matches:
                return f"Found distinct format tools: {', '.join(distinct_matches[:3])}"
        
        return ""
    
    def _find_similar_tools(self, target: str, available_tools: List[str]) -> List[str]:
        """Find similar tool names using difflib"""
        # Use difflib to get close matches with cutoff of 0.6
        close_matches = difflib.get_close_matches(target, available_tools, n=3, cutoff=0.6)
        return close_matches
    
    def _create_scaffold_not_found_error(self, tool_call: ProviderToolCall) -> ScaffoldResult:
        """Create error ScaffoldResult for unknown scaffold."""
        # Since this method is only called when we have a ScaffoldOpDeclaration but execution failed,
        # we can extract scaffold info from the tool name or just use 'unknown'
        scaffold_name = tool_call.tool_name  # Use tool name as fallback
        
        # Try to provide helpful suggestions
        available_scaffolds = []
        if self.agent and hasattr(self.agent, '_agent_config') and self.agent._agent_config.scaffolds:
            available_scaffolds = [getattr(s, 'type', 'unknown') for s in self.agent._agent_config.scaffolds]
        
        if available_scaffolds:
            content = f"Scaffold '{scaffold_name}' not found. Available scaffolds: {', '.join(available_scaffolds)}"
        else:
            content = f"Scaffold '{scaffold_name}' not found. No scaffolds are configured for this agent."
        
        return ScaffoldResult(
            tool_call_id=tool_call.tool_call_id,
            tool_name=tool_call.tool_name,
            scaffold_id="not_found",
            scaffold_type="not_found",
            operation_name=tool_call.tool_name,
            content=content,
            success=False,
            execution_time=datetime.now(),
            error_message=f"Scaffold '{scaffold_name}' not found"
        )
    
    def _validate_parameters(self, tool, params: Dict[str, Any], tool_call_id: str) -> Optional[ToolResult]:
        """Validate tool parameters and return error ToolResult if invalid"""
        try:
            # Check if tool has parameter schema
            if not tool.parameters:
                return None  # No validation needed if no parameters defined
            
            # Check required parameters are present
            required_params = getattr(tool.parameters, 'required', [])
            if required_params:
                missing_params = [param for param in required_params if param not in params]
                if missing_params:
                    result = ToolResult(
                        tool_call_id=tool_call_id,
                        tool_name=tool.name,
                        content=f"Missing required parameters: {', '.join(missing_params)}",
                        success=False,
                        execution_time=datetime.now(),
                        error_message=f"Required parameters missing: {missing_params}"
                    )
                    return result
            
            # Basic type validation for provided parameters
            properties = getattr(tool.parameters, 'properties', {})
            if properties:
                for param_name, param_value in params.items():
                    if param_name in properties:
                        param_schema = properties[param_name]
                        validation_error = self._validate_parameter_type(param_name, param_value, param_schema)
                        if validation_error:
                            result = ToolResult(
                                tool_call_id=tool_call_id,
                                tool_name=tool.name,
                                content=f"Parameter validation failed for '{param_name}': {validation_error}",
                                success=False,
                                execution_time=datetime.now(),
                                error_message=validation_error
                            )
                            return result
            
            return None  # Validation passed
            
        except Exception as e:
            # If validation itself fails, return error
            result = ToolResult(
                tool_call_id=tool_call_id,
                tool_name=tool.name,
                content=f"Parameter validation error: {str(e)}",
                success=False,
                execution_time=datetime.now(),
                error_message=f"Validation system error: {str(e)}"
            )
            return result
    
    def _validate_parameter_type(self, param_name: str, param_value: Any, param_schema) -> Optional[str]:
        """Validate a single parameter type and return error message if invalid"""
        try:
            from egregore.core.tool_calling.schema import SchemaType
            
            schema_type = getattr(param_schema, 'type', None)
            
            if schema_type == SchemaType.STRING and not isinstance(param_value, str):
                return f"Expected string, got {type(param_value).__name__}"
            elif schema_type == SchemaType.INTEGER and not isinstance(param_value, int):
                return f"Expected integer, got {type(param_value).__name__}"
            elif schema_type == SchemaType.NUMBER and not isinstance(param_value, (int, float)):
                return f"Expected number, got {type(param_value).__name__}"
            elif schema_type == SchemaType.BOOLEAN and not isinstance(param_value, bool):
                return f"Expected boolean, got {type(param_value).__name__}"
            elif schema_type == SchemaType.ARRAY and not isinstance(param_value, list):
                return f"Expected array, got {type(param_value).__name__}"
            elif schema_type == SchemaType.OBJECT and not isinstance(param_value, dict):
                return f"Expected object, got {type(param_value).__name__}"
            
            return None  # Type validation passed
            
        except Exception as e:
            return f"Type validation error: {str(e)}"
    
    # ContextHistory integration methods
    
    def set_context_history(self, history) -> None:
        """Set the context history for tracking tool executions
        
        Args:
            history: ContextHistory instance to integrate with
        """
        self.context_history = history
    
    def _track_tool_execution(
        self,
        tool_calls: List[Union[ProviderToolCall, Any]],  # Accept both ProviderToolCall and ToolCall components
        results: List[ToolResult],
        snapshot_id: str,
        cycle_id: str,
        provider_name: Optional[str] = None
    ) -> None:
        """Track tool execution by creating and adding ToolExecutionGroup

        Args:
            tool_calls: List of tool calls made (ProviderToolCall or ToolCall components)
            results: List of execution results
            snapshot_id: ID of snapshot that triggered execution
            cycle_id: Message cycle identifier
            provider_name: Name of provider that made the calls
        """
        from .tool_execution_group import ToolExecutionGroup, ToolExecution
        from .context_components import ToolCall as ToolCallComponent

        group_id = f"group_{cycle_id}_{int(datetime.now().timestamp())}"

        group = ToolExecutionGroup(
            id=group_id,
            cycle_id=cycle_id,
            snapshot_id=snapshot_id,
            provider_name=provider_name,
            context_size_before=None,  # Could be set by caller
            context_size_after=None,   # Could be set by caller
            completed_at=None,
            total_duration_ms=None,
        )

        # Pair tool calls with results
        for i, tool_call in enumerate(tool_calls):
            result = results[i] if i < len(results) else None

            # Convert ToolCall component to ProviderToolCall for tracking if needed
            if isinstance(tool_call, ToolCallComponent):
                provider_tool_call = ProviderToolCall(
                    tool_name=tool_call.tool_name,
                    tool_call_id=tool_call.tool_call_id,
                    parameters=tool_call.parameters
                )
            else:
                provider_tool_call = tool_call

            execution = ToolExecution(
                tool_call=provider_tool_call,
                result=result,
                scaffold_result=None,
                tool_name=provider_tool_call.tool_name,
                tool_call_id=provider_tool_call.tool_call_id,
                execution_time=(result.execution_time or datetime.now()) if result else datetime.now(),
                duration_ms=None,  # Could be calculated if timing data available
                error=None if result and result.success else "Execution failed"
            )

            group.add_execution(execution)

        group.mark_completed()

        # Add to history
        if self.context_history is not None:
            self.context_history.add_execution_group(group, snapshot_id)
