# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.

# Python
import logging

# Django
from django.db import models
from django.db.models import Q
from django.db.models.aggregates import Max
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

# AWX
from awx.main.models.base import * # noqa

__all__ = [
    'Role',
    'RolePermission',
    'get_user_permissions_on_resource',
    'get_role_permissions_on_resource',
    'ROLE_SINGLETON_SYSTEM_ADMINISTRATOR',
    'ROLE_SINGLETON_SYSTEM_AUDITOR',
]

logger = logging.getLogger('awx.main.models.rbac')

ROLE_SINGLETON_SYSTEM_ADMINISTRATOR='System Administrator'
ROLE_SINGLETON_SYSTEM_AUDITOR='System Auditor'

role_rebuilding_paused = False
roles_needing_rebuilding = set()

ALL_PERMISSIONS = {'create': True, 'read': True, 'update': True, 'delete': True,
                   'write': True, 'scm_update': True, 'use': True, 'execute': True}

class Role(CommonModelNameNotUnique):
    '''
    Role model
    '''

    class Meta:
        app_label = 'main'
        verbose_name_plural = _('roles')
        db_table = 'main_rbac_roles'

    singleton_name = models.TextField(null=True, default=None, db_index=True, unique=True)
    parents = models.ManyToManyField('Role', related_name='children')
    ancestors = models.ManyToManyField('Role', related_name='descendents') # auto-generated by `rebuild_role_ancestor_list`
    members = models.ManyToManyField('auth.User', related_name='roles')
    content_type = models.ForeignKey(ContentType, null=True, default=None)
    object_id = models.PositiveIntegerField(null=True, default=None)
    content_object = GenericForeignKey('content_type', 'object_id')

    def save(self, *args, **kwargs):
        super(Role, self).save(*args, **kwargs)
        self.rebuild_role_ancestor_list()

    def get_absolute_url(self):
        return reverse('api:role_detail', args=(self.pk,))

    @staticmethod
    def pause_role_ancestor_rebuilding():
        '''
        Pauses role ancestor list updating. This is useful when you're making
        many changes to the same roles, for example doing bulk inserts or
        making many changes to the same object in succession.

        Note that the unpause_role_ancestor_rebuilding MUST be called within
        the same execution context (preferably within the same transaction),
        otherwise the RBAC role ancestor hierarchy will not be properly
        updated.
        '''

        global role_rebuilding_paused
        role_rebuilding_paused = True

    @staticmethod
    def unpause_role_ancestor_rebuilding():
        '''
        Unpauses the role ancestor list updating. This will will rebuild all
        roles that need updating since the last call to
        pause_role_ancestor_rebuilding and bring everything back into sync.
        '''
        global role_rebuilding_paused
        global roles_needing_rebuilding
        role_rebuilding_paused = False
        for role in Role.objects.filter(id__in=list(roles_needing_rebuilding)).all():
            role.rebuild_role_ancestor_list()
        roles_needing_rebuilding = set()

    def rebuild_role_ancestor_list(self):
        '''
        Updates our `ancestors` map to accurately reflect all of the ancestors for a role

        You should never need to call this. Signal handlers should be calling
        this method when the role hierachy changes automatically.

        Note that this method relies on any parents' ancestor list being correct.
        '''
        global role_rebuilding_paused, roles_needing_rebuilding

        if role_rebuilding_paused:
            roles_needing_rebuilding.add(self.id)
            return

        actual_ancestors = set(Role.objects.filter(id=self.id).values_list('parents__ancestors__id', flat=True))
        actual_ancestors.add(self.id)
        if None in actual_ancestors:
            actual_ancestors.remove(None)
        stored_ancestors = set(self.ancestors.all().values_list('id', flat=True))

        # If it differs, update, and then update all of our children
        if actual_ancestors != stored_ancestors:
            for id in actual_ancestors - stored_ancestors:
                self.ancestors.add(id)
            for id in stored_ancestors - actual_ancestors:
                self.ancestors.remove(id)

            for child in self.children.all():
                child.rebuild_role_ancestor_list()

    @staticmethod
    def visible_roles(user):
        return Role.objects.filter(Q(descendents__in=user.roles.filter()) | Q(ancestors__in=user.roles.filter()))

    @staticmethod
    def singleton(name):
        try:
            return Role.objects.get(singleton_name=name)
        except Role.DoesNotExist:
            ret = Role.objects.create(singleton_name=name, name=name)
            return ret

    def is_ancestor_of(self, role):
        return role.ancestors.filter(id=self.id).exists()


class RolePermission(CreatedModifiedModel):
    '''
    Defines the permissions a role has
    '''

    class Meta:
        app_label = 'main'
        verbose_name_plural = _('permissions')
        db_table = 'main_rbac_permissions'
        index_together = [
            ('content_type', 'object_id')
        ]

    role = models.ForeignKey(
        Role,
        null=False,
        on_delete=models.CASCADE,
        related_name='permissions',
    )
    content_type = models.ForeignKey(ContentType, null=False, default=None)
    object_id = models.PositiveIntegerField(null=False, default=None)
    resource = GenericForeignKey('content_type', 'object_id')

    create     = models.IntegerField(default = 0)
    read       = models.IntegerField(default = 0)
    write      = models.IntegerField(default = 0)
    delete     = models.IntegerField(default = 0)
    update     = models.IntegerField(default = 0)
    execute    = models.IntegerField(default = 0)
    scm_update = models.IntegerField(default = 0)
    use        = models.IntegerField(default = 0)



def get_user_permissions_on_resource(resource, user):
    '''
    Returns a dict (or None) of the permissions a user has for a given
    resource.

    Note: Each field in the dict is the `or` of all respective permissions
    that have been granted to the roles that are applicable for the given
    user.

    In example, if a user has been granted read access through a permission
    on one role and write access through a permission on a separate role,
    the returned dict will denote that the user has both read and write
    access.
    '''

    qs = RolePermission.objects.filter(
        content_type=ContentType.objects.get_for_model(resource),
        object_id=resource.id,
        role__ancestors__in=user.roles.all()
    )

    res = qs = qs.aggregate(
        create = Max('create'),
        read = Max('read'),
        write = Max('write'),
        update = Max('update'),
        delete = Max('delete'),
        scm_update = Max('scm_update'),
        execute = Max('execute'),
        use = Max('use')
    )
    if res['read'] is None:
        return None
    return res

def get_role_permissions_on_resource(resource, role):
    '''
    Returns a dict (or None) of the permissions a role has for a given
    resource.

    Note: Each field in the dict is the `or` of all respective permissions
    that have been granted to either the role or any descendents of that role.
    '''

    qs = RolePermission.objects.filter(
        content_type=ContentType.objects.get_for_model(resource),
        object_id=resource.id,
        role__ancestors=role
    )

    res = qs = qs.aggregate(
        create = Max('create'),
        read = Max('read'),
        write = Max('write'),
        update = Max('update'),
        delete = Max('delete'),
        scm_update = Max('scm_update'),
        execute = Max('execute'),
        use = Max('use')
    )
    if res['read'] is None:
        return None
    return res
