"""Decorators for authorization and audit logging.

This module provides decorators that can be applied to any function to add
authorization checks or audit logging.

Example:
    >>> from wl_apdp import authorize, audit_action
    >>>
    >>> @authorize(action="read", resource='Document::"report.pdf"')
    ... def read_document(doc_id: str) -> str:
    ...     return f"Contents of {doc_id}"
    >>>
    >>> @audit_action(action="file_write")
    ... def write_file(path: str, content: str) -> None:
    ...     with open(path, "w") as f:
    ...         f.write(content)
"""

from __future__ import annotations

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

from .client import WlApdpClient, 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 authorize(
    action: str,
    resource: str,
    base_url: str = "http://localhost:8081/api",
    token: str | None = None,
    principal_from_context: bool = True,
    principal: str | None = None,
    on_deny: str = "raise",
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Decorator for pre-authorization checks.

    This decorator checks authorization before the function executes.
    It can get the principal from the authorization context or from
    an explicit parameter.

    Args:
        action: The action to authorize (e.g., "read", "write", "execute").
        resource: Cedar resource reference (e.g., 'Document::"report.pdf"').
        base_url: Base URL for wl-apdp API.
        token: Optional bearer token for authentication.
        principal_from_context: If True, get principal from context.
        principal: Explicit principal reference (overrides context).
        on_deny: What to do on denial: "raise", "return_none", or "log".

    Returns:
        Decorated function with authorization checks.

    Example:
        >>> @authorize(action="execute", resource='Tool::"web_search"')
        ... async def search_web(query: str) -> str:
        ...     return await perform_search(query)
    """
    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]:
        if asyncio.iscoroutinefunction(func):
            @functools.wraps(func)
            async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
                # Determine principal
                auth_principal = principal
                ctx = get_current_context()
                if principal_from_context and ctx:
                    auth_principal = ctx.to_cedar_principal()

                if not auth_principal:
                    if on_deny == "raise":
                        raise AuthorizationDenied(
                            "No principal available for authorization",
                            action=cedar_action(action),
                            resource=resource,
                        )
                    elif on_deny == "return_none":
                        return None  # type: ignore
                    else:
                        logger.warning(f"No principal for {func.__name__}, skipping auth")
                        return await func(*args, **kwargs)

                # Check authorization
                context_dict = ctx.to_context_dict() if ctx else {}
                async with WlApdpClient(base_url=base_url, token=token) as client:
                    result = await client.authorize(
                        principal=auth_principal,
                        action=cedar_action(action),
                        resource=resource,
                        context=context_dict,
                    )

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

                return await func(*args, **kwargs)

            return async_wrapper  # type: ignore
        else:
            @functools.wraps(func)
            def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
                auth_principal = principal
                ctx = get_current_context()
                if principal_from_context and ctx:
                    auth_principal = ctx.to_cedar_principal()

                if not auth_principal:
                    if on_deny == "raise":
                        raise AuthorizationDenied(
                            "No principal available for authorization",
                            action=cedar_action(action),
                            resource=resource,
                        )
                    elif on_deny == "return_none":
                        return None  # type: ignore
                    else:
                        logger.warning(f"No principal for {func.__name__}, skipping auth")
                        return func(*args, **kwargs)

                context_dict = ctx.to_context_dict() if ctx else {}
                with WlApdpSyncClient(base_url=base_url, token=token) as client:
                    result = client.authorize(
                        principal=auth_principal,
                        action=cedar_action(action),
                        resource=resource,
                        context=context_dict,
                    )

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

                return func(*args, **kwargs)

            return sync_wrapper

    return decorator


def audit_action(
    action: str,
    resource_extractor: Callable[..., str] | None = None,
    resource: str | None = None,
    log_result: bool = False,
    log_args: bool = False,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Decorator for post-execution audit logging.

    This decorator logs the execution of a function for audit purposes.
    It captures success/failure, optional result, and any errors.

    Args:
        action: The action name for logging (e.g., "file_write", "api_call").
        resource_extractor: Function to extract resource from arguments.
        resource: Static resource string (used if extractor not provided).
        log_result: If True, include the result in the log (truncated).
        log_args: If True, include the arguments in the log.

    Returns:
        Decorated function with audit logging.

    Example:
        >>> @audit_action(
        ...     action="file_write",
        ...     resource_extractor=lambda path, **_: f'File::"{path}"'
        ... )
        ... def write_file(path: str, content: str) -> None:
        ...     with open(path, "w") as f:
        ...         f.write(content)
    """

    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        if asyncio.iscoroutinefunction(func):
            @functools.wraps(func)
            async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
                ctx = get_current_context()
                principal = ctx.to_cedar_principal() if ctx else "Unknown"

                # Determine resource
                res = resource or "Unknown"
                if resource_extractor:
                    try:
                        res = resource_extractor(*args, **kwargs)
                    except Exception:
                        res = "Unknown"

                log_extra: dict[str, Any] = {
                    "audit": True,
                    "principal": principal,
                    "action": action,
                    "resource": res,
                    "function": func.__name__,
                }

                if log_args:
                    log_extra["args"] = str(args)[:200]
                    log_extra["kwargs"] = str(kwargs)[:200]

                try:
                    result = await func(*args, **kwargs)
                    log_extra["success"] = True
                    if log_result and result is not None:
                        log_extra["result"] = str(result)[:200]
                    logger.info(
                        f"AUDIT: {principal} performed {action} on {res}",
                        extra=log_extra,
                    )
                    return result
                except Exception as e:
                    log_extra["success"] = False
                    log_extra["error"] = str(e)[:500]
                    logger.error(
                        f"AUDIT: {principal} failed {action} on {res}: {e}",
                        extra=log_extra,
                    )
                    raise

            return async_wrapper  # type: ignore
        else:
            @functools.wraps(func)
            def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
                ctx = get_current_context()
                principal = ctx.to_cedar_principal() if ctx else "Unknown"

                res = resource or "Unknown"
                if resource_extractor:
                    try:
                        res = resource_extractor(*args, **kwargs)
                    except Exception:
                        res = "Unknown"

                log_extra: dict[str, Any] = {
                    "audit": True,
                    "principal": principal,
                    "action": action,
                    "resource": res,
                    "function": func.__name__,
                }

                if log_args:
                    log_extra["args"] = str(args)[:200]
                    log_extra["kwargs"] = str(kwargs)[:200]

                try:
                    result = func(*args, **kwargs)
                    log_extra["success"] = True
                    if log_result and result is not None:
                        log_extra["result"] = str(result)[:200]
                    logger.info(
                        f"AUDIT: {principal} performed {action} on {res}",
                        extra=log_extra,
                    )
                    return result
                except Exception as e:
                    log_extra["success"] = False
                    log_extra["error"] = str(e)[:500]
                    logger.error(
                        f"AUDIT: {principal} failed {action} on {res}: {e}",
                        extra=log_extra,
                    )
                    raise

            return sync_wrapper

    return decorator


def require_authorization(
    action: str,
    resource: str,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Simplified decorator that requires authorization or raises.

    This is a convenience wrapper around @authorize with on_deny="raise".

    Args:
        action: The action to authorize.
        resource: Cedar resource reference.

    Returns:
        Decorated function that raises AuthorizationDenied if not allowed.

    Example:
        >>> @require_authorization(action="admin", resource='System::"config"')
        ... def update_config(settings: dict) -> None:
        ...     # Only runs if authorized
        ...     pass
    """
    return authorize(action=action, resource=resource, on_deny="raise")


def optional_authorization(
    action: str,
    resource: str,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Decorator that checks authorization but continues on denial.

    This decorator logs authorization failures but allows the function
    to execute regardless. Useful for soft enforcement or gradual rollout.

    Args:
        action: The action to check.
        resource: Cedar resource reference.

    Returns:
        Decorated function that logs but doesn't block on denial.

    Example:
        >>> @optional_authorization(action="read", resource='Feature::"beta"')
        ... def beta_feature():
        ...     # Always runs, but logs if not authorized
        ...     pass
    """
    return authorize(action=action, resource=resource, on_deny="log")
