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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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
#
##############################################################################

"""Import / Export de formations
"""
import xml.dom.minidom

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

import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import db
from app import log
from app.models import Formation, FormSemestre, Module, UniteEns
from app.models import ScolarNews
from app.models.but_refcomp import (
    ApcAppCritique,
    ApcCompetence,
    ApcNiveau,
    ApcParcours,
    ApcReferentielCompetences,
)
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
from app.scodoc import sco_xml
import sco_version
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.sco_permissions import Permission

_formationEditor = ndb.EditableTable(
    "notes_formations",
    "formation_id",
    (
        "formation_id",
        "acronyme",
        "titre",
        "titre_officiel",
        "version",
        "formation_code",
        "type_parcours",
        "code_specialite",
        "referentiel_competence_id",
        "commentaire",
    ),
    filter_dept=True,
    sortkey="acronyme",
)


def formation_list(formation_id=None, args={}):  ### XXX obsolete, à supprimer
    """List formation(s) with given id, or matching args
    (when args is given, formation_id is ignored).
    """
    if not args:
        if formation_id is None:
            args = {}
        else:
            args = {"formation_id": formation_id}
    cnx = ndb.GetDBConnexion()
    r = _formationEditor.list(cnx, args=args)
    return r


def formation_export_dict(
    formation: Formation,
    export_ids=False,
    export_tags=True,
    export_external_ues=False,
    export_codes_apo=True,
    ac_as_list=False,
    ue_reference_style="id",
) -> dict:
    """Get a formation, with UE, matieres, modules...
    as a deep dict.
    ac_as_list spécifie le format des Appentissages Critiques.
    """
    f_dict = formation.to_dict(with_refcomp_attrs=True)
    if not export_ids:
        del f_dict["id"]
        del f_dict["formation_id"]
        del f_dict["dept_id"]
    ues = formation.ues
    if not export_external_ues:
        ues = ues.filter_by(is_external=False)
    ues = ues.all()
    ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
    f_dict["ue"] = []
    ue: UniteEns
    for ue in ues:
        ue_dict = ue.to_dict()
        f_dict["ue"].append(ue_dict)
        ue_dict.pop("module_ue_coefs", None)
        if formation.is_apc():
            # BUT: indique niveau de compétence associé à l'UE
            if ue.niveau_competence:
                ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
                ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
                ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre

        # pour les coefficients:
        ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
        if not export_ids:
            for id_id in (
                "id",
                "ue_id",
                "formation_id",
                "parcour_id",
                "niveau_competence_id",
            ):
                ue_dict.pop(id_id, None)

        if not export_codes_apo:
            ue_dict.pop("code_apogee", None)
        if ue_dict.get("ects") is None:
            ue_dict.pop("ects", None)
        mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
        mats.sort(key=lambda m: m["numero"] or 0)
        ue_dict["matiere"] = mats
        for mat in mats:
            matiere_id = mat["matiere_id"]
            if not export_ids:
                del mat["id"]
                del mat["matiere_id"]
                del mat["ue_id"]
            mods = sco_edit_module.module_list({"matiere_id": matiere_id})
            mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
            mat["module"] = mods
            for mod in mods:
                module_id = mod["module_id"]
                if export_tags:
                    tags = sco_tag_module.module_tag_list(module_id=mod["module_id"])
                    if tags:
                        mod["tags"] = [{"name": x} for x in tags]
                #
                module: Module = db.session.get(Module, module_id)
                if module.is_apc():
                    # Exporte les coefficients
                    if ue_reference_style == "id":
                        mod["coefficients"] = [
                            {"ue_reference": str(ue_id), "coef": str(coef)}
                            for (ue_id, coef) in module.get_ue_coef_dict().items()
                        ]
                    else:
                        mod["coefficients"] = [
                            {"ue_reference": ue_acronyme, "coef": str(coef)}
                            for (
                                ue_acronyme,
                                coef,
                            ) in module.get_ue_coef_dict_acronyme().items()
                        ]
                    # Et les parcours
                    mod["parcours"] = [
                        p.to_dict(with_annees=False) for p in module.parcours
                    ]
                    # Et les AC
                    if ac_as_list:
                        # XML préfère une liste
                        mod["app_critiques"] = [
                            x.to_dict(with_code=True) for x in module.app_critiques
                        ]
                    else:
                        mod["app_critiques"] = {
                            x.code: x.to_dict() for x in module.app_critiques
                        }
                if not export_ids:
                    del mod["id"]
                    del mod["ue_id"]
                    del mod["matiere_id"]
                    del mod["module_id"]
                    del mod["formation_id"]
                if not export_codes_apo:
                    del mod["code_apogee"]
                if mod["ects"] is None:
                    del mod["ects"]
    return f_dict


def formation_export(
    formation_id,
    export_ids=False,
    export_tags=True,
    export_external_ues=False,
    export_codes_apo=True,
    fmt=None,
) -> flask.Response:
    """Get a formation, with UE, matieres, modules
    in desired format
    """
    formation: Formation = Formation.query.get_or_404(formation_id)
    f_dict = formation_export_dict(
        formation,
        export_ids=export_ids,
        export_tags=export_tags,
        export_external_ues=export_external_ues,
        export_codes_apo=export_codes_apo,
        ac_as_list=fmt == "xml",
    )
    filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
    return scu.sendResult(
        f_dict,
        name="formation",
        fmt=fmt,
        force_outer_xml_tag=False,
        attached=True,
        filename=filename,
    )


def _formation_retreive_refcomp(f_dict: dict) -> int:
    """Recherche si on un référentiel de compétence chargé pour
    cette formation: utilise comme clé (version_orebut, specialite, type_titre)
    Retourne: referentiel_competence_id ou None
    """
    refcomp_version_orebut = f_dict.get("refcomp_version_orebut")
    refcomp_specialite = f_dict.get("refcomp_specialite")
    refcomp_type_titre = f_dict.get("refcomp_type_titre")
    if all((refcomp_version_orebut, refcomp_specialite, refcomp_type_titre)):
        refcomp = ApcReferentielCompetences.query.filter_by(
            dept_id=g.scodoc_dept_id,
            type_titre=refcomp_type_titre,
            specialite=refcomp_specialite,
            version_orebut=refcomp_version_orebut,
        ).first()
        if refcomp:
            return refcomp.id
        else:
            flash(
                f"Impossible de trouver le référentiel de compétence pour {refcomp_specialite} : est-il chargé ?"
            )
    return None


def _formation_retreive_apc_niveau(
    referentiel_competence_id: int, ue_dict: dict
) -> int:
    """Recherche dans le ref. de comp. un niveau pour cette UE.
    Utilise (libelle, annee, ordre) comme clé.
    """
    libelle = ue_dict.get("apc_niveau_libelle")
    annee = ue_dict.get("apc_niveau_annee")
    ordre = ue_dict.get("apc_niveau_ordre")
    if all((libelle, annee, ordre)):
        niveau = (
            ApcNiveau.query.filter_by(libelle=libelle, annee=annee, ordre=ordre)
            .join(ApcCompetence)
            .filter_by(referentiel_id=referentiel_competence_id)
        ).first()
        if niveau is not None:
            return niveau.id
    return None


def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
    """Create a formation from XML representation
    (format dumped by formation_export( fmt='xml' ))
    XML may contain object (UE, modules) ids: this function returns two
    dicts mapping these ids to the created ids.

    Args:
        doc:    str, xml data
        import_tags: if false, does not import tags on modules.
        use_local_refcomp: if True, utilise les id vers les ref. de compétences.
    Returns:
        formation_id, modules_old2new, ues_old2new
    """
    from app.scodoc import sco_edit_formation

    try:
        dom = xml.dom.minidom.parseString(doc)
    except Exception as exc:
        log("formation_import_xml: invalid XML data")
        raise ScoValueError("Fichier XML invalide") from exc

    try:
        f = dom.getElementsByTagName("formation")[0]  # or dom.documentElement
        D = sco_xml.xml_to_dicts(f)
    except Exception as exc:
        raise ScoFormatError(
            """Ce document xml ne correspond pas à un programme exporté par ScoDoc.
            (élément 'formation' inexistant par exemple)."""
        ) from exc
    assert D[0] == "formation"
    f_dict = D[1]
    f_dict["dept_id"] = g.scodoc_dept_id
    # Pour les clonages, on prend le refcomp_id donné:
    referentiel_competence_id = (
        f_dict.get("referentiel_competence_id") if use_local_refcomp else None
    )
    # Sinon, on cherche a retrouver le ref. comp.
    if referentiel_competence_id is None:
        referentiel_competence_id = _formation_retreive_refcomp(f_dict)
    f_dict["referentiel_competence_id"] = referentiel_competence_id
    # find new version number
    acronyme_lower = f_dict["acronyme"].lower() if f_dict["acronyme"] else ""
    titre_lower = f_dict["titre"].lower() if f_dict["titre"] else ""
    formations: list[Formation] = Formation.query.filter_by(
        dept_id=f_dict["dept_id"]
    ).filter(
        db.func.lower(Formation.acronyme) == acronyme_lower,
        db.func.lower(Formation.titre) == titre_lower,
    )
    if formations.count():
        version = max(f.version or 0 for f in formations)
    else:
        version = 0
    f_dict["version"] = version + 1

    # create formation
    formation = sco_edit_formation.do_formation_create(f_dict)
    log(f"formation {formation.id} created")

    ues_old2new = {}  # xml ue_id : new ue_id
    modules_old2new = {}  # xml module_id : new module_id
    # (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)

    ue_reference_to_id = {}  # pour les coefs APC (map reference -> ue_id)
    modules_a_coefficienter = []  # Liste des modules avec coefs APC
    with sco_cache.DeferredSemCacheManager():
        # -- create UEs
        for ue_info in D[2]:
            assert ue_info[0] == "ue"
            ue_info[1]["formation_id"] = formation.id
            if "ue_id" in ue_info[1]:
                xml_ue_id = int(ue_info[1]["ue_id"])
                del ue_info[1]["ue_id"]
            else:
                xml_ue_id = None
            if referentiel_competence_id is None:
                if "niveau_competence_id" in ue_info[1]:
                    del ue_info[1]["niveau_competence_id"]
            else:
                ue_info[1]["niveau_competence_id"] = _formation_retreive_apc_niveau(
                    referentiel_competence_id, ue_info[1]
                )
            # Note: si le code est indiqué "" dans le xml, il faut le conserver vide
            # pour la comparaison ultérieure des formations XXX
            ue_id = sco_edit_ue.do_ue_create(ue_info[1], allow_empty_ue_code=True)
            ue: UniteEns = db.session.get(UniteEns, ue_id)
            assert ue
            if xml_ue_id:
                ues_old2new[xml_ue_id] = ue_id

            # élément optionnel présent dans les exports BUT:
            ue_reference = ue_info[1].get("reference")
            if ue_reference:
                ue_reference_to_id[int(ue_reference)] = ue_id

            # -- Create matieres
            for mat_info in ue_info[2]:
                # Backward compat: un seul parcours par UE (ScoDoc < 9.4.71)
                if mat_info[0] == "parcour":
                    # Parcours (BUT)
                    code_parcours = mat_info[1]["code"]
                    parcour = ApcParcours.query.filter_by(
                        code=code_parcours,
                        referentiel_id=referentiel_competence_id,
                    ).first()
                    if parcour:
                        ue.parcours = [parcour]
                        db.session.add(ue)
                    else:
                        flash(f"Attention: parcours {code_parcours} inexistant !")
                        log(f"Warning: parcours {code_parcours} inexistant !")
                    continue
                elif mat_info[0] == "parcours":
                    # Parcours (BUT), liste (ScoDoc > 9.4.70), avec ECTS en option
                    code_parcours = mat_info[1]["code"]
                    ue_parcour_ects = mat_info[1].get("ects")
                    parcour = ApcParcours.query.filter_by(
                        code=code_parcours,
                        referentiel_id=referentiel_competence_id,
                    ).first()
                    if parcour:
                        ue.parcours.append(parcour)
                    else:
                        flash(f"Attention: parcours {code_parcours} inexistant !")
                        log(f"Warning: parcours {code_parcours} inexistant !")
                    if ue_parcour_ects is not None:
                        ue.set_ects(ue_parcour_ects, parcour)
                    db.session.add(ue)
                    continue

                assert mat_info[0] == "matiere"
                mat_info[1]["ue_id"] = ue_id
                mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
                # -- create modules
                for mod_info in mat_info[2]:
                    assert mod_info[0] == "module"
                    if "module_id" in mod_info[1]:
                        xml_module_id = int(mod_info[1]["module_id"])
                        del mod_info[1]["module_id"]
                    else:
                        xml_module_id = None
                    mod_info[1]["formation_id"] = formation.id
                    mod_info[1]["matiere_id"] = mat_id
                    mod_info[1]["ue_id"] = ue_id
                    if not "module_type" in mod_info[1]:
                        mod_info[1]["module_type"] = scu.ModuleType.STANDARD
                    mod_id = sco_edit_module.do_module_create(mod_info[1])
                    if xml_module_id:
                        modules_old2new[int(xml_module_id)] = mod_id
                    if len(mod_info) > 2:
                        module: Module = db.session.get(Module, mod_id)
                        tag_names = []
                        ue_coef_dict = {}
                        for child in mod_info[2]:
                            if child[0] == "tags" and import_tags:
                                tag_names.append(child[1]["name"])
                            elif child[0] == "coefficients":
                                ue_reference = int(child[1]["ue_reference"])
                                coef = float(child[1]["coef"])
                                ue_coef_dict[ue_reference] = coef
                            elif child[0] == "app_critiques" and (
                                referentiel_competence_id is not None
                            ):
                                ac_code = child[1]["code"]
                                ac = (
                                    ApcAppCritique.query.filter_by(code=ac_code)
                                    .join(ApcNiveau)
                                    .join(ApcCompetence)
                                    .filter_by(referentiel_id=referentiel_competence_id)
                                ).first()
                                if ac is not None:
                                    module.app_critiques.append(ac)
                                    db.session.add(module)
                                else:
                                    log(f"Warning: AC {ac_code} inexistant !")

                            elif child[0] == "parcours":
                                # Si on a un référentiel de compétences,
                                # associe les parcours de ce module (BUT)
                                if referentiel_competence_id is not None:
                                    code_parcours = child[1]["code"]
                                    parcour = ApcParcours.query.filter_by(
                                        code=code_parcours,
                                        referentiel_id=referentiel_competence_id,
                                    ).first()
                                    if parcour:
                                        module.parcours.append(parcour)
                                        db.session.add(module)
                                    else:
                                        log(
                                            f"Warning: parcours {code_parcours} inexistant !"
                                        )
                        if import_tags and tag_names:
                            sco_tag_module.module_tag_set(mod_id, tag_names)
                        if module.is_apc() and ue_coef_dict:
                            modules_a_coefficienter.append((module, ue_coef_dict))
        # Fixe les coefs APC (à la fin pour que les UE soient créées)
        for module, ue_coef_dict_ref in modules_a_coefficienter:
            # remap ue ids:
            ue_coef_dict = {
                ue_reference_to_id[k]: v for (k, v) in ue_coef_dict_ref.items()
            }
            module.set_ue_coef_dict(ue_coef_dict)
        db.session.commit()
    return formation.id, modules_old2new, ues_old2new


def formation_list_table(detail: bool) -> GenTable:
    """List formation, grouped by titre and sorted by versions
    and listing associated semestres.
    If detail, add column with more details.
    returns a table
    """
    formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
    title = "Formations (programmes pédagogiques)"
    lockicon = scu.icontag(
        "lock32_img", title="Comporte des semestres verrouillés", border="0"
    )
    suppricon = scu.icontag(
        "delete_small_img", border="0", alt="supprimer", title="Supprimer"
    )
    editicon = scu.icontag(
        "edit_img", border="0", alt="modifier", title="Modifier titres et code"
    )

    editable = current_user.has_permission(Permission.EditFormation)
    can_implement = current_user.has_permission(Permission.EditFormSemestre)

    # Traduit/ajoute des champs à afficher:
    rows = []
    for formation in formations:
        acronyme_no_spaces = formation.acronyme.lower().replace(" ", "-")
        row = {
            "acronyme": formation.acronyme,
            "parcours_name": codes_cursus.get_cursus_from_code(
                formation.type_parcours
            ).NAME,
            "titre": formation.titre,
            "_titre_target": url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=formation.id,
            ),
            "_titre_link_class": "stdlink",
            "_titre_id": f"""titre-{acronyme_no_spaces}""",
            "version": formation.version or 0,
            "commentaire": formation.commentaire or "",
            "referentiel": (
                f"""{formation.referentiel_competence.specialite} {
                    formation.referentiel_competence.get_version()}"""
                if formation.referentiel_competence
                else ""
            ),
            "_referentiel_target": (
                url_for(
                    "notes.refcomp_show",
                    scodoc_dept=g.scodoc_dept,
                    refcomp_id=formation.referentiel_competence.id,
                )
                if formation.referentiel_competence
                else ""
            ),
        }
        # Ajoute les semestres associés à chaque formation:
        row["formsemestres"] = formation.formsemestres.order_by(
            FormSemestre.date_debut
        ).all()
        row["sems_list_txt"] = ", ".join(s.session_id() for s in row["formsemestres"])
        row["_sems_list_txt_html"] = ", ".join(
            [
                f"""<a class="discretelink" href="{
                url_for("notes.formsemestre_status",
                    scodoc_dept=g.scodoc_dept, formsemestre_id=s.id
                )}">{s.session_id()}</a>"""
                for s in row["formsemestres"]
            ]
            + (
                [
                    f"""<a class="stdlink"
                href="{ url_for("notes.formsemestre_createwithmodules",
                scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
                )
                }">ajouter</a>
            """
                ]
                if can_implement
                else []
            )
        )
        # Répartition des UEs dans les semestres
        # utilise pour voir si la formation couvre tous les semestres
        row["semestres_ues"] = ", ".join(
            "S" + str(x if (x is not None and x > 0) else "-")
            for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
        )
        # Date surtout utilisées pour le tri:
        if row["formsemestres"]:
            row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
            row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
        else:
            row["date_fin_dernier_sem"] = ""
            row["annee_dernier_sem"] = 0
        #
        if formation.has_locked_sems():
            but_locked = lockicon
            but_suppr = '<span class="but_placeholder"></span>'
        else:
            but_locked = '<span class="but_placeholder"></span>'
            if editable:
                but_suppr = f"""<a class="stdlink" href="{
                        url_for("notes.formation_delete",
                        scodoc_dept=g.scodoc_dept, formation_id=formation.id
                    )}" id="delete-formation-{acronyme_no_spaces}">{suppricon}</a>"""
            else:
                but_suppr = '<span class="but_placeholder"></span>'
        if editable:
            but_edit = f"""<a class="stdlink" href="{
                url_for("notes.formation_edit", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
                }" id="edit-formation-{acronyme_no_spaces}">{editicon}</a>"""
        else:
            but_edit = '<span class="but_placeholder"></span>'
        row["buttons"] = ""
        row["_buttons_html"] = but_locked + but_suppr + but_edit
        rows.append(row)
    # Tri par annee_dernier_sem, type, acronyme, titre, version décroissante
    # donc plus récemment utilisée en tête
    rows.sort(
        key=lambda row: (
            -row["annee_dernier_sem"],
            row["parcours_name"],
            row["acronyme"],
            row["titre"],
            -row["version"],
        )
    )
    for i, row in enumerate(rows):
        row["_buttons_order"] = f"{i:05d}"

    #
    columns_ids = (
        "buttons",
        "acronyme",
        "parcours_name",
        "formation_code",
        "version",
        "titre",
        "referentiel",
        "commentaire",
        "sems_list_txt",
    )
    if detail:
        columns_ids += ("annee_dernier_sem", "semestres_ues")
    titles = {
        "buttons": "",
        "commentaire": "Commentaire",
        "acronyme": "Acro.",
        "parcours_name": "Type",
        "titre": "Titre",
        "version": "Version",
        "formation_code": "Code",
        "sems_list_txt": "Semestres",
        "referentiel": "Réf.",
        "date_fin_dernier_sem": "Fin dernier sem.",
        "annee_dernier_sem": "Année dernier sem.",
        "semestres_ues": "Semestres avec UEs",
    }
    return GenTable(
        columns_ids=columns_ids,
        rows=rows,
        titles=titles,
        origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
        caption=title,
        html_caption=title,
        table_id="formation_list_table",
        html_class="formation_list_table table_leftalign",
        html_with_td_classes=True,
        html_sortable=True,
        base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
        page_title=title,
        pdf_title=title,
        preferences=sco_preferences.SemPreferences(),
    )


def formation_create_new_version(formation_id, redirect=True):
    "duplicate formation, with new version number"
    formation = Formation.query.get_or_404(formation_id)
    resp = formation_export(
        formation_id, export_ids=True, export_external_ues=True, fmt="xml"
    )
    xml_data = resp.get_data(as_text=True)
    new_id, modules_old2new, ues_old2new = formation_import_xml(
        xml_data, use_local_refcomp=True
    )
    # news
    ScolarNews.add(
        typ=ScolarNews.NEWS_FORM,
        obj=new_id,
        text=f"Nouvelle version de la formation {formation.acronyme}",
        max_frequency=0,
    )
    if redirect:
        flash("Nouvelle version !")
        return flask.redirect(
            url_for(
                "notes.ue_table",
                scodoc_dept=g.scodoc_dept,
                formation_id=new_id,
                msg="Nouvelle version !",
            )
        )
    return new_id, modules_old2new, ues_old2new