from datetime import timedelta
from itertools import groupby, islice
from operator import attrgetter

from django import forms
from django.conf import settings
from django.db import transaction
from django.db.models import Count
from django.forms.models import ModelChoiceIterator, ModelChoiceIteratorValue
from django.utils.timezone import localdate, localtime
from django.utils.translation import gettext_lazy as _

from club.forms import ClubRoleChoiceField
from club.models import ClubRole, Membership
from club.widgets.ajax_select import AutoCompleteSelectMultipleClub
from core.models import User
from core.views.forms import SelectDateTime
from core.views.widgets.ajax_select import (
    AutoCompleteSelect,
    AutoCompleteSelectMultipleGroup,
    AutoCompleteSelectUser,
)
from core.views.widgets.markdown import MarkdownInput
from election.models import Candidature, Election, ElectionList, Role


class LimitedCheckboxField(forms.ModelMultipleChoiceField):
    """A `ModelMultipleChoiceField`, with a max limit of selectable inputs."""

    def __init__(self, queryset, max_choice, **kwargs):
        self.max_choice = max_choice
        super().__init__(queryset, **kwargs)

    def clean(self, value):
        qs = super().clean(value)
        self.validate(qs)
        return qs

    def validate(self, qs):
        if qs.count() > self.max_choice:
            raise forms.ValidationError(
                _("You have selected too many candidates."), code="invalid"
            )


class CandidateForm(forms.ModelForm):
    """Form to candidate."""

    required_css_class = "required"

    class Meta:
        model = Candidature
        fields = ["user", "role", "program", "election_list"]
        labels = {
            "user": _("User to candidate"),
        }
        widgets = {
            "program": MarkdownInput,
            "user": AutoCompleteSelectUser,
            "role": AutoCompleteSelect,
            "election_list": AutoCompleteSelect,
        }

    def __init__(self, *args, election: Election, can_edit: bool = False, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["role"].queryset = election.roles.select_related("election")
        self.fields["election_list"].queryset = election.election_lists.all()
        if not can_edit:
            self.fields["user"].widget = forms.HiddenInput()


class VoteForm(forms.Form):
    def __init__(self, election: Election, user: User, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for role in election.roles.all():
            cand = role.candidatures
            if role.max_choice > 1:
                self.fields[role.title] = LimitedCheckboxField(
                    cand, role.max_choice, required=False
                )
            else:
                self.fields[role.title] = forms.ModelChoiceField(
                    cand,
                    required=False,
                    widget=forms.RadioSelect(),
                    empty_label=_("Blank vote"),
                    blank=True,
                )


class RoleForm(forms.ModelForm):
    """Form for creating a role."""

    required_css_class = "required"
    error_css_class = "error"

    class Meta:
        model = Role
        fields = ["club_role", "title", "description", "max_choice"]
        field_classes = {"club_role": ClubRoleChoiceField}

    def __init__(self, *args, election: Election, **kwargs):
        super().__init__(*args, **kwargs)
        self.instance.election = election
        self.fields["club_role"].queryset = ClubRole.objects.filter(
            is_board=True, club__in=election.clubs.all()
        )


class ElectionListForm(forms.ModelForm):
    class Meta:
        model = ElectionList
        fields = ("title", "election")
        widgets = {"election": AutoCompleteSelect}

    def __init__(self, *args, election: Election, **kwargs):
        super().__init__(*args, **kwargs)
        self.instance.election = election


class ElectionForm(forms.ModelForm):
    required_css_class = "required"
    error_css_class = "error"

    class Meta:
        model = Election
        fields = [
            "title",
            "description",
            "clubs",
            "archived",
            "start_candidature",
            "end_candidature",
            "start_date",
            "end_date",
            "edit_groups",
            "view_groups",
            "vote_groups",
            "candidature_groups",
        ]
        widgets = {
            "clubs": AutoCompleteSelectMultipleClub,
            "edit_groups": AutoCompleteSelectMultipleGroup,
            "view_groups": AutoCompleteSelectMultipleGroup,
            "vote_groups": AutoCompleteSelectMultipleGroup,
            "candidature_groups": AutoCompleteSelectMultipleGroup,
            "start_date": SelectDateTime,
            "end_date": SelectDateTime,
            "start_candidature": SelectDateTime,
            "end_candidature": SelectDateTime,
        }


class ElectionCreateForm(ElectionForm):
    """ElectionForm, but specifically for creation."""

    def __init__(self, *args, initial: dict | None = None, **kwargs):
        # propose sound default timestamps :
        # start of candidatures at tomorrow 00h01, start of votes a week later.
        start = localtime().replace(hour=0, minute=1, second=0) + timedelta(days=1)
        default_initial = {
            "start_candidature": start,
            "end_candidature": start + timedelta(days=7, minutes=-2),  # 23h59
            "start_date": start + timedelta(days=7),  # 00h01
            "end_date": start + timedelta(days=14, minutes=-2),  # 23h59
            "view_groups": [settings.SITH_GROUP_PUBLIC_ID],
            "vote_groups": [settings.SITH_GROUP_SUBSCRIBERS_ID],
            "candidature_groups": [settings.SITH_GROUP_SUBSCRIBERS_ID],
        }
        if initial:
            default_initial.update(initial)
        super().__init__(*args, initial=default_initial, **kwargs)

    def save(self, commit=True):  # noqa: FBT002
        instance = super().save(commit=commit)
        if commit:
            ElectionList.objects.create(title="Candidat⸱e libre", election=instance)
        return instance


class ElectionWinnerChoiceIterator(ModelChoiceIterator):
    """Iterate over the candidates that gathered enough votes"""

    def __iter__(self):
        # for each role, yield only the N first candidates,
        # where N is the election role max_choice
        qs = (
            self.queryset.annotate(nb_votes=Count("votes"))
            .order_by("role__order", "-nb_votes")
            .select_related("role", "user", "role__club_role", "role__club_role__club")
        )
        yield from (
            (
                f"{role.title} \u2013 {role.club_role.club.name}",
                [self.choice(cand) for cand in islice(candidates, role.max_choice)],
            )
            for role, candidates in groupby(qs, key=attrgetter("role"))
        )

    def choice(self, obj: Candidature):
        return (
            ModelChoiceIteratorValue(self.field.prepare_value(obj), obj),
            obj.user.get_full_name(),
        )


class ElectionWinnerChoiceField(forms.ModelMultipleChoiceField):
    """Custom `ModelChoiceField` for `[ClubRole][club.models.ClubRole]`.

    If only one club is involved, behave like the base `ModelChoiceField`.
    If dealing with the roles of multiple clubs, group the roles
    into a different `optgroup` for each club.
    """

    iterator = ElectionWinnerChoiceIterator
    widget = forms.CheckboxSelectMultiple


class ApplyElectionResultForm(forms.Form):
    """Form to select winners of an election, and automatically apply the results."""

    candidates = ElectionWinnerChoiceField(Candidature.objects.none())

    def __init__(self, *args, election: Election, **kwargs):
        self.election = election
        super().__init__(*args, **kwargs)
        qs = Candidature.objects.filter(
            role__election=election, role__club_role__isnull=False
        )
        # pass all candidates to the ModelChoiceField ;
        # its inner choice iterator will take care of filtering only the winners.
        self.fields["candidates"].queryset = qs
        # By default, mark every candidate as selected.
        # Election results are usually completely validated during the AG,
        # so it makes more sense UX-wise to eventually unselect a candidate
        # than to select everyone.
        self.fields["candidates"].initial = qs.values_list("id", flat=True)

    def save(self):
        if self.errors:
            return
        candidates: list[Candidature] = list(self.cleaned_data["candidates"])
        with transaction.atomic():
            Membership.objects.filter(
                role__in=[c.role.club_role for c in candidates],
                end_date=None,
                start_date__lt=self.election.end_date,
            ).update(end_date=localdate())
            memberships = [
                Membership(
                    user_id=c.user_id,
                    club_id=c.role.club_role.club_id,
                    role=c.role.club_role,
                    description=(
                        c.role.title if c.role.title != c.role.club_role.name else ""
                    ),
                )
                for c in candidates
            ]
            Membership.objects.bulk_create(memberships)
            Membership._add_club_groups(memberships)
        return memberships
