# -*- coding: UTF-8 -*-
###########################################################################
# Eole NG - 2007
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# schedule.py
#
# Classes de gestion de la périodicité des tâches
#
###########################################################################
from zephir.backend import config
from zephir.backend.config import log
from zephir.backend.lib_backend import cx_pool, ResourceAuthError

from twisted.internet import task
import datetime, calendar, traceback

class Task:
    """Classe représentant une tâche à effectuer à un moment donné

    sous classes définies:

    WeekdayTask (weekdays, hour, min)
    MonthdayTask (monthdays, hour, min)
    DayTask(hour, min)
    IntervalTask (interval)
    SingleTask (month, day, hour, min)
    """

    def __init__(self, id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date=None):
        self.id_task = id_task
        self.name = name
        self.cmd = cmd
        self.month = month
        self.day = day
        self.min = min
        self.hour = hour
        self.week_day = week_day
        self.periodicity = periodicity
        self.exec_date = exec_date

    def __cmp__(self, b):
        if self.__class__.__name__ == b.__class__.__name__:
            return self.id_task.__cmp__(b.id_task)
        else:
            return self.periodicity.__cmp__(b.periodicity)

    def __str__(self):
        return "%d (%s) -> %s" % (self.id_task, self.name, self.__repr__())

    def update_date(self):
        # sauvegarde de la prochaine date d'exécution
        cu = cx_pool.create()
        try:
            query = """update tasks set exec_date=%s where id_task=%s"""
            params = (self.exec_date.strftime('%c'),self.id_task)
            cu.execute(query, params)
            cx_pool.commit(cu)
        except:
            import traceback
            traceback.print_exc()
            cx_pool.rollback(cu)

    def check_time(self):
        """méthode à redéfinir dans les classes filles
        doit retourner True si il est temps d'exécuter la tâche, False sinon.
        Dans le cas ou True est retourné, l'attribut exec_date doit être mis à jour pour
        refléter la prochaine date d'exécution.
        """
        pass

    def run(self, serveurs):
        """met en place l'appel de la tache pour tous les serveurs concernés"""
        # mise en place de la commande
        log.msg("tâche %d (%s) : execution de '%s' sur les serveurs %s" % (self.id_task, self.name, self.cmd, ','.join([str(serv) for serv in serveurs])))
        # lancement de la commande
        # mise à jour de la date de prochaine exécution en base
        if not self.single_use:
            self.update_date()

class WeeklyTask (Task):

    single_use = False

    def __repr__(self):
        return """WeeklyTask  : %s (%s - %d:%d)""" % (self.cmd, calendar.day_name[self.week_day], self.hour, self.min)

    def check_time(self):
        now = datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if self.exec_date is None:
            if now.isoweekday == self.week_day:
                # date de la première exécution
                self.exec_date = datetime.datetime(now.year, now.month, now.day, self.hour, self.min)
            else:
                return False
        if now >= self.exec_date:
            # date de la prochaine exécution (+1 semaine)
            delta = datetime.timedelta(days=7)
            self.exec_date = self.exec_date + delta
            # on a atteint (ou dépassé) la date d'exécution
            return True
        return False

class MonthlyTask (Task):

    single_use = False

    def __repr__(self):
        return """MonthlyTask : %s (%s - %d:%d)""" % (self.cmd, self.day, self.hour, self.min)

    def check_time(self):
        now = datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if self.exec_date is None:
            if now.day == self.day:
                # date de la première exécution
                self.exec_date = datetime.datetime(now.year, now.month, self.day, self.hour, self.min)
            else:
                return False
        if now >= self.exec_date:
            # date de la prochaine exécution (+1 mois)
            month_days = calendar.month_range(now.year,now.month)[1]
            delta = datetime.timedelta(days=month_days)
            self.exec_date = self.exec_date + delta
            # on a atteint (ou dépassé) la date d'exécution
            return True
        return False

class DailyTask (Task):

    single_use = False

    def __repr__(self):
        return """DailyTask   : %s (%d:%d)""" % (self.cmd, self.hour, self.min)

    def check_time(self):
        now = datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if self.exec_date is None:
            # date de la première exécution
            self.exec_date = datetime.datetime(now.year, now.month, now.day, self.hour, self.min)
        if now >= self.exec_date:
            # date de la prochaine exécution (+1 jour)
            delta = datetime.timedelta(days=1)
            self.exec_date = self.exec_date + delta
            # on a atteint (ou dépassé) la date d'exécution
            return True
        return False

class LoopingTask (Task):

    single_use = False

    def __repr__(self):
        return """LoopingTask : %s (%d min)""" % (self.cmd, self.day*24*60 + self.hour*60 + self.min)

    def check_time(self):
        now = datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if self.exec_date is None:
            # date de la première exécution
            self.exec_date = now
        if now >= self.exec_date:
            # date de la prochaine exécution (+1 jour)
            delta = datetime.timedelta(days=self.day or 0, hours=self.hour or 0, minutes=self.min or 0)
            self.exec_date = now + delta
            # on a atteint (ou dépassé) la date d'exécution
            return True
        return False

class SingleTask (Task):

    single_use = True

    def __repr__(self):
        return """SingleTask  : %s (%d %s - %d:%d)""" % (self.cmd, self.day, calendar.month_name[self.month], self.hour, self.min)

    def __init__(self, id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date=None):
        """Pour ce type de tâche, la date d'exécution est
        calculée une seule fois à l'initialisation
        (évite les problèmes en cas de changement d'année)
        """
        self.id_task = id_task
        self.name = name
        self.cmd = cmd
        self.month = month
        self.day = day
        self.hour = hour
        self.min = min
        self.week_day = week_day
        self.periodicity = periodicity
        now =  datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if exec_date is None:
            exec_date = datetime.datetime(now.year, month, day, hour, min)
            if exec_date < now:
                exec_date = datetime.datetime(now.year + 1, month, day, hour, min)
        self.exec_date = exec_date
        self.update_date()

    def check_time(self):
        now = datetime.datetime(*datetime.datetime.now().timetuple()[:5])
        if now >= self.exec_date:
            # on a atteint (ou dépassé) la date d'exécution
            return True
        return False

class TaskScheduler:
    """Classe utilitaire pour permettre une gestion des taches uucp à la mode 'crontab'
    """

    def __init__(self, s_pool):
        self.s_pool = s_pool
        self.loop = task.LoopingCall(self.check_tasks)
        self.tasks = {}
        self.targets = {}
        self.running=False
        self.period_to_task = {0:SingleTask,
                               1:LoopingTask,
                               2:DailyTask,
                               3:WeeklyTask,
                               4:MonthlyTask}

    def start(self):
        """démarre la boucle du scheduler
        """
        self.load_tasks()
        log.msg("Scheduler mainloop starting (delay : %d)" % config.SCHEDULER_DELAY)
        self.loop.start(config.SCHEDULER_DELAY,now=False)

    def get_tasks(self, id_res=None, type_res='serveur', all=False):
        """retourne la liste des tâches programmées pour un ou plusieurs serveur(s)
        all : dans le cas d'un serveur, retourne aussi les taĉhes associées à ses groupes
        """
        cu = cx_pool.create()
        # recherche des tâches associées si ressource spécifiée
        tasks = []
        query = """select id_res, type_res, id_task from task_targets"""
        cu.execute(query)
        targets = cu.fetchall()
        cx_pool.close(cu)
        if id_res is not None:
            # récupération des associations tâche/ressource
            groupes = []
            id_res = int(id_res)
            # tâches spécifiques à la ressource
            for res, type, id_task in targets:
                if type == type_res and res == id_res and id_task not in tasks:
                    tasks.append(id_task)
            if type_res == 'serveur' and all:
                # serveur : on recherche les groupes possibles
                for gr_id, groupe in self.s_pool._groupes_serveurs.items():
                    if id_res in groupe[1]:
                        groupes.append(gr_id)
                for res, type, id_task in targets:
                    if type == 'groupe' and res in groupes and id_task not in tasks:
                        tasks.append(id_task)
        else:
            tasks = self.tasks.keys()
        # envoi de la liste des taches
        res = []
        for id_task in tasks:
            res.append(self.tasks[id_task])
        # tri du résultat par periodicité et id de tâche
        res.sort()
        # return [(task.id_task, task.name, task.cmd, task.month, task.day, task.hour, task.min, task.week_day, task.exec_date) for task in res]
        return res

    def load_tasks(self):
        """Charge les tâches définies dans la base de données"""
        self.tasks = {}
        self.targets = {}
        query = """select id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date from tasks"""
        cu = cx_pool.create()
        cu.execute(query)
        # récupération de l'id de la tâche ajoutée
        tasks = cu.fetchall()
        # instanciation des tâches
        for task in tasks:
            id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date = task
            self.init_task(id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date)
        # chargement des associations serveur(groupe) / tâche
        query = """select id_res, type_res, id_task from task_targets"""
        cu.execute(query)
        targets = cu.fetchall()
        cx_pool.close(cu)
        for target in targets:
            id_res, type_res, id_task = target
            if not id_task in self.targets:
                self.targets[id_task] = {}
            liste_res = self.targets[id_task].get(type_res,[])
            liste_res.append(id_res)
            self.targets[id_task][type_res] = liste_res

    def init_task(self, id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date=None):
        """initialise un objet tâche dans le gestionnaire
        """
        # par défaut ou si periodicité = 0 : tâche unique
        task_class = self.period_to_task.get(periodicity, SingleTask)
        new_t = task_class(id_task, name, cmd, month, day, hour, min, week_day, periodicity, exec_date)
        self.tasks[id_task] = new_t

    def add_task(self, name, cmd, month, day, hour, min, week_day, periodicity):
        """programme une tâche sur un/plusieur(s) serveurs
        periodicity : 0 = tâche unique
                      1 = tâche répétitive (délai : day, hour, min) -> lancée au début de la boucle
                      2 = tâche journalière
                      3 = tâche hebdomadaire
                      4 = tâche mensuelle
        """
        # ajout de la tâche en base
        cu = cx_pool.create()
        try:
            query = """insert into tasks (name, cmd, month, day, hour, min, week_day, periodicity) values (%s, %s, %s, %s, %s, %s, %s, %s)"""
            params = (name, cmd, int(month), int(day), int(hour), int(min), int(week_day), int(periodicity))
            cu.execute(query, params)
            # récupération de l'id de la tâche ajoutée
            query = """select id_task from tasks where name=%s and cmd=%s and """
            params = [name, cmd]
            for arg in [('month',month), ('day',day), ('hour',hour), ('min',min), ('week_day',week_day)]:
                if arg[1] in [None, 'null']:
                    query += "%s is null and " % arg[0]
                else:
                    query += arg[0] + "=%s and "
                    params.append(int(arg[1]))
            query += """periodicity=%s order by id_task desc"""
            params.append(int(periodicity))
            cu.execute(query, params)
            id_task = int(cu.fetchone()[0])
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
        self.init_task(id_task, name, cmd, month, day, hour, min, week_day, periodicity)
        return id_task

    def assign_task(self, cred_user, id_task, id_res, type_res):
        """attribue une tâche à un(des) serveur(s)
        @cred_user : utilisateur ayant demandé l'association: permet de vérifier les droits d'accès aux ressources
        @id_task : numéro de tâche à associer à la ressource
        @id_res : identifiant de la ressource à associer (serveur ou groupe)
        @type_res : type de la ressource (serveur par défaut)
        """
        try:
            id_task = int(id_task)
            id_res = int(id_res)
        except:
            return 0, """Identifiant de ressource de type incorrect"""
        # association de la tâche aux serveurs
        if id_task not in self.tasks:
            return 0, """identifiant de tâche inconnu"""
        try:
            if type_res == 'groupe':
                gr = self.s_pool.get_groupes(cred_user, id_res)
            elif type_res == 'serveur':
                serv = self.s_pool.get(cred_user, id_res)
            else:
                return 0, """type de ressource non reconnu (utiliser 'groupe' ou 'serveur')"""
        except (KeyError, ResourceAuthError):
            return 0, """Permissions insuffisantes ou %s inexistant""" % type_res
        if id_task not in self.targets:
            self.targets[id_task] = {}
        task_res = self.targets[id_task].get(type_res,[])
        if id_res in task_res:
            return 0, """Tâche déjà assignée à cette ressource"""
        # insertion dans la base
        cu = cx_pool.create()
        query = """insert into task_targets (id_res, type_res, id_task) values (%s, %s, %s)"""
        params = (int(id_res), type_res, int(id_task))
        try:
            cu.execute(query, params)
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return 0, """Erreur de mise à jour de la base de données"""
        # mise à jour en mémoire
        task_res.append(id_res)
        self.targets[id_task][type_res] = task_res
        return 1, "OK"

    def unassign_task(self, cred_user, id_task, id_res, type_res):
        """Supprime une association tâche/serveur(ou groupe)
        """
        try:
            id_task = int(id_task)
            id_res = int(id_res)
        except:
            return 0, """Identifiant de ressource de type incorrect"""
        # vérification des droits d'accès au serveur ou groupe
        if id_task not in self.tasks:
            return 0, """identifiant de tâche inconnu"""
        try:
            if type_res == 'groupe':
                gr = self.s_pool.get_groupes(cred_user, id_res)
            elif type_res == 'serveur':
                serv = self.s_pool.get(cred_user, id_res)
            else:
                return 0, """type de ressource non reconnu (utiliser 'groupe' ou 'serveur')"""
        except (KeyError, ResourceAuthError):
            return 0, """Permissions insuffisantes ou %s inexistant""" % type_res
        assoc_ok = False
        if id_task in self.targets:
            task_res = self.targets[id_task].get(type_res,[])
            if id_res in task_res:
                assoc_ok = True
        if not assoc_ok:
            return 0, """Tâche non assignée à cette ressource"""
        # suppression dans la base
        cu = cx_pool.create()
        query = """delete from task_targets where id_res=%s and type_res=%s and id_task=%s"""
        params = (int(id_res), type_res, int(id_task))
        try:
            cu.execute(query, params)
            cx_pool.commit(cu)
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return 0, """Erreur de mise à jour de la base de données"""
        # mise à jour en mémoire
        self.targets[id_task][type_res].remove(id_res)
        return 1, "OK"

    def del_task(self, id_task):
        """supprime une tâche programmée.
        si serveurs est spécifié, la tâche sera supprimée seulement sur ce(s) serveur(s)
        """
        cu = cx_pool.create()
        try:
            query = """delete from tasks where id_task=%d"""
            del(self.tasks[int(id_task)])
            cu.execute(query, (int(id_task),))
            cx_pool.commit(cu)
            return True
        except:
            traceback.print_exc()
            cx_pool.rollback(cu)
            return False

    def check_tasks(self, serveur=None):
        """met en place les tâches a exécuter pour l'itération en cours
        """
        for id_task, task in self.tasks.items():
            # on ne vérifie que les tâches associées à des serveurs / groupes
            if id_task in self.targets:
                if task.check_time():
                    serveurs = []
                    # recherche des serveurs affectés
                    for type_res in self.targets.get(id_task,[]):
                        for res in self.targets[id_task][type_res]:
                            # récupération de l'identifiant des serveurs suivant le type de ressource
                            if type_res == 'groupe':
                                gr_serv = self.s_pool._groupes_serveurs[res][1]
                                for serv in gr_serv:
                                    if serv not in serveurs: serveurs.append(serv)
                            else:
                                serveurs.append(res)
                    # exécution de la tâche
                    task.run(serveurs)
                    # la tâche a été mise en place
                    if task.single_use == True:
                        # tâche unique : on la supprime après exécution
                        self.del_task(id_task)
        # après un premier passage de la boucle, self.running passe à True
        # (permet de démarrer les tâches basées sur un délai)
        if not self.running:
            self.running = True
