"""The @requires() decorator for FastAPI route-level authorization.

Checks authorization before the route handler runs. On denial, raises
a Denied exception which the middleware converts to a 403 response.
"""

from __future__ import annotations

import functools
import inspect
from collections.abc import Awaitable, Callable
from typing import Any

from fastapi import Request

from pyauthz import PyAuthz, Resource, Subject

# Type for the optional resource resolver.
# Receives the request and any path/query params, returns a Resource.
ResourceResolver = Callable[..., Resource | Awaitable[Resource]]


def requires(
    action: str,
    *,
    resource_resolver: ResourceResolver | None = None,
    resource_type: str | None = None,
) -> Callable:
    """Decorator that enforces authorization on a FastAPI route.

    Checks that the request's Subject is authorized to perform the given action.
    If a resource_resolver is provided, it extracts the Resource from the request.
    Otherwise, a default Resource is created from the resource_type or action string.

    Args:
        action: The action to authorize (e.g., "documents:read" or "read").
        resource_resolver: Optional callable to extract a Resource from the request.
        resource_type: Optional resource type override. If not provided and action
            contains a colon, the resource type is inferred from the action string.

    Example:
        @app.get("/documents/{doc_id}")
        @requires("documents:read")
        async def get_document(doc_id: str):
            ...

        @app.delete("/documents/{doc_id}")
        @requires("documents:delete", resource_resolver=resolve_document)
        async def delete_document(doc_id: str):
            ...
    """

    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
            request = _extract_request(args, kwargs)
            rp: PyAuthz = request.state.pyauthz
            subject: Subject = request.state.subject

            # Resolve the resource
            resource = await _resolve_resource(
                request, resource_resolver, resource_type, action, kwargs
            )

            # Authorize — raises Denied on failure
            decision = rp.authorize(subject, action, resource)
            request.state.pyauthz_decision = decision

            return await func(*args, **kwargs)

        @functools.wraps(func)
        def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
            request = _extract_request(args, kwargs)
            rp: PyAuthz = request.state.pyauthz
            subject: Subject = request.state.subject

            # Resolve the resource (sync path)
            resource = _resolve_resource_sync(
                request, resource_resolver, resource_type, action, kwargs
            )

            # Authorize — raises Denied on failure
            decision = rp.authorize(subject, action, resource)
            request.state.pyauthz_decision = decision

            return func(*args, **kwargs)

        if inspect.iscoroutinefunction(func):
            return async_wrapper
        return sync_wrapper

    return decorator


def _extract_request(args: tuple, kwargs: dict) -> Request:
    """Extract the Request object from route handler arguments."""
    # Check kwargs first (most common with FastAPI dependency injection)
    if "request" in kwargs:
        return kwargs["request"]
    # Check positional args
    for arg in args:
        if isinstance(arg, Request):
            return arg
    raise RuntimeError(
        "Could not find Request object in route handler arguments. "
        "Add 'request: Request' as a parameter to your route handler."
    )


async def _resolve_resource(
    request: Request,
    resolver: ResourceResolver | None,
    resource_type_override: str | None,
    action: str,
    kwargs: dict,
) -> Resource:
    """Resolve the Resource for authorization, async-compatible."""
    if resolver is not None:
        result = resolver(request, **{k: v for k, v in kwargs.items() if k != "request"})
        if inspect.isawaitable(result):
            return await result
        return result

    return _default_resource(resource_type_override, action, request)


def _resolve_resource_sync(
    request: Request,
    resolver: ResourceResolver | None,
    resource_type_override: str | None,
    action: str,
    kwargs: dict,
) -> Resource:
    """Resolve the Resource for authorization, sync-only."""
    if resolver is not None:
        result = resolver(request, **{k: v for k, v in kwargs.items() if k != "request"})
        return result

    return _default_resource(resource_type_override, action, request)


def _default_resource(
    resource_type_override: str | None,
    action: str,
    request: Request,
) -> Resource:
    """Create a default Resource when no resolver is provided."""
    # Infer resource type from the action string if it contains a colon
    if resource_type_override:
        rtype = resource_type_override
    elif ":" in action:
        rtype = action.split(":", 1)[0]
    else:
        rtype = "unknown"

    return Resource(
        id=str(request.url.path),
        type=rtype,
    )
