from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db.models import Q
from django.utils.functional import cached_property


class BasePermissionPolicy:
    """
    A 'permission policy' is an object that handles all decisions about the actions
    users are allowed to perform on a given model. The mechanism by which it does this
    is arbitrary, and may or may not involve the django.contrib.auth Permission model;
    it could be as simple as "allow all users to do everything".

    In this way, admin apps can change their permission-handling logic just by swapping
    to a different policy object, rather than having that logic spread across numerous
    view functions.

    BasePermissionPolicy is an abstract class that all permission policies inherit from.
    The only method that subclasses need to implement is users_with_any_permission;
    all other methods can be derived from that (but in practice, subclasses will probably
    want to override additional methods, either for efficiency or to implement more
    fine-grained permission logic).
    """

    def __init__(self, model):
        self.model = model

    # Basic user permission tests. Most policies are expected to override these,
    # since the default implementation is to query the set of permitted users
    # (which is pretty inefficient).

    def user_has_permission(self, user, action):
        """
        Return whether the given user has permission to perform the given action
        on some or all instances of this model
        """
        return user in self.users_with_permission(action)

    def user_has_any_permission(self, user, actions):
        """
        Return whether the given user has permission to perform any of the given actions
        on some or all instances of this model
        """
        return any(self.user_has_permission(user, action) for action in actions)

    # Operations for retrieving a list of users matching the permission criteria.
    # All policies must implement, at minimum, users_with_any_permission.

    def users_with_any_permission(self, actions):
        """
        Return a queryset of users who have permission to perform any of the given actions
        on some or all instances of this model
        """
        raise NotImplementedError

    def users_with_permission(self, action):
        """
        Return a queryset of users who have permission to perform the given action on
        some or all instances of this model
        """
        return self.users_with_any_permission([action])

    # Per-instance permission tests. In the simplest cases - corresponding to the
    # basic Django permission model - permissions are enforced on a per-model basis
    # and so these methods can simply defer to the per-model tests. Policies that
    # require per-instance permission logic must override, at minimum:
    #     user_has_permission_for_instance
    #     instances_user_has_any_permission_for
    #     users_with_any_permission_for_instance

    def user_has_permission_for_instance(self, user, action, instance):
        """
        Return whether the given user has permission to perform the given action on the
        given model instance
        """
        return self.user_has_permission(user, action)

    def user_has_any_permission_for_instance(self, user, actions, instance):
        """
        Return whether the given user has permission to perform any of the given actions
        on the given model instance
        """
        return any(
            self.user_has_permission_for_instance(user, action, instance)
            for action in actions
        )

    def instances_user_has_any_permission_for(self, user, actions):
        """
        Return a queryset of all instances of this model for which the given user has
        permission to perform any of the given actions
        """
        if self.user_has_any_permission(user, actions):
            return self.model.objects.all()
        else:
            return self.model.objects.none()

    def instances_user_has_permission_for(self, user, action):
        """
        Return a queryset of all instances of this model for which the given user has
        permission to perform the given action
        """
        return self.instances_user_has_any_permission_for(user, [action])

    def users_with_any_permission_for_instance(self, actions, instance):
        """
        Return a queryset of all users who have permission to perform any of the given
        actions on the given model instance
        """
        return self.users_with_any_permission(actions)

    def users_with_permission_for_instance(self, action, instance):
        return self.users_with_any_permission_for_instance([action], instance)


class BlanketPermissionPolicy(BasePermissionPolicy):
    """
    A permission policy that gives everyone (including anonymous users)
    full permission over the given model
    """

    def user_has_permission(self, user, action):
        return True

    def user_has_any_permission(self, user, actions):
        return True

    def users_with_any_permission(self, actions):
        # Here we filter out inactive users from the results, even though inactive users
        # - and for that matter anonymous users - still have permission according to the
        # user_has_permission method. This is appropriate because, for most applications,
        # setting is_active=False is equivalent to deleting the user account; you would
        # not want these accounts to appear in, for example, a dropdown of users to
        # assign a task to. The result here could never be completely logically correct
        # (because it will not include anonymous users), so as the next best thing we
        # return the "least surprise" result.
        return get_user_model().objects.filter(is_active=True)

    def users_with_permission(self, action):
        return get_user_model().objects.filter(is_active=True)


class AuthenticationOnlyPermissionPolicy(BasePermissionPolicy):
    """
    A permission policy that gives all active authenticated users
    full permission over the given model
    """

    def user_has_permission(self, user, action):
        return user.is_authenticated and user.is_active

    def user_has_any_permission(self, user, actions):
        return user.is_authenticated and user.is_active

    def users_with_any_permission(self, actions):
        return get_user_model().objects.filter(is_active=True)

    def users_with_permission(self, action):
        return get_user_model().objects.filter(is_active=True)


class BaseDjangoAuthPermissionPolicy(BasePermissionPolicy):
    """
    Extends BasePermissionPolicy with helper methods useful for policies that need to
    perform lookups against the django.contrib.auth permission model
    """

    def __init__(self, model, auth_model=None):
        # `auth_model` specifies the model to be used for permission record lookups;
        # usually this will match `model` (which specifies the type of instances that
        # `instances_user_has_permission_for` will return), but this may differ when
        # swappable models are in use - for example, an interface for editing user
        # records might use a custom User model but will typically still refer to the
        # permission records for auth.user.
        super().__init__(model)
        self.auth_model = auth_model or self.model
        self.app_label = self.auth_model._meta.app_label
        self.model_name = self.auth_model._meta.model_name

    @cached_property
    def _content_type(self):
        return ContentType.objects.get_for_model(self.auth_model)

    def _get_permission_name(self, action):
        """
        Get the full app-label-qualified permission name (as required by
        user.has_perm(...) ) for the given action on this model
        """
        return "%s.%s_%s" % (self.app_label, action, self.model_name)

    def _get_users_with_any_permission_codenames_filter(self, permission_codenames):
        """
        Given a list of permission codenames, return a filter expression which
        will find all users which have any of those permissions - either
        through group permissions, user permissions, or implicitly through
        being a superuser.
        """
        permissions = Permission.objects.filter(
            content_type=self._content_type, codename__in=permission_codenames
        )
        return (
            Q(is_superuser=True)
            | Q(user_permissions__in=permissions)