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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################

"""Ajout/Modification/Suppression UE

"""
import flask
from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user

from app import db
from app import log
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models import ScolarNews
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
    ScoValueError,
    ScoLockedFormError,
    ScoNonEmptyFormationObject,
)

from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_tag_module

_ueEditor = ndb.EditableTable(
    "notes_ue",
    "ue_id",
    (
        "ue_id",
        "formation_id",
        "acronyme",
        "numero",
        "titre",
        "semestre_idx",
        "type",
        "ue_code",
        "ects",
        "is_external",
        "code_apogee",
        "coefficient",
        "color",
    ),
    sortkey="numero",
    input_formators={
        "type": ndb.int_null_is_zero,
        "is_external": ndb.bool_or_str,
        "ects": ndb.float_null_is_null,
    },
    output_formators={
        "numero": ndb.int_null_is_zero,
        "ects": ndb.float_null_is_null,
        "coefficient": ndb.float_null_is_zero,
        "semestre_idx": ndb.int_null_is_null,
    },
)


def ue_list(*args, **kw):
    "list UEs"
    cnx = ndb.GetDBConnexion()
    return _ueEditor.list(cnx, *args, **kw)


def do_ue_create(args):
    "create an ue"
    cnx = ndb.GetDBConnexion()
    # check duplicates
    ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
    if ues:
        raise ScoValueError(
            f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! 
                (chaque UE doit avoir un acronyme unique dans la formation)"""
        )
    if not "ue_code" in args:
        # évite les conflits de code
        while True:
            cursor = db.session.execute("select notes_newid_ucod();")
            code = cursor.fetchone()[0]
            if UniteEns.query.filter_by(ue_code=code).count() == 0:
                break
        args["ue_code"] = code
    # create
    ue_id = _ueEditor.create(cnx, args)

    # Invalidate cache: vire les poids de toutes les évals de la formation
    for modimpl in ModuleImpl.query.filter(
        ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"]
    ):
        modimpl.invalidate_evaluations_poids()
    formation = Formation.query.get(args["formation_id"])
    formation.invalidate_module_coefs()
    # news
    ue = UniteEns.query.get(ue_id)
    flash(f"UE créée (code {ue.ue_code})")
    formation = Formation.query.get(args["formation_id"])
    ScolarNews.add(
        typ=ScolarNews.NEWS_FORM,
        obj=args["formation_id"],
        text=f"Modification de la formation {formation.acronyme}",
        max_frequency=10 * 60,
    )
    formation.invalidate_cached_sems()
    return ue_id


def do_ue_delete(ue_id, delete_validations=False, force=False):
    "delete UE and attached matieres (but not modules)"
    from app.scodoc import sco_formations
    from app.scodoc import sco_parcours_dut

    ue = UniteEns.query.get_or_404(ue_id)
    formation_id = ue.formation_id
    semestre_idx = ue.semestre_idx
    if not ue.can_be_deleted():
        raise ScoNonEmptyFormationObject(
            f"UE (id={ue.id}, dud)",
            msg=ue.titre,
            dest_url=url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=semestre_idx,
            ),
        )

    cnx = ndb.GetDBConnexion()
    log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
    # check
    # if ue_is_locked(ue.id):
    #    raise ScoLockedFormError()
    # Il y a-t-il des etudiants ayant validé cette UE ?
    # si oui, propose de supprimer les validations
    validations = sco_parcours_dut.scolar_formsemestre_validation_list(
        cnx, args={"ue_id": ue.id}
    )
    if validations and not delete_validations and not force:
        return scu.confirm_dialog(
            "<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
            % (len(validations), ue.acronyme, ue.titre),
            dest_url="",
            target_variable="delete_validations",
            cancel_url=url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=semestre_idx,
            ),
            parameters={"ue_id": ue.id, "dialog_confirmed": 1},
        )
    if delete_validations:
        log(f"deleting all validations of UE {ue.id}")
        ndb.SimpleQuery(
            "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
            {"ue_id": ue.id},
        )

    # delete all matiere in this UE
    mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
    for mat in mats:
        sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
    # delete uecoef and events
    ndb.SimpleQuery(
        "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
        {"ue_id": ue.id},
    )
    ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id})
    cnx = ndb.GetDBConnexion()
    _ueEditor.delete(cnx, ue.id)
    # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
    # utilisé: acceptable de tout invalider):
    sco_cache.invalidate_formsemestre()
    # news
    F = sco_formations.formation_list(args={"formation_id": formation_id})[0]
    ScolarNews.add(
        typ=ScolarNews.NEWS_FORM,
        obj=formation_id,
        text=f"Modification de la formation {F['acronyme']}",
        max_frequency=10 * 60,
    )
    #
    if not force:
        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=semestre_idx,
            )
        )
    return None


def ue_create(formation_id=None, default_semestre_idx=None):
    """Formulaire création d'une UE"""
    return ue_edit(
        create=True,
        formation_id=formation_id,
        default_semestre_idx=default_semestre_idx,
    )


def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=None):
    """Formulaire modification ou création d'une UE"""
    create = int(create)
    if not create:
        ue: UniteEns = UniteEns.query.get_or_404(ue_id)
        ue_dict = ue.to_dict()
        formation_id = ue.formation_id
        title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
        initvalues = ue_dict
        submitlabel = "Modifier les valeurs"
        can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None)
    else:
        ue = None
        title = "Création d'une UE"
        initvalues = {
            "semestre_idx": default_semestre_idx,
            "color": ue_guess_color_default(formation_id, default_semestre_idx),
        }
        submitlabel = "Créer cette UE"
        can_change_semestre_id = True
    formation = Formation.query.get(formation_id)
    if not formation:
        raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
    parcours = formation.get_parcours()
    is_apc = parcours.APC_SAE
    semestres_indices = list(range(1, parcours.NB_SEM + 1))
    H = [
        html_sco_header.sco_header(page_title=title, javascripts=["js/edit_ue.js"]),
        "<h2>" + title,
        f" (formation {formation.acronyme}, version {formation.version})</h2>",
        """
    <p class="help">Les UE sont des groupes de modules dans une formation donnée,
    utilisés pour la validation (on calcule des moyennes par UE et applique des
    seuils ("barres")).
    </p>

    <p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
    Seuls les <em>modules</em> ont des coefficients.
    </p>""",
    ]

    ue_types = parcours.ALLOWED_UE_TYPES
    ue_types.sort()
    ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
    ue_types = [str(x) for x in ue_types]

    form_descr = [
        ("ue_id", {"input_type": "hidden"}),
        ("create", {"input_type": "hidden", "default": create}),
        ("formation_id", {"input_type": "hidden", "default": formation_id}),
        ("titre", {"size": 30, "explanation": "nom de l'UE"}),
        ("acronyme", {"size": 8, "explanation": "abbréviation", "allow_null": False}),
        (
            "numero",
            {
                "size": 4,
                "explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
                "type": "int",
            },
        ),
    ]
    if can_change_semestre_id:
        form_descr += [
            (
                "semestre_idx",
                {
                    "input_type": "menu",
                    "type": "int",
                    "allow_null": False,
                    "title": parcours.SESSION_NAME.capitalize(),
                    "explanation": "%s de l'UE dans la formation"
                    % parcours.SESSION_NAME,
                    "labels": ["non spécifié"] + [str(x) for x in semestres_indices],
                    "allowed_values": [""] + semestres_indices,
                },
            ),
        ]
    else:
        form_descr += [
            ("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}),
        ]
    form_descr += [
        (
            "type",
            {
                "explanation": "type d'UE",
                "input_type": "menu",
                "allowed_values": ue_types,
                "labels": ue_types_names,
            },
        ),
        (
            "ects",
            {
                "size": 4,
                "type": "float",
                "title": "ECTS",
                "explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)",
                "allow_null": not is_apc,  # ects requis en APC
            },
        ),
        (
            "coefficient",
            {
                "size": 4,
                "type": "float",
                "title": "Coefficient",
                "explanation": """les coefficients d'UE ne sont utilisés que
                    lorsque l'option <em>Utiliser les coefficients d'UE pour calculer
                    la moyenne générale</em> est activée. Par défaut, le coefficient
                    d'une UE est simplement la somme des coefficients des modules dans
                    lesquels l'étudiant a des notes.
                    Jamais utilisé en BUT.
                    """,
                "enabled": not is_apc,
            },
        ),
        (
            "ue_code",
            {
                "size": 12,
                "title": "Code UE",
                "max_length": SHORT_STR_LEN,
                "explanation": """code interne (non vide). Toutes les UE partageant le même code 
                (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). 
                Voir liste ci-dessous.""",
            },
        ),
        (
            "code_apogee",
            {
                "title": "Code Apogée",
                "size": 25,
                "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
                "max_length": APO_CODE_STR_LEN,
            },
        ),
        (
            "is_external",
            {
                "input_type": "boolcheckbox",
                "title": "UE externe",
                "explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
            },
        ),
        (
            "color",
            {
                "input_type": "color",
                "title": "Couleur",
                "explanation": "pour affichages",
            },
        ),
    ]
    if create and not parcours.UE_IS_MODULE and not is_apc:
        form_descr.append(
            (
                "create_matiere",
                {
                    "input_type": "boolcheckbox",
                    "default": True,
                    "title": "Créer matière identique",
                    "explanation": "créer immédiatement une matière dans cette UE (utile si on n'utilise pas de matières)",
                },
            )
        )
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        form_descr,
        initvalues=initvalues,
        submitlabel=submitlabel,
    )
    if tf[0] == 0:
        if ue and ue.modules.count() and ue.semestre_idx is not None:
            modules_div = f"""<div id="ue_list_modules">
            <div><b>{ue.modules.count()} modules sont rattachés
            à cette UE</b> du semestre S{ue.semestre_idx},
            elle ne peut donc pas être changée de semestre.</div>
            <ul>"""
            for m in ue.modules:
                modules_div += f"""<li><a class="stdlink" href="{url_for(
                    "notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre}</a></li>"""
            modules_div += """</ul></div>"""
        else:
            modules_div = ""
        bonus_div = """<div id="bonus_description"></div>"""
        ue_div = """<div id="ue_list_code"></div>"""
        return (
            "\n".join(H)
            + tf[1]
            + modules_div
            + bonus_div
            + ue_div
            + html_sco_header.sco_footer()
        )
    else:
        if create:
            if not tf[2]["ue_code"]:
                del tf[2]["ue_code"]
            if not tf[2]["numero"]:
                # numero regroupant par semestre ou année:
                tf[2]["numero"] = next_ue_numero(
                    formation_id, int(tf[2]["semestre_idx"])
                )
            ue_id = do_ue_create(tf[2])
            if is_apc or parcours.UE_IS_MODULE or tf[2]["create_matiere"]:
                # rappel: en APC, toutes les UE ont une matière, créée ici
                # (inutilisée mais à laquelle les modules sont rattachés)
                matiere_id = sco_edit_matiere.do_matiere_create(
                    {"ue_id": ue_id, "titre": tf[2]["titre"], "numero": 1},
                )
            if parcours.UE_IS_MODULE:
                # dans ce mode, crée un (unique) module dans l'UE:
                _ = sco_edit_module.do_module_create(
                    {
                        "titre": tf[2]["titre"],
                        "code": tf[2]["acronyme"],
                        "coefficient": 1.0,  # tous les modules auront coef 1, et on utilisera les ECTS
                        "ue_id": ue_id,
                        "matiere_id": matiere_id,
                        "formation_id": formation_id,
                        "semestre_id": tf[2]["semestre_idx"],
                    },
                )
            flash("UE créée")
        else:
            do_ue_edit(tf[2])
            flash("UE modifiée")
        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation_id,
                semestre_idx=tf[2]["semestre_idx"],
            )
        )


def _add_ue_semestre_id(ues: list[dict], is_apc):
    """ajoute semestre_id dans les ue, en regardant
    semestre_idx ou à défaut, pour les formations non APC, le premier module
    de chacune.
    Les UE sans modules se voient attribuer  le numero UE_SEM_DEFAULT (1000000),
    qui les place à la fin de la liste.
    """
    for ue in ues:
        if ue["semestre_idx"] is not None:
            ue["semestre_id"] = ue["semestre_idx"]
        elif is_apc:
            ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
        else:
            # était le comportement ScoDoc7
            modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
            if modules:
                ue["semestre_id"] = modules[0]["semestre_id"]
            else:
                ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT


def next_ue_numero(formation_id, semestre_id=None):
    """Numero d'une nouvelle UE dans cette formation.
    Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
    """
    formation = Formation.query.get(formation_id)
    ues = ue_list(args={"formation_id": formation_id})
    if not ues:
        return 0
    if semestre_id is None:
        return ues[-1]["numero"] + 1000
    else:
        # Avec semestre: (prend le semestre du 1er module de l'UE)
        _add_ue_semestre_id(ues, formation.get_parcours().APC_SAE)
        ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
        if ue_list_semestre:
            return ue_list_semestre[-1]["numero"] + 10
        else:
            return ues[-1]["numero"] + 1000


def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
    """Delete an UE"""
    ue = UniteEns.query.get_or_404(ue_id)
    if ue.modules.all():
        raise ScoValueError(
            f"""Suppression de l'UE {ue.titre} impossible car
        des modules (ou SAÉ ou ressources) lui sont rattachés.""",
            dest_url=url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=ue.formation.id,
                semestre_idx=ue.semestre_idx,
            ),
        )
    if not ue.can_be_deleted():
        raise ScoNonEmptyFormationObject(
            f"UE",
            msg=ue.titre,
            dest_url=url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=ue.formation_id,
                semestre_idx=ue.semestre_idx,
            ),
        )

    if not dialog_confirmed:
        return scu.confirm_dialog(
            f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
            dest_url="",
            parameters={"ue_id": ue.id},
            cancel_url=url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=ue.formation_id,
                semestre_idx=ue.semestre_idx,
            ),
        )

    return do_ue_delete(ue.id, delete_validations=delete_validations)


def ue_table(formation_id=None, semestre_idx=1, msg=""):  # was ue_list
    """Liste des matières et modules d'une formation, avec liens pour
    éditer (si non verrouillée).
    """
    from app.scodoc import sco_formsemestre_validation

    formation: Formation = Formation.query.get(formation_id)
    if not formation:
        raise ScoValueError("invalid formation_id")
    parcours = formation.get_parcours()
    is_apc = parcours.APC_SAE
    locked = formation.has_locked_sems()
    if semestre_idx == "all":
        semestre_idx = None
    else:
        semestre_idx = int(semestre_idx)
    semestre_ids = range(1, parcours.NB_SEM + 1)
    # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
    # basées sur des dicts
    ues_obj = UniteEns.query.filter_by(
        formation_id=formation_id, is_external=False
    ).order_by(UniteEns.semestre_idx, UniteEns.numero)
    ues_externes_obj = UniteEns.query.filter_by(
        formation_id=formation_id, is_external=True
    )
    if is_apc:
        # Pour faciliter la transition des anciens programmes non APC
        for ue in ues_obj:
            ue.guess_semestre_idx()
            # vérifie qu'on a bien au moins une matière dans chaque UE
            if ue.matieres.count() < 1:
                mat = Matiere(ue_id=ue.id)
                db.session.add(mat)
        # donne des couleurs aux UEs crées avant
        colorie_anciennes_ues(ues_obj)
        db.session.commit()
    ues = [ue.to_dict() for ue in ues_obj]
    ues_externes = [ue.to_dict() for ue in ues_externes_obj]

    # tri par semestre et numero:
    _add_ue_semestre_id(ues, is_apc)
    _add_ue_semestre_id(ues_externes, is_apc)
    ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
    ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
    # Codes dupliqués (pour aider l'utilisateur)
    seen = set()
    duplicated_codes = {
        ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
    }
    ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]

    has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
    # editable = (not locked) and has_perm_change
    # On autorise maintenant la modification des formations qui ont
    # des semestres verrouillés, sauf si cela affect les notes passées
    # (verrouillées):
    #   - pas de modif des modules utilisés dans des semestres verrouillés
    #   - pas de changement des codes d'UE utilisés dans des semestres verrouillés
    editable = has_perm_change
    tag_editable = (
        current_user.has_permission(Permission.ScoEditFormationTags) or has_perm_change
    )
    if locked:
        lockicon = scu.icontag("lock32_img", title="verrouillé")
    else:
        lockicon = ""

    arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
    delete_icon = scu.icontag(
        "delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer"
    )
    delete_disabled_icon = scu.icontag(
        "delete_small_dis_img", title="Suppression impossible (module utilisé)"
    )
    H = [
        html_sco_header.sco_header(
            cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
            javascripts=[
                "libjs/jinplace-1.2.1.min.js",
                "js/ue_list.js",
                "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
                "libjs/jQuery-tagEditor/jquery.caret.min.js",
                "js/module_tag_editor.js",
            ],
            page_title=f"Programme {formation.acronyme}",
        ),
        f"""<h2>Formation {formation.titre} ({formation.acronyme}) 
        [version {formation.version}] code {formation.formation_code}
        {lockicon}
        </h2>
        """,
    ]
    if locked:
        H.append(
            f"""<p class="help">Cette formation est verrouillée car
des semestres verrouillés s'y réferent.
Si vous souhaitez modifier cette formation (par exemple pour y ajouter un module), 
vous devez:
</p>
<ul class="help">
<li>soit créer une nouvelle version de cette formation pour pouvoir l'éditer
librement (vous pouvez passer par la fonction "Associer à une nouvelle version
du programme" (menu "Semestre") si vous avez un semestre en cours);
</li>
<li>soit déverrouiller le ou les semestres qui s'y réfèrent (attention, en
 principe ces semestres sont archivés et ne devraient pas être modifiés).
</li>
</ul>"""
        )
    if msg:
        H.append('<p class="msg">' + msg + "</p>")

    if ues_with_duplicated_code:
        H.append(
            f"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
            formation ont le même code : <tt>{
                ', '.join([
                '<a class="stdlink" href="' + url_for( "notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] )
                + '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
                for ue in ues_with_duplicated_code ])
            }</tt>.
            Il faut corriger cela, sinon les capitalisations et ECTS seront 
            erronés !</span></div>"""
        )

    # Description de la formation
    H.append(
        render_template(
            "pn/form_descr.html",
            formation=formation,
            parcours=parcours,
            editable=editable,
        )
    )

    # Formation APC (BUT) ?
    if is_apc:
        H.append(
            f"""<div class="formation_apc_infos">
        <div class="ue_list_tit">Formation par compétences (BUT)
        - Semestre {_html_select_semestre_idx(formation_id, semestre_ids, semestre_idx)}
        </form>
        </div>
        """
        )
        if formation.referentiel_competence is None:
            descr_refcomp = ""
            msg_refcomp = "associer à un référentiel de compétences"
        else:
            descr_refcomp = f"""Référentiel de compétences: 
            <a href="{url_for('notes.refcomp_show', 
            scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
            {formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
            </a>&nbsp;"""
            msg_refcomp = "changer"
        H.append(f"""<ul><li>{descr_refcomp}""")
        if current_user.has_permission(Permission.ScoChangeFormation):
            H.append(
                f"""<a class="stdlink" href="{url_for('notes.refcomp_assoc_formation', 
        scodoc_dept=g.scodoc_dept, formation_id=formation_id)
        }">{msg_refcomp}</a>"""
            )

        H.append(
            f"""</li>
        <li> <a class="stdlink" href="{
            url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
        }">éditer les coefficients des ressources et SAÉs</a>
        </li>
        </ul>
        """
        )
    # Description des UE/matières/modules
    H.append(
        """
    <div class="formation_ue_list">
    <div class="ue_list_tit">Programme pédagogique:</div>
    <form>
    <input type="checkbox" class="sco_tag_checkbox">montrer les tags</input>
    </form>
    """
    )
    if is_apc:
        H.append(
            sco_edit_apc.html_edit_formation_apc(
                formation,
                semestre_idx=semestre_idx,
                editable=editable,
                tag_editable=tag_editable,
            )
        )
    else:
        H.append('<div class="formation_classic_infos">')
        H.append(
            _ue_table_ues(
                parcours,
                ues,
                editable,
                tag_editable,
                has_perm_change,
                arrow_up,
                arrow_down,
                arrow_none,
                delete_icon,
                delete_disabled_icon,
            )
        )
        if editable:
            H.append(
                f"""<ul>
                <li><a class="stdlink" href="{
                    url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, formation_id=formation_id)
                    }">Ajouter une UE</a>
                </li>
                <li><a href="{
                    url_for('notes.formation_add_malus_modules', 
                    scodoc_dept=g.scodoc_dept, formation_id=formation_id)
                }" class="stdlink">Ajouter des modules de malus dans chaque UE</a>
                </li>
                </ul>
                """
            )
        H.append("</div>")
    H.append("</div>")  # formation_ue_list

    if ues_externes:
        H.append(
            f"""
        <div class="formation_ue_list formation_ue_list_externes">
            <div class="ue_list_tit">UE externes déclarées (pour information):
            </div>
            {_ue_table_ues(
                parcours,
                ues_externes,
                editable,
                tag_editable,
                has_perm_change,
                arrow_up,
                arrow_down,
                arrow_none,
                delete_icon,
                delete_disabled_icon,
            )}
        </div>
        """
        )
    H.append("<p><ul>")
    if editable:
        H.append(
            f"""
            <li><a class="stdlink" href="{
            url_for('notes.formation_create_new_version', 
            scodoc_dept=g.scodoc_dept, formation_id=formation_id
            )
            }">Créer une nouvelle version (non verrouillée)</a>
            </li>
        """
        )
    H.append(
        f"""
        <li><a class="stdlink" href="{
            url_for('notes.formation_table_recap', scodoc_dept=g.scodoc_dept,
                formation_id=formation_id)
            }">Table récapitulative de la formation</a>
        </li>    
        <li><a class="stdlink" href="{
            url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
                formation_id=formation_id, format='xml')
            }">Export XML de la formation</a> 
            (permet de la sauvegarder pour l'échanger avec un autre site)
        </li>

        <li><a class="stdlink" href="{
            url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
            formation_id=formation_id, format='json')
            }">Export JSON de la formation</a>
        </li>

        <li><a class="stdlink" href="{
            url_for('notes.module_table', scodoc_dept=g.scodoc_dept,
            formation_id=formation_id)
            }">Liste détaillée des modules de la formation</a> (debug) 
        </li>
        </ul>
        </p>"""
    )
    if has_perm_change:
        H.append(
            """
        <h3> <a name="sems">Semestres ou sessions de cette formation</a></h3>
        <p><ul>"""
        )
        for sem in sco_formsemestre.do_formsemestre_list(
            args={"formation_id": formation_id}
        ):
            H.append(
                '<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a>'
                % sem
            )
            if not sem["etat"]:
                H.append(" [verrouillé]")
            else:
                H.append(
                    ' <a class="stdlink" href="formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s">Modifier</a>'
                    % sem
                )
            H.append("</li>")
        H.append("</ul>")

    if current_user.has_permission(Permission.ScoImplement):
        H.append(
            f"""<ul>
        <li><a class="stdlink" href="{
            url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
            formation_id=formation_id, semestre_id=1)
        }">Mettre en place un nouveau semestre de formation {formation.acronyme}</a>
        </li>
        </ul>"""
        )
    #   <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>

    warn, _ = sco_formsemestre_validation.check_formation_ues(formation_id)
    H.append(warn)

    H.append(html_sco_header.sco_footer())
    return "".join(H)


def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
    htm = """<form method="get">Semestre: 
    <select onchange="this.form.submit()" name="semestre_idx" id="semestre_idx" >
    """
    for i in list(semestre_ids) + ["all"]:
        if i == "all":
            label = "tous"
        else:
            label = f"S{i}"
        htm += f"""<option value="{i}" {
            'selected' 
            if (semestre_idx == i) 
            or (i == "all" and semestre_idx is None) 
            else ''
            }>{label}</option>
        """

    htm += f"""
    </select>
    <input type="hidden" name="formation_id" value="{formation_id}"></input>
    </form>"""
    return htm


def _ue_table_ues(
    parcours,
    ues,
    editable,
    tag_editable,
    has_perm_change,
    arrow_up,
    arrow_down,
    arrow_none,
    delete_icon,
    delete_disabled_icon,
):
    """Édition de programme: liste des UEs (avec leurs matières et modules).
    Pour les formations classiques (non APC/BUT)
    """
    H = []
    cur_ue_semestre_id = None
    iue = 0
    for ue in ues:
        if ue["ects"] is None:
            ue["ects_str"] = ""
        else:
            ue["ects_str"] = ", %g ECTS" % ue["ects"]
        if editable:
            klass = "span_apo_edit"
        else:
            klass = ""
        ue["code_apogee_str"] = (
            """, Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
            % (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR)
            + (ue["code_apogee"] or "")
            + "</span>"
        )

        if cur_ue_semestre_id != ue["semestre_id"]:
            cur_ue_semestre_id = ue["semestre_id"]
            if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
                lab = "Pas d'indication de semestre:"
            else:
                lab = "Semestre %s:" % ue["semestre_id"]
            H.append(
                '<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
            )
            H.append('<ul class="notes_ue_list">')
        H.append('<li class="notes_ue_list">')
        if iue != 0 and editable:
            H.append(
                '<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>'
                % (ue["ue_id"], arrow_up)
            )
        else:
            H.append(arrow_none)
        if iue < len(ues) - 1 and editable:
            H.append(
                '<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>'
                % (ue["ue_id"], arrow_down)
            )
        else:
            H.append(arrow_none)
        ue["acro_titre"] = str(ue["acronyme"])
        if ue["titre"] != ue["acronyme"]:
            ue["acro_titre"] += " " + str(ue["titre"])
        H.append(
            """%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span>
            <span class="ue_coef"></span>
            """
            % ue
        )
        if ue["type"] != sco_codes_parcours.UE_STANDARD:
            H.append(
                '<span class="ue_type">%s</span>'
                % sco_codes_parcours.UE_TYPE_NAME[ue["type"]]
            )
        if ue["is_external"]:
            # Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE
            # qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml)
            # Dans ce cas, propose de changer le type (même si verrouillée)
            if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1:
                H.append('<span class="ue_is_external">')
                if has_perm_change:
                    H.append(
                        f"""<a class="stdlink" href="{
                            url_for("notes.ue_set_internal", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"])
                            }">transformer en UE ordinaire</a>&nbsp;"""
                    )
                H.append("</span>")
        ue_editable = editable and not ue_is_locked(ue["ue_id"])
        if ue_editable:
            H.append(
                '<a class="stdlink" href="ue_edit?ue_id=%(ue_id)s">modifier</a>' % ue
            )
        else:
            H.append('<span class="locked">[verrouillé]</span>')
        H.append(
            _ue_table_matieres(
                parcours,
                ue,
                editable,
                tag_editable,
                arrow_up,
                arrow_down,
                arrow_none,
                delete_icon,
                delete_disabled_icon,
            )
        )
        if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
            H.append(
                f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, 
                formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
            }">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>
            </div>
            """
            )
        iue += 1

    return "\n".join(H)


def _ue_table_matieres(
    parcours,
    ue,
    editable,
    tag_editable,
    arrow_up,
    arrow_down,
    arrow_none,
    delete_icon,
    delete_disabled_icon,
):
    """Édition de programme: liste des matières (et leurs modules) d'une UE."""
    H = []
    if not parcours.UE_IS_MODULE:
        H.append('<ul class="notes_matiere_list">')
    matieres = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
    for mat in matieres:
        if not parcours.UE_IS_MODULE:
            H.append('<li class="notes_matiere_list">')
            if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
                H.append(
                    f"""<a class="stdlink" href="{
                        url_for("notes.matiere_edit", 
                        scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])
                        }">
                    """
                )
            H.append("%(titre)s" % mat)
            if editable and not sco_edit_matiere.matiere_is_locked(mat["matiere_id"]):
                H.append("</a>")

        modules = sco_edit_module.module_list(args={"matiere_id": mat["matiere_id"]})
        H.append(
            _ue_table_modules(
                parcours,
                ue,
                mat,
                modules,
                editable,
                tag_editable,
                arrow_up,
                arrow_down,
                arrow_none,
                delete_icon,
                delete_disabled_icon,
            )
        )
        if not parcours.UE_IS_MODULE:
            H.append("</li>")
    if not matieres:
        H.append("<li>Aucune matière dans cette UE ! ")
        if editable:
            H.append(
                """<a class="stdlink" href="ue_delete?ue_id=%(ue_id)s">supprimer l'UE</a>"""
                % ue
            )
        H.append("</li>")
    if editable and not parcours.UE_IS_MODULE:
        H.append(
            '<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>'
            % ue
        )
    if not parcours.UE_IS_MODULE:
        H.append("</ul>")
    return "\n".join(H)


def _ue_table_modules(
    parcours,
    ue,
    mat,
    modules,
    editable,
    tag_editable,
    arrow_up,
    arrow_down,
    arrow_none,
    delete_icon,
    delete_disabled_icon,
    unit_name="matière",
    add_suppress_link=True,  # lien "supprimer cette matière"
    empty_list_msg="Aucun élément dans cette matière",
    create_element_msg="créer un module",
):
    """Édition de programme: liste des modules d'une matière d'une UE"""
    H = ['<ul class="notes_module_list">']
    im = 0
    for mod in modules:
        mod["nb_moduleimpls"] = sco_edit_module.module_count_moduleimpls(
            mod["module_id"]
        )
        klass = "notes_module_list"
        if mod["module_type"] == ModuleType.MALUS:
            klass += " module_malus"
        H.append('<li class="%s">' % klass)

        H.append('<span class="notes_module_list_buts">')
        if im != 0 and editable:
            H.append(
                '<a href="module_move?module_id=%s&after=0" class="aud">%s</a>'
                % (mod["module_id"], arrow_up)
            )
        else:
            H.append(arrow_none)
        if im < len(modules) - 1 and editable:
            H.append(
                '<a href="module_move?module_id=%s&after=1" class="aud">%s</a>'
                % (mod["module_id"], arrow_down)
            )
        else:
            H.append(arrow_none)
        im += 1
        if mod["nb_moduleimpls"] == 0 and editable:
            icon = delete_icon
        else:
            icon = delete_disabled_icon
        H.append(
            '<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
            % (mod["module_id"], icon)
        )

        H.append("</span>")

        mod_editable = (
            editable  # and not sco_edit_module.module_is_locked( Mod['module_id'])
        )
        if mod_editable:
            H.append(
                '<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">'
                % mod
            )
        H.append(
            '<span class="formation_module_tit">%s</span>'
            % scu.join_words(mod["code"], mod["titre"])
        )
        if mod_editable:
            H.append("</a>")
        heurescoef = (
            "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
        )
        if mod_editable:
            klass = "span_apo_edit"
        else:
            klass = ""
        heurescoef += (
            ', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
            % (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
            + (mod["code_apogee"] or "")
            + "</span>"
        )
        if tag_editable:
            tag_cls = "module_tag_editor"
        else:
            tag_cls = "module_tag_editor_ro"
        tag_mk = """<span class="sco_tag_edit"><form><textarea data-module_id="{}" class="{}">{}</textarea></form></span>"""
        tag_edit = tag_mk.format(
            mod["module_id"],
            tag_cls,
            ",".join(sco_tag_module.module_tag_list(mod["module_id"])),
        )
        if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
            warning_semestre = ' <span class="red">incohérent ?</span>'
        else:
            warning_semestre = ""
        H.append(
            " %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
            + " (%s)" % heurescoef
            + tag_edit
        )
        H.append("</li>")
    if not modules:
        H.append(f"<li>{empty_list_msg} ! ")
        if editable and add_suppress_link:
            H.append(
                f"""<a class="stdlink" href="{
                    url_for("notes.matiere_delete", 
                    scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
                >la supprimer</a>
                """
            )
        H.append("</li>")
    if editable:  # and ((not parcours.UE_IS_MODULE) or len(Modlist) == 0):
        H.append(
            f"""<li> <a class="stdlink" href="{
                    url_for("notes.module_create", 
                    scodoc_dept=g.scodoc_dept, matiere_id=mat["matiere_id"])}"
            >{create_element_msg}</a></li>
            """
        )
    H.append("</ul>")
    return "\n".join(H)


def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
    """HTML list of UE sharing this code
    Either ue_code or ue_id may be specified.
    hide_ue_id spécifie un id à retirer de la liste.
    """
    from app.scodoc import sco_formations

    ue_code = str(ue_code)
    if ue_id:
        ue = ue_list(args={"ue_id": ue_id})[0]
        if not ue_code:
            ue_code = ue["ue_code"]
        F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
        formation_code = F["formation_code"]
        # UE du même code, code formation et departement:
        q_ues = (
            UniteEns.query.filter_by(ue_code=ue_code)
            .join(UniteEns.formation, aliased=True)
            .filter_by(dept_id=g.scodoc_dept_id, formation_code=formation_code)
        )
    else:
        # Toutes les UE du departement avec ce code:
        q_ues = (
            UniteEns.query.filter_by(ue_code=ue_code)
            .join(UniteEns.formation, aliased=True)
            .filter_by(dept_id=g.scodoc_dept_id)
        )

    if hide_ue_id:  # enlève l'ue de depart
        q_ues = q_ues.filter(UniteEns.id != hide_ue_id)

    ues = q_ues.all()
    if not ues:
        if ue_id:
            return """<span class="ue_share">Seule UE avec code %s</span>""" % ue_code
        else:
            return """<span class="ue_share">Aucune UE avec code %s</span>""" % ue_code
    H = []
    if ue_id:
        H.append('<span class="ue_share">Autres UE avec le code %s:</span>' % ue_code)
    else:
        H.append('<span class="ue_share">UE avec le code %s:</span>' % ue_code)
    H.append("<ul>")
    for ue in ues:
        H.append(
            f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink" 
            href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
            >{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
            </li>
            """
        )
    H.append("</ul>")
    return "\n".join(H)


def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
    "edit an UE"
    # check
    ue_id = args["ue_id"]
    ue = ue_list({"ue_id": ue_id})[0]
    if (not bypass_lock) and ue_is_locked(ue["ue_id"]):
        raise ScoLockedFormError()
    # check: acronyme unique dans cette formation
    if "acronyme" in args:
        new_acro = args["acronyme"]
        ues = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro})
        if ues and ues[0]["ue_id"] != ue_id:
            raise ScoValueError(
                f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! 
                (chaque UE doit avoir un acronyme unique dans la formation)"""
            )
    # On ne peut pas supprimer le code UE:
    if "ue_code" in args and not args["ue_code"]:
        del args["ue_code"]

    cnx = ndb.GetDBConnexion()
    _ueEditor.edit(cnx, args)

    formation = Formation.query.get(ue["formation_id"])
    if not dont_invalidate_cache:
        # Invalide les semestres utilisant cette formation:
        formation.invalidate_cached_sems()


# essai edition en ligne:
def edit_ue_set_code_apogee(id=None, value=None):
    "set UE code apogee"
    ue_id = id
    value = value.strip("-_ \t")[:APO_CODE_STR_LEN]  # tronque

    log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))

    ues = ue_list(args={"ue_id": ue_id})
    if not ues:
        return "ue invalide"

    do_ue_edit(
        {"ue_id": ue_id, "code_apogee": value},
        bypass_lock=True,
        dont_invalidate_cache=False,
    )
    if not value:
        value = scu.APO_MISSING_CODE_STR
    return value


def ue_is_locked(ue_id):
    """True if UE should not be modified
    (contains modules used in a locked formsemestre)
    """
    r = ndb.SimpleDictFetch(
        """SELECT ue.id
        FROM notes_ue ue, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi
        WHERE ue.id = mod.ue_id
        AND mi.module_id = mod.id AND mi.formsemestre_id = sem.id
        AND ue.id = %(ue_id)s AND sem.etat = false
        """,
        {"ue_id": ue_id},
    )
    return len(r) > 0


UE_PALETTE = [
    "#B80004",  # rouge
    "#F97B3D",  # Orange Crayola
    "#FEB40B",  # Honey Yellow
    "#80CB3F",  # Yellow Green
    "#05162E",  # Oxford Blue
    "#548687",  # Steel Teal
    "#444054",  # Independence
    "#889696",  # Spanish Gray
    "#0CA4A5",  # Viridian Green
]


def colorie_anciennes_ues(ues: list[UniteEns]) -> None:
    """Avant ScoDoc 9.2, les ue n'avaient pas de couleurs
    Met des défauts raisonnables
    """
    nb_colors = len(UE_PALETTE)
    index = 0
    last_sem_idx = 0
    for ue in ues:
        if ue.semestre_idx != last_sem_idx:
            index = 0
        last_sem_idx = ue.semestre_idx
        if ue.color is None:
            ue.color = UE_PALETTE[index % nb_colors]
            index += 1
            db.session.add(ue)


def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str:
    """Un code couleur pour une nouvelle UE dans ce semestre"""
    nb_colors = len(UE_PALETTE)
    # UE existantes dans ce semestre:
    nb_ues = UniteEns.query.filter(
        UniteEns.formation_id == formation_id,
        UniteEns.semestre_idx == default_semestre_idx,
    ).count()
    index = nb_ues
    return UE_PALETTE[index % nb_colors]