__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"


import datetime
from typing import Any, Dict

from django import forms
from django.contrib.sessions.backends.db import SessionStore
from django.db.models import F, Q, Count, OuterRef, Subquery
from django.db.models.functions import Coalesce

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit, HTML
from crispy_bootstrap5.bootstrap5 import FloatingField
from dal import autocomplete
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import timedelta

from ontology.models import Specialty
from ontology.models.academic_field import AcademicField
from proceedings.models import Proceedings
from profiles.models import Profile
from submissions.models import Submission
from submissions.models.assignment import EditorialAssignment
from submissions.models.qualification import Qualification
from submissions.models.recommendation import EICRecommendation
from scipost.forms import RequestFormMixin
from scipost.models import Contributor

from colleges.permissions import is_edadmin

from .models import (
    College,
    Fellowship,
    PotentialFellowship,
    PotentialFellowshipEvent,
    FellowshipNomination,
    FellowshipNominationComment,
    FellowshipNominationDecision,
    FellowshipNominationVotingRound,
    FellowshipNominationEvent,
    FellowshipInvitation,
)
from .constants import (
    POTENTIAL_FELLOWSHIP_IDENTIFIED,
    POTENTIAL_FELLOWSHIP_NOMINATED,
    POTENTIAL_FELLOWSHIP_EVENT_DEFINED,
    POTENTIAL_FELLOWSHIP_EVENT_NOMINATED,
)
from .utils import check_profile_eligibility_for_fellowship


class CollegeChoiceForm(forms.Form):
    college = forms.ModelChoiceField(queryset=College.objects.all())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                FloatingField("college"),
            )
        )


class FellowshipSearchForm(forms.Form):
    college = forms.ModelChoiceField(
        queryset=College.objects.all(),
        widget=forms.HiddenInput(),
    )
    specialty = forms.ModelChoiceField(
        queryset=Specialty.objects.all(),
        label="Specialty",
        required=False,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if "initial" in kwargs:
            self.fields["specialty"].queryset = Specialty.objects.filter(
                acad_field=kwargs["initial"].get("college").acad_field
            )
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                FloatingField("college"),
            ),
            Div(
                FloatingField("specialty"),
            ),
        )

    def search_results(self):
        fellowships = Fellowship.objects.active()
        if self.initial and self.initial.get("college", None):
            fellowships = fellowships.filter(college=self.initial["college"])
        if hasattr(self, "cleaned_data"):
            if self.cleaned_data.get("college"):
                fellowships = fellowships.filter(
                    college=self.cleaned_data.get("college")
                )
            if self.cleaned_data.get("specialty"):
                fellowships = fellowships.filter(
                    contributor__profile__specialties__in=[
                        self.cleaned_data.get("specialty"),
                    ]
                )
        return fellowships


class FellowshipSelectForm(forms.Form):
    fellowship = forms.ModelChoiceField(
        queryset=Fellowship.objects.all(),
        widget=autocomplete.ModelSelect2(url="/colleges/fellowship-autocomplete"),
        help_text=("Start typing, and select from the popup."),
    )


class FellowshipDynSelForm(forms.Form):
    q = forms.CharField(max_length=32, label="Search (by name)")
    action_url_name = forms.CharField()
    action_url_base_kwargs = forms.JSONField(required=False)
    action_target_element_id = forms.CharField()
    action_target_swap = forms.CharField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            FloatingField("q", autocomplete="off"),
            Field("action_url_name", type="hidden"),
            Field("action_url_base_kwargs", type="hidden"),
            Field("action_target_element_id", type="hidden"),
            Field("action_target_swap", type="hidden"),
        )

    def search_results(self):
        if q := self.cleaned_data["q"]:
            fellowships = Fellowship.objects.filter(
                Q(contributor__profile__last_name__unaccent__icontains=q)
                | Q(contributor__profile__first_name__unaccent__icontains=q)
            ).distinct()
            return fellowships
        else:
            return Fellowship.objects.none()


class FellowshipForm(forms.ModelForm):
    class Meta:
        model = Fellowship
        fields = (
            "college",
            "contributor",
            "start_date",
            "until_date",
            "status",
        )
        help_texts = {
            "status": "[select if this is a regular, senior or guest Fellowship]"
        }
        widgets = {
            "start_date": forms.DateInput(attrs={"type": "date"}),
            "until_date": forms.DateInput(attrs={"type": "date"}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["contributor"].disabled = True

    def clean(self):
        super().clean()
        start = self.cleaned_data.get("start_date")
        until = self.cleaned_data.get("until_date")
        if start and until:
            if until <= start:
                self.add_error(
                    "until_date", "The given dates are not in chronological order."
                )


class FellowshipTerminateForm(forms.ModelForm):
    class Meta:
        model = Fellowship
        fields = []

    def save(self):
        today = datetime.date.today()
        fellowship = self.instance
        if not fellowship.until_date or fellowship.until_date > today:
            fellowship.until_date = today
        return fellowship.save()


class FellowshipRemoveSubmissionForm(forms.ModelForm):
    """
    Use this form in admin-accessible views only! It could possibly reveal the
    identity of the Editor-in-charge!
    """

    class Meta:
        model = Fellowship
        fields = []

    def __init__(self, *args, **kwargs):
        self.submission = kwargs.pop("submission")
        super().__init__(*args, **kwargs)

    def clean(self):
        if self.submission.editor_in_charge == self.instance.contributor:
            self.add_error(
                None,
                (
                    "Submission cannot be removed as the Fellow is"
                    " Editor-in-charge of this Submission."
                ),
            )

    def save(self):
        fellowship = self.instance
        fellowship.pool.remove(self.submission)
        return fellowship


class FellowshipAddSubmissionForm(forms.ModelForm):
    submission = forms.ModelChoiceField(
        queryset=Submission.objects.none(),
        empty_label="Please choose the Submission to add to the pool",
    )

    class Meta:
        model = Fellowship
        fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        pool = self.instance.pool.values_list("id", flat=True)
        self.fields["submission"].queryset = Submission.objects.exclude(id__in=pool)

    def save(self):
        submission = self.cleaned_data["submission"]
        fellowship = self.instance
        fellowship.pool.add(submission)
        return fellowship


class SubmissionAddFellowshipForm(forms.ModelForm):
    fellowship = forms.ModelChoiceField(
        queryset=None,
        to_field_name="id",
        empty_label="Please choose the Fellow to add to this Submission's Fellowship",
    )

    class Meta:
        model = Submission
        fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        pool = self.instance.fellows.values_list("id", flat=True)
        self.fields["fellowship"].label = ""
        self.fields["fellowship"].queryset = (
            Fellowship.objects.active()
            .filter(college=self.instance.submitted_to.college)
            .exclude(id__in=pool)
        )
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                Div(Field("fellowship"), css_class="col-lg-6"),
                Div(
                    Submit(
                        "submit",
                        "Add",
                    ),
                    css_class="col-lg-6",
                ),
                css_class="row",
            )
        )

    def save(self):
        fellowship = self.cleaned_data["fellowship"]
        submission = self.instance
        submission.fellows.add(fellowship)
        return submission


class FellowshipRemoveProceedingsForm(forms.ModelForm):
    """
    Use this form in admin-accessible views only! It could possibly reveal the
    identity of the Editor-in-charge!
    """

    class Meta:
        model = Fellowship
        fields = []

    def __init__(self, *args, **kwargs):
        self.proceedings = kwargs.pop("proceedings")
        super().__init__(*args, **kwargs)

    def clean(self):
        if self.proceedings.lead_fellow == self.instance:
            self.add_error(
                None, "Fellowship cannot be removed as it is assigned as lead fellow."
            )

    def save(self):
        fellowship = self.instance
        self.proceedings.fellowships.remove(fellowship)
        return fellowship


class FellowshipAddProceedingsForm(forms.ModelForm):
    proceedings = forms.ModelChoiceField(
        queryset=None,
        to_field_name="id",
        empty_label="Please choose the Proceedings to add to the Pool",
    )

    class Meta:
        model = Fellowship
        fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        proceedings = self.instance.proceedings.values_list("id", flat=True)
        self.fields["proceedings"].queryset = Proceedings.objects.exclude(
            id__in=proceedings
        )

    def save(self):
        proceedings = self.cleaned_data["proceedings"]
        fellowship = self.instance
        proceedings.fellowships.add(fellowship)
        return fellowship


class PotentialFellowshipForm(RequestFormMixin, forms.ModelForm):
    profile = forms.ModelChoiceField(
        queryset=Profile.objects.all(),
        widget=autocomplete.ModelSelect2(url="/profiles/profile-autocomplete"),
    )

    class Meta:
        model = PotentialFellowship
        fields = ["college", "profile"]

    def clean_profile(self):
        """Check that no preexisting PotentialFellowship exists."""
        cleaned_profile = self.cleaned_data["profile"]
        if cleaned_profile.potentialfellowship_set.all():
            self.add_error(
                "profile",
                "This profile already has a PotentialFellowship. Update that instead.",
            )
        return cleaned_profile

    def save(self):
        """
        The default status is IDENTIFIED, which is appropriate
        if the PotentialFellow was added directly by SciPost Admin.
        But if the PotFel is nominated by somebody on the Advisory Board
        or by an existing Fellow, the status is set to NOMINATED and
        the person nominating is added to the list of in_agreement with election.
        """
        potfel = super().save()
        nominated = self.request.user.groups.filter(
            name__in=["Advisory Board", "Editorial College"]
        ).exists()
        if nominated:
            potfel.status = POTENTIAL_FELLOWSHIP_NOMINATED
            # If user is Senior Fellow for that College, auto-add Agree vote
            if (
                self.request.user.contributor.fellowships.senior()
                .filter(college=potfel.college)
                .exists()
            ):
                potfel.in_agreement.add(self.request.user.contributor)
            event = POTENTIAL_FELLOWSHIP_EVENT_NOMINATED
        else:
            potfel.status = POTENTIAL_FELLOWSHIP_IDENTIFIED
            event = POTENTIAL_FELLOWSHIP_EVENT_DEFINED
        potfel.save()
        newevent = PotentialFellowshipEvent(
            potfel=potfel, event=event, noted_by=self.request.user.contributor
        )
        newevent.save()
        return potfel


class PotentialFellowshipStatusForm(forms.ModelForm):
    class Meta:
        model = PotentialFellowship
        fields = ["status"]


class PotentialFellowshipEventForm(forms.ModelForm):
    class Meta:
        model = PotentialFellowshipEvent
        fields = ["event", "comments"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["comments"].widget.attrs.update(
            {
                "placeholder": "NOTA BENE: careful, will be visible to all who have voting rights"
            }
        )


###############
# Nominations #
###############


class FellowshipNominationForm(forms.ModelForm):
    class Meta:
        model = FellowshipNomination
        fields = ["nominated_by", "college", "nominator_comments"]  # hidden  # visible

    specialties = forms.MultipleChoiceField(
        choices=[],
        label="Specialties",
        required=False,
        widget=forms.CheckboxSelectMultiple(),
    )

    def __init__(self, *args, **kwargs):
        self.profile = kwargs.pop("profile")
        super().__init__(*args, **kwargs)
        self.fields["college"].queryset = College.objects.filter(
            acad_field=self.profile.acad_field,
            # id__in=Fellowship.objects.active()
            # .filter(contributor=self.fields["nominated_by"].initial)
            # .values_list("college", flat=True),
        )
        self.fields["college"].empty_label = None
        self.fields["nominator_comments"].label = False
        self.fields["nominator_comments"].widget = forms.Textarea(
            attrs={
                "rows": 4,
                "placeholder": "Please provide a short motivation for the nomination, "
                "as well as a personal website or external profile page to help us identify the nominee.",
            }
        )
        self.fields["nominator_comments"].required = True

        self.fields["specialties"].choices = [
            (s.pk, s.name)
            for s in Specialty.objects.filter(acad_field=self.profile.acad_field)
        ]
        self.fields["specialties"].initial = list(
            self.profile.specialties.all().values_list("pk", flat=True)
        )

        self.helper = FormHelper()
        self.helper.layout = Layout(
            Field("profile_id", type="hidden"),
            Field("nominated_by", type="hidden"),
            Div(
                Div(Field("nominator_comments"), css_class="col-lg-8"),
                Div(
                    FloatingField("college"),
                    ButtonHolder(
                        Submit(
                            "submit", "Nominate", css_class="btn btn-success float-end"
                        )
                    ),
                    css_class="col-lg-4",
                ),
                Div(
                    Field(
                        "specialties",
                        css_class="border border-secondary p-2 d-flex flex-wrap gap-3",
                    ),
                    css_class="col-12",
                ),
                css_class="row pt-1",
            ),
        )

    def clean(self):
        data = super().clean()
        failed_eligibility_criteria = check_profile_eligibility_for_fellowship(
            self.profile
        )
        if failed_eligibility_criteria:
            for criterion in failed_eligibility_criteria:
                self.add_error(None, criterion)
        if data["college"].acad_field != self.profile.acad_field:
            self.add_error(
                "college", "Mismatch between college.acad_field and profile.acad_field."
            )
        if (not is_edadmin(data["nominated_by"].user)) and (
            data["college"].id
            not in Fellowship.objects.active()
            .filter(contributor=data["nominated_by"])
            .values_list("college", flat=True)
        ):
            self.add_error(
                "college",
                "You do not have an active Fellowship in the selected College.",
            )

        profile_specialties_of_field = self.profile.specialties.filter(
            acad_field=data["college"].acad_field
        )
        if profile_specialties_of_field.count() == 0 and len(data["specialties"]) == 0:
            self.add_error(
                None,
                "You must denote at least one specialty for the nominee in their nominated college.",
            )
        return data

    def save(self):
        nomination = super().save(commit=False)
        nomination.profile = self.profile
        # add specialties to profile
        nomination.profile.specialties.add(*self.cleaned_data["specialties"])
        nomination.save()
        return nomination


# class FellowshipNominationSearchForm(forms.Form):
#     """Filter a FellowshipNomination queryset using basic search fields."""

#     college = forms.ModelChoiceField(queryset=College.objects.all(), required=False)
#     specialty = forms.ModelChoiceField(
#         queryset=Specialty.objects.all(),
#         label="Specialty",
#         required=False,
#     )
#     name = forms.CharField(max_length=128, required=False)

#     def __init__(self, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         self.helper = FormHelper()
#         self.helper.layout = Layout(
#             Div(
#                 FloatingField("category"),
#                 css_class="row",
#             ),
#             Div(
#                 Div(FloatingField("college"), css_class="col-lg-6"),
#                 Div(FloatingField("specialty"), css_class="col-lg-6"),
#                 css_class="row",
#             ),
#             Div(
#                 Div(FloatingField("name", autocomplete="off"), css_class="col-lg-6"),
#                 css_class="row",
#             ),
#         )

#     def search_results(self):
#         if self.cleaned_data.get("name"):
#             nominations = FellowshipNomination.objects.filter(
#                 Q(profile__last_name__icontains=self.cleaned_data.get("name"))
#                 | Q(profile__first_name__icontains=self.cleaned_data.get("name"))
#             )
#         else:
#             nominations = FellowshipNomination.objects.all()
#         if self.cleaned_data.get("college"):
#             nominations = nominations.filter(college=self.cleaned_data.get("college"))
#         if self.cleaned_data.get("specialty"):
#             nominations = nominations.filter(
#                 profile__specialties__in=[
#                     self.cleaned_data.get("specialty"),
#                 ]
#             )
#         return nominations


class FellowshipNominationSearchForm(forms.Form):
    all_nominations = FellowshipNomination.objects.all()
    nomination_colleges = all_nominations.values_list("college", flat=True).distinct()

    nominee = forms.CharField(max_length=100, required=False, label="Nominee")

    college = forms.MultipleChoiceField(
        choices=College.objects.filter(id__in=nomination_colleges)
        .order_by("name")
        .values_list("id", "name"),
        required=False,
    )

    decision = forms.ChoiceField(
        choices=[("", "Any"), ("pending", "Pending")]
        + FellowshipNominationDecision.OUTCOME_CHOICES,
        required=False,
    )

    invitation_response = forms.ChoiceField(
        choices=[("", "Any")] + FellowshipInvitation.RESPONSE_CHOICES,
        required=False,
    )

    can_vote = forms.BooleanField(
        label="I can vote",
        required=False,
        initial=True,
    )
    voting_open = forms.BooleanField(
        label="Voting open now",
        required=False,
        initial=True,
    )
    has_rounds = forms.BooleanField(
        label="Has voting rounds",
        required=False,
        initial=True,
    )
    needs_specialties = forms.BooleanField(
        label="Needs specialties",
        required=False,
        initial=False,
    )
    needs_edadmin_attention = forms.BooleanField(
        label="Needs EdAdmin attention",
        required=False,
        initial=False,
    )

    orderby = forms.ChoiceField(
        label="Order by",
        choices=(
            ("latest_round_deadline", "Deadline"),
            ("latest_round_open", "Voting start"),
            ("latest_round_decision_outcome", "Decision"),
            ("profile__last_name", "Nominee"),
            ("nominated_on", "Nominated date"),
            ("latest_event_on", "Last event date"),
        ),
        required=False,
    )
    ordering = forms.ChoiceField(
        label="Ordering",
        choices=(
            ("+", "Ascending"),
            ("-", "Descending"),
        ),
        required=False,
    )

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

        # Set the initial values of the form fields from the session data
        if self.session_key:
            session = SessionStore(session_key=self.session_key)

            for field in self.fields:
                if field in session:
                    self.fields[field].initial = session[field]

        self.helper = FormHelper()

        div_block_ordering = Div(
            Div(FloatingField("orderby"), css_class="col-6 col-md-12 col-xl-6"),
            Div(FloatingField("ordering"), css_class="col-6 col-md-12 col-xl-6"),
            css_class="row mb-0",
        )
        div_block_checkbox = Div(
            Div(Field("can_vote"), css_class="col-auto col-lg-12 col-xl-auto"),
            Div(Field("voting_open"), css_class="col-auto col-lg-12 col-xl-auto"),
            Div(Field("has_rounds"), css_class="col-auto col-lg-12 col-xl-auto"),
            Div(Field("needs_specialties"), css_class="col-auto col-lg-12 col-xl-auto"),
            css_class="row mb-0",
        )

        if is_edadmin(self.user):
            div_block_checkbox.append(
                Div(
                    Field("needs_edadmin_attention"),
                    css_class="col-auto col-lg-12 col-xl-auto",
                )
            )

        self.helper.layout = Layout(
            Div(
                Div(
                    Div(
                        Div(FloatingField("nominee"), css_class="col-12 col-lg-6"),
                        Div(FloatingField("decision"), css_class="col-6 col-lg-3"),
                        Div(
                            FloatingField("invitation_response"),
                            css_class="col-6 col-lg-3",
                        ),
                        Div(div_block_ordering, css_class="col-12 col-md-6 col-xl-12"),
                        Div(div_block_checkbox, css_class="col-12 col-md-6 col-xl-12"),
                        css_class="row mb-0",
                    ),
                    css_class="col",
                ),
                Div(
                    Field("college", size=6),
                    css_class="col-12 col-md-6 col-lg-4",
                ),
                css_class="row mb-0",
            ),
        )

    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
        # Apply the filter set to the form
        for key in self.fields:
            if key in filters:
                self.fields[key].initial = filters[key]
            elif none_on_empty:
                if isinstance(self.fields[key], forms.MultipleChoiceField):
                    self.fields[key].initial = []
                else:
                    self.fields[key].initial = None

    def search_results(self):
        # Save the form data to the session
        if self.session_key is not None:
            session = SessionStore(session_key=self.session_key)

            for key in self.cleaned_data:
                session[key] = self.cleaned_data.get(key)

            session.save()

        def latest_round_subquery(key):
            return Subquery(
                FellowshipNominationVotingRound.objects.filter(
                    nomination=OuterRef("pk")
                )
                .order_by("-voting_deadline")
                .values(key)[:1]
            )

        def latest_event_subquery(key):
            return Subquery(
                FellowshipNominationEvent.objects.filter(nomination=OuterRef("pk"))
                .order_by("-on")
                .values(key)[:1]
            )

        nominations = (
            FellowshipNomination.objects.all()
            .annotate(
                latest_round_deadline=latest_round_subquery("voting_deadline"),
                latest_round_open=latest_round_subquery("voting_opens"),
                latest_round_decision_outcome=latest_round_subquery(
                    "decision__outcome"
                ),
                latest_event_on=latest_event_subquery("on"),
                latest_event_description=latest_event_subquery("description"),
            )
            .distinct()
        )

        if self.cleaned_data.get("can_vote"):
            # Restrict rounds to those the user can vote on
            nominations = nominations.with_user_votable_rounds(self.user).distinct()

        if nominee := self.cleaned_data.get("nominee"):
            nominations = nominations.filter(
                Q(profile__first_name__unaccent__icontains=nominee)
                | Q(profile__last_name__unaccent__icontains=nominee)
            )
        if college := self.cleaned_data.get("college"):
            nominations = nominations.filter(college__id__in=college)
        if decision := self.cleaned_data.get("decision"):
            if decision == "pending":
                nominations = nominations.filter(
                    voting_rounds__decision__isnull=True,
                )
            else:
                nominations = nominations.filter(
                    voting_rounds__decision__outcome=decision,
                )
        if invitation_response := self.cleaned_data.get("invitation_response"):
            nominations = nominations.filter(
                invitation__response=invitation_response,
            )
        if self.cleaned_data.get("voting_open"):
            nominations = nominations.filter(
                Q(voting_rounds__voting_opens__lte=timezone.now())
                & Q(voting_rounds__voting_deadline__gte=timezone.now())
            )
        if self.cleaned_data.get("has_rounds"):
            nominations = nominations.filter(voting_rounds__isnull=False)
        if self.cleaned_data.get("needs_specialties"):
            nominations = nominations.filter(profile__specialties__isnull=True)

        # Ordering of nominations
        # Only order if both fields are set
        if (orderby_value := self.cleaned_data.get("orderby")) and (
            ordering_value := self.cleaned_data.get("ordering")
        ):
            # Remove the + from the ordering value, causes a Django error
            ordering_value = ordering_value.replace("+", "")

            # Ordering string is built by the ordering (+/-), and the field name
            # from the orderby field split by "," and joined together
            nominations = nominations.order_by(
                *[
                    ordering_value + order_part
                    for order_part in orderby_value.split(",")
                ]
            )

        # Render the queryset to evaluate properties
        nominations = list(nominations)

        if self.cleaned_data.get("needs_edadmin_attention"):
            nominations = [
                nomination for nomination in nominations if nomination.edadmin_notes
            ]

        return nominations


class FellowshipNominationCommentForm(forms.ModelForm):
    class Meta:
        model = FellowshipNominationComment
        fields = [
            "nomination",
            "by",
            "text",
            "on",
        ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.fields["text"].label = False
        self.fields
        self.helper.layout = Layout(
            Field("nomination", type="hidden"),
            Field("by", type="hidden"),
            Field("on", type="hidden"),
            Div(
                Div(
                    Field(
                        "text",
                        placeholder="Add a comment (visible to EdAdmin and Senior Fellows)",
                        rows=2,
                    ),
                ),
                Div(ButtonHolder(Submit("submit", "Add comment"))),
                css_class="row",
            ),
        )


class FellowshipNominationDecisionForm(forms.ModelForm):
    class Meta:
        model = FellowshipNominationDecision
        fields = [
            "voting_round",
            "outcome",
            "fixed_on",
            "comments",
        ]

        widgets = {
            "comments": forms.Textarea(attrs={"rows": 4}),
        }

    def __init__(self, *args, **kwargs):
        voting_round = kwargs.pop("voting_round", None)
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Field("voting_round", type="hidden"),
            Field("fixed_on", type="hidden"),
            Div(
                Div(Field("comments"), css_class="col-12 col-lg-8"),
                Div(
                    Field("outcome"),
                    ButtonHolder(Submit("submit", "Submit")),
                    css_class="col-12 col-lg-4",
                ),
                css_class="row",
            ),
        )
        if voting_round:
            self.fields["voting_round"].initial = voting_round
            self.fields["outcome"].initial = voting_round.vote_outcome

        if nomination := getattr(self.instance, "nomination", None):
            if voting_outcome := nomination.latest_voting_round.outcome:
                self.fields["outcome"].initial = voting_outcome


#################
# Voting Rounds #
#################


class FellowshipNominationVotingRoundSearchForm(forms.Form):
    all_rounds = FellowshipNominationVotingRound.objects.all()

    nominee = forms.CharField(max_length=100, required=False, label="Nominee")

    college = forms.MultipleChoiceField(
        choices=College.objects.all().order_by("name").values_list("id", "name"),
        required=False,
    )

    decision = forms.ChoiceField(
        choices=[("", "Any"), ("pending", "Pending")]
        + FellowshipNominationDecision.OUTCOME_CHOICES,
        required=False,
    )

    can_vote = forms.BooleanField(
        label="I can vote",
        required=False,
        initial=True,
    )
    voting_open = forms.BooleanField(
        label="Voting open",
        required=False,
        initial=True,
    )

    orderby = forms.ChoiceField(
        label="Order by",
        choices=(
            ("voting_deadline", "Deadline"),
            ("voting_opens", "Voting start"),
            ("decision__outcome", "Decision"),
            ("nomination__profile__last_name", "Nominee"),
        ),
        required=False,
    )
    ordering = forms.ChoiceField(
        label="Ordering",
        choices=(
            # FIXME: Emperically, the ordering appers to be reversed for dates?
            ("-", "Ascending"),
            ("+", "Descending"),
        ),
        required=False,
    )

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

        # Set the initial values of the form fields from the session data
        if self.session_key:
            session = SessionStore(session_key=self.session_key)

            for field in self.fields:
                if field in session:
                    self.fields[field].initial = session[field]

        self.helper = FormHelper()
        self.helper.layout = Layout(
            Div(
                Div(
                    Div(
                        Div(FloatingField("nominee"), css_class="col-6 col-lg-6"),
                        Div(FloatingField("decision"), css_class="col-3 col-lg-4"),
                        Div(
                            Div(
                                Div(Field("can_vote"), css_class="col-12"),
                                Div(Field("voting_open"), css_class="col-12"),
                                css_class="row mb-0",
                            ),
                            css_class="col-3 col-lg-2",
                        ),
                        Div(FloatingField("orderby"), css_class="col-6"),
                        Div(FloatingField("ordering"), css_class="col-6"),
                        css_class="row mb-0",
                    ),
                    css_class="col",
                ),
                Div(
                    Field("college", size=5),
                    css_class="col-12 col-md-6 col-lg-4",
                ),
                css_class="row mb-0",
            ),
        )

    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
        # Apply the filter set to the form
        for key in self.fields:
            if key in filters:
                self.fields[key].initial = filters[key]
            elif none_on_empty:
                if isinstance(self.fields[key], forms.MultipleChoiceField):
                    self.fields[key].initial = []
                else:
                    self.fields[key].initial = None

    def search_results(self):
        # Save the form data to the session
        if self.session_key is not None:
            session = SessionStore(session_key=self.session_key)

            for key in self.cleaned_data:
                session[key] = self.cleaned_data.get(key)

            session.save()

        rounds = FellowshipNominationVotingRound.objects.all()

        if self.cleaned_data.get("can_vote"):
            # or not self.user.has_perm("scipost.can_view_all_nomination_voting_rounds"):
            # Restrict rounds to those the user can vote on
            rounds = rounds.where_user_can_vote(self.user)

        if nominee := self.cleaned_data.get("nominee"):
            rounds = rounds.filter(
                Q(nomination__profile__first_name__icontains=nominee)
                | Q(nomination__profile__last_name__icontains=nominee)
            )
        if college := self.cleaned_data.get("college"):
            rounds = rounds.filter(nomination__college__id__in=college)
        if decision := self.cleaned_data.get("decision"):
            if decision == "pending":
                rounds = rounds.filter(decision__isnull=True)
            else:
                rounds = rounds.filter(decision__outcome=decision)
        if self.cleaned_data.get("voting_open"):
            rounds = rounds.filter(
                Q(voting_opens__lte=timezone.now())
                & Q(voting_deadline__gte=timezone.now())
            )

        # Ordering of voting rounds
        # Only order if both fields are set
        if (orderby_value := self.cleaned_data.get("orderby")) and (
            ordering_value := self.cleaned_data.get("ordering")
        ):
            # Remove the + from the ordering value, causes a Django error
            ordering_value = ordering_value.replace("+", "")

            # Ordering string is built by the ordering (+/-), and the field name
            # from the orderby field split by "," and joined together
            rounds = rounds.order_by(
                *[
                    ordering_value + order_part
                    for order_part in orderby_value.split(",")
                ]
            )

        return rounds


from datetime import date


class FellowshipNominationVotingRoundStartForm(forms.ModelForm):
    class Meta:
        model = FellowshipNominationVotingRound
        fields = ["type", "voting_opens", "voting_deadline"]

        widgets = {
            "voting_opens": forms.DateInput(attrs={"type": "date"}),
            "voting_deadline": forms.DateInput(attrs={"type": "date"}),
        }

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

        today = date.today()
        self.fields["voting_opens"].widget.attrs.update(
            {
                "min": today.strftime("%Y-%m-%d"),
                "value": today.strftime("%Y-%m-%d"),
            }
        )

        in_two_weeks = today + timedelta(days=14)
        self.fields["voting_deadline"].widget.attrs.update(
            {
                "min": today.strftime("%Y-%m-%d"),
                "value": in_two_weeks.strftime("%Y-%m-%d"),
            }
        )

        self.helper = FormHelper()
        self.helper.attrs = {
            "hx-target": f"#nomination-{self.instance.nomination.id}-round-tab-holder",
            "hx-swap": "outerHTML",
            "hx-post": reverse(
                "colleges:_hx_nomination_voting_rounds_tab",
                kwargs={
                    "nomination_id": self.instance.nomination.id,
                    "round_id": self.instance.id,
                },
            ),
        }
        self.helper.layout = Layout(
            Div(
                Div(Field("type"), css_class="col-2"),
                Div(Field("voting_opens"), css_class="col"),
                Div(Field("voting_deadline"), css_class="col"),
                Div(
                    ButtonHolder(Submit("submit", "Start")),
                    css_class="col-auto align-self-end mb-3",
                ),
                Div(
                    HTML(
                        "Tip: Set both dates to today and remove all fellows to take decision immediately."
                    ),
                    css_class="col-12 text-muted small",
                ),
                css_class="row mb-0",
            )
        )

    def clean(self):
        open_date = self.cleaned_data.get("voting_opens", None)
        deadline_date = self.cleaned_data.get("voting_deadline", None)

        if open_date is None or deadline_date is None:
            self.add_error(
                None,
                "Both the voting opens and voting deadline must be set.",
            )

        # Check that the voting deadline is after the voting opens
        if deadline_date <= open_date:
            self.add_error(
                "voting_deadline",
                "The voting deadline must be after the voting opens.",
            )

        # Check that the voting opens after today
        if open_date.date() < date.today():
            self.add_error(
                "voting_opens", "The voting opening date may not be in the past."
            )

        if self.instance.eligible_to_vote.count() == 0:
            # If both dates are set to today, then it is implied that
            # the voting round should never be opened and
            # the decision should be made by the foundation
            if open_date.date() == deadline_date.date() == date.today():
                yesterday = date.today() - timedelta(days=1)
                self.instance.voting_opens = yesterday
                self.instance.voting_deadline = yesterday
                self.instance.save()


class FellowshipNominationVetoForm(forms.Form):
    edadmin_comments = forms.CharField(
        label="Comments for editorial administration",
        widget=forms.Textarea(attrs={"rows": 4}),
        required=True,
    )

    fellow_comments = forms.CharField(
        label="Comments for voting Fellows",
        widget=forms.Textarea(attrs={"rows": 4}),
        required=True,
    )

    def __init__(self, *args, **kwargs):
        self.fellow = kwargs.pop("fellow", None)
        self.nomination = kwargs.pop("nomination", None)
        super().__init__(*args, **kwargs)

        self.helper = FormHelper()
        self.helper.attrs = {
            "hx-post": reverse(
                "colleges:_hx_nomination_veto",
                kwargs={"nomination_id": self.nomination.id},
            ),
            "hx-target": "closest .veto-btn-container",
            "hx-swap": "outerHTML",
        }

        self.helper.layout = Layout(
            Div(
                Div(Field("edadmin_comments"), css_class="col"),
                Div(Field("fellow_comments"), css_class="col"),
                Div(
                    ButtonHolder(Submit("submit", "Veto", css_class="btn btn-dark")),
                    css_class="col-auto d-flex align-items-end",
                ),
                css_class="row mb-0",
            ),
        )

    def save(self):
        self.nomination.vetoes.add(self.fellow)
        self.nomination.save()

        # Fellow's comments are added as a regular comment
        FellowshipNominationComment.objects.create(
            nomination=self.nomination,
            by=self.fellow.contributor,
            text=self.cleaned_data["fellow_comments"],
        )

        # EdAdmin's comments are added as an event
        FellowshipNominationEvent.objects.create(
            nomination=self.nomination,
            by=self.fellow.contributor,
            description=f"Vetoed with justification: {self.cleaned_data['edadmin_comments']}",
        )


###############
# Invitations #
###############
class FellowshipInvitationResponseForm(forms.ModelForm):
    class Meta:
        model = FellowshipInvitation
        fields = [
            "nomination",
            "response",
            "postponement_date",
            "comments",
        ]
        widgets = {
            "postponement_date": forms.DateInput(attrs={"type": "date"}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Field("nomination", type="hidden"),
            Div(
                Div(
                    Div(
                        Div(Field("response"), css_class="col-12"),
                        Div(Field("postponement_date"), css_class="col-12"),
                        css_class="row mb-0",
                    ),
                    css_class="col-12 col-md-5",
                ),
                Div(
                    Field(
                        "comments",
                        placeholder="Add a comment (visible to EdAdmin)",
                        rows=4,
                    ),
                    css_class="col-12 col-md-7",
                ),
                Div(ButtonHolder(Submit("submit", "Update")), css_class="col-auto"),
                css_class="row mb-0",
            ),
        )

    def clean(self):
        invitation_accepted = self.cleaned_data["response"] == (
            FellowshipInvitation.RESPONSE_ACCEPTED
        )
        invitation_postponed = self.cleaned_data["response"] in [
            FellowshipInvitation.RESPONSE_POSTPONED,
            FellowshipInvitation.RESPONSE_REINVITE_LATER,
        ]
        postponement_date = self.cleaned_data["postponement_date"]

        if postponement_date and (timezone.now().date() > postponement_date):
            self.add_error(
                "postponement_date",
                "You cannot set a postponed start date in the past.",
            )

        if (
            invitation_accepted
            and (postponement_date is not None)
            and (postponement_date != timezone.now().date())
        ):
            self.add_error(
                "postponement_date",
                "If the invitation is accepted for immediate start, you cannot postpone its start date.",
            )

        if invitation_postponed and not postponement_date:
            self.add_error(
                "postponement_date",
                "If the invitation is postponed, you must set a postponement date in the future.",
            )


class FellowshipsMonitorSearchForm(forms.Form):
    form_id = "fellowships-monitor-search-form"
    all_fellowships = Fellowship.objects.all()
    fellowships_colleges = all_fellowships.values_list("college", flat=True).distinct()
    fellowships_acad_fields = all_fellowships.values("college__acad_field")

    fellow = forms.CharField(max_length=100, required=False, label="Fellow")

    college = forms.MultipleChoiceField(
        choices=College.objects.filter(id__in=fellowships_colleges)
        .order_by("name")
        .values_list("id", "name"),
        required=False,
    )

    # Specialty multiple-choice grouped by the academic field that contains it.
    acad_fields = AcademicField.objects.filter(
        colleges__in=fellowships_acad_fields
    ).values_list("name", "id")
    specialty_grouped_choices = [
        (
            field_name,
            tuple(
                Specialty.objects.filter(acad_field=field_id).values_list("id", "name")
            ),
        )
        for (field_name, field_id) in acad_fields
    ]
    specialties = forms.MultipleChoiceField(
        choices=specialty_grouped_choices,
        label="Specialties",
        required=False,
    )

    date_from = forms.DateField(
        label="From date",
        widget=forms.DateInput(attrs={"type": "date"}),
        required=False,
    )
    date_to = forms.DateField(
        label="To date",
        widget=forms.DateInput(attrs={"type": "date"}),
        required=False,
    )

    has_regular = forms.BooleanField(
        label="Regular",
        required=False,
        initial=True,
    )
    has_senior = forms.BooleanField(
        label="Senior",
        required=False,
        initial=True,
    )
    has_guest = forms.BooleanField(
        label="Guest",
        required=False,
        initial=True,
    )
    show_expired = forms.BooleanField(
        label="Expired",
        required=False,
        initial=False,
    )

    orderby = forms.ChoiceField(
        label="Order by",
        choices=[
            ("", "-----"),
            ("contributor__profile__last_name", "Fellow"),
            ("nr_in_pool", "# in pool"),
            ("nr_appraised", "# appraised"),
            ("nr_assignments_completed", "# completed"),
            ("nr_assignments_ongoing", "# ongoing"),
            ("start_date", "Start date"),
            ("until_date", "End date"),
        ],
        initial="",
        required=False,
    )
    ordering = forms.ChoiceField(
        label="Ordering",
        choices=[
            ("-", "Descending"),
            ("+", "Ascending"),
        ],
        required=False,
    )

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

        # Set the initial values of the form fields from the session data
        if self.session_key:
            session = SessionStore(session_key=self.session_key)

            for field_key in self.fields:
                session_key = (
                    f"{self.form_id}_{field_key}"
                    if hasattr(self, "form_id")
                    else field_key
                )

                if session_value := session.get(session_key):
                    self.fields[field_key].initial = session_value

        self.helper = FormHelper()

        div_block_ordering = Div(
            Div(Field("orderby"), css_class="col-6"),
            Div(Field("ordering"), css_class="col-6"),
            css_class="row mb-0",
        )
        div_block_dates = Div(
            Div(Field("date_from"), css_class="col-6"),
            Div(Field("date_to"), css_class="col-6"),
            css_class="row mb-0",
        )
        div_block_fellow_types = Div(
            Div(Field("has_regular"), css_class="col-auto"),
            Div(Field("has_senior"), css_class="col-auto"),
            Div(Field("has_guest"), css_class="col-auto"),
            Div(Field("show_expired"), css_class="col-auto"),
            css_class="row mb-0",
        )

        # Date ranges until today
        today = date.today()
        time_ranges = [
            ("Last month", today - timedelta(days=30), today),
            ("Last year", today - timedelta(days=365), today),
            ("Last 2 years", today - timedelta(days=2 * 365), today),
        ]

        div_block_date_buttons = Div(
            Div(
                *[
                    HTML(
                        f'<button class="btn btn-outline-secondary" '
                        f"""hx-get={reverse("colleges:fellowships_monitor:_hx_search_form", kwargs={"filter_set":f"from_{from_date}_to_{to_date}"})} """
                        f'hx-target="#fellowships-monitor-search-form-container"'
                        f">{date_range_name}</button>"
                    )
                    for (date_range_name, from_date, to_date) in time_ranges
                ],
                css_class="d-grid gap-1 my-3",
            ),
            css_class="row mb-0",
        )

        self.helper.layout = Layout(
            Div(
                Div(
                    Div(
                        Div(Field("fellow"), css_class="col-12 mb-2"),
                        Div(div_block_fellow_types, css_class="col-12"),
                        Div(Field("college", size=8), css_class="col-12"),
                        css_class="row mb-0 d-flex flex-column justify-content-between h-100",
                    ),
                    css_class="col",
                ),
                Div(
                    Field("specialties", size=13),
                    css_class="col-12 col-sm-6 col-md-4 col-lg-5 col-xl-6",
                ),
                Div(
                    Div(
                        Div(div_block_dates, css_class="col-12"),
                        Div(div_block_date_buttons, css_class="col-12"),
                        Div(div_block_ordering, css_class="col-12"),
                        css_class="row mb-0 d-flex flex-column justify-content-between h-100",
                    ),
                    css_class="col-12 col-md",
                ),
                css_class="row mb-0",
            ),
        )

    def save_fields_to_session(self):
        # Save the form data to the session
        if self.session_key is not None:
            session = SessionStore(session_key=self.session_key)

            for field_key in self.cleaned_data:
                session_key = (
                    f"{self.form_id}_{field_key}"
                    if hasattr(self, "form_id")
                    else field_key
                )

                if field_value := self.cleaned_data.get(field_key):
                    if isinstance(field_value, date):
                        field_value = field_value.strftime("%Y-%m-%d")

                session[session_key] = field_value

            session.save()

    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
        # Apply the filter set to the form
        for key in self.fields:
            if key in filters:
                self.fields[key].initial = filters[key]
            elif none_on_empty:
                if isinstance(self.fields[key], forms.MultipleChoiceField):
                    self.fields[key].initial = []
                else:
                    self.fields[key].initial = None

    def search_results(self):
        self.save_fields_to_session()

        fellowships = Fellowship.objects.all().distinct()

        if fellow := self.cleaned_data.get("fellow"):
            fellowships = fellowships.filter(
                Q(contributor__profile__first_name__unaccent__icontains=fellow)
                | Q(contributor__profile__last_name__unaccent__icontains=fellow)
            )
        if college := self.cleaned_data.get("college"):
            fellowships = fellowships.filter(college__id__in=college)
        if specialties := self.cleaned_data.get("specialties"):
            fellowships = fellowships.filter(
                contributor__profile__specialties__in=specialties
            )

        date_from = self.cleaned_data.get("date_from")
        date_to = self.cleaned_data.get("date_to")

        def filter_submissions_in_pool(qs, prefix=""):
            """
            Filter a Submission queryset to only items in the pool between some dates.
            """
            if not date_from and not date_to:
                return qs
            else:
                date_filter = Q()

            # Should not have left the pool before the "from" start date or not left at all
            if date_from:
                date_filter = Q(**{prefix + "eic_first_assigned_date__isnull": True})
                date_filter |= Q(**{prefix + "eic_first_assigned_date__gte": date_from})

            # Should have been added to the pool before the "to" final date
            # Only dates in the past can change the query result
            if date_to and date_to < date.today():
                date_filter &= Q(**{prefix + "checks_cleared_date__lte": date_to})

            return qs.filter(date_filter)

        def count_q(qs, key="pk"):
            """Count the number of items in a queryset, or return 0 if the queryset is empty."""
            return Coalesce(
                Subquery(qs.values(key).annotate(count=Count(key)).values("count")),
                0,
            )

        fellowships = fellowships.annotate(
            nr_in_pool=count_q(
                filter_submissions_in_pool(
                    Submission.objects.filter(fellows__exact=OuterRef("id")),
                ),
                key="fellows",
            ),
            nr_appraised=count_q(
                filter_submissions_in_pool(
                    Qualification.objects.filter(fellow=OuterRef("id")),
                    prefix="submission__",
                ),
                key="fellow",
            ),
            nr_qualified_for=count_q(
                filter_submissions_in_pool(
                    Qualification.objects.filter(
                        fellow=OuterRef("id"),
                        expertise_level__in=[
                            Qualification.EXPERT,
                            Qualification.VERY_KNOWLEDGEABLE,
                            Qualification.KNOWLEDGEABLE,
                            Qualification.MARGINALLY_QUALIFIED,
                        ],
                    ),
                    prefix="submission__",
                ),
                key="fellow",
            ),
            nr_assignments_completed=count_q(
                filter_submissions_in_pool(
                    EditorialAssignment.objects.filter(
                        to=OuterRef("contributor"),
                        status=EditorialAssignment.STATUS_COMPLETED,
                    ),
                    prefix="submission__",
                ),
                key="to",
            ),
            nr_assignments_ongoing=count_q(
                filter_submissions_in_pool(
                    EditorialAssignment.objects.filter(
                        to=OuterRef("contributor"),
                        status=EditorialAssignment.STATUS_ACCEPTED,
                    ),
                    prefix="submission__",
                ),
                key="to",
            ),
            nr_recommendations_eligible=count_q(
                filter_submissions_in_pool(
                    EICRecommendation.objects.filter(
                        eligible_to_vote__exact=OuterRef("contributor")
                    ),
                    prefix="submission__",
                ),
                key="eligible_to_vote",
            ),
            nr_recommendations_voted_for=count_q(
                filter_submissions_in_pool(
                    EICRecommendation.objects.filter(
                        voted_for__exact=OuterRef("contributor"),
                    ),
                    prefix="submission__",
                ),
                key="voted_for",
            ),
            nr_recommendations_voted_against=count_q(
                filter_submissions_in_pool(
                    EICRecommendation.objects.filter(
                        voted_against__exact=OuterRef("contributor"),
                    ),
                    prefix="submission__",
                ),
                key="voted_against",
            ),
            nr_recommendations_voted_abstain=count_q(
                filter_submissions_in_pool(
                    EICRecommendation.objects.filter(
                        voted_abstain__exact=OuterRef("contributor"),
                    ),
                    prefix="submission__",
                ),
                key="voted_abstain",
            ),
        ).annotate(
            nr_recommendations_voted=F("nr_recommendations_voted_for")
            + F("nr_recommendations_voted_against")
            + F("nr_recommendations_voted_abstain")
        )

        if not self.cleaned_data.get("has_regular"):
            fellowships = fellowships.exclude(status=Fellowship.STATUS_REGULAR)
        if not self.cleaned_data.get("has_senior"):
            fellowships = fellowships.exclude(status=Fellowship.STATUS_SENIOR)
        if not self.cleaned_data.get("has_guest"):
            fellowships = fellowships.exclude(status=Fellowship.STATUS_GUEST)
        if not self.cleaned_data.get("show_expired"):
            fellowships = fellowships.exclude(until_date__lt=date.today())

        # Ordering of nominations
        # Only order if both fields are set
        if (orderby_value := self.cleaned_data.get("orderby")) and (
            ordering_value := self.cleaned_data.get("ordering")
        ):
            # Remove the + from the ordering value, causes a Django error
            ordering_value = ordering_value.replace("+", "")

            # Ordering string is built by the ordering (+/-), and the field name
            # from the orderby field split by "," and joined together
            fellowships = fellowships.order_by(
                *[
                    ordering_value + order_part
                    for order_part in orderby_value.split(",")
                ]
            )

        return fellowships