# 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/>.

import logging
from collections import OrderedDict

from django import forms
from django.conf import settings
from django.contrib.auth import forms as auth_forms
from django.core.exceptions import ValidationError
from django.forms import Form
from django.utils.translation import ugettext_lazy as _

from authentic2.journal import journal

from .. import app_settings, hooks, models, validators
from ..backends import get_user_queryset
from ..utils import misc as utils_misc
from .fields import CheckPasswordField, NewPasswordField, PasswordField, ValidatedEmailField
from .honeypot import HoneypotForm
from .utils import NextUrlFormMixin

logger = logging.getLogger(__name__)


class PasswordResetForm(HoneypotForm):
    next_url = forms.CharField(widget=forms.HiddenInput, required=False)

    email = ValidatedEmailField(label=_("Email"))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if app_settings.A2_USER_CAN_RESET_PASSWORD_BY_USERNAME:
            del self.fields['email']
            self.fields['email_or_username'] = forms.CharField(label=_('Email or username'), max_length=254)

    def clean_email(self):
        email = self.cleaned_data.get('email')
        if email:
            self.users = get_user_queryset().filter(email__iexact=email)
            return email

    def clean_email_or_username(self):
        email_or_username = self.cleaned_data.get('email_or_username')
        if email_or_username:
            self.users = get_user_queryset().filter(
                models.Q(username__iexact=email_or_username) | models.Q(email__iexact=email_or_username)
            )
            try:
                validators.email_validator(email_or_username)
            except ValidationError:
                pass
            else:
                self.cleaned_data['email'] = email_or_username
        return email_or_username

    def save(self):
        """
        Generates a one-use only link for resetting password and sends to the
        user.
        """
        email = self.cleaned_data.get('email')
        email_or_username = self.cleaned_data.get('email_or_username')

        active_users = self.users.filter(is_active=True)
        for user in active_users:
            # we don't set the password to a random string, as some users should not have
            # a password
            set_random_password = user.has_usable_password() and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET
            utils_misc.send_password_reset_mail(
                user, set_random_password=set_random_password, next_url=self.cleaned_data.get('next_url')
            )
            journal.record('user.password.reset.request', email=user.email, user=user)
        for user in self.users.filter(is_active=False):
            logger.info('password reset failed for user "%r": account is disabled', user)
            utils_misc.send_templated_mail(user, ['authentic2/password_reset_refused'])
        if not self.users.exists() and email:
            logger.info('password reset request for "%s", no user found', email)
            if getattr(settings, 'REGISTRATION_OPEN', True):
                ctx = {
                    'registration_url': utils_misc.make_url('registration_register', absolute=True),
                }
            else:
                ctx = {}
            utils_misc.send_templated_mail(email, ['authentic2/password_reset_no_account'], context=ctx)
        hooks.call_hooks('event', name='password-reset', email=email or email_or_username, users=active_users)


class PasswordResetMixin(Form):
    """Remove all password reset object for the current user when password is
    successfully changed."""

    def save(self, commit=True):
        ret = super().save(commit=commit)
        if commit:
            models.PasswordReset.objects.filter(user=self.user).delete()
        else:
            old_save = self.user.save

            def save(*args, **kwargs):
                ret = old_save(*args, **kwargs)
                models.PasswordReset.objects.filter(user=self.user).delete()
                return ret

            self.user.save = save
        return ret


class NotifyOfPasswordChange:
    def save(self, commit=True):
        user = super().save(commit=commit)
        if user.email:
            ctx = {
                'user': user,
                'password': self.cleaned_data['new_password1'],
            }
            utils_misc.send_templated_mail(user, "authentic2/password_change", ctx)
        return user


class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm):
    new_password1 = NewPasswordField(label=_("New password"))
    new_password2 = CheckPasswordField(label=_("New password confirmation"))

    def clean_new_password1(self):
        new_password1 = self.cleaned_data.get('new_password1')
        if new_password1 and self.user.check_password(new_password1):
            raise ValidationError(_('New password must differ from old password'))
        return new_password1


class PasswordChangeForm(
    NotifyOfPasswordChange, NextUrlFormMixin, PasswordResetMixin, auth_forms.PasswordChangeForm
):
    old_password = PasswordField(label=_('Old password'))
    new_password1 = NewPasswordField(label=_('New password'))
    new_password2 = CheckPasswordField(label=_("New password confirmation"))

    def clean_new_password1(self):
        new_password1 = self.cleaned_data.get('new_password1')
        old_password = self.cleaned_data.get('old_password')
        if new_password1 and new_password1 == old_password:
            raise ValidationError(_('New password must differ from old password'))
        return new_password1


# make old_password the first field
new_base_fields = OrderedDict()

for k in ['old_password', 'new_password1', 'new_password2']:
    new_base_fields[k] = PasswordChangeForm.base_fields[k]

for k in PasswordChangeForm.base_fields:
    if k not in ['old_password', 'new_password1', 'new_password2']:
        new_base_fields[k] = PasswordChangeForm.base_fields[k]

PasswordChangeForm.base_fields = new_base_fields
