from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from django.core.cache import cache
from django.core.exceptions import ValidationError

from .models import CustomUser

# A PER-EMAIL LOCKOUT, BECAUSE THE IP LIMITER CANNOT DO THIS JOB.
# core.middleware.RateLimitMiddleware keys on the client IP, which an attacker
# rotates for free, and its cache read-modify-write is unlocked so two
# concurrent requests both pass. Keying on the address being attacked is the
# part that cannot be rotated away.
LOCKOUT_ATTEMPTS = 10
LOCKOUT_SECONDS = 900


def _lockout_key(email):
    return f'login-fail:{email}'


def is_locked_out(email):
    return cache.get(_lockout_key(email), 0) >= LOCKOUT_ATTEMPTS


def record_failure(email):
    key = _lockout_key(email)
    # `add` then `incr`: incr on a missing key raises, and add is a no-op when
    # the key already exists, so the window starts at the first failure and is
    # not extended by later ones.
    cache.add(key, 0, LOCKOUT_SECONDS)
    try:
        cache.incr(key)
    except ValueError:
        cache.set(key, 1, LOCKOUT_SECONDS)


def clear_failures(email):
    cache.delete(_lockout_key(email))


class DesktopSignInForm(forms.Form):
    """The browser sign-in form. Never seen by the Electron window."""

    email = forms.EmailField(widget=forms.EmailInput(attrs={
        'autocomplete': 'username',
        'autofocus': True,
        'required': True,
    }))
    password = forms.CharField(widget=forms.PasswordInput(attrs={
        'autocomplete': 'current-password',
        'required': True,
    }))

    def __init__(self, *args, request=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.request = request
        self.user = None

    def clean_email(self):
        return self.cleaned_data['email'].strip().lower()

    def clean(self):
        cleaned = super().clean()
        email = cleaned.get('email')
        password = cleaned.get('password')

        if not email or not password:
            return cleaned

        if is_locked_out(email):
            raise ValidationError('Too many attempts. Try again in 15 minutes.')

        self.user = authenticate(self.request, username=email, password=password)

        if self.user is None:
            record_failure(email)
            # One message for a wrong password and an unknown address alike,
            # or the form becomes a way to ask whether somebody has an account.
            raise ValidationError('Email or password is incorrect.')

        if not self.user.is_active:
            raise ValidationError('This account is not active.')

        clear_failures(email)
        return cleaned


class DesktopRegisterForm(forms.Form):
    """The browser registration form."""

    email = forms.EmailField(widget=forms.EmailInput(attrs={
        'autocomplete': 'username',
        'autofocus': True,
        'required': True,
    }))
    password = forms.CharField(
        widget=forms.PasswordInput(attrs={
            'autocomplete': 'new-password',
            'required': True,
        }),
        help_text='At least 8 characters.',
    )

    def clean_email(self):
        email = self.cleaned_data['email'].strip().lower()
        if CustomUser.objects.filter(email=email).exists():
            # This one DOES disclose that the address is taken, and it has to:
            # registration cannot silently succeed into somebody else's
            # account, and "an account already exists" is what every sign-up
            # form on the web says. The sign-in form above is where the
            # enumeration defence belongs.
            raise ValidationError('An account with this email already exists.')
        return email

    def clean_password(self):
        password = self.cleaned_data['password']
        validate_password(password)
        return password

    def save(self):
        return CustomUser.objects.create_user(
            email=self.cleaned_data['email'],
            password=self.cleaned_data['password'],
        )
