"""Tool registry and base classes for Oclawma.

This module provides the foundation for the tool system, including:
- Tool registration and discovery
- Base tool interface
- Tool result formatting for LLM context
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Generic, Optional, TypeVar


class ToolError(Exception):
    """Base exception for tool errors."""

    def __init__(
        self, message: str, tool_name: str = "", cause: Optional[Exception] = None
    ) -> None:
        super().__init__(message)
        self.message = message
        self.tool_name = tool_name
        self.cause = cause


class ToolExecutionError(ToolError):
    """Raised when a tool execution fails."""

    pass


class ToolValidationError(ToolError):
    """Raised when tool parameters are invalid."""

    pass


class ToolNotFoundError(ToolError):
    """Raised when a requested tool is not found."""

    pass


class ToolTimeoutError(ToolError):
    """Raised when a tool execution times out."""

    pass


@dataclass(frozen=True)
class ToolParameter:
    """Definition of a tool parameter."""

    name: str
    description: str
    type: str = "string"
    required: bool = True
    default: Any = None
    enum: Optional[list[str]] = None


@dataclass(frozen=True)
class ToolSchema:
    """Schema definition for a tool."""

    name: str
    description: str
    parameters: list[ToolParameter] = field(default_factory=list)
    returns: str = "string"
    examples: list[str] = field(default_factory=list)


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

    This class encapsulates the output of a tool execution along with
    metadata for formatting and context management.
    """

    tool_name: str
    success: bool
    output: str
    error_message: Optional[str] = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_llm_context(self, max_length: int = 4000) -> str:
        """Format the result for LLM context.

        Args:
            max_length: Maximum length of output to include

        Returns:
            Formatted string suitable for LLM context
        """
        status = "✓" if self.success else "✗"

        # Truncate output if too long
        output = self.output
        if len(output) > max_length:
            truncated_len = len(output) - max_length
            output = output[:max_length] + f"\n\n[... {truncated_len} characters truncated ...]"

        lines = [
            f"<{self.tool_name}>",
            f"  Status: {status}",
        ]

        if self.error_message and not self.success:
            lines.append(f"  Error: {self.error_message}")

        if output:
            lines.append(f"  Output:\n{self._indent(output, 4)}")

        if self.metadata:
            lines.append(f"  Metadata: {self.metadata}")

        lines.append(f"</{self.tool_name}>")

        return "\n".join(lines)

    def _indent(self, text: str, spaces: int) -> str:
        """Indent text by specified number of spaces."""
        indent = " " * spaces
        return "\n".join(indent + line for line in text.split("\n"))

    def to_dict(self) -> dict[str, Any]:
        """Convert result to dictionary."""
        return {
            "tool_name": self.tool_name,
            "success": self.success,
            "output": self.output,
            "error_message": self.error_message,
            "metadata": self.metadata,
        }


T = TypeVar("T")


class BaseTool(ABC, Generic[T]):
    """Abstract base class for all tools.

    Tools are the primary way Oclawma interacts with the system.
    Each tool has a name, description, parameters, and execution logic.
    """

    def __init__(self) -> None:
        """Initialize the tool."""
        self._schema = self._define_schema()

    @property
    def name(self) -> str:
        """Get the tool name."""
        return self._schema.name

    @property
    def description(self) -> str:
        """Get the tool description."""
        return self._schema.description

    @property
    def schema(self) -> ToolSchema:
        """Get the tool schema."""
        return self._schema

    @abstractmethod
    def _define_schema(self) -> ToolSchema:
        """Define the tool's schema.

        Returns:
            ToolSchema with name, description, and parameters.
        """
        ...

    @abstractmethod
    async def execute(self, **kwargs: Any) -> ToolResult:
        """Execute the tool with the given parameters.

        Args:
            **kwargs: Tool parameters as defined in schema

        Returns:
            ToolResult with execution output

        Raises:
            ToolValidationError: If parameters are invalid
            ToolExecutionError: If execution fails
        """
        ...

    def validate_params(self, params: dict[str, Any]) -> dict[str, Any]:
        """Validate and normalize parameters.

        Args:
            params: Parameters to validate

        Returns:
            Validated and normalized parameters

        Raises:
            ToolValidationError: If parameters are invalid
        """
        validated = {}
        errors = []

        for param in self._schema.parameters:
            if param.name in params:
                value = params[param.name]
                # Type checking (basic)
                if param.type == "integer" and not isinstance(value, int):
                    try:
                        value = int(value)
                    except (ValueError, TypeError):
                        errors.append(f"Parameter '{param.name}' must be an integer")
                        continue
                elif param.type == "boolean" and not isinstance(value, bool):
                    if isinstance(value, str):
                        value = value.lower() in ("true", "1", "yes", "on")
                    else:
                        errors.append(f"Parameter '{param.name}' must be a boolean")
                        continue

                # Enum checking
                if param.enum and value not in param.enum:
                    errors.append(
                        f"Parameter '{param.name}' must be one of: {', '.join(param.enum)}"
                    )
                    continue

                validated[param.name] = value
            elif param.required:
                errors.append(f"Required parameter '{param.name}' is missing")
            elif param.default is not None:
                validated[param.name] = param.default

        # Check for unknown parameters
        known_params = {p.name for p in self._schema.parameters}
        for key in params:
            if key not in known_params:
                errors.append(f"Unknown parameter: '{key}'")

        if errors:
            raise ToolValidationError(
                f"Validation failed for tool '{self.name}':\n"
                + "\n".join(f"  - {e}" for e in errors),
                tool_name=self.name,
            )

        return validated

    def to_dict(self) -> dict[str, Any]:
        """Convert tool to dictionary representation."""
        return {
            "name": self._schema.name,
            "description": self._schema.description,
            "parameters": [
                {
                    "name": p.name,
                    "description": p.description,
                    "type": p.type,
                    "required": p.required,
                    "default": p.default,
                    "enum": p.enum,
                }
                for p in self._schema.parameters
            ],
            "returns": self._schema.returns,
            "examples": self._schema.examples,
        }


class ToolRegistry:
    """Registry for managing available tools.

    The registry maintains a collection of tools and provides
    methods for registration, lookup, and discovery.
    """

    def __init__(self) -> None:
        """Initialize an empty tool registry."""
        self._tools: dict[str, BaseTool] = {}
        self._categories: dict[str, list[str]] = {}

    def register(self, tool: BaseTool, category: str = "general") -> "ToolRegistry":
        """Register a tool in the registry.

        Args:
            tool: The tool to register
            category: Category for grouping tools

        Returns:
            Self for chaining

        Raises:
            ToolError: If a tool with the same name is already registered
        """
        if tool.name in self._tools:
            raise ToolError(f"Tool '{tool.name}' is already registered", tool_name=tool.name)

        self._tools[tool.name] = tool

        if category not in self._categories:
            self._categories[category] = []
        self._categories[category].append(tool.name)

        return self

    def unregister(self, name: str) -> bool:
        """Unregister a tool from the registry.

        Args:
            name: Name of the tool to unregister

        Returns:
            True if the tool was removed, False if not found
        """
        if name not in self._tools:
            return False

        self._tools.pop(name)

        # Remove from categories
        for _category, tools in self._categories.items():
            if name in tools:
                tools.remove(name)
                break

        return True

    def get(self, name: str) -> BaseTool:
        """Get a tool by name.

        Args:
            name: Tool name

        Returns:
            The requested tool

        Raises:
            ToolNotFoundError: If the tool is not found
        """
        if name not in self._tools:
            raise ToolNotFoundError(f"Tool '{name}' not found", tool_name=name)
        return self._tools[name]

    def has(self, name: str) -> bool:
        """Check if a tool exists in the registry.

        Args:
            name: Tool name

        Returns:
            True if the tool exists, False otherwise
        """
        return name in self._tools

    def list_tools(self, category: Optional[str] = None) -> list[str]:
        """List available tool names.

        Args:
            category: Optional category filter

        Returns:
            List of tool names
        """
        if category:
            return list(self._categories.get(category, []))
        return list(self._tools.keys())

    def list_categories(self) -> list[str]:
        """List available tool categories.

        Returns:
            List of category names
        """
        return list(self._categories.keys())

    def get_tools_by_category(self, category: str) -> list[BaseTool]:
        """Get all tools in a category.

        Args:
            category: Category name

        Returns:
            List of tools in the category
        """
        names = self._categories.get(category, [])
        return [self._tools[name] for name in names if name in self._tools]

    def get_all_schemas(self) -> list[dict[str, Any]]:
        """Get schemas for all registered tools.

        Returns:
            List of tool schemas as dictionaries
        """
        return [tool.to_dict() for tool in self._tools.values()]

    def clear(self) -> None:
        """Clear all tools from the registry."""
        self._tools.clear()
        self._categories.clear()

    async def execute(self, name: str, **params: Any) -> ToolResult:
        """Execute a tool by name with the given parameters.

        Args:
            name: Tool name
            **params: Tool parameters

        Returns:
            ToolResult from execution

        Raises:
            ToolNotFoundError: If the tool is not found
            ToolValidationError: If parameters are invalid
            ToolExecutionError: If execution fails
        """
        tool = self.get(name)
        validated_params = tool.validate_params(params)
        return await tool.execute(**validated_params)

    def __len__(self) -> int:
        """Return the number of registered tools."""
        return len(self._tools)

    def __contains__(self, name: str) -> bool:
        """Check if a tool name is registered."""
        return name in self._tools


# Global registry instance
_default_registry: Optional[ToolRegistry] = None


def get_default_registry() -> ToolRegistry:
    """Get the default global tool registry.

    Returns:
        The default tool registry instance
    """
    global _default_registry
    if _default_registry is None:
        _default_registry = ToolRegistry()
    return _default_registry


def set_default_registry(registry: ToolRegistry) -> None:
    """Set the default global tool registry.

    Args:
        registry: The registry to set as default
    """
    global _default_registry
    _default_registry = registry


def register_tool(tool: BaseTool, category: str = "general") -> ToolRegistry:
    """Register a tool in the default registry.

    Args:
        tool: The tool to register
        category: Category for the tool

    Returns:
        The default registry
    """
    return get_default_registry().register(tool, category)


def get_tool(name: str) -> BaseTool:
    """Get a tool from the default registry.

    Args:
        name: Tool name

    Returns:
        The requested tool
    """
    return get_default_registry().get(name)


def list_all_tools(category: Optional[str] = None) -> list[str]:
    """List tools from the default registry.

    Args:
        category: Optional category filter

    Returns:
        List of tool names
    """
    return get_default_registry().list_tools(category)
