# -*- pole: 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 Generfal 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
"""
import pandas as pd

from app import ScoValueError
from app import comp
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, UniteEns
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_etudiant as pe_etudiant
from app.pe.moys import pe_tabletags, pe_moytag
from app.scodoc import sco_tag_module
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.sco_utils import *


class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag):
    """
    Un ResSemBUTTag représente les résultats des étudiants à un semestre, en donnant
    accès aux moyennes par tag.
    Il s'appuie principalement sur un ResultatsSemestreBUT.
    """

    def __init__(
        self,
        formsemestre: FormSemestre,
        options={"moyennes_tags": True, "moyennes_ue_res_sae": False},
    ):
        """
        Args:
            formsemestre: le ``FormSemestre`` sur lequel il se base
            options: Un dictionnaire d'options
        """
        ResultatsSemestreBUT.__init__(self, formsemestre)
        pe_tabletags.TableTag.__init__(self)

        # Le nom du res_semestre taggué
        self.nom = self.get_repr(verbose=True)

        # Les étudiants (etuds, états civils & etudis) ajouté
        self.add_etuds(self.etuds)
        self.etudids_sorted = sorted(self.etudids)
        """Les etudids des étudiants du ResultatsSemestreBUT triés"""

        pe_affichage.pe_print(
            f"*** ResSemBUTTag du {self.nom} => {len(self.etudids_sorted)} étudiants"
        )

        # Les UEs (et les dispenses d'UE)
        self.ues_standards: list[UniteEns] = [
            ue for ue in self.ues if ue.type == sco_codes.UE_STANDARD
        ]
        """Liste des UEs standards du ResultatsSemestreBUT"""

        # Les parcours des étudiants à ce semestre
        self.parcours = []
        """Parcours auxquels sont inscrits les étudiants"""
        for etudid in self.etudids_sorted:
            parcour = self.formsemestre.etuds_inscriptions[etudid].parcour
            if parcour:
                self.parcours += [parcour.libelle]
            else:
                self.parcours += [None]

        # Les UEs en fonction des parcours
        self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
        """Inscription des étudiants aux UEs des parcours"""

        # Les acronymes des UEs
        self.ues_to_acronymes = {ue.id: ue.acronyme for ue in self.ues_standards}
        self.acronymes_sorted = sorted(self.ues_to_acronymes.values())
        """Les acronymes de UE triés par ordre alphabétique"""

        # Les compétences associées aux UEs (définies par les acronymes)
        self.acronymes_ues_to_competences = {}
        """Association acronyme d'UEs -> compétence"""
        for ue in self.ues_standards:
            assert ue.niveau_competence, ScoValueError(
                "Des UEs ne sont pas rattachées à des compétences"
            )
            nom = ue.niveau_competence.competence.titre
            self.acronymes_ues_to_competences[ue.acronyme] = nom
        self.competences_sorted = sorted(
            list(set(self.acronymes_ues_to_competences.values()))
        )
        """Compétences triées par nom"""
        aff = pe_affichage.repr_asso_ue_comp(self.acronymes_ues_to_competences)
        pe_affichage.pe_print(f"--> UEs/Compétences : {aff}")

        # Les tags personnalisés et auto:
        if "moyennes_tags" in options:
            tags_dict = self._get_tags_dict(avec_moyennes_tags=options["moyennes_tags"])
        else:
            tags_dict = self._get_tags_dict()

        pe_affichage.pe_print(
            f"""--> {pe_affichage.aff_tags_par_categories(tags_dict)}"""
        )
        self._check_tags(tags_dict)

        # Les coefficients pour le calcul de la moyenne générale, donnés par
        # acronymes d'UE
        self.matrice_coeffs_moy_gen = self._get_matrice_coeffs(
            self.ues_inscr_parcours_df, self.ues_standards
        )
        """DataFrame indiquant les coeffs des UEs par ordre alphabétique d'acronyme"""
        profils_aff = pe_affichage.repr_profil_coeffs(self.matrice_coeffs_moy_gen)
        pe_affichage.pe_print(
            f"--> Moyenne générale calculée avec pour coeffs d'UEs : {profils_aff}"
        )

        # Les capitalisations (mask etuids x acronyme_ue valant True si capitalisée, False sinon)
        self.capitalisations = self._get_capitalisations(self.ues_standards)
        """DataFrame indiquant les UEs capitalisables d'un étudiant (etudids x )"""

        # Calcul des moyennes & les classements de chaque étudiant à chaque tag
        self.moyennes_tags = {}
        """Moyennes  par tags (personnalisés ou 'but')"""
        for tag in tags_dict["personnalises"]:
            # pe_affichage.pe_print(f"  -> Traitement du tag {tag}")
            info_tag = tags_dict["personnalises"][tag]
            # Les moyennes générales par UEs
            moy_ues_tag = self.compute_moy_ues_tag(
                self.ues_inscr_parcours_df, info_tag=info_tag, pole=None
            )
            # Mémorise les moyennes
            self.moyennes_tags[tag] = pe_moytag.MoyennesTag(
                tag,
                pe_moytag.CODE_MOY_UE,
                moy_ues_tag,
                self.matrice_coeffs_moy_gen,
            )

        # Ajoute les moyennes par UEs + la moyenne générale (but)
        moy_gen = self.compute_moy_gen()
        self.moyennes_tags["but"] = pe_moytag.MoyennesTag(
            "but",
            pe_moytag.CODE_MOY_UE,
            moy_gen,
            self.matrice_coeffs_moy_gen,
        )

        # Ajoute la moyenne générale par ressources
        if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
            moy_res_gen = self.compute_moy_ues_tag(
                self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.RESSOURCE
            )
            self.moyennes_tags["ressources"] = pe_moytag.MoyennesTag(
                "ressources",
                pe_moytag.CODE_MOY_UE,
                moy_res_gen,
                self.matrice_coeffs_moy_gen,
            )

        # Ajoute la moyenne générale par saes
        if "moyennes_ue_res_sae" in options and options["moyennes_ue_res_sae"]:
            moy_saes_gen = self.compute_moy_ues_tag(
                self.ues_inscr_parcours_df, info_tag=None, pole=ModuleType.SAE
            )
            self.moyennes_tags["saes"] = pe_moytag.MoyennesTag(
                "saes",
                pe_moytag.CODE_MOY_UE,
                moy_saes_gen,
                self.matrice_coeffs_moy_gen,
            )

        # Tous les tags
        self.tags_sorted = self.get_all_significant_tags()
        """Tags (personnalisés+compétences) par ordre alphabétique"""

    def get_repr(self, verbose=False) -> str:
        """Nom affiché pour le semestre taggué, de la forme (par ex.):

        * S1#69 si verbose est False
        * S1 FI 2023 si verbose est True
        """
        if not verbose:
            return f"{self.formsemestre}#{self.formsemestre.formsemestre_id}"
        else:
            return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)

    def _get_matrice_coeffs(
        self, ues_inscr_parcours_df: pd.DataFrame, ues_standards: list[UniteEns]
    ) -> pd.DataFrame:
        """Renvoie un dataFrame donnant les coefficients à appliquer aux UEs
        dans le calcul de la moyenne générale (toutes UEs confondues).
        Prend en compte l'inscription des étudiants aux UEs en fonction de leur parcours
        (cf. ues_inscr_parcours_df).

        Args:
            ues_inscr_parcours_df: Les inscriptions des étudiants aux UEs
            ues_standards: Les UEs standards à prendre en compte

        Returns:
            Un dataFrame etudids x acronymes_UEs avec les coeffs des UEs
        """
        matrice_coeffs_moy_gen = ues_inscr_parcours_df * [
            ue.ects for ue in ues_standards  # if ue.type != UE_SPORT <= déjà supprimé
        ]
        matrice_coeffs_moy_gen.columns = [
            self.ues_to_acronymes[ue.id] for ue in ues_standards
        ]
        # Tri par etudids (dim 0) et par acronymes (dim 1)
        matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index()
        matrice_coeffs_moy_gen = matrice_coeffs_moy_gen.sort_index(axis=1)
        return matrice_coeffs_moy_gen

    def _get_capitalisations(self, ues_standards) -> pd.DataFrame:
        """Renvoie un dataFrame résumant les UEs capitalisables par les
        étudiants, d'après les décisions de jury (sous réserve qu'elles existent).

        Args:
            ues_standards: Liste des UEs standards (notamment autres que le sport)
        Returns:
            Un dataFrame etudids x acronymes_UEs dont les valeurs sont ``True`` si l'UE
            est capitalisable, ``False`` sinon
        """
        capitalisations = pd.DataFrame(
            False, index=self.etudids_sorted, columns=self.acronymes_sorted
        )
        self.get_formsemestre_validations()  # charge les validations
        res_jury = self.validations
        if res_jury:
            for etud in self.etuds:
                etudid = etud.etudid
                decisions = res_jury.decisions_jury_ues.get(etudid, {})
                for ue in ues_standards:
                    if ue.id in decisions and decisions[ue.id]["code"] == sco_codes.ADM:
                        capitalisations.loc[etudid, ue.acronyme] = True
        # Tri par etudis et par accronyme d'UE
        capitalisations = capitalisations.sort_index()
        capitalisations = capitalisations.sort_index(axis=1)
        return capitalisations

    def compute_moy_ues_tag(
        self,
        ues_inscr_parcours_df: pd.DataFrame,
        info_tag: dict[int, dict] = None,
        pole=None,
    ) -> pd.DataFrame:
        """Calcule la moyenne par UE des étudiants pour un tag donné,
        en ayant connaissance des informations sur le tag et des inscriptions des étudiants aux différentes UEs.

        info_tag détermine les modules pris en compte :
        * si non `None`, seuls les modules rattachés au tag sont pris en compte
        * si `None`, tous les modules (quelque soit leur rattachement au tag) sont pris
          en compte (sert au calcul de la moyenne générale par ressource ou SAE)

        ues_inscr_parcours_df détermine les UEs pour lesquels le calcul d'une moyenne à un sens.

        `pole` détermine les modules pris en compte :

        * si `pole` vaut `ModuleType.RESSOURCE`, seules les ressources sont prises
          en compte (moyenne de ressources par UEs)
        * si `pole` vaut `ModuleType.SAE`, seules les SAEs sont prises en compte
        * si `pole` vaut `None` (ou toute autre valeur),
          tous les modules sont pris en compte (moyenne d'UEs)

        Les informations sur le tag sont un dictionnaire listant les modimpl_id rattachés au tag,
        et pour chacun leur éventuel coefficient de **repondération**.

        Args:
            ues_inscr_parcours_df: L'inscription aux UEs
        Returns:
            Le dataframe des moyennes du tag par UE
        """
        modimpls_sorted = self.formsemestre.modimpls_sorted

        # Adaptation du mask de calcul des moyennes au tag visé
        modimpls_mask = []
        for modimpl in modimpls_sorted:
            module = modimpl.module  # Le module
            mask = module.ue.type == sco_codes.UE_STANDARD  # Est-ce une UE stantard ?
            if pole == ModuleType.RESSOURCE:
                mask &= module.module_type == ModuleType.RESSOURCE
            elif pole == ModuleType.SAE:
                mask &= module.module_type == ModuleType.SAE
            modimpls_mask += [mask]

        # Prise en compte du tag
        if info_tag:
            # Désactive tous les modules qui ne sont pas pris en compte pour ce tag
            for i, modimpl in enumerate(modimpls_sorted):
                if modimpl.moduleimpl_id not in info_tag:
                    modimpls_mask[i] = False

        # Applique la pondération des coefficients
        modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
        if info_tag:
            for modimpl_id in info_tag:
                ponderation = info_tag[modimpl_id]["ponderation"]
                modimpl_coefs_ponderes_df[modimpl_id] *= ponderation

        # Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)
        moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
            self.sem_cube,
            self.etuds,
            self.formsemestre.modimpls_sorted,
            self.modimpl_inscr_df,
            modimpl_coefs_ponderes_df,
            modimpls_mask,
            self.dispense_ues,
            block=self.formsemestre.block_moyennes,
        )

        # Ne conserve que les UEs standards
        colonnes = [ue.id for ue in self.ues_standards]
        moyennes_ues_tag = moyennes_ues_tag[colonnes]

        # Applique le masque d'inscription aux UE pour ne conserver que les UE dans lequel l'étudiant est inscrit
        moyennes_ues_tag = moyennes_ues_tag[colonnes] * ues_inscr_parcours_df[colonnes]

        # Transforme les UEs en acronyme
        acronymes = [self.ues_to_acronymes[ue.id] for ue in self.ues_standards]
        moyennes_ues_tag.columns = acronymes

        # Tri par etudids et par ordre alphabétique d'acronyme
        moyennes_ues_tag = moyennes_ues_tag.sort_index()
        moyennes_ues_tag = moyennes_ues_tag.sort_index(axis=1)

        return moyennes_ues_tag

    def compute_moy_gen(self):
        """Récupère les moyennes des UEs pour le calcul de la moyenne générale,
        en associant à chaque UE.id son acronyme (toutes UEs confondues)
        """
        df_ues = pd.DataFrame(
            {ue.id: self.etud_moy_ue[ue.id] for ue in self.ues_standards},
            index=self.etudids,
        )
        # Transforme les UEs en acronyme
        colonnes = df_ues.columns
        acronymes = [self.ues_to_acronymes[col] for col in colonnes]
        df_ues.columns = acronymes

        # Tri par ordre aphabétique de colonnes
        df_ues.sort_index(axis=1)

        return df_ues

    def _get_tags_dict(self, avec_moyennes_tags=True):
        """Renvoie les tags personnalisés (déduits des modules du semestre)
        et les tags automatiques ('but'), et toutes leurs informations,
        dans un dictionnaire de la forme :

        ``{"personnalises": {tag: info_sur_le_tag},
           "auto": {tag: {}}``

        Returns:
            Le dictionnaire structuré des tags ("personnalises" vs. "auto")
        """
        dict_tags = {"personnalises": dict(), "auto": dict()}

        if avec_moyennes_tags:
            # Les tags perso (seulement si l'option d'utiliser les tags perso est choisie)
            dict_tags["personnalises"] = get_synthese_tags_personnalises_semestre(
                self.formsemestre
            )

        # Les tags automatiques
        # Déduit des compétences
        # dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
        # noms_tags_comp = list(set(dict_ues_competences.values()))

        # BUT
        dict_tags["auto"] = {"but": {}, "ressources": {}, "saes": {}}
        return dict_tags

    def _check_tags(self, dict_tags):
        """Vérifie l'unicité des tags"""
        noms_tags_perso = sorted(list(set(dict_tags["personnalises"].keys())))
        noms_tags_auto = sorted(list(set(dict_tags["auto"].keys())))  # + noms_tags_comp
        noms_tags = noms_tags_perso + noms_tags_auto

        intersection = list(set(noms_tags_perso) & set(noms_tags_auto))

        if intersection:
            liste_intersection = "\n".join(
                [f"<li><code>{tag}</code></li>" for tag in intersection]
            )
            s = "s" if len(intersection) > 1 else ""
            message = f"""Erreur dans le module PE : Un des tags saisis dans votre
                    programme de formation fait parti des tags réservés. En particulier,
                    votre semestre <em>{self.formsemestre.titre_annee()}</em>
                    contient le{s} tag{s} réservé{s} suivant  :
                    <ul>
                    {liste_intersection}
                    </ul>
                    Modifiez votre programme de formation pour le{s} supprimer.
                    Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
                    """
            raise ScoValueError(message)


def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
    """Etant données les implémentations des modules du semestre (modimpls),
    synthétise les tags renseignés dans le programme pédagogique &
    associés aux modules du semestre,
    en les associant aux modimpls qui les concernent (modimpl_id) et
    au coeff de repondération fournie avec le tag (par défaut 1 si non indiquée)).

    Le dictionnaire fournit est de la forme :

    ``{ tag : { modimplid: {"modimpl": ModImpl,
                            "ponderation": coeff_de_reponderation}
              } }``

    Args:
        formsemestre: Le formsemestre à la base de la recherche des tags

    Return:
        Un dictionnaire décrivant les tags
    """
    synthese_tags = {}

    # Instance des modules du semestre
    modimpls = formsemestre.modimpls_sorted

    for modimpl in modimpls:
        modimpl_id = modimpl.id

        # Liste des tags pour le module concerné
        tags = sco_tag_module.module_tag_list(modimpl.module.id)

        # Traitement des tags recensés, chacun pouvant étant de la forme
        # "mathématiques", "théorie", "pe:0", "maths:2"
        for tag in tags:
            # Extraction du nom du tag et du coeff de pondération
            (tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)

            # Ajout d'une clé pour le tag
            if tagname not in synthese_tags:
                synthese_tags[tagname] = {}

            # Ajout du module (modimpl) au tagname considéré
            synthese_tags[tagname][modimpl_id] = {
                "modimpl": modimpl,  # les données sur le module
                "ponderation": ponderation,  # la pondération demandée pour le tag sur le module
            }

    return synthese_tags
