from __future__ import annotations

import inspect
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence

from .errors import ToolNotFoundError
from .tool import ToolDef

OpenAIToolPayload = Dict[str, Any]
ToolChoicePayload = Dict[str, Any]


class ToolRegistry:
    """A small, explicit registry for OpenAI function tools (Python 3.9+)."""

    def __init__(self, tools: Optional[Sequence[ToolDef]] = None) -> None:
        self._tools: Dict[str, ToolDef] = {}
        if tools:
            for tool in tools:
                self.add(tool)

        # Auto-register methods if this is a subclass or instance
        self.register_object(self)

    def register_object(self, obj: Any) -> None:
        """Register public methods of an object as tools.

        Automatically skips methods that are part of the ToolRegistry API
        if `obj` is a ToolRegistry instance.
        """
        # Identify base methods to skip if we are scanning a Registry instance
        exclude_names = set()
        if isinstance(obj, ToolRegistry):
            exclude_names = set(dir(ToolRegistry))

        for name, method in inspect.getmembers(obj, predicate=inspect.ismethod):
            if name.startswith("_"):
                continue
            if name in exclude_names:
                continue
            if name in self._tools:
                continue

            try:
                tool_def = ToolDef.from_callable(method)
                self.add(tool_def)
            except Exception:
                # If we can't create a tool from it (e.g. no signature?), skip.
                continue

    def add(self, tool: ToolDef, *, overwrite: bool = False) -> None:
        """Register a ToolDef."""
        if not overwrite and tool.name in self._tools:
            raise ValueError(f"Tool '{tool.name}' is already registered")
        self._tools[tool.name] = tool

    def tool(
        self,
        *,
        name: Optional[str] = None,
        description: Optional[str] = None,
        parameters: Optional[Dict[str, Any]] = None,
        strict: bool = True,
        overwrite: bool = False,
    ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
        """Decorator that registers a Python callable as a tool.

        If `description` is None, it is taken from the function docstring.
        """

        def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
            resolved_description = description
            if resolved_description is None:
                resolved_description = (inspect.getdoc(func) or "").strip()

            tool_def = ToolDef.from_callable(
                func,
                name=name,
                description=resolved_description,
                parameters=parameters,
                strict=strict,
            )
            self.add(tool_def, overwrite=overwrite)
            return func

        return decorator

    def get(self, name: str) -> ToolDef:
        """Get a tool by name; raise if missing."""
        try:
            return self._tools[name]
        except KeyError as exc:
            raise ToolNotFoundError(f"Tool '{name}' is not registered") from exc

    def maybe_get(self, name: str) -> Optional[ToolDef]:
        """Get a tool by name; return None if missing."""
        return self._tools.get(name)

    def names(self) -> List[str]:
        """Return tool names (in insertion order)."""
        return list(self._tools.keys())

    def __contains__(self, name: str) -> bool:
        return name in self._tools

    def __len__(self) -> int:
        return len(self._tools)

    def __iter__(self) -> Iterator[ToolDef]:
        return iter(self._tools.values())

    def as_openai_tools(self) -> List[OpenAIToolPayload]:
        """Build `tools=[...]` payload for `client.responses.create(...)`."""
        return [self._to_openai_tool_payload(tool) for tool in self._tools.values()]

    def allowed_tools_choice(
        self,
        names: Optional[Iterable[str]] = None,
        *,
        mode: str = "auto",
        validate: bool = True,
    ) -> ToolChoicePayload:
        """Build `tool_choice` payload of type `allowed_tools`."""
        if names is None:
            names = self._tools.keys()

        names_list = list(names)
        if validate:
            unknown = [n for n in names_list if n not in self._tools]
            if unknown:
                raise ToolNotFoundError(f"Unknown tool(s): {', '.join(unknown)}")

        return {
            "type": "allowed_tools",
            "mode": mode,
            "tools": [{"type": "function", "name": n} for n in names_list],
        }

    @staticmethod
    def _to_openai_tool_payload(tool: ToolDef) -> OpenAIToolPayload:
        """Convert ToolDef -> OpenAI function tool payload."""
        return {
            "type": "function",
            "name": tool.name,
            "description": tool.description,
            "parameters": tool.parameters,
            "strict": tool.strict,
        }
