##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet.  All rights reserved.
# See LICENSE
##############################################################################

"""
  ScoDoc 9 API : partitions
"""
from operator import attrgetter

from flask import g, request
from flask_json import as_json
from flask_login import login_required
import sqlalchemy as sa
from sqlalchemy.exc import IntegrityError

import app
from app import db, log
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
from app.decorators import scodoc, permission_required
from app.scodoc.sco_utils import json_error
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import GroupDescr, Partition, Scolog
from app.models.groups import group_membership
from app.scodoc import sco_cache
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu


@bp.route("/partition/<int:partition_id>")
@api_web_bp.route("/partition/<int:partition_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_info(partition_id: int):
    """Info sur une partition.

    Exemple de résultat :
    ```
    {
        'bul_show_rank': False,
        'formsemestre_id': 39,
        'groups': [
            {'id': 268, 'name': 'A', 'partition_id': 100},
            {'id': 269, 'name': 'B', 'partition_id': 100}
        ],
        'groups_editable': True,
        'id': 100,
        'numero': 100,
        'partition_name': 'TD',
        'show_in_lists': True
    }
    ```
    """
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition = query.first_or_404()
    return partition.to_dict(with_groups=True)


@bp.route("/formsemestre/<int:formsemestre_id>/partitions")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/partitions")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_partitions(formsemestre_id: int):
    """Liste de toutes les partitions d'un formsemestre

    formsemestre_id : l'id d'un formsemestre

    {
        partition_id : {
            "bul_show_rank": False,
            "formsemestre_id": 1063,
            "groups" :
            group_id : {
                "id" : 12,
                "name" : "A",
                "partition_id" : partition_id,
            }
        },
        ...
    }

    """
    query = FormSemestre.query.filter_by(id=formsemestre_id)
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
    partitions = sorted(formsemestre.partitions, key=attrgetter("numero"))
    return {
        str(partition.id): partition.to_dict(with_groups=True, str_keys=True)
        for partition in partitions
        if partition.partition_name is not None
    }


@bp.route("/group/<int:group_id>/etudiants")
@api_web_bp.route("/group/<int:group_id>/etudiants")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etud_in_group(group_id: int):
    """
    Retourne la liste des étudiants dans un groupe
    (inscrits au groupe et inscrits au semestre).
    group_id : l'id d'un groupe

    Exemple de résultat :
    [
        {
        'civilite': 'M',
        'id': 123456,
        'ine': None,
        'nip': '987654321',
        'nom': 'MARTIN',
        'nom_usuel': null,
        'prenom': 'JEAN'}
        },
        ...
    ]
    """
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group = query.first_or_404()

    query = (
        Identite.query.join(group_membership)
        .filter_by(group_id=group_id)
        .join(FormSemestreInscription)
        .filter_by(formsemestre_id=group.partition.formsemestre_id)
    )

    return [etud.to_dict_short() for etud in query]


@bp.route("/group/<int:group_id>/etudiants/query")
@api_web_bp.route("/group/<int:group_id>/etudiants/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etud_in_group_query(group_id: int):
    """Étudiants du groupe, filtrés par état"""
    etat = request.args.get("etat")
    if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
        return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group = query.first_or_404()  # just to ckeck that group exists in accessible dept

    query = Identite.query.join(FormSemestreInscription).filter_by(
        formsemestre_id=group.partition.formsemestre_id
    )
    if etat is not None:
        query = query.filter_by(etat=etat)

    query = query.join(group_membership).filter_by(group_id=group_id)
    return [etud.to_dict_short() for etud in query]


@bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"])
@api_web_bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def set_etud_group(etudid: int, group_id: int):
    """Affecte l'étudiant au groupe indiqué"""
    etud = Identite.query.get_or_404(etudid)
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group = query.first_or_404()
    if not group.partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not group.partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
        return json_error(404, "etud non inscrit au formsemestre du groupe")

    try:
        sco_groups.change_etud_group_in_partition(etudid, group)
    except ScoValueError as exc:
        return json_error(404, exc.args[0])
    except IntegrityError:
        return json_error(404, "échec de l'enregistrement")
    return {"group_id": group_id, "etudid": etudid}


@bp.route("/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"])
@api_web_bp.route(
    "/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def group_remove_etud(group_id: int, etudid: int):
    """Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien."""
    etud = Identite.query.get_or_404(etudid)
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group = query.first_or_404()
    if not group.partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not group.partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")

    group.remove_etud(etud)

    return {"group_id": group_id, "etudid": etudid}


@bp.route(
    "/partition/<int:partition_id>/remove_etudiant/<int:etudid>", methods=["POST"]
)
@api_web_bp.route(
    "/partition/<int:partition_id>/remove_etudiant/<int:etudid>", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_remove_etud(partition_id: int, etudid: int):
    """Enlève l'étudiant de tous les groupes de cette partition
    (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
    """
    etud = Identite.query.get_or_404(etudid)
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition = query.first_or_404()
    if not partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    db.session.execute(
        sa.text(
            """DELETE FROM group_membership
        WHERE etudid=:etudid
        and group_id IN (
            SELECT id FROM group_descr WHERE partition_id = :partition_id
        );
        """
        ),
        {"etudid": etudid, "partition_id": partition_id},
    )

    Scolog.logdb(
        method="partition_remove_etud",
        etudid=etud.id,
        msg=f"Retrait de la partition {partition.partition_name}",
        commit=False,
    )
    db.session.commit()
    # Update parcours
    partition.formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)
    app.set_sco_dept(partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(partition.formsemestre_id)
    return {"partition_id": partition_id, "etudid": etudid}


@bp.route("/partition/<int:partition_id>/group/create", methods=["POST"])
@api_web_bp.route("/partition/<int:partition_id>/group/create", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def group_create(partition_id: int):  # partition-group-create
    """Création d'un groupe dans une partition

    The request content type should be "application/json":
    {
        "group_name" : nom_du_groupe,
    }
    """
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition: Partition = query.first_or_404()
    if not partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not partition.groups_editable:
        return json_error(403, "partition non editable")
    if not partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")

    args = request.get_json(force=True)  # may raise 400 Bad Request
    group_name = args.get("group_name")
    if not isinstance(group_name, str):
        return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
    args["group_name"] = args["group_name"].strip()
    if not GroupDescr.check_name(partition, args["group_name"]):
        return json_error(API_CLIENT_ERROR, "invalid group_name")

    # le numero est optionnel
    numero = args.get("numero")
    if numero is None:
        numeros = [gr.numero or 0 for gr in partition.groups]
        numero = (max(numeros) + 1) if numeros else 0
        args["numero"] = numero
    args["partition_id"] = partition_id
    try:
        group = GroupDescr(**args)
    except TypeError:
        return json_error(API_CLIENT_ERROR, "invalid arguments")
    db.session.add(group)
    db.session.commit()
    log(f"created group {group}")
    app.set_sco_dept(partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(partition.formsemestre_id)
    return group.to_dict(with_partition=True)


@bp.route("/group/<int:group_id>/delete", methods=["POST"])
@api_web_bp.route("/group/<int:group_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def group_delete(group_id: int):
    """Suppression d'un groupe"""
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group: GroupDescr = query.first_or_404()
    if not group.partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not group.partition.groups_editable:
        return json_error(403, "partition non editable")
    if not group.partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    formsemestre_id = group.partition.formsemestre_id
    log(f"deleting {group}")
    db.session.delete(group)
    db.session.commit()
    app.set_sco_dept(group.partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(formsemestre_id)
    return {"OK": True}


@bp.route("/group/<int:group_id>/edit", methods=["POST"])
@api_web_bp.route("/group/<int:group_id>/edit", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def group_edit(group_id: int):
    """Edit a group"""
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group: GroupDescr = query.first_or_404()
    if not group.partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not group.partition.groups_editable:
        return json_error(403, "partition non editable")
    if not group.partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")

    args = request.get_json(force=True)  # may raise 400 Bad Request
    if "group_name" in args:
        if not isinstance(args["group_name"], str):
            return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
        args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
        if not GroupDescr.check_name(
            group.partition, args["group_name"], existing=True
        ):
            return json_error(API_CLIENT_ERROR, "invalid group_name")

    group.from_dict(args)
    db.session.add(group)
    db.session.commit()
    log(f"modified {group}")

    app.set_sco_dept(group.partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
    return group.to_dict(with_partition=True)


@bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
@api_web_bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def group_set_edt_id(group_id: int, edt_id: str):
    """Set edt_id for this group.
    Contrairement à /edit, peut-être changé pour toute partition
    ou formsemestre non verrouillé.
    """
    query = GroupDescr.query.filter_by(id=group_id)
    if g.scodoc_dept:
        query = (
            query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
        )
    group: GroupDescr = query.first_or_404()
    if not group.partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
    group.edt_id = edt_id
    db.session.add(group)
    db.session.commit()
    return group.to_dict(with_partition=True)


@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
@api_web_bp.route(
    "/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_create(formsemestre_id: int):
    """Création d'une partition dans un semestre

    The request content type should be "application/json":
    {
        "partition_name": str,
        "numero":int,
        "bul_show_rank":bool,
        "show_in_lists":bool,
        "groups_editable":bool
    }
    """
    query = FormSemestre.query.filter_by(id=formsemestre_id)
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
    if not formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    data = request.get_json(force=True)  # may raise 400 Bad Request
    partition_name = data.get("partition_name")
    if partition_name is None:
        return json_error(
            API_CLIENT_ERROR, "missing partition_name or invalid data format"
        )
    if partition_name == scu.PARTITION_PARCOURS:
        return json_error(
            API_CLIENT_ERROR, f"invalid partition_name {scu.PARTITION_PARCOURS}"
        )
    if not Partition.check_name(formsemestre, partition_name):
        return json_error(API_CLIENT_ERROR, "invalid partition_name")
    numero = data.get("numero", 0)
    if not isinstance(numero, int):
        return json_error(API_CLIENT_ERROR, "invalid type for numero")
    args = {
        "formsemestre_id": formsemestre_id,
        "partition_name": partition_name.strip(),
        "numero": numero,
    }
    for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"):
        value = data.get(
            boolean_field, False if boolean_field != "groups_editable" else True
        )
        if not isinstance(value, bool):
            return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
        args[boolean_field] = value

    partition = Partition(**args)
    db.session.add(partition)
    db.session.commit()
    log(f"created partition {partition}")
    app.set_sco_dept(formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(formsemestre_id)
    return partition.to_dict(with_groups=True)


@bp.route("/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"])
@api_web_bp.route(
    "/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def formsemestre_order_partitions(formsemestre_id: int):
    """Modifie l'ordre des partitions du formsemestre
    JSON args: [partition_id1, partition_id2, ...]
    """
    query = FormSemestre.query.filter_by(id=formsemestre_id)
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
    if not formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    partition_ids = request.get_json(force=True)  # may raise 400 Bad Request
    if not isinstance(partition_ids, int) and not all(
        isinstance(x, int) for x in partition_ids
    ):
        return json_error(
            API_CLIENT_ERROR,
            message="paramètre liste des partitions invalide",
        )
    for p_id, numero in zip(partition_ids, range(len(partition_ids))):
        partition = Partition.query.get_or_404(p_id)
        partition.numero = numero
        db.session.add(partition)
    db.session.commit()
    app.set_sco_dept(formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(formsemestre_id)
    log(f"formsemestre_order_partitions({partition_ids})")
    return [
        partition.to_dict()
        for partition in formsemestre.partitions.order_by(Partition.numero)
        if partition.partition_name is not None
    ]


@bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"])
@api_web_bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_order_groups(partition_id: int):
    """Modifie l'ordre des groupes de la partition
    JSON args: [group_id1, group_id2, ...]
    """
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition: Partition = query.first_or_404()
    if not partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    group_ids = request.get_json(force=True)  # may raise 400 Bad Request
    if not isinstance(group_ids, int) and not all(
        isinstance(x, int) for x in group_ids
    ):
        return json_error(
            API_CLIENT_ERROR,
            message="paramètre liste de groupe invalide",
        )
    for group_id, numero in zip(group_ids, range(len(group_ids))):
        group = GroupDescr.query.get_or_404(group_id)
        group.numero = numero
        db.session.add(group)
    db.session.commit()
    app.set_sco_dept(partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(partition.formsemestre_id)
    log(f"partition_order_groups: {partition} : {group_ids}")
    return partition.to_dict(with_groups=True)


@bp.route("/partition/<int:partition_id>/edit", methods=["POST"])
@api_web_bp.route("/partition/<int:partition_id>/edit", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_edit(partition_id: int):
    """Modification d'une partition dans un semestre

    The request content type should be "application/json"
    All fields are optional:
    {
        "partition_name": str,
        "numero":int,
        "bul_show_rank":bool,
        "show_in_lists":bool,
        "groups_editable":bool
    }
    """
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition: Partition = query.first_or_404()
    if not partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    data = request.get_json(force=True)  # may raise 400 Bad Request
    modified = False
    partition_name = data.get("partition_name")
    #
    if partition_name is not None and partition_name != partition.partition_name:
        if partition.is_parcours():
            return json_error(
                API_CLIENT_ERROR, f"can't rename {scu.PARTITION_PARCOURS}"
            )
        if not Partition.check_name(
            partition.formsemestre, partition_name, existing=True
        ):
            return json_error(API_CLIENT_ERROR, "invalid partition_name")
        partition.partition_name = partition_name.strip()
        modified = True

    numero = data.get("numero")
    if numero is not None and numero != partition.numero:
        if not isinstance(numero, int):
            return json_error(API_CLIENT_ERROR, "invalid type for numero")
        partition.numero = numero
        modified = True

    for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"):
        value = data.get(boolean_field)
        if value is not None and value != getattr(partition, boolean_field):
            if not isinstance(value, bool):
                return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
            if boolean_field == "groups_editable" and partition.is_parcours():
                return json_error(
                    API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
                )
            setattr(partition, boolean_field, value)
            modified = True

    if modified:
        db.session.add(partition)
        db.session.commit()
        log(f"modified partition {partition}")
        app.set_sco_dept(partition.formsemestre.departement.acronym)
        sco_cache.invalidate_formsemestre(partition.formsemestre_id)

    return partition.to_dict(with_groups=True)


@bp.route("/partition/<int:partition_id>/delete", methods=["POST"])
@api_web_bp.route("/partition/<int:partition_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def partition_delete(partition_id: int):
    """Suppression d'une partition (et de tous ses groupes).

    Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
        pas être supprimée.
    Note 2: Si la partition de parcours est supprimée, les étudiants
        sont désinscrits des parcours.
    """
    query = Partition.query.filter_by(id=partition_id)
    if g.scodoc_dept:
        query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
    partition: Partition = query.first_or_404()
    if not partition.formsemestre.etat:
        return json_error(403, "formsemestre verrouillé")
    if not partition.formsemestre.can_change_groups():
        return json_error(401, "opération non autorisée")
    if not partition.partition_name:
        return json_error(
            API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"
        )
    is_parcours = partition.is_parcours()
    formsemestre: FormSemestre = partition.formsemestre
    log(f"deleting partition {partition}")
    db.session.delete(partition)
    db.session.commit()
    app.set_sco_dept(partition.formsemestre.departement.acronym)
    sco_cache.invalidate_formsemestre(formsemestre.id)
    if is_parcours:
        formsemestre.update_inscriptions_parcours_from_groups()
    return {"OK": True}
