# -*- 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.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)
    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 __init__(self, **kwargs):
        super(ModuleImpl, self).__init__(**kwargs)

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

    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)
        return d

    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.ScoImplement):
            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"),
    )

    @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)
            ]
        )
