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

from functools import lru_cache

from django import forms
from django.utils.safestring import mark_safe
from django.db import transaction
from django.db.models import Q

from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership


buses_descr = [
    [
        "Magi[Kar]p 🐙🎼đŸŽČ", "#ef5568", 1,
        """Vous l'aurez compris au nom du bus, l'ambiance est aux jeux et Ă  la culture geek ! Ici, vous trouverez une ambiance
        calme avec une bonne dose d'autodérision et de second degré. Que vous ayez besoin de beaucoup dormir pour tenir la soirée
        du lendemain, ou que vous souhaitiez faire nuit blanche pour jouer toute la nuit, vous pouvez nous rejoindre. Votre voix
        n'y survivra peut-ĂȘtre pas Ă  force de chanter. PS : les meilleurs cocktails du WEI sont chez nous, Ă  dĂ©guster, pas Ă 
        siphonner !""",
    ],
    [
        "Va[car]me đŸŽ·đŸŽđŸ”Š", "#fd7a28", 3,
        """Ici, c'est le bus du bruit. Si vous voulez réveiller les autres bus en musique, apprendre de merveilleuses
        mélodies au kazoo tout le week-end, ou simplement profiter d'une bonne ambiance musicale, le BDA et la
        F[ENS]foire sont là pour vous. Vous pourrez également goûter au célÚbre cocktail de la fanfare, concocté
        pour l'occasion par les tout nouveaux "meilleurs artisans v*********** de France" ! Alors que vous soyez artiste
        dans l'Ăąme ou que vous souhaitiez juste faire le plus grand Vacarme, rejoignez-nous !""",
    ],
    [
        "[Kar]aĂŻbes đŸïžđŸŽâ€â˜ ïžđŸ„„", "#a5cfdd", 3,
        """Ahoy, explorateurs du WEI ! Le bus Karaibes t’invite Ă  une traversĂ©e sous les tropiques, oĂč l’ambiance est
        toujours au beau fixe ! ☀đŸč Ici, c’est soleil, rhum, et bonne humeur assurĂ©e : une atmosphĂšre de vacances oĂč
        l’on se laisse porter par la chaleur humaine et la fĂȘte. Que tu sois un pirate en quĂȘte de sensations fortes ou
        un amateur de chill avec un cocktail à la main, tu seras à ta place dans notre bus. Les soirées seront marquées
        par des rythmes tropicaux qui te feront vibrer jusqu’à l’aube. PrĂȘt Ă  embarquer pour une aventure inoubliable
        avec les meilleurs matelots du WEI ? On t’attend sur le pont du Karaibes pour lever l’ancre ensemble !""",
    ],
    [
        "[Kar]di [Bus] đŸŽ™ïžđŸ’…", "#e46398", 2.5,
        """Bienvenue Ă  bord du Kardi Bus, la seul, l’unique, l’inimitable pĂ©pite de ce weekend d’intĂ©gration ! InspirĂ© par les
        icĂŽnes suprĂȘmes de la pop culture telles les Bratz, les Winx et autres Mean Girls, notre bus est un sanctuaire de style,
        d’audace et de pur plaisir. A nos cotĂ©s attends toi Ă  siroter tes meilleurs Cosmo, sex on the Beach et autres cocktails
        de maxi pĂ©tasse tout en papotant entre copains copines ! Si tu rejoins le Kardi Bus, tu entres dans un monde oĂč tu
        pourras te dĂ©hancher sur du BeyoncĂ©, Britney, Aya et autres reines de la pop ! À trĂšs vite, les futures stars du Kardi
        Bus !""",
    ],
    [
        "Sparta[bus] đŸș🐒🏉", "#ebdac2", 5,
        """Dans notre bus, on vous donne un avant goût des plus grandes assos de l'ENS : les Kyottes et l'Aspique (clubs de rugby
        féminin et masculin, mais pas que). Bien entendu, qui dit rugby dit les copaings, le pastaga et la Pena Bayona, mais vous
        verrez par vous mĂȘme qu'on est ouvert⋅e Ă  toutes propositions quand il s'agit de faire la fĂȘte. Pour les casse-cous comme
        pour les plus calmes, vous trouverez au bus Aspique-Kyottes les 2A+ qui vous feront kiffer votre WEI.""",
    ],
    [
        "Zanzo[Bus] đŸ€ŻđŸšžđŸ’", "#FFFF", 3,
        """Dans un entre-trois bien senti entre zinzinerie, enfance et vieillerie, le Zanzo[BUS] est un concentrĂ© de fun mĂȘlĂ© Ă 
        de la dinguerie à gogo. N'hésitez plus et rejoignez-nous pour un WEI toujours plus déjanté !""",
    ],
    [
        "Bran[Kar] đŸčđŸ„ł", "#6da1ac", 4,
        """Si vous ne connaissez pas le Bran[Kar], c’est comme une grande famille qui fait un apĂ©ro, qui se bourre un peu la
        gueule en discutant des heures autour d’une table remplie de bouffe et de super bons cocktails (la plupart des
        barmen/barwomen du bus sont les barmans de Shakens), sauf qu’on est un bus du Wei (vous comprendrez bien le nom de notre
        bus en voyant l’état de certain·e·s). Il nous arrive de faire quelques conneries, mais surtout de jouer au BiĂšre-pong en
        musique !""",
    ],
    [
        "Techno [kar]ade đŸ”ŠđŸš©", "#8065a3", 3,
        """Avis Ă  tous·tes les gauchos, amoureux·ses de la fĂȘte et des manifs : le Techno [kar]ade vous ouvre grand ses bras pour
        finir en beautĂ© votre premiĂšre intĂ©. PrĂ©parez-vous Ă  vous abreuver de cocktails (savamment Ă©laborĂ©s) Ă  la vibration d’un
        systĂšme son fabriquĂ© pour l’occasion. Des sets technos Ă  « Mon pĂšre Ă©tait tellement de gauche » en passant par « Female
        Body », le car accueillant les meilleures DJs du plateau saura animer le trajet aussi bien que les soirées. Si alcool et
        musique seront au rendez-vous, les maßtres mots sont sécurité et inclusivité. Qui que vous soyez et quelle que soit votre
        maniùre de vous amuser, notre objectif est que vous vous sentiez à l’aise pour rencontrer au mieux les 1A, les 2A et les
        (nombreux⋅ses) 3A+ qui auront rĂ©pondu Ă  l’appel. Bref, rejoignez-nous, on est super cools :)"""
    ],
    [
        "[Bus]ka-P đŸ„‡đŸ»đŸŽ€", "#7c4768", 4.5,
        """Booska-p, c’est le « site N°1 du Rap français ». Le [Bus]ka-p ? Le bus N°1 sur l’ambiance au WEI. Les nuits vont ĂȘtre
        courtes, les cocktails vont couler à flots : tout sera réuni pour vivre un week-end dont tu te souviendras toute ta vie.
        Au programme pas un seul temps mort et un maximum de rencontres pour bien commencer ta premiĂšre annĂ©e Ă  l’ENS. Et bien
        entendu, le tout accompagnĂ© des meilleurs sons, de Jul Ă  Aya, en passant par ABBA et Sexion d’Assaut. Bref, si tu veux
        vivre un WEI d’anthologie et faire la fĂȘte, de jour comme de nuit, nous t’accueillons avec plaisir !""",
    ],
]


def print_bus(i):
    return f"""<h1 style="color:{buses_descr[i][1]};-webkit-text-stroke: 2px black;font-size: 50px;">{buses_descr[i][0]}</h1><br>
    <b>AlcoolomĂštre : {buses_descr[i][2]} / 5 đŸ»</b><br><br>{buses_descr[i][3]}<br>"""


def print_all_buses():
    liste = [print_bus(i) for i in range(len(buses_descr))]
    return "<br><br><br><br>".join(liste)


def get_number_comment(i):
    if i == 1:
        return "MĂȘme pas en rĂȘve"
    elif i == 2:
        return "Pas envie"
    elif i == 3:
        return "Mouais..."
    elif i == 4:
        return "Pourquoi pas !"
    elif i == 5:
        return "Ce bus ou rien !!!"
    else:
        return ""


WORDS = {
    "recap":
        [
            """<b>ChĂšr⋅e 1A, te voilĂ  arrivĂ©â‹…e au moment fatidique du choix de ton bus !<br><br><br>
            Ton bus est constitué des gens avec qui tu passeras la majorité de ton temps : que ce soit le voyage d'aller et de
            retour et les différentes activité qu'ils pourront te proposer tout au long du WEI donc choisis le bien !
            <br><br>Tu trouveras ci-dessous la liste de tous les bus ainsi qu'une description détaillée de ces derniers.
            Prends ton temps pour Ă©tudier chacun d'eux et quand tu te sens prĂȘt⋅e, appuie sur le bouton « J'ai pris connaissance
            des bus » pour continuer
            <br>(pas besoin d'apprendre par cƓur chaque bus, la description de chaque bus te sera rappeler avant de lui attribuer
            une note !)</b><br><br><br>""" + print_all_buses(),
            {
                "1": "J'ai pris connaissance des diffĂ©rents bus et me sent fin prĂȘt Ă  choisir celui qui me convient le mieux !",
            }
        ]
}

WORDS.update({
    f"bus{id}": [print_bus(id), {i: f"{get_number_comment(i)}   ({i}/5)" for i in range(1, 5 + 1)}] for id in range(len(buses_descr))
})


class WEISurveyForm2024(forms.Form):
    """
    Survey form for the year 2024.
    Members score the different buses, from which we calculate the best associated bus.
    """
    def set_registration(self, registration):
        """
        Filter the bus selector with the buses of the current WEI.
        """
        information = WEISurveyInformation2024(registration)

        question = information.questions[information.step]
        self.fields[question] = forms.ChoiceField(
            label=mark_safe(WORDS[question][0]),
            widget=forms.RadioSelect(),
        )
        answers = [(answer, WORDS[question][1][answer]) for answer in WORDS[question][1]]
        self.fields[question].choices = answers


class WEIBusInformation2024(WEIBusInformation):
    """
    For each question, the bus has ordered answers
    """
    scores: dict

    def __init__(self, bus):
        self.scores = {}
        for question in WORDS:
            self.scores[question] = []
        super().__init__(bus)


class WEISurveyInformation2024(WEISurveyInformation):
    """
    We store the id of the selected bus. We store only the name, but is not used in the selection:
    that's only for humans that try to read data.
    """

    step = 0
    questions = list(WORDS.keys())

    def __init__(self, registration):
        for question in WORDS:
            setattr(self, str(question), None)
        super().__init__(registration)


class WEISurvey2024(WEISurvey):
    """
    Survey for the year 2024.
    """

    @classmethod
    def get_year(cls):
        return 2024

    @classmethod
    def get_survey_information_class(cls):
        return WEISurveyInformation2024

    def get_form_class(self):
        return WEISurveyForm2024

    def update_form(self, form):
        """
        Filter the bus selector with the buses of the WEI.
        """
        form.set_registration(self.registration)

    @transaction.atomic
    def form_valid(self, form):
        self.information.step += 1
        for question in WORDS:
            if question in form.cleaned_data:
                answer = form.cleaned_data[question]
                setattr(self.information, question, answer)
        self.save()

    @classmethod
    def get_algorithm_class(cls):
        return WEISurveyAlgorithm2024

    def is_complete(self) -> bool:
        """
        The survey is complete once the bus is chosen.
        """
        for question in WORDS:
            if not getattr(self.information, question):
                return False
        return True

    @lru_cache()
    def score(self, bus):
        if not self.is_complete():
            raise ValueError("Survey is not ended, can't calculate score")

        bus_info = self.get_algorithm_class().get_bus_information(bus)
        # Score is the given score by the bus subtracted to the mid-score of the buses.
        s = 0
        for question in WORDS:
            s += bus_info.scores[question][str(getattr(self.information, question))]
        return s

    @lru_cache()
    def scores_per_bus(self):
        return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}

    @lru_cache()
    def ordered_buses(self):
        values = list(self.scores_per_bus().items())
        values.sort(key=lambda item: -item[1])
        return values

    @classmethod
    def clear_cache(cls):
        return super().clear_cache()


class WEISurveyAlgorithm2024(WEISurveyAlgorithm):
    """
    The algorithm class for the year 2024.
    We use Gale-Shapley algorithm to attribute 1y students into buses.
    """

    @classmethod
    def get_survey_class(cls):
        return WEISurvey2024

    @classmethod
    def get_bus_information_class(cls):
        return WEIBusInformation2024

    def run_algorithm(self, display_tqdm=False):
        """
        Gale-Shapley algorithm implementation.
        We modify it to allow buses to have multiple "weddings".
        """
        surveys = list(self.get_survey_class()(r) for r in self.get_registrations())  # All surveys
        surveys = [s for s in surveys if s.is_complete()]  # Don't consider invalid surveys
        # Don't manage hardcoded people
        # surveys = [s for s in surveys if s.bus_id != None]
        # surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]

        # surveys = [s for s in surveys if s.registration.user_id in free_users]

        # hardcoded_first_year_mb = WEIMembership.objects.filter(bus != None,registration__first_year=True)
        # hardcoded_first_year = hardcoded_first_year_mb.values_list('user__id', 'bus__id')

        hardcoded_first_year_mb = WEIMembership.objects.filter(registration__first_year=True)
        hardcoded_first_year = {mb.user.id if mb.bus else None: mb.bus.id if mb.bus else None for mb in hardcoded_first_year_mb}

        # Reset previous algorithm run
        for survey in surveys:
            survey.free()
            if survey.registration.user_id in hardcoded_first_year.keys():
                survey.select_bus(hardcoded_first_year[survey.registration.user_id])
            survey.save()

        non_men = [s for s in surveys if s.registration.gender != 'male']
        men = [s for s in surveys if s.registration.gender == 'male']

        quotas = {}
        registrations = self.get_registrations()
        non_men_total = registrations.filter(~Q(gender='male')).count()
        for bus in self.get_buses():
            free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
            free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
            quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)

        tqdm_obj = None
        if display_tqdm:
            from tqdm import tqdm
            tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")

        # Repartition for non men people first
        self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)

        quotas = {}
        for bus in self.get_buses():
            free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
            free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
            quotas[bus] = free_seats

        if display_tqdm:
            tqdm_obj.close()

            from tqdm import tqdm
            tqdm_obj = tqdm(total=len(men), desc="Hommes")

        self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)

        if display_tqdm:
            tqdm_obj.close()

        # Clear cache information after running algorithm
        WEISurvey2024.clear_cache()

    def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
        free_surveys = surveys.copy()  # Remaining surveys
        while free_surveys:  # Some students are not affected
            survey = free_surveys[0]
            buses = survey.ordered_buses()  # Preferences of the student
            for bus, current_score in buses:
                if self.get_bus_information(bus).has_free_seats(surveys, quotas):
                    # Selected bus has free places. Put student in the bus
                    survey.select_bus(bus)
                    survey.save()
                    free_surveys.remove(survey)
                    break
                else:
                    # Current bus has not enough places. Remove the least preferred student from the bus if existing
                    least_preferred_survey = None
                    least_score = -1
                    # Find the least student in the bus that has a lower score than the current student
                    for survey2 in surveys:
                        if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
                            continue
                        score2 = survey2.score(bus)
                        if current_score <= score2:  # Ignore better students
                            continue
                        if least_preferred_survey is None or score2 < least_score:
                            least_preferred_survey = survey2
                            least_score = score2

                    if least_preferred_survey is not None:
                        # Remove the least student from the bus and put the current student in.
                        # If it does not exist, choose the next bus.
                        least_preferred_survey.free()
                        least_preferred_survey.save()
                        free_surveys.append(least_preferred_survey)
                        survey.select_bus(bus)
                        survey.save()
                        free_surveys.remove(survey)
                        break
            else:
                raise ValueError(f"User {survey.registration.user} has no free seat")

            if tqdm_obj is not None:
                tqdm_obj.n = len(surveys) - len(free_surveys)
                tqdm_obj.refresh()