# -*- coding: utf-8 -*-
from django.db import models, IntegrityError
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.contrib import auth
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
from django.utils.translation import ugettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from protector.internals import (
    DEFAULT_ROLE,
    ADD_PERMISSION_PERMISSION,
    VIEW_RESTRICTED_OBJECTS,
    VIEW_PERMISSION_NAME,
    VIEW_GENERIC_GROUP_HISTORY,
    VIEW_OWNER_TO_PERM_HISTORY,
    get_user_ctype,
)
from protector.helpers import get_view_permission, check_responsible_reason
from protector.managers import (
    GenericUserToGroupManager,
    OwnerToPermissionManager,
    OwnerPermissionManager,
    UserGroupManager,
    GroupUserManager,
    RestrictedManager,
    GenericGroupManager,
)
from protector.reserved_reasons import MEMBER_FK_UPDATE_REASON, SELF_GROUP_ROLE_UPDATED


#  Form a from clause for all permission related to their owners
#  role of user in group must not be empty
#  if permission roles is empty than it is applied to all roles in group


class AbstractGenericUserToGroup(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='%(class)s_generic_user_relations', on_delete=models.CASCADE
    )
    roles = models.IntegerField(verbose_name=_('roles'), blank=True, null=True)
    group_id = models.PositiveIntegerField(verbose_name=_('group id'))
    group_content_type = models.ForeignKey(
        verbose_name=_('group content type'), to=ContentType, on_delete=models.CASCADE, db_index=False
    )
    group = GenericForeignKey('group_content_type', 'group_id')

    responsible = models.ForeignKey(
        verbose_name=_('responsible'),
        to=settings.AUTH_USER_MODEL, related_name='%(class)s_created_group_relations',
        blank=True, null=True, on_delete=models.SET_NULL
    )

    class Meta:
        abstract = True


class AbstractOwnerToPermission(models.Model):
    object_id = models.PositiveIntegerField(
        verbose_name=_('object id'), null=True, blank=True,
    )
    content_type = models.ForeignKey(
        verbose_name=_('object type'),
        to=ContentType, related_name='%(class)s_restriction_group_relations',
        null=True, blank=True,
        on_delete=models.CASCADE
    )
    content_object = GenericForeignKey('content_type', 'object_id')

    owner_object_id = models.PositiveIntegerField(verbose_name=_('owner id'))
    owner_content_type = models.ForeignKey(
        verbose_name=_('owner type'),
        to=ContentType, related_name='%(class)s_restricted_object_relations',
        on_delete=models.CASCADE
    )
    owner = GenericForeignKey('owner_content_type', 'owner_object_id')

    permission = models.ForeignKey(
        verbose_name=_('permission'),
        to=Permission, related_name='%(class)s_generic_restriction_relations',
        on_delete=models.CASCADE
    )
    responsible = models.ForeignKey(
        verbose_name=_('responsible'),
        to=settings.AUTH_USER_MODEL, related_name='%(class)s_responsible',
        blank=True, null=True,
        on_delete=models.SET_NULL
    )
    roles = models.IntegerField(verbose_name=_('roles'), default=DEFAULT_ROLE)

    class Meta:
        abstract = True


class AbstractBaseHistory(models.Model):
    reason = models.TextField(verbose_name=_('change reason'), blank=False, null=False)
    changed_at = models.DateTimeField(
        _('change date'), auto_now_add=True
    )

    class Meta:
        abstract = True


class GenericGlobalPerm(models.Model):
    """
        This model is for defining template-like permissions
        e.g. Every blog moderator could edit his blog
    """
    content_type = models.ForeignKey(
        ContentType, related_name='global_perms', verbose_name=_('content type'),
        on_delete=models.CASCADE, null=True
    )
    roles = models.IntegerField(verbose_name=_('roles'), default=DEFAULT_ROLE)
    permission = models.ForeignKey(
        to=Permission, verbose_name=_('permission'), on_delete=models.CASCADE
    )

    class Meta:
        verbose_name = _('global group permission')
        verbose_name_plural = _('global group permissions')
        unique_together = ('content_type', 'permission')


class GenericUserToGroup(AbstractGenericUserToGroup):
    """
        This models is used for linking user to any possible group
        User can have only one link to group
        In case of multiple roles bitmasks is used
    """
    date_joined = models.DateTimeField(verbose_name=_('date joined'), auto_now_add=True)

    FIELDS_TO_IGNORE_FOR_HISTORY = ('id', 'date_joined', 'group',)

    objects = GenericUserToGroupManager()

    class Meta:
        verbose_name = _('user to group link')
        verbose_name_plural = _('user to group links')
        unique_together = ('group_content_type', 'group_id', 'user')

    def __unicode__(self):
        return "{app}.{model}.{group_id} - {username}".format(
            app=self.group_content_type.app_label,
            model=self.group_content_type.model,
            group_id=self.group_id,
            username=self.user.username
        )

    def values_to_save_for_history(self):
        # for related fields we want them to end on _id for more accurate perfomance
        fields_to_save = [
            f.name + '_id' if f.related_model else f.name for f in self._meta.get_fields()
            if f.name not in self.FIELDS_TO_IGNORE_FOR_HISTORY
        ]
        return {
            model_key: model_value for model_key, model_value in self.__dict__.items()
            if model_key in fields_to_save
        }

    @check_responsible_reason
    def delete(self, **kwargs):
        history_dict = self.values_to_save_for_history()
        history_dict.update({
            'reason': kwargs.get('reason'),
            'responsible': kwargs.get('responsible'),
            'change_type': HistoryGenericUserToGroup.TYPE_REMOVE,
        })
        HistoryGenericUserToGroup.objects.create(**history_dict)
        return super(GenericUserToGroup, self).delete()

    @check_responsible_reason
    def save(self, *args, **kwargs):
        model_fields = self.values_to_save_for_history()
        model_fields.update({
            'reason': kwargs.get('reason'),
            'change_type': HistoryGenericUserToGroup.TYPE_ADD if not self.pk else HistoryGenericUserToGroup.TYPE_CHANGE,
        })
        HistoryGenericUserToGroup.objects.create(**model_fields)
        try:
            del kwargs['reason']
        except KeyError:
            pass
        super(GenericUserToGroup, self).save(*args, **kwargs)


class HistoryGenericUserToGroup(AbstractBaseHistory, AbstractGenericUserToGroup):
    TYPE_ADD = 1
    TYPE_REMOVE = 2
    TYPE_CHANGE = 3

    CHANGE_TYPES = (
        (TYPE_ADD, 'add user to group'),
        (TYPE_REMOVE, 'remove user from group'),
        (TYPE_CHANGE, 'role changes')
    )

    change_type = models.SmallIntegerField(
        choices=CHANGE_TYPES, null=False, blank=False,
    )

    class Meta:
        verbose_name = _('generic user to group history')
        verbose_name_plural = _('generic user to group histories')
        index_together = ('group_content_type', 'group_id', 'user')
        permissions = (
            (VIEW_GENERIC_GROUP_HISTORY, _('view generic group history')),
        )

    def __str__(self):
        return '{history_id} | initiated by {responsible}, action: {action_type} | {group_name} {group_id}'.\
            format(
                history_id=self.id,
                responsible=self.responsible.username if self.responsible else '',
                action_type=self.change_type,
                group_name=self.group_content_type,
                group_id=self.group_id,
            )

    objects = models.Manager()


class OwnerToPermission(AbstractOwnerToPermission):
    """
        This model is two-way generic many-to-many link from owner_object to owned object
        Multiple links from owner to object is supported i.e. different permissions
    """
    ADD_PERMISSION = ADD_PERMISSION_PERMISSION
    date_issued = models.DateTimeField(verbose_name=_('date issued'), auto_now_add=True)

    FIELDS_TO_IGNORE_FOR_HISTORY = ('id', 'date_issued', 'owner', 'content_object',)

    objects = OwnerToPermissionManager()

    class Meta:
        verbose_name = _('owner to permission link')
        verbose_name_plural = _('owner to permission links')
        index_together = (
            ['owner_content_type', 'owner_object_id'],
            ['content_type', 'object_id', 'permission'],
        )
        unique_together = (
            'content_type', 'object_id', 'owner_content_type', 'owner_object_id', 'permission'
        )
        permissions = (
            (ADD_PERMISSION_PERMISSION, _('add permission')),
            (VIEW_RESTRICTED_OBJECTS, _('view restricted objects')),
        )

    def __unicode__(self):
        if self.object_id is None:
            ctype = None
        else:
            ctype = self.content_type
        result = "{app}.{model}.{pk} ".format(
            app=self.owner_content_type.app_label,
            model=self.owner_content_type.model,
            pk=self.owner_object_id,
        )
        if self.object_id is not None:  # real object not global permission
            result += "- {app}.{model}.{pk}. ".format(
                app=ctype.app_label if ctype else '',
                model=ctype.model if ctype else '',
                pk=self.object_id or '',
            )
        if self.roles:
            result += "Roles {roles}. ".format(roles=self.roles)
        result += "Permission {perm}".format(perm=self.permission.codename)
        return result

    def values_to_save_for_history(self):
        # for related fields we want them to end on _id for more accurate perfomance
        fields_to_save = [
            f.name + '_id' if f.related_model else f.name for f in self._meta.get_fields()
            if f.name not in self.FIELDS_TO_IGNORE_FOR_HISTORY
        ]
        return {
            model_key: model_value for model_key, model_value in self.__dict__.items()
            if model_key in fields_to_save
        }

    @check_responsible_reason
    def delete(self, **kwargs):
        history_dict = self.values_to_save_for_history()
        history_dict.update({
            'reason': kwargs.get('reason'),
            'responsible': kwargs.get('responsible'),
            'change_type': HistoryOwnerToPermission.TYPE_REMOVE,
        })
        HistoryOwnerToPermission.objects.create(**history_dict)
        return super(OwnerToPermission, self).delete()

    @check_responsible_reason
    def save(self, *args, **kwargs):
        # This is made for cases when object_id or content_type are None,
        # as db engines do not take into account NULL fields when talking about uniqueness.
        model_fields = self.values_to_save_for_history()

        if OwnerToPermission.objects.filter(**model_fields).exists():
            raise IntegrityError('Duplicate with kwargs: {}'.format(model_fields))

        model_fields.update({
            'reason': kwargs.get('reason'),
            'change_type': HistoryOwnerToPermission.TYPE_ADD if not self.pk else HistoryOwnerToPermission.TYPE_CHANGE,
        })
        HistoryOwnerToPermission.objects.create(**model_fields)

        if self.owner_content_type == get_user_ctype():
            # Here is a bit of denormalization
            # User is a part of group of his own
            # This is done to drastically improve perm checking performance
            self_role = 1  # role == 1 in user model is assumed to mean user himself
            generic_user_to_group, created = GenericUserToGroup.objects.get_or_create(
                reason=kwargs.get('reason'),
                group_id=self.owner_object_id,
                group_content_type=self.owner_content_type,
                user_id=self.owner_object_id,
                defaults={
                    'responsible': kwargs.get('responsible'),
                    'roles': self_role,
                }
            )
            if not generic_user_to_group.roles & self_role:
                # If (for some reason) user is a part of group of his own without self role (1),
                # we ensure that self role is included
                generic_user_to_group.roles |= self_role
                generic_user_to_group.save(reason=SELF_GROUP_ROLE_UPDATED)
        try:
            del kwargs['reason']
        except KeyError:
            pass
        super(OwnerToPermission, self).save(*args, **kwargs)


class HistoryOwnerToPermission(AbstractBaseHistory, AbstractOwnerToPermission):
    TYPE_ADD = 1
    TYPE_REMOVE = 2
    TYPE_CHANGE = 3

    CHANGE_TYPES = (
        (TYPE_ADD, 'add permission'),
        (TYPE_REMOVE, 'remove permission'),
        (TYPE_CHANGE, 'role changes'),
    )

    change_type = models.SmallIntegerField(
        choices=CHANGE_TYPES, null=False, blank=False,
    )

    class Meta:
        verbose_name = _('owner to permission history')
        verbose_name_plural = _('owner to permission histories')
        permissions = (
            (VIEW_OWNER_TO_PERM_HISTORY, _('view owner to permission history')),
        )

    def __str__(self):
        return (
            '{history_id} | initiated by {responsible}, '
            'action: {action_type} | {group_name} {group_id} for perm {permission}'
        ).format(
            history_id=self.id,
            responsible=self.responsible.username if self.responsible else '',
            action_type=self.change_type,
            group_name=self.owner_content_type,
            group_id=self.owner_object_id,
            permission=self.permission.codename if self.permission else '',
        )

    objects = models.Manager()


class GenericPermsMixin(models.Model):
    """
        Mixin is can be used to easily retrieve all owners of permissions to this object
    """
    permission_relations = GenericRelation(
        OwnerToPermission, content_type_field='owner_content_type',
        object_id_field='owner_object_id'
    )

    permissions = None

    def __init__(self, *args, **kwargs):
        super(GenericPermsMixin, self).__init__(*args, **kwargs)
        self.permissions = OwnerPermissionManager(self)

    class Meta:
        abstract = True


class UserGenericPermsMixin(GenericPermsMixin):
    """
        Mixin is used for replacing standard user authorization mechanism
        Also mimics all methods of standard user and is completely compatible
    """
    is_superuser = models.BooleanField(
        verbose_name=_('superuser status'), default=False,
        help_text=_('Designates that user has all perms')
    )

    @property
    def groups(self):
        return UserGroupManager(self)

    class Meta:
        abstract = True

    def has_perm(self, perm, obj=None):
        if self.is_active and self.is_superuser:
            return True

        for backend in auth.get_backends():
            if not hasattr(backend, 'has_perm'):
                continue
            try:
                if backend.has_perm(self, perm, obj):
                    return True
            except PermissionDenied:
                return False
        return False

    def has_perms(self, perm_list, obj=None):
        """
        Returns True if the user has each of the specified permissions. If
        object is passed, it checks if the user has all required perms for this
        object.
        """
        for perm in perm_list:
            if not self.has_perm(perm, obj):
                return False
        return True

    def has_module_perms(self, app_label):
        """
        Returns True if the user has any permissions in the given app label.
        Uses pretty much the same logic as has_perm, above.
        """
        if self.is_active and self.is_superuser:
            return True

        for backend in auth.get_backends():
            if not hasattr(backend, 'has_module_perms'):
                continue
            try:
                if backend.has_module_perms(self, app_label):
                    return True
            except PermissionDenied:
                return False
        return False


class AbstractGenericGroup(GenericPermsMixin):
    """
        Base model for all Groups
        Inherit your model from that to enable generic group features
    """
    PARTICIPANT = 1
    ROLES = (
        (PARTICIPANT, _('Participant')),
    )
    DEFAULT_ROLE = PARTICIPANT

    """
        You could define a list of pairs "field_name, roles"
        Field name should be a foreign key to user
        to auto add this user to this group.
        This is useful for cases like auto assign permissions to post author
    """
    MEMBER_FOREIGN_KEY_FIELDS = []

    users_relations = GenericRelation(
        GenericUserToGroup, content_type_field='group_content_type',
        object_id_field='group_id'
    )

    objects = GenericGroupManager()

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super(AbstractGenericGroup, self).__init__(*args, **kwargs)
        self.users = GroupUserManager(self)

    def save(self, *args, **kwargs):
        super(AbstractGenericGroup, self).save(*args, **kwargs)
        self._update_member_foreign_key()

    def _update_member_foreign_key(self):
        for field, roles in self.MEMBER_FOREIGN_KEY_FIELDS:
            self.users.add(getattr(self, field), MEMBER_FK_UPDATE_REASON(field), roles=roles)

    def get_roles(self, user):
        try:
            user_roles = self.users_relations.values('roles').get(user=user)['roles']
        except GenericUserToGroup.DoesNotExist:
            return []
        else:
            return get_roles_from_mask(user_roles)


def get_roles_from_mask(mask):
    counter = 0
    result = []
    for val in reversed(bin(mask)[2:]):
        if int(val) == 1:
            result.append(pow(2, counter))
        counter += 1
    return result


class Restriction(MPTTModel, models.Model):
    """
        This model contains resriction hierarchy
    """
    object_id = models.PositiveIntegerField(verbose_name=_('object id'), blank=False, null=False)
    content_type = models.ForeignKey(
        to=ContentType, verbose_name=_('content type'), blank=False, null=False,
        on_delete=models.CASCADE
    )
    restricted_object = GenericForeignKey('content_type', 'object_id')

    parent = TreeForeignKey(
        'self', verbose_name=_('parent object'),
        null=True, blank=True, related_name='children',
        on_delete=models.SET_NULL
    )

    class Meta:
        verbose_name = _('Object restriction')
        verbose_name_plural = _('Objects restrictions')
        unique_together = (('object_id', 'content_type'), )

    def __unicode__(self):
        return '{app}.{model} {pk}'.format(
            app=self.content_type.app_label,
            model=self.content_type.model,
            pk=self.object_id
        )


class Restricted(models.Model):
    """
        Inherit your model from that to enable visiblity restrictions
    """
    VIEW_PERMISSION_NAME = VIEW_PERMISSION_NAME

    restriction_id = models.PositiveIntegerField(
        verbose_name=_('restriction id'), blank=True, null=True
    )
    restriction_content_type = models.ForeignKey(
        verbose_name=_('restriction content type id'),
        to=ContentType, blank=True, null=True, related_name="%(app_label)s_%(class)s_restrictions",
        on_delete=models.SET_NULL
    )
    restriction = GenericForeignKey('restriction_content_type', 'restriction_id')

    objects = RestrictedManager()

    class Meta:
        abstract = True

    @classmethod
    def get_view_permission(cls):
        return get_view_permission()

    def get_parent_object(self):
        return None

    def get_restriction_obj(self):
        return Restriction.objects.get_or_create(
            object_id=self.pk,
            content_type=ContentType.objects.get_for_model(self)
        )[0]

    def get_restriction_descendants(self):
        ctype_dict = {}
        restriction_obj = self.get_restriction_obj()
        descendants = restriction_obj.get_descendants()
        ctype_dict = {}
        for restriction in descendants:
            ctype_id = restriction.content_type_id
            if ctype_id not in ctype_dict:
                ctype_dict[ctype_id] = []
            ctype_dict[ctype_id].append(restriction.object_id)
        return ctype_dict

    def restrict(self):
        # method restricts all objects down by restriction hierarchy
        if self.restriction != self:
            current_restriction_id = self.restriction_id
            current_restriction_ctype_id = self.restriction_content_type_id
            self.restriction = self
            if self.pk is not None:
                ctype_dict = self.get_restriction_descendants()
                for ctype_id, object_ids in ctype_dict.items():
                    ctype = ContentType.objects.get_for_id(ctype_id)
                    objs = ctype.model_class().objects.filter(
                        pk__in=object_ids, restriction_id=current_restriction_id,
                        restriction_content_type_id=current_restriction_ctype_id
                    )
                    objs.update(
                        restriction_id=self.id,
                        restriction_content_type=ContentType.objects.get_for_model(self)
                    )
            self.save()

    def unrestrict(self):
        if self.restriction is None:
            return
        ctype_dict = self.get_restriction_descendants()
        current_restriction_id = self.restriction_id
        current_restriction_ctype_id = self.restriction_content_type_id
        for ctype_id, object_ids in ctype_dict.items():
            ctype = ContentType.objects.get_for_id(ctype_id)
            objs = ctype.model_class().objects.filter(
                pk__in=object_ids, restriction_id=current_restriction_id,
                restriction_content_type_id=current_restriction_ctype_id
            )
            # take only objects that restricted by same object as self
            objs.update(
                restriction_id=None,
                restriction_content_type=None
            )
        self.restriction = None
        self.save()

    def is_visible(self, user=None):
        return self.restriction is None or (user is not None and user.has_perm(
            VIEW_PERMISSION_NAME, self.restriction
        ))

    def is_restricted(self):
        return self.restriction_id is not None

    def inherit_restriction(self):
        parent_object = self.get_parent_object()
        if parent_object is not None and isinstance(parent_object, Restricted):
            self.restriction_id = parent_object.restriction_id
            self.restriction_content_type_id = parent_object.restriction_content_type_id

    def generate_restriction(self):
        parent_object = self.get_parent_object()
        if parent_object is not None and isinstance(parent_object, Restricted):
            parent_restriction = Restriction.objects.get(
                object_id=parent_object.id,
                content_type=ContentType.objects.get_for_model(parent_object)
            )
        else:
            parent_restriction = None
        Restriction.objects.get_or_create(
            object_id=self.pk,
            content_type=ContentType.objects.get_for_model(self),
            defaults={'parent': parent_restriction}
        )

    def save(self, *args, **kwargs):
        created = self.pk is None
        if created:
            self.inherit_restriction()
        super(Restricted, self).save(*args, **kwargs)
        if created and self.pk is not None:
            self.generate_restriction()
            # Create a corresponding restriction object and link it to parent

    @check_responsible_reason
    def add_viewer(self, viewer, reason, responsible=None, roles=None):
        roles = roles or DEFAULT_ROLE
        otp, created = OwnerToPermission.objects.get_or_create(
            object_id=self.pk,
            content_type=ContentType.objects.get_for_model(self),
            owner_object_id=viewer.pk,
            owner_content_type=ContentType.objects.get_for_model(viewer),
            permission=get_view_permission(),
            reason=reason,
            defaults={'responsible': responsible, 'roles': roles}
        )
        if not created and otp.roles != roles:
            otp.roles |= roles
            otp.save(reason=reason)


class PermissionInfo(models.Model):
    permission = models.OneToOneField(to=Permission, related_name='info', on_delete=models.CASCADE)
    description = models.TextField(verbose_name=_('description'), blank=True, null=True)

    class Meta:
        verbose_name = _('permission info')
        verbose_name_plural = _('permissions info')
