# File: src/noveler/infrastructure/json/mcp/servers/tool_executor.py
# Purpose: Unified tool execution engine with consistent error handling and lifecycle management
# Context: Extracted from JSONConversionServer to improve SRP (Tier 1b Phase 3)

"""Unified MCP tool execution engine with error handling and result formatting.

This module provides a centralized tool execution coordinator that handles:
- Argument validation
- Execution context management
- Error handling and recovery
- Result formatting
- Execution logging and monitoring

Components:
- ToolExecutor: Central execution engine
- ExecutionContext: Contextual information for tool execution
- ToolResult: Standardized result format
"""

from __future__ import annotations

import asyncio
import logging
from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Any


class ExecutionStatus(Enum):
    """Tool execution status enumeration."""

    SUCCESS = "success"
    FAILURE = "failure"
    ERROR = "error"
    TIMEOUT = "timeout"
    CANCELLED = "cancelled"


@dataclass
class ExecutionContext:
    """Contextual information for tool execution.

    Attributes:
        tool_name: Name of the tool being executed
        project_root: Root directory of the project
        timeout_seconds: Execution timeout in seconds
        async_mode: Whether to execute asynchronously
        retry_count: Number of retry attempts on failure
    """

    tool_name: str
    project_root: Path | None = None
    timeout_seconds: int = 300
    async_mode: bool = False
    retry_count: int = 0


@dataclass
class ToolResult:
    """Standardized tool execution result.

    Attributes:
        status: Execution status (success, failure, error, timeout)
        output: Tool execution output
        error: Error message if execution failed
        duration_ms: Execution time in milliseconds
        retry_count: Number of retries performed
    """

    status: ExecutionStatus
    output: Any
    error: str | None = None
    duration_ms: float = 0.0
    retry_count: int = 0

    def to_dict(self) -> dict[str, Any]:
        """Convert result to dictionary format."""
        return {
            "status": self.status.value,
            "output": self.output,
            "error": self.error,
            "duration_ms": self.duration_ms,
            "retry_count": self.retry_count,
        }


class ToolExecutor:
    """Unified MCP tool execution engine.

    Purpose:
        Provides centralized tool execution with consistent error handling,
        argument validation, and result formatting.

    Responsibilities:
        - Execute tool handlers with consistent interface
        - Validate arguments before execution
        - Handle execution errors gracefully
        - Implement retry logic
        - Format results consistently
        - Log execution details

    Context:
        Used by JSONConversionServer to execute all MCP tools with unified
        error handling and lifecycle management.

    Design Patterns:
        - Command Pattern: Tool execution as self-contained commands
        - Strategy Pattern: Different execution strategies (sync vs async)
        - Template Method: Consistent execution flow
    """

    def __init__(self, logger: logging.Logger) -> None:
        """Initialize the tool executor.

        Args:
            logger: Logger instance for execution events

        Preconditions:
            - logger must be a valid Logger instance

        Side Effects:
            - Initializes execution statistics tracker
            - Logs executor initialization
        """
        self._logger = logger
        self._execution_count = 0
        self._failure_count = 0
        self._total_duration_ms = 0.0

        self._logger.info("Initializing tool executor engine")

    def execute(
        self,
        handler: Callable[..., Any],
        arguments: dict[str, Any],
        context: ExecutionContext,
    ) -> ToolResult:
        """Execute a tool handler with error handling.

        Purpose:
            Execute a tool with automatic error handling, retry logic,
            and consistent result formatting.

        Args:
            handler: Callable that implements the tool logic
            arguments: Arguments to pass to the handler
            context: Execution context with metadata

        Returns:
            ToolResult with status, output, and execution metadata

        Side Effects:
            - Updates execution statistics
            - Logs execution details
            - Implements retry logic on failure
        """
        import time

        self._logger.debug(
            "Executing tool: %s",
            context.tool_name,
        )
        start_time = time.time()

        # Validate arguments
        if not isinstance(arguments, dict):
            return ToolResult(
                status=ExecutionStatus.ERROR,
                output=None,
                error=f"Invalid arguments type: {type(arguments)}",
                duration_ms=0.0,
            )

        # Execute with retry logic
        for attempt in range(context.retry_count + 1):
            try:
                result = handler(**arguments)
                duration_ms = (time.time() - start_time) * 1000

                self._execution_count += 1
                self._total_duration_ms += duration_ms

                self._logger.info(
                    "Tool executed successfully: %s (%.1fms, attempt %s)",
                    context.tool_name,
                    duration_ms,
                    attempt + 1,
                )

                return ToolResult(
                    status=ExecutionStatus.SUCCESS,
                    output=result,
                    duration_ms=duration_ms,
                    retry_count=attempt,
                )

            except asyncio.TimeoutError:
                self._logger.warning(
                    "Tool execution timeout: %s (%ss)",
                    context.tool_name,
                    context.timeout_seconds,
                )
                self._failure_count += 1

                if attempt == context.retry_count:
                    self._execution_count += 1
                    return ToolResult(
                        status=ExecutionStatus.TIMEOUT,
                        output=None,
                        error=f"Execution timeout after {context.timeout_seconds}s",
                        duration_ms=(time.time() - start_time) * 1000,
                        retry_count=attempt,
                    )

            except TypeError as e:
                self._logger.error(
                    "Invalid argument signature: %s",
                    e,
                )
                self._failure_count += 1
                self._execution_count += 1
                return ToolResult(
                    status=ExecutionStatus.ERROR,
                    output=None,
                    error=f"Argument mismatch: {e}",
                    duration_ms=(time.time() - start_time) * 1000,
                    retry_count=attempt,
                )

            except Exception as e:
                self._logger.exception(
                    "Tool execution error: %s",
                    context.tool_name,
                )
                self._failure_count += 1

                if attempt == context.retry_count:
                    duration_ms = (time.time() - start_time) * 1000
                    self._execution_count += 1
                    return ToolResult(
                        status=ExecutionStatus.FAILURE,
                        output=None,
                        error=str(e),
                        duration_ms=duration_ms,
                        retry_count=attempt,
                    )

                # Wait before retry
                wait_time = min(2 ** attempt, 10)  # Exponential backoff
                self._logger.debug(
                    "Retrying tool execution in %ss (attempt %s/%s)",
                    wait_time,
                    attempt + 1,
                    context.retry_count + 1,
                )
                time.sleep(wait_time)

        # Should not reach here
        return ToolResult(
            status=ExecutionStatus.FAILURE,
            output=None,
            error="Unexpected execution failure",
            duration_ms=(time.time() - start_time) * 1000,
            retry_count=context.retry_count,
        )

    async def execute_async(
        self,
        handler: Callable[..., Any],
        arguments: dict[str, Any],
        context: ExecutionContext,
    ) -> ToolResult:
        """Execute a tool handler asynchronously with error handling.

        Purpose:
            Execute an async tool with error handling and timeout management.

        Args:
            handler: Async callable that implements the tool logic
            arguments: Arguments to pass to the handler
            context: Execution context with metadata

        Returns:
            ToolResult with status, output, and execution metadata

        Side Effects:
            - Updates execution statistics
            - Implements timeout using asyncio.wait_for
        """
        import time

        self._logger.debug(
            "Executing async tool: %s",
            context.tool_name,
        )
        start_time = time.time()

        # Validate arguments
        if not isinstance(arguments, dict):
            return ToolResult(
                status=ExecutionStatus.ERROR,
                output=None,
                error=f"Invalid arguments type: {type(arguments)}",
                duration_ms=0.0,
            )

        try:
            # Execute with timeout
            result = await asyncio.wait_for(
                handler(**arguments),
                timeout=context.timeout_seconds,
            )

            duration_ms = (time.time() - start_time) * 1000
            self._execution_count += 1
            self._total_duration_ms += duration_ms

            self._logger.info(
                "Async tool executed successfully: %s (%.1fms)",
                context.tool_name,
                duration_ms,
            )

            return ToolResult(
                status=ExecutionStatus.SUCCESS,
                output=result,
                duration_ms=duration_ms,
            )

        except asyncio.TimeoutError:
            self._logger.warning(
                "Async tool execution timeout: %s (%ss)",
                context.tool_name,
                context.timeout_seconds,
            )
            self._failure_count += 1

            return ToolResult(
                status=ExecutionStatus.TIMEOUT,
                output=None,
                error=f"Async execution timeout after {context.timeout_seconds}s",
                duration_ms=(time.time() - start_time) * 1000,
            )

        except TypeError as e:
            self._logger.error(
                "Invalid async argument signature: %s",
                e,
            )
            self._failure_count += 1

            return ToolResult(
                status=ExecutionStatus.ERROR,
                output=None,
                error=f"Argument mismatch: {e}",
                duration_ms=(time.time() - start_time) * 1000,
            )

        except Exception as e:
            self._logger.exception(
                "Async tool execution error: %s",
                context.tool_name,
            )
            self._failure_count += 1

            return ToolResult(
                status=ExecutionStatus.FAILURE,
                output=None,
                error=str(e),
                duration_ms=(time.time() - start_time) * 1000,
            )

    def get_statistics(self) -> dict[str, Any]:
        """Get execution statistics.

        Returns:
            Dictionary with execution count, failure count, and average duration
        """
        avg_duration_ms = (
            self._total_duration_ms / self._execution_count
            if self._execution_count > 0
            else 0.0
        )

        return {
            "total_executions": self._execution_count,
            "total_failures": self._failure_count,
            "success_rate": (
                ((self._execution_count - self._failure_count) / self._execution_count)
                if self._execution_count > 0
                else 0.0
            ),
            "average_duration_ms": avg_duration_ms,
            "total_duration_ms": self._total_duration_ms,
        }
