# -*- coding: UTF-8 -*

"""ScoDoc models: formsemestre
"""
import datetime
from functools import cached_property

import flask_sqlalchemy

from app import db
from app import log
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models import UniteEns

import app.scodoc.sco_utils as scu
from app.models.ues import UniteEns
from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from app.models.etudiants import Identite
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission


class FormSemestre(db.Model):
    """Mise en oeuvre d'un semestre de formation"""

    __tablename__ = "notes_formsemestre"

    id = db.Column(db.Integer, primary_key=True)
    formsemestre_id = db.synonym("id")
    # dept_id est aussi dans la formation, ajouté ici pour
    # simplifier et accélérer les selects dans notesdb
    dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
    formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
    semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
    titre = db.Column(db.Text())
    date_debut = db.Column(db.Date())
    date_fin = db.Column(db.Date())
    etat = db.Column(
        db.Boolean(), nullable=False, default=True, server_default="true"
    )  # False si verrouillé
    modalite = db.Column(
        db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
    )  # "FI", "FAP", "FC", ...
    # gestion compensation sem DUT:
    gestion_compensation = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="false"
    )
    # ne publie pas le bulletin XML ou JSON:
    bul_hide_xml = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="false"
    )
    # Bloque le calcul des moyennes (générale et d'UE)
    block_moyennes = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="false"
    )
    # semestres decales (pour gestion jurys):
    gestion_semestrielle = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="false"
    )
    # couleur fond bulletins HTML:
    bul_bgcolor = db.Column(
        db.String(SHORT_STR_LEN), default="white", server_default="white"
    )
    # autorise resp. a modifier semestre:
    resp_can_edit = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="false"
    )
    # autorise resp. a modifier slt les enseignants:
    resp_can_change_ens = db.Column(
        db.Boolean(), nullable=False, default=True, server_default="true"
    )
    # autorise les ens a creer des evals:
    ens_can_edit_eval = db.Column(
        db.Boolean(), nullable=False, default=False, server_default="False"
    )
    # code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
    elt_sem_apo = db.Column(db.Text())  # peut être fort long !
    # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
    elt_annee_apo = db.Column(db.Text())

    # Relations:
    etapes = db.relationship(
        "FormSemestreEtape", cascade="all,delete", backref="formsemestre"
    )
    modimpls = db.relationship(
        "ModuleImpl",
        backref="formsemestre",
        lazy="dynamic",
    )
    etuds = db.relationship(
        "Identite",
        secondary="notes_formsemestre_inscription",
        viewonly=True,
        lazy="dynamic",
    )
    responsables = db.relationship(
        "User",
        secondary="notes_formsemestre_responsables",
        lazy=True,
        backref=db.backref("formsemestres", lazy=True),
    )
    partitions = db.relationship(
        "Partition",
        backref=db.backref("formsemestre", lazy=True),
        lazy="dynamic",
    )
    # Ancien id ScoDoc7 pour les migrations de bases anciennes
    # ne pas utiliser après migrate_scodoc7_dept_archives
    scodoc7_id = db.Column(db.Text(), nullable=True)

    def __init__(self, **kwargs):
        super(FormSemestre, self).__init__(**kwargs)
        if self.modalite is None:
            self.modalite = FormationModalite.DEFAULT_MODALITE

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

    def to_dict(self):
        d = dict(self.__dict__)
        d.pop("_sa_instance_state", None)
        # ScoDoc7 output_formators: (backward compat)
        d["formsemestre_id"] = self.id
        d["titre_num"] = self.titre_num()
        if self.date_debut:
            d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
            d["date_debut_iso"] = self.date_debut.isoformat()
        else:
            d["date_debut"] = d["date_debut_iso"] = ""
        if self.date_fin:
            d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
            d["date_fin_iso"] = self.date_fin.isoformat()
        else:
            d["date_fin"] = d["date_fin_iso"] = ""
        d["responsables"] = [u.id for u in self.responsables]
        return d

    def get_infos_dict(self) -> dict:
        """Un dict avec des informations sur le semestre
        pour les bulletins et autres templates
        (contenu compatible scodoc7 / anciens templates)
        """
        d = self.to_dict()
        d["anneescolaire"] = self.annee_scolaire_str()
        d["annee_debut"] = str(self.date_debut.year)
        d["annee"] = d["annee_debut"]
        d["annee_fin"] = str(self.date_fin.year)
        if d["annee_fin"] != d["annee_debut"]:
            d["annee"] += "-" + str(d["annee_fin"])
        d["mois_debut_ord"] = self.date_debut.month
        d["mois_fin_ord"] = self.date_fin.month
        # La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
        # devrait sans doute pouvoir etre changé...
        if self.date_debut.month >= 8 and self.date_debut.month <= 10:
            d["periode"] = 1  # typiquement, début en septembre: S1, S3...
        else:
            d["periode"] = 2  # typiquement, début en février: S2, S4...
        d["titre_num"] = self.titre_num()
        d["titreannee"] = self.titre_annee()
        d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}"
        d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}"
        d["titremois"] = "%s %s  (%s - %s)" % (
            d["titre_num"],
            self.modalite or "",
            d["mois_debut"],
            d["mois_fin"],
        )
        d["session_id"] = self.session_id()
        d["etapes"] = self.etapes_apo_vdi()
        d["etapes_apo_str"] = self.etapes_apo_str()
        d["responsables"] = [u.id for u in self.responsables]  # liste des ids
        return d

    def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
        """UE des modules de ce semestre, triées par numéro.
        - Formations classiques: les UEs auxquelles appartiennent
          les modules mis en place dans ce semestre.
        - Formations APC / BUT: les UEs de la formation qui ont
        le même numéro de semestre que ce formsemestre.
        """
        if self.formation.get_parcours().APC_SAE:
            sem_ues = UniteEns.query.filter_by(
                formation=self.formation, semestre_idx=self.semestre_id
            )
        else:
            sem_ues = db.session.query(UniteEns).filter(
                ModuleImpl.formsemestre_id == self.id,
                Module.id == ModuleImpl.module_id,
                UniteEns.id == Module.ue_id,
            )
        if not with_sport:
            sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
        return sem_ues.order_by(UniteEns.numero)

    @cached_property
    def modimpls_sorted(self) -> list[ModuleImpl]:
        """Liste des modimpls du semestre (y compris bonus)
        - triée par type/numéro/code en APC
        - triée par numéros d'UE/matières/modules pour les formations standard.
        """
        modimpls = self.modimpls.all()
        if self.formation.is_apc():
            modimpls.sort(
                key=lambda m: (m.module.module_type, m.module.numero, m.module.code)
            )
        else:
            modimpls.sort(
                key=lambda m: (
                    m.module.ue.numero or 0,
                    m.module.matiere.numero or 0,
                    m.module.numero or 0,
                    m.module.code or "",
                )
            )
        return modimpls

    def can_be_edited_by(self, user):
        """Vrai si user peut modifier ce semestre"""
        if not user.has_permission(Permission.ScoImplement):  # pas chef
            if not self.resp_can_edit or user.id not in [
                resp.id for resp in self.responsables
            ]:
                return False

        return True

    def est_courant(self) -> bool:
        """Vrai si la date actuelle (now) est dans le semestre
        (les dates de début et fin sont incluses)
        """
        today = datetime.date.today()
        return (self.date_debut <= today) and (today <= self.date_fin)

    def contient_periode(self, date_debut, date_fin) -> bool:
        """Vrai si l'intervalle [date_debut, date_fin] est
        inclus dans le semestre.
        (les dates de début et fin sont incluses)
        """
        return (self.date_debut <= date_debut) and (date_fin <= self.date_fin)

    def est_sur_une_annee(self):
        """Test si sem est entièrement sur la même année scolaire.
        (ce n'est pas obligatoire mais si ce n'est pas le
        cas les exports Apogée risquent de mal fonctionner)
        Pivot au 1er août.
        """
        if self.date_debut > self.date_fin:
            log(f"Warning: semestre {self.id} begins after ending !")
        annee_debut = self.date_debut.year
        if self.date_debut.month < 8:  # août
            # considere que debut sur l'anne scolaire precedente
            annee_debut -= 1
        annee_fin = self.date_fin.year
        if self.date_fin.month < 9:
            # 9 (sept) pour autoriser un début en sept et une fin en aout
            annee_fin -= 1
        return annee_debut == annee_fin

    def est_decale(self):
        """Vrai si semestre "décalé"
        c'est à dire semestres impairs commençant entre janvier et juin
        et les pairs entre juillet et decembre
        """
        if self.semestre_id <= 0:
            return False  # formations sans semestres
        return (self.semestre_id % 2 and self.date_debut.month <= 6) or (
            not self.semestre_id % 2 and self.date_debut.month > 6
        )

    def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
        "Liste des vdis"
        # was read_formsemestre_etapes
        return [e.as_apovdi() for e in self.etapes if e.etape_apo]

    def etapes_apo_str(self) -> str:
        """Chaine décrivant les étapes de ce semestre
        ex: "V1RT, V1RT3, V1RT4"
        """
        if not self.etapes:
            return ""
        return ", ".join([str(x.etape_apo) for x in self.etapes])

    def responsables_str(self, abbrev_prenom=True) -> str:
        """chaîne "J. Dupond, X. Martin"
        ou  "Jacques Dupond, Xavier Martin"
        """
        if not self.responsables:
            return ""
        if abbrev_prenom:
            return ", ".join([u.get_prenomnom() for u in self.responsables])
        else:
            return ", ".join([u.get_nomcomplet() for u in self.responsables])

    def annee_scolaire_str(self):
        "2021 - 2022"
        return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)

    def session_id(self) -> str:
        """identifiant externe de semestre de formation
        Exemple:  RT-DUT-FI-S1-ANNEE

        DEPT-TYPE-MODALITE+-S?|SPECIALITE

        TYPE=DUT|LP*|M*
        MODALITE=FC|FI|FA (si plusieurs, en inverse alpha)

        SPECIALITE=[A-Z]+   EON,ASSUR, ... (si pas Sn ou SnD)

        ANNEE=annee universitaire de debut (exemple: un S2 de 2013-2014 sera S2-2013)
        """
        imputation_dept = sco_preferences.get_preference("ImputationDept", self.id)
        if not imputation_dept:
            imputation_dept = sco_preferences.get_preference("DeptName")
        imputation_dept = imputation_dept.upper()
        parcours_name = self.formation.get_parcours().NAME
        modalite = self.modalite
        # exception pour code Apprentissage:
        modalite = (modalite or "").replace("FAP", "FA").replace("APP", "FA")
        if self.semestre_id > 0:
            decale = "D" if self.est_decale() else ""
            semestre_id = f"S{self.semestre_id}{decale}"
        else:
            semestre_id = self.formation.code_specialite or ""
        annee_sco = str(
            scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
        )
        return scu.sanitize_string(
            "-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco))
        )

    def titre_annee(self) -> str:
        """ """
        titre_annee = (
            f"{self.titre_num()} {self.modalite or ''}  {self.date_debut.year}"
        )
        if self.date_fin.year != self.date_debut.year:
            titre_annee += "-" + str(self.date_fin.year)
        return titre_annee

    def titre_mois(self) -> str:
        """Le titre et les dates du semestre, pour affichage dans des listes
        Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
        """
        return f"""{self.titre_num()} {self.modalite or ''} ({
            scu.MONTH_NAMES_ABBREV[self.date_debut.month-1]} {
                self.date_debut.year} - {
            scu.MONTH_NAMES_ABBREV[self.date_fin.month -1]} {
                self.date_fin.year})"""

    def titre_num(self) -> str:
        """Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
        if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
            return self.titre
        return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"

    def get_abs_count(self, etudid):
        """Les comptes d'absences de cet étudiant dans ce semestre:
        tuple (nb abs non justifiées, nb abs justifiées)
        Utilise un cache.
        """
        from app.scodoc import sco_abs

        return sco_abs.get_abs_count_in_interval(
            etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
        )

    def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
        """Liste des étudiants inscrits à ce semestre
        Si include_demdef, tous les étudiants, avec les démissionnaires
        et défaillants.
        Si order, tri par clé sort_key
        """
        if include_demdef:
            etuds = [ins.etud for ins in self.inscriptions]
        else:
            etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
        if order:
            etuds.sort(key=lambda e: e.sort_key)
        return etuds

    @cached_property
    def etudids_actifs(self) -> set:
        "Set des etudids inscrits non démissionnaires"
        return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}

    @cached_property
    def etuds_inscriptions(self) -> dict:
        """Map { etudid : inscription } (incluant DEM et DEF)"""
        return {ins.etud.id: ins for ins in self.inscriptions}


# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table(
    "notes_formsemestre_responsables",
    db.Column(
        "formsemestre_id",
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
    ),
    db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")),
)


class FormSemestreEtape(db.Model):
    """Étape Apogée associées au semestre"""

    __tablename__ = "notes_formsemestre_etapes"
    id = db.Column(db.Integer, primary_key=True)
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
    )
    etape_apo = db.Column(db.String(APO_CODE_STR_LEN), index=True)

    def __repr__(self):
        return f"<Etape {self.id} apo={self.etape_apo}>"

    def as_apovdi(self):
        return ApoEtapeVDI(self.etape_apo)


class FormationModalite(db.Model):
    """Modalités de formation, utilisées pour la présentation
    (grouper les semestres, générer des codes, etc.)
    """

    __tablename__ = "notes_form_modalites"

    DEFAULT_MODALITE = "FI"

    id = db.Column(db.Integer, primary_key=True)
    modalite = db.Column(
        db.String(SHORT_STR_LEN),
        unique=True,
        index=True,
        default=DEFAULT_MODALITE,
        server_default=DEFAULT_MODALITE,
    )  # code
    titre = db.Column(db.Text())  # texte explicatif
    # numero = ordre de presentation)
    numero = db.Column(db.Integer)

    @staticmethod
    def insert_modalites():
        """Create default modalities"""
        numero = 0
        try:
            for (code, titre) in (
                (FormationModalite.DEFAULT_MODALITE, "Formation Initiale"),
                ("FAP", "Apprentissage"),
                ("FC", "Formation Continue"),
                ("DEC", "Formation Décalées"),
                ("LIC", "Licence"),
                ("CPRO", "Contrats de Professionnalisation"),
                ("DIST", "À distance"),
                ("ETR", "À l'étranger"),
                ("EXT", "Extérieur"),
                ("OTHER", "Autres formations"),
            ):
                modalite = FormationModalite.query.filter_by(modalite=code).first()
                if modalite is None:
                    modalite = FormationModalite(
                        modalite=code, titre=titre, numero=numero
                    )
                    db.session.add(modalite)
                    numero += 1
            db.session.commit()
        except:
            db.session.rollback()
            raise


class FormSemestreUECoef(db.Model):
    """Coef des UE capitalisees arrivant dans ce semestre"""

    __tablename__ = "notes_formsemestre_uecoef"
    __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),)

    id = db.Column(db.Integer, primary_key=True)
    formsemestre_uecoef_id = db.synonym("id")
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
        index=True,
    )
    ue_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_ue.id"),
        index=True,
    )
    coefficient = db.Column(db.Float, nullable=False)


class FormSemestreUEComputationExpr(db.Model):
    """Formules utilisateurs pour calcul moyenne UE (désactivées en 9.2+)."""

    __tablename__ = "notes_formsemestre_ue_computation_expr"
    __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),)

    id = db.Column(db.Integer, primary_key=True)
    notes_formsemestre_ue_computation_expr_id = db.synonym("id")
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
    )
    ue_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_ue.id"),
    )
    # formule de calcul moyenne
    computation_expr = db.Column(db.Text())


class FormSemestreCustomMenu(db.Model):
    """Menu custom associe au semestre"""

    __tablename__ = "notes_formsemestre_custommenu"

    id = db.Column(db.Integer, primary_key=True)
    custommenu_id = db.synonym("id")
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
    )
    title = db.Column(db.Text())
    url = db.Column(db.Text())
    idx = db.Column(db.Integer, default=0, server_default="0")  #  rang dans le menu


class FormSemestreInscription(db.Model):
    """Inscription à un semestre de formation"""

    __tablename__ = "notes_formsemestre_inscription"
    __table_args__ = (db.UniqueConstraint("formsemestre_id", "etudid"),)

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

    etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
    formsemestre_id = db.Column(
        db.Integer,
        db.ForeignKey("notes_formsemestre.id"),
        index=True,
    )
    etud = db.relationship(
        Identite,
        backref=db.backref("formsemestre_inscriptions", cascade="all, delete-orphan"),
    )
    formsemestre = db.relationship(
        FormSemestre,
        backref=db.backref(
            "inscriptions",
            cascade="all, delete-orphan",
            order_by="FormSemestreInscription.etudid",
        ),
    )
    # I inscrit, D demission en cours de semestre, DEF si "defaillant"
    etat = db.Column(db.String(CODE_STR_LEN), index=True)
    # etape apogee d'inscription (experimental 2020)
    etape = db.Column(db.String(APO_CODE_STR_LEN))


class NotesSemSet(db.Model):
    """semsets: ensemble de formsemestres pour exports Apogée"""

    __tablename__ = "notes_semset"

    id = db.Column(db.Integer, primary_key=True)
    semset_id = db.synonym("id")
    dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"))

    title = db.Column(db.Text)
    annee_scolaire = db.Column(db.Integer, nullable=True, default=None)
    # periode: 0 (année), 1 (Simpair), 2 (Spair)
    sem_id = db.Column(db.Integer, nullable=True, default=None)


# Association: many to many
notes_semset_formsemestre = db.Table(
    "notes_semset_formsemestre",
    db.Column("formsemestre_id", db.Integer, db.ForeignKey("notes_formsemestre.id")),
    db.Column(
        "semset_id",
        db.Integer,
        db.ForeignKey("notes_semset.id", ondelete="CASCADE"),
        nullable=False,
    ),
    db.UniqueConstraint("formsemestre_id", "semset_id"),
)