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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################

"""Semestres: gestion parcours DUT (Arreté du 13 août 2005)
"""

from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription

import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.scolog import logdb
from app.scodoc import sco_cache, sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formations
from app.scodoc.sco_codes_parcours import (
    CMP,
    ADC,
    ADJ,
    ADM,
    AJ,
    ATT,
    NO_SEMESTRE_ID,
    BUG,
    NEXT,
    NEXT2,
    NEXT_OR_NEXT2,
    REO,
    REDOANNEE,
    REDOSEM,
    RA_OR_NEXT,
    RA_OR_RS,
    RS_OR_NEXT,
    CODES_SEM_VALIDES,
    NOTES_BARRE_GEN_COMPENSATION,
    code_semestre_attente,
    code_semestre_validant,
)
from app.scodoc.dutrules import DUTRules  # regles generees a partir du CSV
from app.scodoc.sco_exceptions import ScoValueError


class DecisionSem(object):
    "Decision prenable pour un semestre"

    def __init__(
        self,
        code_etat=None,
        code_etat_ues={},  # { ue_id : code }
        new_code_prev="",
        explication="",  # aide pour le jury
        formsemestre_id_utilise_pour_compenser=None,  # None si code != ADC
        devenir=None,  # code devenir
        assiduite=True,
        rule_id=None,  # id regle correspondante
    ):
        self.code_etat = code_etat
        self.code_etat_ues = code_etat_ues
        self.new_code_prev = new_code_prev
        self.explication = explication
        self.formsemestre_id_utilise_pour_compenser = (
            formsemestre_id_utilise_pour_compenser
        )
        self.devenir = devenir
        self.assiduite = assiduite
        self.rule_id = rule_id
        # code unique (string) utilise pour la gestion du formulaire
        self.codechoice = (
            "C"  # prefix pour éviter que Flask le considère comme int
            + str(
                hash(
                    (
                        code_etat,
                        new_code_prev,
                        formsemestre_id_utilise_pour_compenser,
                        devenir,
                        assiduite,
                    )
                )
            )
        )


class SituationEtudCursus:
    "Semestre dans un cursus"
    pass


class SituationEtudCursusClassic(SituationEtudCursus):
    "Semestre dans un parcours"

    def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
        """
        etud: dict filled by fill_etuds_info()
        """
        self.etud = etud
        self.etudid = etud["etudid"]
        self.formsemestre_id = formsemestre_id
        self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
        self.nt: NotesTableCompat = nt
        self.formation = self.nt.formsemestre.formation
        self.parcours = self.nt.parcours
        # Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
        # pour le DUT, le dernier est toujours S4.
        # Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
        #        (licences et autres formations en 1 seule session))
        self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
            self.semestre_non_terminal = False
        # Liste des semestres du parcours de cet étudiant:
        self._comp_semestres()
        # Determine le semestre "precedent"
        self.prev_formsemestre_id = self._search_prev()
        # Verifie barres
        self._comp_barres()
        # Verifie compensation
        if self.prev and self.sem["gestion_compensation"]:
            self.can_compensate_with_prev = self.prev["can_compensate"]
        else:
            self.can_compensate_with_prev = False

    def get_possible_choices(self, assiduite=True):
        """Donne la liste des décisions possibles en jury (hors décisions manuelles)
        (liste d'instances de DecisionSem)
        assiduite = True si pas de probleme d'assiduité
        """
        choices = []
        if self.prev_decision:
            prev_code_etat = self.prev_decision["code"]
        else:
            prev_code_etat = None

        state = (
            prev_code_etat,
            assiduite,
            self.barre_moy_ok,
            self.barres_ue_ok,
            self.can_compensate_with_prev,
            self.semestre_non_terminal,
        )
        # log('get_possible_choices: state=%s' % str(state) )
        for rule in DUTRules:
            # Saute codes non autorisés dans ce parcours (eg ATT en LP)
            if rule.conclusion[0] in self.parcours.UNUSED_CODES:
                continue
            # Saute regles REDOSEM si pas de semestres decales:
            if (not self.sem["gestion_semestrielle"]) and rule.conclusion[
                3
            ] == "REDOSEM":
                continue
            if rule.match(state):
                if rule.conclusion[0] == ADC:
                    # dans les regles on ne peut compenser qu'avec le PRECEDENT:
                    fiduc = self.prev_formsemestre_id
                    assert fiduc
                else:
                    fiduc = None
                # Detection d'incoherences (regles BUG)
                if rule.conclusion[5] == BUG:
                    log("get_possible_choices: inconsistency: state=%s" % str(state))
                #
                # valid_semestre = code_semestre_validant(rule.conclusion[0])
                choices.append(
                    DecisionSem(
                        code_etat=rule.conclusion[0],
                        new_code_prev=rule.conclusion[2],
                        devenir=rule.conclusion[3],
                        formsemestre_id_utilise_pour_compenser=fiduc,
                        explication=rule.conclusion[5],
                        assiduite=assiduite,
                        rule_id=rule.rule_id,
                    )
                )
        return choices

    def explique_devenir(self, devenir):
        "Phrase d'explication pour le code devenir"
        if not devenir:
            return ""
        s = self.sem["semestre_id"]  # numero semestre courant
        if s < 0:  # formation sans semestres (eg licence)
            next_s = 1
        else:
            next_s = self._get_next_semestre_id()
        # log('s=%s  next=%s' % (s, next_s))
        SA = self.parcours.SESSION_ABBRV  # 'S' ou 'A'
        if self.semestre_non_terminal and not self.all_other_validated():
            passage = "Passe en %s%s" % (SA, next_s)
        else:
            passage = "Formation terminée"
        if devenir == NEXT:
            return passage
        elif devenir == REO:
            return "Réorienté"
        elif devenir == REDOANNEE:
            return "Redouble année (recommence %s%s)" % (SA, (s - 1))
        elif devenir == REDOSEM:
            return "Redouble semestre (recommence en %s%s)" % (SA, s)
        elif devenir == RA_OR_NEXT:
            return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
        elif devenir == RA_OR_RS:
            return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
                SA,
                s,
                SA,
                s - 1,
            )
        elif devenir == RS_OR_NEXT:
            return passage + ", ou semestre %s%s" % (SA, s)
        elif devenir == NEXT_OR_NEXT2:
            return passage + ", ou en semestre %s%s" % (
                SA,
                s + 2,
            )  # coherent avec  get_next_semestre_ids
        elif devenir == NEXT2:
            return "Passe en %s%s" % (SA, s + 2)
        else:
            log("explique_devenir: code devenir inconnu: %s" % devenir)
            return "Code devenir inconnu !"

    def all_other_validated(self):
        "True si tous les autres semestres de cette formation sont validés"
        return self._sems_validated(exclude_current=True)

    def sem_idx_is_validated(self, semestre_id):
        "True si le semestre d'indice indiqué est validé dans ce parcours"
        return self._sem_list_validated(set([semestre_id]))

    def parcours_validated(self):
        "True si parcours validé (diplôme obtenu, donc)."
        return self._sems_validated()

    def _sems_validated(self, exclude_current=False):
        "True si semestres du parcours validés"
        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
            # mono-semestre: juste celui ci
            decision = self.nt.get_etud_decision_sem(self.etudid)
            return decision and code_semestre_validant(decision["code"])
        else:
            to_validate = set(
                range(1, self.parcours.NB_SEM + 1)
            )  # ensemble des indices à valider
            if exclude_current and self.sem["semestre_id"] in to_validate:
                to_validate.remove(self.sem["semestre_id"])
            return self._sem_list_validated(to_validate)

    def can_jump_to_next2(self):
        """True si l'étudiant peut passer directement en Sn+2 (eg de S2 en S4).
        Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente.
        (et que le sem courant n soit validé, ce qui n'est pas testé ici)
        """
        n = self.sem["semestre_id"]
        if not self.sem["gestion_semestrielle"]:
            return False  # pas de semestre décalés
        if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
            return False  # n+2 en dehors du parcours
        if self._sem_list_validated(set(range(1, n))):
            # antérieurs validé, teste suivant
            n1 = n + 1
            for sem in self.get_semestres():
                if (
                    sem["semestre_id"] == n1
                    and sem["formation_code"] == self.formation.formation_code
                ):
                    formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
                    nt: NotesTableCompat = res_sem.load_formsemestre_results(
                        formsemestre
                    )
                    decision = nt.get_etud_decision_sem(self.etudid)
                    if decision and (
                        code_semestre_validant(decision["code"])
                        or code_semestre_attente(decision["code"])
                    ):
                        return True
        return False

    def _sem_list_validated(self, sem_idx_set):
        """True si les semestres dont les indices sont donnés en argument (modifié)
        sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés."""
        for sem in self.get_semestres():
            if sem["formation_code"] == self.formation.formation_code:
                formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
                nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
                decision = nt.get_etud_decision_sem(self.etudid)
                if decision and code_semestre_validant(decision["code"]):
                    # validé
                    sem_idx_set.discard(sem["semestre_id"])

        return not sem_idx_set

    def _comp_semestres(self):
        # etud['sems'] est trie par date decroissante (voir fill_etuds_info)
        if not "sems" in self.etud:
            self.etud["sems"] = sco_etud.etud_inscriptions_infos(
                self.etud["etudid"], self.etud["ne"]
            )["sems"]
        sems = self.etud["sems"][:]  # copy
        sems.reverse()
        # Nb max d'UE et acronymes
        ue_acros = {}  # acronyme ue : 1
        nb_max_ue = 0
        for sem in sems:
            formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
            nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
            ues = nt.get_ues_stat_dict(filter_sport=True)
            for ue in ues:
                ue_acros[ue["acronyme"]] = 1
            nb_ue = len(ues)
            if nb_ue > nb_max_ue:
                nb_max_ue = nb_ue
            # add formation_code to each sem:
            sem["formation_code"] = sco_formations.formation_list(
                args={"formation_id": sem["formation_id"]}
            )[0]["formation_code"]
            # si sem peut servir à compenser le semestre courant, positionne
            #  can_compensate
            sem["can_compensate"] = self.check_compensation_dut(sem, nt)

        self.ue_acros = list(ue_acros.keys())
        self.ue_acros.sort()
        self.nb_max_ue = nb_max_ue
        self.sems = sems

    def get_semestres(self):
        """Liste des semestres dans lesquels a été inscrit
        l'étudiant (quelle que soit la formation), le plus ancien en tête"""
        return self.sems

    def get_parcours_descr(self, filter_futur=False):
        """Description brève du parcours: "S1, S2, ..."
        Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
        """
        cur_begin_date = self.sem["dateord"]
        p = []
        for s in self.sems:
            if s["ins"]["etat"] == "D":
                dem = " (dem.)"
            else:
                dem = ""
            if filter_futur and s["dateord"] > cur_begin_date:
                continue  # skip semestres demarrant apres le courant
            SA = self.parcours.SESSION_ABBRV  # 'S' ou 'A'
            if s["semestre_id"] < 0:
                SA = "A"  # force, cas des DUT annuels par exemple
                p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
            else:
                p.append("%s%d%s" % (SA, s["semestre_id"], dem))
        return ", ".join(p)

    def get_parcours_decisions(self):
        """Decisions de jury de chacun des semestres du parcours,
        du S1 au NB_SEM+1, ou mono-semestre.
        Returns: { semestre_id : code }
        """
        r = {}
        if self.sem["semestre_id"] == NO_SEMESTRE_ID:
            indices = [NO_SEMESTRE_ID]
        else:
            indices = list(range(1, self.parcours.NB_SEM + 1))
        for i in indices:
            # cherche dans les semestres de l'étudiant, en partant du plus récent
            sem = None
            for asem in reversed(self.get_semestres()):
                if asem["semestre_id"] == i:
                    sem = asem
                    break
            if not sem:
                code = ""  # non inscrit à ce semestre
            else:
                formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
                nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
                decision = nt.get_etud_decision_sem(self.etudid)
                if decision:
                    code = decision["code"]
                else:
                    code = "-"
            r[i] = code
        return r

    def _comp_barres(self):
        "calcule barres_ue_ok et barre_moy_ok:  barre moy. gen. et barres UE"
        self.barres_ue_ok, self.barres_ue_diag = self.nt.etud_check_conditions_ues(
            self.etudid
        )
        self.moy_gen = self.nt.get_etud_moy_gen(self.etudid)
        self.barre_moy_ok = (isinstance(self.moy_gen, float)) and (
            self.moy_gen >= (self.parcours.BARRE_MOY - scu.NOTES_TOLERANCE)
        )
        # conserve etat UEs
        ue_ids = [x["ue_id"] for x in self.nt.get_ues_stat_dict(filter_sport=True)]
        self.ues_status = {}  # ue_id : status
        for ue_id in ue_ids:
            self.ues_status[ue_id] = self.nt.get_etud_ue_status(self.etudid, ue_id)

    def could_be_compensated(self):
        "true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
        return self.barres_ue_ok

    def _search_prev(self):
        """Recherche semestre 'precedent'.
        return prev_formsemestre_id
        """
        self.prev = None
        self.prev_decision = None
        if len(self.sems) < 2:
            return None
        # Cherche sem courant dans la liste triee par date_debut
        cur = None
        icur = -1
        for cur in self.sems:
            icur += 1
            if cur["formsemestre_id"] == self.formsemestre_id:
                break
        if not cur or cur["formsemestre_id"] != self.formsemestre_id:
            log(
                f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
            )
            return None  # pas de semestre courant !!!
        # Cherche semestre antérieur de même formation (code) et semestre_id precedent
        #
        # i = icur - 1 # part du courant, remonte vers le passé
        i = len(self.sems) - 1  # par du dernier, remonte vers le passé
        prev = None
        while i >= 0:
            if (
                self.sems[i]["formation_code"] == self.formation.formation_code
                and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
            ):
                prev = self.sems[i]
                break
            i -= 1
        if not prev:
            return None  # pas de precedent trouvé
        self.prev = prev
        # Verifications basiques:
        # ?
        # Code etat du semestre precedent:
        formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
        nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
        self.prev_decision = nt.get_etud_decision_sem(self.etudid)
        self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
        self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
        return self.prev["formsemestre_id"]

    def get_next_semestre_ids(self, devenir):
        """Liste des numeros de semestres autorises avec ce devenir
        Ne vérifie pas que le devenir est possible (doit être fait avant),
        juste que le rang du semestre est dans le parcours [1..NB_SEM]
        """
        s = self.sem["semestre_id"]
        if devenir == NEXT:
            ids = [self._get_next_semestre_id()]
        elif devenir == REDOANNEE:
            ids = [s - 1]
        elif devenir == REDOSEM:
            ids = [s]
        elif devenir == RA_OR_NEXT:
            ids = [s - 1, self._get_next_semestre_id()]
        elif devenir == RA_OR_RS:
            ids = [s - 1, s]
        elif devenir == RS_OR_NEXT:
            ids = [s, self._get_next_semestre_id()]
        elif devenir == NEXT_OR_NEXT2:
            ids = [
                self._get_next_semestre_id(),
                s + 2,
            ]  # cohérent avec explique_devenir()
        elif devenir == NEXT2:
            ids = [s + 2]
        else:
            ids = []  # reoriente ou autre: pas de next !
        # clip [1..NB_SEM]
        r = []
        for idx in ids:
            if idx > 0 and idx <= self.parcours.NB_SEM:
                r.append(idx)
        return r

    def _get_next_semestre_id(self):
        """Indice du semestre suivant non validé.
        S'il n'y en a pas, ramène NB_SEM+1
        """
        s = self.sem["semestre_id"]
        if s >= self.parcours.NB_SEM:
            return self.parcours.NB_SEM + 1
        validated = True
        while validated and (s < self.parcours.NB_SEM):
            s = s + 1
            # semestre s validé ?
            validated = False
            for sem in self.sems:
                if (
                    sem["formation_code"] == self.formation.formation_code
                    and sem["semestre_id"] == s
                ):
                    formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
                    nt: NotesTableCompat = res_sem.load_formsemestre_results(
                        formsemestre
                    )
                    decision = nt.get_etud_decision_sem(self.etudid)
                    if decision and code_semestre_validant(decision["code"]):
                        validated = True
        return s

    def valide_decision(self, decision):
        """Enregistre la decision (instance de DecisionSem)
        Enregistre codes semestre et UE, et autorisations inscription.
        """
        cnx = ndb.GetDBConnexion()
        # -- check
        if decision.code_etat in self.parcours.UNUSED_CODES:
            raise ScoValueError("code decision invalide dans ce parcours")
        #
        if decision.code_etat == ADC:
            fsid = decision.formsemestre_id_utilise_pour_compenser
            if fsid:
                ok = False
                for sem in self.sems:
                    if sem["formsemestre_id"] == fsid and sem["can_compensate"]:
                        ok = True
                        break
                if not ok:
                    raise ScoValueError("valide_decision: compensation impossible")
        # -- supprime decision precedente et enregistre decision
        to_invalidate = []
        if self.nt.get_etud_decision_sem(self.etudid):
            to_invalidate = formsemestre_update_validation_sem(
                cnx,
                self.formsemestre_id,
                self.etudid,
                decision.code_etat,
                decision.assiduite,
                decision.formsemestre_id_utilise_pour_compenser,
            )
        else:
            formsemestre_validate_sem(
                cnx,
                self.formsemestre_id,
                self.etudid,
                decision.code_etat,
                decision.assiduite,
                decision.formsemestre_id_utilise_pour_compenser,
            )
        logdb(
            cnx,
            method="validate_sem",
            etudid=self.etudid,
            commit=False,
            msg="formsemestre_id=%s code=%s"
            % (self.formsemestre_id, decision.code_etat),
        )
        # -- decisions UEs
        formsemestre_validate_ues(
            self.formsemestre_id,
            self.etudid,
            decision.code_etat,
            decision.assiduite,
        )
        # -- modification du code du semestre precedent
        if self.prev and decision.new_code_prev:
            if decision.new_code_prev == ADC:
                # ne compense le prec. qu'avec le sem. courant
                fsid = self.formsemestre_id
            else:
                fsid = None
            to_invalidate += formsemestre_update_validation_sem(
                cnx,
                self.prev["formsemestre_id"],
                self.etudid,
                decision.new_code_prev,
                assidu=True,
                formsemestre_id_utilise_pour_compenser=fsid,
            )
            logdb(
                cnx,
                method="validate_sem",
                etudid=self.etudid,
                commit=False,
                msg="formsemestre_id=%s code=%s"
                % (self.prev["formsemestre_id"], decision.new_code_prev),
            )
            # modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
            formsemestre_validate_ues(
                self.prev["formsemestre_id"],
                self.etudid,
                decision.new_code_prev,
                decision.assiduite,  # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
            )

            sco_cache.invalidate_formsemestre(
                formsemestre_id=self.prev["formsemestre_id"]
            )  # > modif decisions jury (sem, UE)

        try:
            # -- Supprime autorisations venant de ce formsemestre
            autorisations = ScolarAutorisationInscription.query.filter_by(
                etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id
            )
            for autorisation in autorisations:
                db.session.delete(autorisation)
            db.session.flush()
            # -- Enregistre autorisations inscription
            next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
            for next_semestre_id in next_semestre_ids:
                autorisation = ScolarAutorisationInscription(
                    etudid=self.etudid,
                    formation_code=self.formation.formation_code,
                    semestre_id=next_semestre_id,
                    origin_formsemestre_id=self.formsemestre_id,
                )
                db.session.add(autorisation)
            db.session.commit()
        except:
            cnx.session.rollback()
            raise
        sco_cache.invalidate_formsemestre(
            formsemestre_id=self.formsemestre_id
        )  # > modif decisions jury et autorisations inscription
        if decision.formsemestre_id_utilise_pour_compenser:
            # inval aussi le semestre utilisé pour compenser:
            sco_cache.invalidate_formsemestre(
                formsemestre_id=decision.formsemestre_id_utilise_pour_compenser,
            )  # > modif decision jury
        for formsemestre_id in to_invalidate:
            sco_cache.invalidate_formsemestre(
                formsemestre_id=formsemestre_id
            )  # > modif decision jury

    def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat):
        """Compensations DUT
        Vérifie si le semestre sem peut se compenser en utilisant semc
        - semc non utilisé par un autre semestre
        - decision du jury prise  ADM ou ADJ ou ATT ou ADC
        - barres UE (moy ue > 8) dans sem et semc
        - moyenne des moy_gen > 10
        Return boolean
        """
        # -- deja utilise ?
        decc = ntc.get_etud_decision_sem(self.etudid)
        if (
            decc
            and decc["compense_formsemestre_id"]
            and decc["compense_formsemestre_id"] != self.sem["formsemestre_id"]
        ):
            return False
        # -- semestres consecutifs ?
        if abs(self.sem["semestre_id"] - semc["semestre_id"]) != 1:
            return False
        # -- decision jury:
        if decc and not decc["code"] in (ADM, ADJ, ATT, ADC):
            return False
        # -- barres UE et moyenne des moyennes:
        moy_gen = self.nt.get_etud_moy_gen(self.etudid)
        moy_genc = ntc.get_etud_moy_gen(self.etudid)
        try:
            moy_moy = (moy_gen + moy_genc) / 2
        except:  # un des semestres sans aucune note !
            return False

        if (
            self.nt.etud_check_conditions_ues(self.etudid)[0]
            and ntc.etud_check_conditions_ues(self.etudid)[0]
            and moy_moy >= NOTES_BARRE_GEN_COMPENSATION
        ):
            return True
        else:
            return False


class SituationEtudCursusECTS(SituationEtudCursusClassic):
    """Gestion parcours basés sur ECTS"""

    def __init__(self, etud, formsemestre_id, nt):
        SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)

    def could_be_compensated(self):
        return False  # jamais de compensations dans ce parcours

    def get_possible_choices(self, assiduite=True):
        """Listes de décisions "recommandées" (hors décisions manuelles)

        Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?).
        """
        etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid)
        if (
            etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
            and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
        ):
            choices = [
                DecisionSem(
                    code_etat=ADM,
                    new_code_prev=None,
                    devenir=NEXT,
                    formsemestre_id_utilise_pour_compenser=None,
                    explication="Semestre validé",
                    assiduite=assiduite,
                    rule_id="1000",
                )
            ]
        else:
            choices = [
                DecisionSem(
                    code_etat=AJ,
                    new_code_prev=None,
                    devenir=NEXT,
                    formsemestre_id_utilise_pour_compenser=None,
                    explication="Semestre non validé",
                    assiduite=assiduite,
                    rule_id="1001",
                )
            ]
        return choices


# -------------------------------------------------------------------------------------------


def int_or_null(s):
    if s == "":
        return None
    else:
        return int(s)


_scolar_formsemestre_validation_editor = ndb.EditableTable(
    "scolar_formsemestre_validation",
    "formsemestre_validation_id",
    (
        "formsemestre_validation_id",
        "etudid",
        "formsemestre_id",
        "ue_id",
        "code",
        "assidu",
        "event_date",
        "compense_formsemestre_id",
        "moy_ue",
        "semestre_id",
        "is_external",
    ),
    output_formators={
        "event_date": ndb.DateISOtoDMY,
    },
    input_formators={
        "event_date": ndb.DateDMYtoISO,
        "assidu": bool,
        "is_external": bool,
    },
)

scolar_formsemestre_validation_create = _scolar_formsemestre_validation_editor.create
scolar_formsemestre_validation_list = _scolar_formsemestre_validation_editor.list
scolar_formsemestre_validation_delete = _scolar_formsemestre_validation_editor.delete
scolar_formsemestre_validation_edit = _scolar_formsemestre_validation_editor.edit


def formsemestre_validate_sem(
    cnx,
    formsemestre_id,
    etudid,
    code,
    assidu=True,
    formsemestre_id_utilise_pour_compenser=None,
):
    "Ajoute ou change validation semestre"
    args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
    # delete existing
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    try:
        cursor.execute(
            """delete from scolar_formsemestre_validation
        where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""",
            args,
        )
        # insert
        args["code"] = code
        args["assidu"] = assidu
        log("formsemestre_validate_sem: %s" % args)
        scolar_formsemestre_validation_create(cnx, args)
        # marque sem. utilise pour compenser:
        if formsemestre_id_utilise_pour_compenser:
            assert code == ADC
            args2 = {
                "formsemestre_id": formsemestre_id_utilise_pour_compenser,
                "compense_formsemestre_id": formsemestre_id,
                "etudid": etudid,
            }
            cursor.execute(
                """update scolar_formsemestre_validation
            set compense_formsemestre_id=%(compense_formsemestre_id)s
            where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
            and ue_id is null""",
                args2,
            )
    except:
        cnx.rollback()
        raise


def formsemestre_update_validation_sem(
    cnx,
    formsemestre_id,
    etudid,
    code,
    assidu=True,
    formsemestre_id_utilise_pour_compenser=None,
):
    "Update validation semestre"
    args = {
        "formsemestre_id": formsemestre_id,
        "etudid": etudid,
        "code": code,
        "assidu": assidu,
    }
    log("formsemestre_update_validation_sem: %s" % args)
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    to_invalidate = []

    # enleve compensations si necessaire
    # recupere les semestres auparavant utilisés pour invalider les caches
    # correspondants:
    cursor.execute(
        """select formsemestre_id from scolar_formsemestre_validation
    where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""",
        args,
    )
    to_invalidate = [x[0] for x in cursor.fetchall()]
    # suppress:
    cursor.execute(
        """update scolar_formsemestre_validation set compense_formsemestre_id=NULL
    where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""",
        args,
    )
    if formsemestre_id_utilise_pour_compenser:
        assert code == ADC
        # marque sem. utilise pour compenser:
        args2 = {
            "formsemestre_id": formsemestre_id_utilise_pour_compenser,
            "compense_formsemestre_id": formsemestre_id,
            "etudid": etudid,
        }
        cursor.execute(
            """update scolar_formsemestre_validation
        set compense_formsemestre_id=%(compense_formsemestre_id)s
        where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
        and ue_id is null""",
            args2,
        )

    cursor.execute(
        """update scolar_formsemestre_validation
    set code = %(code)s, event_date=DEFAULT, assidu=%(assidu)s
    where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s
    and ue_id is null""",
        args,
    )
    return to_invalidate


def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite):
    """Enregistre codes UE, selon état semestre.
    Les codes UE sont toujours calculés ici, et non passés en paramètres
    car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
    Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
    """
    valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
    cnx = ndb.GetDBConnexion()
    formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    ue_ids = [x["ue_id"] for x in nt.get_ues_stat_dict(filter_sport=True)]
    for ue_id in ue_ids:
        ue_status = nt.get_etud_ue_status(etudid, ue_id)
        if not assiduite:
            code_ue = AJ
        else:
            # log('%s: %s: ue_status=%s' % (formsemestre_id,ue_id,ue_status))
            if (
                isinstance(ue_status["moy"], float)
                and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
            ):
                code_ue = ADM
            elif not isinstance(ue_status["moy"], float):
                # aucune note (pas de moyenne) dans l'UE: ne la valide pas
                code_ue = None
            elif valid_semestre:
                code_ue = CMP
            else:
                code_ue = AJ
        # log('code_ue=%s' % code_ue)
        if etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id) and code_ue:
            do_formsemestre_validate_ue(
                cnx, nt, formsemestre_id, etudid, ue_id, code_ue
            )

        logdb(
            cnx,
            method="validate_ue",
            etudid=etudid,
            msg="ue_id=%s code=%s" % (ue_id, code_ue),
            commit=False,
        )
    cnx.commit()


def do_formsemestre_validate_ue(
    cnx,
    nt,
    formsemestre_id,
    etudid,
    ue_id,
    code,
    moy_ue=None,
    date=None,
    semestre_id=None,
    is_external=False,
):
    """Ajoute ou change validation UE"""
    if semestre_id is None:
        ue = UniteEns.query.get_or_404(ue_id)
        semestre_id = ue.semestre_idx
    args = {
        "formsemestre_id": formsemestre_id,
        "etudid": etudid,
        "ue_id": ue_id,
        "semestre_id": semestre_id,
        "is_external": is_external,
    }
    if date:
        args["event_date"] = date

    # delete existing
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    try:
        cond = "etudid = %(etudid)s and ue_id=%(ue_id)s"
        if formsemestre_id:
            cond += " and formsemestre_id=%(formsemestre_id)s"
        if semestre_id:
            cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)"
        log(f"formsemestre_validate_ue: deleting where {cond}, args={args})")
        cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
        # insert
        args["code"] = code
        if code == ADM:
            if moy_ue is None:
                # stocke la moyenne d'UE capitalisée:
                ue_status = nt.get_etud_ue_status(etudid, ue_id)
                moy_ue = ue_status["moy"] if ue_status else ""
            args["moy_ue"] = moy_ue
        log("formsemestre_validate_ue: create %s" % args)
        if code != None:
            scolar_formsemestre_validation_create(cnx, args)
        else:
            log("formsemestre_validate_ue: code is None, not recording validation")
    except:
        cnx.rollback()
        raise


def formsemestre_has_decisions(formsemestre_id):
    """True s'il y a au moins une validation (decision de jury) dans ce semestre
    equivalent to notes_table.sem_has_decisions() but much faster when nt not cached
    """
    cnx = ndb.GetDBConnexion()
    validations = scolar_formsemestre_validation_list(
        cnx, args={"formsemestre_id": formsemestre_id}
    )
    return len(validations) > 0


def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
    """Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre.
    Ne pas utiliser pour les formations APC !
    """
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        """SELECT mi.*
    FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i
    WHERE i.etudid = %(etudid)s
    and i.moduleimpl_id=mi.id
    and mi.formsemestre_id = %(formsemestre_id)s
    and mi.module_id = mo.id
    and mo.ue_id = %(ue_id)s
    """,
        {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
    )

    return len(cursor.fetchall())