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

"""Stockage des décisions de jury
"""
import pandas as pd
import sqlalchemy as sa

from app import db
from app.comp.res_cache import ResultatsCache
from app.models import (
    ApcValidationAnnee,
    ApcValidationRCUE,
    Formation,
    FormSemestre,
    Identite,
    ScolarAutorisationInscription,
    ScolarFormSemestreValidation,
    UniteEns,
)
from app.scodoc import sco_cache
from app.scodoc import codes_cursus


class ValidationsSemestre(ResultatsCache):
    """Les décisions de jury pour un semestre"""

    _cached_attrs = (
        "decisions_jury",
        "decisions_jury_ues",
        "ue_capitalisees",
    )

    def __init__(self, formsemestre: FormSemestre):
        super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)

        self.decisions_jury = {}
        """Décisions prises dans ce semestre: 
        { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
        self.decisions_jury_ues = {}
        """Décisions sur des UEs dans ce semestre:
        { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date': "d/m/y", "ects" : x}}}
        """
        self.ue_capitalisees: pd.DataFrame = None
        """DataFrame, index etudid
        formsemestre_id : origine de l'UE capitalisée
        is_external : vrai si validation effectuée dans un semestre extérieur
        ue_id : dans le semestre origine (pas toujours de la même formation)
        ue_code : code de l'UE
        moy_ue : note enregistrée
        event_date : date de la validation (jury)"""

        if not self.load_cached():
            self.compute()
            self.store()

    def compute(self):
        "Charge les résultats de jury et UEs capitalisées"
        self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre)
        self.comp_decisions_jury()

    def comp_decisions_jury(self):
        """Cherche les decisions du jury pour le semestre (pas les RCUE).
        Calcule les attributs:
        decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
        decision_jury_ues={ etudid :
            { ue_id : { 'code' : Note|ADM|CMP, 'event_date' : "d/m/y", 'ects' : x }}
        }
        Si la décision n'a pas été prise, la clé etudid n'est pas présente.
        Si l'étudiant est défaillant, pas de décisions d'UE.
        """
        # repris de NotesTable.comp_decisions_jury pour la compatibilité
        decisions_jury_q = ScolarFormSemestreValidation.query.filter_by(
            formsemestre_id=self.formsemestre.id
        )
        decisions_jury = {}
        for decision in decisions_jury_q.filter(
            db.text("ue_id is NULL")  # slt dec. sem.
        ):
            decisions_jury[decision.etudid] = {
                "code": decision.code,
                "assidu": decision.assidu,
                "compense_formsemestre_id": decision.compense_formsemestre_id,
                "event_date": decision.event_date.strftime("%d/%m/%Y"),
            }
        self.decisions_jury = decisions_jury

        # UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
        decisions_jury_ues = {}
        # Parcoure les décisions d'UE:
        for decision in (
            decisions_jury_q.filter(db.text("ue_id is not NULL"))
            .join(UniteEns)
            .order_by(UniteEns.numero)
        ):
            if decision.etudid not in decisions_jury_ues:
                decisions_jury_ues[decision.etudid] = {}
            # Calcul des ECTS associés à cette UE:
            if codes_cursus.code_ue_validant(decision.code) and decision.ue:
                ects = decision.ue.ects or 0.0  # 0 if None
            else:
                ects = 0.0

            decisions_jury_ues[decision.etudid][decision.ue.id] = {
                "code": decision.code,
                "ects": ects,  # 0. si UE non validée
                "event_date": decision.event_date.strftime("%d/%m/%Y"),
            }

        self.decisions_jury_ues = decisions_jury_ues

    def has_decision(self, etud: Identite) -> bool:
        """Vrai si etud a au moins une décision enregistrée depuis
        ce semestre (quelle qu'elle soit)
        """
        return (etud.id in self.decisions_jury_ues) or (etud.id in self.decisions_jury)


def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
    """Liste des UE capitalisées (ADM) utilisables dans ce formsemestre

    Recherche dans les semestres des formations de même code, avec le même semestre_id
    et une date de début antérieure que celle du formsemestre.
    Prend aussi les UE externes validées.

    Attention: fonction très coûteuse, cacher le résultat.

    Résultat: DataFrame avec
        etudid (index)
        formsemestre_id : origine de l'UE capitalisée
        is_external : vrai si validation effectuée dans un semestre extérieur
        ue_id :  dans le semestre origine (pas toujours de la même formation)
        ue_code : code de l'UE
        moy_ue :
        event_date :
    } ]
    """

    # Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
    #   and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )

    query = sa.text(
        """
    SELECT DISTINCT SFV.*, ue.ue_code
    FROM 
    notes_ue ue, 
    notes_formations nf,
    notes_formations nf2, 
    scolar_formsemestre_validation SFV, 
    notes_formsemestre sem,
    notes_formsemestre_inscription ins

    WHERE ue.formation_id = nf.id
    and nf.formation_code = nf2.formation_code
    and nf2.id=:formation_id
    and ins.etudid = SFV.etudid
    and ins.formsemestre_id = :formsemestre_id

    and SFV.ue_id = ue.id
    and SFV.code = 'ADM'

    and ( (sem.id = SFV.formsemestre_id
        and sem.date_debut < :date_debut
        and sem.semestre_id = :semestre_id )
        or (
            ((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
            AND (SFV.semestre_id is NULL OR SFV.semestre_id=:semestre_id)
           ) )
    """
    )
    params = {
        "formation_id": formsemestre.formation.id,
        "formsemestre_id": formsemestre.id,
        "semestre_id": formsemestre.semestre_id,
        "date_debut": formsemestre.date_debut,
    }

    with db.engine.begin() as connection:
        df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
    return df


def erase_decisions_annee_formation(
    etud: Identite, formation: Formation, annee: int, delete=False
) -> list:
    """Efface toutes les décisions de jury de l'étudiant dans les formations de même code
    que celle donnée pour cette année de la formation:
        UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
    Ne considère pas l'origine de la décision.
    annee: entier, 1, 2, 3, ...
    Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
    """
    sem1, sem2 = annee * 2 - 1, annee * 2
    # UEs
    validations = (
        ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
        .join(UniteEns)
        .filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
        .join(Formation)
        .filter_by(formation_code=formation.formation_code)
        .order_by(
            UniteEns.acronyme, UniteEns.numero
        )  # acronyme d'abord car 2 semestres
        .all()
    )
    # RCUEs (a priori inutile de matcher sur l'ue2_id)
    validations += (
        ApcValidationRCUE.query.filter_by(etudid=etud.id)
        .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
        .filter_by(semestre_idx=sem1)
        .join(Formation)
        .filter_by(formation_code=formation.formation_code)
        .order_by(UniteEns.acronyme, UniteEns.numero)
        .all()
    )
    # Validation de semestres classiques
    validations += (
        ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
        .join(
            FormSemestre,
            FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
        )
        .filter(
            db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
        )
        .join(Formation)
        .filter_by(formation_code=formation.formation_code)
        .all()
    )
    # Année BUT
    validations += ApcValidationAnnee.query.filter_by(
        etudid=etud.id,
        ordre=annee,
        referentiel_competence_id=formation.referentiel_competence_id,
    ).all()
    # Autorisations vers les semestres suivants ceux de l'année:
    validations += (
        ScolarAutorisationInscription.query.filter_by(
            etudid=etud.id, formation_code=formation.formation_code
        )
        .filter(
            db.or_(
                ScolarAutorisationInscription.semestre_id == sem1 + 1,
                ScolarAutorisationInscription.semestre_id == sem2 + 1,
            )
        )
        .all()
    )

    if delete:
        for validation in validations:
            db.session.delete(validation)
        db.session.commit()
        sco_cache.invalidate_formsemestre_etud(etud)
        return []
    return validations
