# -*- coding: UTF-8 -*
"""ScoDoc models: moduleimpls
"""
import pandas as pd
from flask_sqlalchemy.query import Query

from app import db
from app.auth.models import User
from app.comp import df_cache
from app.models import APO_CODE_STR_LEN
from app.models.etudiants import Identite
from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu


class ModuleImpl(db.Model):
    """Mise en oeuvre d'un module pour une annee/semestre"""

    __tablename__ = "notes_moduleimpl"
    __table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)

    id = db.Column(db.Integer, primary_key=True)
    code_apogee = db.Column(db.String(APO_CODE_STR_LEN), index=True, nullable=True)
    "id de l'element pedagogique Apogee correspondant"
    edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
    "identifiant emplois du temps (unicité non imposée)"
    moduleimpl_id = db.synonym("id")
    module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
        index=True,
        nullable=False,
    )
    responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
    # formule de calcul moyenne:
    computation_expr = db.Column(db.Text())

    evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl")
    enseignants = db.relationship(
        "User",
        secondary="notes_modules_enseignants",
        lazy="dynamic",
        backref="moduleimpl",
        viewonly=True,
    )

    def __repr__(self):
        return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"

    def get_codes_apogee(self) -> set[str]:
        """Les codes Apogée (codés en base comme "VRT1,VRT2").
        (si non renseigné, ceux du module)
        """
        if self.code_apogee:
            return {x.strip() for x in self.code_apogee.split(",") if x}
        return self.module.get_codes_apogee()

    def get_edt_ids(self) -> list[str]:
        "les ids pour l'emploi du temps: à défaut, les codes Apogée"
        return (
            scu.split_id(self.edt_id)
            or scu.split_id(self.code_apogee)
            or self.module.get_edt_ids()
        )

    def get_evaluations_poids(self) -> pd.DataFrame:
        """Les poids des évaluations vers les UE (accès via cache)"""
        evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
        if evaluations_poids is None:
            from app.comp import moy_mod

            evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id)
            df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
        return evaluations_poids

    def invalidate_evaluations_poids(self):
        """Invalide poids cachés"""
        df_cache.EvaluationsPoidsCache.delete(self.id)

    def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
        """true si les poids des évaluations du module permettent de satisfaire
        les coefficients du PN.
        """
        if not self.module.formation.get_cursus().APC_SAE or (
            self.module.module_type != scu.ModuleType.RESSOURCE
            and self.module.module_type != scu.ModuleType.SAE
        ):
            return True  # Non BUT, toujours conforme
        from app.comp import moy_mod

        return moy_mod.moduleimpl_is_conforme(
            self,
            self.get_evaluations_poids(),
            res.modimpl_coefs_df,
        )

    def to_dict(self, convert_objects=False, with_module=True):
        """as a dict, with the same conversions as in ScoDoc7, including module.
        If convert_objects, convert all attributes to native types
        (suitable jor json encoding).
        """
        d = dict(self.__dict__)
        d.pop("_sa_instance_state", None)
        if convert_objects:
            # on n'exporte pas le formsemestre et les inscriptions
            d.pop("formsemestre", None)
            d.pop("inscriptions", None)
        # ScoDoc7 output_formators: (backward compat)
        d["moduleimpl_id"] = self.id
        d["ens"] = [
            {"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants
        ]
        if with_module:
            d["module"] = self.module.to_dict(convert_objects=convert_objects)
        else:
            d.pop("module", None)
        d["code_apogee"] = d["code_apogee"] or ""  # pas de None
        return d

    def can_edit_evaluation(self, user) -> bool:
        """True if this user can create, delete or edit and evaluation in this modimpl
        (nb: n'implique pas le droit de saisir ou modifier des notes)
        """
        # acces pour resp. moduleimpl et resp. form semestre (dir etud)
        if (
            user.has_permission(Permission.EditAllEvals)
            or user.id == self.responsable_id
            or user.id in (r.id for r in self.formsemestre.responsables)
        ):
            return True
        elif self.formsemestre.ens_can_edit_eval:
            if user.id in (e.id for e in self.enseignants):
                return True

        return False

    def can_edit_notes(self, user: "User", allow_ens=True) -> bool:
        """True if authuser can enter or edit notes in this module.
        If allow_ens, grant access to all ens in this module

        Si des décisions de jury ont déjà été saisies dans ce semestre,
        seul le directeur des études peut saisir des notes (et il ne devrait pas).
        """
        # was sco_permissions_check.can_edit_notes
        from app.scodoc import sco_cursus_dut

        if not self.formsemestre.etat:
            return False  # semestre verrouillé
        is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables)
        can_edit_all_notes = user.has_permission(Permission.EditAllNotes)
        if sco_cursus_dut.formsemestre_has_decisions(self.formsemestre_id):
            # il y a des décisions de jury dans ce semestre !
            return can_edit_all_notes or is_dir_etud
        if (
            not can_edit_all_notes
            and user.id != self.responsable_id
            and not is_dir_etud
        ):
            # enseignant (chargé de TD) ?
            return allow_ens and user.id in (ens.id for ens in self.enseignants)
        return True

    def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
        """Check if user can modify module resp.
        If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
        = Admin, et dir des etud. (si option l'y autorise)
        """
        if not self.formsemestre.etat:
            if raise_exc:
                raise ScoLockedSemError("Modification impossible: semestre verrouille")
            return False
        # -- check access
        # admin ou resp. semestre avec flag resp_can_change_resp
        if user.has_permission(Permission.EditFormSemestre):
            return True
        if (
            user.id in [resp.id for resp in self.formsemestre.responsables]
        ) and self.formsemestre.resp_can_change_ens:
            return True
        if raise_exc:
            raise AccessDenied(f"Modification impossible pour {user}")
        return False

    def est_inscrit(self, etud: Identite) -> bool:
        """
        Vérifie si l'étudiant est bien inscrit au moduleimpl

        Retourne Vrai si c'est le cas, faux sinon
        """

        is_module: int = (
            ModuleImplInscription.query.filter_by(
                etudid=etud.id, moduleimpl_id=self.id
            ).count()
            > 0
        )

        return is_module


# Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table(
    "notes_modules_enseignants",
    db.Column(
        "moduleimpl_id",
        db.Integer,
        db.ForeignKey("notes_moduleimpl.id", ondelete="CASCADE"),
    ),
    db.Column("ens_id", db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")),
    # ? db.UniqueConstraint("moduleimpl_id", "ens_id"),
)
# XXX il manque probablement une relation pour gérer cela


class ModuleImplInscription(db.Model):
    """Inscription à un module  (etudiants,moduleimpl)"""

    __tablename__ = "notes_moduleimpl_inscription"
    __table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)

    id = db.Column(db.Integer, primary_key=True)
    moduleimpl_inscription_id = db.synonym("id")
    moduleimpl_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_moduleimpl.id"),
        index=True,
    )
    etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
    etud = db.relationship(
        Identite,
        backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"),
    )
    modimpl = db.relationship(
        ModuleImpl,
        backref=db.backref("inscriptions", cascade="all, delete-orphan"),
    )

    def to_dict(self) -> dict:
        "dict repr."
        return {
            "id": self.id,
            "etudid": self.etudid,
            "moduleimpl_id": self.moduleimpl_id,
        }

    @classmethod
    def etud_modimpls_in_ue(
        cls, formsemestre_id: int, etudid: int, ue_id: int
    ) -> Query:
        """moduleimpls de l'UE auxquels l'étudiant est inscrit.
        (Attention: inutile en APC, il faut considérer les coefficients)
        """
        return ModuleImplInscription.query.filter(
            ModuleImplInscription.etudid == etudid,
            ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
            ModuleImpl.formsemestre_id == formsemestre_id,
            ModuleImpl.module_id == Module.id,
            Module.ue_id == ue_id,
        )

    @classmethod
    def nb_inscriptions_dans_ue(
        cls, formsemestre_id: int, etudid: int, ue_id: int
    ) -> int:
        """Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
        return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()

    @classmethod
    def sum_coefs_modimpl_ue(
        cls, formsemestre_id: int, etudid: int, ue_id: int
    ) -> float:
        """Somme des coefficients des modules auxquels l'étudiant est inscrit
        dans l'UE du semestre indiqué.
        N'utilise que les coefficients, donc inadapté aux formations APC.
        """
        return sum(
            [
                inscr.modimpl.module.coefficient
                for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
            ]
        )