from typing import Any, Callable, Literal, Self

from pyidentity import resources
from pyidentity.exc import ArgumentNullError, InvalidOperationError
from pyidentity.security.authorization.abc import IAuthorizationHandler
from pyidentity.security.authorization.handler_context import AuthorizationHandlerContext
from pyidentity.security.authorization.policy import AuthorizationPolicy
from pyidentity.security.authorization.types import AuthorizationHandler
from pyidentity.security.claims import Claim

__all__ = (
    "AssertionRequirement",
    "AuthorizationPolicyBuilder",
    "ClaimsAuthorizationRequirement",
    "DenyAnonymousAuthorizationRequirement",
    "NameAuthorizationRequirement",
    "RolesAuthorizationRequirement",
)


class RolesAuthorizationRequirement(IAuthorizationHandler):
    """Implements an *IAuthorizationHandler* which requires at least one role claim
    whose value must be any of the allowed roles.
    """

    __slots__ = ("_allowed_roles", "_mode")

    def __init__(self, *allowed_roles: str, mode: Literal["all", "any"] = "any") -> None:
        """Creates a new instance of *RolesAuthorizationRequirement*.

        :param allowed_roles: A collection of allowed roles.
        :param mode: Role comparison mode. Defaults to *any*.
                If *any* is selected, at least one role statement is required,
                the value of which must be any of the allowed roles.
                If *all* is specified, then approvals are required for each of the allowed roles.
        """
        if not allowed_roles:
            raise InvalidOperationError(resources.RoleRequirementEmpty)

        self._allowed_roles = allowed_roles
        self._mode = mode

    @property
    def allowed_roles(self) -> tuple[str, ...]:
        """Gets the collection of allowed roles."""
        return self._allowed_roles

    @property
    def mode(self) -> Literal["all", "any"]:
        return self._mode

    async def handle(self, context: AuthorizationHandlerContext) -> None:
        """Makes a decision if authorization is allowed based on a specific requirement.

        :param context: The authorization context.
        :return:
        """
        if context.user and context.user.is_in_roles(*self._allowed_roles, mode=self._mode):
            context.succeed()


class ClaimsAuthorizationRequirement(IAuthorizationHandler):
    """Implements an *IAuthorizationHandler* which requires at least one instance of the specified claim type,
    and, if allowed values are specified, the claim value must be any of the allowed values.
    """

    __slots__ = ("_allowed_values", "_claim_type")

    def __init__(self, claim_type: str, *allowed_values: Any) -> None:
        """Creates a new instance of *ClaimsAuthorizationRequirement*.

        :param claim_type: The claim type that must be present.
        :param allowed_values: Optional list of claim values.
                            If specified, the claim must match one or more of these values.
        """
        if claim_type is None:
            raise ArgumentNullError("claim_type")

        self._claim_type = claim_type
        self._allowed_values = allowed_values

        def check_claim(claim: Claim) -> bool:
            return claim.type.casefold() == claim_type.casefold()

        def check_claim_with_value(claim: Claim) -> bool:
            return check_claim(claim) and claim.value in allowed_values

        self._predicate: Callable[[Claim], bool] = check_claim_with_value if allowed_values else check_claim

    @property
    def claim_type(self) -> str:
        """Gets the claim type that must be present."""
        return self._claim_type

    @property
    def allowed_values(self) -> tuple[Any, ...]:
        """Gets the optional list of claim values, which, if present, the claim must match."""
        return self._allowed_values

    async def handle(self, context: AuthorizationHandlerContext) -> None:
        """Makes a decision if authorization is allowed based on the claims requirements specified.

        :param context: The authorization context.
        :return:
        """
        if context.user:
            for claim in context.user.claims:
                if self._predicate(claim):
                    context.succeed()
                    break


class NameAuthorizationRequirement(IAuthorizationHandler):
    """Implements an *IAuthorizationHandler* which requires the current username must match the specified value."""

    __slots__ = ("_required_name",)

    def __init__(self, required_name: str) -> None:
        """Constructs a new instance of *NameAuthorizationRequirement*.

        :param required_name: The required name that the current user must have.
        """
        if required_name is None:
            raise ArgumentNullError("required_name")

        self._required_name = required_name

    @property
    def required_name(self) -> str:
        """Gets the required name that the current user must have."""
        return self._required_name

    async def handle(self, context: AuthorizationHandlerContext) -> None:
        """Makes a decision if authorization is allowed based on a specific requirement.

        :param context: The authorization context.
        :return:
        """
        if context.user:
            for identity in context.user.identities:
                if self._required_name == identity.name:
                    context.succeed()
                    break


class AssertionRequirement(IAuthorizationHandler):
    """Implements an *IAuthorizationHandler* that takes a user specified assertion."""

    __slots__ = ("_handler",)

    def __init__(self, handler: AuthorizationHandler) -> None:
        """Creates a new instance of *AssertionRequirement*.

        :param handler: Function that is called to handle this requirement.
        """
        if handler is None:
            raise ArgumentNullError("handler")

        self._handler = handler

    @property
    def handler(self) -> AuthorizationHandler:
        """Function that is called to handle this requirement."""
        return self._handler

    async def handle(self, context: AuthorizationHandlerContext) -> None:
        """Calls *AssertionRequirement.handler* to see if authorization is allowed.

        :param context: The authorization information.
        :return:
        """
        if await self._handler(context):
            context.succeed()


class DenyAnonymousAuthorizationRequirement(IAuthorizationHandler):
    """Implements an *IAuthorizationHandler* which requires the current user must be authenticated."""

    async def handle(self, context: AuthorizationHandlerContext) -> None:
        """Makes a decision if authorization is allowed based on a specific requirement.

        :param context: The authorization context.
        :return:
        """
        if (
            context.user is not None
            and context.user.identity is not None
            and any(True for identity in context.user.identities if identity.is_authenticated)
        ):
            context.succeed()


class AuthorizationPolicyBuilder:
    __slots__ = ("_requirements", "authentication_schemes")

    def __init__(self) -> None:
        self._requirements: list[IAuthorizationHandler] = []
        self.authentication_schemes: list[str] = []

    def add_authentication_schemes(self, *schemes: str) -> Self:
        """Adds the specified authentication *schemes* to the *authentication_schemes* for this instance.

        :param schemes: The schemes to add.
        :return: A reference to this instance after the operation has completed.
        """
        self.authentication_schemes.extend(schemes)
        return self

    def add_requirements(self, *requirements: IAuthorizationHandler) -> Self:
        """Adds the specified requirements to the for this instance.

        :param requirements: The authorization requirements to add.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.extend(requirements)
        return self

    def require_claim(self, claim_type: str, *allowed_values: Any) -> Self:
        """Adds *ClaimsAuthorizationRequirement* to the current instance
        which requires that the current user
        has the specified claim and that the claim value must be one of the allowed values.

        :param claim_type: The claim type required.
        :param allowed_values: Optional list of claim values.
                            If specified, the claim must match one or more of these values.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(ClaimsAuthorizationRequirement(claim_type, *allowed_values))
        return self

    def require_role(self, *roles: str) -> Self:
        """Adds a *RolesAuthorizationRequirement* to the current instance
        which enforces that the current user must have at least one of the specified roles.

        :param roles: The allowed roles.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(RolesAuthorizationRequirement(*roles, mode="any"))
        return self

    def require_roles(self, *roles: str) -> Self:
        """Adds a *RolesAuthorizationRequirement* to the current instance
        which enforces that the current user must have at all the specified roles.

        :param roles: The allowed roles.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(RolesAuthorizationRequirement(*roles, mode="all"))
        return self

    def require_username(self, name: str) -> Self:
        """Adds a *NameAuthorizationRequirement* to the current instance
        which enforces that the current user matches the specified name.

        :param name: The username the current user must have.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(NameAuthorizationRequirement(name))
        return self

    def require_assertion(self, handler: AuthorizationHandler) -> Self:
        """Adds an *AssertionRequirement* to the current instance.

        :param handler: The handler to evaluate during authorization.
        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(AssertionRequirement(handler))
        return self

    def require_authenticated_user(self) -> Self:
        """Adds *DenyAnonymousAuthorizationRequirement* to the current instance
        which enforces that the current user is authenticated.

        :return: A reference to this instance after the operation has completed.
        """
        self._requirements.append(DenyAnonymousAuthorizationRequirement())
        return self

    def build(self) -> AuthorizationPolicy:
        """Builds a new *AuthorizationPolicy* from the requirements in this instance.

        :return: A new *AuthorizationPolicy* built from the requirements in this instance.
        """
        return AuthorizationPolicy(self._requirements, self.authentication_schemes)
