"""Authorization helpers."""

from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from functools import wraps
from typing import Any, ParamSpec, TypeVar


class DomainValidationError(ValueError):
    """Raised when a domain value is invalid."""


class ForbiddenError(PermissionError):
    """Raised when the principal is not authorized to perform the action."""


class Role(str, Enum):
    GUEST = "GUEST"
    ADMIN = "ADMIN"
    READER = "READER"


@dataclass(slots=True, frozen=True)
class Principal:
    name: str
    role: Role
    tenant_id: str

    @classmethod
    def of(cls, name: str, role: Role, tenant_id: str) -> "Principal":
        if not isinstance(name, str) or not name.strip():
            raise DomainValidationError("Principal.name must be a non-empty string")
        if not isinstance(tenant_id, str):
            raise DomainValidationError("Principal.tenant_id must be a string")
        return cls(name=name.strip(), role=role, tenant_id=tenant_id.strip())

    @classmethod
    def guest(cls) -> "Principal":
        return cls(name="guest", role=Role.GUEST, tenant_id="guest")

    @classmethod
    def system(cls) -> "Principal":
        return cls(name="system", role=Role.ADMIN, tenant_id="system")

    def to_dict(self) -> dict[str, str]:
        return {
            "name": self.name,
            "role": self.role.value,
            "tenant_id": self.tenant_id,
        }


ROLE_ORDER = {
    Role.GUEST: 0,
    Role.READER: 1,
    Role.ADMIN: 2,
}

P = ParamSpec("P")
R = TypeVar("R")
PrincipalGetter = Callable[[tuple[Any, ...], dict[str, Any]], Principal]


def has_role(subject: Principal, role: Role) -> bool:
    return subject.role is role


def at_least(subject: Principal, minimum_role: Role) -> bool:
    return ROLE_ORDER[subject.role] >= ROLE_ORDER[minimum_role]


def authorize(subject: Principal, required_role: Role, *, use_hierarchy: bool = True) -> None:
    allowed = at_least(subject, required_role) if use_hierarchy else has_role(subject, required_role)
    if not allowed:
        raise ForbiddenError(f"Requires role {required_role.value}")


def ensure_at_least(subject: Principal, minimum_role: Role) -> None:
    authorize(subject, minimum_role, use_hierarchy=True)


def is_admin(subject: Principal) -> bool:
    return has_role(subject, Role.ADMIN)


def is_reader(subject: Principal) -> bool:
    return has_role(subject, Role.READER)


def _resolve_principal(
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    *,
    principal_getter: PrincipalGetter | None,
) -> Principal:
    if principal_getter is not None:
        subject = principal_getter(args, kwargs)
    elif "principal" in kwargs:
        subject = kwargs["principal"]
    else:
        raise TypeError("require_role could not resolve principal; pass principal=... or principal_getter=...")

    if not isinstance(subject, Principal):
        raise TypeError("require_role expected a Principal instance")

    return subject


def require_role(
    required_role: Role,
    *,
    principal_getter: PrincipalGetter | None = None,
    use_hierarchy: bool = True,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
    """Guard a callable with an explicit principal."""

    def decorator(fn: Callable[P, R]) -> Callable[P, R]:
        @wraps(fn)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            authorize(
                _resolve_principal(args, kwargs, principal_getter=principal_getter),
                required_role,
                use_hierarchy=use_hierarchy,
            )
            return fn(*args, **kwargs)

        return wrapper

    return decorator


__all__ = [
    "DomainValidationError",
    "ForbiddenError",
    "Principal",
    "PrincipalGetter",
    "ROLE_ORDER",
    "Role",
    "at_least",
    "authorize",
    "ensure_at_least",
    "has_role",
    "is_admin",
    "is_reader",
    "require_role",
]
