# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from django.contrib.contenttypes.models import ContentType

from django_rbac import utils as rbac_utils
from django_rbac.managers import AbstractBaseManager
from django_rbac.managers import RoleManager as BaseRoleManager
from django_rbac.models import ADMIN_OP
from django_rbac.utils import get_operation


class OrganizationalUnitManager(AbstractBaseManager):
    def get_by_natural_key(self, slug):
        return self.get(slug=slug)


class RoleManager(BaseRoleManager):
    def get_admin_role(
        self,
        instance,
        name,
        slug,
        ou=None,
        operation=ADMIN_OP,
        update_name=False,
        update_slug=False,
        permissions=(),
        self_administered=False,
        create=True,
    ):
        '''Get or create the role of manager's of this object instance'''
        kwargs = {}
        assert not ou or isinstance(
            instance, ContentType
        ), 'get_admin_role(ou=...) can only be used with ContentType instances: %s %s %s' % (
            name,
            ou,
            instance,
        )

        # Does the permission need to be scoped by ou ? Yes if the target is a
        # ContentType and ou is given. It's a general administration right upon
        # all instance of a ContentType, eventually scoped to the given ou.
        defaults = {}
        if isinstance(instance, ContentType):
            if ou:
                kwargs['ou'] = ou
            else:
                kwargs['ou__isnull'] = True
        else:  # for non ContentType instances, OU must be set to NULL, always.
            defaults['ou'] = None
        # find an operation matching the template
        op = get_operation(operation)
        Permission = rbac_utils.get_permission_model()
        if create:
            perm, _ = Permission.objects.update_or_create(
                operation=op,
                target_ct=ContentType.objects.get_for_model(instance),
                target_id=instance.pk,
                defaults=defaults,
                **kwargs,
            )
        else:
            try:
                perm = Permission.objects.get(
                    operation=op,
                    target_ct=ContentType.objects.get_for_model(instance),
                    target_id=instance.pk,
                    **kwargs,
                )
            except Permission.DoesNotExist:
                return None

        # in which ou do we put the role ?
        if ou:
            mirror_role_ou = ou
        elif getattr(instance, 'ou', None):
            mirror_role_ou = instance.ou
        else:
            mirror_role_ou = None
        admin_role = self.get_mirror_role(
            perm,
            name,
            slug,
            ou=mirror_role_ou,
            update_name=update_name,
            update_slug=update_slug,
            create=create,
        )

        if not admin_role:
            return None

        permissions = set(permissions)
        permissions.add(perm)
        if self_administered:
            self_perm = admin_role.add_self_administration()
            permissions.add(self_perm)
        if set(admin_role.permissions.all()) != permissions:
            admin_role.permissions.set(permissions)
        return admin_role

    def get_mirror_role(
        self, instance, name, slug, ou=None, update_name=False, update_slug=False, create=True
    ):
        """Get or create a role which mirrors another model, for example a
        permission.
        """
        ct = ContentType.objects.get_for_model(instance)
        update_fields = {}
        kwargs = {}
        if ou:
            update_fields['ou'] = ou
        else:
            update_fields['ou'] = None
        if update_name:
            update_fields['name'] = name
        if update_slug:
            update_fields['slug'] = slug

        if create:
            role, _ = self.prefetch_related('permissions').update_or_create(
                admin_scope_ct=ct, admin_scope_id=instance.pk, defaults=update_fields, **kwargs
            )
        else:
            try:
                role = self.prefetch_related('permissions').get(
                    admin_scope_ct=ct, admin_scope_id=instance.pk, **kwargs
                )
            except self.model.DoesNotExist:
                return None
            for field, value in update_fields.items():
                setattr(role, field, value)
            role.save(update_fields=update_fields)
        return role

    def get_by_natural_key(self, slug, ou_natural_key, service_natural_key):
        kwargs = {'slug': slug}
        if ou_natural_key is None:
            kwargs['ou__isnull'] = True
        else:
            OU = rbac_utils.get_ou_model()
            try:
                ou = OU.objects.get_by_natural_key(*ou_natural_key)
            except OU.DoesNotExist:
                raise self.model.DoesNotExist
            kwargs['ou'] = ou
        if service_natural_key is None:
            kwargs['service__isnull'] = True
        else:
            # XXX: prevent an import loop
            from authentic2.models import Service

            try:
                service = Service.objects.get_by_natural_key(*service_natural_key)
            except Service.DoesNotExist:
                raise self.model.DoesNotExist
            kwargs['service'] = service
        return self.get(**kwargs)
