"""ScoDoc 9 models : Modules
"""
from flask import current_app

from app import db
from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType


class Module(db.Model):
    """Module"""

    __tablename__ = "notes_modules"

    id = db.Column(db.Integer, primary_key=True)
    module_id = db.synonym("id")
    titre = db.Column(db.Text())
    abbrev = db.Column(db.Text())  # nom court
    # certains départements ont des codes infiniment longs: donc Text !
    code = db.Column(db.Text(), nullable=False)
    heures_cours = db.Column(db.Float)
    heures_td = db.Column(db.Float)
    heures_tp = db.Column(db.Float)
    coefficient = db.Column(db.Float)  # coef PPN (sauf en APC)
    ects = db.Column(db.Float)  # Crédits ECTS
    ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), index=True)
    formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
    matiere_id = db.Column(db.Integer, db.ForeignKey("notes_matieres.id"))
    # pas un id mais le numéro du semestre: 1, 2, ...
    # note: en APC, le semestre qui fait autorité est celui de l'UE
    semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
    numero = db.Column(db.Integer)  # ordre de présentation
    # id de l'element pedagogique Apogee correspondant:
    code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
    # Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
    module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
    # Relations:
    modimpls = db.relationship(
        "ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan"
    )
    ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
    tags = db.relationship(
        "NotesTag",
        secondary="notes_modules_tags",
        lazy=True,
        backref=db.backref("modules", lazy=True),
    )
    # BUT
    parcours = db.relationship(
        "ApcParcours",
        secondary=parcours_modules,
        lazy="subquery",
        backref=db.backref("modules", lazy=True),
        order_by="ApcParcours.numero",
    )

    app_critiques = db.relationship(
        "ApcAppCritique",
        secondary=app_critiques_modules,
        lazy="subquery",
        backref=db.backref("modules", lazy=True),
    )

    def __init__(self, **kwargs):
        self.ue_coefs = []
        super(Module, self).__init__(**kwargs)

    def __repr__(self):
        return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
            } id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""

    def clone(self):
        """Create a new copy of this module."""
        mod = Module(
            titre=self.titre,
            abbrev=self.abbrev,
            code=self.code + "-copie",
            heures_cours=self.heures_cours,
            heures_td=self.heures_td,
            heures_tp=self.heures_tp,
            coefficient=self.coefficient,
            ects=self.ects,
            ue_id=self.ue_id,
            matiere_id=self.matiere_id,
            formation_id=self.formation_id,
            semestre_id=self.semestre_id,
            numero=self.numero,  # il est conseillé de renuméroter
            code_apogee="",  # volontairement vide pour éviter les erreurs
            module_type=self.module_type,
        )

        # Les tags:
        for tag in self.tags:
            mod.tags.append(tag)
        # Les parcours
        for parcour in self.parcours:
            mod.parcours.append(parcour)
        # Les AC
        for app_critique in self.app_critiques:
            mod.app_critiques.append(app_critique)
        return mod

    def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
        """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:
            d["parcours"] = [p.to_dict() for p in self.parcours]
            d["ue_coefs"] = [
                c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
            ]
            d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
        if not with_matiere:
            d.pop("matiere", None)
        if not with_ue:
            d.pop("ue", None)
        if convert_objects and with_matiere:
            d["matiere"] = self.matiere.to_dict(convert_objects=True)
        if convert_objects and with_ue:
            d["ue"] = self.ue.to_dict(convert_objects=True)

        # ScoDoc7 output_formators: (backward compat)
        d["module_id"] = self.id
        d["heures_cours"] = 0.0 if self.heures_cours is None else self.heures_cours
        d["heures_td"] = 0.0 if self.heures_td is None else self.heures_td
        d["heures_tp"] = 0.0 if self.heures_tp is None else self.heures_tp
        d["numero"] = 0 if self.numero is None else self.numero
        d["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
        d["module_type"] = 0 if self.module_type is None else self.module_type
        d["code_apogee"] = d["code_apogee"] or ""  # pas de None
        return d

    def is_apc(self):
        "True si module SAÉ ou Ressource"
        return self.module_type and scu.ModuleType(self.module_type) in {
            scu.ModuleType.RESSOURCE,
            scu.ModuleType.SAE,
        }

    def type_name(self) -> str:
        "Le nom du type de module, pour les humains (avec majuscules et accents)"
        return scu.MODULE_TYPE_NAMES[self.module_type]

    def type_abbrv(self) -> str:
        """Le nom du type de module, pour les styles CSS.
        "mod", "malus", "res", "sae"
        """
        return scu.ModuleType.get_abbrev(self.module_type)

    def sort_key_apc(self) -> tuple:
        """Clé de tri pour avoir
        présentation par type (res, sae), parcours, type, numéro
        """
        if (
            self.formation.referentiel_competence is None
            or len(self.parcours)
            == self.formation.referentiel_competence.parcours.count()
            or len(self.parcours) == 0
        ):
            key_parcours = ""
        else:
            key_parcours = "/".join([p.code for p in self.parcours])
        return self.module_type, key_parcours, self.numero or 0

    def set_ue_coef(self, ue, coef: float) -> None:
        """Set coef module vers cette UE"""
        self.update_ue_coef_dict({ue.id: coef})

    def set_ue_coef_dict(self, ue_coef_dict: dict) -> None:
        """set coefs vers les UE (remplace existants)
        ue_coef_dict = { ue_id : coef }
        Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
        """
        if self.formation.has_locked_sems(self.ue.semestre_idx):
            current_app.logguer.info(
                f"set_ue_coef_dict: locked formation, ignoring request"
            )
            raise ScoValueError("Formation verrouillée")
        changed = False
        for ue_id, coef in ue_coef_dict.items():
            # Existant ?
            coefs = [c for c in self.ue_coefs if c.ue_id == ue_id]
            if coefs:
                ue_coef = coefs[0]
                if coef == 0.0:  # supprime ce coef
                    db.session.delete(ue_coef)
                    changed = True
                elif coef != ue_coef.coef:
                    ue_coef.coef = coef
                    db.session.add(ue_coef)
                    changed = True
            else:
                # crée nouveau coef:
                if coef != 0.0:
                    ue = UniteEns.query.get(ue_id)
                    ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
                    db.session.add(ue_coef)
                    self.ue_coefs.append(ue_coef)
                    changed = True
        if changed:
            self.formation.invalidate_module_coefs()

    def update_ue_coef_dict(self, ue_coef_dict: dict):
        """update coefs vers UE (ajoute aux existants)"""
        if self.formation.has_locked_sems(self.ue.semestre_idx):
            current_app.logguer.info(
                f"update_ue_coef_dict: locked formation, ignoring request"
            )
            raise ScoValueError("Formation verrouillée")
        current = self.get_ue_coef_dict()
        current.update(ue_coef_dict)
        self.set_ue_coef_dict(current)

    def get_ue_coef_dict(self):
        """returns { ue_id : coef }"""
        return {p.ue.id: p.coef for p in self.ue_coefs}

    def get_ue_coef_dict_acronyme(self):
        """returns { ue_acronyme : coef }"""
        return {p.ue.acronyme: p.coef for p in self.ue_coefs}

    def delete_ue_coef(self, ue):
        """delete coef"""
        if self.formation.has_locked_sems(self.ue.semestre_idx):
            current_app.logguer.info(
                f"delete_ue_coef: locked formation, ignoring request"
            )
            raise ScoValueError("Formation verrouillée")
        ue_coef = ModuleUECoef.query.get((self.id, ue.id))
        if ue_coef:
            db.session.delete(ue_coef)
            self.formation.invalidate_module_coefs()

    def get_ue_coefs_sorted(self):
        "les coefs d'UE, trié par numéro d'UE"
        # je n'ai pas su mettre un order_by sur le backref sans avoir
        # à redéfinir les relationships...
        return sorted(self.ue_coefs, key=lambda x: x.ue.numero)

    def ue_coefs_list(
        self, include_zeros=True, ues: list["UniteEns"] = None
    ) -> list[tuple["UniteEns", float]]:
        """Liste des coefs vers les UE (pour les modules APC).
        Si ues est spécifié, restreint aux UE indiquées.
        Sinon si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
        sauf UE bonus sport.
        Result: List of tuples [ (ue, coef) ]
        """
        if not self.is_apc():
            return []
        if include_zeros and ues is None:
            # Toutes les UE du même semestre:
            ues = (
                self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
                .filter(UniteEns.type != UE_SPORT)
                .order_by(UniteEns.numero)
                .all()
            )
            if not ues:
                return []
        if ues:
            coefs_dict = self.get_ue_coef_dict()
            coefs_list = []
            for ue in ues:
                coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
            return coefs_list
        # Liste seulement les coefs définis:
        return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]

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

    def get_parcours(self) -> list[ApcParcours]:
        """Les parcours utilisant ce module.
        Si tous les parcours, liste vide (!).
        """
        ref_comp = self.formation.referentiel_competence
        if not ref_comp:
            return []
        tous_parcours_ids = {p.id for p in ref_comp.parcours}
        parcours_ids = {p.id for p in self.parcours}
        if tous_parcours_ids == parcours_ids:
            return []
        return self.parcours


class ModuleUECoef(db.Model):
    """Coefficients des modules vers les UE (APC, BUT)
    En mode APC, ces coefs remplacent le coefficient "PPN" du module.
    """

    __tablename__ = "module_ue_coef"

    module_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
        primary_key=True,
    )
    ue_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_ue.id", ondelete="CASCADE"),
        primary_key=True,
    )
    coef = db.Column(
        db.Float,
        nullable=False,
    )
    module = db.relationship(
        Module,
        backref=db.backref(
            "ue_coefs",
            passive_deletes=True,
            cascade="save-update, merge, delete, delete-orphan",
        ),
    )
    ue = db.relationship(
        "UniteEns",
        backref=db.backref(
            "module_ue_coefs",
            passive_deletes=True,
            cascade="save-update, merge, delete, delete-orphan",
        ),
    )

    def to_dict(self, convert_objects=False) -> dict:
        """If convert_objects, convert all attributes to native types
        (suitable for json encoding).
        """
        d = dict(self.__dict__)
        d.pop("_sa_instance_state", None)
        if convert_objects:
            d["ue"] = self.ue.to_dict(with_module_ue_coefs=False, convert_objects=True)
        return d


class NotesTag(db.Model):
    """Tag sur un module"""

    __tablename__ = "notes_tags"
    __table_args__ = (db.UniqueConstraint("title", "dept_id"),)

    id = db.Column(db.Integer, primary_key=True)
    tag_id = db.synonym("id")

    dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
    title = db.Column(db.Text(), nullable=False)


# Association tag <-> module
notes_modules_tags = db.Table(
    "notes_modules_tags",
    db.Column(
        "tag_id",
        db.Integer,
        db.ForeignKey("notes_tags.id", ondelete="CASCADE"),
    ),
    db.Column(
        "module_id", db.Integer, db.ForeignKey("notes_modules.id", ondelete="CASCADE")
    ),
)

from app.models.ues import UniteEns