from __future__ import absolute_import, unicode_literals

import logging

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext, ugettext_lazy as _

from common.utils import return_attrib, return_related
from permissions import Permission
from permissions.models import StoredPermission

from .exceptions import PermissionNotValidForClass
from .classes import ModelPermission

logger = logging.getLogger(__name__)


class AccessControlListManager(models.Manager):
    """
    Implement a 3 tier permission system, involving a permissions, an actor
    and an object
    """
    def check_access(self, permissions, user, obj, related=None):
        if user.is_superuser or user.is_staff:
            logger.debug(
                'Permissions "%s" on "%s" granted to user "%s" as superuser '
                'or staff', permissions, obj, user
            )
            return True

        try:
            return Permission.check_permissions(
                requester=user, permissions=permissions
            )
        except PermissionDenied:
            try:
                stored_permissions = [
                    permission.stored_permission for permission in permissions
                ]
            except TypeError:
                # Not a list of permissions, just one
                stored_permissions = (permissions.stored_permission,)

            if related:
                obj = return_attrib(obj, related)

            try:
                parent_accessor = ModelPermission.get_inheritance(
                    model=obj._meta.model
                )
            except AttributeError:
                # AttributeError means non model objects: ie Statistics
                # These can't have ACLs so we raise PermissionDenied
                raise PermissionDenied(_('Insufficient access for: %s') % obj)
            except KeyError:
                pass
            else:
                try:
                    return self.check_access(
                        obj=getattr(obj, parent_accessor),
                        permissions=permissions, user=user
                    )
                except PermissionDenied:
                    pass

            user_roles = []
            for group in user.groups.all():
                for role in group.roles.all():
                    if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
                        logger.debug(
                            'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL',
                            permissions, obj, user, role
                        )
                        return True

                    user_roles.append(role)

            if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
                logger.debug(
                    'Permissions "%s" on "%s" denied for user "%s"',
                    permissions, obj, user
                )
                raise PermissionDenied(ugettext('Insufficient access for: %s') % obj)

            logger.debug(
                'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
                permissions, obj, user, user_roles
            )

    def filter_by_access(self, permission, user, queryset):
        if user.is_superuser or user.is_staff:
            logger.debug(
                'Unfiltered queryset returned to user "%s" as superuser or staff',
                user
            )
            return queryset

        try:
            Permission.check_permissions(
                requester=user, permissions=(permission,)
            )
        except PermissionDenied:
            user_roles = []
            for group in user.groups.all():
                for role in group.roles.all():
                    user_roles.append(role)

            try:
                parent_accessor = ModelPermission.get_inheritance(
                    model=queryset.model
                )
            except KeyError:
                parent_acl_query = Q()
            else:
                instance = queryset.first()
                if instance:
                    parent_object = return_related(instance, parent_accessor)

                    try:
                        # Try to see if parent_object is a function
                        parent_object()
                    except TypeError:
                        # Is not a function, try it as a field
                        parent_content_type = ContentType.objects.get_for_model(
                            parent_object
                        )
                        parent_queryset = self.filter(
                            content_type=parent_content_type, role__in=user_roles,
                            permissions=permission.stored_permission
                        )
                        parent_acl_query = Q(
                            **{
                                '{}__pk__in'.format(
                                    parent_accessor
                                ): parent_queryset.values_list(
                                    'object_id', flat=True
                                )
                            }
                        )
                    else:
                        # Is a function. Can't perform Q object filtering.
                        # Perform iterative filtering.
                        result = []
                        for entry in queryset:
                            try:
                                self.check_access(permissions=permission, user=user, obj=entry)
                            except PermissionDenied:
                                pass
                            else:
                                result.append(entry.pk)

                        return queryset.filter(pk__in=result)
                else:
                    parent_acl_query = Q()

            # Directly granted access
            content_type = ContentType.objects.get_for_model(queryset.model)
            acl_query = Q(pk__in=self.filter(
                content_type=content_type, role__in=user_roles,
                permissions=permission.stored_permission
            ).values_list('object_id', flat=True))
            logger.debug(
                'Filtered queryset returned to user "%s" based on roles "%s"',
                user, user_roles
            )

            return queryset.filter(parent_acl_query | acl_query)
        else:
            return queryset

    def get_inherited_permissions(self, role, obj):
        try:
            instance = obj.first()
        except AttributeError:
            instance = obj
        else:
            if not instance:
                return StoredPermission.objects.none()

        try:
            parent_accessor = ModelPermission.get_inheritance(type(instance))
        except KeyError:
            return StoredPermission.objects.none()
        else:
            parent_object = return_attrib(instance, parent_accessor)
            content_type = ContentType.objects.get_for_model(parent_object)
            try:
                return self.get(
                    role=role, content_type=content_type,
                    object_id=parent_object.pk
                ).permissions.all()
            except self.model.DoesNotExist:
                return StoredPermission.objects.none()

    def grant(self, permission, role, obj):
        class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
        if permission not in class_permissions:
            raise PermissionNotValidForClass

        content_type = ContentType.objects.get_for_model(model=obj)
        acl, created = self.get_or_create(
            content_type=content_type, object_id=obj.pk,
            role=role
        )

        acl.permissions.add(permission.stored_permission)

    def revoke(self, permission, role, obj):
        content_type = ContentType.objects.get_for_model(model=obj)
        acl, created = self.get_or_create(
            content_type=content_type, object_id=obj.pk,
            role=role
        )

        acl.permissions.remove(permission.stored_permission)

        if acl.permissions.count() == 0:
            acl.delete()
