import logging
import zoneinfo

from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm as DjangoSetPasswordForm
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.urls import reverse
from django.utils.safestring import mark_safe

from apps.core.countries import CountryChoiceField
from apps.core.authentication.models import Institution, ProfileVisibility, UserProfile
from apps.core.permissions.roles import UserRole
from apps.core.storage import UploadHandler

User = get_user_model()
logger = logging.getLogger(__name__)


_input = "w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-ruforum-primary focus:outline-none focus:ring-1 focus:ring-ruforum-primary"

TIMEZONE_CHOICES = (("", ""), *((tz, tz) for tz in sorted(zoneinfo.available_timezones())))


class LoginForm(forms.Form):
    email = forms.EmailField(
        widget=forms.EmailInput(
            attrs={"autocomplete": "email", "class": _input},
        ),
    )
    password = forms.CharField(
        strip=False,
        widget=forms.PasswordInput(
            attrs={"autocomplete": "current-password", "class": _input},
        ),
    )


class RegisterForm(forms.ModelForm):
    password1 = forms.CharField(
        label="Password",
        widget=forms.PasswordInput(attrs={"class": _input}),
    )
    password2 = forms.CharField(
        label="Confirm password",
        widget=forms.PasswordInput(attrs={"class": _input}),
    )
    institution = forms.ModelChoiceField(
        queryset=Institution.objects.filter(is_active=True),
        required=False,
        empty_label="Select institution (optional)",
        widget=forms.Select(attrs={"class": _input, "data-s2": True, "data-s2-placeholder": "Search institutions…"}),
    )
    # WS5.1 / FRREP-UR003 — free-text path for non-partner institutions. Users
    # who can't find theirs in the dropdown can type it here. The account
    # flips to ``pending_admin_review = True`` until staff review.
    non_partner_institution = forms.CharField(
        max_length=255,
        required=False,
        widget=forms.TextInput(attrs={
            "class": _input,
            "placeholder": "If your institution isn't listed, enter its full name…",
        }),
    )
    # WS5.1 / FRREP-UR008 — primary role at signup.
    role = forms.ChoiceField(
        choices=[
            (UserRole.APPLICANT,   "Applicant — I want to apply for grants or scholarships"),
            (UserRole.LEARNER,     "Learner — I want to take courses"),
            (UserRole.INSTRUCTOR,  "Instructor — I want to author courses"),
        ],
        initial=UserRole.APPLICANT,
        widget=forms.RadioSelect,
    )

    class Meta:
        model = User
        fields = ("email", "first_name", "last_name", "phone", "institution")
        widgets = {
            "email": forms.EmailInput(attrs={"class": _input}),
            "first_name": forms.TextInput(attrs={"class": _input}),
            "last_name": forms.TextInput(attrs={"class": _input}),
            "phone": forms.TextInput(attrs={"class": _input}),
            "institution": forms.Select(attrs={"class": _input}),
        }

    # Render order: identity → credentials → institution choice → role.
    field_order = (
        "first_name",
        "last_name",
        "email",
        "phone",
        "password1",
        "password2",
        "institution",
        "non_partner_institution",
        "role",
    )

    def clean_email(self):
        # FRREP-UR004 — duplicate emails must block registration with an
        # inline error that offers a way out (the email is already taken,
        # so the most likely next step is "I forgot my password").
        email = (self.cleaned_data.get("email") or "").strip().lower()
        if email and User.objects.filter(email__iexact=email).exists():
            reset_url = reverse("accounts:password_reset")
            raise ValidationError(
                mark_safe(
                    "An account with this email already exists. "
                    f'<a href="{reset_url}" class="font-semibold underline">'
                    "Forgot your password?</a>"
                )
            )
        return email

    def clean_password2(self):
        p1 = self.cleaned_data.get("password1")
        p2 = self.cleaned_data.get("password2")
        if p1 and p2 and p1 != p2:
            raise ValidationError("Passwords do not match.")
        return p2

    def clean(self):
        cleaned = super().clean()
        institution = cleaned.get("institution")
        non_partner = (cleaned.get("non_partner_institution") or "").strip()
        if not institution and not non_partner:
            self.add_error(
                "institution",
                "Pick your institution from the list, or type its name if it isn't listed.",
            )
        # If both are set, prefer the dropdown selection — keep the free-text
        # but blank it out so we don't double-flag the account.
        if institution and non_partner:
            cleaned["non_partner_institution"] = ""
        return cleaned

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        user.email_verified = False
        user.role = self.cleaned_data.get("role") or UserRole.LEARNER
        non_partner = (self.cleaned_data.get("non_partner_institution") or "").strip()
        if non_partner and not self.cleaned_data.get("institution"):
            user.non_partner_institution_name = non_partner
            user.pending_admin_review = True
        if commit:
            user.save()
        return user


class PasswordResetRequestForm(forms.Form):
    email = forms.EmailField()


class IILMPPasswordResetForm(PasswordResetForm):
    """
    Stock PasswordResetForm catches all exceptions in send(); misconfigured SMTP
    then looks like success. We propagate send errors. When no user matches,
    Django still shows success (no account enumeration); we log for operators.
    """

    def save(self, *args, **kwargs):
        email = self.cleaned_data["email"]
        if not any(self.get_users(email)):
            logger.info(
                "Password reset: no active user with usable password matched "
                "(submitted address is not echoed)."
            )
        return super().save(*args, **kwargs)

    def send_mail(
        self,
        subject_template_name,
        email_template_name,
        context,
        from_email,
        to_email,
        html_email_template_name=None,
    ):
        from django.urls import reverse

        ctx = dict(context)
        ctx["portal_base"] = getattr(settings, "PUBLIC_APP_BASE_URL", "http://127.0.0.1:8000").rstrip("/")
        ctx["preferences_path"] = reverse("core:notification_inbox")
        u = ctx.get("user")
        ctx["recipient_name"] = (u.get_full_name() if u else "") or (getattr(u, "email", None) or "").split("@")[0] or "colleague"

        subject = loader.render_to_string(subject_template_name, ctx)
        subject = "".join(subject.splitlines())
        body = loader.render_to_string(email_template_name, ctx)
        from_addr = from_email or settings.DEFAULT_FROM_EMAIL
        email_message = EmailMultiAlternatives(subject, body, from_addr, [to_email])
        if html_email_template_name is not None:
            html_email = loader.render_to_string(html_email_template_name, ctx)
            email_message.attach_alternative(html_email, "text/html")
            from apps.core.notifications.channels import _attach_brand_logo_inline
            _attach_brand_logo_inline(email_message)
        email_message.send(fail_silently=False)


class MFAVerifyForm(forms.Form):
    code = forms.CharField(
        label="Authentication code",
        max_length=14,
        widget=forms.TextInput(
            attrs={
                "inputmode": "numeric",
                "autocomplete": "one-time-code",
                "class": _input,
                "placeholder": "000000",
            },
        ),
    )

    def clean_code(self):
        raw = self.cleaned_data["code"]
        normalized = raw.strip().replace(" ", "")
        if len(normalized) != 6 or not normalized.isdigit():
            raise ValidationError("Enter a 6-digit code.")
        return normalized


class MFAEnrollForm(forms.Form):
    code = forms.CharField(
        label="Verification code",
        max_length=14,
        widget=forms.TextInput(
            attrs={
                "inputmode": "numeric",
                "autocomplete": "one-time-code",
                "class": _input,
                "placeholder": "000000",
            },
        ),
    )
    device_name = forms.CharField(
        required=False,
        max_length=64,
        label="Device name (optional)",
        widget=forms.TextInput(
            attrs={"class": _input, "placeholder": "e.g. Phone, Work laptop"},
        ),
    )

    def clean_code(self):
        raw = self.cleaned_data["code"]
        normalized = raw.strip().replace(" ", "")
        if len(normalized) != 6 or not normalized.isdigit():
            raise ValidationError("Enter a 6-digit code.")
        return normalized


class MFARemoveForm(forms.Form):
    password = forms.CharField(
        strip=False,
        label="Current password",
        widget=forms.PasswordInput(attrs={"autocomplete": "current-password", "class": _input}),
    )


class SetPasswordForm(DjangoSetPasswordForm):
    pass


class ProfileForm(forms.ModelForm):
    first_name = forms.CharField(required=False, widget=forms.TextInput(attrs={"class": _input}))
    last_name = forms.CharField(required=False, widget=forms.TextInput(attrs={"class": _input}))
    phone = forms.CharField(required=False, widget=forms.TextInput(attrs={"class": _input}))
    country = CountryChoiceField(required=False, attrs={"class": _input})
    timezone = forms.ChoiceField(
        required=False,
        choices=TIMEZONE_CHOICES,
        widget=forms.Select(
            attrs={"class": _input, "data-s2": True, "data-s2-placeholder": "Search timezones…"},
        ),
    )
    preferred_language = forms.ChoiceField(
        required=False,
        choices=(("", ""), *settings.LANGUAGES),
        widget=forms.Select(attrs={"class": _input}),
    )

    class Meta:
        model = UserProfile
        fields = (
            "bio",
            "avatar",
            "country",
            "degree_level",
            # WS5.2 / FRREP-UR006 — expanded professional profile. The
            # `qualifications`/`experience` JSONFields are repeatable lists
            # edited through their own popup add/edit/delete views
            # (QualificationEntryForm/ExperienceEntryForm + the
            # qualification_*/experience_* views below) rather than this
            # single-instance ModelForm.
            "professional_background",
            "areas_of_interest",
            # WS1.5 / FRREP-AR010 — optional demographic fields used to derive
            # the women & youth participation indicator. Both are self-reported,
            # blank-allowed, and never displayed publicly.
            "gender",
            "date_of_birth",
            "timezone",
            "preferred_language",
        )
        widgets = {
            "bio": forms.Textarea(attrs={"rows": 4, "class": _input}),
            "avatar": forms.FileInput(attrs={"class": _input}),
            "degree_level": forms.Select(attrs={"class": _input}),
            "professional_background": forms.Textarea(
                attrs={"rows": 3, "class": _input,
                       "placeholder": "Briefly describe your professional experience…"},
            ),
            "areas_of_interest": forms.TextInput(
                attrs={"class": _input,
                       "placeholder": "e.g. soil science, agroecology, dairy value chains"},
            ),
            "gender": forms.Select(attrs={"class": _input}),
            "date_of_birth": forms.DateInput(
                attrs={"type": "date", "class": _input, "autocomplete": "bday"},
            ),
        }

    def __init__(self, *args, user=None, **kwargs):
        self.user = user
        super().__init__(*args, **kwargs)
        if user:
            self.fields["first_name"].initial = user.first_name
            self.fields["last_name"].initial = user.last_name
            self.fields["phone"].initial = user.phone

    # WS5.4 / FRREP-UR007 — PRD pins avatars to JPEG/PNG only and a 2MB cap.
    # The shared UploadHandler accepts the broader image set (gif/webp/svg up
    # to 10MB), so we tighten validation here for the profile form specifically.
    AVATAR_MAX_BYTES = 2 * 1024 * 1024  # 2 MB
    AVATAR_ALLOWED_EXTS = {".jpg", ".jpeg", ".png"}
    AVATAR_ALLOWED_MIMES = {"image/jpeg", "image/png"}

    def clean_avatar(self):
        f = self.cleaned_data.get("avatar")
        if not f:
            return f
        # Use the existing handler for the format-and-malware sanity pass.
        UploadHandler(module="rims", allowed_categories=["image"]).handle(f)

        import os

        _, ext = os.path.splitext(f.name.lower())
        if ext not in self.AVATAR_ALLOWED_EXTS:
            raise forms.ValidationError("Profile photos must be JPEG or PNG.")

        # MIME check — UploadHandler already inspects content via `magic`,
        # but it allows the full image category. Re-inspect just the first
        # bytes here so a renamed .jpg can't sneak through.
        try:
            import magic

            f.seek(0)
            sniffed = magic.from_buffer(f.read(2048), mime=True)
            f.seek(0)
            if sniffed not in self.AVATAR_ALLOWED_MIMES:
                raise forms.ValidationError("Profile photo must be a JPEG or PNG file.")
        except ImportError:
            # python-magic unavailable in some environments; fall back to ext check.
            pass

        if getattr(f, "size", 0) > self.AVATAR_MAX_BYTES:
            raise forms.ValidationError(
                f"Profile photo must be 2 MB or smaller (got {f.size / (1024 * 1024):.1f} MB)."
            )
        return f

    def save(self, commit=True):
        profile = super().save(commit=False)
        if self.user:
            self.user.first_name = self.cleaned_data.get("first_name") or ""
            self.user.last_name = self.cleaned_data.get("last_name") or ""
            self.user.phone = self.cleaned_data.get("phone") or ""
            if commit:
                self.user.save(update_fields=["first_name", "last_name", "phone"])
        if commit:
            profile.save()
        return profile


class QualificationEntryForm(forms.Form):
    """One row of the profile's repeatable "Academic qualifications" list
    (`UserProfile.qualifications`, a free-form JSON list of dicts). Not a
    ModelForm — there's no separate model, each submission adds or updates
    one dict in the list. See qualification_save_view."""

    institution = forms.CharField(max_length=200, widget=forms.TextInput(attrs={"class": _input}))
    qualification = forms.CharField(
        max_length=200,
        widget=forms.TextInput(attrs={"class": _input, "placeholder": "e.g. BSc Agricultural Economics"}),
    )
    field_of_study = forms.CharField(max_length=200, required=False, widget=forms.TextInput(attrs={"class": _input}))
    year = forms.IntegerField(
        required=False,
        min_value=1950,
        max_value=2100,
        widget=forms.NumberInput(attrs={"class": _input}),
    )


class ExperienceEntryForm(forms.Form):
    """One row of the profile's repeatable "Work experience" list
    (`UserProfile.experience`). Same shape as QualificationEntryForm — see
    experience_save_view."""

    organization = forms.CharField(max_length=200, widget=forms.TextInput(attrs={"class": _input}))
    title = forms.CharField(max_length=200, widget=forms.TextInput(attrs={"class": _input}))
    start_year = forms.IntegerField(
        required=False, min_value=1950, max_value=2100, widget=forms.NumberInput(attrs={"class": _input})
    )
    end_year = forms.IntegerField(
        required=False, min_value=1950, max_value=2100, widget=forms.NumberInput(attrs={"class": _input})
    )
    is_current = forms.BooleanField(required=False)
    description = forms.CharField(
        required=False,
        widget=forms.Textarea(attrs={"class": _input, "rows": 3, "placeholder": "What did you do in this role?"}),
    )

    def clean(self):
        cleaned = super().clean()
        start_year = cleaned.get("start_year")
        end_year = cleaned.get("end_year")
        if cleaned.get("is_current"):
            cleaned["end_year"] = None
        elif start_year and end_year and end_year < start_year:
            raise ValidationError("End year can't be before the start year.")
        return cleaned


class ProfileVisibilityForm(forms.ModelForm):
    """WS5.2 / FRREP-UR012 — per-field visibility toggles for the user's
    public profile. Lives on its own form (not merged into ProfileForm) so the
    Privacy section can be POSTed independently and so a future admin/staff
    "view this profile as another user would see it" tool can reuse the model
    without inheriting unrelated form fields.
    """

    class Meta:
        model = ProfileVisibility
        fields = (
            "appear_in_directory",
            "show_email",
            "show_phone",
            "show_country",
            "show_institution",
            "show_degree_level",
            "show_qualifications",
            "show_experience",
            "show_professional_background",
            "show_areas_of_interest",
            "show_bio",
        )
