"""Forms for the pages app."""

from __future__ import annotations

from django import forms
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables

from core.backends import (
    get_user_totp_devices,
    totp_devices_allow_passwordless,
    totp_devices_require_password,
    verify_user_totp_token,
)
from core.form_fields import Base64FileField

from .models import UserManual, UserStory


class AuthenticatorLoginForm(AuthenticationForm):
    """Authentication form that supports password or authenticator codes."""

    otp_token = forms.CharField(
        label=_("Authenticator code"),
        required=False,
        widget=forms.TextInput(
            attrs={
                "autocomplete": "one-time-code",
                "inputmode": "numeric",
                "pattern": "[0-9]*",
            }
        ),
    )
    auth_method = forms.CharField(required=False, widget=forms.HiddenInput(), initial="password")
    otp_ready = forms.CharField(required=False, widget=forms.HiddenInput(), initial="0")
    otp_requires_password = forms.CharField(
        required=False,
        widget=forms.HiddenInput(),
        initial="1",
    )
    otp_password_optional = forms.CharField(
        required=False,
        widget=forms.HiddenInput(),
        initial="0",
    )

    error_messages = {
        **AuthenticationForm.error_messages,
        "invalid_token": _("The authenticator code is invalid or has expired."),
        "token_required": _("Enter the code from your authenticator app."),
        "password_required": _("Enter your password."),
    }

    def __init__(self, request=None, *args, **kwargs):
        super().__init__(request=request, *args, **kwargs)
        self.fields["password"].required = False
        self.fields["otp_token"].strip = True
        self.fields["auth_method"].initial = "password"
        self.verified_device = None

    def get_invalid_token_error(self) -> ValidationError:
        return ValidationError(self.error_messages["invalid_token"], code="invalid_token")

    def get_token_required_error(self) -> ValidationError:
        return ValidationError(self.error_messages["token_required"], code="token_required")

    def get_password_required_error(self) -> ValidationError:
        return ValidationError(self.error_messages["password_required"], code="password_required")

    @sensitive_variables()
    def clean(self):
        username = self.cleaned_data.get("username")
        method = (self.cleaned_data.get("auth_method") or "password").lower()
        if method not in {"password", "otp"}:
            method = "password"
        self.cleaned_data["auth_method"] = method

        if username is not None:
            if method == "otp":
                token = (self.cleaned_data.get("otp_token") or "").strip().replace(" ", "")
                if not token:
                    raise self.get_token_required_error()
                UserModel = get_user_model()
                try:
                    user = UserModel._default_manager.get_by_natural_key(username)
                except UserModel.DoesNotExist:
                    raise self.get_invalid_login_error()
                devices = list(get_user_totp_devices(user))
                if not devices:
                    raise self.get_invalid_token_error()
                requires_password = totp_devices_require_password(devices)
                allows_passwordless = totp_devices_allow_passwordless(devices)
                self.cleaned_data["otp_requires_password"] = (
                    "1" if requires_password else "0"
                )
                self.cleaned_data["otp_password_optional"] = (
                    "1" if requires_password and allows_passwordless else "0"
                )
                password = self.cleaned_data.get("password") or ""
                device, result = verify_user_totp_token(user, token, password)
                if device is None:
                    error_code = (result or {}).get("error")
                    if error_code == "password_required":
                        raise self.get_password_required_error()
                    if error_code == "invalid_password":
                        raise self.get_invalid_login_error()
                    raise self.get_invalid_token_error()
                if device.user_id != user.pk:
                    device.user = user
                    device.user_id = user.pk
                user.otp_device = device
                backend_path = "core.backends.TOTPBackend"
                user.backend = backend_path
                self.user_cache = user
                self.cleaned_data["otp_token"] = token
                self.verified_device = device
            else:
                password = self.cleaned_data.get("password")
                if not password:
                    raise self.get_password_required_error()
                self.user_cache = authenticate(
                    self.request, username=username, password=password
                )
                if self.user_cache is None:
                    raise self.get_invalid_login_error()
            self.confirm_login_allowed(self.user_cache)

        return self.cleaned_data

    def get_verified_device(self):
        return self.verified_device


class AuthenticatorEnrollmentForm(forms.Form):
    """Form used to confirm a pending authenticator enrollment."""

    token = forms.CharField(
        label=_("Authenticator code"),
        min_length=6,
        max_length=8,
        widget=forms.TextInput(
            attrs={
                "autocomplete": "one-time-code",
                "inputmode": "numeric",
                "pattern": "[0-9]*",
            }
        ),
    )

    error_messages = {
        "invalid_token": _("The provided code is invalid or has expired."),
        "missing_device": _("Generate a new authenticator secret before confirming it."),
    }

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

    def clean_token(self):
        token = (self.cleaned_data.get("token") or "").strip().replace(" ", "")
        if not token:
            raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
        if self.device is None:
            raise forms.ValidationError(self.error_messages["missing_device"], code="missing_device")
        try:
            verified = self.device.verify_token(token)
        except Exception:
            verified = False
        if not verified:
            raise forms.ValidationError(self.error_messages["invalid_token"], code="invalid_token")
        return token

    def get_verified_device(self):
        return self.device


_manual_pdf_field = UserManual._meta.get_field("content_pdf")


class UserManualAdminForm(forms.ModelForm):
    content_pdf = Base64FileField(
        label=_manual_pdf_field.verbose_name,
        help_text=_manual_pdf_field.help_text,
        required=not _manual_pdf_field.blank,
        content_type="application/pdf",
        download_name="manual.pdf",
    )

    class Meta:
        model = UserManual
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        instance = getattr(self, "instance", None)
        slug = getattr(instance, "slug", "")
        if slug:
            self.fields["content_pdf"].widget.download_name = f"{slug}.pdf"
        self.fields["content_pdf"].widget.attrs.setdefault(
            "accept", "application/pdf"
        )


class UserStoryForm(forms.ModelForm):
    class Meta:
        model = UserStory
        fields = ("name", "rating", "comments", "take_screenshot", "path")
        widgets = {
            "path": forms.HiddenInput(),
            "comments": forms.Textarea(attrs={"rows": 4, "maxlength": 400}),
            "take_screenshot": forms.HiddenInput(),
        }

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

        if user is not None and user.is_authenticated:
            name_field = self.fields["name"]
            name_field.required = False
            name_field.label = _("Username")
            name_field.initial = (user.get_username() or "")[:40]
            name_field.widget.attrs.update(
                {
                    "maxlength": 40,
                    "readonly": "readonly",
                }
            )
        else:
            self.fields["name"] = forms.EmailField(
                label=_("Email address"),
                max_length=40,
                required=True,
                widget=forms.EmailInput(
                    attrs={
                        "maxlength": 40,
                        "placeholder": _("name@example.com"),
                        "autocomplete": "email",
                        "inputmode": "email",
                    }
                ),
            )
        self.fields["take_screenshot"].initial = True
        self.fields["rating"].widget = forms.RadioSelect(
            choices=[(i, str(i)) for i in range(1, 6)]
        )

    def clean_take_screenshot(self):
        # Screenshots are always requested when possible, regardless of user input.
        return True

    def clean_comments(self):
        comments = (self.cleaned_data.get("comments") or "").strip()
        if len(comments) > 400:
            raise forms.ValidationError(
                _("Feedback must be 400 characters or fewer."), code="max_length"
            )
        return comments

    def clean_name(self):
        if self.user is not None and self.user.is_authenticated:
            return (self.user.get_username() or "")[:40]

        name = (self.cleaned_data.get("name") or "").strip()
        return name[:40]
