# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import Optional, List

from django.db.models import QuerySet
from django.forms import Form

from ...models import WEIClub, WEIRegistration, Bus, WEIMembership


class WEISurveyInformation:
    """
    Abstract data of the survey.
    """
    valid = False
    selected_bus_pk = None
    selected_bus_name = None

    def __init__(self, registration):
        self.__dict__.update(registration.information)

    def get_selected_bus(self) -> Optional[Bus]:
        """
        If the algorithm ran, return the prefered bus according to the survey.
        In the other case, return None.
        """
        return Bus.objects.get(pk=self.selected_bus_pk) if self.valid else None

    def save(self, registration) -> None:
        """
        Store the data of the survey into the database, with the information of the registration.
        """
        registration.information = self.__dict__


class WEIBusInformation:
    """
    Abstract data of the bus.
    """

    def __init__(self, bus: Bus):
        self.__dict__.update(bus.information)
        self.bus = bus
        self.save()

    def save(self):
        d = self.__dict__.copy()
        d.pop("bus")
        self.bus.information = d
        self.bus.save()

    def free_seats(self, surveys: List["WEISurvey"] = None, quotas=None):
        if not quotas:
            size = self.bus.size
            already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
            quotas = {self.bus: size - already_occupied}

        quota = quotas[self.bus]
        valid_surveys = sum(1 for survey in surveys if survey.information.valid
                            and survey.information.get_selected_bus() == self.bus) if surveys else 0
        return quota - valid_surveys

    def has_free_seats(self, surveys=None, quotas=None):
        return self.free_seats(surveys, quotas) > 0


class WEISurveyAlgorithm:
    """
    Abstract algorithm that attributes a bus to each new member.
    """

    @classmethod
    def get_survey_class(cls):
        """
        The class of the survey associated with this algorithm.
        """
        raise NotImplementedError

    @classmethod
    def get_bus_information_class(cls):
        """
        The class of the information associated to a bus extending WEIBusInformation.
        Default: WEIBusInformation (contains nothing)
        """
        return WEIBusInformation

    @classmethod
    def get_registrations(cls) -> QuerySet:
        """
        Queryset of all first year registrations
        """
        if not hasattr(cls, '_registrations'):
            cls._registrations = WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(),
                                                                first_year=True).all()

        return cls._registrations

    @classmethod
    def get_buses(cls) -> QuerySet:
        """
        Queryset of all buses of the associated wei.
        """
        if not hasattr(cls, '_buses'):
            cls._buses = Bus.objects.filter(wei__year=cls.get_survey_class().get_year(), size__gt=0).all()
        return cls._buses

    @classmethod
    def get_bus_information(cls, bus):
        """
        Return the WEIBusInformation object containing the data stored in a given bus.
        """
        return cls.get_bus_information_class()(bus)

    def run_algorithm(self) -> None:
        """
        Once this method implemented, run the algorithm that attributes a bus to each first year member.
        This method can be run in command line through ``python manage.py wei_algorithm``
        See ``wei.management.commmands.wei_algorithm``
        This method must call Survey.select_bus for each survey.
        """
        raise NotImplementedError

    @classmethod
    def get_bus_information_form(cls):
        """
        The class of the form to update the bus information.
        """
        raise NotImplementedError


class WEISurvey:
    """
    Survey associated to a first year WEI registration.
    The data is stored into WEISurveyInformation, this class acts as a manager.
    This is an abstract class: this has to be extended each year to implement custom methods.
    """

    def __init__(self, registration: WEIRegistration):
        self.registration = registration
        self.information = self.get_survey_information_class()(registration)

    @classmethod
    def get_year(cls) -> int:
        """
        Get year of the wei concerned by the type of the survey.
        """
        raise NotImplementedError

    @classmethod
    def get_wei(cls) -> WEIClub:
        """
        The WEI associated to this kind of survey.
        """
        if not hasattr(cls, '_wei'):
            cls._wei = WEIClub.objects.get(year=cls.get_year())

        return cls._wei

    @classmethod
    def get_survey_information_class(cls):
        """
        The class of the data (extending WEISurveyInformation).
        """
        raise NotImplementedError

    def get_form_class(self) -> Form:
        """
        The form class of the survey.
        This is proper to the status of the survey: the form class can evolve according to the progress of the survey.
        """
        raise NotImplementedError

    def update_form(self, form) -> None:
        """
        Once the form is instanciated, the information can be updated with the information of the registration
        and the information of the survey.
        This method is called once the form is created.
        """
        pass

    def form_valid(self, form) -> None:
        """
        Called when the information of the form are validated.
        This method should update the information of the survey.
        """
        raise NotImplementedError

    def is_complete(self) -> bool:
        """
        Return True if the survey is complete.
        If the survey is complete, then the button "Next" will display some text for the end of the survey.
        If not, the survey is reloaded and continues.
        """
        raise NotImplementedError

    def save(self) -> None:
        """
        Store the information of the survey into the database.
        """
        self.information.save(self.registration)
        # The information is forced-saved.
        # We don't want that anyone can update manually the information, so since most users don't have the
        # right to save the information of a registration, we force save.
        self.registration._force_save = True
        self.registration.save()

    @classmethod
    def get_algorithm_class(cls):
        """
        Algorithm class associated to the survey.
        The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member.
        The association is not permanent: that's only a suggestion.
        """
        raise NotImplementedError

    def select_bus(self, bus) -> None:
        """
        Set the suggestion into the data of the membership.
        :param bus: The bus suggested.
        """
        self.information.selected_bus_pk = bus.pk
        self.information.selected_bus_name = bus.name
        self.information.valid = True

    def free(self) -> None:
        """
        Unselect the select bus.
        """
        self.information.selected_bus_pk = None
        self.information.selected_bus_name = None
        self.information.valid = False

    @classmethod
    def clear_cache(cls):
        """
        Clear stored information.
        """
        if hasattr(cls, '_wei'):
            del cls._wei
        if hasattr(cls.get_algorithm_class(), '_registrations'):
            del cls.get_algorithm_class()._registrations
        if hasattr(cls.get_algorithm_class(), '_buses'):
            del cls.get_algorithm_class()._buses
