# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

"""Operations de base sur les formsemestres
"""
import datetime
import time
from operator import itemgetter

from flask import g, request, url_for

import app
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.models import Departement
from app.models import Formation, FormSemestre
from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences
from app.scodoc.gen_tables import GenTable
from app.scodoc.codes_cursus import NO_SEMESTRE_ID
from app.scodoc.sco_exceptions import ScoInvalidIdType, ScoValueError
from app.scodoc.sco_vdi import ApoEtapeVDI

_formsemestreEditor = ndb.EditableTable(
    "notes_formsemestre",
    "formsemestre_id",
    (
        "formsemestre_id",
        "semestre_id",
        "formation_id",
        "titre",
        "date_debut",
        "date_fin",
        "gestion_compensation",
        "gestion_semestrielle",
        "etat",
        "bul_hide_xml",
        "block_moyennes",
        "block_moyenne_generale",
        "bul_bgcolor",
        "modalite",
        "resp_can_edit",
        "resp_can_change_ens",
        "ens_can_edit_eval",
        "elt_sem_apo",
        "elt_annee_apo",
        "edt_id",
    ),
    filter_dept=True,
    sortkey="date_debut",
    output_formators={
        "date_debut": ndb.DateISOtoDMY,
        "date_fin": ndb.DateISOtoDMY,
    },
    input_formators={
        "date_debut": ndb.DateDMYtoISO,
        "date_fin": ndb.DateDMYtoISO,
        "etat": bool,
        "bul_hide_xml": bool,
        "block_moyennes": bool,
        "block_moyenne_generale": bool,
        "gestion_compensation": bool,
        "gestion_semestrielle": bool,
        "resp_can_edit": bool,
        "resp_can_change_ens": bool,
        "ens_can_edit_eval": bool,
        "bul_bgcolor": lambda color: color or "white",
        "titre": lambda titre: titre or "sans titre",
    },
)


def get_formsemestre(formsemestre_id: int) -> dict:
    "list ONE formsemestre"
    if formsemestre_id is None:
        raise ValueError("get_formsemestre: id manquant")
    if formsemestre_id in g.stored_get_formsemestre:
        return g.stored_get_formsemestre[formsemestre_id]
    if not isinstance(formsemestre_id, int):
        log(f"get_formsemestre: invalid id '{formsemestre_id}'")
        raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !")
    sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
    if not sems:
        log(f"get_formsemestre: invalid formsemestre_id ({formsemestre_id})")
        raise ScoValueError(f"semestre {formsemestre_id} inconnu !")

    g.stored_get_formsemestre[formsemestre_id] = sems[0]
    return sems[0]


def do_formsemestre_list(*a, **kw):
    "list formsemestres"
    # log('do_formsemestre_list: a=%s kw=%s' % (str(a),str(kw)))
    cnx = ndb.GetDBConnexion()

    sems = _formsemestreEditor.list(cnx, *a, **kw)

    # Ajoute les étapes Apogee et les responsables:
    for sem in sems:
        sem["etapes"] = read_formsemestre_etapes(sem["formsemestre_id"])
        sem["responsables"] = read_formsemestre_responsables(sem["formsemestre_id"])

    # Filtre sur code etape si indiqué:
    if "args" in kw:
        etape = kw["args"].get("etape_apo", None)
        if etape:
            sems = [sem for sem in sems if etape in sem["etapes"]]

    for sem in sems:
        _formsemestre_enrich(sem)

    # tri par date, le plus récent d'abord
    sems.sort(key=itemgetter("dateord", "semestre_id"), reverse=True)

    return sems


def _formsemestre_enrich(sem):
    """Ajoute champs souvent utiles: titre + annee et dateord (pour tris).
    XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre.
    """
    # imports ici pour eviter refs circulaires
    from app.scodoc import sco_formsemestre_edit

    formation: Formation = Formation.query.get_or_404(sem["formation_id"])
    parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
    # 'S1', 'S2', ... ou '' pour les monosemestres
    if sem["semestre_id"] != NO_SEMESTRE_ID:
        sem["sem_id_txt"] = "S%s" % sem["semestre_id"]
    else:
        sem["sem_id_txt"] = ""
    # Nom avec numero semestre:
    sem["titre_num"] = sem["titre"]  # eg "DUT Informatique"
    if sem["semestre_id"] != NO_SEMESTRE_ID:
        sem["titre_num"] += " %s %s" % (
            parcours.SESSION_NAME,
            sem["semestre_id"],
        )  # eg "DUT Informatique semestre 2"

    sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"])
    sem["date_fin_iso"] = ndb.DateDMYtoISO(sem["date_fin"])
    sem["dateord"] = sem["date_debut_iso"]  # pour les tris
    try:
        mois_debut, annee_debut = sem["date_debut"].split("/")[1:]
    except:
        mois_debut, annee_debut = "", ""
    try:
        mois_fin, annee_fin = sem["date_fin"].split("/")[1:]
    except:
        mois_fin, annee_fin = "", ""
    sem["annee_debut"] = annee_debut
    sem["annee_fin"] = annee_fin
    sem["mois_debut_ord"] = int(mois_debut)
    sem["mois_fin_ord"] = int(mois_fin)

    sem["annee"] = annee_debut
    # 2007 ou 2007-2008:
    sem["anneescolaire"] = scu.annee_scolaire_repr(
        int(annee_debut), sem["mois_debut_ord"]
    )
    # La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
    # devrait sans doute pouvoir etre changé...
    if sem["mois_debut_ord"] >= 8 and sem["mois_debut_ord"] <= 10:
        sem["periode"] = 1  # typiquement, début en septembre: S1, S3...
    else:
        sem["periode"] = 2  # typiquement, début en février: S2, S4...

    sem["titreannee"] = "%s %s  %s" % (
        sem["titre_num"],
        sem.get("modalite", ""),
        annee_debut,
    )
    if annee_fin != annee_debut:
        sem["titreannee"] += "-" + annee_fin
        sem["annee"] += "-" + annee_fin
    # et les dates sous la forme "oct 2007 - fev 2008"
    months = scu.MONTH_NAMES_ABBREV
    if mois_debut:
        mois_debut = months[int(mois_debut) - 1]
    if mois_fin:
        mois_fin = months[int(mois_fin) - 1]
    sem["mois_debut"] = mois_debut + " " + annee_debut
    sem["mois_fin"] = mois_fin + " " + annee_fin
    sem["titremois"] = "%s %s  (%s - %s)" % (
        sem["titre_num"],
        sem.get("modalite", ""),
        sem["mois_debut"],
        sem["mois_fin"],
    )
    sem["session_id"] = sco_formsemestre_edit.get_formsemestre_session_id(
        sem, formation.code_specialite, parcours
    )
    sem["etapes"] = read_formsemestre_etapes(sem["formsemestre_id"])
    sem["etapes_apo_str"] = formsemestre_etape_apo_str(sem)
    sem["responsables"] = read_formsemestre_responsables(sem["formsemestre_id"])


def formsemestre_etape_apo_str(sem):
    "chaine décrivant le(s) codes étapes Apogée"
    return etapes_apo_str(sem["etapes"])


def etapes_apo_str(etapes):
    "Chaine decrivant une liste d'instance de ApoEtapeVDI"
    return ", ".join([str(x) for x in etapes])


def do_formsemestre_create(args, silent=False):
    "create a formsemestre"
    from app.models import ScolarNews
    from app.scodoc import sco_groups

    cnx = ndb.GetDBConnexion()
    formsemestre_id = _formsemestreEditor.create(cnx, args)
    if args["etapes"]:
        args["formsemestre_id"] = formsemestre_id
        write_formsemestre_etapes(args)
    if args["responsables"]:
        args["formsemestre_id"] = formsemestre_id
        _write_formsemestre_responsables(args)

    # create default partition
    partition_id = sco_groups.partition_create(
        formsemestre_id,
        default=True,
        redirect=0,
        numero=1000000,  # à la fin
    )
    _ = sco_groups.create_group(partition_id, default=True)

    # news
    if "titre" not in args:
        args["titre"] = "sans titre"
    args["formsemestre_id"] = formsemestre_id
    args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
    if not silent:
        ScolarNews.add(
            typ=ScolarNews.NEWS_SEM,
            text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
            url=args["url"],
            max_frequency=0,
        )
    return formsemestre_id


def do_formsemestre_edit(sem, cnx=None, **kw):
    """Apply modifications to formsemestre.
    Update etapes and resps. Invalidate cache."""
    if not cnx:
        cnx = ndb.GetDBConnexion()

    _formsemestreEditor.edit(cnx, sem, **kw)
    write_formsemestre_etapes(sem)
    _write_formsemestre_responsables(sem)

    sco_cache.invalidate_formsemestre(
        formsemestre_id=sem["formsemestre_id"]
    )  # > modif formsemestre


def read_formsemestre_responsables(formsemestre_id: int) -> list[int]:  # py3.9+ syntax
    """recupere liste des responsables de ce semestre
    :returns: liste d'id
    """
    r = ndb.SimpleDictFetch(
        """SELECT responsable_id
        FROM notes_formsemestre_responsables
        WHERE formsemestre_id = %(formsemestre_id)s
        """,
        {"formsemestre_id": formsemestre_id},
    )
    return [x["responsable_id"] for x in r]


def _write_formsemestre_responsables(sem):  # TODO old, à ré-écrire avec models
    if sem and "responsables" in sem:
        sem["responsables"] = [
            uid for uid in sem["responsables"] if (uid is not None) and (uid != -1)
        ]
        _write_formsemestre_aux(sem, "responsables", "responsable_id")


# ----------------------  Coefs des UE

_formsemestre_uecoef_editor = ndb.EditableTable(
    "notes_formsemestre_uecoef",
    "formsemestre_uecoef_id",
    ("formsemestre_uecoef_id", "formsemestre_id", "ue_id", "coefficient"),
)

formsemestre_uecoef_create = _formsemestre_uecoef_editor.create
formsemestre_uecoef_edit = _formsemestre_uecoef_editor.edit
formsemestre_uecoef_list = _formsemestre_uecoef_editor.list
formsemestre_uecoef_delete = _formsemestre_uecoef_editor.delete


def do_formsemestre_uecoef_edit_or_create(cnx, formsemestre_id, ue_id, coef):
    "modify or create the coef"
    coefs = formsemestre_uecoef_list(
        cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id}
    )
    if coefs:
        formsemestre_uecoef_edit(
            cnx,
            args={
                "formsemestre_uecoef_id": coefs[0]["formsemestre_uecoef_id"],
                "coefficient": coef,
            },
        )
    else:
        formsemestre_uecoef_create(
            cnx,
            args={
                "formsemestre_id": formsemestre_id,
                "ue_id": ue_id,
                "coefficient": coef,
            },
        )


def do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id):
    "delete coef for this (ue,sem)"
    coefs = formsemestre_uecoef_list(
        cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id}
    )
    if coefs:
        formsemestre_uecoef_delete(cnx, coefs[0]["formsemestre_uecoef_id"])


def read_formsemestre_etapes(formsemestre_id):  # OBSOLETE
    """recupere liste des codes etapes associés à ce semestre
    :returns: liste d'instance de ApoEtapeVDI
    """
    r = ndb.SimpleDictFetch(
        """SELECT etape_apo
        FROM notes_formsemestre_etapes
        WHERE formsemestre_id = %(formsemestre_id)s
        ORDER BY etape_apo
        """,
        {"formsemestre_id": formsemestre_id},
    )
    return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]


def write_formsemestre_etapes(sem):  # TODO old, à ré-écrire avec models
    return _write_formsemestre_aux(sem, "etapes", "etape_apo")


# TODO old, à ré-écrire avec models
def _write_formsemestre_aux(sem, fieldname, valuename):
    """fieldname: 'etapes' ou 'responsables'
    valuename: 'etape_apo' ou 'responsable_id'
    """
    if not fieldname in sem:
        return
    # uniquify
    values = set([str(x) for x in sem[fieldname]])

    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    tablename = "notes_formsemestre_" + fieldname
    try:
        cursor.execute(
            "DELETE from " + tablename + " where formsemestre_id = %(formsemestre_id)s",
            {"formsemestre_id": sem["formsemestre_id"]},
        )
        for item in values:
            if item:
                cursor.execute(
                    "INSERT INTO "
                    + tablename
                    + " (formsemestre_id, "
                    + valuename
                    + ") VALUES (%(formsemestre_id)s, %("
                    + valuename
                    + ")s)",
                    {"formsemestre_id": sem["formsemestre_id"], valuename: item},
                )
    except:
        log("Warning: exception in write_formsemestre_aux !")
        cnx.rollback()
        raise
    cnx.commit()


def sem_set_responsable_name(sem):
    "ajoute champs responsable_name"
    from app.scodoc import sco_users

    sem["responsable_name"] = ", ".join(
        [
            sco_users.user_info(responsable_id)["nomprenom"]
            for responsable_id in sem["responsables"]
        ]
    )


def sem_in_semestre_scolaire(
    sem,
    year=False,
    periode=None,
    mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
    mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
) -> bool:
    """Vrai si la date du début du semestre est dans la période indiquée (1,2,0)
    du semestre `periode` de l'année scolaire indiquée
    (ou, à défaut, de celle en cours).

    La période utilise les même conventions que semset["sem_id"];
    * 1 : première période
    * 2 : deuxième période
    * 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
    )
    """
    if not year:
        year = scu.annee_scolaire()
    # n'utilise pas le jour pivot
    jour_pivot_annee = jour_pivot_periode = 1
    # calcule l'année universitaire et la période
    sem_annee, sem_periode = FormSemestre.comp_periode(
        datetime.datetime.fromisoformat(sem["date_debut_iso"]),
        mois_pivot_annee,
        mois_pivot_periode,
        jour_pivot_annee,
        jour_pivot_periode,
    )
    if periode is None or periode == 0:
        return sem_annee == year
    return sem_annee == year and sem_periode == periode


def sem_in_annee_scolaire(sem, year=False):
    """Test si sem appartient à l'année scolaire year (int).
    N'utilise que la date de début, pivot au 1er août.
    Si année non specifiée, année scolaire courante
    """
    return sem_in_semestre_scolaire(sem, year, periode=0)


def sem_est_courant(sem):  # -> FormSemestre.est_courant
    """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
    now = time.strftime("%Y-%m-%d")
    debut = ndb.DateDMYtoISO(sem["date_debut"])
    fin = ndb.DateDMYtoISO(sem["date_fin"])
    return debut <= now <= fin


def scodoc_get_all_unlocked_sems():
    """Liste de tous les semestres non verrouillés de _tous_ les départements
    (utilisé pour rapports d'activités)
    """
    cur_dept = g.scodoc_dept
    depts = Departement.query.filter_by(visible=True).all()
    semdepts = []
    try:
        for dept in depts:
            app.set_sco_dept(dept.acronym)
            semdepts += [(sem, dept) for sem in do_formsemestre_list() if sem["etat"]]
    finally:
        app.set_sco_dept(cur_dept)
    return semdepts


def table_formsemestres(
    sems: list[dict],
    columns_ids=(),
    sup_columns_ids=(),
    html_title="<h2>Semestres</h2>",
    html_next_section="",
):
    """Une table presentant des semestres"""
    for sem in sems:
        sem_set_responsable_name(sem)
        sem["_titre_num_target"] = url_for(
            "notes.formsemestre_status",
            scodoc_dept=g.scodoc_dept,
            formsemestre_id=sem["formsemestre_id"],
        )

    if not columns_ids:
        columns_ids = (
            "etat",
            "modalite",
            "mois_debut",
            "mois_fin",
            "titre_num",
            "responsable_name",
            "etapes_apo_str",
        )
    columns_ids += sup_columns_ids

    titles = {
        "modalite": "",
        "mois_debut": "Début",
        "mois_fin": "Fin",
        "titre_num": "Semestre",
        "responsable_name": "Resp.",
        "etapes_apo_str": "Apo.",
    }
    if sems:
        preferences = sco_preferences.SemPreferences(sems[0]["formsemestre_id"])
    else:
        preferences = sco_preferences.SemPreferences()
    tab = GenTable(
        columns_ids=columns_ids,
        rows=sems,
        titles=titles,
        html_class="table_leftalign",
        html_sortable=True,
        html_title=html_title,
        html_next_section=html_next_section,
        html_empty_element="<p><em>aucun résultat</em></p>",
        page_title="Semestres",
        preferences=preferences,
    )
    return tab


def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False) -> list[dict]:
    """Liste des semestres de cette etape,
    pour l'annee scolaire indiquée (sinon, pour toutes).
    """
    ds = {}  # formsemestre_id : sem
    if etape_apo:
        sems = do_formsemestre_list(args={"etape_apo": etape_apo})
        for sem in sems:
            if annee_scolaire:  # restriction annee scolaire
                if sem_in_annee_scolaire(sem, year=int(annee_scolaire)):
                    ds[sem["formsemestre_id"]] = sem
        sems = list(ds.values())
    else:
        sems = do_formsemestre_list()
        if annee_scolaire:
            sems = [
                sem
                for sem in sems
                if sem_in_annee_scolaire(sem, year=int(annee_scolaire))
            ]

    sems.sort(key=lambda s: (s["modalite"], s["dateord"]))
    return sems


def view_formsemestre_by_etape(etape_apo=None, fmt="html"):
    """Affiche table des semestres correspondants à l'étape"""
    if etape_apo:
        html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>"""
    else:
        html_title = """<h2>Semestres courants</h2>"""
    tab = table_formsemestres(
        list_formsemestre_by_etape(
            etape_apo=etape_apo, annee_scolaire=scu.annee_scolaire()
        ),
        html_title=html_title,
        html_next_section="""<form action="view_formsemestre_by_etape">
    Etape: <input name="etape_apo" type="text" size="8"></input>
        </form>""",
    )
    tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")
    return tab.make_page(fmt=fmt)


def sem_has_etape(sem, code_etape):
    return code_etape in sem["etapes"]
