from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Any

from pyidentity.exc import InvalidOperationError
from pyidentity.security.authorization.abc import IAuthorizationPolicyProvider
from pyidentity.security.authorization.exc import AuthorizationError
from pyidentity.security.authorization.handler_context import AuthorizationHandlerContext
from pyidentity.security.claims import Claim

__all__ = ("Authorize",)


def ensure_set(roles: Iterable[str] | str | None) -> set[str]:
    if roles is None:
        return set()
    if isinstance(roles, str):
        roles = roles.replace(" ", "").split(",")
    return set(roles)


class BaseAuthorize(ABC):
    @abstractmethod
    async def __call__(
        self,
        context: AuthorizationHandlerContext,
        provider: IAuthorizationPolicyProvider,
    ) -> None:
        pass


class Authorize(BaseAuthorize):
    def __init__(self, roles: Iterable[str] | str | None = None, policy: str | None = None) -> None:
        """Indicates that the route or router to which this dependency is applied requires the specified authorization.

        :param roles: A list of roles that are allowed to access the resource.
        :param policy: Policy name that determines access to the resource.
        :return:
        :raise InvalidOperationException: If the specified policy name is not found.
        :raise AuthorizationError: If authorization failed.
        """
        self.policy = policy
        self.roles = ensure_set(roles)

    async def __call__(
        self,
        context: AuthorizationHandlerContext,
        provider: IAuthorizationPolicyProvider,
    ) -> None:
        await self._check_policy(context, provider)
        await self._check_roles(context)

    async def _check_roles(self, context: AuthorizationHandlerContext) -> None:
        if context.user is None or (self.roles and not any(context.user.is_in_role(r) for r in self.roles)):
            raise AuthorizationError()

    async def _check_policy(
        self,
        context: AuthorizationHandlerContext,
        provider: IAuthorizationPolicyProvider,
    ) -> None:
        if self.policy is not None:
            policy = provider.get_policy(self.policy)
            if policy is None:
                raise InvalidOperationError(f"The AuthorizationPolicy named: '{self.policy}' was not found.")
            for req in policy.requirements:
                await req.handle(context)
        else:
            if default_policy := provider.get_default_policy():
                for req in default_policy.requirements:
                    await req.handle(context)

        if not context.has_succeeded:
            raise AuthorizationError()


class ClaimAuthorize(BaseAuthorize):
    def __init__(
        self,
        claim_type: str,
        *allowed_values: Any,
        authentication_scheme: str | None = None,
    ) -> None:
        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

        claim_check_predicate = check_claim_with_value if allowed_values else check_claim

        def check_claims(context, identity):
            for claim in identity.claims:
                if claim_check_predicate(claim):
                    context.succeed()
                    break

        def check_claims_with_scheme(context, identity):
            if identity.authentication_type == authentication_scheme:
                check_claims(context, identity)

        has_scheme = authentication_scheme is not None
        self._predicate = check_claims_with_scheme if has_scheme else check_claims

    async def __call__(
        self,
        context: AuthorizationHandlerContext,
        provider: IAuthorizationPolicyProvider,
    ) -> None:
        if context.user:
            for identity in context.user.identities:
                self._predicate(context, identity)

        if not context.has_succeeded:
            raise AuthorizationError()
