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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  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 Fri Sep  9 09:15:05 2016

@author: barasc
"""

# ----------------------------------------------------------
# Ensemble des fonctions et des classes
# permettant les calculs preliminaires (hors affichage)
# a l'edition d'un jury de poursuites d'etudes
# ----------------------------------------------------------

import io
import os
import time
from zipfile import ZipFile

import numpy as np
import pandas as pd
import jinja2

from app.pe.rcss import pe_rcs
from app.pe.moys import pe_sxtag

import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant
from app.pe.moys import (
    pe_tabletags,
    pe_ressemtag,
    pe_sxtag,
    pe_rcstag,
    pe_interclasstag,
    pe_moytag,
)
import app.pe.pe_rcss_jury as pe_rcss_jury
from app.scodoc.sco_utils import *


class JuryPE(object):
    """
    Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
    d'une année de diplôme. De ce semestre est déduit :
    1. l'année d'obtention du DUT,
    2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.

    Les options sont :
    ``
    options = {
        "cohorte_restreinte": False,
        "moyennes_tags": True,
        "moyennes_ue_res_sae": True,
        "moyennes_ues_rcues": True,
        "min_max_moy": False,
        "synthese_individuelle_etud": False,
    }

    Args:
        diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
    """

    def __init__(
        self,
        diplome: int,
        formsemestre_id_base,
        options={
            "moyennes_tags": True,
            "moyennes_ue_res_sae": True,
            "moyennes_ues_rcues": True,
            "min_max_moy": False,
            "publipostage": False,
        },
    ):
        pe_affichage.pe_start_log()
        self.diplome = diplome
        "L'année du diplome"

        self.formsemestre_id_base = formsemestre_id_base
        """L'identifiant du formsemestre ayant servi à lancer le jury"""

        self.nom_export_zip = f"Jury_PE_{self.diplome}"
        "Nom du zip où ranger les fichiers générés"

        # Les options
        self.options = options
        """Options de configuration (cf. pe_sem_recap)"""

        pe_affichage.pe_print(
            f"Données de poursuite d'étude générées le {time.strftime('%d/%m/%Y à %H:%M')}\n",
            info=True,
        )

        pe_affichage.pe_print("Options", info=True)
        for cle, val in self.options.items():
            pe_affichage.pe_print(f" > {cle} -> {val}", info=True)

        # Chargement des étudiants à prendre en compte dans le jury
        pe_affichage.pe_print(
            f"""***********************************************************"""
        )
        pe_affichage.pe_print(
            f"""*** Recherche des étudiants diplômés 🎓 en {self.diplome}""", info=True
        )
        pe_affichage.pe_print(
            f"""***********************************************************"""
        )

        # Les infos sur les étudiants
        self.etudiants = pe_etudiant.EtudiantsJuryPE(self.diplome)
        """Les informations sur les étudiants du jury PE"""
        self.etudiants.find_etudiants()
        self.diplomes_ids = self.etudiants.diplomes_ids

        self.rcss_jury = pe_rcss_jury.RCSsJuryPE(self.diplome, self.etudiants)
        """Les informations sur les regroupements de semestres"""

        self.zipdata = io.BytesIO()
        with ZipFile(self.zipdata, "w") as zipfile:
            if not self.diplomes_ids:
                pe_affichage.pe_print("*** Aucun étudiant diplômé", info=True)
            else:
                try:
                    self._gen_xls_diplomes(zipfile)
                    self._gen_xls_ressembuttags(zipfile)
                    self._gen_trajectoires()
                    self._gen_semXs()
                    self._gen_xls_sxtags(zipfile)
                    self._gen_rcsemxs()
                    self._gen_xls_rcstags(zipfile)
                    self._gen_xls_interclasstags(zipfile)
                    self._gen_xls_synthese_jury_par_tag(zipfile)
                    self._gen_html_synthese_par_etudiant(zipfile)
                except Exception as e:
                    if pe_affichage.PE_DEBUG == True:
                        raise e
                    else:
                        pe_affichage.pe_print(str(e))
                    # raise e
            # et le log
            self._add_log_to_zip(zipfile)

        # Fin !!!! Tada :)

    def _gen_xls_diplomes(self, zipfile: ZipFile):
        "Intègre le bilan des semestres taggués au zip"
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            if self.diplomes_ids:
                onglet = "diplômés"
                df_diplome = self.etudiants.df_administratif(self.diplomes_ids)
                df_diplome.to_excel(writer, onglet, index=True, header=True)
            if self.etudiants.abandons_ids:
                onglet = "redoublants-réorientés"
                df_abandon = self.etudiants.df_administratif(
                    self.etudiants.abandons_ids
                )
                df_abandon.to_excel(writer, onglet, index=True, header=True)
        output.seek(0)

        self.add_file_to_zip(
            zipfile,
            f"etudiants_{self.diplome}.xlsx",
            output.read(),
            path="details",
        )

    def _gen_xls_ressembuttags(self, zipfile: ZipFile):
        """Calcule les moyennes par tag des résultats des Semestres BUT"""
        pe_affichage.pe_print(
            f"""*************************************************************************"""
        )
        pe_affichage.pe_print(
            f"""*** Génère les ResSemBUTTag (ResSemestreBUT taggués)""", info=True
        )
        pe_affichage.pe_print(
            f"""*************************************************************************"""
        )

        # Tous les formsestres des étudiants
        formsemestres = get_formsemestres_etudiants(self.etudiants)
        pe_affichage.pe_print(
            f"1) Génère les {len(formsemestres)} ResSemBUTTag", info=True
        )

        self.ressembuttags = {}
        for frmsem_id, formsemestre in formsemestres.items():
            # Crée le semestre_tag et exécute les calculs de moyennes
            ressembuttag = pe_ressemtag.ResSemBUTTag(formsemestre, options=self.options)
            self.ressembuttags[frmsem_id] = ressembuttag
            # Ajoute les étudiants découverts dans les ressembuttags aux données des étudiants
            # nbre_etudiants_ajoutes = self.etudiants.add_etudiants(
            #    ressembuttag.etudids_sorted
            # )
            # if nbre_etudiants_ajoutes:
            #    pe_affichage.pe_print(
            #        f"--> Ajout de {nbre_etudiants_ajoutes} étudiants aux données du jury"
            #    )

        # Intègre le bilan des semestres taggués au zip final
        pe_affichage.pe_print(f"2) Bilan", info=True)
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            onglets = []
            for res_sem_tag in self.ressembuttags.values():
                if res_sem_tag.is_significatif():
                    onglet = res_sem_tag.get_repr(verbose=True)
                    onglet = onglet.replace("Semestre ", "S")
                    onglets += ["📊" + onglet]
                    df = res_sem_tag.to_df(options=self.options)
                    # Conversion colonnes en multiindex
                    df = convert_colonnes_to_multiindex(df)
                    # écriture dans l'onglet
                    df.to_excel(writer, onglet, index=True, header=True)
            pe_affichage.pe_print(
                f"--> Export excel de {', '.join(onglets)}", info=True
            )
        output.seek(0)

        self.add_file_to_zip(
            zipfile,
            f"ResSemBUTTags_{self.diplome}.xlsx",
            output.read(),
            path="details",
        )

    def _gen_trajectoires(self):
        """Génère l'ensemble des trajectoires (RCS), qui traduisent les différents
        chemins au sein des (form)semestres pour atteindre la cible d'un
        RCS (par ex: 'S2' ou '3S').
        """
        pe_affichage.pe_print(
            "***************************************************************************"
        )
        pe_affichage.pe_print(
            "*** Génère les trajectoires (≠tes combinaisons de semestres) des étudiants",
            info=True,
        )
        pe_affichage.pe_print(
            "***************************************************************************"
        )

        self.rcss_jury.cree_trajectoires()
        pe_affichage.aff_trajectoires_suivies_par_etudiants(self.etudiants)

    def _gen_semXs(self):
        """Génère les SemXs (trajectoires/combinaisons de semestre de même rang x)
        qui traduisent les différents chemins des étudiants pour valider un semestre Sx.
        """
        pe_affichage.pe_print(
            "***************************************************************************"
        )
        pe_affichage.pe_print(
            "*** Génère les SemXs (RCS de même Sx donnant lieu à validation du semestre)",
            info=True,
        )
        pe_affichage.pe_print(
            "***************************************************************************"
        )

        # Génère les regroupements de semestres de type Sx

        self.rcss_jury.cree_semxs()
        pe_affichage.aff_semXs_suivis_par_etudiants(self.etudiants)

    def _gen_xls_sxtags(self, zipfile: ZipFile):
        """Génère les semestres taggués en s'appuyant sur les RCF de type Sx (pour
        identifier les redoublements impactant les semestres taggués).
        """
        # Génère les moyennes des RCS de type Sx
        pe_affichage.pe_print(
            "***************************************************************************"
        )
        pe_affichage.pe_print(
            "*** Calcule les moyennes des SxTag (moyennes d'un RCS de type Sx)",
            info=True,
        )
        pe_affichage.pe_print(
            "***************************************************************************"
        )

        # Les SxTag (moyenne de Sx par UE)
        pe_affichage.pe_print("1) Calcul des moyennes", info=True)
        self.sxtags = {}
        for rcf_id, rcf in self.rcss_jury.semXs.items():
            # SxTag traduisant le RCF
            sxtag_id = rcf_id
            self.sxtags[sxtag_id] = pe_sxtag.SxTag(sxtag_id, rcf, self.ressembuttags)

        # Intègre le bilan des semestres taggués au zip final
        pe_affichage.pe_print("2) Bilan", info=True)
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            onglets = []
            for sxtag in self.sxtags.values():
                if sxtag.is_significatif():
                    onglet = sxtag.get_repr(verbose=False)
                    onglets += ["📊" + onglet]
                    df = sxtag.to_df(options=self.options)
                    # Conversion colonnes en multiindex
                    df = convert_colonnes_to_multiindex(df)

                    # écriture dans l'onglet
                    df.to_excel(writer, onglet, index=True, header=True)
            pe_affichage.pe_print(
                f"--> Export excel de {', '.join(onglets)}", info=True
            )

        output.seek(0)
        if onglets:
            self.add_file_to_zip(
                zipfile,
                f"semestres_taggues_{self.diplome}.xlsx",
                output.read(),
                path="details",
            )

    def _gen_rcsemxs(self):
        """Génère les regroupements cohérents de RCFs qu'ont suivi chaque étudiant"""

        pe_affichage.pe_print(
            """******************************************************************************"""
        )
        pe_affichage.pe_print(
            """*** Génère les RCSemX (regroupements cohérents de données extraites des SemX)\n"""
            """*** amenant du S1 à un semestre final""",
            info=True,
        )
        pe_affichage.pe_print(
            """******************************************************************************"""
        )
        self.rcss_jury.cree_rcsemxs(options=self.options)
        if "moyennes_ues_rcues" in self.options and self.options["moyennes_ues_rcues"]:
            pe_affichage.aff_rcsemxs_suivis_par_etudiants(self.etudiants)

    def _gen_xls_rcstags(self, zipfile: ZipFile):
        """Génère les RCS taggués traduisant les moyennes (orientées compétences)
        de regroupements de semestre de type Sx, xA ou xS.

        Stocke le résultat dans self.rccs_tag, un dictionnaire de
        la forme ``{nom_aggregat: {fid_terminal: RCSTag(fid_terminal)} }``

        Pour rappel : Chaque RCS est identifié par un nom d'aggrégat et par un formsemestre terminal.

        Par exemple :

        * combinaisons '3S' : S1+S2+S3 en prenant en compte tous les S3 qu'ont fréquenté les
          étudiants du jury PE. Ces S3 marquent les formsemestre terminal de chaque combinaison.

        Args:
            etudiants: Les données des étudiants
            semestres_tag: Les semestres tag (pour lesquels des moyennes par tag ont été calculés)
        """

        # Génère les moyennes des RCS de type Sx
        pe_affichage.pe_print(
            """****************************************************"""
        )
        pe_affichage.pe_print(
            """*** Génère les moyennes associées aux RCSemX""", info=True
        )
        pe_affichage.pe_print(
            """****************************************************"""
        )

        pe_affichage.pe_print("1) Calcul des moyennes des RCSTag", info=True)
        if not self.rcss_jury.rcsemxs:
            if (
                "moyennes_ues_rcues" in self.options
                and not self.options["moyennes_ues_rcues"]
            ):
                pe_affichage.pe_print("  -> Pas de RCSemX à calculer (cf. options)")
            else:
                pe_affichage.pe_print(
                    "  -> Pas de RCSemX à calculer (alors qu'aucune option ne les limite) => problème"
                )
            self.rcsstags = {}
            return

        # Calcul des RCSTags sur la base des RCSemX
        self.rcsstags = {}
        for rcs_id, rcsemx in self.rcss_jury.rcsemxs.items():
            self.rcsstags[rcs_id] = pe_rcstag.RCSemXTag(
                rcsemx, self.sxtags, self.rcss_jury.semXs_suivis
            )

        # Intègre le bilan des trajectoires tagguées au zip final
        pe_affichage.pe_print("2) Bilan", info=True)
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            onglets = []
            for rcs_tag in self.rcsstags.values():
                if rcs_tag.is_significatif():
                    onglet = rcs_tag.get_repr(verbose=False)
                    onglets += ["📊" + onglet]

                    df = rcs_tag.to_df(options=self.options)
                    # Conversion colonnes en multiindex
                    df = convert_colonnes_to_multiindex(df)
                    onglets += ["📊" + onglet]
                    # écriture dans l'onglet
                    df.to_excel(writer, onglet, index=True, header=True)
            pe_affichage.pe_print(
                f"--> Export excel de {', '.join(onglets)}", info=True
            )
        output.seek(0)

        if onglets:
            self.add_file_to_zip(
                zipfile,
                f"RCRCFs_{self.diplome}.xlsx",
                output.read(),
                path="details",
            )

    def _gen_xls_interclasstags(self, zipfile: ZipFile):
        """Génère les interclassements sur la promo de diplômés
        par (nom d') aggrégat
        en distinguant les interclassements par accronymes d'UEs (sur les SxTag)
        et ceux par compétences (sur les RCSTag).
        """
        pe_affichage.pe_print(
            """******************************************************************"""
        )
        pe_affichage.pe_print(
            """*** Génère les interclassements sur chaque type de RCS/agrégat"""
        )
        pe_affichage.pe_print(
            """******************************************************************"""
        )

        if (
            "moyennes_ues_rcues" not in self.options
            or self.options["moyennes_ues_rcues"]
        ):
            self.interclasstags = {
                pe_moytag.CODE_MOY_UE: {},
                pe_moytag.CODE_MOY_COMPETENCES: {},
            }
        else:
            self.interclasstags = {pe_moytag.CODE_MOY_UE: {}}

        etudiants_diplomes = self.etudiants.etudiants_diplomes

        # Les interclassements par UE (toujours présents par défaut)
        for Sx in pe_rcs.TOUS_LES_SEMESTRES:
            interclass = pe_interclasstag.InterClassTag(
                Sx,
                pe_moytag.CODE_MOY_UE,
                etudiants_diplomes,
                self.rcss_jury.semXs,
                self.sxtags,
                self.rcss_jury.semXs_suivis,
            )
            self.interclasstags[pe_moytag.CODE_MOY_UE][Sx] = interclass

        # Les interclassements par compétences
        if (
            "moyennes_ues_rcues" not in self.options
            or self.options["moyennes_ues_rcues"]
        ):
            for nom_rcs in pe_rcs.TOUS_LES_RCS:
                interclass = pe_interclasstag.InterClassTag(
                    nom_rcs,
                    pe_moytag.CODE_MOY_COMPETENCES,
                    etudiants_diplomes,
                    self.rcss_jury.rcsemxs,
                    self.rcsstags,
                    self.rcss_jury.rcsemxs_suivis,
                )
                self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][
                    nom_rcs
                ] = interclass

        # Intègre le bilan des aggrégats (interclassé par promo) au zip final
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            onglets = []
            for (
                type_interclass
            ) in self.interclasstags:  # Pour les types d'interclassements prévus
                interclasstag = self.interclasstags[type_interclass]
                for nom_rcs, interclass in interclasstag.items():
                    if interclass.is_significatif():
                        onglet = interclass.get_repr()
                        onglets += ["📊" + onglet]
                        df = interclass.to_df(cohorte="Promo", options=self.options)
                        # Conversion colonnes en multiindex
                        df = convert_colonnes_to_multiindex(df)
                        onglets += [onglet]
                        # écriture dans l'onglet
                        df.to_excel(writer, onglet, index=True, header=True)
            pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}", info=True)

        output.seek(0)

        if onglets:
            self.add_file_to_zip(
                zipfile,
                f"InterClassTags_{self.diplome}.xlsx",
                output.read(),
                path="details",
            )

    def _gen_xls_synthese_jury_par_tag(self, zipfile: ZipFile):
        """Synthèse des éléments du jury PE tag par tag"""
        pe_affichage.pe_print(
            "**************************************************************************************"
        )
        pe_affichage.pe_print(
            "*** Synthèse finale des moyennes par tag et par type de moyennes (UEs ou Compétences)",
            info=True,
        )
        pe_affichage.pe_print(
            "**************************************************************************************"
        )

        self.synthese = {}
        pe_affichage.pe_print("  -> Synthèse des données administratives", info=True)
        self.synthese["administratif"] = self.etudiants.df_administratif(
            self.diplomes_ids
        )

        tags = self._do_tags_list(self.interclasstags)
        for tag in tags:
            for type_moy in self.interclasstags:
                self.synthese[(tag, type_moy)] = self.df_tag_type(tag, type_moy)

        # Export des données => mode 1 seule feuille -> supprimé
        pe_affichage.pe_print("*** Export du jury de synthese par tags", info=True)
        output = io.BytesIO()
        with pd.ExcelWriter(  # pylint: disable=abstract-class-instantiated
            output, engine="openpyxl"
        ) as writer:
            onglets = []
            for onglet, df in self.synthese.items():
                # Conversion colonnes en multiindex
                df_final = df.copy()
                if (
                    "publipostage" not in self.options
                    or not self.options["publipostage"]
                ):
                    df_final = convert_colonnes_to_multiindex(df_final)
                # Nom de l'onglet
                if isinstance(onglet, tuple):
                    (repr, type_moy) = onglet
                    nom_onglet = onglet[0][: 31 - 7]
                    if type_moy == pe_moytag.CODE_MOY_COMPETENCES:
                        nom_onglet = nom_onglet + " (Comp)"
                    else:
                        nom_onglet = nom_onglet + " (UEs)"
                else:
                    nom_onglet = onglet
                onglets += [nom_onglet]
                # écriture dans l'onglet:
                df_final = df_final.replace("nan", "")
                df_final.to_excel(writer, nom_onglet, index=True, header=True)
            pe_affichage.pe_print(f"=> Export excel de {', '.join(onglets)}", info=True)
        output.seek(0)

        if onglets:
            self.add_file_to_zip(
                zipfile, f"synthese_jury_{self.diplome}.xlsx", output.read()
            )

    def _gen_html_synthese_par_etudiant(self, zipfile: ZipFile):
        """Synthèse des éléments du jury PE, étudiant par étudiant"""
        # Synthèse des éléments du jury PE
        pe_affichage.pe_print("**************************************************")
        pe_affichage.pe_print("*** Synthèse finale étudiant par étudiant", info=True)
        pe_affichage.pe_print("**************************************************")

        if (
            "moyennes_ues_rcues" not in self.options
            or self.options["moyennes_ues_rcues"]
        ):
            etudids = list(self.diplomes_ids)
            for etudid in etudids:
                nom, prenom, html = self.synthetise_jury_etudiant(etudid)
                self.add_file_to_zip(
                    zipfile, f"{nom}_{prenom}.html", html, path="etudiants"
                )
        else:
            pe_affichage.pe_print(" > Pas de synthèse étudiant/étudiant possible/prévu")

    def _add_log_to_zip(self, zipfile):
        """Add a text file with the log messages"""
        log_data = pe_affichage.pe_get_log()
        self.add_file_to_zip(zipfile, "pe_log.txt", log_data)

    def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
        """Add a file to given zip
        All files under NOM_EXPORT_ZIP/
        path may specify a subdirectory

        Args:
             zipfile: ZipFile
             filename: Le nom du fichier à intégrer au zip
             data: Les données du fichier
             path: Un dossier dans l'arborescence du zip
        """
        path_in_zip = os.path.join(path, filename)  # self.nom_export_zip,
        zipfile.writestr(path_in_zip, data)

    def get_zipped_data(self) -> io.BytesIO | None:
        """returns file-like data with a zip of all generated (CSV) files.
        Warning: reset stream to the begining.
        """
        self.zipdata.seek(0)
        return self.zipdata

    def _do_tags_list(self, interclassements: dict[str, dict]):
        """La liste des tags extraites des interclassements"""
        tags = []
        # Pour chaque type d'interclassements
        for type in interclassements:
            interclassement = interclassements[type]
            for aggregat in interclassement:
                interclass = interclassement[aggregat]
                if interclass.tags_sorted:
                    tags.extend(interclass.tags_sorted)
            tags = sorted(set(tags))
            return tags

    # **************************************************************************************************************** #
    # Méthodes pour la synthèse du juryPE
    # *****************************************************************************************************************

    def df_tag_type(self, tag, type_moy):
        """Génère le DataFrame synthétisant les moyennes/classements (groupe +
        interclassement promo) pour tous les aggrégats prévus, en fonction
        du type (UEs ou Compétences) de données souhaitées,
        tels que fourni dans l'excel final.

        Si type=UEs => tous les sxtag du tag
        Si type=Compétences => tous les rcstag du tag

        Args:
            tag: Un des tags (a minima `but`)
            type_moy: Un type de moyenne

        Returns:
        """

        # Les données des étudiants
        etuds = [etud for etudid, etud in self.etudiants.etudiants_diplomes.items()]
        df = pe_tabletags.df_administratif(etuds, aggregat="Administratif", cohorte="")

        if type_moy == pe_moytag.CODE_MOY_UE:
            aggregats = pe_rcs.TOUS_LES_SEMESTRES
        else:
            aggregats = pe_rcs.TOUS_LES_RCS

        aff_aggregat = []
        for aggregat in aggregats:
            # Descr de l'aggrégat
            descr = pe_rcs.TYPES_RCS[aggregat]["descr"]

            # L'interclassement associé
            interclass = self.interclasstags[type_moy][aggregat]

            if interclass.is_significatif():
                # Le dataframe du classement sur le groupe
                df_groupe = interclass.compute_df_synthese_moyennes_tag(
                    tag, aggregat=aggregat, type_colonnes=False, options=self.options
                )
                if not df_groupe.empty:
                    aff_aggregat += [aggregat]
                    df = df.join(df_groupe)

                # Le dataframe du classement sur la promo
                df_promo = interclass.to_df(
                    administratif=False,
                    aggregat=aggregat,
                    tags_cibles=[tag],
                    cohorte="Promo",
                    options=self.options,
                )

                if not df_promo.empty:
                    aff_aggregat += [aggregat]
                    df = df.join(df_promo)

        if aff_aggregat:
            pe_affichage.pe_print(
                f"  -> Synthèse de 👜{tag} par {type_moy} avec {', '.join(aff_aggregat)}"
            )
        else:
            pe_affichage.pe_print(f"  -> Synthèse du tag {tag} par {type_moy} : <vide>")

        return df
        # Fin de l'aggrégat

    def synthetise_jury_etudiant(self, etudid) -> (str, str, str):
        """Synthétise les résultats d'un étudiant dans un
        fichier html à son nom en s'appuyant sur la synthese final

        Returns:
             Un tuple nom, prenom, html
        """
        etudiant = self.etudiants.identites[etudid]
        nom = etudiant.nom
        prenom = etudiant.prenom  # initial du prénom
        parcours = self.etudiants.cursus[etudid]["parcours"]
        if not parcours:
            parcours = "<parcours indéterminé>"

        # Accès au template
        environnement = jinja2.Environment(
            loader=jinja2.FileSystemLoader("app/templates/")
        )
        template = environnement.get_template("pe/pe_view_resultats_etudiant.j2")

        # Colonnes des tableaux htmls => competences
        competences = []
        for aggregat in pe_rcs.TOUS_LES_RCS:
            # L'interclassement associé
            interclass = self.interclasstags[pe_moytag.CODE_MOY_COMPETENCES][aggregat]
            competences.extend(interclass.champs_sorted)
        competences = sorted(set(competences))
        colonnes_html = competences

        tags = self._do_tags_list(self.interclasstags)

        # Les données par UE
        moyennes = {}
        for tag in tags:
            moyennes[tag] = {}
            # Les données de synthèse
            df = self.synthese[(tag, pe_moytag.CODE_MOY_COMPETENCES)]
            for aggregat in pe_rcs.TOUS_LES_RCS:
                # moyennes[tag][aggregat] = {}
                descr = pe_rcs.get_descr_rcs(aggregat)

                moy = {}
                est_significatif = False
                for comp in competences + ["Général"]:
                    moy[comp] = {
                        "note": "",
                        "rang_groupe": "",
                        "rang_promo": "",
                    }
                    colonne = pe_moytag.get_colonne_df(
                        aggregat, tag, comp, "Groupe", "note"
                    )
                    if colonne in df.columns:
                        valeur = df.loc[etudid, colonne]
                        if not np.isnan(valeur):
                            moy[comp]["note"] = round(valeur, 2)
                            est_significatif = True
                    # else:
                    #    print(f"{colonne} manquante")
                    colonne = pe_moytag.get_colonne_df(
                        aggregat, tag, comp, "Groupe", "rang"
                    )
                    if colonne in df.columns:
                        valeur = df.loc[etudid, colonne]
                        if valeur and str(valeur) != "nan":
                            moy[comp]["rang_groupe"] = valeur
                    colonne = pe_moytag.get_colonne_df(
                        aggregat, tag, comp, "Promo", "rang"
                    )
                    if colonne in df.columns:
                        valeur = df.loc[etudid, colonne]
                        if valeur and str(valeur) != "nan":
                            moy[comp]["rang_promo"] = valeur

                if est_significatif:
                    moyennes[tag][descr] = moy

        html = template.render(
            nom=nom,
            prenom=prenom,
            parcours=parcours,
            colonnes_html=colonnes_html,
            tags=tags,
            moyennes=moyennes,
        )

        return (nom, prenom, html)


def get_formsemestres_etudiants(etudiants: pe_etudiant.EtudiantsJuryPE) -> dict:
    """Ayant connaissance des étudiants dont il faut calculer les moyennes pour
    le jury PE (attribut `self.etudiant_ids) et de leurs trajectoires (semestres
    parcourus), renvoie un dictionnaire ``{fid: FormSemestre(fid)}``
    contenant l'ensemble des formsemestres de leurs cursus, dont il faudra calculer
    la moyenne.

    Args:
        etudiants: Les étudiants du jury PE

    Returns:
        Un dictionnaire de la forme `{fid: FormSemestre(fid)}`
    """
    semestres = {}
    for etudid in etudiants.etudiants_ids:
        for cle in etudiants.cursus[etudid]:
            if cle.startswith("S"):
                semestres = semestres | etudiants.cursus[etudid][cle]
    return semestres


def convert_colonnes_to_multiindex(df):
    """Convertit les colonnes d'un df pour obtenir des colonnes
    multiindex"""
    df_final = df.copy()
    colonnes = list(df.columns)
    colonnes = [tuple(col.split("|")) for col in colonnes]
    # modifie le nom du semestre par sa descr
    colonnes_verbose = []

    for col in colonnes:
        if col[0] in pe_rcs.TYPES_RCS:
            descr = pe_rcs.get_descr_rcs(col[0])
            col_verbose = [descr] + list(col[1:])
            col_verbose = tuple(col_verbose)
        else:
            col_verbose = col
        colonnes_verbose.append(col_verbose)

    df_final.columns = pd.MultiIndex.from_tuples(colonnes_verbose)
    return df_final