"""Pack proxy creation for dot notation access.

Creates proxy objects that allow:
- brave.web_search(query="test") - pack access to tool functions
- context7.resolve_library_id() - MCP proxy access
- proxy.list_servers() - introspection of MCP servers

Used by the runner to build the execution namespace.
"""

from __future__ import annotations

from collections import OrderedDict
from functools import wraps
from typing import TYPE_CHECKING, Any

from ot.executor.param_resolver import (
    get_mcp_tool_param_names,
    get_tool_param_names,
    resolve_kwargs,
)
from ot.stats import timed_tool_call

if TYPE_CHECKING:
    from collections.abc import Callable

    from ot.executor.tool_loader import LoadedTools


def _wrap_with_stats(
    pack_name: str, func_name: str, func: Callable[..., Any]
) -> Callable[..., Any]:
    """Wrap a function to record execution-level stats, track calls, and resolve param prefixes."""
    tool_name = f"{pack_name}.{func_name}"

    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        # Resolve abbreviated parameter names (cached lookup)
        if kwargs:
            param_names = get_tool_param_names(tool_name)
            if param_names:
                kwargs = resolve_kwargs(kwargs, param_names)

        with timed_tool_call(tool_name):
            return func(*args, **kwargs)

    return wrapper


def _create_pack_proxy(pack_name: str, pack_funcs: dict[str, Any]) -> Any:
    """Create a pack proxy instance for dot notation access.

    Returns an object that allows pack.func() syntax where func is looked up
    from pack_funcs dict. Each function call is tracked for execution-level stats.
    """

    class PackProxy:
        """Proxy object that provides dot notation access to pack functions."""

        def __init__(self) -> None:
            # Cache wrapped functions to avoid recreating on each access
            self._function_cache: dict[str, Callable[..., Any]] = {}

        def __getattr__(self, name: str) -> Any:
            if name.startswith("_"):
                raise AttributeError(f"Cannot access private attribute '{name}'")

            if name in pack_funcs:
                # Return cached wrapper or create and cache new one
                if name not in self._function_cache:
                    self._function_cache[name] = _wrap_with_stats(
                        pack_name, name, pack_funcs[name]
                    )
                return self._function_cache[name]

            available = ", ".join(sorted(pack_funcs.keys()))
            raise AttributeError(
                f"Function '{name}' not found in pack '{pack_name}'. "
                f"Available: {available}"
            )

    return PackProxy()


def _create_mcp_proxy_pack(server_name: str) -> Any:
    """Create a pack proxy for an MCP server.

    Allows calling proxied MCP tools using dot notation:
    - context7.resolve_library_id(library_name="next.js")

    Each call is tracked for execution-level stats.

    Args:
        server_name: Name of the MCP server.

    Returns:
        Object with __getattr__ that routes to proxy manager.
    """
    from ot.proxy import get_proxy_manager

    class McpProxyPack:
        """Proxy object that routes tool calls to an MCP server."""

        def __init__(self) -> None:
            # Cache callable proxies to avoid recreating on each access
            self._function_cache: dict[str, Callable[..., str]] = {}

        def __getattr__(self, tool_name: str) -> Any:
            if tool_name.startswith("_"):
                raise AttributeError(f"Cannot access private attribute '{tool_name}'")

            if tool_name in self._function_cache:
                return self._function_cache[tool_name]

            def call_proxy_tool(**kwargs: Any) -> str:
                tool_full_name = f"{server_name}.{tool_name}"

                # Resolve abbreviated parameter names (cached lookup)
                if kwargs:
                    param_names = get_mcp_tool_param_names(server_name, tool_name)
                    if param_names:
                        kwargs = resolve_kwargs(kwargs, param_names)

                with timed_tool_call(tool_full_name):
                    proxy = get_proxy_manager()
                    return proxy.call_tool_sync(server_name, tool_name, kwargs)

            self._function_cache[tool_name] = call_proxy_tool
            return call_proxy_tool

    return McpProxyPack()


def _create_proxy_introspection_pack() -> Any:
    """Create the 'proxy' pack for introspection.

    Provides:
    - proxy.list_servers() - List all configured MCP servers with status
    - proxy.list_tools(server="name") - List tools available on a server

    Returns:
        Object with introspection methods.
    """
    from ot.proxy import get_proxy_manager

    class ProxyIntrospectionPack:
        """Provides introspection methods for proxied MCP servers."""

        def list_servers(self) -> list[dict[str, Any]]:
            """List all configured MCP servers with connection status.

            Returns:
                List of dicts with server name, type, enabled, and connected status.
            """
            from ot.config import get_config

            config = get_config()
            proxy = get_proxy_manager()

            servers = []
            for name, cfg in (config.servers or {}).items():
                servers.append(
                    {
                        "name": name,
                        "type": cfg.type,
                        "enabled": cfg.enabled,
                        "connected": name in proxy.servers,
                    }
                )
            return servers

        def list_tools(self, server: str) -> list[dict[str, str]]:
            """List tools available on a proxied MCP server.

            Args:
                server: Name of the MCP server.

            Returns:
                List of dicts with tool name and description.

            Raises:
                ValueError: If server is not connected.
            """
            proxy = get_proxy_manager()

            if server not in proxy.servers:
                available = ", ".join(proxy.servers) or "none"
                raise ValueError(
                    f"Server '{server}' not connected. Available: {available}"
                )

            tools = proxy.list_tools(server)
            return [{"name": t.name, "description": t.description} for t in tools]

    return ProxyIntrospectionPack()


# Cache for execution namespace: key=(registry_id, frozenset of proxy servers)
# Uses OrderedDict for proper LRU eviction
_NAMESPACE_CACHE_MAXSIZE = 10
_namespace_cache: OrderedDict[tuple[int, frozenset[str]], dict[str, Any]] = (
    OrderedDict()
)


def build_execution_namespace(
    registry: LoadedTools,
) -> dict[str, Any]:
    """Build execution namespace with pack proxies for dot notation access.

    Results are cached based on registry identity and proxy server configuration.
    Cache is invalidated when registry changes or proxy servers are added/removed.

    Provides dot notation access to tools:
    - brave.web_search(query="test")  # pack access
    - context7.resolve_library_id()   # MCP proxy access

    Args:
        registry: LoadedTools registry with functions and packs

    Returns:
        Dict suitable for use as exec() globals
    """
    from ot.executor.worker_proxy import WorkerPackProxy
    from ot.proxy import get_proxy_manager

    # Check cache - key is registry identity + current proxy servers
    proxy_mgr = get_proxy_manager()
    cache_key = (id(registry), frozenset(proxy_mgr.servers))

    if cache_key in _namespace_cache:
        # LRU: move to end on access
        _namespace_cache.move_to_end(cache_key)
        return _namespace_cache[cache_key]

    namespace: dict[str, Any] = {}

    # Add pack proxies for dot notation
    for pack_name, pack_funcs in registry.packs.items():
        if isinstance(pack_funcs, WorkerPackProxy):
            # Extension tools already have a proxy - use directly
            namespace[pack_name] = pack_funcs
        else:
            namespace[pack_name] = _create_pack_proxy(pack_name, pack_funcs)

    # Add MCP proxy packs (only if not already defined locally)
    for server_name in proxy_mgr.servers:
        if server_name not in namespace:
            namespace[server_name] = _create_mcp_proxy_pack(server_name)

    # Add proxy introspection pack (always available)
    if "proxy" not in namespace:
        namespace["proxy"] = _create_proxy_introspection_pack()

    # Cache result with LRU eviction
    _namespace_cache[cache_key] = namespace
    while len(_namespace_cache) > _NAMESPACE_CACHE_MAXSIZE:
        _namespace_cache.popitem(last=False)

    return namespace
