# -*- coding: utf-8 -*-

from django import forms
from django.conf import settings
from django.contrib.auth.forms import (
    AuthenticationForm,
    PasswordChangeForm as AuthPasswordChangeForm,
    PasswordResetForm as AuthPasswordResetForm,
    SetPasswordForm,
)
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import MinLengthValidator
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from extended_choices import Choices
from django_select2.forms import ModelSelect2Widget, ModelSelect2MultipleWidget

from mtp.apps.themes.models import Theme

from .models import Group, GroupRole, MTPUser
from .models import Avatar
from .utils import validate_extra_secure_password


class PasswordChangeForm(AuthPasswordChangeForm):
    """
    We need to override the default `PasswordChangeForm` to implement the
    reinforced security policy required by our clients.
    """

    def clean(self):
        """
        Validate the password according to the security policy.
        """
        old_password = self.cleaned_data.get('old_password')
        new_password1 = self.cleaned_data.get('new_password1')

        if settings.PASSWORD_EXTRA_SECURITY:
            is_valid, reason, __ = validate_extra_secure_password(
                new_password1, previous=old_password
            )
            if not is_valid:
                raise forms.ValidationError(reason)

        return self.cleaned_data

    def save(self, commit=True):
        """Save the last changed password time

        For the parameters, see `django.contrib.auth.forms.PasswordChangeForm

        """

        self.user.last_changed_password = timezone.now()
        return super().save(commit)


class PasswordResetForm(AuthPasswordResetForm):
    """Override the default ``PasswordResetForm`` passing extra context to the email template"""

    def get_users(self, email):
        """Override the ``get_users`` method, filtering out the deleted users

        For the parameters, see ``django.contrib.auth.forms.PasswordResetForm``
        """

        active_users = super().get_users(email)

        return (u for u in active_users if not u.is_deleted)

    def send_mail(self, subject_template_name, email_template_name,
                  context, from_email, to_email, html_email_template_name=None):
        """Sends a django.core.mail.EmailMultiAlternatives to `to_email`.

        For the parameters, see ``django.contrib.auth.forms.PasswordResetForm``"""

        context['instance_name'] = settings.INSTANCE_NAME
        context['master_url'] = settings.MASTER_INSTANCE_URL
        context['theme'] = Theme.objects.get_default()

        super().send_mail(subject_template_name, email_template_name,
                          context, from_email, to_email, html_email_template_name)


class EmailPasswordForm(forms.Form):
    email = forms.EmailField()


class UploadFileForm(forms.Form):
    file = forms.FileField(required=True)


class UserImportForm(forms.Form):
    """Form used to import multiple User"""

    filename = forms.CharField(widget=forms.HiddenInput)
    send_mail = forms.BooleanField(
        label=_('Send a welcome email to all trainee imported in this file'),
        required=False,
        initial=True,
    )


USER_GROUPSET_CHOICES = Choices(
    ('EVALUATOR', 'evaluator', _("the participant can assess the other participants")),
    ('AUTO_EVALUATOR', 'auto_evaluator', _("the participant can assess himself")),
    ('BOTH', 'both', _("the participant can both assess himself and the other participants")),
)


class UserGroupSetForm(forms.Form):
    eval_type = forms.ChoiceField(required=True, choices=USER_GROUPSET_CHOICES,
                                  widget=forms.RadioSelect, label=_("evaluation type"))


class AvatarForm(forms.ModelForm):

    class Meta:
        model = Avatar
        exclude = ('last_update', 'first_update', 'update_locally', 'order', 'is_deleted', 'hash')


class MTPUserSelectWidget(ModelSelect2Widget):
    """Define a Searchable select widget for the ``MTPUser`` instance"""

    search_fields = [
        'username__icontains',
        'email__icontains',
        'first_name__icontains',
        'last_name__icontains'
    ]
    to_field_name = 'username'


class MTPUserSelectMultipleWidget(ModelSelect2MultipleWidget):
    """Define a Searchable multiple select widget for the ``MTPUser`` instance"""

    search_fields = [
        'username__icontains',
        'email__icontains',
        'first_name__icontains',
        'last_name__icontains'
    ]
    to_field_name = 'username'


class GroupForm(forms.ModelForm):
    """Django form for the ``Group`` model"""

    manager_queryset = MTPUser.objects.non_deleted()
    members_queryset = MTPUser.objects.non_deleted()

    manager = forms.ModelChoiceField(
        widget=MTPUserSelectWidget(queryset=manager_queryset),
        queryset=manager_queryset,
        required=False
    )
    members = forms.ModelMultipleChoiceField(
        widget=MTPUserSelectMultipleWidget(queryset=members_queryset),
        queryset=members_queryset,
        required=False
    )

    class Meta:
        model = Group
        fields = ('name', 'type', 'members')

    def __init__(self, *args, **kwargs):

        # We try to populate the `members` and `manager` form fields if we are editing an
        # existing model instance.
        if 'instance' in kwargs:
            initial = kwargs.setdefault('initial', {})
            try:
                initial['members'] = [
                    m.pk for m in kwargs['instance'].members.all()
                    if not m == kwargs['instance'].manager_member
                ]
            except AttributeError:
                pass

            try:
                initial['manager'] = kwargs['instance'].manager_member
            except AttributeError:
                pass

        super().__init__(*args, **kwargs)

    def save(self, commit=True):

        # First we save our form as usual.
        instance = forms.ModelForm.save(self, False)

        # Then we need to save the group members, which are "on the other
        # side" of the M2M relationship.

        # So we redefine the `save_m2m` method (defined at runtime in
        # django.forms.models.save_instance)
        old_save_m2m = self.save_m2m  # pylint: disable=access-member-before-definition

        def save_m2m():
            old_save_m2m()
            instance.members.clear()

            for member in self.cleaned_data['members']:
                if member == self.cleaned_data['manager']:
                    continue
                GroupRole.objects.create(
                    user=member,
                    group=instance
                )

            if self.cleaned_data['manager']:
                # If we get a manager, we must remove the old one first
                try:
                    GroupRole.objects.get(
                        group=instance,
                        role=GroupRole.ROLES.MANAGER
                    ).delete()
                except ObjectDoesNotExist:
                    pass

                # And add the new manager
                GroupRole.objects.create(
                    group=instance,
                    role=GroupRole.ROLES.MANAGER,
                    user=self.cleaned_data['manager']
                )

        self.save_m2m = save_m2m  # pylint: disable=attribute-defined-outside-init

        # Then we use it if necessary.
        if commit:
            instance.save()
            self.save_m2m()

        return instance


class BaseGroupField:
    """Base class to use for the field related to ``Group`` instances"""

    search_fields = [
        'name__icontains',
    ]
    to_field_name = 'name'
    queryset = Group.objects.non_deleted()


class GroupSelectMultipleWidget(BaseGroupField, ModelSelect2MultipleWidget):
    """Field to select multiple ``Group`` instances"""


class GroupSelectWidget(BaseGroupField, ModelSelect2Widget):
    """Field to select ``Group`` instances"""


class UserForm(forms.ModelForm):
    """Form for the ``mtp.apps.accounts.models.MTPUser`` model"""

    mgroups = forms.ModelMultipleChoiceField(
        queryset=Group.objects.non_deleted(),
        required=False,
        label=_('Business units'),
        widget=GroupSelectMultipleWidget()
    )
    pass1 = forms.CharField(widget=forms.PasswordInput, label=_("Password"),
                            required=True)
    pass2 = forms.CharField(widget=forms.PasswordInput, label=_("Confirm password"),
                            required=True)

    class Meta:
        model = MTPUser

        fields = (
            'username',
            'first_name',
            'last_name',
            'email',
            'natural_language',
            'mgroups',
            'external_id'

        )

    def __init__(self, *args, **kwargs):

        """Get the current user and set the queryset for the editable groups

        For the parameters, see ``django.forms.ModelForm``"""

        current_user = kwargs.pop('current_user')
        super().__init__(*args, **kwargs)

        if not current_user.is_superuser:
            self.fields.pop('external_id')

        for field in ['first_name', 'last_name']:
            self.fields[field].required = True

        # If we are editing an existing user, the password can be left empty
        if self.instance.pk:
            for field in ['pass1', 'pass2']:
                self.fields[field].required = False

        # The current user can only see / add / remove the groups he manage
        self.editable_groups_qs = Group.objects.non_deleted().managed_by(current_user)
        self.fields['mgroups'].queryset = self.editable_groups_qs

    def save(self, commit=True):
        """Save the user and his associated group roles

        For the parameters, see ``django.forms.ModelForm``
        """

        # First we save our form as usual.
        instance = super().save(commit=False)

        # Then we need to save the groups of the user, so we redefine the save_m2m (defined at
        # runtime in django.forms.models.save_instance)
        def save_m2m():
            # So, we first remove all the managed groups from the user...
            GroupRole.objects.filter(
                role=GroupRole.ROLES.MEMBER, user=instance,
                group__pk__in=self.editable_groups_qs.values_list('pk', flat=True)
            ).delete()

            # ...and create the new ones
            for group in self.cleaned_data['mgroups']:
                if group.manager_member == instance:
                    continue
                GroupRole.objects.create(
                    group=group,
                    user=instance
                )

        self.save_m2m = save_m2m  # pylint: disable=attribute-defined-outside-init

        if commit:
            instance.save()
            self.save_m2m()

        return instance

    def clean_username(self):
        username = self.cleaned_data.get("username")
        username = username.lower().strip()
        return username

    def clean_pass1(self):
        password1 = self.cleaned_data.get('pass1')
        if len(password1) < 6 and len(password1) != 0:
            raise forms.ValidationError("The password must contain at least 6 characters.")

        return password1

    def clean_pass2(self):
        password1 = self.cleaned_data.get('pass1')
        password2 = self.cleaned_data.get('pass2')
        if password1 and password1 != password2:
            raise forms.ValidationError("The passwords don't match.")

        return password2


class MTPAuthenticationForm(AuthenticationForm):
    """Authenticated form subclass for the MTP platform

    This subclass deactivate the authentication of the users who belongs
    to the ``trainee`` group.

    """

    trainee_error_message = _('Log in not allowed, please log in on the trainee interface.')
    instance_is_slave_error_message = _(
        'You are not allowed to access the CMS from the box'
    )

    def confirm_login_allowed(self, user):
        """Check if the user is authorized to log-in

        Raises
        ------
        ValidationError
            If the user is a trainee
            If the instance is a "SLAVE" one and the user is not the magency superuser

        For the parameters, see ``django.contrib.auth.forms.AuthenticationForm``

        """

        super().confirm_login_allowed(user)

        if user.is_trainee:
            raise forms.ValidationError(
                self.trainee_error_message,
                code='is_trainee'
            )

        if settings.INSTANCE_IS_SLAVE and user.username != settings.MAGENCY_ADMIN_USERNAME:
            raise forms.ValidationError(
                self.instance_is_slave_error_message,
                code='instance_is_slave'
            )


class UserBulkForm(forms.Form):
    """Form to select multiple users

    To use it, you need to add `current_user` to the kwargs,
    by using `get_form_kwargs` on view for example.
    """

    users = forms.ModelMultipleChoiceField(
        # queryset will be set in __init__
        queryset=None,
        widget=forms.CheckboxSelectMultiple
    )

    def __init__(self, *args, **kwargs):
        """Get the current user and set the queryset for users

        For the parameters, see ``django.forms.Form``"""
        current_user = kwargs.pop('current_user')
        super().__init__(*args, **kwargs)

        queryset = MTPUser.objects.exclude(is_superuser=True).exclude(pk=current_user)
        self.fields['users'].queryset = queryset


class SetPasswordFormWithMinLength(SetPasswordForm):
    """A form that lets a user change set their password

    This form inherit from the django builtin ``SetPasswordForm``, enforcing the minimum length
    of the user's password.

    """

    def __init__(self, user, *args, **kwargs):
        """Enforce the minimum length of the user's password

        For the parameters, see ``django.contrib.auth.forms.SetPasswordForm``"""

        super().__init__(user, *args, **kwargs)

        self.fields['new_password1'].validators.append(
            MinLengthValidator(6, _('Password must be at least 6 characters.'))
        )


class PotentialOriginalUserSearchForm(forms.Form):
    """A form to search a potential original user

    By "potential original", we mean the users with a non-empty "external_id".

    """

    user_queryset = MTPUser.objects.non_deleted().exclude(external_id='')

    user = forms.ModelChoiceField(
        widget=MTPUserSelectWidget(
            queryset=user_queryset,
            attrs={'data-placeholder': _('Search')}
        ),
        queryset=user_queryset,
    )


class PotentialDuplicateUserSearchForm(forms.Form):
    """A form to search a potential duplicate user

    By "potential duplicate", we mean the users with an empty "external_id".

    """

    user_queryset = MTPUser.objects.non_deleted().filter(external_id='')

    user = forms.ModelChoiceField(
        widget=MTPUserSelectWidget(
            queryset=user_queryset,
            attrs={'data-placeholder': _('Search')}
        ),
        queryset=user_queryset,
    )
