# 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 collections import namedtuple

from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.text import slugify
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _

from authentic2.decorators import errorcollector
from django_rbac import utils as rbac_utils
from django_rbac.models import (
    VIEW_OP,
    Operation,
    OrganizationalUnitAbstractBase,
    PermissionAbstractBase,
    RoleAbstractBase,
    RoleParentingAbstractBase,
)

try:
    from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
except ImportError:
    # Django < 1.8
    from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation

from authentic2.decorators import GlobalCache

from . import app_settings, fields, managers


class OrganizationalUnit(OrganizationalUnitAbstractBase):

    RESET_LINK_POLICY = 0
    MANUAL_PASSWORD_POLICY = 1

    USER_ADD_PASSWD_POLICY_CHOICES = (
        (RESET_LINK_POLICY, _('Send reset link')),
        (MANUAL_PASSWORD_POLICY, _('Manual password definition')),
    )

    USER_CAN_RESET_PASSWD_CHOICES = (
        (None, _("System default")),
        (True, _("Yes")),
        (False, _("No")),
    )

    PolicyValue = namedtuple(
        'PolicyValue',
        ['generate_password', 'reset_password_at_next_login', 'send_mail', 'send_password_reset'],
    )

    USER_ADD_PASSWD_POLICY_VALUES = {
        RESET_LINK_POLICY: PolicyValue(False, False, False, True),
        MANUAL_PASSWORD_POLICY: PolicyValue(False, False, True, False),
    }

    username_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Username is unique'))
    email_is_unique = models.BooleanField(blank=True, default=False, verbose_name=_('Email is unique'))
    default = fields.UniqueBooleanField(verbose_name=_('Default organizational unit'))

    validate_emails = models.BooleanField(blank=True, default=False, verbose_name=_('Validate emails'))

    show_username = models.BooleanField(blank=True, default=True, verbose_name=_('Show username'))

    admin_perms = GenericRelation(
        rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id'
    )

    user_can_reset_password = models.NullBooleanField(
        verbose_name=_('Users can reset password'), choices=USER_CAN_RESET_PASSWD_CHOICES
    )

    user_add_password_policy = models.IntegerField(
        verbose_name=_('User creation password policy'), choices=USER_ADD_PASSWD_POLICY_CHOICES, default=0
    )

    clean_unused_accounts_alert = models.PositiveIntegerField(
        verbose_name=_('Days after which the user receives an account deletion alert'),
        validators=[
            MinValueValidator(
                30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.')
            )
        ],
        null=True,
        blank=True,
    )

    clean_unused_accounts_deletion = models.PositiveIntegerField(
        verbose_name=_('Delay in days before cleaning unused accounts'),
        validators=[
            MinValueValidator(
                30, _('Ensure that this value is greater than 30 days, or leave blank for deactivating.')
            )
        ],
        null=True,
        blank=True,
    )

    objects = managers.OrganizationalUnitManager()

    class Meta:
        verbose_name = _('organizational unit')
        verbose_name_plural = _('organizational units')
        ordering = ('name',)
        unique_together = (
            ('name',),
            ('slug',),
        )

    def clean(self):
        # if we set this ou as the default one, we must unset the other one if
        # there is
        if self.default:
            qs = self.__class__.objects.filter(default=True)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            qs.update(default=None)
        if self.pk and not self.default and self.__class__.objects.get(pk=self.pk).default:
            raise ValidationError(
                _(
                    'You cannot unset this organizational '
                    'unit as the default, but you can set '
                    'another one as the default.'
                )
            )
        if bool(self.clean_unused_accounts_alert) ^ bool(self.clean_unused_accounts_deletion):
            raise ValidationError(_('Deletion and alert delays must be set together.'))
        if (
            self.clean_unused_accounts_alert
            and self.clean_unused_accounts_alert >= self.clean_unused_accounts_deletion
        ):
            raise ValidationError(_('Deletion alert delay must be less than actual deletion delay.'))
        super().clean()

    def get_admin_role(self):
        """Get or create the generic admin role for this organizational
        unit.
        """
        name = _('Managers of "{ou}"').format(ou=self)
        slug = f'_a2-managers-of-{self.slug}'
        return Role.objects.get_admin_role(
            instance=self,
            name=name,
            slug=slug,
            operation=VIEW_OP,
            update_name=True,
            update_slug=True,
            create=True,
        )

    def delete(self, *args, **kwargs):
        Permission.objects.filter(ou=self).delete()
        return super(OrganizationalUnitAbstractBase, self).delete(*args, **kwargs)

    def natural_key(self):
        return [self.slug]

    @classmethod
    @GlobalCache(timeout=5)
    def cached(cls):
        return cls.objects.all()

    def export_json(self):
        return {
            'uuid': self.uuid,
            'slug': self.slug,
            'name': self.name,
            'description': self.description,
            'default': self.default,
            'email_is_unique': self.email_is_unique,
            'username_is_unique': self.username_is_unique,
            'validate_emails': self.validate_emails,
        }

    def __str__(self):
        return self.name


OrganizationalUnit._meta.natural_key = [['uuid'], ['slug'], ['name']]


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

    mirror_roles = GenericRelation(
        rbac_utils.get_role_model_name(),
        content_type_field='admin_scope_ct',
        object_id_field='admin_scope_id',
    )


Permission._meta.natural_key = [
    ['operation', 'ou', 'target'],
    ['operation', 'ou__isnull', 'target'],
]


class Role(RoleAbstractBase):
    admin_scope_ct = models.ForeignKey(
        to='contenttypes.ContentType',
        null=True,
        blank=True,
        verbose_name=_('administrative scope content type'),
        on_delete=models.CASCADE,
    )
    admin_scope_id = models.PositiveIntegerField(
        verbose_name=_('administrative scope id'), null=True, blank=True
    )
    admin_scope = GenericForeignKey('admin_scope_ct', 'admin_scope_id')
    service = models.ForeignKey(
        to='authentic2.Service',
        verbose_name=_('service'),
        null=True,
        blank=True,
        related_name='roles',
        on_delete=models.CASCADE,
    )
    external_id = models.TextField(verbose_name=_('external id'), blank=True, db_index=True)

    admin_perms = GenericRelation(
        rbac_utils.get_permission_model_name(), content_type_field='target_ct', object_id_field='target_id'
    )

    can_manage_members = models.BooleanField(
        default=True, verbose_name=_('Allow adding or deleting role members')
    )

    def get_admin_role(self, create=True):
        from . import utils

        if app_settings.ROLE_ADMIN_RESTRICT_TO_OU_USERS:
            view_user_perm = utils.get_view_user_perm(ou=self.ou)
        else:
            view_user_perm = utils.get_view_user_perm()

        admin_role = self.__class__.objects.get_admin_role(
            self,
            name=_('Managers of role "{role}"').format(role=str(self)),
            slug=f'_a2-managers-of-role-{slugify(str(self))}',
            permissions=(view_user_perm,),
            self_administered=True,
            update_name=True,
            update_slug=True,
            create=create,
            operation=MANAGE_MEMBERS_OP,
        )
        return admin_role

    def validate_unique(self, exclude=None):
        errors = {}

        with errorcollector(errors):
            super().validate_unique(exclude=exclude)

        exclude = exclude or []

        if 'name' not in exclude:
            qs = self.__class__.objects.filter(name=self.name, ou=self.ou)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.exists():
                errors.setdefault('name', []).append(_('Name already used'))

        if 'slug' not in exclude:
            qs = self.__class__.objects.filter(slug=self.slug, ou=self.ou)
            if self.pk:
                qs = qs.exclude(pk=self.pk)
            if qs.exists():
                errors.setdefault('slug', []).append(_('Slug already used'))

        if errors:
            raise ValidationError(errors)

    def save(self, *args, **kwargs):
        # Service roles can only be part of the same ou as the service
        if self.service:
            self.ou = self.service.ou
        result = super().save(*args, **kwargs)
        self.get_admin_role(create=False)
        return result

    def has_self_administration(self, op=None):
        if not op:
            op = MANAGE_MEMBERS_OP
        Permission = rbac_utils.get_permission_model()
        operation = rbac_utils.get_operation(op)
        self_perm, created = Permission.objects.get_or_create(
            operation=operation,
            target_ct=ContentType.objects.get_for_model(self),
            target_id=self.pk,
            ou__is_null=True,
        )
        return self.permissions.filter(pk=self_perm.pk).exists()

    def add_self_administration(self, op=None):
        'Add permission to role so that it is self-administered'
        if not op:
            op = MANAGE_MEMBERS_OP
        Permission = rbac_utils.get_permission_model()
        operation = rbac_utils.get_operation(op)
        self_perm, created = Permission.objects.get_or_create(
            operation=operation, target_ct=ContentType.objects.get_for_model(self), target_id=self.pk
        )
        self.permissions.through.objects.get_or_create(role=self, permission=self_perm)
        return self_perm

    def is_internal(self):
        return self.slug.startswith('_')

    objects = managers.RoleManager()

    class Meta:
        verbose_name = _('role')
        verbose_name_plural = _('roles')
        ordering = (
            'ou',
            'service',
            'name',
        )
        unique_together = (('admin_scope_ct', 'admin_scope_id'),)

    def natural_key(self):
        return [
            self.slug,
            self.ou and self.ou.natural_key(),
            self.service and self.service.natural_key(),
        ]

    def to_json(self):
        return {
            'uuid': self.uuid,
            'name': self.name,
            'slug': self.slug,
            'is_admin': bool(self.admin_scope_ct and self.admin_scope_id),
            'is_service': bool(self.service),
            'ou__uuid': self.ou.uuid if self.ou else None,
            'ou__name': self.ou.name if self.ou else None,
            'ou__slug': self.ou.slug if self.ou else None,
        }

    def export_json(self, attributes=False, parents=False, permissions=False):
        d = {
            'uuid': self.uuid,
            'slug': self.slug,
            'name': self.name,
            'description': self.description,
            'external_id': self.external_id,
            'ou': self.ou and self.ou.natural_key_json(),
            'service': self.service and self.service.natural_key_json(),
        }

        if attributes:
            for attribute in self.attributes.all():
                d.setdefault('attributes', []).append(attribute.to_json())

        if parents:
            RoleParenting = rbac_utils.get_role_parenting_model()
            for parenting in RoleParenting.objects.filter(child_id=self.id, direct=True):
                d.setdefault('parents', []).append(parenting.parent.natural_key_json())

        if permissions:
            for perm in self.permissions.all():
                d.setdefault('permissions', []).append(perm.export_json())

        return d


Role._meta.natural_key = [
    ['uuid'],
    ['slug', 'ou__isnull', 'service__isnull'],
    ['name', 'ou__isnull', 'service__isnull'],
    ['slug', 'ou', 'service'],
    ['name', 'ou', 'service'],
    ['slug', 'ou', 'service__isnull'],
    ['name', 'ou', 'service__isnull'],
]


class RoleParenting(RoleParentingAbstractBase):
    class Meta(RoleParentingAbstractBase.Meta):
        verbose_name = _('role parenting relation')
        verbose_name_plural = _('role parenting relations')

    def __str__(self):
        return '{} {}> {}'.format(self.parent.name, '-' if self.direct else '~', self.child.name)


class RoleAttribute(models.Model):
    KINDS = (('string', _('string')),)
    role = models.ForeignKey(
        to=Role, verbose_name=_('role'), related_name='attributes', on_delete=models.CASCADE
    )
    name = models.CharField(max_length=64, verbose_name=_('name'))
    kind = models.CharField(max_length=32, choices=KINDS, verbose_name=_('kind'))
    value = models.TextField(verbose_name=_('value'))

    class Meta:
        verbose_name = 'role attribute'
        verbose_name_plural = _('role attributes')
        unique_together = (('role', 'name', 'kind', 'value'),)

    def to_json(self):
        return {'name': self.name, 'kind': self.kind, 'value': self.value}


GenericRelation(Permission, content_type_field='target_ct', object_id_field='target_id').contribute_to_class(
    ContentType, 'admin_perms'
)


CHANGE_PASSWORD_OP = Operation.register(name=_('Change password'), slug='change_password')
RESET_PASSWORD_OP = Operation.register(name=_('Password reset'), slug='reset_password')
ACTIVATE_OP = Operation.register(name=_('Activation'), slug='activate')
CHANGE_EMAIL_OP = Operation.register(name=pgettext_lazy('operation', 'Change email'), slug='change_email')
MANAGE_MEMBERS_OP = Operation.register(name=_('Manage role members'), slug='manage_members')
MANAGE_AUTHORIZATIONS_OP = Operation.register(name=_('Manage service consents'), slug='manage_authorizations')
