# 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 copy
import math

from django import forms
from django.conf import settings
from django.contrib.auth import forms as auth_forms
from django.forms.widgets import Media
from django.utils import html
from django.utils.encoding import force_text
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _

from authentic2.forms.fields import PasswordField
from authentic2.utils.lazy import lazy_label

from .. import app_settings
from ..a2_rbac.models import OrganizationalUnit as OU
from ..exponential_retry_timeout import ExponentialRetryTimeout
from ..utils import misc as utils_misc


class AuthenticationForm(auth_forms.AuthenticationForm):
    password = PasswordField(label=_('Password'))
    remember_me = forms.BooleanField(
        initial=False,
        required=False,
        label=_('Remember me'),
        help_text=_('Do not ask for authentication next time'),
    )
    ou = forms.ModelChoiceField(
        label=lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL),
        required=True,
        queryset=OU.objects.all(),
    )

    def __init__(self, *args, **kwargs):
        preferred_ous = kwargs.pop('preferred_ous', [])

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

        self.exponential_backoff = ExponentialRetryTimeout(
            key_prefix='login-exp-backoff-',
            duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
            factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR,
        )

        if not app_settings.A2_USER_REMEMBER_ME:
            del self.fields['remember_me']

        if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
            del self.fields['ou']
        else:
            if preferred_ous:
                choices = self.fields['ou'].choices
                new_choices = list(choices)[:1] + [
                    (ugettext('Preferred organizational units'), [(ou.pk, ou.name) for ou in preferred_ous]),
                    (ugettext('All organizational units'), list(choices)[1:]),
                ]
                self.fields['ou'].choices = new_choices

        if self.request:
            self.remote_addr = self.request.META['REMOTE_ADDR']
        else:
            self.remote_addr = '0.0.0.0'

    def exp_backoff_keys(self):
        return self.cleaned_data['username'], self.remote_addr

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        keys = None
        if username and password:
            keys = self.exp_backoff_keys()
            seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys)
            if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION:
                seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION
                msg = _(
                    'You made too many login errors recently, you must '
                    'wait <span class="js-seconds-until">%s</span> seconds '
                    'to try again.'
                )
                msg = msg % int(math.ceil(seconds_to_wait))
                msg = html.mark_safe(msg)
                raise forms.ValidationError(msg)

        try:
            self.clean_authenticate()
        except Exception:
            if keys:
                self.exponential_backoff.failure(*keys)
            raise
        else:
            if keys:
                self.exponential_backoff.success(*keys)
        return self.cleaned_data

    def clean_authenticate(self):
        # copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        ou = self.cleaned_data.get('ou')

        if username is not None and password:
            self.user_cache = utils_misc.authenticate(
                self.request, username=username, password=password, ou=ou
            )
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    @property
    def media(self):
        media = super().media
        media = media + Media(js=['authentic2/js/js_seconds_until.js'])
        if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
            media = media + Media(js=['authentic2/js/ou_selector.js'])
        return media

    @property
    def error_messages(self):
        error_messages = copy.copy(auth_forms.AuthenticationForm.error_messages)
        username_label = _('Username')
        if app_settings.A2_USERNAME_LABEL:
            username_label = app_settings.A2_USERNAME_LABEL
        invalid_login_message = [
            _('Incorrect %(username_label)s or password.') % {'username_label': username_label},
        ]
        if app_settings.A2_USER_CAN_RESET_PASSWORD is not False and getattr(
            settings, 'REGISTRATION_OPEN', True
        ):
            invalid_login_message.append(
                _('Try again, use the forgotten password link below, or create an account.')
            )
        elif app_settings.A2_USER_CAN_RESET_PASSWORD is not False:
            invalid_login_message.append(_('Try again or use the forgotten password link below.'))
        elif getattr(settings, 'REGISTRATION_OPEN', True):
            invalid_login_message.append(_('Try again or create an account.'))
        error_messages['invalid_login'] = ' '.join([force_text(x) for x in invalid_login_message])
        return error_messages
