"""
OPT-002: Batch Tool Executor

Provides batch execution capabilities for tools:
- Multiple tool calls in a single request
- Parallel execution where safe
- Batched permission checks
- Result aggregation
"""

import asyncio
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
from concurrent.futures import ThreadPoolExecutor

from groknroll.tools.base_tool import BaseTool


@dataclass
class ToolCall:
    """
    Represents a single tool call in a batch.

    Attributes:
        tool_name: Name of the tool to execute
        params: Parameters for the tool
        allow_parallel: Whether this call can run in parallel with others
    """
    tool_name: str
    params: dict[str, Any]
    allow_parallel: bool = True


@dataclass
class ToolResult:
    """
    Result of a single tool execution.

    Attributes:
        tool_name: Name of the tool that was executed
        success: Whether execution succeeded
        result: The tool's result (if successful)
        error: Error message (if failed)
        execution_time_ms: Time taken in milliseconds
    """
    tool_name: str
    success: bool
    result: Any = None
    error: Optional[str] = None
    execution_time_ms: float = 0.0


@dataclass
class BatchResult:
    """
    Result of batch tool execution.

    Attributes:
        results: List of individual tool results
        total_time_ms: Total batch execution time
        parallel_execution: Whether parallel execution was used
    """
    results: list[ToolResult] = field(default_factory=list)
    total_time_ms: float = 0.0
    parallel_execution: bool = False

    @property
    def all_succeeded(self) -> bool:
        """Check if all tool calls succeeded."""
        return all(r.success for r in self.results)

    @property
    def failed_count(self) -> int:
        """Get count of failed tool calls."""
        return sum(1 for r in self.results if not r.success)

    def get_result(self, tool_name: str) -> Optional[ToolResult]:
        """Get result for a specific tool by name."""
        for r in self.results:
            if r.tool_name == tool_name:
                return r
        return None


class ToolExecutor:
    """
    Batch tool executor with parallel execution support.

    OPT-002: Allows multiple tool calls in single request with:
    - Parallel execution for independent operations
    - Sequential execution for dependent operations
    - Batched permission checks

    Example:
        executor = ToolExecutor(tool_registry, permission_manager)

        # Batch execute multiple tools
        calls = [
            ToolCall("read", {"path": "file1.py"}),
            ToolCall("read", {"path": "file2.py"}),
            ToolCall("glob", {"pattern": "*.txt"}),
        ]
        result = await executor.execute_batch(calls)

        # Check results
        for r in result.results:
            if r.success:
                print(f"{r.tool_name}: {r.result}")
            else:
                print(f"{r.tool_name} failed: {r.error}")
    """

    # Tools that can safely run in parallel (read-only operations)
    PARALLEL_SAFE_TOOLS = frozenset([
        "read",
        "glob",
        "grep",
        "list",
        "lsp",
        "todoread",
    ])

    # Tools that must run sequentially (write operations or state changes)
    SEQUENTIAL_TOOLS = frozenset([
        "write",
        "edit",
        "bash",
        "todowrite",
    ])

    def __init__(
        self,
        tool_registry: Any,
        permission_manager: Optional[Any] = None,
        max_parallel: int = 4,
    ):
        """
        Initialize ToolExecutor.

        Args:
            tool_registry: Registry to get tool instances from
            permission_manager: Optional permission manager for checks
            max_parallel: Maximum number of parallel executions
        """
        self._registry = tool_registry
        self._permission_manager = permission_manager
        self._max_parallel = max_parallel
        self._executor = ThreadPoolExecutor(max_workers=max_parallel)

    async def execute_batch(
        self,
        calls: list[ToolCall],
        parallel: bool = True,
    ) -> BatchResult:
        """
        Execute a batch of tool calls.

        Args:
            calls: List of tool calls to execute
            parallel: Whether to use parallel execution where safe

        Returns:
            BatchResult with all individual results
        """
        import time
        start_time = time.perf_counter()

        batch_result = BatchResult()

        if not calls:
            return batch_result

        # Check permissions for all calls first
        if self._permission_manager:
            for call in calls:
                result = self._permission_manager.check(call.tool_name, **call.params)
                if result == "deny":
                    batch_result.results.append(ToolResult(
                        tool_name=call.tool_name,
                        success=False,
                        error=f"Permission denied for tool: {call.tool_name}",
                    ))
                    # Continue to check other permissions but mark this as failed

        # Determine execution strategy
        if parallel and self._can_execute_parallel(calls):
            batch_result.parallel_execution = True
            results = await self._execute_parallel(calls)
        else:
            results = await self._execute_sequential(calls)

        # Combine with any permission failures
        existing_tools = {r.tool_name for r in batch_result.results}
        for result in results:
            if result.tool_name not in existing_tools:
                batch_result.results.append(result)

        batch_result.total_time_ms = (time.perf_counter() - start_time) * 1000
        return batch_result

    def _can_execute_parallel(self, calls: list[ToolCall]) -> bool:
        """
        Check if calls can be executed in parallel.

        Returns False if any call:
        - Explicitly disallows parallel execution
        - Is a sequential-only tool
        - Could conflict with another call (e.g., write to same file)
        """
        # Check for sequential-only tools
        for call in calls:
            if call.tool_name in self.SEQUENTIAL_TOOLS:
                return False
            if not call.allow_parallel:
                return False

        # Check for potential conflicts (same file operations)
        paths = []
        for call in calls:
            path = call.params.get("path")
            if path:
                if path in paths:
                    return False  # Duplicate path operation
                paths.append(path)

        return True

    async def _execute_parallel(self, calls: list[ToolCall]) -> list[ToolResult]:
        """Execute calls in parallel."""
        tasks = [self._execute_single(call) for call in calls]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        # Convert exceptions to ToolResults
        final_results = []
        for call, result in zip(calls, results):
            if isinstance(result, Exception):
                final_results.append(ToolResult(
                    tool_name=call.tool_name,
                    success=False,
                    error=str(result),
                ))
            else:
                final_results.append(result)

        return final_results

    async def _execute_sequential(self, calls: list[ToolCall]) -> list[ToolResult]:
        """Execute calls sequentially."""
        results = []
        for call in calls:
            try:
                result = await self._execute_single(call)
                results.append(result)
            except Exception as e:
                results.append(ToolResult(
                    tool_name=call.tool_name,
                    success=False,
                    error=str(e),
                ))
        return results

    async def _execute_single(self, call: ToolCall) -> ToolResult:
        """Execute a single tool call."""
        import time
        start_time = time.perf_counter()

        # Get tool from registry
        tool = self._registry.get(call.tool_name)
        if tool is None:
            return ToolResult(
                tool_name=call.tool_name,
                success=False,
                error=f"Tool not found: {call.tool_name}",
            )

        try:
            result = await tool.execute(**call.params)
            execution_time = (time.perf_counter() - start_time) * 1000

            return ToolResult(
                tool_name=call.tool_name,
                success=True,
                result=result,
                execution_time_ms=execution_time,
            )

        except Exception as e:
            execution_time = (time.perf_counter() - start_time) * 1000
            return ToolResult(
                tool_name=call.tool_name,
                success=False,
                error=str(e),
                execution_time_ms=execution_time,
            )

    async def execute_single(
        self,
        tool_name: str,
        **params,
    ) -> ToolResult:
        """
        Execute a single tool call (convenience method).

        Args:
            tool_name: Name of the tool to execute
            **params: Tool parameters

        Returns:
            ToolResult with execution outcome
        """
        call = ToolCall(tool_name=tool_name, params=params)
        batch_result = await self.execute_batch([call], parallel=False)
        return batch_result.results[0] if batch_result.results else ToolResult(
            tool_name=tool_name,
            success=False,
            error="No result returned",
        )

    def close(self):
        """Shutdown the executor."""
        self._executor.shutdown(wait=True)

    def __del__(self):
        """Cleanup on deletion."""
        try:
            self._executor.shutdown(wait=False)
        except Exception:
            pass
