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

"""Cursus en BUT

Classe raccordant avec ScoDoc 7:
 ScoDoc 7 utilisait sco_cursus_dut.SituationEtudCursus

 Ce module définit une classe SituationEtudCursusBUT
 avec la même interface.

"""
import collections
from typing import Union

from flask import g, url_for

from app import db
from app import log
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat

from app.comp import res_sem

from app.models.but_refcomp import (
    ApcAnneeParcours,
    ApcCompetence,
    ApcNiveau,
    ApcParcours,
    ApcParcoursNiveauCompetence,
    ApcReferentielCompetences,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
    ApcValidationAnnee,
    ApcValidationRCUE,
    RegroupementCoherentUE,
)
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError

from app.scodoc import sco_cursus_dut


class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
    """Pour compat ScoDoc 7: à revoir pour le BUT"""

    def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
        super().__init__(etud, formsemestre_id, res)
        # Ajustements pour le BUT
        self.can_compensate_with_prev = False  # jamais de compensation à la mode DUT

    def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat):
        "Jamais de compensation façon DUT"
        return False

    def parcours_validated(self):
        "True si le parcours est validé"
        return False  # XXX TODO


class EtudCursusBUT:
    """L'état de l'étudiant dans son cursus BUT
    Liste des niveaux validés/à valider
    """

    def __init__(self, etud: Identite, formation: Formation):
        """formation indique la spécialité préparée"""
        # Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
        if formation.id not in (
            ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
        ):
            raise ScoValueError(
                f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
            )
        if not formation.referentiel_competence:
            raise ScoNoReferentielCompetences(formation=formation)
        #
        self.etud = etud
        self.formation = formation
        self.inscriptions = sorted(
            [
                ins
                for ins in etud.formsemestre_inscriptions
                if ins.formsemestre.formation.referentiel_competence
                and (
                    ins.formsemestre.formation.referentiel_competence.id
                    == formation.referentiel_competence.id
                )
            ],
            key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
        )
        "Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
        self.parcour: ApcParcours = self.inscriptions[-1].parcour
        "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
        self.niveaux_by_annee = {}
        "{ annee : liste des niveaux à valider }"
        self.niveaux: dict[int, ApcNiveau] = {}
        "cache les niveaux"
        for annee in (1, 2, 3):
            niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
                annee, [self.parcour] if self.parcour else None
            )[1]
            # groupe les niveaux de tronc commun et ceux spécifiques au parcour
            self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
                niveaux_d[self.parcour.id] if self.parcour else []
            )
            self.niveaux.update(
                {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
            )
        # Probablement inutile:
        # # Cherche les validations de jury enregistrées pour chaque niveau
        # self.validations_by_niveau = collections.defaultdict(lambda: [])
        # " { niveau_id : [ ApcValidationRCUE ] }"
        # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
        #     self.validations_by_niveau[validation_rcue.niveau().id].append(
        #         validation_rcue
        #     )
        # self.validation_by_niveau = {
        #     niveau_id: sorted(
        #         validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
        #     )[0]
        #     for niveau_id, validations in self.validations_by_niveau.items()
        # }
        # "{ niveau_id : meilleure validation pour ce niveau }"

        self.validation_par_competence_et_annee = {}
        """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
        for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
            niveau = validation_rcue.niveau()
            if not niveau.competence.id in self.validation_par_competence_et_annee:
                self.validation_par_competence_et_annee[niveau.competence.id] = {}
            previous_validation = self.validation_par_competence_et_annee.get(
                niveau.competence.id
            ).get(validation_rcue.annee())
            # prend la "meilleure" validation
            if (not previous_validation) or (
                sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
                > sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
            ):
                self.validation_par_competence_et_annee[niveau.competence.id][
                    niveau.annee
                ] = validation_rcue

        self.competences = {
            competence.id: competence
            for competence in (
                self.parcour.query_competences()
                if self.parcour
                else self.formation.referentiel_competence.get_competences_tronc_commun()
            )
        }
        "cache { competence_id : competence }"

    def to_dict(self):
        """
        {
            competence_id : {
                annee : meilleure_validation
            }
        }
        """
        # XXX lent, provisoirement utilisé par TableJury.add_but_competences()
        return {
            competence.id: {
                annee: self.validation_par_competence_et_annee.get(
                    competence.id, {}
                ).get(annee)
                for annee in ("BUT1", "BUT2", "BUT3")
            }
            for competence in self.competences.values()
        }

    # XXX TODO OPTIMISATION ACCESS TABLE JURY
    def to_dict_codes(self) -> dict[int, dict[str, int]]:
        """
        {
            competence_id : {
                annee : { validation }
            }
        }
        où validation est un petit dict avec niveau_id, etc.
        """
        d = {}
        for competence in self.competences.values():
            d[competence.id] = {}
            for annee in ("BUT1", "BUT2", "BUT3"):
                validation_rcue: ApcValidationRCUE = (
                    self.validation_par_competence_et_annee.get(competence.id, {}).get(
                        annee
                    )
                )

                d[competence.id][annee] = (
                    validation_rcue.to_dict_codes() if validation_rcue else None
                )
        return d


class FormSemestreCursusBUT:
    """L'état des étudiants d'un formsemestre dans leur cursus BUT
    Permet d'obtenir pour chacun liste des niveaux validés/à valider
    """

    def __init__(self, res: ResultatsSemestreBUT):
        """res indique le formsemestre de référence,
        qui donne la liste des étudiants et le référentiel de compétence.
        """
        self.res = res
        self.formsemestre = res.formsemestre
        if not res.formsemestre.formation.referentiel_competence:
            raise ScoNoReferentielCompetences(formation=res.formsemestre.formation)
        # Données cachées pour accélerer les accès:
        self.referentiel_competences_id: int = (
            self.res.formsemestre.formation.referentiel_competence_id
        )
        self.ue_ids: set[int] = set()
        "set of ue_ids known to belong to our cursus"
        self.parcours_by_id: dict[int, ApcParcours] = {}
        "cache des parcours"
        self.niveaux_by_parcour_by_annee: dict[int, dict[int, list[ApcNiveau]]] = {}
        "cache { parcour_id : { annee : [ parcour] } }"
        self.niveaux_by_id: dict[int, ApcNiveau] = {}
        "cache niveaux"

    def get_niveaux_parcours_etud(self, etud: Identite) -> dict[int, list[ApcNiveau]]:
        """Les niveaux compétences que doit valider cet étudiant.
        Le parcour considéré est celui de l'inscription dans le semestre courant.
        Si on est en début de cursus, on peut être en tronc commun sans avoir choisi
        de parcours. Dans ce cas, on n'aura que les compétences de tronc commun.
        Il faudra donc, avant de diplômer, s'assurer que les compétences du parcours
        du dernier semestre (S6) sont validées (avec parcour non NULL).
        """
        parcour_id = self.res.etuds_parcour_id.get(etud.id)
        if parcour_id is None:
            parcour = None
        else:
            if parcour_id not in self.parcours_by_id:
                self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
            parcour = self.parcours_by_id[parcour_id]

        return self.get_niveaux_parcours_by_annee(parcour)

    def get_niveaux_parcours_by_annee(
        self, parcour: ApcParcours
    ) -> dict[int, list[ApcNiveau]]:
        """La liste des niveaux de compétences du parcours, par année BUT.
        { 1 : [ niveau, ... ] }
        Si parcour est None, donne uniquement les niveaux tronc commun
            (cas utile par exemple en 1ere année, mais surtout pas pour donner un diplôme!)
        """
        parcour_id = None if parcour is None else parcour.id
        if parcour_id in self.niveaux_by_parcour_by_annee:
            return self.niveaux_by_parcour_by_annee[parcour_id]

        ref_comp: ApcReferentielCompetences = (
            self.res.formsemestre.formation.referentiel_competence
        )
        niveaux_by_annee = {}
        for annee in (1, 2, 3):
            niveaux_d = ref_comp.get_niveaux_by_parcours(
                annee, [parcour] if parcour else None
            )[1]
            # groupe les niveaux de tronc commun et ceux spécifiques au parcour
            niveaux_by_annee[annee] = niveaux_d["TC"] + (
                niveaux_d[parcour.id] if parcour else []
            )
            self.niveaux_by_parcour_by_annee[parcour_id] = niveaux_by_annee
            self.niveaux_by_id.update(
                {niveau.id: niveau for niveau in niveaux_by_annee[annee]}
            )
        return niveaux_by_annee

    def get_etud_validation_par_competence_et_annee(self, etud: Identite):
        """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
        validation_par_competence_et_annee = {}
        for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
            # On s'assurer qu'elle concerne notre cursus !
            ue = validation_rcue.ue2
            if ue.id not in self.ue_ids:
                if (
                    ue.formation.referentiel_competences_id
                    == self.referentiel_competences_id
                ):
                    self.ue_ids = ue.id
                else:
                    continue  # skip this validation
            niveau = validation_rcue.niveau()
            if not niveau.competence.id in validation_par_competence_et_annee:
                validation_par_competence_et_annee[niveau.competence.id] = {}
            previous_validation = validation_par_competence_et_annee.get(
                niveau.competence.id
            ).get(validation_rcue.annee())
            # prend la "meilleure" validation
            if (not previous_validation) or (
                sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
                > sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
            ):
                self.validation_par_competence_et_annee[niveau.competence.id][
                    niveau.annee
                ] = validation_rcue
        return validation_par_competence_et_annee

    def list_etud_inscriptions(self, etud: Identite):
        "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
        self.niveaux_by_annee = {}
        "{ annee : liste des niveaux à valider }"
        self.niveaux: dict[int, ApcNiveau] = {}
        "cache les niveaux"
        for annee in (1, 2, 3):
            niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
                annee, [self.parcour] if self.parcour else None  # XXX WIP
            )[1]
            # groupe les niveaux de tronc commun et ceux spécifiques au parcour
            self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
                niveaux_d[self.parcour.id] if self.parcour else []
            )
            self.niveaux.update(
                {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
            )

        self.validation_par_competence_et_annee = {}
        """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
        for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
            niveau = validation_rcue.niveau()
            if not niveau.competence.id in self.validation_par_competence_et_annee:
                self.validation_par_competence_et_annee[niveau.competence.id] = {}
            previous_validation = self.validation_par_competence_et_annee.get(
                niveau.competence.id
            ).get(validation_rcue.annee())
            # prend la "meilleure" validation
            if (not previous_validation) or (
                sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
                > sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
            ):
                self.validation_par_competence_et_annee[niveau.competence.id][
                    niveau.annee
                ] = validation_rcue

        self.competences = {
            competence.id: competence
            for competence in (
                self.parcour.query_competences()
                if self.parcour
                else self.formation.referentiel_competence.get_competences_tronc_commun()
            )
        }
        "cache { competence_id : competence }"


def formsemestre_warning_apc_setup(
    formsemestre: FormSemestre, res: ResultatsSemestreBUT
) -> str:
    """Vérifie que la formation est OK pour un BUT:
    - ref. compétence associé
    - tous les niveaux des parcours du semestre associés à des UEs du formsemestre
    - pas d'UE non associée à un niveau
    Renvoie fragment de HTML.
    """
    if not formsemestre.formation.is_apc():
        return ""
    if formsemestre.formation.referentiel_competence is None:
        return f"""<div class="formsemestre_status_warning">
        La <a class="stdlink" href="{
            url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
        }">formation n'est pas associée à un référentiel de compétence.</a>
        </div>
        """
    # Vérifie les niveaux de chaque parcours
    H = []
    for parcour in formsemestre.parcours or [None]:
        annee = (formsemestre.semestre_id + 1) // 2
        niveaux_ids = {
            niveau.id
            for niveau in ApcNiveau.niveaux_annee_de_parcours(
                parcour, annee, formsemestre.formation.referentiel_competence
            )
        }
        ues_parcour = formsemestre.formation.query_ues_parcour(parcour).filter(
            UniteEns.semestre_idx == formsemestre.semestre_id
        )
        ues_niveaux_ids = {
            ue.niveau_competence.id for ue in ues_parcour if ue.niveau_competence
        }
        if niveaux_ids != ues_niveaux_ids:
            H.append(
                f"""Parcours {parcour.code if parcour else "Tronc commun"} : 
                {len(ues_niveaux_ids)} UE avec niveaux
                mais {len(niveaux_ids)} niveaux à valider !
            """
            )
    if not H:
        return ""
    return f"""<div class="formsemestre_status_warning">
    Problème dans la configuration de la formation:
    <ul>
        <li>{ '</li><li>'.join(H) }</li>
    </ul>
    <p class="help">Vérifiez les parcours cochés pour ce semestre, 
    et les associations entre UE et niveaux <a class="stdlink" href="{
            url_for("notes.parcour_formation", scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)
        }">dans la formation.</a>
    </p>
    </div>
    """