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

"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
"""
from flask_sqlalchemy.query import Query

from app.comp.res_but import ResultatsSemestreBUT
from app.models import (
    ApcNiveau,
    ApcValidationRCUE,
    Identite,
    ScolarFormSemestreValidation,
    UniteEns,
)
from app.scodoc import codes_cursus
from app.scodoc.codes_cursus import BUT_CODES_ORDER


class RegroupementCoherentUE:
    """Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
    de la même année (BUT1,2,3) liées au *même niveau de compétence*.

    La moyenne (10/20) au RCUE déclenche la compensation des UEs.
    """

    def __init__(
        self,
        etud: Identite,
        niveau: ApcNiveau,
        res_pair: ResultatsSemestreBUT,
        res_impair: ResultatsSemestreBUT,
        semestre_id_impair: int,
        cur_ues_pair: list[UniteEns],
        cur_ues_impair: list[UniteEns],
    ):
        """
        res_pair, res_impair: résultats des formsemestre de l'année en cours, ou None
        cur_ues_pair, cur_ues_impair: ues auxquelles l'étudiant est inscrit cette année
        """
        self.semestre_id_impair = semestre_id_impair
        self.semestre_id_pair = semestre_id_impair + 1
        self.etud: Identite = etud
        self.niveau: ApcNiveau = niveau
        "Le niveau de compétences de ce RCUE"
        # Chercher l'UE en cours pour pair, impair
        # une UE à laquelle l'étudiant est inscrit (non dispensé)
        # dans l'un des formsemestre en cours
        ues = [ue for ue in cur_ues_pair if ue.niveau_competence_id == niveau.id]
        self.ue_cur_pair = ues[0] if ues else None
        "UE paire en cours"
        ues = [ue for ue in cur_ues_impair if ue.niveau_competence_id == niveau.id]
        self.ue_cur_impair = ues[0] if ues else None
        "UE impaire en cours"

        self.validation_ue_cur_pair = (
            ScolarFormSemestreValidation.query.filter_by(
                etudid=etud.id,
                formsemestre_id=res_pair.formsemestre.id,
                ue_id=self.ue_cur_pair.id,
            ).first()
            if self.ue_cur_pair
            else None
        )
        self.validation_ue_cur_impair = (
            ScolarFormSemestreValidation.query.filter_by(
                etudid=etud.id,
                formsemestre_id=res_impair.formsemestre.id,
                ue_id=self.ue_cur_impair.id,
            ).first()
            if self.ue_cur_impair
            else None
        )

        # Autres validations pour l'UE paire
        self.validation_ue_best_pair = best_autre_ue_validation(
            etud.id,
            niveau.id,
            semestre_id_impair + 1,
            res_pair.formsemestre.id if (res_pair and self.ue_cur_pair) else None,
        )
        self.validation_ue_best_impair = best_autre_ue_validation(
            etud.id,
            niveau.id,
            semestre_id_impair,
            res_impair.formsemestre.id if (res_impair and self.ue_cur_impair) else None,
        )

        # Suis-je complet ? (= en cours ou validé sur les deux moitiés)
        self.complete = (self.ue_cur_pair or self.validation_ue_best_pair) and (
            self.ue_cur_impair or self.validation_ue_best_impair
        )
        if not self.complete:
            self.moy_rcue = None

        # Stocke les moyennes d'UE
        self.res_impair = None
        "résultats formsemestre de l'UE si elle est courante, None sinon"
        self.ue_status_impair = None
        if self.ue_cur_impair:
            ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
            self.moy_ue_1 = ue_status["moy"] if ue_status else None  # avec capitalisée
            self.ue_1 = self.ue_cur_impair
            self.res_impair = res_impair
            self.ue_status_impair = ue_status
        elif self.validation_ue_best_impair:
            self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
            self.ue_1 = self.validation_ue_best_impair.ue
        else:
            self.moy_ue_1, self.ue_1 = None, None
        self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0

        self.res_pair = None
        "résultats formsemestre de l'UE si elle est courante, None sinon"
        self.ue_status_pair = None
        if self.ue_cur_pair:
            ue_status = res_pair.get_etud_ue_status(etud.id, self.ue_cur_pair.id)
            self.moy_ue_2 = ue_status["moy"] if ue_status else None  # avec capitalisée
            self.ue_2 = self.ue_cur_pair
            self.res_pair = res_pair
            self.ue_status_pair = ue_status
        elif self.validation_ue_best_pair:
            self.moy_ue_2 = self.validation_ue_best_pair.moy_ue
            self.ue_2 = self.validation_ue_best_pair.ue
        else:
            self.moy_ue_2, self.ue_2 = None, None
        self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0

        # Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées ou antérieures)
        if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
            # Moyenne RCUE (les pondérations par défaut sont 1.)
            self.moy_rcue = (
                self.moy_ue_1 * self.ue_1.coef_rcue
                + self.moy_ue_2 * self.ue_2.coef_rcue
            ) / (self.ue_1.coef_rcue + self.ue_2.coef_rcue)
        else:
            self.moy_rcue = None

    def __repr__(self) -> str:
        return f"""<{self.__class__.__name__} {
            self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) {
            self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})>"""

    def __str__(self) -> str:
        return f"""RCUE {
            self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) + {
            self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})"""

    def query_validations(
        self,
    ) -> Query:  # list[ApcValidationRCUE]
        """Les validations de jury enregistrées pour ce RCUE"""
        return (
            ApcValidationRCUE.query.filter_by(
                etudid=self.etud.id,
            )
            .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
            .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
            .filter(ApcNiveau.id == self.niveau.id)
        )

    def other_ue(self, ue: UniteEns) -> UniteEns:
        """L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
        if ue.id == self.ue_1.id:
            return self.ue_2
        elif ue.id == self.ue_2.id:
            return self.ue_1
        raise ValueError(f"ue {ue} hors RCUE {self}")

    def est_enregistre(self) -> bool:
        """Vrai si ce RCUE, donc le niveau de compétences correspondant
        a une décision jury enregistrée
        """
        return self.query_validations().count() > 0

    def est_compensable(self):
        """Vrai si ce RCUE est validable (uniquement) par compensation
        c'est à dire que sa moyenne est > 10 avec une UE < 10.
        Note: si ADM, est_compensable est faux.
        """
        return (
            (self.moy_rcue is not None)
            and (self.moy_rcue > codes_cursus.BUT_BARRE_RCUE)
            and (
                (self.moy_ue_1_val < codes_cursus.NOTES_BARRE_GEN)
                or (self.moy_ue_2_val < codes_cursus.NOTES_BARRE_GEN)
            )
        )

    def est_suffisant(self) -> bool:
        """Vrai si ce RCUE est > 8"""
        return (self.moy_rcue is not None) and (
            self.moy_rcue > codes_cursus.BUT_RCUE_SUFFISANT
        )

    def est_validable(self) -> bool:
        """Vrai si ce RCUE satisfait les conditions pour être validé,
        c'est à dire que la moyenne des UE qui le constituent soit > 10
        """
        return (self.moy_rcue is not None) and (
            self.moy_rcue > codes_cursus.BUT_BARRE_RCUE
        )

    def code_valide(self) -> ApcValidationRCUE | None:
        "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
        validation = self.query_validations().first()
        if (validation is not None) and (
            validation.code in codes_cursus.CODES_RCUE_VALIDES
        ):
            return validation
        return None


def best_autre_ue_validation(
    etudid: int, niveau_id: int, semestre_id: int, formsemestre_id: int
) -> ScolarFormSemestreValidation:
    """La "meilleure" validation validante d'UE pour ce niveau/semestre"""
    validations = (
        ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
        .join(UniteEns)
        .filter_by(semestre_idx=semestre_id)
        .join(ApcNiveau)
        .filter(ApcNiveau.id == niveau_id)
    )
    validations = [v for v in validations if codes_cursus.code_ue_validant(v.code)]
    # Elimine l'UE en cours si elle existe
    if formsemestre_id is not None:
        validations = [v for v in validations if v.formsemestre_id != formsemestre_id]
    validations = sorted(validations, key=lambda v: BUT_CODES_ORDER.get(v.code, 0))
    return validations[-1] if validations else None


# def compute_ues_by_niveau(
#     niveaux: list[ApcNiveau],
# ) -> dict[int, tuple[list[UniteEns], list[UniteEns]]]:
#     """UEs à valider cette année pour cet étudiant, selon son parcours.
#     Considérer les UEs associées aux niveaux et non celles des formsemestres
#     en cours. Notez que même si l'étudiant n'est pas inscrit ("dispensé") à une UE
#     dans le formsemestre origine, elle doit apparaitre sur la page jury.
#     Return: { niveau_id : ( [ues impair], [ues pair]) }
#     """
#     # Les UEs associées à ce niveau, toutes formations confondues
#     return {
#         niveau.id: (
#             [ue for ue in niveau.ues if ue.semestre_idx % 2],
#             [ue for ue in niveau.ues if not (ue.semestre_idx % 2)],
#         )
#         for niveau in niveaux
#     }