import inspect
from typing import Callable, Any
from .exceptions import DraculaException


class ToolException(DraculaException):
    """Raised when something goes wrong with a tool."""

    pass


class Tool:
    """
    Represents a callable tool that Gemini can use.

    Args:
        func (Callable): The function to wrap as a tool.
        descrition (str): A description of what the tool does.

    Example:
        >>> @tool(description="Get the weather for a city")
        ... def get_weather(city: str) -> str:
        ...     return f"It's sunny in {city}"
    """

    def __init__(self, func: Callable, description: str):
        self.func = func
        self.description = description
        self.name = func.__name__
        self.parameters = self._extract_parameters()

    def _extract_parameters(self) -> dict:
        """
        Automatically extract parameter names and types from the function signature.
        This is used to build the tool schema for Gemini.
        """

        sig = inspect.signature(self.func)
        hints = self.func.__annotations__

        properties = {}
        required = []

        TYPE_MAP = {
            str: "string",
            int: "integer",
            float: "number",
            bool: "boolean",
            list: "array",
            dict: "object",
        }

        for name, param in sig.parameters.items():
            if name == "return":
                continue

            python_type = hints.get(name, str)
            json_type = TYPE_MAP.get(python_type, "string")

            doc = inspect.getdoc(self.func) or ""
            param_desc = f"The {name} parameter doc: {doc}"

            properties[name] = {"type": json_type, "description": param_desc}

            if param.default == inspect.Parameter.empty:
                required.append(name)

        return {"type": "object", "properties": properties, "required": required}

    def to_gemini_schema(self) -> dict:
        """
        Convert the tool to a Gemini-compatible function declaration schema.

        Returns:
            dict: Gemini function declaration schema.
        """

        return {
            "name": self.name,
            "description": self.description,
            "parameters": self.parameters,
        }

    def call(self, **kwargs) -> Any:
        """
        Call the underlying function with the given arguments.

        Args:
            **kwargs: Arguments to pass to the function.

        Returns:
            Any: The function's return value.

        Raises:
            ToolException: If the function call fails.
        """

        try:
            return self.func(**kwargs)

        except Exception as e:
            raise ToolException(f"Tool '{self.name}' failed: {str(e)}")

    async def async_call(self, **kwargs) -> Any:
        """
        Asynchronously call the underlying function.

        Args:
            **kwargs: Arguments to pass to the function.

        Returns:
            Any: The function's return value.

        Raises:
            ToolException: If the function call fails.
        """

        try:
            if inspect.iscoroutinefunction(self.func):
                return await self.func(**kwargs)
            return self.func(**kwargs)

        except Exception as e:
            raise ToolException(f"Tool '{self.name}' failed: {str(e)}")

    def __call__(self, *args, **kwargs) -> Any:
        """Allow the Tool to be called like a normal function."""
        return self.func(*args, **kwargs)

    def __repr__(self) -> str:
        return f"Tool(name={self.name!r}, description={self.description})"


class ToolResult:
    """
    Represents the result of a chat when a tool call is required.

    This is returned when auto_call=False and Gemini wants to call a tool.

    Attributes:
        requires_tool_call (bool): Whether a tool call is required.
        tool_name (str): The name of the tool to call.
        tool_args (dict): The arguments to pass to the tool.
        text (str): Any text response from Gemini alongside the tool call.
    """

    def __init__(
        self,
        requires_tool_call: bool = False,
        tool_name: str = None,
        tool_args: str = None,
        text: str = None,
    ):
        self.requires_tool_call = requires_tool_call
        self.tool_name = tool_name
        self.tool_args = tool_args or {}
        self.text = text

    def __repr__(self) -> str:
        return (
            f"ToolResult("
            f"requires_tool_call={self.requires_tool_call},"
            f"tool_name={self.tool_name!r},"
            f"tool_args={self.tool_args})"
        )


class ToolRegistry:
    """
    Manages a collection of tools available to Dracula.

    Args:
        tools (list): List of Tool objects to register.
    """

    def __init__(self, tools: list = None):
        self._tools: dict[str, Tool] = {}
        if tools:
            for tool in tools:
                self.register(tool)

    def register(self, tool: Tool):
        """
        Register a tool.

        Args:
            tool (Tool): The tool to register.
        """

        if not isinstance(tool, Tool):
            raise ToolException(f"Expected a Tool object, got {type(tool)}")
        self._tools[tool.name] = tool

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

        Args:
            name (str): The tool name.

        Returns:
            Tool: The tool object.

        Raises:
            ToolException: If the tool is not found.
        """

        if name not in self._tools:
            raise ToolException(f"Tool '{name}' not found.")
        return self._tools[name]

    def get_gemini_schemas(self) -> list:
        """
        Get all tool schemas in Gemini-compatible format.

        Returns:
            list: List of function declaration schemas.
        """
        return [tool.to_gemini_schema() for tool in self._tools.values()]

    def has_tools(self) -> bool:
        """Check if any tools are registered."""
        return len(self._tools) > 0

    def list_tools(self) -> list:
        """Return a list of all registered tool names."""
        return list(self._tools.keys())

    def __repr__(self) -> str:
        return f"ToolRegistry(tools={self.list_tools()})"


def tool(description: str) -> Callable:
    """
    Decorator to register a function as a Dracula tool.

    Args:
        description (str): A clear description of what the tool does.
                        Gemini uses this to decide when to call the tool.

    Returns:
        Callable: The wrapped function as a Tool object.

    Example:
        >>> @tool(description="Get the current weather for a city")
        ... def get_weather(city: str) -> str:
        ...     return f"It's 25°C and sunny in {city}"

        >>> @tool(description="Search the web for information")
        ... def search_web(query: str) -> str:
        ...     return f"Search results for {query}..."
    """

    def decarator(func: Callable) -> Tool:
        return Tool(func=func, description=description)

    return decarator
