##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################

from collections import defaultdict
import datetime
from flask import url_for, g
import numpy as np
import pandas as pd

from app import db

from app.comp import moy_ue, moy_sem, inscr_mod
from app.models import ModuleImpl
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_utils import jsnan, fmt_note


class ResultatsSemestreBUT:
    """Structure légère pour stocker les résultats du semestre et
    générer les bulletins.
    __init__ : charge depuis le cache ou calcule
    """

    _cached_attrs = (
        "sem_cube",
        "modimpl_inscr_df",
        "modimpl_coefs_df",
        "etud_moy_ue",
        "modimpls_evals_poids",
        "modimpls_evals_notes",
        "etud_moy_gen",
        "etud_moy_gen_ranks",
        "modimpls_evaluations_complete",
    )

    def __init__(self, formsemestre):
        self.formsemestre = formsemestre
        self.ues = formsemestre.query_ues().all()
        self.modimpls = formsemestre.modimpls.all()
        self.etuds = self.formsemestre.get_inscrits(include_dem=False)
        self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
        self.saes = [
            m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
        ]
        self.ressources = [
            m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
        ]
        if not self.load_cached():
            self.compute()
            self.store()

    def load_cached(self) -> bool:
        "Load cached dataframes, returns False si pas en cache"
        data = ResultatsSemestreBUTCache.get(self.formsemestre.id)
        if not data:
            return False
        for attr in self._cached_attrs:
            setattr(self, attr, data[attr])
        return True

    def store(self):
        "Cache our dataframes"
        ResultatsSemestreBUTCache.set(
            self.formsemestre.id,
            {attr: getattr(self, attr) for attr in self._cached_attrs},
        )

    def compute(self):
        "Charge les notes et inscriptions et calcule toutes les moyennes"
        (
            self.sem_cube,
            self.modimpls_evals_poids,
            self.modimpls_evals_notes,
            modimpls_evaluations,
            self.modimpls_evaluations_complete,
        ) = moy_ue.notes_sem_load_cube(self.formsemestre)
        self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
        self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
            self.formsemestre, ues=self.ues, modimpls=self.modimpls
        )
        # l'idx de la colonne du mod modimpl.id est
        #       modimpl_coefs_df.columns.get_loc(modimpl.id)
        # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
        self.etud_moy_ue = moy_ue.compute_ue_moys(
            self.sem_cube,
            self.etuds,
            self.modimpls,
            self.ues,
            self.modimpl_inscr_df,
            self.modimpl_coefs_df,
        )
        self.etud_moy_gen = moy_sem.compute_sem_moys(
            self.etud_moy_ue, self.modimpl_coefs_df
        )
        self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)

    def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
        "dict synthèse résultats dans l'UE pour les modules indiqués"
        d = {}
        etud_idx = self.etud_index[etud.id]
        ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
        etud_moy_module = self.sem_cube[etud_idx]  # module x UE
        for mi in modimpls:
            if self.modimpl_inscr_df[str(mi.id)][etud.id]:  # si inscrit
                coef = self.modimpl_coefs_df[mi.id][ue.id]
                if coef > 0:
                    d[mi.module.code] = {
                        "id": mi.id,
                        "coef": coef,
                        "moyenne": fmt_note(
                            etud_moy_module[
                                self.modimpl_coefs_df.columns.get_loc(mi.id)
                            ][ue_idx]
                        ),
                    }
        return d

    def etud_ue_results(self, etud, ue):
        "dict synthèse résultats UE"
        d = {
            "id": ue.id,
            "numero": ue.numero,
            "ECTS": {
                "acquis": 0,  # XXX TODO voir jury
                "total": ue.ects,
            },
            "competence": None,  # XXX TODO lien avec référentiel
            "moyenne": {
                "value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
                "min": fmt_note(self.etud_moy_ue[ue.id].min()),
                "max": fmt_note(self.etud_moy_ue[ue.id].max()),
                "moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
            },
            "bonus": None,  # XXX TODO
            "malus": None,  # XXX TODO voir ce qui est ici
            "capitalise": None,  # "AAAA-MM-JJ" TODO
            "ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
            "saes": self.etud_ue_mod_results(etud, ue, self.saes),
        }
        return d

    def etud_mods_results(self, etud, modimpls) -> dict:
        """dict synthèse résultats des modules indiqués,
        avec évaluations de chacun."""
        d = {}
        # etud_idx = self.etud_index[etud.id]
        for mi in modimpls:
            # mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
            # # moyennes indicatives (moyennes de moyennes d'UE)
            # try:
            #     moyennes_etuds = np.nan_to_num(
            #         np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
            #         copy=False,
            #     )
            # except RuntimeWarning:  # all nans in np.nanmean (sur certains etuds sans notes valides)
            #     pass
            # try:
            #     moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
            # except RuntimeWarning:  # all nans in np.nanmean
            #     pass
            if self.modimpl_inscr_df[str(mi.id)][etud.id]:  # si inscrit
                d[mi.module.code] = {
                    "id": mi.id,
                    "titre": mi.module.titre,
                    "code_apogee": mi.module.code_apogee,
                    "url": url_for(
                        "notes.moduleimpl_status",
                        scodoc_dept=g.scodoc_dept,
                        moduleimpl_id=mi.id,
                    ),
                    "moyenne": {
                        # # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
                        # "value": fmt_note(moy_indicative_mod),
                        # "min": fmt_note(moyennes_etuds.min()),
                        # "max": fmt_note(moyennes_etuds.max()),
                        # "moy": fmt_note(moyennes_etuds.mean()),
                    },
                    "evaluations": [
                        self.etud_eval_results(etud, e)
                        for eidx, e in enumerate(mi.evaluations)
                        if e.visibulletin
                        and self.modimpls_evaluations_complete[mi.id][eidx]
                    ],
                }
        return d

    def etud_eval_results(self, etud, e) -> dict:
        "dict resultats d'un étudiant à une évaluation"
        eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id]  # pd.Series
        notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
        d = {
            "id": e.id,
            "description": e.description,
            "date": e.jour.isoformat() if e.jour else None,
            "heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
            "heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
            "coef": e.coefficient,
            "poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
            "note": {
                "value": fmt_note(
                    self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id],
                    note_max=e.note_max,
                ),
                "min": fmt_note(notes_ok.min()),
                "max": fmt_note(notes_ok.max()),
                "moy": fmt_note(notes_ok.mean()),
            },
            "url": url_for(
                "notes.evaluation_listenotes",
                scodoc_dept=g.scodoc_dept,
                evaluation_id=e.id,
            ),
        }
        return d

    def bulletin_etud(self, etud, formsemestre) -> dict:
        """Le bulletin de l'étudiant dans ce semestre"""
        etat_inscription = etud.etat_inscription(formsemestre.id)
        d = {
            "version": "0",
            "type": "BUT",
            "date": datetime.datetime.utcnow().isoformat() + "Z",
            "etudiant": etud.to_dict_bul(),
            "formation": {
                "id": formsemestre.formation.id,
                "acronyme": formsemestre.formation.acronyme,
                "titre_officiel": formsemestre.formation.titre_officiel,
                "titre": formsemestre.formation.titre,
            },
            "formsemestre_id": formsemestre.id,
            "etat_inscription": etat_inscription,
            "options": bulletin_option_affichage(formsemestre),
        }
        semestre_infos = {
            "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
            "date_debut": formsemestre.date_debut.isoformat(),
            "date_fin": formsemestre.date_fin.isoformat(),
            "annee_universitaire": self.formsemestre.annee_scolaire_str(),
            "inscription": "TODO-MM-JJ",  # XXX TODO
            "numero": formsemestre.semestre_id,
            "groupes": [],  # XXX TODO
            "absences": {  # XXX TODO
                "injustifie": 1,
                "total": 33,
            },
        }
        semestre_infos.update(
            sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
        )
        if etat_inscription == scu.INSCRIT:
            semestre_infos.update(
                {
                    "notes": {  # moyenne des moyennes générales du semestre
                        "value": fmt_note(self.etud_moy_gen[etud.id]),
                        "min": fmt_note(self.etud_moy_gen.min()),
                        "moy": fmt_note(self.etud_moy_gen.mean()),
                        "max": fmt_note(self.etud_moy_gen.max()),
                    },
                    "rang": {  # classement wrt moyenne général, indicatif
                        "value": self.etud_moy_gen_ranks[etud.id],
                        "total": len(self.etuds),
                    },
                },
            )
            d.update(
                {
                    "ressources": self.etud_mods_results(etud, self.ressources),
                    "saes": self.etud_mods_results(etud, self.saes),
                    "ues": {
                        ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues
                    },
                    "semestre": semestre_infos,
                },
            )
        else:
            semestre_infos.update(
                {
                    "notes": {
                        "value": "DEM",
                        "min": "",
                        "moy": "",
                        "max": "",
                    },
                    "rang": {"value": "DEM", "total": len(self.etuds)},
                }
            )
            d.update(
                {
                    "semestre": semestre_infos,
                    "ressources": {},
                    "saes": {},
                    "ues": {},
                }
            )

        return d


def bulletin_option_affichage(formsemestre):
    "dict avec les options d'affichages (préférences) pour ce semestre"
    prefs = sco_preferences.SemPreferences(formsemestre.id)
    fields = (
        "bul_show_abs",
        "bul_show_abs_modules",
        "bul_show_ects",
        "bul_show_codemodules",
        "bul_show_matieres",
        "bul_show_rangs",
        "bul_show_ue_rangs",
        "bul_show_mod_rangs",
        "bul_show_moypromo",
        "bul_show_minmax",
        "bul_show_minmax_mod",
        "bul_show_minmax_eval",
        "bul_show_coef",
        "bul_show_ue_cap_details",
        "bul_show_ue_cap_current",
        "bul_show_temporary",
        "bul_temporary_txt",
        "bul_show_uevalid",
        "bul_show_date_inscr",
    )
    # on enlève le "bul_" de la clé:
    return {field[4:]: prefs[field] for field in fields}


# Pour raccorder le code des anciens bulletins qui attendent une NoteTable
class APCNotesTableCompat:
    """Implementation partielle de NotesTable pour les formations APC
    Accès aux notes et rangs.
    """

    def __init__(self, formsemestre):
        self.results = ResultatsSemestreBUT(formsemestre)
        nb_etuds = len(self.results.etuds)
        self.rangs = self.results.etud_moy_gen_ranks
        self.moy_min = self.results.etud_moy_gen.min()
        self.moy_max = self.results.etud_moy_gen.max()
        self.moy_moy = self.results.etud_moy_gen.mean()
        self.bonus = defaultdict(lambda: 0.0)  # XXX
        self.ue_rangs = {
            u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues
        }
        self.mod_rangs = {
            m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls
        }

    def get_ues(self):
        ues = []
        for ue in self.results.ues:
            d = ue.to_dict()
            d.update(
                {
                    "max": self.results.etud_moy_ue[ue.id].max(),
                    "min": self.results.etud_moy_ue[ue.id].min(),
                    "moy": self.results.etud_moy_ue[ue.id].mean(),
                    "nb_moy": len(self.results.etud_moy_ue),
                }
            )
            ues.append(d)
        return ues

    def get_modimpls(self):
        return [m.to_dict() for m in self.results.modimpls]

    def get_etud_moy_gen(self, etudid):
        return self.results.etud_moy_gen[etudid]

    def get_moduleimpls_attente(self):
        return []  # XXX TODO

    def get_etud_rang(self, etudid):
        return self.rangs[etudid]

    def get_etud_rang_group(self, etudid, group_id):
        return (None, 0)  # XXX unimplemented TODO

    def get_etud_ue_status(self, etudid, ue_id):
        return {
            "cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
            "is_capitalized": False,  # XXX TODO
        }

    def get_etud_mod_moy(self, moduleimpl_id, etudid):
        mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
        etud_idx = self.results.etud_index[etudid]
        # moyenne sur les UE:
        self.results.sem_cube[etud_idx, mod_idx].mean()

    def get_mod_stats(self, moduleimpl_id):
        return {
            "moy": "-",
            "max": "-",
            "min": "-",
            "nb_notes": "-",
            "nb_missing": "-",
            "nb_valid_evals": "-",
        }

    def get_evals_in_mod(self, moduleimpl_id):
        mi = ModuleImpl.query.get(moduleimpl_id)
        evals_results = []
        for e in mi.evaluations:
            d = e.to_dict()
            d["heure_debut"] = e.heure_debut  # datetime.time
            d["heure_fin"] = e.heure_fin
            d["jour"] = e.jour  # datetime
            d["notes"] = {
                etud.id: {
                    "etudid": etud.id,
                    "value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
                        etud.id
                    ],
                }
                for etud in self.results.etuds
            }
            evals_results.append(d)
        return evals_results