import django
from django import forms
from django.contrib.auth import (authenticate, get_user_model,
                                 password_validation)
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables

from cuser.models import CUser

try:
    from django.contrib.auth.forms import (SetPasswordMixin,
                                           SetUnusablePasswordMixin)
except ImportError:
    pass

UserModel = get_user_model()


class AuthenticationForm(forms.Form):
    """
    Base class for authenticating users. Extend this to get a form that accepts
    email/password logins.
    """
    email = forms.EmailField(
        label=_("Email address"),
        max_length=254,
        widget=forms.EmailInput(attrs={'autofocus': True}),
    )
    password = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
    )

    error_messages = {
        'invalid_login': _(
            "Please enter a correct %(username)s and password. Note that both "
            "fields may be case-sensitive."
        ),
        'inactive': _("This account is inactive."),
    }

    def __init__(self, request=None, *args, **kwargs):  # pylint: disable=keyword-arg-before-vararg
        """
        The 'request' parameter is set for custom auth use by subclasses.
        The form data comes in via the standard 'data' kwarg.
        """
        self.request = request
        self.user_cache = None
        super().__init__(*args, **kwargs)

        self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)

    @sensitive_variables()
    def clean(self):
        email = self.cleaned_data.get('email')
        password = self.cleaned_data.get('password')

        if email and password:
            self.user_cache = authenticate(self.request, email=email, password=password)
            if self.user_cache is None:
                raise self.get_invalid_login_error()
            else:
                self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    def confirm_login_allowed(self, user):
        """
        Controls whether the given User may log in. This is a policy setting,
        independent of end-user authentication. This default behavior is to
        allow login by active users, and reject login by inactive users.

        If the given user cannot log in, this method should raise a
        ``forms.ValidationError``.

        If the given user may log in, this method should return None.
        """
        if not user.is_active:
            raise forms.ValidationError(
                self.error_messages['inactive'],
                code='inactive',
            )

    def get_user(self):
        return self.user_cache

    def get_invalid_login_error(self):
        return forms.ValidationError(
            self.error_messages['invalid_login'],
            code='invalid_login',
            params={'username': self.username_field.verbose_name},
        )


if django.VERSION >= (5, 1):
    class UserCreationForm(SetPasswordMixin, forms.ModelForm):
        """
        A form that creates a user, with no privileges, from the given email and
        password.
        """

        email = forms.EmailField(
            label=_("Email address"),
            max_length=254,
            widget=forms.EmailInput(attrs={'autofocus': True}),
        )
        password1, password2 = SetPasswordMixin.create_password_fields()

        class Meta:
            model = CUser
            fields = ("email",)

        def clean(self):
            self.validate_passwords()
            return super().clean()

        def _post_clean(self):
            super()._post_clean()
            # Validate the password after self.instance is updated with form data
            # by super().
            self.validate_password_for_user(self.instance)

        def save(self, commit=True):
            user = super().save(commit=False)
            user = self.set_password_and_save(user, commit=commit)
            if commit and hasattr(self, "save_m2m"):
                self.save_m2m()
            return user

    class AdminUserCreationForm(SetUnusablePasswordMixin, UserCreationForm):
        usable_password = SetUnusablePasswordMixin.create_usable_password_field()

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

            if django.VERSION >= (5, 2):
                self.fields["password1"].required = False
                self.fields["password2"].required = False
else:
    class UserCreationForm(forms.ModelForm):
        """
        A form that creates a user, with no privileges, from the given email and
        password.
        """
        error_messages = {
            'password_mismatch': _('The two password fields didn’t match.'),
        }
        email = forms.EmailField(
            label=_("Email address"),
            max_length=254,
            widget=forms.EmailInput(attrs={'autofocus': True}),
        )
        password1 = forms.CharField(
            label=_("Password"),
            strip=False,
            widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
            help_text=password_validation.password_validators_help_text_html(),
        )
        password2 = forms.CharField(
            label=_("Password confirmation"),
            widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
            strip=False,
            help_text=_("Enter the same password as before, for verification."),
        )

        class Meta:
            model = CUser
            fields = ("email",)

        def clean_password2(self):
            password1 = self.cleaned_data.get("password1")
            password2 = self.cleaned_data.get("password2")
            if password1 and password2 and password1 != password2:
                raise forms.ValidationError(
                    self.error_messages['password_mismatch'],
                    code='password_mismatch',
                )
            return password2

        def _post_clean(self):
            super()._post_clean()
            # Validate the password after self.instance is updated with form data
            # by super().
            password = self.cleaned_data.get('password2')
            if password:
                try:
                    password_validation.validate_password(password, self.instance)
                except forms.ValidationError as error:
                    self.add_error('password2', error)

        def save(self, commit=True):
            user = super().save(commit=False)
            user.set_password(self.cleaned_data["password1"])
            if commit:
                user.save()
                if django.VERSION >= (4, 2) and hasattr(self, "save_m2m"):
                    self.save_m2m()
            return user

    class AdminUserCreationForm(UserCreationForm):
        pass


class UserChangeForm(forms.ModelForm):
    email = forms.EmailField(
        label=_("Email address"),
        max_length=254,
        widget=forms.EmailInput(),
    )
    password = ReadOnlyPasswordHashField(
        label=_("Password"),
        help_text=_(
            'Raw passwords are not stored, so there is no way to see this '
            'user’s password, but you can change the password using '
            '<a href="{}">this form</a>.'
        ),
    )

    class Meta:
        model = CUser
        fields = '__all__'

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

        password = self.fields.get('password')
        if password:
            if django.VERSION >= (4, 2):
                password.help_text = password.help_text.format(
                    f"../../{self.instance.pk}/password/"
                )
            else:
                password.help_text = password.help_text.format('../password/')
        user_permissions = self.fields.get('user_permissions')
        if user_permissions:
            user_permissions.queryset = user_permissions.queryset.select_related('content_type')

    if django.VERSION < (3, 2):
        def clean_password(self):
            # Regardless of what the user provides, return the initial value.
            # This is done here, rather than on the field, because the
            # field does not have access to the initial value
            return self.initial.get('password')
