# -*- mode: python -*-
# -*- coding: utf-8 -*-

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   Emmanuel Viennet      emmanuel.viennet@viennet.net
#
##############################################################################

"""Saisie et gestion des semestres extérieurs à ScoDoc dans un parcours.

On va créer/gérer des semestres de la même formation que le semestre ScoDoc 
où est inscrit l'étudiant, leur attribuer la modalité 'EXT'.
Ces semestres n'auront qu'un seul inscrit !
"""
import time

import flask
from flask import url_for, g, request
from flask_login import current_user

from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
    Formation,
    FormSemestre,
    FormSemestreUECoef,
    Identite,
    ScolarFormSemestreValidation,
    UniteEns,
)
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc import html_sco_header
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation
from app.scodoc.codes_cursus import UE_SPORT


def formsemestre_ext_create(etudid, sem_params):
    """Crée un formsemestre exterieur et y inscrit l'étudiant.
    sem_params: dict nécessaire à la création du formsemestre
    """
    # Check args
    _ = Formation.query.get_or_404(sem_params["formation_id"])
    if etudid:
        _ = Identite.get_etud(etudid)

    # Create formsemestre
    sem_params["modalite"] = "EXT"
    sem_params["etapes"] = None
    sem_params["responsables"] = [current_user.id]
    formsemestre_id = sco_formsemestre.do_formsemestre_create(sem_params, silent=True)
    # nota: le semestre est créé vide: pas de modules

    # Inscription au semestre
    sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
        formsemestre_id,
        etudid,
        method="formsemestre_ext_create",
    )
    return formsemestre_id


def formsemestre_ext_create_form(etudid, formsemestre_id):
    """Formulaire création/inscription à un semestre extérieur"""
    etud = Identite.get_etud(etudid)
    H = [
        html_sco_header.sco_header(),
        f"""<h2>Enregistrement d'une inscription antérieure dans un autre
        établissement</h2>
        <p class="help">
        Cette opération crée un semestre extérieur ("ancien") de la même
        formation que le semestre courant, et y inscrit juste cet étudiant.
        La décision de jury peut ensuite y être saisie.
        </p>
        <p class="help">
        Notez que si un semestre extérieur similaire a déjà été créé pour un autre
        étudiant, il est préférable d'utiliser la fonction
        "<a href="{ url_for('notes.formsemestre_inscription_with_modules_form',
        scodoc_dept=g.scodoc_dept, etudid=etudid, only_ext=1) }">
        inscrire à un autre semestre</a>"
        </p>
        <h3><a href="{ url_for('scolar.ficheEtud',
            scodoc_dept=g.scodoc_dept, etudid=etudid)
            }" class="stdlink">Étudiant {etud.nomprenom}</a></h3>
        """,
    ]
    F = html_sco_header.sco_footer()
    orig_sem = sco_formsemestre.get_formsemestre(formsemestre_id)
    # Ne propose que des semestres de semestre_id strictement inférieur
    # au semestre courant
    # et seulement si pas inscrit au même semestre_id d'un semestre ordinaire ScoDoc.
    # Les autres situations (eg redoublements en changeant d'établissement)
    # doivent être gérées par les validations de semestres "antérieurs"
    insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
        args={"etudid": etudid, "etat": scu.INSCRIT}
    )
    semlist = [sco_formsemestre.get_formsemestre(i["formsemestre_id"]) for i in insem]
    existing_semestre_ids = {s["semestre_id"] for s in semlist}
    min_semestre_id = 1
    max_semestre_id = orig_sem["semestre_id"]
    semestre_ids = set(range(min_semestre_id, max_semestre_id)) - existing_semestre_ids
    H.append(
        f"""<p>L'étudiant est déjà inscrit dans des semestres ScoDoc de rangs:
            { sorted(list(existing_semestre_ids)) }
            </p>
        """
    )
    if not semestre_ids:
        H.append(
            f"""<p class="warning">pas de semestres extérieurs possibles
            (indices entre {min_semestre_id} et {max_semestre_id}, semestre courant.)
            </p>"""
        )
        return "\n".join(H) + F
    # Formulaire
    semestre_ids_list = sorted(semestre_ids)
    semestre_ids_labels = [f"S{x}" for x in semestre_ids_list]
    descr = [
        ("formsemestre_id", {"input_type": "hidden"}),
        ("etudid", {"input_type": "hidden"}),
        (
            "semestre_id",
            {
                "input_type": "menu",
                "title": "Indice du semestre dans le cursus",
                "allowed_values": semestre_ids_list,
                "labels": semestre_ids_labels,
            },
        ),
        (
            "titre",
            {
                "size": 40,
                "title": "Nom de ce semestre extérieur",
                "explanation": """par exemple: établissement.
                    N'indiquez pas les dates, ni le semestre, ni la modalité dans
                    le titre: ils seront automatiquement ajoutés""",
            },
        ),
        (
            "date_debut",
            {
                "title": "Date de début",  # j/m/a
                "input_type": "datedmy",
                "explanation": "j/m/a (peut être approximatif)",
                "size": 9,
                "allow_null": False,
            },
        ),
        (
            "date_fin",
            {
                "title": "Date de fin",  # j/m/a
                "input_type": "datedmy",
                "explanation": "j/m/a (peut être approximatif)",
                "size": 9,
                "allow_null": False,
            },
        ),
        (
            "elt_help_ue",
            {
                "title": """Les notes et coefficients des UE
                capitalisées seront saisis ensuite""",
                "input_type": "separator",
            },
        ),
    ]

    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        descr,
        cancelbutton="Annuler",
        method="post",
        submitlabel="Créer semestre extérieur et y inscrire l'étudiant",
        cssclass="inscription",
        name="tf",
    )
    if tf[0] == 0:
        H.append(
            """<p>Ce formulaire sert à enregistrer un semestre antérieur dans
            la formation effectué dans un autre établissement.
            </p>"""
        )
        return "\n".join(H) + "\n" + tf[1] + F
    elif tf[0] == -1:
        return flask.redirect(
            url_for(
                "notes.formsemestre_bulletinetud",
                scodoc_dept=g.scodoc_dept,
                formsemestre_id=formsemestre_id,
                etudid=etudid,
            )
        )
    else:
        # Le semestre extérieur est créé dans la même formation que le semestre courant
        tf[2]["formation_id"] = orig_sem["formation_id"]
        formsemestre_ext_create(etudid, tf[2])
        return flask.redirect(
            url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
        )


def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
    """Edition des validations d'UE et de semestre (jury)
    pour un semestre extérieur.
    On peut saisir pour chaque UE du programme de formation
    sa validation, son code jury, sa note, son coefficient
    (sauf en BUT où le coef. des UE est toujours égal aux ECTS).

    La moyenne générale indicative du semestre est calculée et affichée,
    mais pas enregistrée.
    """
    formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
    etud = Identite.get_etud(etudid)
    ues = formsemestre.formation.ues.filter(UniteEns.type != UE_SPORT).order_by(
        UniteEns.semestre_idx, UniteEns.numero
    )
    if formsemestre.formation.is_apc():
        ues = ues.filter_by(semestre_idx=formsemestre.semestre_id)
    descr = _ue_form_description(formsemestre, etud, ues, scu.get_request_args())
    initvalues = {}
    if request.method == "GET":
        for ue in ues:
            validation = ScolarFormSemestreValidation.query.filter_by(
                ue_id=ue.id, etudid=etud.id, formsemestre_id=formsemestre.id
            ).first()
            initvalues[f"note_{ue.id}"] = validation.moy_ue if validation else ""

    tf = TrivialFormulator(
        request.base_url,
        scu.get_request_args(),
        descr,
        submitlabel="Enregistrer ces validations",
        cancelbutton="Annuler",
        initvalues=initvalues,
        cssclass="tf_ext_edit_ue_validations ext_apc"
        if formsemestre.formation.is_apc()
        else "tf_ext_edit_ue_validations",
        # En APC, stocke les coefficients pour l'affichage de la moyenne en direct
        form_attrs=f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
        if formsemestre.formation.is_apc()
        else "",
    )
    if tf[0] == -1:
        return "<h4>annulation</h4>"
    else:
        H = _make_page(etud, formsemestre, tf)
        if tf[0] == 0:  # premier affichage
            return "\n".join(H)
        else:  # soumission
            # simule erreur
            ok, message = _check_values(formsemestre, ues, tf[2])
            if not ok:
                H = _make_page(etud, formsemestre, tf, message=message)
                return "\n".join(H)
            else:
                # Submit
                _record_ue_validations_and_coefs(formsemestre, etud, ues, tf[2])
                return flask.redirect(
                    url_for(
                        "notes.formsemestre_bulletinetud",
                        scodoc_dept=g.scodoc_dept,
                        formsemestre_id=formsemestre_id,
                        etudid=etudid,
                    )
                )


def _make_page(etud: Identite, formsemestre: FormSemestre, tf, message="") -> list[str]:
    """html formulaire saisie"""
    nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
    moy_gen = nt.get_etud_moy_gen(etud.id)
    H = [
        html_sco_header.sco_header(
            page_title="Validation des UE d'un semestre extérieur",
            javascripts=["js/formsemestre_ext_edit_ue_validations.js"],
        ),
        tf_error_message(message),
        f"""<p><b>{etud.nomprenom}</b> est inscrit{etud.e} à ce semestre extérieur.</p>
        <p>Voici ses UE enregistrées avec leur notes 
        { "et coefficients" if not formsemestre.formation.is_apc()
        else " (en BUT, les coefficients sont égaux aux ECTS)"}.
        </p>
        """,
        f"""<p>La moyenne de ce semestre serait: 
        <span class="ext_sem_moy"><span class="ext_sem_moy_val">{moy_gen}</span> / 20</span>
        </p>
        """,
        '<div id="formsemestre_ext_edit_ue_validations">',
        tf[1],
        "</div>",
        f"""<div>
        <a class="stdlink"
        href="{url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
                    formsemestre_id=formsemestre.id, etudid=etud.id
        )}">retour au bulletin de notes</a>
        </div>
        """,
        html_sco_header.sco_footer(),
    ]
    return H


_UE_VALID_CODES = {
    None: "Non inscrit",
    "ADM": "Capitalisée (ADM)",
    # "CMP": "Acquise (car semestre validé)",
}


def _ue_form_description(
    formsemestre: FormSemestre, etud: Identite, ues: list[UniteEns], values
):
    """Description du formulaire de saisie des UE / validations
    Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient.
    """
    descr = [
        (
            "head_sep",
            {
                "input_type": "separator",
                "template": """<tr %(item_dom_attr)s><th>UE</th>
            <th>Code jury</th><th>Note/20</th>
            """
                + (
                    """<th>Coefficient UE</th>"""
                    if not formsemestre.formation.is_apc()
                    else ""
                )
                + "</tr>",
            },
        ),
        ("formsemestre_id", {"input_type": "hidden"}),
        ("etudid", {"input_type": "hidden"}),
    ]
    for ue in ues:
        # Menu pour code validation UE:
        # Ne propose que ADM, CMP et "Non inscrit"
        select_name = f"valid_{ue.id}"
        menu_code_ue = f"""<select class="ueext_valid_select" name="{select_name}">"""
        cur_code_value = values.get("valid_{ue.id}", False)
        for (code, explanation) in _UE_VALID_CODES.items():
            if cur_code_value is False:  # pas dans le form, cherche en base
                validation = ScolarFormSemestreValidation.query.filter_by(
                    ue_id=ue.id, etudid=etud.id, formsemestre_id=formsemestre.id
                ).first()
                cur_code_value = validation.code if validation else None
            if str(cur_code_value) == str(code):
                selected = "selected"
            else:
                selected = ""
            # code jury:
            menu_code_ue += (
                f"""<option value="{code}" {selected}>{explanation}</option>"""
            )
            if cur_code_value is None:
                coef_disabled = 'disabled="1"'
            else:
                coef_disabled = ""
        menu_code_ue += "</select>"
        if formsemestre.formation.is_apc():
            coef_disabled = 'disabled="1"'
            cur_coef_value = ue.ects or 0
            coef_input_class = "ext_coef_disabled"
        else:
            cur_coef_value = values.get(f"coef_{ue.id}", False)
            coef_input_class = ""
        if cur_coef_value is False:  # pas dans le form, cherche en base
            ue_coef: FormSemestreUECoef = FormSemestreUECoef.query.filter_by(
                formsemestre_id=formsemestre.id, ue_id=ue.id
            ).first()
            cur_coef_value = (ue_coef.coefficient if ue_coef else "") or ""
        itemtemplate = (
            f"""
        <tr>
        <td class="tf-fieldlabel">%(label)s</td>
        <td>{ menu_code_ue }</td>
        <td class="tf-field tf_field_note">%(elem)s</td>
        """
            + (
                f"""<td class="tf-field tf_field_coef">
            <input type="text" size="4" name="coef_{ue.id}" 
            class="{coef_input_class}"
            value="{cur_coef_value}" {coef_disabled}></input>
        </td>"""
                if not formsemestre.formation.is_apc()
                else ""
            )
            + """</tr>"""
        )

        descr.append(
            (
                f"note_{ue.id}",
                {
                    "input_type": "text",
                    "size": 4,
                    "template": itemtemplate,
                    "title": "<tt>"
                    + (f"S{ue.semestre_idx} " if ue.semestre_idx is not None else "")
                    + f"<b>{ue.acronyme}</b></tt> {ue.titre}"
                    + f" ({ue.ects} ECTS)"
                    if ue.ects is not None
                    else "",
                    "attributes": [coef_disabled],
                },
            )
        )
    return descr


def _check_values(formsemestre: FormSemestre, ue_list, values):
    """Check that form values are ok
    for each UE:
        code != None => note and coef
        note or coef => code != None
        note float in [0, 20]
        note => coef
        coef float >= 0
    """
    for ue in ue_list:
        pu = f" pour UE {ue.acronyme}"
        code = values.get(f"valid_{ue.id}", False)
        if code == "None":
            code = None
        note = values.get(f"note_{ue.id}", False)
        try:
            note = _convert_field_to_float(note)
        except ValueError:
            return False, "note invalide" + pu

        if code is not False:
            if code not in _UE_VALID_CODES:
                return False, "code invalide" + pu
            if code is not None:
                if note is False or note == "":
                    return False, "note manquante" + pu
        coef = values.get(f"coef_{ue.id}", False)
        try:
            coef = _convert_field_to_float(coef)
        except ValueError:
            return False, "coefficient invalide" + pu
        if note is not False and note != "":
            if code is None:
                return (
                    False,
                    f"""code jury incohérent (code {code}, note {note}) {pu}
                     (supprimer note)""",
                )
            if note < 0 or note > 20:
                return False, "valeur note invalide" + pu
            if not isinstance(coef, float) and not formsemestre.formation.is_apc():
                return False, f"coefficient manquant pour note {note} {pu}"

        # Vérifie valeur coef seulement pour formations classiques:
        if not formsemestre.formation.is_apc():
            if coef is not False and coef != "":
                if coef < 0:
                    return False, "valeur coefficient invalide" + pu

    return True, "ok"


def _convert_field_to_float(val):
    """val may be empty, False (left unchanged), or a float. Raise exception ValueError"""
    if val is not False:
        val = val.strip()
    if val:
        val = float(val)
    return val


def _record_ue_validations_and_coefs(
    formsemestre: FormSemestre, etud: Identite, ues: list[UniteEns], values
):
    """Enregistre en base les validations
    En APC, le coef est toujours NULL
    """
    for ue in ues:
        code = values.get(f"valid_{ue.id}", False)
        if code == "None":
            code = None
        note = values.get(f"note_{ue.id}", False)
        note = _convert_field_to_float(note)
        coef = values.get(f"coef_{ue.id}", False)
        coef = _convert_field_to_float(coef)
        if coef == "" or coef is False:
            coef = None
        now_dmy = time.strftime("%d/%m/%Y")
        log(
            f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"
        )
        assert code is None or (note)  # si code validant, il faut une note
        sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
            formsemestre.id,
            etud.id,
            ue.id,
            note,
            now_dmy,
            code=code,
            ue_coefficient=coef,
        )
