# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. c All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

##############################################################################
#  Module "Avis de poursuite d'étude"
#  conçu et développé par Cléo Baras (IUT de Grenoble)
##############################################################################

"""
Created on 17/01/2024

@author: barasc
"""
import pandas as pd

from app import ScoValueError
from app.models import FormSemestre, Identite, Formation
from app.pe import pe_comp, pe_affichage
from app.pe.rcss import pe_rcs
from app.scodoc import codes_cursus
from app.scodoc import sco_utils as scu
from app.comp.res_sem import load_formsemestre_results


class EtudiantsJuryPE:
    """Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""

    def __init__(self, annee_diplome: int):
        """
        Args:
            annee_diplome: L'année de diplomation
        """
        self.annee_diplome = annee_diplome
        """L'année du diplôme"""

        self.identites: dict[int:Identite] = {}  # ex. ETUDINFO_DICT
        """Les identités des étudiants traités pour le jury"""

        self.cursus: dict[int:dict] = {}
        """Les cursus (semestres suivis, abandons) des étudiants"""

        self.trajectoires: dict[int:dict] = {}
        """Les trajectoires (regroupement cohérents de semestres) suivis par les étudiants"""

        self.semXs: dict[int:dict] = {}
        """Les semXs (RCS de type Sx) suivis par chaque étudiant"""

        self.rcsemXs: dict[int:dict] = {}
        """Les RC de SemXs (RCS de type Sx, xA, xS) suivis par chaque étudiant"""

        self.etudiants_diplomes = {}
        """Les identités des étudiants à considérer au jury (ceux qui seront effectivement
        diplômés)"""

        self.diplomes_ids = {}
        """Les etudids des étudiants diplômés"""

        self.etudiants_ids = {}
        """Les etudids des étudiants dont il faut calculer les moyennes/classements
        (même si d'éventuels abandons).
        Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi
        d'autres ayant été réorientés ou ayant abandonnés)"""

        self.cosemestres: dict[int, FormSemestre] = None
        "Les cosemestres donnant lieu à même année de diplome"

        self.abandons = {}
        """Les étudiants qui ne seront pas diplômés à ce jury (redoublants/réorientés)"""
        self.abandons_ids = {}
        """Les etudids des étudiants redoublants/réorientés"""

    def find_etudiants(self):
        """Liste des étudiants à prendre en compte dans le jury PE, en les recherchant
        de manière automatique par rapport à leur année de diplomation ``annee_diplome``.

        Les données obtenues sont stockées dans les attributs de EtudiantsJuryPE.

        *Remarque* : ex: JuryPE.get_etudiants_in_jury()
        """
        cosemestres = pe_comp.get_cosemestres_diplomants(self.annee_diplome)
        self.cosemestres = cosemestres

        pe_affichage.pe_print(
            f"1) Recherche des cosemestres -> {len(cosemestres)} trouvés", info=True
        )

        pe_affichage.pe_print(
            "2) Liste des étudiants dans les différents cosemestres", info=True
        )
        etudiants_ids = get_etudiants_dans_semestres(cosemestres)
        pe_affichage.pe_print(
            f" => {len(etudiants_ids)} étudiants trouvés dans les cosemestres",
            info=True,
        )

        # Analyse des parcours étudiants pour déterminer leur année effective de diplome
        # avec prise en compte des redoublements, des abandons, ....
        pe_affichage.pe_print(
            "3) Analyse des parcours individuels des étudiants", info=True
        )

        # Ajoute une liste d'étudiants
        self.add_etudiants(etudiants_ids)

        # Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
        self.etudiants_diplomes = self.get_etudiants_diplomes()
        self.diplomes_ids = set(self.etudiants_diplomes.keys())
        self.etudiants_ids = set(self.identites.keys())

        # Les abandons (pour debug)
        self.abandons = self.get_etudiants_redoublants_ou_reorientes()
        # Les identités des étudiants ayant redoublés ou ayant abandonnés

        self.abandons_ids = set(self.abandons)
        # Les identifiants des étudiants ayant redoublés ou ayant abandonnés

        # Synthèse
        pe_affichage.pe_print(f"4) Bilan", info=True)
        pe_affichage.pe_print(
            f"--> {len(self.etudiants_diplomes)} étudiants à diplômer en {self.annee_diplome}",
            info=True,
        )
        nbre_abandons = len(self.etudiants_ids) - len(self.etudiants_diplomes)
        assert nbre_abandons == len(self.abandons_ids)

        pe_affichage.pe_print(
            f"--> {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)"
        )

    def add_etudiants(self, etudiants_ids):
        """Ajoute une liste d'étudiants aux données du jury"""
        nbre_etudiants_ajoutes = 0
        for etudid in etudiants_ids:
            if etudid not in self.identites:
                nbre_etudiants_ajoutes += 1

                # L'identité de l'étudiant
                self.identites[etudid] = Identite.get_etud(etudid)

                # Analyse son cursus
                self.analyse_etat_etudiant(etudid, self.cosemestres)

                # Analyse son parcours pour atteindre chaque semestre de la formation
                self.structure_cursus_etudiant(etudid)
        self.etudiants_ids = set(self.identites.keys())
        return nbre_etudiants_ajoutes

    def get_etudiants_diplomes(self) -> dict[int, Identite]:
        """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
        qui vont être à traiter au jury PE pour
        l'année de diplômation donnée et n'ayant ni été réorienté, ni abandonné.


        Returns:
            Un dictionnaire `{etudid: Identite(etudid)}`
        """
        etudids = [
            etudid
            for etudid, cursus_etud in self.cursus.items()
            if cursus_etud["diplome"] == self.annee_diplome
            and cursus_etud["abandon"] is False
        ]
        etudiants = {etudid: self.identites[etudid] for etudid in etudids}
        return etudiants

    def get_etudiants_redoublants_ou_reorientes(self) -> dict[int, Identite]:
        """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
        dont les notes seront prises en compte (pour les classements) mais qui n'apparaitront
        pas dans le jury car diplômé une autre année (redoublants) ou réorienté ou démissionnaire.

        Returns:
            Un dictionnaire `{etudid: Identite(etudid)}`
        """
        etudids = [
            etudid
            for etudid, cursus_etud in self.cursus.items()
            if cursus_etud["diplome"] != self.annee_diplome
            or cursus_etud["abandon"] is True
        ]
        etudiants = {etudid: self.identites[etudid] for etudid in etudids}
        return etudiants

    def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]):
        """Analyse le cursus d'un étudiant pouvant être :

        * l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré)
        * un étudiant qui ne sera pas considéré dans le jury mais qui a participé dans sa scolarité
          à un (ou plusieurs) semestres communs aux étudiants du jury (et impactera les classements)

        L'analyse consiste :

        * à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
          avec son nom, prénom, etc...
        * à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme)
          ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré
          en abandon si connaissant son dernier semestre (par ex. un S3) il n'est pas systématiquement
          inscrit à l'un des S4, S5 ou S6 existants dans les cosemestres.


        Args:
            etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
            cosemestres: Dictionnaire {fid: Formsemestre(fid)} donnant accès aux cosemestres
                         de même année de diplomation
        """
        identite = Identite.get_etud(etudid)

        # Le cursus global de l'étudiant (restreint aux semestres APC)
        formsemestres = identite.get_formsemestres()

        semestres_etudiant = {
            formsemestre.formsemestre_id: formsemestre
            for formsemestre in formsemestres
            if formsemestre.formation.is_apc()
        }

        # Le parcours final
        parcour = formsemestres[0].etuds_inscriptions[etudid].parcour
        if parcour:
            libelle = parcour.libelle
        else:
            libelle = None

        self.cursus[etudid] = {
            "etudid": etudid,  # les infos sur l'étudiant
            "etat_civil": identite.etat_civil,  # Ajout à la table jury
            "nom": identite.nom,
            "entree": formsemestres[-1].date_debut.year,  # La date d'entrée à l'IUT
            "parcours": libelle,  # Le parcours final
            "diplome": get_annee_diplome(
                identite
            ),  # Le date prévisionnelle de son diplôme
            "formsemestres": semestres_etudiant,  # les semestres de l'étudiant
            "nb_semestres": len(
                semestres_etudiant
            ),  # le nombre de semestres de l'étudiant
            "abandon": False,  # va être traité en dessous
        }

        # Si l'étudiant est succeptible d'être diplomé
        if self.cursus[etudid]["diplome"] == self.annee_diplome:
            # Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
            dernier_semes_etudiant = formsemestres[0]
            res = load_formsemestre_results(dernier_semes_etudiant)
            etud_etat = res.get_etud_etat(etudid)
            if etud_etat == scu.DEMISSION:
                self.cursus[etudid]["abandon"] = True
            else:
                # Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ?
                self.cursus[etudid]["abandon"] = arret_de_formation(
                    identite, cosemestres
                )

        # Initialise ses trajectoires/SemX/RCSemX
        self.trajectoires[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}
        self.semXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_SEMESTRES}
        self.rcsemXs[etudid] = {aggregat: None for aggregat in pe_rcs.TOUS_LES_RCS}

    def structure_cursus_etudiant(self, etudid: int):
        """Structure les informations sur les semestres suivis par un
        étudiant, pour identifier les semestres qui seront pris en compte lors de ses calculs
        de moyennes PE.

        Cette structuration s'appuie sur les numéros de semestre: pour chaque Si, stocke :
        le dernier semestre (en date) de numéro i qu'il a suivi (1 ou 0 si pas encore suivi).
        Ce semestre influera les interclassements par semestre dans la promo.
        """
        semestres_significatifs = get_semestres_significatifs(
            self.cursus[etudid]["formsemestres"], self.annee_diplome
        )

        # Tri des semestres par numéro de semestre
        for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
            # les semestres de n°i de l'étudiant:
            semestres_i = {
                fid: sem_sig
                for fid, sem_sig in semestres_significatifs.items()
                if sem_sig.semestre_id == i
            }
            self.cursus[etudid][f"S{i}"] = semestres_i

    def get_formsemestres_finals_des_rcs(self, nom_rcs: str) -> dict[int, FormSemestre]:
        """Pour un nom de RCS donné, ensemble des formsemestres finals possibles
        pour les RCS. Par ex. un RCS '3S' incluant S1+S2+S3 a pour semestre final un S3.
        Les formsemestres finals obtenus traduisent :

        * les différents parcours des étudiants liés par exemple au choix de modalité
        (par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
        formsemestre_id du S3 FI et du S3 UFA.
        * les éventuelles situations de redoublement (par ex pour 1 étudiant ayant
        redoublé sa 2ème année :
          S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en
          renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)

        Args:
            nom_rcs: Le nom du RCS (parmi Sx, xA, xS)

        Returns:
            Un dictionnaire ``{fid: FormSemestre(fid)}``
        """
        formsemestres_terminaux = {}
        for trajectoire_aggr in self.cursus.values():
            trajectoire = trajectoire_aggr[nom_rcs]
            if trajectoire:
                # Le semestre terminal de l'étudiant de l'aggrégat
                fid = trajectoire.formsemestre_final.formsemestre_id
                formsemestres_terminaux[fid] = trajectoire.formsemestre_final
        return formsemestres_terminaux

    def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
        """Partant d'un ensemble d'étudiants,
        nombre de semestres (étapes) maximum suivis par les étudiants du jury.

        Args:
            etudids: Liste d'étudid d'étudiants
        """
        nbres_semestres = []
        for etudid in etudids:
            nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
        if not nbres_semestres:
            return 0
        return max(nbres_semestres)

    def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
        """Synthétise toutes les données administratives d'un groupe
        d'étudiants fournis par les etudid dans un dataFrame

        Args:
            etudids: La liste des étudiants à prendre en compte
        """

        etudids = list(etudids)

        # Récupération des données des étudiants
        administratif = {}
        nbre_semestres_max = self.nbre_etapes_max_diplomes(etudids)

        for etudid in etudids:
            etudiant = self.identites[etudid]
            cursus = self.cursus[etudid]
            formsemestres = cursus["formsemestres"]
            parcours = cursus["parcours"]
            if not parcours:
                parcours = ""
            if cursus["diplome"]:
                diplome = cursus["diplome"]
            else:
                diplome = "indéterminé"

            administratif[etudid] = {
                "etudid": etudiant.id,
                "INE": etudiant.code_ine or "",
                "NIP": etudiant.code_nip or "",
                "Nom": etudiant.nom,
                "Prenom": etudiant.prenom,
                "Civilite": etudiant.civilite_str,
                "Age": pe_comp.calcul_age(etudiant.date_naissance),
                "Parcours": parcours,
                "Date entree": cursus["entree"],
                "Date diplome": diplome,
                "Nb semestres": len(formsemestres),
            }

            # Ajout des noms de semestres parcourus
            etapes = etapes_du_cursus(formsemestres, nbre_semestres_max)
            administratif[etudid] |= etapes

        # Construction du dataframe
        df = pd.DataFrame.from_dict(administratif, orient="index")

        # Tri par nom/prénom
        df.sort_values(by=["Nom", "Prenom"], inplace=True)
        return df


def get_semestres_significatifs(formsemestres, annee_diplome):
    """Partant d'un ensemble de semestre, renvoie les semestres qui amèneraient les étudiants
    à être diplômé à l'année visée, y compris s'ils n'avaient pas redoublé et seraient donc
    diplômé plus tard.

    De fait, supprime les semestres qui conduisent à une diplomation postérieure
    à celle visée.

    Args:
        formsemestres: une liste de formsemestres
        annee_diplome: l'année du diplôme visée

    Returns:
        Un dictionnaire ``{fid: FormSemestre(fid)}`` dans lequel les semestres
        amènent à une diplômation antérieur à celle de la diplômation visée par le jury
    """
    # semestres_etudiant = self.cursus[etudid]["formsemestres"]
    semestres_significatifs = {}
    for fid in formsemestres:
        semestre = formsemestres[fid]
        if pe_comp.get_annee_diplome_semestre(semestre) <= annee_diplome:
            semestres_significatifs[fid] = semestre
    return semestres_significatifs


def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
    """Ensemble d'identifiants des étudiants (identifiés via leur ``etudid``)
    inscrits à l'un des semestres de la liste de ``semestres``.

    Args:
        semestres: Un dictionnaire ``{fid: Formsemestre(fid)}`` donnant un
                   ensemble d'identifiant de semestres

    Returns:
        Un ensemble d``etudid``
    """

    etudiants_ids = set()
    for sem in semestres.values():  # pour chacun des semestres de la liste
        etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}

        pe_affichage.pe_print(f"  --> {sem} : {len(etudiants_du_sem)} etudiants")
        etudiants_ids = (
            etudiants_ids | etudiants_du_sem
        )  # incluant la suppression des doublons

    return etudiants_ids


def get_annee_diplome(etud: Identite) -> int | None:
    """L'année de diplôme prévue d'un étudiant en fonction de ses semestres
    d'inscription (pour un BUT).

    Args:
        identite: L'identité d'un étudiant

    Returns:
        L'année prévue de sa diplômation, ou None si aucun semestre
    """
    formsemestres_apc = get_semestres_apc(etud)

    if formsemestres_apc:
        dates_possibles_diplome = []
        # Années de diplômation prédites en fonction des semestres
        # (d'une formation APC) d'un étudiant
        for sem_base in formsemestres_apc:
            annee = pe_comp.get_annee_diplome_semestre(sem_base)
            if annee:
                dates_possibles_diplome.append(annee)
        if dates_possibles_diplome:
            return max(dates_possibles_diplome)

    return None


def get_semestres_apc(identite: Identite) -> list:
    """Liste des semestres d'un étudiant qui correspondent à une formation APC.

    Args:
        identite: L'identité d'un étudiant

    Returns:
        Liste de ``FormSemestre`` correspondant à une formation APC
    """
    semestres = identite.get_formsemestres()
    semestres_apc = []
    for sem in semestres:
        if sem.formation.is_apc():
            semestres_apc.append(sem)
    return semestres_apc


def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> bool:
    """Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir :

    * d'une réorientation à l'initiative du jury de semestre ou d'une démission
    (on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
    des résultats du jury renseigné dans la BDD, mais pas nécessaire ici)

    * d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour
    autant avoir été indiqué NAR ou DEM).

    Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
    dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
    connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres
    de rang/semestre_id supérieur (et donc de dates) au dernier semestre dans lequel il a été inscrit.

    Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
    l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
    l'année visée. S'il n'est que dans un S4, il a sans doute arrêté. A moins qu'il ne soit
    parti à l'étranger et là, pas de notes.
    TODO:: Cas de l'étranger, à coder/tester

    **Attention** : Cela suppose que toutes les instances d'un semestre donné
    (par ex: toutes les instances de S6 accueillant un étudiant soient créées ; sinon les
    étudiants non inscrits dans un S6 seront considérés comme ayant abandonnés)
    TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre

    Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et
    regarde s'il n'existe pas parmi les semestres existants dans Scodoc un semestre :
    * dont les dates sont postérieures (en terme de date de début)
    * de n° au moins égal à celui de son dernier semestre valide (S5 -> S5 ou S5 -> S6)
    dans lequel il aurait pu s'inscrire mais ne l'a pas fait.

    Args:
        etud: L'identité d'un étudiant
        cosemestres: Les semestres donnant lieu à diplômation (sans redoublement) en date du jury

    Returns:
        Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ?

    TODO:: A reprendre pour le cas des étudiants à l'étranger
    """
    # Les semestres APC de l'étudiant
    semestres = get_semestres_apc(etud)
    semestres_apc = {sem.semestre_id: sem for sem in semestres}
    if not semestres_apc:
        return True

    # Le dernier semestre de l'étudiant
    dernier_formsemestre = semestres[0]
    rang_dernier_semestre = dernier_formsemestre.semestre_id

    # Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
    # sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
    cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres)

    cosemestres_superieurs = {}
    for rang in cosemestres_tries_par_rang:
        if rang > rang_dernier_semestre:
            cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]

    # Si pas d'autres cosemestres postérieurs
    if not cosemestres_superieurs:
        return False

    # Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ?
    etat_inscriptions = {rang: False for rang in cosemestres_superieurs}
    for rang in etat_inscriptions:
        for sem in cosemestres_superieurs[rang]:
            etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
            if etud.etudid in etudiants_du_sem:
                etat_inscriptions[rang] = True

    # Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres
    rangs = sorted(etat_inscriptions.keys())
    if list(rangs) != list(range(min(rangs), max(rangs) + 1)):
        difference = set(range(min(rangs), max(rangs) + 1)) - set(rangs)
        affichage = ",".join([f"S{val}" for val in difference])
        raise ScoValueError(
            f"Il manque le(s) semestre(s) {affichage} au cursus de {etud.etat_civil} ({etud.etudid})."
        )

    # Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire
    est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs)
    if est_demissionnaire:
        non_inscrit_a = [
            rang for rang in etat_inscriptions if not etat_inscriptions[rang]
        ]
        affichage = ", ".join([f"S{val}" for val in non_inscrit_a])
        pe_affichage.pe_print(
            f"--> ⛔  {etud.etat_civil} ({etud.etudid}), non inscrit dans {affichage} amenant à diplômation"
        )
    else:
        pe_affichage.pe_print(f"--> ✅  {etud.etat_civil} ({etud.etudid})")

    return est_demissionnaire


def etapes_du_cursus(
    semestres: dict[int, FormSemestre], nbre_etapes_max: int
) -> list[str]:
    """Partant d'un dictionnaire de semestres (qui retrace
    la scolarité d'un étudiant), liste les noms des
    semestres (en version abbrégée)
    qu'un étudiant a suivi au cours de sa scolarité à l'IUT.
    Les noms des semestres sont renvoyés dans un dictionnaire
    ``{"etape i": nom_semestre_a_etape_i}``
    avec i variant jusqu'à nbre_semestres_max. (S'il n'y a pas de semestre à l'étape i,
    le nom affiché est vide.

    La fonction suppose la liste des semestres triées par ordre
    décroissant de date.

    Args:
        semestres: une liste de ``FormSemestre``
        nbre_etapes_max: le nombre d'étapes max prise en compte

    Returns:
        Une liste de nom de semestre (dans le même ordre que les ``semestres``)

    See also:
         app.pe.pe_affichage.nom_semestre_etape
    """
    assert len(semestres) <= nbre_etapes_max

    noms = [nom_semestre_etape(sem, avec_fid=False) for (fid, sem) in semestres.items()]
    noms = noms[::-1]  # trie par ordre croissant

    dico = {f"Etape {i+1}": "" for i in range(nbre_etapes_max)}
    for i, nom in enumerate(noms):  # Charge les noms de semestres
        dico[f"Etape {i+1}"] = nom
    return dico


def nom_semestre_etape(semestre: FormSemestre, avec_fid=False) -> str:
    """Nom d'un semestre à afficher dans le descriptif des étapes de la scolarité
    d'un étudiant.

    Par ex: Pour un S2, affiche ``"Semestre 2 FI S014-2015 (129)"`` avec :

    * 2 le numéro du semestre,
    * FI la modalité,
    * 2014-2015 les dates

    Args:
        semestre: Un ``FormSemestre``
        avec_fid: Ajoute le n° du semestre à la description

    Returns:
        La chaine de caractères décrivant succintement le semestre
    """
    formation: Formation = semestre.formation
    parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)

    description = [
        parcours.SESSION_NAME.capitalize(),
        str(semestre.semestre_id),
        semestre.modalite,  # eg FI ou FC
        f"{semestre.date_debut.year}-{semestre.date_fin.year}",
    ]
    if avec_fid:
        description.append(f"(#{semestre.formsemestre_id})")

    return " ".join(description)