##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 API : Assiduités
"""
from datetime import datetime

from flask import g, request
from flask_json import as_json
from flask_login import current_user, login_required
from flask_sqlalchemy.query import Query

from app import db, log, set_sco_dept
import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu
from app.api import api_bp as bp
from app.api import api_web_bp, get_model_api_object, tools
from app.decorators import permission_required, scodoc
from app.models import (
    Assiduite,
    FormSemestre,
    Identite,
    ModuleImpl,
    Scolog,
)
from app.models.assiduites import (
    get_assiduites_justif,
    get_justifs_from_date,
    get_formsemestre_from_data,
)
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error


@bp.route("/assiduite/<int:assiduite_id>")
@api_web_bp.route("/assiduite/<int:assiduite_id>")
@scodoc
@permission_required(Permission.ScoView)
@as_json
def assiduite(assiduite_id: int = None):
    """Retourne un objet assiduité à partir de son id

    Exemple de résultat:
    {
        "assiduite_id": 1,
        "etudid": 2,
        "moduleimpl_id": 3,
        "date_debut": "2022-10-31T08:00+01:00",
        "date_fin": "2022-10-31T10:00+01:00",
        "etat": "retard",
        "desc": "une description",
        "user_id: 1 or null,
        "user_name" : login scodoc or null
        "user_nom_complet": "Marie Dupont"
        "est_just": False or True,
    }
    """

    return get_model_api_object(Assiduite, assiduite_id, Identite)


@bp.route("/assiduite/<int:assiduite_id>/justificatifs", defaults={"long": False})
@api_web_bp.route(
    "/assiduite/<int:assiduite_id>/justificatifs", defaults={"long": False}
)
@bp.route("/assiduite/<int:assiduite_id>/justificatifs/long", defaults={"long": True})
@api_web_bp.route(
    "/assiduite/<int:assiduite_id>/justificatifs/long", defaults={"long": True}
)
@scodoc
@permission_required(Permission.ScoView)
@as_json
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
    """Retourne la liste des justificatifs qui justifie cette assiduitée

    Exemple de résultat:
    [
        1,
        2,
        3,
        ...
    ]
    """

    return get_assiduites_justif(assiduite_id, long)


# etudid
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
@bp.route("/assiduites/etudid/<int:etudid>/count", defaults={"with_query": False})
@api_web_bp.route(
    "/assiduites/etudid/<int:etudid>/count", defaults={"with_query": False}
)
@bp.route("/assiduites/etudid/<int:etudid>/count/query", defaults={"with_query": True})
@api_web_bp.route(
    "/assiduites/etudid/<int:etudid>/count/query", defaults={"with_query": True}
)
# nip
@bp.route("/assiduites/nip/<nip>/count", defaults={"with_query": False})
@api_web_bp.route("/assiduites/nip/<nip>/count", defaults={"with_query": False})
@bp.route("/assiduites/nip/<nip>/count/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/nip/<nip>/count/query", defaults={"with_query": True})
# ine
@bp.route("/assiduites/ine/<ine>/count", defaults={"with_query": False})
@api_web_bp.route("/assiduites/ine/<ine>/count", defaults={"with_query": False})
@bp.route("/assiduites/ine/<ine>/count/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/ine/<ine>/count/query", defaults={"with_query": True})
#
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def count_assiduites(
    etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
):
    """
    Retourne le nombre d'assiduités d'un étudiant
    chemin : /assiduites/<int:etudid>/count

    Un filtrage peut être donné avec une query
    chemin : /assiduites/<int:etudid>/count/query?

    Les différents filtres :
        Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
            query?type=(journee, demi, heure) -> une seule valeur parmis les trois
            ex: .../query?type=heure
            Comportement par défaut : compte le nombre d'assiduité enregistrée

        Etat (etat de l'étudiant -> absent, present ou retard):
            query?etat=[- liste des états séparé par une virgule -]
            ex: .../query?etat=present,retard
        Date debut
        (date de début de l'assiduité, sont affichés les assiduités
        dont la date de début est supérieur ou égale à la valeur donnée):
            query?date_debut=[- date au format iso -]
            ex: query?date_debut=2022-11-03T08:00+01:00
        Date fin
        (date de fin de l'assiduité, sont affichés les assiduités
        dont la date de fin est inférieure ou égale à la valeur donnée):
            query?date_fin=[- date au format iso -]
            ex: query?date_fin=2022-11-03T10:00+01:00
        Moduleimpl_id (l'id du module concerné par l'assiduité):
            query?moduleimpl_id=[- int ou vide -]
            ex: query?moduleimpl_id=1234
                query?moduleimpl_od=
        Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
            query?formsemestre_id=[int]
            ex query?formsemestre_id=3
        user_id (l'id de l'auteur de l'assiduité)
            query?user_id=[int]
            ex query?user_id=3
        est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
            query?est_just=[bool]
            query?est_just=f
            query?est_just=t




    """

    # Récupération de l'étudiant
    etud: Identite = tools.get_etud(etudid, nip, ine)
    # Vérification que l'étudiant existe
    if etud is None:
        return json_error(
            404,
            message="étudiant inconnu",
        )
    set_sco_dept(etud.departement.acronym)

    # Les filtres qui seront appliqués au comptage (type, date, etudid...)
    filtered: dict[str, object] = {}
    # la métrique du comptage (all, demi, heure, journee)
    metric: str = "all"

    # Si la requête a des paramètres
    if with_query:
        metric, filtered = _count_manager(request)

    return scass.get_assiduites_stats(
        assiduites=etud.assiduites, metric=metric, filtered=filtered
    )


# etudid
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
@bp.route("/assiduites/etudid/<int:etudid>", defaults={"with_query": False})
@api_web_bp.route("/assiduites/etudid/<int:etudid>", defaults={"with_query": False})
@bp.route("/assiduites/etudid/<int:etudid>/query", defaults={"with_query": True})
@api_web_bp.route(
    "/assiduites/etudid/<int:etudid>/query", defaults={"with_query": True}
)
# nip
@bp.route("/assiduites/nip/<nip>", defaults={"with_query": False})
@api_web_bp.route("/assiduites/nip/<nip>", defaults={"with_query": False})
@bp.route("/assiduites/nip/<nip>/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/nip/<nip>/query", defaults={"with_query": True})
# ine
@bp.route("/assiduites/ine/<ine>", defaults={"with_query": False})
@api_web_bp.route("/assiduites/ine/<ine>", defaults={"with_query": False})
@bp.route("/assiduites/ine/<ine>/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/ine/<ine>/query", defaults={"with_query": True})
#
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
    """
    Retourne toutes les assiduités d'un étudiant
    chemin : /assiduites/<int:etudid>

    Un filtrage peut être donné avec une query
    chemin : /assiduites/<int:etudid>/query?

    Les différents filtres :
        Etat (etat de l'étudiant -> absent, present ou retard):
            query?etat=[- liste des états séparé par une virgule -]
            ex: .../query?etat=present,retard
        Date debut
        (date de début de l'assiduité, sont affichés les assiduités
        dont la date de début est supérieur ou égale à la valeur donnée):
            query?date_debut=[- date au format iso -]
            ex: query?date_debut=2022-11-03T08:00+01:00
        Date fin
        (date de fin de l'assiduité, sont affichés les assiduités
        dont la date de fin est inférieure ou égale à la valeur donnée):
            query?date_fin=[- date au format iso -]
            ex: query?date_fin=2022-11-03T10:00+01:00
        Moduleimpl_id (l'id du module concerné par l'assiduité):
            query?moduleimpl_id=[- int ou vide -]
            ex: query?moduleimpl_id=1234
                query?moduleimpl_od=
        Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
            query?formsemstre_id=[int]
            ex query?formsemestre_id=3
        user_id (l'id de l'auteur de l'assiduité)
            query?user_id=[int]
            ex query?user_id=3
        est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
            query?est_just=[bool]
            query?est_just=f
            query?est_just=t


    """

    # Récupération de l'étudiant
    etud: Identite = tools.get_etud(etudid, nip, ine)

    if etud is None:
        return json_error(
            404,
            message="étudiant inconnu",
        )
    # Récupération des assiduités de l'étudiant
    assiduites_query: Query = etud.assiduites

    # Filtrage des assiduités en fonction des paramètres de la requête
    if with_query:
        assiduites_query = _filter_manager(request, assiduites_query)

    # Préparation de la réponse json

    data_set: list[dict] = []

    for ass in assiduites_query.all():
        # conversion Assiduite -> Dict
        data = ass.to_dict(format_api=True)
        # Ajout des justificatifs (ou non dépendamment de la requête)
        data = _with_justifs(data)
        # Ajout de l'assiduité dans la liste de retour
        data_set.append(data)

    return data_set


@bp.route("/assiduites/group/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def assiduites_group(with_query: bool = False):
    """
    Retourne toutes les assiduités d'un groupe d'étudiants
    chemin : /assiduites/group/query?etudids=1,2,3

    Un filtrage peut être donné avec une query
    chemin : /assiduites/group/query?etudids=1,2,3

    Les différents filtres :
        Etat (etat de l'étudiant -> absent, present ou retard):
            query?etat=[- liste des états séparé par une virgule -]
            ex: .../query?etat=present,retard
        Date debut
        (date de début de l'assiduité, sont affichés les assiduités
        dont la date de début est supérieur ou égale à la valeur donnée):
            query?date_debut=[- date au format iso -]
            ex: query?date_debut=2022-11-03T08:00+01:00
        Date fin
        (date de fin de l'assiduité, sont affichés les assiduités
        dont la date de fin est inférieure ou égale à la valeur donnée):
            query?date_fin=[- date au format iso -]
            ex: query?date_fin=2022-11-03T10:00+01:00
        Moduleimpl_id (l'id du module concerné par l'assiduité):
            query?moduleimpl_id=[- int ou vide -]
            ex: query?moduleimpl_id=1234
                query?moduleimpl_od=
        Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
            query?formsemstre_id=[int]
            ex query?formsemestre_id=3
        user_id (l'id de l'auteur de l'assiduité)
            query?user_id=[int]
            ex query?user_id=3
        est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
            query?est_just=[bool]
            query?est_just=f
            query?est_just=t


    """

    # Récupération des étudiants dans la requête
    etuds = request.args.get("etudids", "")
    etuds = etuds.split(",")
    try:
        etuds = [int(etu) for etu in etuds]
    except ValueError:
        return json_error(404, "Le champ etudids n'est pas correctement formé")

    # Vérification que tous les étudiants sont du même département
    query = Identite.query.filter(Identite.id.in_(etuds))
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)

    if len(etuds) != query.count() or len(etuds) == 0:
        return json_error(
            404,
            "Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
        )

    # Récupération de toutes les assiduités liés aux étudiants
    assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))

    # Filtrage des assiduités en fonction des filtres passés dans la requête
    if with_query:
        assiduites_query = _filter_manager(request, assiduites_query)

    # Préparation de retour json
    # Dict représentant chaque étudiant avec sa liste d'assiduité
    data_set: dict[list[dict]] = {str(key): [] for key in etuds}
    for ass in assiduites_query.all():
        data = ass.to_dict(format_api=True)
        data = _with_justifs(data)
        # Ajout de l'assiduité dans la liste du bon étudiant
        data_set.get(str(data["etudid"])).append(data)
    return data_set


@bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@api_web_bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/query",
    defaults={"with_query": True},
)
@api_web_bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/query",
    defaults={"with_query": True},
)
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
    """Retourne toutes les assiduités du formsemestre"""

    # Récupération du formsemestre à partir du formsemestre_id
    formsemestre: FormSemestre = None
    formsemestre_id = int(formsemestre_id)
    formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
    if formsemestre is None:
        return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")

    # Récupération des assiduités du formsemestre
    assiduites_query = scass.filter_by_formsemestre(
        Assiduite.query, Assiduite, formsemestre
    )
    # Filtrage en fonction des paramètres de la requête
    if with_query:
        assiduites_query = _filter_manager(request, assiduites_query)

    # Préparation du retour JSON
    data_set: list[dict] = []
    for ass in assiduites_query.all():
        data = ass.to_dict(format_api=True)
        data = _with_justifs(data)
        data_set.append(data)

    return data_set


@bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/count",
    defaults={"with_query": False},
)
@api_web_bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/count",
    defaults={"with_query": False},
)
@bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/count/query",
    defaults={"with_query": True},
)
@api_web_bp.route(
    "/assiduites/formsemestre/<int:formsemestre_id>/count/query",
    defaults={"with_query": True},
)
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def count_assiduites_formsemestre(
    formsemestre_id: int = None, with_query: bool = False
):
    """Comptage des assiduités du formsemestre"""

    # Récupération du formsemestre à partir du formsemestre_id
    formsemestre: FormSemestre = None
    formsemestre_id = int(formsemestre_id)
    formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
    if formsemestre is None:
        return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")

    set_sco_dept(formsemestre.departement.acronym)

    # Récupération des étudiants du formsemestre
    etuds = formsemestre.etuds.all()
    etuds_id = [etud.id for etud in etuds]

    # Récupération des assiduités des étudiants du semestre
    assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
    # Filtrage des assiduités en fonction des dates du semestre
    assiduites_query = scass.filter_by_formsemestre(
        assiduites_query, Assiduite, formsemestre
    )

    # Gestion de la métrique de comptage (all,demi,heure,journee)
    metric: str = "all"
    # Gestion du filtre (en fonction des paramètres de la requête)
    filtered: dict = {}
    if with_query:
        metric, filtered = _count_manager(request)

    return scass.get_assiduites_stats(assiduites_query, metric, filtered)


# etudid
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
@bp.route("/assiduite/etudid/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/assiduite/etudid/<int:etudid>/create", methods=["POST"])
# nip
@bp.route("/assiduite/nip/<nip>/create", methods=["POST"])
@api_web_bp.route("/assiduite/nip/<nip>/create", methods=["POST"])
# ine
@bp.route("/assiduite/ine/<ine>/create", methods=["POST"])
@api_web_bp.route("/assiduite/ine/<ine>/create", methods=["POST"])
#
@scodoc
@as_json
@login_required
@permission_required(Permission.AbsChange)
def assiduite_create(etudid: int = None, nip=None, ine=None):
    """
    Enregistrement d'assiduités pour un étudiant (etudid)
    La requête doit avoir un content type "application/json":
    [
        {
            "date_debut": str,
            "date_fin": str,
            "etat": str,
        },
        {
            "date_debut": str,
            "date_fin": str,
            "etat": str,
            "moduleimpl_id": int,
            "desc":str,
        }
        ...
    ]

    """
    # Récupération de l'étudiant
    etud: Identite = tools.get_etud(etudid, nip, ine)
    if etud is None:
        return json_error(
            404,
            message="étudiant inconnu",
        )
    # Mise à jour du "g.scodoc_dept" si route sans dept
    if g.scodoc_dept is None and etud.dept_id is not None:
        # route sans département
        set_sco_dept(etud.departement.acronym)

    # Récupération de la liste des assiduités à créer
    create_list: list[object] = request.get_json(force=True)

    # Vérification que c'est bien une liste
    if not isinstance(create_list, list):
        return json_error(404, "Le contenu envoyé n'est pas une liste")

    # Préparation du retour

    errors: list[dict[str, object]] = []
    success: list[dict[str, object]] = []

    # Pour chaque objet de la liste,
    # on récupère son indice et l'objet
    for i, data in enumerate(create_list):
        # On créé l'assiduité
        # 200 + obj si réussi
        # 404 + message d'erreur si non réussi
        code, obj = _create_one(data, etud)
        if code == 404:
            errors.append({"indice": i, "message": obj})
        else:
            success.append({"indice": i, "message": obj})
            scass.simple_invalidate_cache(data, etud.id)

    db.session.commit()

    return {"errors": errors, "success": success}


@bp.route("/assiduites/create", methods=["POST"])
@api_web_bp.route("/assiduites/create", methods=["POST"])
@scodoc
@as_json
@login_required
@permission_required(Permission.AbsChange)
def assiduites_create():
    """
    Création d'une assiduité ou plusieurs assiduites
    La requête doit avoir un content type "application/json":
    [
        {
            "date_debut": str,
            "date_fin": str,
            "etat": str,
            "etudid":int,
        },
        {
            "date_debut": str,
            "date_fin": str,
            "etat": str,
            "etudid":int,

            "moduleimpl_id": int,
            "desc":str,
        }
        ...
    ]

    """

    create_list: list[object] = request.get_json(force=True)
    if not isinstance(create_list, list):
        return json_error(404, "Le contenu envoyé n'est pas une liste")

    errors: list = []
    success: list = []
    for i, data in enumerate(create_list):
        etud: Identite = Identite.query.filter_by(id=data["etudid"]).first()
        if etud is None:
            errors.append({"indice": i, "message": "Cet étudiant n'existe pas."})
            continue
        if g.scodoc_dept is None and etud.dept_id is not None:
            # route sans département
            set_sco_dept(etud.departement.acronym)

        code, obj = _create_one(data, etud)
        if code == 404:
            errors.append({"indice": i, "message": obj})
        else:
            success.append({"indice": i, "message": obj})
            scass.simple_invalidate_cache(data)

    return {"errors": errors, "success": success}


def _create_one(
    data: dict,
    etud: Identite,
) -> tuple[int, object]:
    """
    Création d'une assiduité à partir d'un dict

    Cette fonction vérifie les données du dict (qui vient du JSON API)

    Puis crée l'assiduité si la représentation est valide.

    Args:
        data (dict): représentation json d'une assiduité
        etud (Identite): l'étudiant concerné par l'assiduité

    Returns:
        tuple[int, object]: code, objet
            code : 200 si réussi 404 sinon
            objet : dict{assiduite_id:?} si réussi str"message d'erreur" sinon
    """

    errors: list[str] = []

    # -- vérifications de l'objet json --
    # cas 1 : ETAT
    etat: str = data.get("etat", None)
    if etat is None:
        errors.append("param 'etat': manquant")
    elif not scu.EtatAssiduite.contains(etat):
        errors.append("param 'etat': invalide")

    etat: scu.EtatAssiduite = scu.EtatAssiduite.get(etat)

    # cas 2 : date_debut
    date_debut: str = data.get("date_debut", None)
    if date_debut is None:
        errors.append("param 'date_debut': manquant")
    # Conversion de la chaine de caractère en datetime (tz ou non)
    deb: datetime = scu.is_iso_formated(date_debut, convert=True)
    # si chaine invalide
    if deb is None:
        errors.append("param 'date_debut': format invalide")
    # Si datetime sans timezone
    elif deb.tzinfo is None:
        # Mise à jour de la timezone avec celle du serveur
        deb: datetime = scu.localize_datetime(deb)

    # cas 3 : date_fin (Même fonctionnement ^ )
    date_fin: str = data.get("date_fin", None)
    if date_fin is None:
        errors.append("param 'date_fin': manquant")
    fin: datetime = scu.is_iso_formated(date_fin, convert=True)
    if fin is None:
        errors.append("param 'date_fin': format invalide")
    elif fin.tzinfo is None:
        fin: datetime = scu.localize_datetime(fin)

    # cas 4 : desc
    desc: str = data.get("desc", None)

    # cas 5 : external data
    external_data: dict = data.get("external_data", None)
    if external_data is not None:
        if not isinstance(external_data, dict):
            errors.append("param 'external_data' : n'est pas un objet JSON")

    # cas 6 : moduleimpl_id

    # On récupère le moduleimpl
    moduleimpl_id = data.get("moduleimpl_id", False)
    moduleimpl: ModuleImpl = None

    # On vérifie si le moduleimpl existe (uniquement s'il a été renseigné)
    if moduleimpl_id not in [False, None, "", "-1"]:
        # Si le moduleimpl n'est pas "autre" alors on vérifie si l'id est valide
        if moduleimpl_id != "autre":
            try:
                moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
            except ValueError:
                moduleimpl = None
            if moduleimpl is None:
                errors.append("param 'moduleimpl_id': invalide")
        else:
            # Sinon on met à none le moduleimpl
            # et on ajoute dans external data
            # le module "autre"
            moduleimpl_id = None
            external_data: dict = external_data if external_data is not None else {}
            external_data["module"] = "Autre"

    # Si il y a des erreurs alors on ne crée pas l'assiduité et on renvoie les erreurs
    if errors:
        # Construit une chaine de caractère avec les erreurs séparées par `,`
        err: str = ", ".join(errors)
        # 404 représente le code d'erreur et err la chaine nouvellement créée
        return (404, err)

    # SI TOUT EST OK
    try:
        # On essaye de créer l'assiduité
        nouv_assiduite: Assiduite = Assiduite.create_assiduite(
            date_debut=deb,
            date_fin=fin,
            etat=etat,
            etud=etud,
            moduleimpl=moduleimpl,
            description=desc,
            user_id=current_user.id,
            external_data=external_data,
            notify_mail=True,
        )

        # create_assiduite générera des ScoValueError si jamais il y a un autre problème
        # - Etudiant non inscrit dans le module
        # - module obligatoire
        # - Assiduité conflictuelles

        # Si tout s'est bien passé on ajoute l'assiduité à la session
        # et on retourne un code 200 avec un objet possèdant l'assiduite_id
        db.session.add(nouv_assiduite)
        db.session.commit()
        return (200, {"assiduite_id": nouv_assiduite.id})
    except ScoValueError as excp:
        # ici on utilise pas json_error car on doit renvoyer status, message
        # Ici json_error ne peut être utilisé car il terminerai le processus de création
        # Cela voudrait dire qu'une seule erreur dans une assiduité imposerait de
        # tout refaire à partir de l'erreur.

        # renvoit un code 404 et le message d'erreur de la ScoValueError
        return 404, excp.args[0]


@bp.route("/assiduite/delete", methods=["POST"])
@api_web_bp.route("/assiduite/delete", methods=["POST"])
@login_required
@scodoc
@as_json
@permission_required(Permission.AbsChange)
def assiduite_delete():
    """
    Suppression d'une assiduité à partir de son id

    Forme des données envoyées :

    [
        <assiduite_id:int>,
        ...
    ]


    """
    # Récupération des ids envoyés dans la liste
    assiduites_list: list[int] = request.get_json(force=True)
    if not isinstance(assiduites_list, list):
        return json_error(404, "Le contenu envoyé n'est pas une liste")

    # Préparation du retour json
    output = {"errors": [], "success": []}

    # Pour chaque assiduite_id on essaye de supprimer l'assiduité
    for i, assiduite_id in enumerate(assiduites_list):
        # De la même façon que "_create_one"
        # Ici le code est soit 200 si réussi ou 404 si raté
        # Le message est le message d'erreur si erreur
        code, msg = _delete_one(assiduite_id)
        if code == 404:
            output["errors"].append({"indice": i, "message": msg})
        else:
            output["success"].append({"indice": i, "message": "OK"})

    db.session.commit()
    return output


def _delete_one(assiduite_id: int) -> tuple[int, str]:
    """
    _delete_singular Supprime une assiduité à partir de son id

    Args:
        assiduite_id (int): l'identifiant de l'assiduité
    Returns:
        tuple[int, str]: code, message
            code : 200 si réussi, 404 sinon
            message : OK si réussi, le message d'erreur sinon
    """
    assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
    if assiduite_unique is None:
        # Ici json_error ne peut être utilisé car il terminerai le processus de création
        # Cela voudrait dire qu'une seule erreur d'id imposerait de
        # tout refaire à partir de l'erreur.
        return 404, "Assiduite non existante"

    # Mise à jour du g.scodoc_dept si la route est sans département
    if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
        # route sans département
        set_sco_dept(assiduite_unique.etudiant.departement.acronym)

    # Récupération de la version dict de l'assiduité
    # Pour invalider le cache
    assi_dict = assiduite_unique.to_dict()

    # Suppression de l'assiduité et LOG
    log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}")
    Scolog.logdb(
        method="delete_assiduite",
        etudid=assiduite_unique.etudiant.id,
        msg=f"assiduité: {assiduite_unique}",
    )
    db.session.delete(assiduite_unique)
    # Invalidation du cache
    scass.simple_invalidate_cache(assi_dict)
    return 200, "OK"


@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
@login_required
@scodoc
@as_json
@permission_required(Permission.AbsChange)
def assiduite_edit(assiduite_id: int):
    """
    Edition d'une assiduité à partir de son id
    La requête doit avoir un content type "application/json":
    {
        "etat"?: str,
        "moduleimpl_id"?: int
        "desc"?: str
        "est_just"?: bool
    }
    """

    # Récupération de l'assiduité à modifier
    assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
    if assiduite_unique is None:
        return json_error(404, "Assiduité non existante")
    # Récupération des valeurs à modifier
    data = request.get_json(force=True)

    # Code 200 si modification réussie
    # Code 404 si raté + message d'erreur
    code, obj = _edit_one(assiduite_unique, data)

    if code == 404:
        return json_error(404, obj)

    # Mise à jour de l'assiduité et LOG
    log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}")
    Scolog.logdb(
        "assiduite_edit",
        assiduite_unique.etudiant.id,
        msg=f"assiduite: modif {assiduite_unique}",
    )
    db.session.commit()
    scass.simple_invalidate_cache(assiduite_unique.to_dict())

    return {"OK": True}


@bp.route("/assiduites/edit", methods=["POST"])
@api_web_bp.route("/assiduites/edit", methods=["POST"])
@login_required
@scodoc
@as_json
@permission_required(Permission.AbsChange)
def assiduites_edit():
    """
    Edition de plusieurs assiduités
    La requête doit avoir un content type "application/json":
    [
        {
            "assiduite_id" : int,
            "etat"?: str,
            "moduleimpl_id"?: int
            "desc"?: str
            "est_just"?: bool
        }
    ]
    """
    edit_list: list[object] = request.get_json(force=True)

    if not isinstance(edit_list, list):
        return json_error(404, "Le contenu envoyé n'est pas une liste")

    errors: list[dict] = []
    success: list[dict] = []
    for i, data in enumerate(edit_list):
        assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first()
        if assi is None:
            errors.append(
                {
                    "indice": i,
                    "message": f"assiduité {data['assiduite_id']} n'existe pas.",
                }
            )
            continue

        code, obj = _edit_one(assi, data)
        obj_retour = {
            "indice": i,
            "message": obj,
        }
        if code == 404:
            errors.append(obj_retour)
        else:
            success.append(obj_retour)

    db.session.commit()

    return {"errors": errors, "success": success}


def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
    """
    _edit_singular Modifie une assiduité à partir de données JSON

    Args:
        assiduite_unique (Assiduite): l'assiduité à modifier
        data (dict): les nouvelles données

    Returns:
        tuple[int,str]: code, message
            code : 200 si réussi, 404 sinon
            message : OK si réussi, message d'erreur sinon
    """

    # Mise à jour du g.scodoc_dept en cas de route sans département
    if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
        # route sans département
        set_sco_dept(assiduite_unique.etudiant.departement.acronym)

    errors: list[str] = []

    # Vérifications de data

    # Cas 1 : Etat
    if data.get("etat") is not None:
        etat: scu.EtatAssiduite = scu.EtatAssiduite.get(data.get("etat"))
        if etat is None:
            errors.append("param 'etat': invalide")
        else:
            # Mise à jour de l'état
            assiduite_unique.etat = etat

    # Cas 2 : external_data
    external_data: dict = data.get("external_data")
    if external_data is not None:
        if not isinstance(external_data, dict):
            errors.append("param 'external_data' : n'est pas un objet JSON")
        else:
            # Mise à jour de l'external data
            assiduite_unique.external_data = external_data

    # Cas 3 : Moduleimpl_id
    moduleimpl_id = data.get("moduleimpl_id", False)
    moduleimpl: ModuleImpl = None

    # False si on modifie pas le moduleimpl
    if moduleimpl_id is not False:
        # Si le module n'est pas nul
        if moduleimpl_id not in [None, "", "-1"]:
            # Gestion du module Autre
            if moduleimpl_id == "autre":
                # module autre = moduleimpl_id:None + external_data["module"]:"Autre"
                assiduite_unique.moduleimpl_id = None
                external_data: dict = (
                    external_data
                    if external_data is not None and isinstance(external_data, dict)
                    else assiduite_unique.external_data
                )
                external_data: dict = external_data if external_data is not None else {}
                external_data["module"] = "Autre"
                assiduite_unique.external_data = external_data
            else:
                # Vérification de l'id et récupération de l'objet ModuleImpl
                try:
                    moduleimpl = ModuleImpl.query.filter_by(
                        id=int(moduleimpl_id)
                    ).first()
                except ValueError:
                    moduleimpl = None

                if moduleimpl is None:
                    errors.append("param 'moduleimpl_id': invalide")
                else:
                    if not moduleimpl.est_inscrit(assiduite_unique.etudiant):
                        errors.append("param 'moduleimpl_id': etud non inscrit")
                    else:
                        # Mise à jour du moduleimpl
                        assiduite_unique.moduleimpl_id = moduleimpl_id
        else:
            # Vérification du force module en cas de modification du moduleimpl en moduleimpl nul
            # Récupération du formsemestre lié à l'assiduité
            formsemestre: FormSemestre = get_formsemestre_from_data(
                assiduite_unique.to_dict()
            )
            force: bool

            if formsemestre:
                force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id)
            else:
                force = scu.is_assiduites_module_forced(
                    dept_id=assiduite_unique.etudiant.dept_id
                )

            external_data = (
                external_data
                if external_data is not None and isinstance(external_data, dict)
                else assiduite_unique.external_data
            )

            if force and not (
                external_data is not None and external_data.get("module", False) != ""
            ):
                errors.append(
                    "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
                )

    # Cas 4 : desc
    desc: str = data.get("desc", False)
    if desc is not False:
        assiduite_unique.description = desc

    # Cas 5 : est_just
    if assiduite_unique.etat == scu.EtatAssiduite.PRESENT:
        assiduite_unique.est_just = False
    else:
        assiduite_unique.est_just = (
            len(
                get_justifs_from_date(
                    assiduite_unique.etudiant.id,
                    assiduite_unique.date_debut,
                    assiduite_unique.date_fin,
                    valid=True,
                )
            )
            > 0
        )

    if errors:
        # Retour des erreurs en une seule chaîne séparée par des `,`
        err: str = ", ".join(errors)
        return (404, err)

    # Mise à jour de l'assiduité et LOG
    log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}")
    Scolog.logdb(
        "assiduite_edit",
        assiduite_unique.etudiant.id,
        msg=f"assiduite: modif {assiduite_unique}",
    )
    db.session.add(assiduite_unique)
    scass.simple_invalidate_cache(assiduite_unique.to_dict())

    return (200, "OK")


# -- Utils --


def _count_manager(requested) -> tuple[str, dict]:
    """
    Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
    """
    filtered: dict = {}
    # cas 1 : etat assiduite
    etat = requested.args.get("etat")
    if etat is not None:
        filtered["etat"] = etat

    # cas 2 : date de début
    deb = requested.args.get("date_debut", "").replace(" ", "+")
    deb: datetime = scu.is_iso_formated(deb, True)
    if deb is not None:
        filtered["date_debut"] = deb

    # cas 3 : date de fin
    fin = requested.args.get("date_fin", "").replace(" ", "+")
    fin = scu.is_iso_formated(fin, True)

    if fin is not None:
        filtered["date_fin"] = fin

    # cas 4 : moduleimpl_id
    module = requested.args.get("moduleimpl_id", False)
    try:
        if module is False:
            raise ValueError
        if module != "":
            module = int(module)
        else:
            module = None
    except ValueError:
        module = False

    if module is not False:
        filtered["moduleimpl_id"] = module

    # cas 5 : formsemestre_id
    formsemestre_id = requested.args.get("formsemestre_id")

    if formsemestre_id is not None:
        formsemestre: FormSemestre = None
        formsemestre_id = int(formsemestre_id)
        formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
        filtered["formsemestre"] = formsemestre

    # cas 6 : type
    metric = requested.args.get("metric", "all")

    # cas 7 : est_just

    est_just: str = requested.args.get("est_just")
    if est_just is not None:
        trues: tuple[str] = ("v", "t", "vrai", "true", "1")
        falses: tuple[str] = ("f", "faux", "false", "0")

        if est_just.lower() in trues:
            filtered["est_just"] = True
        elif est_just.lower() in falses:
            filtered["est_just"] = False

    # cas 8 : user_id

    user_id = requested.args.get("user_id", False)
    if user_id is not False:
        filtered["user_id"] = user_id

    # cas 9 : split

    split = requested.args.get("split", False)
    if split is not False:
        filtered["split"] = True

    return (metric, filtered)


def _filter_manager(requested, assiduites_query: Query) -> Query:
    """
    _filter_manager Retourne les assiduites entrées filtrées en fonction de la request

    Args:
        requested (request): La requête http
        assiduites_query (Query): la query d'assiduités à filtrer

    Returns:
        Query: La query filtrée
    """
    # cas 1 : etat assiduite
    etat: str = requested.args.get("etat")
    if etat is not None:
        assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)

    # cas 2 : date de début
    deb: str = requested.args.get("date_debut", "").replace(" ", "+")
    deb: datetime = scu.is_iso_formated(
        deb, True
    )  # transformation de la chaine en datetime

    # cas 3 : date de fin
    fin: str = requested.args.get("date_fin", "").replace(" ", "+")
    fin: datetime = scu.is_iso_formated(
        fin, True
    )  # transformation de la chaine en datetime

    # Pour filtrer les dates il faut forcement avoir les deux bornes
    # [date_debut : date_fin]
    if (deb, fin) != (None, None):
        assiduites_query: Query = scass.filter_by_date(
            assiduites_query, Assiduite, deb, fin
        )

    # cas 4 : moduleimpl_id
    module = requested.args.get("moduleimpl_id", False)
    try:
        if module is False:
            raise ValueError
        if module != "":
            module = int(module)
        else:
            module = None
    except ValueError:
        module = False

    if module is not False:
        assiduites_query = scass.filter_by_module_impl(assiduites_query, module)

    # cas 5 : formsemestre_id
    formsemestre_id = requested.args.get("formsemestre_id")

    if formsemestre_id is not None:
        formsemestre: FormSemestre = None
        formsemestre_id = int(formsemestre_id)
        formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
        assiduites_query = scass.filter_by_formsemestre(
            assiduites_query, Assiduite, formsemestre
        )

    # cas 6 : est_just

    est_just: str = requested.args.get("est_just")
    if est_just is not None:
        trues: tuple[str] = ("v", "t", "vrai", "true", "1")
        falses: tuple[str] = ("f", "faux", "false", "0")

        if est_just.lower() in trues:
            assiduites_query: Query = scass.filter_assiduites_by_est_just(
                assiduites_query, True
            )
        elif est_just.lower() in falses:
            assiduites_query: Query = scass.filter_assiduites_by_est_just(
                assiduites_query, False
            )

    # cas 8 : user_id

    user_id = requested.args.get("user_id", False)
    if user_id is not False:
        assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id)

    # cas 9 : order (renvoie la query ordonnée en "date début Décroissante")
    order = requested.args.get("order", None)
    if order is not None:
        assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc())

    # cas 10 : courant (Ne renvoie que les assiduités de l'année courante)
    courant = requested.args.get("courant", None)
    if courant is not None:
        annee: int = scu.annee_scolaire()

        assiduites_query: Query = assiduites_query.filter(
            Assiduite.date_debut >= scu.date_debut_annee_scolaire(annee),
            Assiduite.date_fin <= scu.date_fin_annee_scolaire(annee),
        )

    return assiduites_query


def _with_justifs(assi: dict):
    """
    _with_justifs ajoute la liste des justificatifs à l'assiduité

    Condition : `with_justifs` doit se trouver dans les paramètres de la requête
    Args:
        assi (dict): un dictionnaire représentant une assiduité

    Returns:
        dict: l'assiduité avec les justificatifs ajoutés
    """
    if request.args.get("with_justifs") is None:
        return assi
    assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True)
    return assi