"""Authentication forms.

WTForms definitions for registration and login. Validation is server-side;
CSRF protection is provided automatically by Flask-WTF.
"""

from __future__ import annotations

import re

from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField
from wtforms import BooleanField, PasswordField, StringField, SubmitField, TextAreaField
from wtforms.validators import (
    DataRequired,
    Email,
    EqualTo,
    Length,
    Optional,
    ValidationError,
)

# Password policy per docs/02_Architecture/Security_Design.md section 5.
_PASSWORD_PATTERN = re.compile(
    r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).+$"
)

# Single source of truth for password strength. Reused by every password form
# and by the password-reset service, so the rules live in exactly one place.
_PASSWORD_POLICY_MESSAGE = (
    "Password must include uppercase, lowercase, a number, and a special character."
)


def password_meets_policy(password: str) -> bool:
    """True when a password satisfies the documented length + complexity rules."""
    return bool(password) and 8 <= len(password) <= 128 and bool(_PASSWORD_PATTERN.match(password))


class RegisterForm(FlaskForm):
    """New account registration."""

    full_name = StringField(
        "Full name",
        validators=[DataRequired(), Length(min=2, max=150)],
    )
    email = StringField(
        "Email",
        validators=[DataRequired(), Email(), Length(max=255)],
    )
    password = PasswordField(
        "Password",
        validators=[DataRequired(), Length(min=8, max=128)],
    )
    confirm_password = PasswordField(
        "Confirm password",
        validators=[
            DataRequired(),
            EqualTo("password", message="Passwords must match."),
        ],
    )
    submit = SubmitField("Create account")

    def validate_password(self, field: PasswordField) -> None:
        """Enforce the documented password complexity rules."""
        if not _PASSWORD_PATTERN.match(field.data or ""):
            raise ValidationError(
                "Password must include uppercase, lowercase, a number, "
                "and a special character."
            )


class LoginForm(FlaskForm):
    """Existing account login."""

    email = StringField(
        "Email",
        validators=[DataRequired(), Email(), Length(max=255)],
    )
    password = PasswordField("Password", validators=[DataRequired()])
    remember = BooleanField("Remember me")
    submit = SubmitField("Log in")


class OtpVerifyForm(FlaskForm):
    """Six-digit email verification code entry."""

    code = StringField(
        "Verification code",
        validators=[
            DataRequired(),
            Length(min=6, max=6, message="Enter the 6-digit code."),
        ],
        render_kw={
            "inputmode": "numeric",
            "autocomplete": "one-time-code",
            "pattern": r"\d{6}",
            "maxlength": "6",
            "placeholder": "------",
        },
    )
    submit = SubmitField("Verify")

    def validate_code(self, field: StringField) -> None:
        if not (field.data or "").strip().isdigit():
            raise ValidationError("The code must be 6 digits.")


class LoginOtpForm(OtpVerifyForm):
    """Login verification: the shared 6-digit code plus a "trust this device"
    opt-in. Subclasses :class:`OtpVerifyForm` so the code field, its validation
    and the reusable OTP-boxes partial are shared — no duplicated code input."""

    trust_device = BooleanField("Trust this device for 30 days")


class ResendOtpForm(FlaskForm):
    """CSRF-protected resend action (no fields beyond the token)."""

    submit = SubmitField("Resend code")


class TrustedDeviceActionForm(FlaskForm):
    """CSRF-only form for simple security actions (remove device, remove others,
    enable 2FA, save notification prefs, sign out sessions). Any target id travels
    in the URL and any options are read from the posted fields."""

    submit = SubmitField("Remove")


class DeviceRenameForm(FlaskForm):
    """Rename a trusted device with a friendly name."""

    name = StringField("Device name", validators=[Optional(), Length(max=80)])
    submit = SubmitField("Rename")


class ConfirmPasswordForm(FlaskForm):
    """Confirm the account password before a sensitive change (e.g. disabling
    two-factor authentication)."""

    password = PasswordField("Current password", validators=[DataRequired()])
    submit = SubmitField("Confirm")


class ForgotPasswordForm(FlaskForm):
    """Step 1 of password recovery: identify the account by email."""

    email = StringField(
        "Email",
        validators=[DataRequired(), Email(), Length(max=255)],
    )
    submit = SubmitField("Send reset code")


class ResetPasswordForm(FlaskForm):
    """Final step of password recovery: choose a new password.

    Reuses the shared complexity rule (:data:`_PASSWORD_PATTERN`) — the same
    validator used by registration and change-password, not a second one."""

    new_password = PasswordField(
        "New password",
        validators=[DataRequired(), Length(min=8, max=128)],
    )
    confirm_password = PasswordField(
        "Confirm password",
        validators=[
            DataRequired(),
            EqualTo("new_password", message="Passwords must match."),
        ],
    )
    submit = SubmitField("Reset password")

    def validate_new_password(self, field: PasswordField) -> None:
        if not _PASSWORD_PATTERN.match(field.data or ""):
            raise ValidationError(_PASSWORD_POLICY_MESSAGE)


class ChangeEmailForm(FlaskForm):
    """Correct the email address of an account still awaiting verification."""

    email = StringField(
        "New email",
        validators=[DataRequired(), Email(), Length(max=255)],
    )
    submit = SubmitField("Update email")


class ProfileUpdateForm(FlaskForm):
    full_name = StringField(
        "Full Name",
        validators=[DataRequired(), Length(min=2, max=150)],
    )
    email = StringField(
        "Email",
        validators=[DataRequired(), Email(), Length(max=255)],
        render_kw={"readonly": True},
    )
    # Phone number is no longer an editable profile field — it is verified
    # identity data managed only by the Student Verification workflow.
    bio = TextAreaField(
        "Bio",
        validators=[Optional(), Length(max=500)],
    )
    preferred_meeting_location = StringField(
        "Preferred meeting location",
        validators=[Optional(), Length(max=150)],
    )
    submit = SubmitField("Save changes")


class StudentVerificationForm(FlaskForm):
    """Student-verification submission. Presence/length only — the authoritative
    format validation (mobile pattern, trimming) lives in the service so the
    rules aren't duplicated."""

    mobile_number = StringField(
        "Phone number",
        validators=[DataRequired(), Length(max=20)],
    )
    university_name = StringField(
        "University",
        validators=[DataRequired(), Length(max=150)],
    )
    university_student_id = StringField(
        "University student ID number",
        validators=[DataRequired(), Length(max=60)],
    )
    submit = SubmitField("Submit for verification")


class ProfilePictureForm(FlaskForm):
    profile_picture = FileField(
        "Profile picture",
        validators=[
            FileAllowed(["png", "jpg", "jpeg", "webp"], "Images only (PNG, JPG, WEBP)."),
        ],
    )
    submit = SubmitField("Upload")


class PasswordChangeForm(FlaskForm):
    current_password = PasswordField(
        "Current Password",
        validators=[DataRequired()],
    )
    new_password = PasswordField(
        "New Password",
        validators=[DataRequired(), Length(min=8, max=128)],
    )
    confirm_password = PasswordField(
        "Confirm Password",
        validators=[
            DataRequired(),
            EqualTo("new_password", message="Passwords must match."),
        ],
    )
    submit = SubmitField("Update Password")

    def validate_new_password(self, field: PasswordField) -> None:
        if not _PASSWORD_PATTERN.match(field.data or ""):
            raise ValidationError(
                "Password must include uppercase, lowercase, a number, "
                "and a special character."
            )
