import datetime

from flask import g, request, render_template

from flask import abort, url_for

from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.decorators import (
    scodoc,
    permission_required,
)
from app.models import (
    FormSemestre,
    Identite,
    ScoDocSiteConfig,
    Assiduite,
    Departement,
    FormSemestreInscription,
)
from app.views import assiduites_bp as bp
from app.views import ScoData

# ---------------
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_groups_view
from app.scodoc import sco_etud
from app.scodoc import sco_find_etud
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc import sco_assiduites as scass

from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids


CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS

# --- UTILS ---


class HTMLElement:
    """"""


class HTMLElement:
    """Représentation d'un HTMLElement version Python"""

    def __init__(self, tag: str, *attr, **kattr) -> None:
        self.tag: str = tag
        self.children: list[HTMLElement] = []
        self.self_close: bool = kattr.get("self_close", False)
        self.text_content: str = kattr.get("text_content", "")
        self.key_attributes: dict[str, any] = kattr
        self.attributes: list[str] = list(attr)

    def add(self, *child: HTMLElement) -> None:
        """add child element to self"""
        for kid in child:
            self.children.append(kid)

    def remove(self, child: HTMLElement) -> None:
        """Remove child element from self"""
        if child in self.children:
            self.children.remove(child)

    def __str__(self) -> str:
        attr: list[str] = self.attributes

        for att, val in self.key_attributes.items():
            if att in ("self_close", "text_content"):
                continue

            if att != "cls":
                attr.append(f'{att}="{val}"')
            else:
                attr.append(f'class="{val}"')

        if not self.self_close:
            head: str = f"<{self.tag} {' '.join(attr)}>{self.text_content}"
            body: str = "\n".join(map(str, self.children))
            foot: str = f"</{self.tag}>"
            return head + body + foot
        return f"<{self.tag} {' '.join(attr)}/>"

    def __add__(self, other: str):
        return str(self) + other

    def __radd__(self, other: str):
        return other + str(self)


class HTMLStringElement(HTMLElement):
    """Utilisation d'une chaine de caracètres pour représenter un element"""

    def __init__(self, text: str) -> None:
        self.text: str = text
        HTMLElement.__init__(self, "textnode")

    def __str__(self) -> str:
        return self.text


class HTMLBuilder:
    def __init__(self, *content: HTMLElement or str) -> None:
        self.content: list[HTMLElement or str] = list(content)

    def add(self, *element: HTMLElement or str):
        self.content.extend(element)

    def remove(self, element: HTMLElement or str):
        if element in self.content:
            self.content.remove(element)

    def __str__(self) -> str:
        return "\n".join(map(str, self.content))

    def build(self) -> str:
        return self.__str__()


# --------------------------------------------------------------------
#
#   Assiduités (/ScoDoc/<dept>/Scolarite/Assiduites/...)
#
# --------------------------------------------------------------------


@bp.route("/")
@bp.route("/index_html")
@scodoc
@permission_required(Permission.ScoAbsChange)
def index_html():
    """Gestionnaire assiduités, page principale"""
    H = [
        html_sco_header.sco_header(
            page_title="Saisie des assiduités",
            javascripts=[
                "js/assiduites.js",
                "libjs/moment.new.min.js",
                "libjs/moment-timezone.js",
            ],
            cssstyles=[
                "css/assiduites.css",
            ],
        ),
        """<h2>Traitement des assiduités</h2>
        <p class="help">
        Pour saisir des assiduités ou consulter les états, il est recommandé par passer par 
        le semestre concerné (saisie par jour ou saisie différée).
        </p>
        """,
    ]
    H.append(
        """<p class="help">Pour signaler, annuler ou justifier une assiduité pour un seul étudiant, 
        choisissez d'abord le concerné:</p>"""
    )
    H.append(sco_find_etud.form_search_etud())
    # if current_user.has_permission(
    #     Permission.ScoAbsChange
    # ) and sco_preferences.get_preference("handle_billets_abs"):
    #     H.append(
    #         f"""
    #         <h2 style="margin-top: 30px;">Billets d'absence</h2>
    #         <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
    #         }">Traitement des billets d'absence en attente</a>
    #         </li></ul>
    #         """
    #     )

    H.append(
        render_template(
            "assiduites/pages/bilan_dept.j2",
            dept_id=g.scodoc_dept_id,
            annee=scu.annee_scolaire(),
        ),
    )
    H.append(html_sco_header.sco_footer())
    return "\n".join(H)


@bp.route("/SignaleAssiduiteEtud")
@scodoc
@permission_required(Permission.ScoAbsChange)
def signal_assiduites_etud():
    """
    signal_assiduites_etud Saisie de l'assiduité d'un étudiant

    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    header: str = html_sco_header.sco_header(
        page_title="Saisie Assiduités",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=[
            "css/assiduites.css",
        ],
    )

    # Gestion des horaires (journée, matin, soir)

    morning = get_time("assi_morning_time", "08:00:00")
    lunch = get_time("assi_lunch_time", "13:00:00")
    afternoon = get_time("assi_afternoon_time", "18:00:00")

    select = """
    <select class="dynaSelect">
        <option value="" selected> Non spécifié </option>
    </select>
    """

    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/signal_assiduites_etud.j2",
            sco=ScoData(etud),
            date=datetime.date.today().isoformat(),
            morning=morning,
            lunch=lunch,
            timeline=_timeline(),
            afternoon=afternoon,
            nonworkdays=_non_work_days(),
            forcer_module=sco_preferences.get_preference(
                "forcer_module", dept_id=g.scodoc_dept_id
            ),
            moduleimpl_select=_dynamic_module_selector(),
            diff=_differee(
                etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
                moduleimpl_select=select,
            ),
        ),
    ).build()


@bp.route("/ListeAssiduitesEtud")
@scodoc
@permission_required(Permission.ScoView)
def liste_assiduites_etud():
    """
    liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    header: str = html_sco_header.sco_header(
        page_title="Liste des assiduités",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/liste_assiduites.j2",
            sco=ScoData(etud),
            date=datetime.date.today().isoformat(),
        ),
    ).build()


@bp.route("/BilanEtud")
@scodoc
@permission_required(Permission.ScoView)
def bilan_etud():
    """
    bilan_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    header: str = html_sco_header.sco_header(
        page_title="Bilan de l'assiduité étudiante",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    date_debut: str = f"{scu.annee_scolaire()}-09-01"
    date_fin: str = f"{scu.annee_scolaire()+1}-06-30"

    assi_metric = {
        "H.": "heure",
        "J.": "journee",
        "1/2 J.": "demi",
    }.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/bilan_etud.j2",
            sco=ScoData(etud),
            date_debut=date_debut,
            date_fin=date_fin,
            assi_metric=assi_metric,
            assi_seuil=_get_seuil(),
        ),
    ).build()


@bp.route("/AjoutJustificatifEtud")
@scodoc
@permission_required(Permission.ScoAbsChange)
def ajout_justificatif_etud():
    """
    ajout_justificatif_etud : Affichage et création/modification des justificatifs de l'étudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    header: str = html_sco_header.sco_header(
        page_title="Justificatifs",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/ajout_justificatif.j2",
            sco=ScoData(etud),
        ),
    ).build()


@bp.route("/CalendrierAssiduitesEtud")
@scodoc
@permission_required(Permission.ScoView)
def calendrier_etud():
    """
    calendrier_etud : Affichage d'un calendrier des assiduités de l'étudiant
    Args:
        etudid (int): l'identifiant de l'étudiant

    Returns:
        str: l'html généré
    """

    etudid = request.args.get("etudid", -1)
    etud: Identite = Identite.query.get_or_404(etudid)
    if etud.dept_id != g.scodoc_dept_id:
        abort(404, "étudiant inexistant dans ce département")

    header: str = html_sco_header.sco_header(
        page_title="Calendrier des Assiduités",
        init_qtip=True,
        javascripts=[
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/calendrier.j2",
            sco=ScoData(etud),
            annee=scu.annee_scolaire(),
            nonworkdays=_non_work_days(),
            minitimeline=_mini_timeline(),
        ),
    ).build()


@bp.route("/SignalAssiduiteGr")
@scodoc
@permission_required(Permission.ScoAbsChange)
def signal_assiduites_group():
    """
    signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée

    Returns:
        str: l'html généré
    """
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    moduleimpl_id: int = request.args.get("moduleimpl_id")
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    group_ids: list[int] = request.args.get("group_ids", None)

    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    # Vérification du moduleimpl_id
    try:
        moduleimpl_id = int(moduleimpl_id)
    except (TypeError, ValueError):
        moduleimpl_id = None
    # Vérification du formsemestre_id
    try:
        formsemestre_id = int(formsemestre_id)
    except (TypeError, ValueError):
        formsemestre_id = None

    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
    )
    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    # --- URL DEFAULT ---

    base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"

    # --- Filtrage par formsemestre ---
    formsemestre_id = groups_infos.formsemestre_id

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    if formsemestre.dept_id != g.scodoc_dept_id:
        abort(404, "groupes inexistants dans ce département")

    require_module = sco_preferences.get_preference(
        "abs_require_module", formsemestre_id
    )

    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]

    # --- Vérification de la date ---

    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut:
        date = formsemestre.date_debut.isoformat()
    elif real_date > formsemestre.date_fin:
        date = formsemestre.date_fin.isoformat()

    # --- Restriction en fonction du moduleimpl_id ---
    if moduleimpl_id:
        mod_inscrits = {
            x["etudid"]
            for x in sco_moduleimpl.do_moduleimpl_inscription_list(
                moduleimpl_id=moduleimpl_id
            )
        }
        etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
        if etuds_inscrits_module:
            etuds = etuds_inscrits_module
        else:
            # Si aucun etudiant n'est inscrit au module choisi...
            moduleimpl_id = None

    # --- Génération de l'HTML ---
    sem = formsemestre.to_dict()

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    header: str = html_sco_header.sco_header(
        page_title="Saisie journalière des assiduités",
        init_qtip=True,
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            # Voir fonctionnement JS
            "js/etud_info.js",
            "js/abs_ajax.js",
            "js/groups_view.js",
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/signal_assiduites_group.j2",
            gr_tit=gr_tit,
            sem=sem["titre_num"],
            date=date,
            formsemestre_id=formsemestre_id,
            grp=sco_groups_view.menu_groups_choice(groups_infos),
            moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
            timeline=_timeline(),
            nonworkdays=_non_work_days(),
            formsemestre_date_debut=str(formsemestre.date_debut),
            formsemestre_date_fin=str(formsemestre.date_fin),
            forcer_module=sco_preferences.get_preference(
                "forcer_module",
                formsemestre_id=formsemestre_id,
                dept_id=g.scodoc_dept_id,
            ),
            defdem=_get_etuds_dem_def(formsemestre),
            readonly="false",
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/VisuAssiduiteGr")
@scodoc
@permission_required(Permission.ScoView)
def visu_assiduites_group():
    """
    signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée

    Returns:
        str: l'html généré
    """
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    moduleimpl_id: int = request.args.get("moduleimpl_id")
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    group_ids: list[int] = request.args.get("group_ids", None)

    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    # Vérification du moduleimpl_id
    try:
        moduleimpl_id = int(moduleimpl_id)
    except (TypeError, ValueError):
        moduleimpl_id = None
    # Vérification du formsemestre_id
    try:
        formsemestre_id = int(formsemestre_id)
    except (TypeError, ValueError):
        formsemestre_id = None

    groups_infos = sco_groups_view.DisplayedGroupsInfos(
        group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
    )
    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Saisie journalière des Assiduités")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    # --- URL DEFAULT ---

    base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"

    # --- Filtrage par formsemestre ---
    formsemestre_id = groups_infos.formsemestre_id

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    if formsemestre.dept_id != g.scodoc_dept_id:
        abort(404, "groupes inexistants dans ce département")

    require_module = sco_preferences.get_preference(
        "abs_require_module", formsemestre_id
    )

    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]

    # --- Vérification de la date ---

    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut:
        date = formsemestre.date_debut.isoformat()
    elif real_date > formsemestre.date_fin:
        date = formsemestre.date_fin.isoformat()

    # --- Restriction en fonction du moduleimpl_id ---
    if moduleimpl_id:
        mod_inscrits = {
            x["etudid"]
            for x in sco_moduleimpl.do_moduleimpl_inscription_list(
                moduleimpl_id=moduleimpl_id
            )
        }
        etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
        if etuds_inscrits_module:
            etuds = etuds_inscrits_module
        else:
            # Si aucun etudiant n'est inscrit au module choisi...
            moduleimpl_id = None

    # --- Génération de l'HTML ---
    sem = formsemestre.to_dict()

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    header: str = html_sco_header.sco_header(
        page_title="Saisie journalière des assiduités",
        init_qtip=True,
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            # Voir fonctionnement JS
            "js/etud_info.js",
            "js/abs_ajax.js",
            "js/groups_view.js",
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
        cssstyles=CSSSTYLES
        + [
            "css/assiduites.css",
        ],
    )

    return HTMLBuilder(
        header,
        _mini_timeline(),
        render_template(
            "assiduites/pages/signal_assiduites_group.j2",
            gr_tit=gr_tit,
            sem=sem["titre_num"],
            date=date,
            formsemestre_id=formsemestre_id,
            grp=sco_groups_view.menu_groups_choice(groups_infos),
            moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
            timeline=_timeline(),
            nonworkdays=_non_work_days(),
            formsemestre_date_debut=str(formsemestre.date_debut),
            formsemestre_date_fin=str(formsemestre.date_fin),
            forcer_module=sco_preferences.get_preference(
                "forcer_module",
                formsemestre_id=formsemestre_id,
                dept_id=g.scodoc_dept_id,
            ),
            defdem=_get_etuds_dem_def(formsemestre),
            readonly="true",
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/EtatAbsencesDate")
@scodoc
@permission_required(Permission.ScoView)
def get_etat_abs_date():
    evaluation = {
        "jour": request.args.get("jour"),
        "heure_debut": request.args.get("heure_debut"),
        "heure_fin": request.args.get("heure_fin"),
        "title": request.args.get("desc"),
    }
    date: str = evaluation["jour"]
    group_ids: list[int] = request.args.get("group_ids", None)
    etudiants: list[dict] = []

    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)

    etuds = [
        sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
        for m in groups_infos.members
    ]

    date_debut = scu.is_iso_formated(
        f"{evaluation['jour']}T{evaluation['heure_debut'].replace('h',':')}", True
    )
    date_fin = scu.is_iso_formated(
        f"{evaluation['jour']}T{evaluation['heure_fin'].replace('h',':')}", True
    )

    assiduites: Assiduite = Assiduite.query.filter(
        Assiduite.etudid.in_([e["etudid"] for e in etuds])
    )
    assiduites = scass.filter_by_date(
        assiduites, Assiduite, date_debut, date_fin, False
    )

    for etud in etuds:
        assi = assiduites.filter_by(etudid=etud["etudid"]).first()

        etat = ""
        if assi != None and assi.etat != 0:
            etat = scu.EtatAssiduite.inverse().get(assi.etat).name

        etudiant = {
            "nom": f'<a href="{url_for("assiduites.calendrier_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"])}"><font color="#A00000">{etud["nomprenom"]}</font></a>',
            "etat": etat,
        }

        etudiants.append(etudiant)

    etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))

    header: str = html_sco_header.sco_header(
        page_title=evaluation["title"],
        init_qtip=True,
    )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/etat_absence_date.j2",
            etudiants=etudiants,
            eval=evaluation,
        ),
        html_sco_header.sco_footer(),
    ).build()


@bp.route("/VisualisationAssiduitesGroupe")
@scodoc
@permission_required(Permission.ScoView)
def visu_assi_group():
    dates = {
        "debut": request.args.get("date_debut"),
        "fin": request.args.get("date_fin"),
    }
    fmt = request.args.get("format", "html")

    group_ids: list[int] = request.args.get("group_ids", None)
    etudiants: list[dict] = []

    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
    formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id)
    etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
    table: TableAssi = TableAssi(
        etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
    )

    if fmt.startswith("xls"):
        return scu.send_file(
            table.excel(),
            filename=f"assiduite-{groups_infos.groups_filename}",
            mime=scu.XLSX_MIMETYPE,
            suffix=scu.XLSX_SUFFIX,
        )

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = ""
        grp = ""
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    print()

    return render_template(
        "assiduites/pages/visu_assi.j2",
        tableau=table.html(),
        gr_tit=gr_tit,
        date_debut=dates["debut"],
        date_fin=dates["fin"],
        group_ids=request.args.get("group_ids", None),
        sco=ScoData(formsemestre=groups_infos.get_formsemestre()),
        title=f"Assiduité {grp} {groups_infos.groups_titles}",
    )


@bp.route("/SignalAssiduiteDifferee")
@scodoc
@permission_required(Permission.ScoAbsChange)
def signal_assiduites_diff():
    group_ids: list[int] = request.args.get("group_ids", None)
    formsemestre_id: int = request.args.get("formsemestre_id", -1)
    date: str = request.args.get("jour", datetime.date.today().isoformat())
    etudiants: list[dict] = []

    titre = None

    # Vérification du formsemestre_id
    try:
        formsemestre_id = int(formsemestre_id)
    except (TypeError, ValueError):
        formsemestre_id = None

    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)

    # --- Vérification de la date ---

    real_date = scu.is_iso_formated(date, True).date()

    if real_date < formsemestre.date_debut:
        date = formsemestre.date_debut.isoformat()
    elif real_date > formsemestre.date_fin:
        date = formsemestre.date_fin.isoformat()

    if group_ids is None:
        group_ids = []
    else:
        group_ids = group_ids.split(",")
        map(str, group_ids)

    groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)

    if not groups_infos.members:
        return (
            html_sco_header.sco_header(page_title="Assiduités Différées")
            + "<h3>Aucun étudiant ! </h3>"
            + html_sco_header.sco_footer()
        )

    etudiants.extend(
        [
            sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
            for m in groups_infos.members
        ]
    )

    etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))

    header: str = html_sco_header.sco_header(
        page_title="Assiduités Différées",
        init_qtip=True,
        cssstyles=[
            "css/assiduites.css",
        ],
        javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
        + [
            "js/assiduites.js",
            "libjs/moment.new.min.js",
            "libjs/moment-timezone.js",
        ],
    )

    sem = formsemestre.to_dict()

    if groups_infos.tous_les_etuds_du_sem:
        gr_tit = "en"
    else:
        if len(groups_infos.group_ids) > 1:
            grp = "des groupes"
        else:
            grp = "du groupe"
        gr_tit = (
            grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
        )

    return HTMLBuilder(
        header,
        render_template(
            "assiduites/pages/signal_assiduites_diff.j2",
            diff=_differee(
                etudiants=etudiants,
                moduleimpl_select=_module_selector(formsemestre),
                date=date,
                periode={
                    "deb": formsemestre.date_debut.isoformat(),
                    "fin": formsemestre.date_fin.isoformat(),
                },
            ),
            gr=gr_tit,
            sem=sem["titre_num"],
            defdem=_get_etuds_dem_def(formsemestre),
        ),
        html_sco_header.sco_footer(),
    ).build()


def _differee(
    etudiants, moduleimpl_select, date=None, periode=None, formsemestre_id=None
):
    if date is None:
        date = datetime.date.today().isoformat()

    forcer_module = sco_preferences.get_preference(
        "forcer_module",
        formsemestre_id=formsemestre_id,
        dept_id=g.scodoc_dept_id,
    )

    etat_def = sco_preferences.get_preference(
        "assi_etat_defaut",
        formsemestre_id=formsemestre_id,
        dept_id=g.scodoc_dept_id,
    )

    return render_template(
        "assiduites/widgets/differee.j2",
        etudiants=etudiants,
        etat_def=etat_def,
        forcer_module=forcer_module,
        moduleimpl_select=moduleimpl_select,
        date=date,
        periode=periode,
    )


def _module_selector(
    formsemestre: FormSemestre, moduleimpl_id: int = None
) -> HTMLElement:
    """
    _module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre

    Args:
        formsemestre (FormSemestre): Le formsemestre d'où les moduleimpls seront pris.

    Returns:
        str: La représentation str d'un HTMLSelectElement
    """

    ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

    modimpls_list: list[dict] = []
    ues = ntc.get_ues_stat_dict()
    for ue in ues:
        modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])

    selected = moduleimpl_id is not None

    modules = []

    for modimpl in modimpls_list:
        modname: str = (
            (modimpl["module"]["code"] or "")
            + " "
            + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or "")
        )
        modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})

    return render_template(
        "assiduites/widgets/moduleimpl_selector.j2", selected=selected, modules=modules
    )


def _dynamic_module_selector():
    return render_template("assiduites/widgets/moduleimpl_dynamic_selector.j2")


def _timeline(formsemestre_id=None) -> HTMLElement:
    return render_template(
        "assiduites/widgets/timeline.j2",
        t_start=get_time("assi_morning_time", "08:00:00"),
        t_end=get_time("assi_afternoon_time", "18:00:00"),
        tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
        periode_defaut=sco_preferences.get_preference(
            "periode_defaut", formsemestre_id
        ),
    )


def _mini_timeline() -> HTMLElement:
    return render_template(
        "assiduites/widgets/minitimeline.j2",
        t_start=get_time("assi_morning_time", "08:00:00"),
        t_end=get_time("assi_afternoon_time", "18:00:00"),
    )


def _non_work_days():
    non_travail = sco_preferences.get_preference("non_travail", None)
    non_travail = non_travail.replace(" ", "").split(",")
    return ",".join([f"'{i.lower()}'" for i in non_travail])


def _str_to_num(string: str):
    parts = [*map(float, string.split(":"))]
    hour = parts[0]
    minutes = round(parts[1] / 60 * 4) / 4
    return hour + minutes


def get_time(label: str, default: str):
    return _str_to_num(ScoDocSiteConfig.get(label, default))


def _get_seuil():
    return sco_preferences.get_preference("assi_seuil", dept_id=g.scodoc_dept_id)


def _get_etuds_dem_def(formsemestre):
    etuds_dem_def = [
        (f.etudid, f.etat)
        for f in FormSemestreInscription.query.filter(
            FormSemestreInscription.formsemestre_id == formsemestre.id,
            FormSemestreInscription.etat != "I",
        ).all()
    ]

    template: str = '"£" : "$",'

    json_str: str = "{"

    for etud in etuds_dem_def:
        json_str += template.replace("£", str(etud[0])).replace("$", etud[1])

    if json_str != "{":
        json_str = json_str[:-1]

    return json_str + "}"