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

"""
  API : accès aux étudiants
"""
from datetime import datetime
from operator import attrgetter

from flask import g, request
from flask_json import as_json
from flask_login import current_user
from flask_login import login_required
from sqlalchemy import desc, func, or_
from sqlalchemy.dialects.postgresql import VARCHAR

import app
from app import db
from app.api import api_bp as bp, api_web_bp
from app.api import tools
from app.but import bulletin_but_court
from app.decorators import scodoc, permission_required
from app.models import (
    Admission,
    Departement,
    FormSemestreInscription,
    FormSemestre,
    Identite,
    ScolarNews,
)
from app.scodoc import sco_bulletins
from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc import sco_etud
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error, suppress_accents

import app.scodoc.sco_photos as sco_photos
import app.scodoc.sco_utils as scu

# Un exemple:
# @bp.route("/api_function/<int:arg>")
# @api_web_bp.route("/api_function/<int:arg>")
# @login_required
# @scodoc
# @permission_required(Permission.ScoView)
# @as_json
# def api_function(arg: int):
#     """Une fonction quelconque de l'API"""
#     return {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
#


@bp.route("/etudiants/courants", defaults={"long": False})
@bp.route("/etudiants/courants/long", defaults={"long": True})
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
@api_web_bp.route("/etudiants/courants/long", defaults={"long": True})
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiants_courants(long=False):
    """
    La liste des étudiants des semestres "courants" (tous départements)
    (date du jour comprise dans la période couverte par le sem.)
    dans lesquels l'utilisateur a la permission ScoView
    (donc tous si le dept du rôle est None).

    Exemple de résultat :
        [
        {
            "id": 1234,
            "code_nip": "12345678",
            "code_ine": null,
            "nom": "JOHN",
            "nom_usuel": None,
            "prenom": "DEUF",
            "civilite": "M",
        }
            ...
        ]

    En format "long": voir documentation.

    """
    allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
    date_courante = request.args.get("date_courante")
    if date_courante:
        test_date = datetime.fromisoformat(date_courante)
    else:
        test_date = app.db.func.now()
    etuds = Identite.query.filter(
        Identite.id == FormSemestreInscription.etudid,
        FormSemestreInscription.formsemestre_id == FormSemestre.id,
        FormSemestre.date_debut <= test_date,
        FormSemestre.date_fin >= test_date,
    )
    if not None in allowed_depts:
        # restreint aux départements autorisés:
        etuds = etuds.join(Departement).filter(
            or_(Departement.acronym == acronym for acronym in allowed_depts)
        )
    if long:
        data = [etud.to_dict_api() for etud in etuds]
    else:
        data = [etud.to_dict_short() for etud in etuds]
    return data


@bp.route("/etudiant/etudid/<int:etudid>")
@bp.route("/etudiant/nip/<string:nip>")
@bp.route("/etudiant/ine/<string:ine>")
@api_web_bp.route("/etudiant/etudid/<int:etudid>")
@api_web_bp.route("/etudiant/nip/<string:nip>")
@api_web_bp.route("/etudiant/ine/<string:ine>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiant(etudid: int = None, nip: str = None, ine: str = None):
    """
    Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.

    etudid : l'etudid de l'étudiant
    nip : le code nip de l'étudiant
    ine : le code ine de l'étudiant

    Les codes INE et NIP sont uniques au sein d'un département.
    Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
    """
    etud = tools.get_etud(etudid, nip, ine)

    if etud is None:
        return json_error(
            404,
            message="étudiant inconnu",
        )

    return etud.to_dict_api()


@bp.route("/etudiant/etudid/<int:etudid>/photo")
@bp.route("/etudiant/nip/<string:nip>/photo")
@bp.route("/etudiant/ine/<string:ine>/photo")
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
    """
    Retourne la photo de l'étudiant
    correspondant ou un placeholder si non existant.

    etudid : l'etudid de l'étudiant
    nip : le code nip de l'étudiant
    ine : le code ine de l'étudiant
    """

    etud = tools.get_etud(etudid, nip, ine)

    if etud is None:
        return json_error(
            404,
            message="étudiant inconnu",
        )
    if not etudid:
        filename = sco_photos.UNKNOWN_IMAGE_PATH

    size = request.args.get("size", "orig")
    filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
    if not filename:
        filename = sco_photos.UNKNOWN_IMAGE_PATH
    res = sco_photos.build_image_response(filename)
    return res


@bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.EtudChangeAdr)
@as_json
def set_photo_image(etudid: int = None):
    """Enregistre la photo de l'étudiant."""
    allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
    query = Identite.query.filter_by(id=etudid)
    if not None in allowed_depts:
        # restreint aux départements autorisés:
        query = query.join(Departement).filter(
            or_(Departement.acronym == acronym for acronym in allowed_depts)
        )
    if g.scodoc_dept is not None:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    etud: Identite = query.first()
    if etud is None:
        return json_error(404, message="etudiant inexistant")
    # Récupère l'image
    if len(request.files) == 0:
        return json_error(404, "Il n'y a pas de fichier joint")

    file = list(request.files.values())[0]
    if not file.filename:
        return json_error(404, "Il n'y a pas de fichier joint")
    data = file.stream.read()

    status, err_msg = sco_photos.store_photo(etud, data, file.filename)
    if status:
        return {"etudid": etud.id, "message": "recorded photo"}
    return json_error(
        404,
        message=f"Erreur: {err_msg}",
    )


@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
@api_web_bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@api_web_bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@api_web_bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
    """
    Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
    toujours une liste.
    Si non trouvé, liste vide, pas d'erreur.
    Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
    été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
    """
    allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
    if etudid is not None:
        query = Identite.query.filter_by(id=etudid)
    elif nip is not None:
        query = Identite.query.filter_by(code_nip=nip)
    elif ine is not None:
        query = Identite.query.filter_by(code_ine=ine)
    else:
        return json_error(
            404,
            message="parametre manquant",
        )
    if not None in allowed_depts:
        # restreint aux départements autorisés:
        query = query.join(Departement).filter(
            or_(Departement.acronym == acronym for acronym in allowed_depts)
        )
    return [etud.to_dict_api() for etud in query]


@bp.route("/etudiants/name/<string:start>")
@api_web_bp.route("/etudiants/name/<string:start>")
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiants_by_name(start: str = "", min_len=3, limit=32):
    """Liste des étudiants dont le nom débute par start.
    Si start fait moins de min_len=3 caractères, liste vide.
    La casse et les accents sont ignorés.
    """
    if len(start) < min_len:
        return []
    start = suppress_accents(start).lower()
    query = Identite.query.filter(
        func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
    )
    allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
    if not None in allowed_depts:
        # restreint aux départements autorisés:
        query = query.join(Departement).filter(
            or_(Departement.acronym == acronym for acronym in allowed_depts)
        )
    etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
    # Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
    return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]


@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
@api_web_bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@api_web_bp.route("/etudiant/nip/<string:nip>/formsemestres")
@api_web_bp.route("/etudiant/ine/<string:ine>/formsemestres")
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
    """
    Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
    Accès par etudid, nip ou ine.

    Attention, si accès via NIP ou INE, les semestres peuvent être de départements
    différents (si l'étudiant a changé de département). L'id du département est `dept_id`.

    Si accès par département, ne retourne que les formsemestre suivis dans le département.
    """
    if etudid is not None:
        q_etud = Identite.query.filter_by(id=etudid)
    elif nip is not None:
        q_etud = Identite.query.filter_by(code_nip=nip)
    elif ine is not None:
        q_etud = Identite.query.filter_by(code_ine=ine)
    else:
        return json_error(404, message="parametre manquant")
    if g.scodoc_dept is not None:
        q_etud = q_etud.filter_by(dept_id=g.scodoc_dept_id)
    etud = q_etud.join(Admission).order_by(desc(Admission.annee)).first()
    if etud is None:
        return json_error(404, message="etudiant inexistant")
    query = FormSemestre.query.filter(
        FormSemestreInscription.etudid == etud.id,
        FormSemestreInscription.formsemestre_id == FormSemestre.id,
    )
    if g.scodoc_dept is not None:
        query = query.filter_by(dept_id=g.scodoc_dept_id)

    formsemestres = query.order_by(FormSemestre.date_debut)

    return [formsemestre.to_dict_api() for formsemestre in formsemestres]


@bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
)
@bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
)
@bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
    defaults={"pdf": True},
)
@bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
    defaults={"pdf": True, "with_img_signatures_pdf": False},
)
@api_web_bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
)
@api_web_bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
)
@api_web_bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
    defaults={"pdf": True},
)
@api_web_bp.route(
    "/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
    defaults={"pdf": True, "with_img_signatures_pdf": False},
)
@scodoc
@permission_required(Permission.ScoView)
def bulletin(
    code_type: str = "etudid",
    code: str = None,
    formsemestre_id: int = None,
    version: str = "selectedevals",
    pdf: bool = False,
    with_img_signatures_pdf: bool = True,
):
    """
    Retourne le bulletin d'un étudiant dans un formsemestre.

    formsemestre_id : l'id d'un formsemestre
    code_type : "etudid", "nip" ou "ine"
    code : valeur du code INE, NIP ou etudid, selon code_type.
    version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
    pdf : si spécifié, bulletin au format PDF (et non JSON).

    Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
    """
    if version == "pdf":
        version = "long"
        pdf = True
    if version not in scu.BULLETINS_VERSIONS_BUT:
        return json_error(404, "version invalide")
    # return f"{code_type}={code}, version={version}, pdf={pdf}"
    formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
    dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
    if g.scodoc_dept and dept.acronym != g.scodoc_dept:
        return json_error(404, "formsemestre inexistant")
    app.set_sco_dept(dept.acronym)

    if code_type == "nip":
        query = Identite.query.filter_by(code_nip=code, dept_id=dept.id)
    elif code_type == "etudid":
        try:
            etudid = int(code)
        except ValueError:
            return json_error(404, "invalid etudid type")
        query = Identite.query.filter_by(id=etudid)
    elif code_type == "ine":
        query = Identite.query.filter_by(code_ine=code, dept_id=dept.id)
    else:
        return json_error(404, "invalid code_type")
    etud = query.first()
    if etud is None:
        return json_error(404, message="etudiant inexistant")

    if version == "butcourt":
        if pdf:
            return bulletin_but_court.bulletin_but(formsemestre_id, etud.id, fmt="pdf")
        else:
            return json_error(404, message="butcourt available only in pdf")
    if pdf:
        pdf_response, _ = do_formsemestre_bulletinetud(
            formsemestre,
            etud,
            version=version,
            fmt="pdf",
            with_img_signatures_pdf=with_img_signatures_pdf,
        )
        return pdf_response
    return sco_bulletins.get_formsemestre_bulletin_etud_json(
        formsemestre, etud, version=version
    )


@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups")
@api_web_bp.route(
    "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups"
)
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etudiant_groups(formsemestre_id: int, etudid: int = None):
    """
    Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué

    formsemestre_id : l'id d'un formsemestre
    etudid : l'etudid d'un étudiant

    Exemple de résultat :
    [
    {
        "partition_id": 1,
        "id": 1,
        "formsemestre_id": 1,
        "partition_name": null,
        "numero": 0,
        "bul_show_rank": false,
        "show_in_lists": true,
        "group_id": 1,
        "group_name": null
    },
    {
        "partition_id": 2,
        "id": 2,
        "formsemestre_id": 1,
        "partition_name": "TD",
        "numero": 1,
        "bul_show_rank": false,
        "show_in_lists": true,
        "group_id": 2,
        "group_name": "A"
    }
    ]
    """
    query = FormSemestre.query.filter_by(id=formsemestre_id)
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    formsemestre = query.first()
    if formsemestre is None:
        return json_error(
            404,
            message="formsemestre inconnu",
        )
    dept = formsemestre.departement
    etud = Identite.query.filter_by(id=etudid, dept_id=dept.id).first_or_404(etudid)

    app.set_sco_dept(dept.acronym)
    data = sco_groups.get_etud_groups(etud.id, formsemestre.id)

    return data


@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
@api_web_bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
@api_web_bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def etudiant_create(force=False):
    """Création d'un nouvel étudiant
    Si force, crée même si homonymie détectée.
    L'étudiant créé n'est pas inscrit à un semestre.
    Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
    """
    args = request.get_json(force=True)  # may raise 400 Bad Request
    dept = args.get("dept", None)
    if not dept:
        return scu.json_error(400, "dept requis")
    dept_o = Departement.query.filter_by(acronym=dept).first()
    if not dept_o:
        return scu.json_error(400, "dept invalide")
    if g.scodoc_dept and g.scodoc_dept_id != dept_o.id:
        return scu.json_error(400, "dept invalide (route departementale)")
    else:
        app.set_sco_dept(dept)
    args["dept_id"] = dept_o.id
    # vérifie que le département de création est bien autorisé
    if not current_user.has_permission(Permission.EtudInscrit, dept):
        return json_error(403, "departement non autorisé")
    nom = args.get("nom", None)
    prenom = args.get("prenom", None)
    ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
    if not ok:
        return scu.json_error(400, "nom ou prénom invalide")
    if len(homonyms) > 0 and not force:
        return scu.json_error(
            400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
        )
    etud = Identite.create_etud(**args)
    db.session.flush()
    # --- Données admission
    admission_args = args.get("admission", None)
    if admission_args:
        etud.admission.from_dict(admission_args)
    # --- Adresse
    adresses = args.get("adresses", [])
    if adresses:
        # ne prend en compte que la première adresse
        # car si la base est concue pour avoir plusieurs adresses par étudiant,
        # l'application n'en gère plus qu'une seule.
        adresse = etud.adresses.first()
        adresse.from_dict(adresses[0])

    # Poste une nouvelle dans le département concerné:
    ScolarNews.add(
        typ=ScolarNews.NEWS_INSCR,
        text=f"Nouvel étudiant {etud.html_link_fiche()}",
        url=etud.url_fiche(),
        max_frequency=0,
        dept_id=dept_o.id,
    )
    db.session.commit()
    # Note: je ne comprends pas pourquoi un refresh est nécessaire ici
    # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
    db.session.refresh(etud)
    r = etud.to_dict_api()
    return r


@bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
@scodoc
@permission_required(Permission.EtudInscrit)
def etudiant_edit(
    code_type: str = "etudid",
    code: str = None,
):
    """Edition des données étudiant (identité, admission, adresses)"""
    if code_type == "nip":
        query = Identite.query.filter_by(code_nip=code)
    elif code_type == "etudid":
        try:
            etudid = int(code)
        except ValueError:
            return json_error(404, "invalid etudid type")
        query = Identite.query.filter_by(id=etudid)
    elif code_type == "ine":
        query = Identite.query.filter_by(code_ine=code)
    else:
        return json_error(404, "invalid code_type")
    if g.scodoc_dept:
        query = query.filter_by(dept_id=g.scodoc_dept_id)
    etud: Identite = query.first()
    #
    args = request.get_json(force=True)  # may raise 400 Bad Request
    etud.from_dict(args)
    admission_args = args.get("admission", None)
    if admission_args:
        etud.admission.from_dict(admission_args)
    # --- Adresse
    adresses = args.get("adresses", [])
    if adresses:
        # ne prend en compte que la première adresse
        # car si la base est concue pour avoir plusieurs adresses par étudiant,
        # l'application n'en gère plus qu'une seule.
        adresse = etud.adresses.first()
        adresse.from_dict(adresses[0])

    db.session.commit()
    # Note: je ne comprends pas pourquoi un refresh est nécessaire ici
    # sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
    db.session.refresh(etud)
    r = etud.to_dict_api()
    return r