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

##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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@gmail.com
#
##############################################################################

"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
"""

import datetime
import pprint

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

from app import log
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError

from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check


_evaluationEditor = ndb.EditableTable(
    "notes_evaluation",
    "evaluation_id",
    (
        "evaluation_id",
        "moduleimpl_id",
        "jour",
        "heure_debut",
        "heure_fin",
        "description",
        "note_max",
        "coefficient",
        "visibulletin",
        "publish_incomplete",
        "evaluation_type",
        "numero",
    ),
    sortkey="numero desc, jour desc, heure_debut desc",  # plus recente d'abord
    output_formators={
        "jour": ndb.DateISOtoDMY,
        "numero": ndb.int_null_is_zero,
    },
    input_formators={
        "jour": ndb.DateDMYtoISO,
        "heure_debut": ndb.TimetoISO8601,  # converti par evaluation_enrich_dict
        "heure_fin": ndb.TimetoISO8601,  # converti par evaluation_enrich_dict
        "visibulletin": bool,
        "publish_incomplete": bool,
        "evaluation_type": int,
    },
)


def evaluation_enrich_dict(e):
    """add or convert some fileds in an evaluation dict"""
    # For ScoDoc7 compat
    heure_debut_dt = e["heure_debut"] or datetime.time(
        8, 00
    )  # au cas ou pas d'heure (note externe?)
    heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
    e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
    e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
    e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
    heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
    d = ndb.TimeDuration(heure_debut, heure_fin)
    if d is not None:
        m = d % 60
        e["duree"] = "%dh" % (d / 60)
        if m != 0:
            e["duree"] += "%02d" % m
    else:
        e["duree"] = ""
    if heure_debut and (not heure_fin or heure_fin == heure_debut):
        e["descrheure"] = " à " + heure_debut
    elif heure_debut and heure_fin:
        e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
    else:
        e["descrheure"] = ""
    # matin, apresmidi: utile pour se referer aux absences:
    if heure_debut_dt < datetime.time(12, 00):
        e["matin"] = 1
    else:
        e["matin"] = 0
    if heure_fin_dt > datetime.time(12, 00):
        e["apresmidi"] = 1
    else:
        e["apresmidi"] = 0
    return e


def do_evaluation_list(args, sortkey=None):
    """List evaluations, sorted by numero (or most recent date first).

    Ajoute les champs:
    'duree' : '2h30'
    'matin' : 1 (commence avant 12:00) ou 0
    'apresmidi' : 1 (termine après 12:00) ou 0
    'descrheure' : ' de 15h00 à 16h30'
    """
    # Attention: transformation fonction ScoDc7 en SQLAlchemy
    cnx = ndb.GetDBConnexion()
    evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
    # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
    for e in evals:
        evaluation_enrich_dict(e)

    return evals


def do_evaluation_list_in_formsemestre(formsemestre_id):
    "list evaluations in this formsemestre"
    mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
    evals = []
    for modimpl in mods:
        evals += do_evaluation_list(args={"moduleimpl_id": modimpl["moduleimpl_id"]})
    return evals


def _check_evaluation_args(args):
    "Check coefficient, dates and duration, raises exception if invalid"
    moduleimpl_id = args["moduleimpl_id"]
    # check bareme
    note_max = args.get("note_max", None)
    if note_max is None:
        raise ScoValueError("missing note_max")
    try:
        note_max = float(note_max)
    except ValueError:
        raise ScoValueError("Invalid note_max value")
    if note_max < 0:
        raise ScoValueError("Invalid note_max value (must be positive or null)")
    # check coefficient
    coef = args.get("coefficient", None)
    if coef is None:
        raise ScoValueError("missing coefficient")
    try:
        coef = float(coef)
    except ValueError:
        raise ScoValueError("Invalid coefficient value")
    if coef < 0:
        raise ScoValueError("Invalid coefficient value (must be positive or null)")
    # check date
    jour = args.get("jour", None)
    args["jour"] = jour
    if jour:
        M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
        sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
        d, m, y = [int(x) for x in sem["date_debut"].split("/")]
        date_debut = datetime.date(y, m, d)
        d, m, y = [int(x) for x in sem["date_fin"].split("/")]
        date_fin = datetime.date(y, m, d)
        # passe par ndb.DateDMYtoISO pour avoir date pivot
        y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
        jour = datetime.date(y, m, d)
        if (jour > date_fin) or (jour < date_debut):
            raise ScoValueError(
                "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
                % (d, m, y)
            )
    heure_debut = args.get("heure_debut", None)
    args["heure_debut"] = heure_debut
    heure_fin = args.get("heure_fin", None)
    args["heure_fin"] = heure_fin
    if jour and ((not heure_debut) or (not heure_fin)):
        raise ScoValueError("Les heures doivent être précisées")
    d = ndb.TimeDuration(heure_debut, heure_fin)
    if d and ((d < 0) or (d > 60 * 12)):
        raise ScoValueError("Heures de l'évaluation incohérentes !")


def do_evaluation_create(
    moduleimpl_id=None,
    jour=None,
    heure_debut=None,
    heure_fin=None,
    description=None,
    note_max=None,
    coefficient=None,
    visibulletin=None,
    publish_incomplete=None,
    evaluation_type=None,
    numero=None,
    **kw,  # ceci pour absorber les arguments excedentaires de tf #sco8
):
    """Create an evaluation"""
    if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
        raise AccessDenied(
            "Modification évaluation impossible pour %s" % current_user.get_nomplogin()
        )
    args = locals()
    log("do_evaluation_create: args=" + str(args))
    _check_evaluation_args(args)
    # Check numeros
    module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
    if not "numero" in args or args["numero"] is None:
        n = None
        # determine le numero avec la date
        # Liste des eval existantes triees par date, la plus ancienne en tete
        mod_evals = do_evaluation_list(
            args={"moduleimpl_id": moduleimpl_id},
            sortkey="jour asc, heure_debut asc",
        )
        if args["jour"]:
            next_eval = None
            t = (
                ndb.DateDMYtoISO(args["jour"], null_is_empty=True),
                ndb.TimetoISO8601(args["heure_debut"], null_is_empty=True),
            )
            for e in mod_evals:
                if (
                    ndb.DateDMYtoISO(e["jour"], null_is_empty=True),
                    ndb.TimetoISO8601(e["heure_debut"], null_is_empty=True),
                ) > t:
                    next_eval = e
                    break
            if next_eval:
                n = module_evaluation_insert_before(mod_evals, next_eval)
            else:
                n = None  # a placer en fin
        if n is None:  # pas de date ou en fin:
            if mod_evals:
                log(pprint.pformat(mod_evals[-1]))
                n = mod_evals[-1]["numero"] + 1
            else:
                n = 0  # the only one
        # log("creating with numero n=%d" % n)
        args["numero"] = n

    #
    cnx = ndb.GetDBConnexion()
    r = _evaluationEditor.create(cnx, args)

    # news
    M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
    mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
    mod["moduleimpl_id"] = M["moduleimpl_id"]
    mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
    sco_news.add(
        typ=sco_news.NEWS_NOTE,
        object=moduleimpl_id,
        text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
        url=mod["url"],
    )

    return r


def do_evaluation_edit(args):
    "edit an evaluation"
    evaluation_id = args["evaluation_id"]
    the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
    if not the_evals:
        raise ValueError("evaluation inexistante !")
    moduleimpl_id = the_evals[0]["moduleimpl_id"]
    if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
        raise AccessDenied(
            "Modification évaluation impossible pour %s" % current_user.get_nomplogin()
        )
    args["moduleimpl_id"] = moduleimpl_id
    _check_evaluation_args(args)

    cnx = ndb.GetDBConnexion()
    _evaluationEditor.edit(cnx, args)
    # inval cache pour ce semestre
    M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
    sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])


def do_evaluation_delete(evaluation_id):
    "delete evaluation"
    the_evals = do_evaluation_list({"evaluation_id": evaluation_id})
    if not the_evals:
        raise ValueError("evaluation inexistante !")
    moduleimpl_id = the_evals[0]["moduleimpl_id"]
    if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
        raise AccessDenied(
            "Modification évaluation impossible pour %s" % current_user.get_nomplogin()
        )
    notes_db = do_evaluation_get_all_notes(evaluation_id)  # { etudid : value }
    notes = [x["value"] for x in notes_db.values()]
    if notes:
        raise ScoValueError(
            "Impossible de supprimer cette évaluation: il reste des notes"
        )

    cnx = ndb.GetDBConnexion()

    _evaluationEditor.delete(cnx, evaluation_id)
    # inval cache pour ce semestre
    M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
    sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
    # news

    mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
    mod["moduleimpl_id"] = M["moduleimpl_id"]
    mod["url"] = (
        scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
    )
    sco_news.add(
        typ=sco_news.NEWS_NOTE,
        object=moduleimpl_id,
        text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
        url=mod["url"],
    )


# ancien _notes_getall
def do_evaluation_get_all_notes(
    evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
):
    """Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
    Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
    """
    do_cache = (
        filter_suppressed and table == "notes_notes" and (by_uid is None)
    )  # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
    if do_cache:
        r = sco_cache.EvaluationCache.get(evaluation_id)
        if r != None:
            return r
    cnx = ndb.GetDBConnexion()
    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
    cond = " where evaluation_id=%(evaluation_id)s"
    if by_uid:
        cond += " and uid=%(by_uid)s"

    cursor.execute(
        "select * from " + table + cond,
        {"evaluation_id": evaluation_id, "by_uid": by_uid},
    )
    res = cursor.dictfetchall()
    d = {}
    if filter_suppressed:
        for x in res:
            if x["value"] != scu.NOTES_SUPPRESS:
                d[x["etudid"]] = x
    else:
        for x in res:
            d[x["etudid"]] = x
    if do_cache:
        status = sco_cache.EvaluationCache.set(evaluation_id, d)
        if not status:
            log(f"Warning: EvaluationCache.set: {evaluation_id}\t{status}")
    return d


def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0):
    """Renumber evaluations in this module, according to their date. (numero=0: oldest one)
    Needed because previous versions of ScoDoc did not have eval numeros
    Note: existing numeros are ignored
    """
    redirect = int(redirect)
    # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id )
    # List sorted according to date/heure, ignoring numeros:
    # (note that we place  evaluations with NULL date at the end)
    mod_evals = do_evaluation_list(
        args={"moduleimpl_id": moduleimpl_id},
        sortkey="jour asc, heure_debut asc",
    )

    all_numbered = False not in [x["numero"] > 0 for x in mod_evals]
    if all_numbered and only_if_unumbered:
        return  # all ok

    # Reset all numeros:
    i = 1
    for e in mod_evals:
        e["numero"] = i
        do_evaluation_edit(e)
        i += 1

    # If requested, redirect to moduleimpl page:
    if redirect:
        return flask.redirect(
            url_for(
                "notes.moduleimpl_status",
                scodoc_dept=g.scodoc_dept,
                moduleimpl_id=moduleimpl_id,
            )
        )


def module_evaluation_insert_before(mod_evals, next_eval):
    """Renumber evals such that an evaluation with can be inserted before next_eval
    Returns numero suitable for the inserted evaluation
    """
    if next_eval:
        n = next_eval["numero"]
        if not n:
            log("renumbering old evals")
            module_evaluation_renumber(next_eval["moduleimpl_id"])
            next_eval = do_evaluation_list(
                args={"evaluation_id": next_eval["evaluation_id"]}
            )[0]
            n = next_eval["numero"]
    else:
        n = 1
    # log('inserting at position numero %s' % n )
    # all numeros >= n are incremented
    for e in mod_evals:
        if e["numero"] >= n:
            e["numero"] += 1
            # log('incrementing %s to %s' % (e['evaluation_id'], e['numero']))
            do_evaluation_edit(e)

    return n


def module_evaluation_move(evaluation_id, after=0, redirect=1):
    """Move before/after previous one (decrement/increment numero)
    (published)
    """
    e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]
    redirect = int(redirect)
    # access: can change eval ?
    if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]):
        raise AccessDenied(
            "Modification évaluation impossible pour %s" % current_user.get_nomplogin()
        )

    module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True)
    e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0]

    after = int(after)  # 0: deplace avant, 1 deplace apres
    if after not in (0, 1):
        raise ValueError('invalid value for "after"')
    mod_evals = do_evaluation_list({"moduleimpl_id": e["moduleimpl_id"]})
    if len(mod_evals) > 1:
        idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
        neigh = None  # object to swap with
        if after == 0 and idx > 0:
            neigh = mod_evals[idx - 1]
        elif after == 1 and idx < len(mod_evals) - 1:
            neigh = mod_evals[idx + 1]
        if neigh:  #
            if neigh["numero"] == e["numero"]:
                log("Warning: module_evaluation_move: forcing renumber")
                module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False)
            else:
                # swap numero with neighbor
                e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
                do_evaluation_edit(e)
                do_evaluation_edit(neigh)
    # redirect to moduleimpl page:
    if redirect:
        return flask.redirect(
            url_for(
                "notes.moduleimpl_status",
                scodoc_dept=g.scodoc_dept,
                moduleimpl_id=e["moduleimpl_id"],
            )
        )