# -*- 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
#
##############################################################################

"""Association de nouvelles versions de formation à des formsemestre
"""
import flask
from flask import url_for, flash
from flask import g, request

from app import db
from app.models import (
    Module,
    ModuleImpl,
    Evaluation,
    EvaluationUEPoids,
    ScolarEvent,
    ScolarFormSemestreValidation,
    UniteEns,
)

from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu

from app import log
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formations


def formsemestre_associate_new_version(
    formation_id: int,
    formsemestre_id: int = None,
    other_formsemestre_ids: list[int] = None,
):
    """Formulaire nouvelle version formation et association d'un ou plusieurs formsemestre.
    formation_id: la formation à dupliquer
    formsemestre_id: optionnel, formsemestre de départ, qui sera associé à la nouvelle version
    """
    formsemestre_id = int(formsemestre_id) if formsemestre_id else None
    formation: Formation = Formation.query.get_or_404(formation_id)
    other_formsemestre_ids = {int(x) for x in (other_formsemestre_ids or [])}
    if request.method == "GET":
        # dresse la liste des semestres non verrouillés de la même formation
        other_formsemestres: list[FormSemestre] = formation.formsemestres.filter_by(
            etat=True
        )

        H = []
        for other_formsemestre in other_formsemestres:
            checked = (
                'checked="checked"'
                if (
                    other_formsemestre.id == formsemestre_id
                    or other_formsemestre.id in other_formsemestre_ids
                )
                else ""
            )
            disabled = (
                'disabled="1"' if other_formsemestre.id == formsemestre_id else ""
            )

            H.append(
                f"""<div><input type="checkbox" name="other_formsemestre_ids:list"
                value="{other_formsemestre.id}" {checked} {disabled}
                ><a class="stdlink" href="{
                    url_for("notes.formsemestre_status",
                        scodoc_dept=g.scodoc_dept, formsemestre_id=other_formsemestre.id)
                }">{other_formsemestre.titre_mois()}</a></input></div>"""
            )
        if formsemestre_id is None:
            cancel_url = url_for(
                "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
            )
        else:
            cancel_url = url_for(
                "notes.formsemestre_status",
                scodoc_dept=g.scodoc_dept,
                formsemestre_id=formsemestre_id,
            )

        return scu.confirm_dialog(
            (
                """<h2>Associer à une nouvelle version de formation non verrouillée ?</h2>"""
                if formsemestre_id
                else """<h2>Créer une nouvelle version de la formation ?</h2>"""
            )
            + f"""<p><b>Formation: </b><a class="stdlink" href="{
                url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
                }">{formation.titre} version {formation.version}</a></p>

                <p class="help">Le programme pédagogique ("formation") va être dupliqué
                pour que vous puissiez le modifier sans affecter les semestres déjà terminés.
                </p>
                <p class="help">Veillez à ne pas abuser de cette possibilité, car créer
                trop de versions de formations va vous compliquer la gestion
                (à vous de garder trace des différences et à ne pas vous
                tromper par la suite...).
                </p>
                <div class="othersemlist">
                    <p>Si vous voulez associer des semestres à la nouvelle
                    version, cochez-les maintenant <br>
                    (<b>attention&nbsp;: vous ne pourrez pas le faire plus tard car on ne peut pas
                    changer la formation d'un semestre !</b>):
                    </p>
                    { "".join(H) }
                    <p>Les données (étudiants, notes...) de ces semestres seront inchangées.</p>
                </div>
            """
            + (
                f"""
                <div class="othersemlist">
                Vous pouvez aussi essayer d'<a class="stdlink" href="{url_for(
                    "notes.formsemestre_change_formation",
                    scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
                )}
                ">associer ce semestre à une autre formation identique</a>.
                </div>
                """
                if formsemestre_id is not None
                else ""
            ),
            OK="Créer une nouvelle version et y associer ces semestres",
            dest_url="",
            cancel_url=cancel_url,
            parameters={
                "formation_id": formation_id,
                "formsemestre_id": formsemestre_id,
            },
        )
    elif request.method == "POST":
        if formsemestre_id is not None:  # pas dans le form car checkbox disabled
            other_formsemestre_ids |= {formsemestre_id}
        new_formation_id = do_formsemestres_associate_new_version(
            formation_id, other_formsemestre_ids
        )
        flash(
            "Nouvelle version de la formation créée"
            + (" et semestres associés." if other_formsemestre_ids else ".")
        )
        if formsemestre_id is None:
            return flask.redirect(
                url_for(
                    "notes.ue_table",
                    scodoc_dept=g.scodoc_dept,
                    formation_id=new_formation_id,
                )
            )
        return flask.redirect(
            url_for(
                "notes.formsemestre_status",
                scodoc_dept=g.scodoc_dept,
                formsemestre_id=formsemestre_id,
            )
        )
    raise ScoValueError("Méthode invalide")


def do_formsemestres_associate_new_version(
    formation_id: int, formsemestre_ids: list[int]
) -> int:
    """Crée une nouvelle version de la formation du semestre, et y rattache les semestres.
    Tous les moduleimpl sont ré-associés à la nouvelle formation, ainsi que les decisions de jury
    si elles existent (codes d'UE validées).
    Les semestre doivent tous appartenir à la meme version de la formation.
    renvoie l'id de la nouvelle formation.
    """
    log(f"do_formsemestres_associate_new_version {formation_id} {formsemestre_ids}")

    # Check: tous les semestres de la formation
    formsemestres = [FormSemestre.query.get_or_404(i) for i in formsemestre_ids]
    if not all(
        [formsemestre.formation_id == formation_id for formsemestre in formsemestres]
    ):
        raise ScoValueError("les semestres ne sont pas tous de la même formation !")

    # New formation:
    (
        new_formation_id,
        modules_old2new,
        ues_old2new,
    ) = sco_formations.formation_create_new_version(formation_id, redirect=False)
    # Log new ues:
    for ue_id in ues_old2new:
        ue = db.session.get(UniteEns, ue_id)
        new_ue = db.session.get(UniteEns, ues_old2new[ue_id])
        assert ue.semestre_idx == new_ue.semestre_idx
        log(f"{ue} -> {new_ue}")
    # Log new modules
    for module_id in modules_old2new:
        mod = db.session.get(Module, module_id)
        new_mod = db.session.get(Module, modules_old2new[module_id])
        assert mod.semestre_id == new_mod.semestre_id
        log(f"{mod} -> {new_mod}")
    # re-associate
    for formsemestre_id in formsemestre_ids:
        formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
        formsemestre.formation_id = new_formation_id
        db.session.add(formsemestre)
        _reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)

    db.session.commit()
    return new_formation_id


def _reassociate_moduleimpls(
    formsemestre: FormSemestre,
    ues_old2new: dict[int, int],
    modules_old2new: dict[int, int],
):
    """Associe les moduleimpls d'un semestre existant à un autre programme
    et met à jour les décisions de jury (validations d'UE).
    """
    # re-associate moduleimpls to new modules:
    for modimpl in formsemestre.modimpls:
        modimpl.module_id = modules_old2new[modimpl.module_id]
        db.session.add(modimpl)
    # Update poids des évaluations
    # les poids associent les évaluations aux UE (qui ont changé d'id)
    for poids in EvaluationUEPoids.query.filter(
        EvaluationUEPoids.evaluation_id == Evaluation.id,
        Evaluation.moduleimpl_id == ModuleImpl.id,
        ModuleImpl.formsemestre_id == formsemestre.id,
    ):
        if poids.ue_id in ues_old2new:
            poids.ue_id = ues_old2new[poids.ue_id]
            db.session.add(poids)
        else:
            # poids vers une UE qui n'est pas ou plus dans notre formation
            db.session.delete(poids)

    # update decisions:
    for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
        if event.ue_id is not None:
            event.ue_id = ues_old2new[event.ue_id]
        db.session.add(event)

    for validation in ScolarFormSemestreValidation.query.filter_by(
        formsemestre_id=formsemestre.id
    ):
        if (validation.ue_id is not None) and validation.ue_id in ues_old2new:
            validation.ue_id = ues_old2new[validation.ue_id]
            # si l'UE n'est pas ou plus dans notre formation, laisse.
        db.session.add(validation)

    db.session.commit()


def formations_are_equals(
    formation1: Formation, formation2: Formation = None, formation2_dict: dict = None
) -> bool:
    """True if the two formations are exactly the same, except for their versions.
    Can specify either formation2 or its dict repr.
    """
    fd1 = sco_formations.formation_export_dict(
        formation1, export_external_ues=True, ue_reference_style="acronyme"
    )
    if formation2_dict is None:
        if formation2 is None:
            raise ValueError("must specify formation2 or formation2_dict")
        formation2_dict = sco_formations.formation_export_dict(
            formation2, export_external_ues=True, ue_reference_style="acronyme"
        )
    del fd1["version"]
    if "version" in formation2_dict:
        del formation2_dict["version"]
    return fd1 == formation2_dict


def formsemestre_change_formation(formsemestre: FormSemestre, new_formation: Formation):
    """Change la formation d'un semestre. La nouvelle formation doit avoir exactement
    le même contenu que l'actuelle, à l'exception du numéro de version.
    """
    if not formations_are_equals(formsemestre.formation, new_formation):
        raise ScoValueError(
            "formsemestre_change_formation: les deux formations diffèrent"
        )
    log(
        f"formsemestre_change_formation: formsemestre {formsemestre} to formation {new_formation}"
    )
    # Il faut ré-associer tous les modimpls et les UEs
    modules_old2new = {}
    for modimpl in formsemestre.modimpls:
        old_module: Module = modimpl.module
        new_module: Module = (
            Module.query.filter_by(
                formation_id=new_formation.id,
                code=old_module.code,
                titre=old_module.titre,
            )
            .join(UniteEns)
            .filter_by(acronyme=old_module.ue.acronyme)
            .first()
        )
        if new_module is None:
            raise ValueError(
                f"formsemestre_change_formation: erreur sur module {old_module}"
            )
        modules_old2new[old_module.id] = new_module.id

    ues_old2new = {}
    for old_ue in formsemestre.formation.ues:
        new_ue: UniteEns = UniteEns.query.filter_by(
            formation_id=new_formation.id, acronyme=old_ue.acronyme, titre=old_ue.titre
        ).first()
        if new_ue is None:
            raise ValueError(f"formsemestre_change_formation: erreur sur UE {old_ue}")
        ues_old2new[old_ue.id] = new_ue.id

    formsemestre.formation = new_formation
    db.session.add(formsemestre)
    _reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)

    db.session.commit()