# -*- coding: UTF-8 -*

"""Décisions de jury (validations) des RCUE et années du BUT
"""
from typing import Union

from flask_sqlalchemy.query import Query

from app import db
from app.models import CODE_STR_LEN
from app.models.but_refcomp import ApcNiveau
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc import codes_cursus as sco_codes
from app.scodoc import sco_utils as scu


class ApcValidationRCUE(db.Model):
    """Validation des niveaux de compétences

    aka "regroupements cohérents d'UE" dans le jargon BUT.

    Le formsemestre est celui du semestre PAIR du niveau de compétence
    """

    __tablename__ = "apc_validation_rcue"
    # Assure unicité de la décision:
    __table_args__ = (
        db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
    )

    id = db.Column(db.Integer, primary_key=True)
    etudid = db.Column(
        db.Integer,
        db.ForeignKey("identite.id", ondelete="CASCADE"),
        index=True,
        nullable=False,
    )
    formsemestre_id = db.Column(
        db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
    )
    "formsemestre pair du RCUE"
    # Les deux UE associées à ce niveau:
    ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
    ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
    # optionnel, le parcours dans lequel se trouve la compétence:
    parcours_id = db.Column(
        db.Integer, db.ForeignKey("apc_parcours.id", ondelete="set null"), nullable=True
    )
    date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
    code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)

    etud = db.relationship("Identite", backref="apc_validations_rcues")
    formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
    ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
    ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
    parcour = db.relationship("ApcParcours")

    def __repr__(self):
        return f"""<{self.__class__.__name__} {self.id} {self.etud} {
            self.ue1}/{self.ue2}:{self.code!r}>"""

    def __str__(self):
        return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
            self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""

    def to_html(self) -> str:
        "description en HTML"
        return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
                <b>{self.code}</b>
                <em>enregistrée le {self.date.strftime("%d/%m/%Y")}
                à {self.date.strftime("%Hh%M")}</em>"""

    def annee(self) -> str:
        """l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
        niveau = self.niveau()
        return niveau.annee if niveau else None

    def niveau(self) -> ApcNiveau:
        """Le niveau de compétence associé à cet RCUE."""
        # Par convention, il est donné par la seconde UE
        return self.ue2.niveau_competence

    def to_dict(self):
        "as a dict"
        d = dict(self.__dict__)
        d.pop("_sa_instance_state", None)
        return d

    def to_dict_bul(self) -> dict:
        "Export dict pour bulletins: le code et le niveau de compétence"
        niveau = self.niveau()
        return {
            "code": self.code,
            "niveau": None if niveau is None else niveau.to_dict_bul(),
        }

    def to_dict_codes(self) -> dict:
        "Dict avec seulement les ids et la date - pour cache table jury"
        return {
            "id": self.id,
            "code": self.code,
            "date": self.date,
            "etudid": self.etudid,
            "niveau_id": self.niveau().id,
            "formsemestre_id": self.formsemestre_id,
        }


# Attention: ce n'est pas un modèle mais une classe ordinaire:
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 UE.
    """

    def __init__(
        self,
        etud: Identite,
        formsemestre_1: FormSemestre,
        dec_ue_1: "DecisionsProposeesUE",
        formsemestre_2: FormSemestre,
        dec_ue_2: "DecisionsProposeesUE",
        inscription_etat: str,
    ):
        ue_1 = dec_ue_1.ue
        ue_2 = dec_ue_2.ue
        # Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
        if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
            (ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
                (ue_2, formsemestre_2),
                (ue_1, formsemestre_1),
            )
        assert formsemestre_1.semestre_id % 2 == 1
        assert formsemestre_2.semestre_id % 2 == 0
        assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
        assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
        self.etud = etud
        self.formsemestre_1 = formsemestre_1
        "semestre impair"
        self.ue_1 = ue_1
        self.formsemestre_2 = formsemestre_2
        "semestre pair"
        self.ue_2 = ue_2
        # Stocke les moyennes d'UE
        if inscription_etat != scu.INSCRIT:
            self.moy_rcue = None
            self.moy_ue_1 = self.moy_ue_2 = "-"
            self.moy_ue_1_val = self.moy_ue_2_val = 0.0
            return
        self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
        self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
        self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
        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)
        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 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
            ) / (ue_1.coef_rcue + ue_2.coef_rcue)
        else:
            self.moy_rcue = None

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

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

    def query_validations(
        self,
    ) -> Query:  # list[ApcValidationRCUE]
        """Les validations de jury enregistrées pour ce RCUE"""
        niveau = self.ue_2.niveau_competence

        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 == 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 > sco_codes.BUT_BARRE_RCUE)
            and (
                (self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
                or (self.moy_ue_2_val < sco_codes.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 > sco_codes.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 > sco_codes.BUT_BARRE_RCUE
        )

    def code_valide(self) -> Union[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 sco_codes.CODES_RCUE_VALIDES
        ):
            return validation
        return None


# unused
# def find_rcues(
#     formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
# ) -> list[RegroupementCoherentUE]:
#     """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
#     ce semestre pour cette UE.

#     Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
#     En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.

#     Résultat: la liste peut être vide.
#     """
#     if (ue.niveau_competence is None) or (ue.semestre_idx is None):
#         return []

#     if ue.semestre_idx % 2:  # S1, S3, S5
#         other_semestre_idx = ue.semestre_idx + 1
#     else:
#         other_semestre_idx = ue.semestre_idx - 1

#     cursor = db.session.execute(
#         text(
#             """SELECT
#             ue.id, formsemestre.id
#             FROM
#                 notes_ue ue,
#                 notes_formsemestre_inscription inscr,
#                 notes_formsemestre formsemestre

#             WHERE
#                 inscr.etudid = :etudid
#             AND inscr.formsemestre_id = formsemestre.id

#             AND formsemestre.semestre_id = :other_semestre_idx
#             AND ue.formation_id = formsemestre.formation_id
#             AND ue.niveau_competence_id = :ue_niveau_competence_id
#             AND ue.semestre_idx = :other_semestre_idx
#             """
#         ),
#         {
#             "etudid": etud.id,
#             "other_semestre_idx": other_semestre_idx,
#             "ue_niveau_competence_id": ue.niveau_competence_id,
#         },
#     )
#     rcues = []
#     for ue_id, formsemestre_id in cursor:
#         other_ue = UniteEns.query.get(ue_id)
#         other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
#         rcues.append(
#             RegroupementCoherentUE(
#                 etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
#             )
#         )
#     # safety check: 1 seul niveau de comp. concerné:
#     assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
#     return rcues


class ApcValidationAnnee(db.Model):
    """Validation des années du BUT"""

    __tablename__ = "apc_validation_annee"
    # Assure unicité de la décision:
    __table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
    id = db.Column(db.Integer, primary_key=True)
    etudid = db.Column(
        db.Integer,
        db.ForeignKey("identite.id", ondelete="CASCADE"),
        index=True,
        nullable=False,
    )
    ordre = db.Column(db.Integer, nullable=False)
    "numéro de l'année: 1, 2, 3"
    formsemestre_id = db.Column(
        db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
    )
    "le semestre IMPAIR (le 1er) de l'année"
    annee_scolaire = db.Column(db.Integer, nullable=False)  # 2021
    date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
    code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)

    etud = db.relationship("Identite", backref="apc_validations_annees")
    formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")

    def __repr__(self):
        return f"""<{self.__class__.__name__} {self.id} {self.etud
            } BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""

    def __str__(self):
        return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""

    def to_dict_bul(self) -> dict:
        "dict pour bulletins"
        return {
            "annee_scolaire": self.annee_scolaire,
            "date": self.date.isoformat(),
            "code": self.code,
            "ordre": self.ordre,
        }


def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
    """
    Un dict avec les décisions de jury BUT enregistrées:
     - decision_rcue  : list[dict]
     - decision_annee : dict
    Ne reprend pas les décisions d'UE, non spécifiques au BUT.
    """
    decisions = {}
    # --- RCUEs: seulement sur semestres pairs XXX à améliorer
    if formsemestre.semestre_id % 2 == 0:
        # validations émises depuis ce formsemestre:
        validations_rcues = ApcValidationRCUE.query.filter_by(
            etudid=etud.id, formsemestre_id=formsemestre.id
        )
        decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
        titres_rcues = []
        for dec_rcue in decisions["decision_rcue"]:
            niveau = dec_rcue["niveau"]
            if niveau is None:
                titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
            else:
                titres_rcues.append(
                    f"""{niveau["competence"]["titre"]}&nbsp;{niveau["ordre"]}:&nbsp;{
                        dec_rcue["code"]}"""
                )
        decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
        decisions["descr_decisions_niveaux"] = (
            "Niveaux de compétences: " + decisions["descr_decisions_rcue"]
        )
    else:
        decisions["decision_rcue"] = []
        decisions["descr_decisions_rcue"] = ""
        decisions["descr_decisions_niveaux"] = ""
    # --- Année: prend la validation pour l'année scolaire de ce semestre
    validation = (
        ApcValidationAnnee.query.filter_by(
            etudid=etud.id,
            annee_scolaire=formsemestre.annee_scolaire(),
        )
        .join(ApcValidationAnnee.formsemestre)
        .join(FormSemestre.formation)
        .filter(Formation.formation_code == formsemestre.formation.formation_code)
        .first()
    )
    if validation:
        decisions["decision_annee"] = validation.to_dict_bul()
    else:
        decisions["decision_annee"] = None
    return decisions
