# -*- 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
#
##############################################################################

"""Opérations d'inscriptions aux semestres et modules
"""
import collections
import time

import flask
from flask import flash, url_for, g, request

from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
from app.models.etudiants import Identite
from app.models.groups import Partition, GroupDescr
from app.models.scolar_event import ScolarEvent
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.codes_cursus import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_find_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_groups
from app.scodoc import sco_etud
from app.scodoc import sco_cache
from app.scodoc import html_sco_header


# --- Gestion des inscriptions aux semestres
_formsemestre_inscriptionEditor = ndb.EditableTable(
    "notes_formsemestre_inscription",
    "formsemestre_inscription_id",
    ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"),
    sortkey="formsemestre_id",
    insert_ignore_conflicts=True,
)


def do_formsemestre_inscription_list(*args, **kw):
    "list formsemestre_inscriptions"
    cnx = ndb.GetDBConnexion()
    return _formsemestre_inscriptionEditor.list(cnx, *args, **kw)


def do_formsemestre_inscription_listinscrits(formsemestre_id):
    """Liste les inscrits (état I) à ce semestre et cache le résultat.
    Result: [ { "etudid":, "formsemestre_id": , "etat": , "etape": }]
    """
    r = sco_cache.SemInscriptionsCache.get(formsemestre_id)
    if r is None:
        # retreive list
        r = do_formsemestre_inscription_list(
            args={"formsemestre_id": formsemestre_id, "etat": scu.INSCRIT}
        )
        sco_cache.SemInscriptionsCache.set(formsemestre_id, r)
    return r


def do_formsemestre_inscription_create(args, method=None):
    "create a formsemestre_inscription (and sco event)"
    cnx = ndb.GetDBConnexion()
    log(f"do_formsemestre_inscription_create: args={args}")
    sems = sco_formsemestre.do_formsemestre_list(
        {"formsemestre_id": args["formsemestre_id"]}
    )
    if len(sems) != 1:
        raise ScoValueError(f"code de semestre invalide: {args['formsemestre_id']}")
    sem = sems[0]
    # check lock
    if not sem["etat"]:
        raise ScoValueError("inscription: semestre verrouille")
    #
    r = _formsemestre_inscriptionEditor.create(cnx, args)
    # Evenement
    sco_etud.scolar_events_create(
        cnx,
        args={
            "etudid": args["etudid"],
            "event_date": time.strftime("%d/%m/%Y"),
            "formsemestre_id": args["formsemestre_id"],
            "event_type": "INSCRIPTION",
        },
    )
    # Log etudiant
    logdb(
        cnx,
        method=method,
        etudid=args["etudid"],
        msg=f"inscription en semestre {args['formsemestre_id']}",
        commit=False,
    )
    #
    sco_cache.invalidate_formsemestre(formsemestre_id=args["formsemestre_id"])
    return r


def do_formsemestre_inscription_delete(oid, formsemestre_id=None):
    "delete formsemestre_inscription"
    cnx = ndb.GetDBConnexion()
    _formsemestre_inscriptionEditor.delete(cnx, oid)

    sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)


def do_formsemestre_demission(
    etudid,
    formsemestre_id,
    event_date=None,
    etat_new=scu.DEMISSION,  # DEMISSION or DEF
    operation_method="dem_etudiant",
    event_type="DEMISSION",
):
    "Démission ou défaillance d'un étudiant"
    # marque 'D' ou DEF dans l'inscription au semestre et ajoute
    # un "evenement" scolarite
    if etat_new not in (scu.DEF, scu.DEMISSION):
        raise ScoValueError("nouveau code d'état invalide")
    try:
        event_date_iso = ndb.DateDMYtoISO(event_date)
    except ValueError as exc:
        raise ScoValueError("format de date invalide") from exc
    etud = Identite.get_etud(etudid)
    # check lock
    formsemestre: FormSemestre = FormSemestre.query.filter_by(
        id=formsemestre_id, dept_id=g.scodoc_dept_id
    ).first_or_404()
    if not formsemestre.etat:
        raise ScoValueError("Modification impossible: semestre verrouille")
    #
    if formsemestre_id not in (inscr.formsemestre_id for inscr in etud.inscriptions()):
        raise ScoValueError("étudiant non inscrit dans ce semestre !")
    inscr = next(
        inscr
        for inscr in etud.inscriptions()
        if inscr.formsemestre_id == formsemestre_id
    )
    inscr.etat = etat_new
    db.session.add(inscr)
    Scolog.logdb(method=operation_method, etudid=etudid)
    event = ScolarEvent(
        etudid=etudid,
        event_date=event_date_iso,
        formsemestre_id=formsemestre_id,
        event_type=event_type,
    )
    db.session.add(event)
    db.session.commit()
    sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
    if etat_new == scu.DEMISSION:
        flash("Démission enregistrée")
    elif etat_new == scu.DEF:
        flash("Défaillance enregistrée")


def do_formsemestre_inscription_edit(args=None, formsemestre_id=None):
    "edit a formsemestre_inscription"
    cnx = ndb.GetDBConnexion()
    _formsemestre_inscriptionEditor.edit(cnx, args)
    sco_cache.invalidate_formsemestre(
        formsemestre_id=formsemestre_id
    )  # > modif inscription semestre


def check_if_has_decision_jury(
    formsemestre: FormSemestre, etudids: list[int] | set[int]
):
    "raise exception if one of the etuds has a decision in formsemestre"
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    for etudid in etudids:
        if nt.etud_has_decision(etudid):
            etud = Identite.query.get(etudid)
            raise ScoValueError(
                f"""désinscription impossible: l'étudiant {etud.nomprenom} a
                une décision de jury (la supprimer avant si nécessaire)"""
            )


def do_formsemestre_desinscription(
    etudid, formsemestre_id: int, check_has_dec_jury=True
):
    """Désinscription d'un étudiant.
    Si semestre extérieur et dernier inscrit, suppression de ce semestre.
    """
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    etud = Identite.get_etud(etudid)
    # -- check lock
    if not formsemestre.etat:
        raise ScoValueError("désinscription impossible: semestre verrouille")

    # -- Si decisions de jury, désinscription interdite
    if check_has_dec_jury:
        check_if_has_decision_jury(formsemestre, [etudid])

    insem = do_formsemestre_inscription_list(
        args={"formsemestre_id": formsemestre_id, "etudid": etudid}
    )
    if not insem:
        raise ScoValueError(f"{etud.nomprenom} n'est pas inscrit au semestre !")
    insem = insem[0]
    # -- desinscription de tous les modules
    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cursor.execute(
        """SELECT Im.id AS moduleimpl_inscription_id
        FROM notes_moduleimpl_inscription Im, notes_moduleimpl M
        WHERE Im.etudid=%(etudid)s
        and Im.moduleimpl_id = M.id
        and M.formsemestre_id = %(formsemestre_id)s
        """,
        {"etudid": etudid, "formsemestre_id": formsemestre_id},
    )
    res = cursor.fetchall()
    moduleimpl_inscription_ids = [x[0] for x in res]
    for moduleimpl_inscription_id in moduleimpl_inscription_ids:
        sco_moduleimpl.do_moduleimpl_inscription_delete(
            moduleimpl_inscription_id, formsemestre_id=formsemestre_id
        )

    # -- désincription de tous les groupes des partitions de ce semestre
    Partition.formsemestre_remove_etud(formsemestre_id, etud)

    # -- désincription du semestre
    do_formsemestre_inscription_delete(
        insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id
    )
    sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
    # --- Semestre extérieur
    if formsemestre.modalite == "EXT":
        if 0 == len(formsemestre.inscriptions):
            log(
                f"""do_formsemestre_desinscription:
                suppression du semestre extérieur {formsemestre}"""
            )
            db.session.delete(formsemestre)
            db.session.commit()
            flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")

    logdb(
        cnx,
        method="formsemestre_desinscription",
        etudid=etudid,
        msg=f"desinscription semestre {formsemestre_id}",
        commit=False,
    )


def do_formsemestre_inscription_with_modules(
    formsemestre_id,
    etudid,
    group_ids: list = None,
    etat=scu.INSCRIT,
    etape=None,
    method="inscription_with_modules",
    dept_id: int = None,
):
    """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
    (donc sauf le sport)
    Si dept_id est spécifié, utilise ce département au lieu du courant.
    """
    group_ids = group_ids or []
    if isinstance(group_ids, int):
        group_ids = [group_ids]
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
    # inscription au semestre
    args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
    if etat is not None:
        args["etat"] = etat
    if etape is not None:
        args["etape"] = etape
    do_formsemestre_inscription_create(args, method=method)
    log(
        f"""do_formsemestre_inscription_with_modules: etudid={
            etudid} formsemestre_id={formsemestre_id}"""
    )
    # inscriptions aux groupes
    # 1- inscrit au groupe 'tous'
    group_id = sco_groups.get_default_group(formsemestre_id)
    sco_groups.set_group(etudid, group_id)
    gdone = {group_id: 1}  # empeche doublons

    # 2- inscrit aux groupes
    for group_id in group_ids:
        if group_id and group_id not in gdone:
            _ = GroupDescr.query.get_or_404(group_id)
            sco_groups.set_group(etudid, group_id)
            gdone[group_id] = 1

    # Inscription à tous les modules de ce semestre
    for modimpl in formsemestre.modimpls:
        if modimpl.module.ue.type != UE_SPORT:
            sco_moduleimpl.do_moduleimpl_inscription_create(
                {"moduleimpl_id": modimpl.id, "etudid": etudid},
                formsemestre_id=formsemestre_id,
            )
    # Mise à jour des inscriptions aux parcours:
    formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)


def formsemestre_inscription_with_modules_etud(
    formsemestre_id, etudid=None, group_ids=None
):
    """Form. inscription d'un étudiant au semestre.
    Si etudid n'est pas specifié, form. choix etudiant.
    """
    if etudid is None:
        return sco_find_etud.form_search_etud(
            title="Choix de l'étudiant à inscrire dans ce semestre",
            add_headers=True,
            dest_url="notes.formsemestre_inscription_with_modules_etud",
            parameters={"formsemestre_id": formsemestre_id},
            parameters_keys="formsemestre_id",
        )

    return formsemestre_inscription_with_modules(
        etudid, formsemestre_id, group_ids=group_ids
    )


def formsemestre_inscription_with_modules_form(etudid, only_ext=False):
    """Formulaire inscription de l'etud dans l'un des semestres existants.
    Si only_ext, ne montre que les semestre extérieurs.
    """
    etud: Identite = Identite.query.filter_by(
        id=etudid, dept_id=g.scodoc_dept_id
    ).first_or_404()
    H = [
        html_sco_header.sco_header(),
        f"<h2>Inscription de {etud.nomprenom}",
    ]
    if only_ext:
        H.append(" dans un semestre extérieur")
    H.append(
        """</h2>
    <p class="help">L'étudiant sera inscrit à <em>tous</em> les modules du semestre
    choisi (sauf Sport &amp; Culture).
    </p>
    <h3>Choisir un semestre:</h3>"""
    )
    footer = html_sco_header.sco_footer()
    # sems = sco_formsemestre.do_formsemestre_list(args={"etat": "1"})
    formsemestres = (
        FormSemestre.query.filter_by(etat=True, dept_id=g.scodoc_dept_id)
        .join(Formation)
        .order_by(
            Formation.acronyme,
            FormSemestre.semestre_id,
            FormSemestre.modalite,
            FormSemestre.date_debut,
        )
        .all()
    )
    if len(formsemestres):
        H.append("<ul>")
        for formsemestre in formsemestres:
            # Ne propose que les semestres où etudid n'est pas déjà inscrit
            if formsemestre.id not in {
                ins.formsemestre_id for ins in etud.inscriptions()
            }:
                if (not only_ext) or (formsemestre.modalite == "EXT"):
                    H.append(
                        f"""
                    <li>
                    <a class="stdlink" href="{
                    url_for("notes.formsemestre_inscription_with_modules",
                        scodoc_dept=g.scodoc_dept,
                        etudid=etudid, formsemestre_id=formsemestre.id
                    )}">{formsemestre.titre_mois()}</a>
                    </li>
                    """
                    )
        H.append("</ul>")
    else:
        H.append("<p>aucune session de formation !</p>")
    H.append(
        f"""<h3>ou</h3> <a class="stdlink" href="{
            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        }">retour à la fiche de {etud.nomprenom}</a>"""
    )
    return "\n".join(H) + footer


def formsemestre_inscription_with_modules(
    etudid, formsemestre_id, group_ids=None, multiple_ok=False
):
    """
    Inscription de l'etud dans ce semestre.
    Formulaire avec choix groupe.
    """
    log(
        f"""formsemestre_inscription_with_modules: etudid={etudid} formsemestre_id={
            formsemestre_id} group_ids={group_ids}"""
    )
    if multiple_ok:
        multiple_ok = int(multiple_ok)
    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    etud = Identite.get_etud(etudid)
    if etud.dept_id != formsemestre.dept_id:
        raise ScoValueError("l'étudiant n'est pas dans ce département")
    H = [
        html_sco_header.html_sem_header(
            f"Inscription de {etud.nomprenom} dans ce semestre",
        )
    ]
    footer = html_sco_header.sco_footer()
    # Check 1: déjà inscrit ici ?
    inscr = FormSemestreInscription.query.filter_by(
        etudid=etud.id, formsemestre_id=formsemestre.id
    ).first()
    if inscr is not None:
        H.append(
            f"""
            <p class="warning">{etud.nomprenom} est déjà inscrit
            dans le semestre {formsemestre.titre_mois()}
            </p>
            <ul>
            <li><a href="{url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
            }" class="stdlink">retour à la fiche de {etud.nomprenom}</a>
            </li>
            <li><a href="{url_for(
                    "notes.formsemestre_status",
                    scodoc_dept=g.scodoc_dept,
                    formsemestre_id=formsemestre_id,
            )}" class="stdlink">retour au tableau de bord de {formsemestre.titre_mois()}</a></li>
            </ul>
        """
        )
        return "\n".join(H) + footer
    # Check 2: déjà inscrit dans un semestre recouvrant les même dates ?
    # Informe et propose dé-inscriptions
    others = est_inscrit_ailleurs(etudid, formsemestre_id)
    if others and not multiple_ok:
        l = []
        for s in others:
            l.append(
                f"""<a class="discretelink" href="{
                    url_for("notes.formsemestre_status",
                        scodoc_dept=g.scodoc_dept, formsemestre_id=s['formsemestre_id'])
                }">{s['titremois']}</a>"""
            )

        H.append(
            f"""<p class="warning">Attention: {etud.nomprenom} est déjà inscrit sur
            la même période dans: {", ".join(l)}.
            </p>"""
        )
        H.append("<ul>")
        for s in others:
            H.append(
                f"""<li><a href="{
                    url_for("notes.formsemestre_desinscription", scodoc_dept=g.scodoc_dept,
                    formsemestre_id=s["formsemestre_id"], etudid=etudid )
                }" class="stdlink">désinscrire de {s["titreannee"]}
                </li>"""
            )
        H.append("</ul>")
        H.append(
            f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
            scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
            multiple_ok=1,
            group_ids=group_ids )
            }">Continuer quand même l'inscription</a>
            </p>"""
            # was sco_groups.make_query_groups(group_ids)
        )
        return "\n".join(H) + footer
    #
    if group_ids is not None:
        # OK, inscription
        do_formsemestre_inscription_with_modules(
            formsemestre_id,
            etudid,
            group_ids=group_ids,
            etat=scu.INSCRIT,
            method="formsemestre_inscription_with_modules",
        )
        return flask.redirect(
            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        )
    else:
        # formulaire choix groupe
        H.append(
            f"""<form method="GET" name="groupesel" action="{request.base_url}">
            <input type="hidden" name="etudid" value="{etudid}">
            <input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
        """
        )

        H.append(sco_groups.form_group_choice(formsemestre_id, allow_none=True))

        #
        H.append(
            """
        <input type="submit" value="Inscrire"/>
        <p>Note: l'étudiant sera inscrit dans les groupes sélectionnés</p>
        </form>
        """
        )
        return "\n".join(H) + footer


def formsemestre_inscription_option(etudid, formsemestre_id):
    """Dialogue pour (dés)inscription à des modules optionnels."""
    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    if not sem["etat"]:
        raise ScoValueError("Modification impossible: semestre verrouille")

    etud = Identite.get_etud(etudid)
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

    footer = html_sco_header.sco_footer()
    H = [
        html_sco_header.sco_header(),
        f"""<h2>Inscription de {etud.nomprenom} aux modules de {formsemestre.titre_mois()}</h2>""",
    ]

    # Cherche les moduleimpls et les inscriptions
    inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid)
    # Formulaire
    modimpls_by_ue_ids = collections.defaultdict(list)  # ue_id : [ moduleimpl_id ]
    modimpls_by_ue_names = collections.defaultdict(list)  # ue_id : [ moduleimpl_name ]
    ues = []
    ue_ids = set()
    initvalues = {}
    for modimpl in formsemestre.modimpls:
        ue_id = modimpl.module.ue.id
        if not ue_id in ue_ids:
            ues.append(modimpl.module.ue)
            ue_ids.add(ue_id)
        modimpls_by_ue_ids[ue_id].append(modimpl.id)

        modimpls_by_ue_names[ue_id].append(
            f"{modimpl.module.code or ''} {modimpl.module.titre or ''}"
        )
        vals = scu.get_request_args()
        if not vals.get("tf_submitted", False):
            # inscrit ?
            for ins in inscr:
                if ins["moduleimpl_id"] == modimpl.id:
                    key = f"moduleimpls_{ue_id}"
                    if key in initvalues:
                        initvalues[key].append(str(modimpl.id))
                    else:
                        initvalues[key] = [str(modimpl.id)]
                    break

    descr = [
        ("formsemestre_id", {"input_type": "hidden"}),
        ("etudid", {"input_type": "hidden"}),
    ]
    for ue in ues:
        ue_id = ue.id
        ue_descr = ue.acronyme
        if ue.type != UE_STANDARD:
            ue_descr += f" <em>{UE_TYPE_NAME[ue.type]}</em>"
        ue_status = nt.get_etud_ue_status(etudid, ue_id)
        if ue_status and ue_status["is_capitalized"]:
            sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
            ue_descr += f"""
                <a class="discretelink" href="{ url_for(
                    'notes.formsemestre_bulletinetud', scodoc_dept=g.scodoc_dept,
                    formsemestre_id=sem_origin["formsemestre_id"],
                    etudid = etudid
                    )}" title="{sem_origin['titreannee']}">(capitalisée le {
                        ndb.DateISOtoDMY(ue_status["event_date"])
                    })
                """
        descr.append(
            (
                f"sec_{ue_id}",
                {
                    "input_type": "separator",
                    "title": f"""<b>{ue_descr} :</b>
                    <a href="#" onclick="chkbx_select('{ue_id}', true);">inscrire</a> | <a
                    href="#" onclick="chkbx_select('{ue_id}', false);">désinscrire</a>
                    à tous les modules
                    """,
                },
            )
        )
        descr.append(
            (
                f"moduleimpls_{ue_id}",
                {
                    "input_type": "checkbox",
                    "title": "",
                    "dom_id": ue_id,
                    "allowed_values": [str(x) for x in modimpls_by_ue_ids[ue_id]],
                    "labels": modimpls_by_ue_names[ue_id],
                    "vertical": True,
                },
            )
        )

    H.append(
        """<script type="text/javascript">
function chkbx_select(field_id, state) {
   var elems = document.getElementById(field_id).getElementsByTagName("input");
   for (var i=0; i < elems.length; i++) {
      elems[i].checked=state;
   }
}
    </script>
    """
    )
    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        descr,
        initvalues,
        cancelbutton="Annuler",
        submitlabel="Modifier les inscriptions",
        cssclass="inscription",
        name="tf",
    )
    if tf[0] == 0:
        H.append(
            """
    <p>Voici la liste des modules du semestre choisi.</p>
    <p>
    Les modules cochés sont ceux dans lesquels l'étudiant est inscrit.
    Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.
    </p>
    <p>Attention: cette méthode ne devrait être utilisée que pour les modules
     <b>optionnels</b> (ou les activités culturelles et sportives) et pour désinscrire
      les étudiants dispensés (UE validées).
    </p>
    """
        )
        return "\n".join(H) + "\n" + tf[1] + footer
    if tf[0] == -1:
        return flask.redirect(
            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        )

    # Inscriptions aux modules choisis
    # il faut desinscrire des modules qui ne figurent pas
    # et inscrire aux autres, sauf si deja inscrit
    a_desinscrire = {}.fromkeys([x.id for x in formsemestre.modimpls])
    insdict = {}
    for ins in inscr:
        insdict[ins["moduleimpl_id"]] = ins
    for ue in ues:
        for moduleimpl_id in [int(x) for x in tf[2][f"moduleimpls_{ue.id}"]]:
            if moduleimpl_id in a_desinscrire:
                del a_desinscrire[moduleimpl_id]
    # supprime ceux auxquel pas inscrit
    moduleimpls_a_desinscrire = list(a_desinscrire.keys())
    for moduleimpl_id in moduleimpls_a_desinscrire:
        if moduleimpl_id not in insdict:
            del a_desinscrire[moduleimpl_id]

    a_inscrire = set()
    for ue in ues:
        a_inscrire.update(
            int(x) for x in tf[2][f"moduleimpls_{ue.id}"]
        )  # conversion en int !
    # supprime ceux auquel deja inscrit:
    for ins in inscr:
        if ins["moduleimpl_id"] in a_inscrire:
            a_inscrire.remove(ins["moduleimpl_id"])
    # dict des modules:
    modimpls_by_id = {modimpl.id: modimpl for modimpl in formsemestre.modimpls}
    #
    if (not a_inscrire) and (not a_desinscrire):
        H.append(
            f"""<h3>Aucune modification à effectuer</h3>
        <p><a class="stdlink" href="{
            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        }">retour à la fiche étudiant</a></p>
        """
        )
        return "\n".join(H) + footer

    H.append("<h3>Confirmer les modifications:</h3>")
    if a_desinscrire:
        H.append(
            f"""<p>{etud.nomprenom} va être <b>désinscrit{etud.e}</b> des modules:<ul><li>"""
        )
        H.append(
            "</li><li>".join(
                [
                    f"""{modimpls_by_id[x].module.titre or ''} ({
                        modimpls_by_id[x].module.code or '(module sans code)'})"""
                    for x in a_desinscrire
                ]
            )
            + "</p>"
        )
        H.append("</li></ul>")
    if a_inscrire:
        H.append(
            f"""<p>{etud.nomprenom} va être <b>inscrit{etud.e}</b> aux modules:<ul><li>"""
        )
        H.append(
            "</li><li>".join(
                [
                    f"""{modimpls_by_id[x].module.titre or ''} ({
                        modimpls_by_id[x].module.code or '(module sans code)'})"""
                    for x in a_inscrire
                ]
            )
            + "</p>"
        )
        H.append("</li></ul>")
    modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
    modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
    H.append(
        f"""
    <form action="do_moduleimpl_incription_options">
        <input type="hidden" name="etudid" value="{etudid}"/>
        <input type="hidden" name="modulesimpls_ainscrire" value="{modulesimpls_ainscrire}"/>
        <input type="hidden" name="modulesimpls_adesinscrire" value="{modulesimpls_adesinscrire}"/>
        <input type ="submit" value="Confirmer"/>
        <input type ="button" value="Annuler" onclick="document.location='{
            url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        }';"/>
    </form>
    """
    )
    return "\n".join(H) + footer


def do_moduleimpl_incription_options(
    etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire
):
    """
    Effectue l'inscription et la description aux modules optionnels
    """
    if isinstance(modulesimpls_ainscrire, int):
        modulesimpls_ainscrire = str(modulesimpls_ainscrire)
    if isinstance(modulesimpls_adesinscrire, int):
        modulesimpls_adesinscrire = str(modulesimpls_adesinscrire)
    if modulesimpls_ainscrire:
        a_inscrire = [int(x) for x in modulesimpls_ainscrire.split(",")]
    else:
        a_inscrire = []
    if modulesimpls_adesinscrire:
        a_desinscrire = [int(x) for x in modulesimpls_adesinscrire.split(",")]
    else:
        a_desinscrire = []
    # inscriptions
    for moduleimpl_id in a_inscrire:
        # verifie que ce module existe bien
        mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
        if len(mods) != 1:
            raise ScoValueError(f"inscription: invalid moduleimpl_id: {moduleimpl_id}")
        mod = mods[0]
        sco_moduleimpl.do_moduleimpl_inscription_create(
            {"moduleimpl_id": moduleimpl_id, "etudid": etudid},
            formsemestre_id=mod["formsemestre_id"],
        )
    # desinscriptions
    for moduleimpl_id in a_desinscrire:
        # verifie que ce module existe bien
        mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
        if len(mods) != 1:
            raise ScoValueError(
                f"desinscription: invalid moduleimpl_id: {moduleimpl_id}"
            )
        mod = mods[0]
        inscr = sco_moduleimpl.do_moduleimpl_inscription_list(
            moduleimpl_id=moduleimpl_id, etudid=etudid
        )
        if not inscr:
            raise ScoValueError(
                f"pas inscrit a ce module ! (etudid={etudid}, moduleimpl_id={moduleimpl_id})"
            )
        oid = inscr[0]["moduleimpl_inscription_id"]
        sco_moduleimpl.do_moduleimpl_inscription_delete(
            oid, formsemestre_id=mod["formsemestre_id"]
        )

    H = [
        html_sco_header.sco_header(),
        f"""<h3>Modifications effectuées</h3>
            <p><a class="stdlink" href="{
                url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
            }">
            Retour à la fiche étudiant</a>
            </p>
        """,
        html_sco_header.sco_footer(),
    ]
    return "\n".join(H)


def est_inscrit_ailleurs(etudid, formsemestre_id):
    """Vrai si l'étudiant est inscrit dans un semestre en même
    temps que celui indiqué (par formsemestre_id).
    Retourne la liste des semestres concernés (ou liste vide).
    """
    etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
    sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    debut_s = sem["dateord"]
    fin_s = ndb.DateDMYtoISO(sem["date_fin"])
    r = []
    for s in etud["sems"]:
        if s["formsemestre_id"] != formsemestre_id:
            debut = s["dateord"]
            fin = ndb.DateDMYtoISO(s["date_fin"])
            if debut < fin_s and fin > debut_s:
                r.append(s)  # intersection
    return r


def list_inscrits_ailleurs(formsemestre_id):
    """Liste des etudiants inscrits ailleurs en même temps que formsemestre_id.
    Pour chacun, donne la liste des semestres.
    { etudid : [ liste de sems ] }
    """
    formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

    etudids = nt.get_etudids()
    d = {}
    for etudid in etudids:
        d[etudid] = est_inscrit_ailleurs(etudid, formsemestre_id)
    return d


def formsemestre_inscrits_ailleurs(formsemestre_id):
    """Page listant les étudiants inscrits dans un autre semestre
    dont les dates recouvrent le semestre indiqué.
    """
    H = [
        html_sco_header.html_sem_header(
            "Inscriptions multiples parmi les étudiants du semestre ",
            init_qtip=True,
            javascripts=["js/etud_info.js"],
        )
    ]
    insd = list_inscrits_ailleurs(formsemestre_id)
    # liste ordonnée par nom
    etudlist = [Identite.get_etud(etudid) for etudid, sems in insd.items() if sems]
    etudlist.sort(key=lambda x: x.sort_key)
    if etudlist:
        H.append("<ul>")
        for etud in etudlist:
            H.append(
                f"""<li><a id="{etud.id}" class="discretelink etudinfo"
                href={
                    url_for(
                        "scolar.fiche_etud",
                        scodoc_dept=g.scodoc_dept,
                        etudid=etud.id,
                    )
                }
                >{etud.nomprenom}</a> :
                """
            )
            l = []
            for s in insd[etud.id]:
                l.append(
                    f"""<a class="discretelink" href="{
                        url_for('notes.formsemestre_status',
                            scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
                        )}">{s['titremois']}</a>"""
                )
            H.append(", ".join(l))
            H.append("</li>")
        H.append(
            f"""
        </ul>
        <p><b>Total: {len(etudlist)} étudiants concernés.</b></p>

        <p class="help">Ces étudiants sont inscrits dans le semestre sélectionné et aussi
        dans d'autres semestres qui se déroulent en même temps !
        </p>
        <p>
            <b>Sauf exception, cette situation est anormale:</b>
        </p>
        <ul>
            <li>vérifier que les dates des semestres se suivent <em>sans se chevaucher</em>
            </li>
            <li>ou bien si besoin désinscrire le(s) étudiant(s) de l'un des semestres
            (via leurs fiches individuelles).
            </li>
        </ul>
        """
        )
    else:
        H.append("""<p>Aucun étudiant en inscription multiple (c'est normal) !</p>""")
    return "\n".join(H) + html_sco_header.sco_footer()
