"""Tool execution engine for managing and executing tools."""

import asyncio
from typing import Any, Dict, List

from ouro.capabilities.tools.base import BaseTool
from ouro.config import Config
from ouro.core.llm import ToolOutput


class ToolExecutor:
    """Executes tools called by the LLM."""

    def __init__(self, tools: List[BaseTool]):
        """Initialize with a list of tools."""
        self.tools = {tool.name: tool for tool in tools}

    async def execute_tool_call(self, tool_name: str, tool_input: Dict[str, Any]) -> ToolOutput:
        """Execute a single tool call and return result."""
        if tool_name not in self.tools:
            return ToolOutput(content=f"Error: Tool '{tool_name}' not found")

        try:
            timeout = Config.TOOL_TIMEOUT
            if "timeout" in tool_input and tool_input["timeout"] is not None:
                try:
                    timeout = float(tool_input["timeout"])
                except (TypeError, ValueError):
                    timeout = Config.TOOL_TIMEOUT

            if timeout is not None and timeout > 0:
                async with asyncio.timeout(timeout):
                    result = await self.tools[tool_name].execute(**tool_input)
            else:
                result = await self.tools[tool_name].execute(**tool_input)

            if isinstance(result, ToolOutput):
                return result
            return ToolOutput(content=str(result))
        except asyncio.CancelledError:
            # Re-raise CancelledError to allow proper cleanup
            raise
        except TimeoutError:
            return ToolOutput(content=f"Error: Tool '{tool_name}' timed out after {timeout}s")
        except Exception as e:
            return ToolOutput(content=f"Error executing {tool_name}: {str(e)}")

    def is_tool_readonly(self, tool_name: str) -> bool:
        """Check if a tool is readonly (safe for parallel execution)."""
        tool = self.tools.get(tool_name)
        return tool.readonly if tool else False

    def conflict_keys(self, tool_name: str, arguments: Dict[str, Any]) -> set[str] | None:
        """Resource keys for one call. See ``BaseTool.conflict_keys``.

        Unknown tool names map to ``None`` so they run alone — preserving the
        prior fallback behavior.
        """
        tool = self.tools.get(tool_name)
        if tool is None:
            return None
        try:
            return tool.conflict_keys(**arguments)
        except Exception:
            return None

    def get_tool_schemas(self) -> List[Dict[str, Any]]:
        """Get Anthropic-formatted schemas for all tools."""
        return [tool.to_anthropic_schema() for tool in self.tools.values()]

    def add_tool(self, tool: BaseTool):
        """Add a tool to the executor.

        Args:
            tool: Tool instance to add
        """
        self.tools[tool.name] = tool
