"""Generic tool authorization wrapper.

This module provides framework-agnostic tool authorization that works with
any callable. It's the foundation for all framework-specific adapters.

Example:
    >>> from wl_apdp import authorized_tool, authorization_context, AuthorizationContext
    >>>
    >>> @authorized_tool(resource='Tool::"web_search"', action="execute")
    ... def search_web(query: str) -> str:
    ...     return f"Results for: {query}"
    >>>
    >>> with authorization_context(AuthorizationContext(
    ...     principal="agent-123",
    ...     principal_type="Agent"
    ... )):
    ...     result = search_web("python")  # Checks authorization first
"""

from __future__ import annotations

import functools
import logging
from collections.abc import Callable
from typing import Any, ParamSpec, TypeVar

from .client import WlApdpSyncClient
from .context import get_current_context
from .exceptions import AuthorizationDenied, ConfigurationError
from .models import cedar_action

logger = logging.getLogger("wl_apdp")

P = ParamSpec("P")
R = TypeVar("R")


def authorized_tool(
    resource: str,
    action: str = "execute",
    client: WlApdpSyncClient | None = None,
    base_url: str = "http://localhost:8081/api",
    fail_closed: bool = True,
    on_deny: str = "raise",
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Decorator to add authorization checks to any callable.

    This decorator wraps a function with authorization checks. Before the
    function executes, it checks with wl-apdp whether the current principal
    is allowed to perform the action on the resource.

    Args:
        resource: Cedar resource reference (e.g., 'Tool::"web_search"').
        action: Action name (will be wrapped as 'Action::"<action>"').
        client: Optional pre-configured WlApdpSyncClient.
        base_url: Base URL for wl-apdp API (used if client not provided).
        fail_closed: If True, deny access when no context is available.
        on_deny: What to do on denial: "raise", "return_none", or "log".

    Returns:
        Decorated function with authorization checks.

    Example:
        >>> @authorized_tool(resource='Tool::"calculator"', action="execute")
        ... def calculate(expression: str) -> float:
        ...     return eval(expression)
    """
    if on_deny not in ("raise", "return_none", "log"):
        raise ConfigurationError(f"Invalid on_deny value: {on_deny}")

    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            ctx = get_current_context()

            # Handle missing context
            if ctx is None:
                if fail_closed:
                    raise AuthorizationDenied(
                        "No authorization context available",
                        resource=resource,
                        action=action,
                    )
                else:
                    logger.warning(
                        f"No authorization context for {func.__name__}, allowing (fail_open)"
                    )
                    return func(*args, **kwargs)

            # Build authorization request
            principal = ctx.to_cedar_principal()
            action_ref = cedar_action(action)
            context_dict = ctx.to_context_dict()

            # Check authorization
            authz_client = client or WlApdpSyncClient(base_url=base_url)
            should_close = client is None

            try:
                result = authz_client.authorize(
                    principal=principal,
                    action=action_ref,
                    resource=resource,
                    context=context_dict,
                )
            finally:
                if should_close:
                    authz_client.close()

            if not result.is_allowed:
                logger.warning(
                    f"Authorization denied: {principal} -> {action} -> {resource}"
                )
                if on_deny == "raise":
                    raise AuthorizationDenied(
                        f"Not authorized to {action} on {resource}",
                        response=result,
                        principal=principal,
                        action=action_ref,
                        resource=resource,
                    )
                elif on_deny == "return_none":
                    return None  # type: ignore
                # on_deny == "log" falls through to execute

            logger.debug(f"Authorization allowed: {principal} -> {action} -> {resource}")
            return func(*args, **kwargs)

        return wrapper

    return decorator


def authorized_tool_async(
    resource: str,
    action: str = "execute",
    base_url: str = "http://localhost:8081/api",
    token: str | None = None,
    fail_closed: bool = True,
    on_deny: str = "raise",
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Async version of authorized_tool decorator.

    Args:
        resource: Cedar resource reference.
        action: Action name.
        base_url: Base URL for wl-apdp API.
        token: Optional bearer token.
        fail_closed: If True, deny access when no context is available.
        on_deny: What to do on denial: "raise", "return_none", or "log".

    Returns:
        Decorated async function with authorization checks.
    """
    from .client import WlApdpClient

    if on_deny not in ("raise", "return_none", "log"):
        raise ConfigurationError(f"Invalid on_deny value: {on_deny}")

    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            ctx = get_current_context()

            if ctx is None:
                if fail_closed:
                    raise AuthorizationDenied(
                        "No authorization context available",
                        resource=resource,
                        action=action,
                    )
                else:
                    logger.warning(
                        f"No authorization context for {func.__name__}, allowing (fail_open)"
                    )
                    return await func(*args, **kwargs)  # type: ignore

            principal = ctx.to_cedar_principal()
            action_ref = cedar_action(action)
            context_dict = ctx.to_context_dict()

            async with WlApdpClient(base_url=base_url, token=token) as client:
                result = await client.authorize(
                    principal=principal,
                    action=action_ref,
                    resource=resource,
                    context=context_dict,
                )

            if not result.is_allowed:
                logger.warning(
                    f"Authorization denied: {principal} -> {action} -> {resource}"
                )
                if on_deny == "raise":
                    raise AuthorizationDenied(
                        f"Not authorized to {action} on {resource}",
                        response=result,
                        principal=principal,
                        action=action_ref,
                        resource=resource,
                    )
                elif on_deny == "return_none":
                    return None  # type: ignore

            logger.debug(f"Authorization allowed: {principal} -> {action} -> {resource}")
            return await func(*args, **kwargs)  # type: ignore

        return wrapper  # type: ignore

    return decorator


class AuthorizedCallable:
    """Wrapper class for authorized callables.

    This class wraps any callable with authorization checks. It's useful
    when you need to pass an authorized function around as an object.

    Example:
        >>> def my_tool(query: str) -> str:
        ...     return f"Result: {query}"
        >>>
        >>> authorized = AuthorizedCallable(
        ...     func=my_tool,
        ...     resource='Tool::"my_tool"',
        ...     action="execute"
        ... )
        >>> result = authorized("test")  # Checks authorization first
    """

    def __init__(
        self,
        func: Callable[..., Any],
        resource: str,
        action: str = "execute",
        client: WlApdpSyncClient | None = None,
        base_url: str = "http://localhost:8081/api",
        fail_closed: bool = True,
        on_deny: str = "raise",
    ) -> None:
        """Initialize the authorized callable.

        Args:
            func: The function to wrap.
            resource: Cedar resource reference.
            action: Action name.
            client: Optional pre-configured client.
            base_url: Base URL for wl-apdp API.
            fail_closed: If True, deny access when no context is available.
            on_deny: What to do on denial.
        """
        self.func = func
        self.resource = resource
        self.action = action
        self._client = client
        self._base_url = base_url
        self.fail_closed = fail_closed
        self.on_deny = on_deny

        # Copy function metadata
        functools.update_wrapper(self, func)

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        """Call the wrapped function with authorization check."""
        ctx = get_current_context()

        if ctx is None:
            if self.fail_closed:
                raise AuthorizationDenied(
                    "No authorization context available",
                    resource=self.resource,
                    action=self.action,
                )
            else:
                return self.func(*args, **kwargs)

        principal = ctx.to_cedar_principal()
        action_ref = cedar_action(self.action)
        context_dict = ctx.to_context_dict()

        client = self._client or WlApdpSyncClient(base_url=self._base_url)
        should_close = self._client is None

        try:
            result = client.authorize(
                principal=principal,
                action=action_ref,
                resource=self.resource,
                context=context_dict,
            )
        finally:
            if should_close:
                client.close()

        if not result.is_allowed:
            if self.on_deny == "raise":
                raise AuthorizationDenied(
                    f"Not authorized to {self.action} on {self.resource}",
                    response=result,
                    principal=principal,
                    action=action_ref,
                    resource=self.resource,
                )
            elif self.on_deny == "return_none":
                return None

        return self.func(*args, **kwargs)

    @property
    def __name__(self) -> str:
        """Return the wrapped function's name."""
        return getattr(self.func, "__name__", "unknown")

    @property
    def __doc__(self) -> str | None:
        """Return the wrapped function's docstring."""
        return getattr(self.func, "__doc__", None)
