from typing import Any, Union

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Group, User
from django.db.models import Model, QuerySet
from grainy.core import Applicator, PermissionSet

from .conf import ANONYMOUS_GROUP, ANONYMOUS_PERMS
from .helpers import int_flags, namespace, str_flags


def check_permissions(
    obj: Union[User, AnonymousUser, Group, Model],
    target: Any,
    permissions: Union[int, str],
    **kwargs: Any,
):
    if not hasattr(obj, "_permissions_util"):
        obj._permissions_util = Permissions(obj)

    return obj._permissions_util.check(target, permissions, **kwargs)


def get_permissions(
    obj: Union[User, AnonymousUser, Group, Model], target: Any, **kwargs: Any
):
    if not hasattr(obj, "_permissions_util"):
        obj._permissions_util = Permissions(obj)
    return obj._permissions_util.get(target, **kwargs)


class Permissions:

    """
    A utility class that will allow you to perform permission checks
    for a user or group on cached permission sets
    """

    def __init__(self, obj: Union[User, AnonymousUser, Group, Model]) -> None:
        """
        Arguments:
            - obj <User|AnonymousUser|Group|Model>
        """

        if not hasattr(obj, "grainy_permissions") and not isinstance(
            obj, AnonymousUser
        ):
            raise ValueError(
                "`obj` needs to be have a `grainy_permissions` relationship"
            )
        self.obj = obj
        self.pset = PermissionSet()
        self.applicator = Applicator(self.pset)
        self.loaded = False
        self.load()  # lgtm[py/init-calls-subclass]

        self.grant_all = isinstance(obj, get_user_model()) and obj.is_superuser

    def load(self, refresh: bool = False) -> None:
        """
        Loads the permission set for the user or group specified in
        `self.obj`

        In case `self.obj` holds an `AnonymousUser` instance, perms are loaded
        from settings.GRAINY_ANONYMOUS_PERMS

        Keyword Arguments:
            - refresh <bool>: if True, permission set will be reloaded if it
                has been loaded before
        """
        if not hasattr(self.obj, "grainy_permissions"):
            if isinstance(self.obj, AnonymousUser):
                # Permission for AnonymousUser instance are loaded from
                # settigns
                if not self.loaded or refresh:
                    self.pset = PermissionSet(ANONYMOUS_PERMS)

                    # apply anonymous group permissions if specified
                    if ANONYMOUS_GROUP:
                        try:
                            group = Group.objects.get(name__iexact=ANONYMOUS_GROUP)
                            self.pset.update(
                                group.grainy_permissions.permission_set().permissions,
                                override=False,
                            )
                        except Group.DoesNotExist:
                            pass

                    self.loaded = True
            return

        if not self.loaded or refresh:
            self.pset = self.obj.grainy_permissions.permission_set()
            if isinstance(self.obj, get_user_model()):
                # if we are loading permissions for user, we need
                # to also merge in permissions from any of the
                # groups the user is a part of
                for group in self.obj.groups.all():
                    self.pset.update(
                        group.grainy_permissions.permission_set().permissions,
                        override=False,
                    )
            self.loaded = True

    def check(
        self,
        target: Any,
        permissions: Union[int, str],
        explicit: bool = False,
        ignore_grant_all: bool = False,
    ) -> bool:
        """
        Check permissions for the specified target

        Arguments:
            - target <object|class|str>: check permissions to this object / namespace
            - permissions <int|str>: permission flags to check

        Keyword Arguments:
            - explicit <bool>: require explicit permissions to the complete target
                namespsace
            - ignore_grant_all <bool>: if True the `grant_all` property will be ignored
                during permission checks. If false, users with the `superuser` status will
                automatically pass all the permission checks.
        """
        if self.grant_all and not ignore_grant_all:
            return True
        return self.pset.check(
            namespace(target), int_flags(permissions), explicit=explicit
        )

    def get(
        self,
        target: Any,
        as_string: bool = False,
        explicit: bool = False,
    ) -> Union[int, str]:
        """
        Returns the permission flags for the specified target

        Arguments:
            - target <object|class|str>: return permissions to this object /
                namespsace

        Keyword Arguments:
            - as_string <bool>: if True returns string flags instead of int flags
            - explicit <bool>: require explicit permissions to the complete target

        Returns:
            - <int>: permission flags
            - <str>: permission flags, if as_string=True
        """
        if as_string:
            return str_flags(
                self.pset.get_permissions(namespace(target), explicit=explicit)
            )
        return self.pset.get_permissions(namespace(target), explicit=explicit)

    def apply(self, data: dict) -> dict:
        """
        Applies permissions to the specified data, removing all content that
        is not permissioned on a READ level

        Arguments:
            - data <dict>

        Returns:
            - dict: sanitized data
        """
        if self.grant_all:
            return data
        self.applicator.pset = self.pset
        return self.pset.apply(data, applicator=self.applicator)

    def instances(
        self,
        model: Union[Model, QuerySet],
        permissions: Union[int, str],
        explicit: bool = False,
        ignore_grant_all: bool = False,
    ) -> list:
        """
        Return a list of all instances of the specified model that
        are permissioned at the specified level

        Arguments:
            - model <Model|QuerySet>
            - permissions <str|int>: permission flag(s)

        Keyword Arguments:
            - explicit <bool>: require explicit permissions to the complete target
                namespsace
            - ignore_grant_all <bool>: if True the `grant_all` property will be ignored
                during permission checks. If false, users with the `superuser` status will
                automatically pass all the permission checks.

        Returns:
            - list: model instances
        """

        if isinstance(model, QuerySet):
            q = model
        else:
            q = model.objects.all()

        return [
            instance
            for instance in q
            if self.check(
                instance,
                permissions,
                explicit=explicit,
                ignore_grant_all=ignore_grant_all,
            )
        ]
