"""
Base tool classes and interfaces for Agentbyte framework.

This module defines the abstract base class that all tools must implement,
providing a unified interface for tool discovery, validation, and execution.

Includes:
- BaseTool: Abstract base class for all tools
- ApprovalMode: Tool approval requirements
- FunctionTool: Tool that wraps Python functions
"""
import inspect
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Union, get_type_hints

from ..types import ToolResult


class ApprovalMode(Enum):
    """
    Tool approval requirements for enterprise safety.

    NEVER: Execute immediately without approval (default for safe operations)
    ALWAYS: Require human approval before execution (for sensitive operations)

    Example:
        @tool(approval_mode="always_require")
        def delete_database(name: str) -> str:
            '''Delete a database - requires approval.'''
            ...
    """
    NEVER = "never_require"
    ALWAYS = "always_require"


class BaseTool(ABC):
    """
    Abstract base class that all tools must implement.

    Defines the interface for tools that agents can use to perform actions
    beyond text generation. Tools can be simple functions, REST API calls,
    database queries, or any external system integration.

    The base tool provides:
    - Metadata (name, description, version) for LLM discovery
    - Schema definition via JSON Schema format
    - Validation of parameters before execution
    - Async execution with error handling
    - Optional streaming support
    - Conversion to LLM function-calling format

    Example:
        class WeatherTool(BaseTool):
            @property
            def parameters(self) -> Dict[str, Any]:
                return {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string"}
                    },
                    "required": ["city"]
                }

            async def execute(self, parameters: Dict[str, Any]) -> ToolResult:
                city = parameters["city"]
                # ... fetch weather ...
                return ToolResult(success=True, result=weather_data)
    """

    def __init__(
        self,
        name: str,
        description: str,
        version: str = "1.0.0",
        approval_mode: ApprovalMode = ApprovalMode.NEVER,
    ):
        """
        Initialize the base tool.

        Args:
            name: Unique tool identifier (used by LLM to call the tool)
            description: What the tool does (helps LLM decide when to use it)
            version: Tool version following semantic versioning (e.g., "1.0.0")
            approval_mode: Whether human approval is required before execution
        """
        self.name = name
        self.description = description
        self.version = version
        self.approval_mode = approval_mode

    @property
    @abstractmethod
    def parameters(self) -> Dict[str, Any]:
        """
        JSON schema defining expected inputs for this tool.

        Must return a dictionary in JSON Schema format with:
        - type: "object"
        - properties: dict mapping parameter names to schemas
        - required: list of required parameter names

        Returns:
            JSON schema dictionary describing tool parameters

        Example:
            {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name"
                    },
                    "days": {
                        "type": "integer",
                        "description": "Number of days"
                    }
                },
                "required": ["city"]
            }
        """
        pass

    @abstractmethod
    async def execute(self, parameters: Dict[str, Any]) -> ToolResult:
        """
        Execute the tool with the given parameters.

        This method should:
        1. Validate parameters (base validation happens before this)
        2. Perform the tool's action
        3. Return a ToolResult with success/result/error

        Args:
            parameters: Tool input parameters matching the schema

        Returns:
            ToolResult containing execution outcome

        Example:
            async def execute(self, parameters: Dict[str, Any]) -> ToolResult:
                try:
                    result = await some_operation(parameters)
                    return ToolResult(success=True, result=result)
                except Exception as e:
                    return ToolResult(success=False, error=str(e))
        """
        pass

    async def execute_stream(
        self,
        parameters: Dict[str, Any],
        cancellation_token: Optional[Any] = None,
    ) -> AsyncGenerator[Union[ToolResult, Any], None]:
        """
        Execute the tool with streaming output support.

        Tools can override this to provide streaming capabilities for
        long-running operations. The default implementation wraps execute()
        for backward compatibility.

        Args:
            parameters: Tool input parameters
            cancellation_token: Optional token for cancelling execution

        Yields:
            Messages, events during execution, and final ToolResult

        Example (streaming implementation):
            async def execute_stream(self, parameters, cancellation_token=None):
                for i in range(10):
                    yield {"progress": i * 10}
                    await asyncio.sleep(0.1)

                final_result = await self.execute(parameters)
                yield final_result
        """
        # Default implementation: just call execute and yield result
        result = await self.execute(parameters)
        yield result

    def supports_streaming(self) -> bool:
        """
        Check if this tool supports streaming execution.

        Returns:
            True if tool has overridden execute_stream, False otherwise
        """
        # Check if execute_stream was overridden from the base class
        return type(self).execute_stream is not BaseTool.execute_stream

    def validate_parameters(self, params: Dict[str, Any]) -> bool:
        """
        Validate that provided parameters match the tool's schema.

        Performs basic validation:
        - Checks all required fields are present
        - Validates parameter types match schema expectations

        Subclasses can override for more sophisticated validation
        (e.g., range checks, format validation, etc.).

        Args:
            params: Parameters to validate

        Returns:
            True if parameters are valid, False otherwise
        """
        try:
            schema = self.parameters
            required_fields = schema.get("required", [])

            # Check required fields are present
            for field in required_fields:
                if field not in params:
                    return False

            # Check parameter types if specified in schema
            properties = schema.get("properties", {})
            for param_name, param_value in params.items():
                if param_name in properties:
                    expected_type = properties[param_name].get("type")
                    if expected_type and not self._check_type(
                        param_value, expected_type
                    ):
                        return False

            return True
        except Exception:
            return False

    def _check_type(self, value: Any, expected_type: str) -> bool:
        """
        Check if value matches expected JSON schema type.

        Maps JSON schema types to Python types:
        - string -> str
        - integer -> int
        - number -> int or float
        - boolean -> bool
        - array -> list
        - object -> dict

        Args:
            value: Value to check
            expected_type: JSON schema type string

        Returns:
            True if value matches expected type
        """
        type_mapping = {
            "string": str,
            "integer": int,
            "number": (int, float),
            "boolean": bool,
            "array": list,
            "object": dict,
        }

        expected_python_type = type_mapping.get(expected_type)
        if expected_python_type is None:
            return True  # Unknown type, assume valid

        return isinstance(value, expected_python_type)

    def to_llm_format(self) -> Dict[str, Any]:
        """
        Convert tool to OpenAI function calling format.

        This format is compatible with most modern LLM providers that
        support function calling (OpenAI, Anthropic, Bedrock, etc.).

        Version tracking pattern from Anthropic: include version in name
        for tools with version != "1.0.0" (e.g., "tool_name_v2.1.0").
        This enables tracking tool evolution and compatibility.

        Returns:
            Dictionary in OpenAI function calling format

        Example:
            {
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get current weather",
                    "parameters": {...}
                }
            }
        """
        # Include version in tool name for compatibility tracking
        versioned_name = (
            f"{self.name}_v{self.version}"
            if self.version != "1.0.0"
            else self.name
        )

        return {
            "type": "function",
            "function": {
                "name": versioned_name,
                "description": self.description,
                "parameters": self.parameters,
            },
        }

    def __str__(self) -> str:
        return f"{self.__class__.__name__}(name='{self.name}')"

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"name='{self.name}', "
            f"description='{self.description}')"
        )


class FunctionTool(BaseTool):
    """Tool that wraps a Python function for use by agents.

    Automatically extracts function metadata (name, docstring, parameters)
    and provides parameter validation based on type hints. Supports both
    sync and async functions transparently.

    The FunctionTool inspects the function signature at initialization time
    and builds a JSON schema that can be sent to the LLM. When executed,
    it validates parameters and handles both synchronous and asynchronous
    function calls with proper error handling.

    Example:
        def calculate_sum(a: int, b: int) -> int:
            '''Add two numbers.'''
            return a + b

        tool = FunctionTool(calculate_sum)
        result = await tool.execute({"a": 5, "b": 3})
        # result.success == True, result.result == 8
    """

    def __init__(
        self,
        func: Callable,
        name: Optional[str] = None,
        description: Optional[str] = None,
        version: str = "1.0.0",
        approval_mode: ApprovalMode = ApprovalMode.NEVER,
    ):
        """
        Create a tool from a Python function.

        Args:
            func: The function to wrap as a tool
            name: Optional custom name (defaults to function name)
            description: Optional custom description (defaults to docstring)
            version: Tool version following semantic versioning
            approval_mode: Whether approval is required before execution
        """
        self.func = func
        tool_name = name or func.__name__
        tool_description = (
            description or
            func.__doc__ or
            f"Execute {func.__name__} function"
        )

        super().__init__(tool_name, tool_description, version, approval_mode)

        # Extract function metadata for schema generation
        self.signature = inspect.signature(func)
        self.type_hints = get_type_hints(func)
        self._parameters_schema = self._build_parameters_schema()

    @property
    def parameters(self) -> Dict[str, Any]:
        """Get JSON schema for function parameters."""
        return self._parameters_schema

    def __call__(self, *args, **kwargs):
        """
        Make the tool callable like the original function.

        This allows decorated functions to be called directly for testing:

            @tool
            def get_weather(city: str) -> str:
                return f"Weather in {city}"

            # Can still call directly!
            result = get_weather("Seattle")
        """
        return self.func(*args, **kwargs)

    async def execute(self, parameters: Dict[str, Any]) -> ToolResult:
        """
        Execute the wrapped function with validated parameters.

        Handles both sync and async functions transparently. Validates
        parameters before execution and catches all exceptions, returning
        them as ToolResult with success=False.

        Args:
            parameters: Function arguments as dictionary

        Returns:
            ToolResult with function execution outcome
        """
        try:
            # Validate parameters before execution
            if not self.validate_parameters(parameters):
                return ToolResult(
                    success=False,
                    result=None,
                    error="Invalid parameters provided",
                    metadata={"tool_name": self.name},
                )

            # Execute function (handle both sync and async)
            if inspect.iscoroutinefunction(self.func):
                result = await self.func(**parameters)
            else:
                result = self.func(**parameters)

            return ToolResult(
                success=True,
                result=result,
                error=None,
                metadata={"tool_name": self.name},
            )

        except Exception as e:
            return ToolResult(
                success=False,
                result=None,
                error=str(e),
                metadata={
                    "tool_name": self.name,
                    "exception_type": type(e).__name__
                },
            )

    def _build_parameters_schema(self) -> Dict[str, Any]:
        """
        Build JSON schema from function signature and type hints.

        This is the core schema generation logic that converts Python
        type hints into JSON Schema format for LLMs. It:
        1. Iterates through function parameters
        2. Extracts type hints for each parameter
        3. Converts Python types to JSON Schema types
        4. Identifies required vs optional parameters
        5. Extracts enum values from Literal types

        Returns:
            JSON schema dictionary
        """
        schema = {
            "type": "object",
            "properties": {},
            "required": []
        }

        for param_name, param in self.signature.parameters.items():
            # Skip 'self' parameter for methods
            if param_name == "self":
                continue

            # Get parameter type from type hints
            param_type = self.type_hints.get(param_name)
            json_type = self._python_type_to_json_type(param_type)

            property_schema = {"type": json_type}

            # Add enum constraint for Literal types or Enum classes
            enum_values = self._extract_enum_values(param_type)
            if enum_values:
                property_schema["enum"] = enum_values

            # Add description from docstring if available
            # (Simplified - full implementation would parse docstrings)
            if hasattr(param, "annotation") and hasattr(
                param.annotation, "__doc__"
            ):
                property_schema["description"] = param.annotation.__doc__

            schema["properties"][param_name] = property_schema

            # Add to required if no default value
            if param.default == inspect.Parameter.empty:
                schema["required"].append(param_name)

        return schema

    def _python_type_to_json_type(self, python_type: Any) -> str:
        """
        Convert Python type hints to JSON schema types.

        Type mapping:
        - str -> "string"
        - int -> "integer"
        - float -> "number"
        - bool -> "boolean"
        - list/List -> "array"
        - dict/Dict -> "object"
        - None/Optional -> "null"
        - Unknown/Union -> "string" (fallback)

        Args:
            python_type: Python type annotation

        Returns:
            JSON schema type string
        """
        if python_type is None or python_type is type(None):
            return "null"
        elif python_type is str:
            return "string"
        elif python_type is int:
            return "integer"
        elif python_type is float:
            return "number"
        elif python_type is bool:
            return "boolean"
        elif python_type is list or (
            hasattr(python_type, "__origin__") and
            python_type.__origin__ is list
        ):
            return "array"
        elif python_type is dict or (
            hasattr(python_type, "__origin__") and
            python_type.__origin__ is dict
        ):
            return "object"
        else:
            # For Union types, complex types, etc., default to string
            return "string"

    def _extract_enum_values(self, param_type: Any) -> Optional[List[Any]]:
        """
        Extract enum values from Literal types or Enum classes.

        Supports:
        - typing.Literal["a", "b", "c"]
        - enum.Enum classes

        Args:
            param_type: Python type annotation

        Returns:
            List of enum values or None
        """
        # Handle typing.Literal (Python 3.8+)
        if hasattr(param_type, "__origin__"):
            origin = getattr(param_type, "__origin__", None)

            # Check for Literal type
            try:
                from typing import Literal, get_args, get_origin

                if get_origin(param_type) is Literal:
                    return list(get_args(param_type))
            except (ImportError, AttributeError):
                pass

            # Fallback for older Python or typing_extensions
            if hasattr(param_type, "__args__"):
                type_name = str(origin) if origin else str(param_type)
                if "Literal" in type_name:
                    return list(param_type.__args__)

        # Handle Enum classes
        try:
            from enum import Enum

            if isinstance(param_type, type) and issubclass(param_type, Enum):
                return [e.value for e in param_type]
        except (TypeError, AttributeError):
            pass

        return None


__all__ = ["ApprovalMode", "BaseTool", "FunctionTool"]
