# -*- coding: UTF-8 -*-
###########################################################################
# Eole NG - 2012
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# dictpool.py
#
# classes de gestion d'un pool de dictionnaires Creole
# avec liaison au niveau module/variante/serveur
#
###########################################################################
"""classes de gestion d'un pool de dictionnaires Creole
"""
from zephir.backend import config
from zephir.backend.config import log
from zephir.backend.lib_backend import cx_pool, PgSQL
from zephir.utils.convert import to_bytes
import os, sys, time, traceback, shutil
import base64
from glob import glob
from hashlib import md5

BLANK_MD5 = md5(b'').hexdigest()

RESOURCE_TYPES = ('module', 'variante', 'serveur')

class DictPool:
    """
    Classe de gestion des dictionnaires par module/variante/serveur (eole 2.3 et plus)
    """

    def __init__(self, eole_versions, serveur_pool):
        """
        précharge une liste de dictionnaires disponibles pour
        une version donnée de la distribution Eole
        """
        self.eole_versions = eole_versions
        self.dict_dir = {}
        for eole_version in self.eole_versions:
            self.dict_dir[eole_version] = os.path.join(config.ROOT_DIR, 'dictionnaires', config.DISTRIBS[int(eole_version)][1])
        self.serveur_pool = serveur_pool
        self.update_data()
        self.reset_modules()

    def update_data(self):
        """
        met à jour la structure interne depuis l'aborescence
        des dictionnaires eole/locaux
        """
        known_mods = []
        if hasattr(self, 'dicos'):
            # liste des modules déjà pris en compte en cas de relance de la fonction
            known_mods.extend(list(self.modules.keys()))
        self.paqs = {}
        self.dicos = {}
        self.variantes = {}
        self.modules = {}
        for eole_version in self.eole_versions:
            self.paqs[eole_version] = {}
            self.dicos[eole_version] = {}
            # chargement des différents paquets et dictionnaires existants
            for dict_type in ('eole', 'local'):
                self.update_dicts(eole_version, dict_type)
            # chargement de la liste des modules/variantes compatibles
            req = """select modules.id, modules.libelle, variantes.id from variantes, modules where modules.id=variantes.module and modules.version=%s"""
            cu = cx_pool.create()
            try:
                cu.execute(req, (eole_version,))
                data = cu.fetchall()
                cx_pool.close(cu)
            except:
                cx_pool.close(cu)
                raise ValueError('Erreur lors de la recherche des modules %s' % str(config.DISTRIBS[int(eole_version)][1]))
            # on ne prend en compte que les modules pour lesquels au moins
            # un paquet est défini dans le fichier de description du module
            mod_defaults = {}
            for mod, label, var in data:
                if mod not in self.modules:
                    # il faut remplir self.modules avant d'utiliser get_module_defaults
                    # l'entrée est supprimée ensuite si aucun paquet n'est défini
                    self.modules[mod] = (label, eole_version)
                    if len(self.get_module_defaults(mod)) == 0:
                        # pas de paquets déclarés, on ne gère pas ce module avec le pool
                        del(self.modules[mod])
                    elif mod not in known_mods:
                        print("Utilisation du pool de dictionnaires pour le module: %s" % label)
                vars = self.variantes.get(mod, [])
                vars.append(var)
                self.variantes[mod] = vars

    def update_dicts(self, eole_version, dict_type):
        """rechargement de la représentation de l'arborescence des dictionnaires
        dict_type : dictionnaires de type 'local' ou 'eole'
        """
        self.paqs[eole_version][dict_type] = {}
        self.dicos[eole_version][dict_type] = {}
        dict_dir = os.path.join(self.dict_dir[eole_version], dict_type)
        if not os.path.isdir(dict_dir):
            os.makedirs(dict_dir)
        paq_eole = os.listdir(dict_dir)
        # dictionnaires gérés par des paquets
        for paq_dir in paq_eole:
            if os.path.isdir(os.path.join(self.dict_dir[eole_version], dict_type, paq_dir)):
                self.paqs[eole_version][dict_type][paq_dir] = glob(os.path.join(self.dict_dir[eole_version], dict_type, paq_dir, '*.xml'))
        # dictionnaires isolés
        dicos = glob(os.path.join(self.dict_dir[eole_version], dict_type,'*.xml'))
        for dict_path in dicos:
            self.dicos[eole_version][dict_type][os.path.basename(dict_path)] = dict_path

    def get_paq_dict(self, eole_version, liste_paqs, full_path=True):
        """renvoie une liste de dictionnaires
        à partir d'une liste de paquets
        """
        dicos_mod = []
        for paq in liste_paqs:
            # on vérifie qu'on n'a pas une chaine vide (fichier sans paquet) ou une ligne commentée
            if paq and not paq.startswith('#'):
                type_paq, paq_name = os.path.split(paq)
                if paq_name in self.paqs[eole_version].get(type_paq, {}):
                    if full_path:
                        dicos_mod.extend(self.paqs[eole_version][type_paq][paq_name])
                    else:
                        dicos_mod.append(paq_name)
        return dicos_mod

    def check_dict(self, eole_version, dict_type, dict_name):
        """
        vérifie si un dictionnaire donné est bien référencé
        """
        if dict_name.endswith('.xml'):
            if dict_name not in self.dicos[eole_version][dict_type]:
                raise ValueError("dictionnaire non référencé : %s" % dict_name)
        elif dict_name not in self.paqs[eole_version][dict_type]:
            raise ValueError("paquet inconnu : %s" % dict_name)

    def check_type_res(self, res_type):
        """
        vérifie que le type de ressource demandée est reconnu
        """
        if res_type not in RESOURCE_TYPES:
            raise ValueError("Type de ressource inconnu : %s" % res_type)

    def check_module(self, id_module):
        """vérifie si un numéro de module est géré par le pool
        """
        return int(id_module) in self.modules

    def get_database_dict(self, eole_version, type_res, id_res, full_path=True):
        """ récupère les dictionnaires/paquets additionnels
        stockés en base de données
        type_res : type de ressource (serveur, variante, module)
        id_res : identifiant de la ressource
        """
        self.check_type_res(type_res)
        tablename = "dict_%s" % type_res
        req = "select dict_type, dict_name from " + tablename + " where id_resource=%s"
        cursor = cx_pool.create()
        try:
            cursor.execute(req, (int(id_res),))
            data = cursor.fetchall()
            cx_pool.close(cursor)
        except:
            cx_pool.close(cursor)
            raise ValueError('%s introuvable : %s' % (type_res, id_res))
        dicos_mod = []
        for dict_type, dict_name in data:
            if full_path:
                # renvoie le chemin complet des dictionnaires associés (+ dictionnaires des paquets)
                if dict_name in self.paqs[eole_version][dict_type]:
                    dicos_mod.extend(self.paqs[eole_version][dict_type][dict_name])
                elif dict_name.endswith('.xml'):
                    dicos_mod.append(os.path.join(self.dict_dir[eole_version], dict_type, dict_name))
            else:
                # on renvoie juste le nom de paquet/dictionnaire
                dicos_mod.append(dict_name)
        return dicos_mod

    def set_database_dict(self, eole_version, type_res, id_res, dict_type, dict_path):
        """
        associe un(des) dictionnaire(s) à une ressource en base de données
        type_res : 'module', 'variante' ou 'serveur'
        dict_type : type de dictionnaire (eole ou local)
        dict_path : nom du dictionnaire ou du paquet
        """
        self.check_dict(eole_version, dict_type, dict_path)
        # vérification de la présence du dictionnaire dans le pool interne
        assert dict_path in self.dicos[eole_version][dict_type] or dict_path in self.paqs[eole_version][dict_type]
        self.check_type_res(type_res)
        # on détermine le type de dictionnaire en fonction du chemin de la ressource
        tablename = "dict_%s" % type_res
        req = "insert into " + tablename + " values (%s, %s, %s)"
        cursor = cx_pool.create()
        try:
            cursor.execute(req, (int(id_res), dict_type, dict_path))
            cx_pool.commit(cursor)
        except PgSQL.IntegrityError:
            cx_pool.rollback(cursor)
            raise ValueError('%s %s : utilise déjà %s' % (type_res, str(id_res), dict_path))
        except:
            traceback.print_exc()
            cx_pool.rollback(cursor)
            raise ValueError('%s introuvable : %s' % (type_res, id_res))

    def del_database_dict(self, eole_version, type_res, id_res, dict_type, dict_path):
        """
        supprime l'association entre un dictionnaire et une ressource de la base
        """
        self.check_dict(eole_version, dict_type, dict_path)
        self.check_type_res(type_res)
        tablename = "dict_%s" % type_res
        req = "delete from " + tablename + " where id_resource=%s and dict_type=%s and dict_name=%s"
        cursor = cx_pool.create()
        try:
            cursor.execute(req, (int(id_res), dict_type, dict_path))
            cx_pool.commit(cursor)
        except:
            traceback.print_exc()
            cx_pool.rollback(cursor)
            raise ValueError('Entrée introuvable dans la table %s' % tablename)

    ###################################
    # Gestion du pool de dictionnaires
    def get_dict_links(self, eole_version, dict_type, dict_name):
        """
        renvoie la liste des liens établis pour un dictionnaire/paquet donné
        """
        return self.get_dict_resources(eole_version, dict_type, dict_name, True)

    def get_dict_resources(self, eole_version, dict_type, dict_name, get_links=False):
        """
        liste toutes les associations liées à un dictionnaire particulier
        retourne la liste des liens par type de ressource
        get_paths : si False, renvoie la liste des ressources associées
                    si True, renvoie la liste des liens symboliques existants
        """
        dict_resources =  {}
        for type_res in RESOURCE_TYPES:
            dict_resources[type_res] = []
            tablename = 'dict_%s' % type_res
            req = "select id_resource from " + tablename + " where dict_type=%s and dict_name=%s"
            cursor = cx_pool.create()
            try:
                cursor.execute(req, (dict_type, dict_name))
                data = cursor.fetchall()
                cx_pool.close(cursor)
            except:
                cx_pool.close(cursor)
                return dict_resources
            for id_entry in data:
                # calcul du chemin du lien symbolique créé pour la ressource
                id_res = id_entry[0]
                # on limite aux ressources correspondant à cette version de distribution
                if (type_res == 'variante' and self.get_var_mod(id_res)[1] == eole_version) or \
                   (type_res == 'module' and self.modules[id_res][1] == eole_version) or \
                   (type_res == 'serveur' and self.serveur_pool[id_res].module_version == eole_version):
                    if get_links:
                        res_links = self.get_link_path(int(eole_version), id_res, type_res, dict_type, dict_name)
                        if res_links:
                            for res_link in res_links:
                                if os.path.islink(res_link):
                                    dict_resources[type_res].append(res_link)
                    else:
                        dict_resources[type_res].append(id_res)
        return dict_resources

    def update_paq_resources(self, eole_version, paq_name):
        """
        Met à jour les liens des ressources liées à un paquet local
        """
        link_res = self.get_dict_resources(eole_version, 'local', paq_name)
        for type_res, resources in list(link_res.items()):
            for id_res in resources:
                # appel de la fonction de mise à jour des liens
                # suivant le type de ressources
                getattr(self, 'update_%s' % type_res)(id_res)

    def get_link_path(self, eole_version, id_res, type_res, dict_type, dict_name):
        """
        calcule le chemin d'un lien vers un dictionnaire pour une ressource particulière
        """
        if dict_name in self.paqs[eole_version][dict_type]:
            # paquet : on récupère la liste des dictionnaires
            dicts = [os.path.basename(paq) for paq in self.paqs[eole_version][dict_type][dict_name]]
            serveur_dir = var_dir = 'package'
        else:
            dicts = [dict_name]
            serveur_dir = 'local'
            var_dir = 'dicos'
        path_link = None
        if type_res == 'module':
            if int(id_res) in self.modules:
                path_link = os.path.join(config.PATH_MODULES, str(id_res), 'dicos')
        elif type_res == 'variante':
            id_mod = None
            for mod, vars in list(self.variantes.items()):
                if int(id_res) in vars:
                    id_mod = mod
            # on ne prend que les variantes correspondant aux modules gérés
            if id_mod:
                path_link = os.path.join(config.PATH_MODULES, str(id_mod), 'variantes', str(id_res), var_dir)
        elif type_res == 'serveur':
            try:
                serveur = self.serveur_pool[int(id_res)]
                assert serveur.id_mod in self.modules
            except:
                # serveur non trouvé ou mauvaise version de module
                return None
            path_link = os.path.join(serveur.get_confdir(), 'dicos', serveur_dir)
        if path_link is not None:
            return [os.path.join(path_link, dict_file) for dict_file in dicts]
        return None

    def add_dict(self, eole_version, dict_name, content, paq_name=""):
        """
        ajoute/met à jour un dictionnaire dans le pool local
        paq_name : nom du paquet intégrant le dictionnaire ou rien si
                   le dictionnaire est isolé
        """
        # création du répertoire du paquet si besoin
        try:
            if paq_name:
                dict_dir = os.path.join(self.dict_dir[eole_version], 'local', paq_name)
            else:
                dict_dir = os.path.join(self.dict_dir[eole_version], 'local')
            if not os.path.isdir(dict_dir):
                os.makedirs(dict_dir)
            # sauvegarde du contenu du dictionnaire. On l'écrase si il existe déjà
            dict_file = open(os.path.join(dict_dir, dict_name), 'wb')
            dict_file.write(base64.decodebytes(to_bytes(content)))
            dict_file.close()
        except:
            # erreur d'écriture ?
            traceback.print_exc()
            return False
        # ajout du dictionnaire/paquet dans la liste interne si tout s'est bien passé
        self.update_dicts(eole_version, 'local')
        # si ajout dans un paquet, on met à jour les ressources liées (serveurs, variantes)
        if paq_name:
            self.update_paq_resources(eole_version, paq_name)
        return True

    def get_dict(self, eole_version, type_dict, dict_name, paq_name=""):
        """
        renvoie le contenu d'un dictionnaire
        dict_name : nom du dictionnaire à récupérer
        paq_name : nom du paquet contenant le dictionnaire ou rien
        """
        content = ""
        if paq_name:
            dict_path = os.path.join(self.dict_dir[eole_version], type_dict, paq_name, dict_name)
        else:
            dict_path = os.path.join(self.dict_dir[eole_version], type_dict, dict_name)
        assert os.path.exists(dict_path), "Fichier non retrouvé"
        return base64.encodebytes(open(dict_path, 'rb').read()).decode()

    def remove_dict(self, eole_version, dict_name, paq_name=""):
        """
        supprimer un dictionnaire/paquet du pool local
        dict_name : nom du dictionnaire / paquet à supprimer
        paq_name : nom du paquet contenant le dictionnaire ou rien
        """
        # cas possibles :
        #   1) dictionnaire isolé / paquet : rechercher tous les liens et les supprimer
        #   2) dictionnaire dans un paquet : supprimer seulement le dictionnaire
        if not paq_name:
            # récupération de tous les liens sur ce dictionnaire / paquet
            dict_links = self.get_dict_links(eole_version, 'local', dict_name)
            for links in list(dict_links.values()):
                for dict_link in links:
                    if os.path.islink(dict_link):
                        os.unlink(dict_link)
        else:
            # cas 2 : obligatoirement fichier .xml dans un paquet
            assert dict_name.endswith('.xml'), "fichier invalide : dictionnaire xml requis"

        # dans tous les cas : suppression du dictionnaire
        try:
            dict_path = os.path.join(self.dict_dir[eole_version], 'local', paq_name, dict_name)
            assert os.path.exists(dict_path)
            if os.path.isdir(dict_path):
                shutil.rmtree(dict_path)
            else:
                os.unlink(dict_path)
                # cas particulier: dernier dictionnaire d'un paquet
                paq_path = os.path.join(self.dict_dir[eole_version], 'local', paq_name)
                if paq_name and glob(os.path.join(paq_path, "*.xml")) == []:
                    shutil.rmtree(paq_path)
        except:
            return False
        # suppression globale dans la base (cas 1)
        if not paq_name:
            for type_res in RESOURCE_TYPES:
                tablename = 'dict_%s' % type_res
                req = "delete from " + tablename + " where dict_type='local' and dict_name=%s"
                cursor = cx_pool.create()
                try:
                    cursor.execute(req, (dict_name,))
                    cx_pool.commit(cursor)
                except:
                    traceback.print_exc()
                    cx_pool.rollback(cursor)
                    return False
        self.update_dicts(eole_version, 'local')
        if paq_name:
            # après suppression dans un paquet, on met à jour les ressources liées (variantes/serveurs)
            self.update_paq_resources(eole_version, paq_name)
        return True

    #############################################
    # gestion des dictionnaires au niveau module

    def list_module(self, id_module, full_path=True):
        """
        liste les dictionnaires/paquets associés
        à un module dans la base de données
        """
        # récupération du libellé du module
        try:
            libelle, module_version = self.modules[int(id_module)]
        except:
            raise KeyError('numéro de module inconnu ou non géré')
        dicos_mod = self.get_module_defaults(id_module, full_path)
        # récupération des données dans la base
        dicos_mod.extend(self.get_database_dict(module_version, 'module', id_module, full_path))
        return dicos_mod

    def get_module_defaults(self, id_module, full_path=True):
        """
        retourne la liste des paquets définis par défaut pour un module particulier
        """
        try:
            label, module_version = self.modules[int(id_module)]
        except:
            raise KeyError('numéro de module inconnu ou non géré')
        # récupération des paquets par défaut d'un module
        fic_module = os.path.join(config.ROOT_DIR, 'default_modules', str(module_version), label)
        # si le module vient d'être créé, on ajoute un fichier minimum
        if not os.path.isfile(fic_module):
            f_defaults = open(fic_module, 'w')
            f_defaults.write(config.minimum_paqs)
            f_defaults.close()
        liste_paqs = open(fic_module).read()
        liste_paqs = liste_paqs.strip().split('\n')
        return self.get_paq_dict(module_version, liste_paqs, full_path)

    def del_module_defaults(self, id_module):
        """
        supprime le fichier de description du module dans default_modules
        """
        try:
            label, module_version = self.modules[int(id_module)]
        except:
            raise KeyError('numéro de module inconnu ou non géré')
        # récupération des paquets par défaut d'un module
        fic_module = os.path.join(config.ROOT_DIR, 'default_modules', str(module_version), label)
        if os.path.exists(fic_module):
            os.unlink(fic_module)

    def update_module(self, id_module):
        """
        met à jour la liste les liens vers les dictionnaires
        associés à un module
        """
        mod_dir = os.path.join(config.PATH_MODULES, str(id_module), 'dicos')
        # bascule depuis l'ancien mode de gestion si le répertoire est un lien
        if os.path.islink(mod_dir):
            os.unlink(mod_dir)
        if not os.path.isdir(mod_dir):
            os.makedirs(mod_dir)
        # lecture de la liste des dictionnaires du module
        dicts = self.list_module(id_module)
        # suppression et regénération de tous les liens symboliques
        old_dicts = glob(os.path.join(mod_dir, '*.xml'))
        for old_dict in old_dicts:
            os.unlink(old_dict)
        for new_dict in dicts:
            if os.path.exists(new_dict):
                os.symlink(new_dict, os.path.join(mod_dir, os.path.basename(new_dict)))

    def add_module_dict(self, id_module, dict_type, dict_path):
        """
        ajoute un dictionnaire à un module
        """
        # vérification du n° de module
        if not int(id_module) in self.modules:
            raise ValueError('module incompatible (distribution non gérée)')
        module_version = self.modules[int(id_module)][1]
        self.set_database_dict(module_version, 'module', id_module, dict_type, dict_path)
        # mise à jour des liens
        self.update_module(id_module)

    def del_module_dict(self, id_module, dict_type, dict_path):
        """
        supprime un dictionnaire d'un module
        """
        # vérification du n° de module
        try:
            libelle, module_version = self.modules[int(id_module)]
        except KeyError:
            raise ValueError('module incompatible (version non gérée : %s)' % config.DISTRIBS[module_version][1])
        # on supprime toutes les entrées dans la base
        self.del_database_dict(module_version, 'module', id_module, dict_type, dict_path)

        # XXX FIXME : interdire la supression dans les fichiers ? (au moins pour les modules Eole)

        # module : on regarde si le dictionnaire n'est pas dans le fichier de description du module ??
        # récupération des paquets par défaut du module
        fic_module = os.path.join(config.ROOT_DIR, 'default_modules', str(module_version), libelle)
        liste_paqs = open(fic_module).read()
        liste_paqs = liste_paqs.strip().split('\n')
        if dict_path in liste_paqs:
            # suppression de l'entrée dans le fichier
            liste_paqs.remove(dict_path)
            f_mod = open(fic_module, 'w')
            f_mod.write('\n'.join(liste_paqs))
            f_mod.close()
        self.update_module(id_module)

    ###############################################
    # gestion des dictionnaires au niveau variante

    def get_var_mod(self, id_variante):
        """
        vérifie que la distribution de la variante est gérée et renvoie son n° de module
        """
        id_mod = None
        for mod, vars in list(self.variantes.items()):
            if int(id_variante) in vars:
                id_mod = mod
        if id_mod is None:
            raise ValueError("Variante inconnue ou incompatible (distribution non gérée)")
        module_version = self.modules[id_mod][1]
        return id_mod, module_version

    def check_passvar(self, id_variante, cred_user, pass_var):
        """Vérification du mot de passe/propriétaire de la variante
        A appeler avant add(del)_variante_dict
        """
        query = "select owner, passmd5 from variantes where id=%s"
        cursor = cx_pool.create()
        try:
            cursor.execute(query, (int(id_variante),))
            owner, passmd5 = cursor.fetchone()
            cx_pool.close(cursor)
        except:
            cx_pool.close(cursor)
            raise ValueError('variante introuvable : %s' % str(id_variante))
        if owner != cred_user:
            # vérification du mot de passe
            if passmd5 != pass_var and passmd5 not in [None,'', BLANK_MD5]:
                # mauvais mot de passe
                return False
        return True

    def list_variante(self, id_variante, full_path=True):
        """
        liste les dictionnaires/paquets associés
        à une variante dans la base de données
        """
        # vérification de la version de distribution
        id_mod, module_version = self.get_var_mod(id_variante)
        return self.get_database_dict(module_version, 'variante', id_variante, full_path)

    def update_variante(self, id_variante, id_mod = None):
        """
        met à jour la liste les liens vers les dictionnaires
        associés à une variante
        """
        if id_mod is None:
            id_mod, module_version = self.get_var_mod(id_variante)
        else:
            module_version = self.modules[id_mod][1]
        dicts = self.get_database_dict(module_version, 'variante', int(id_variante))
        var_dir = os.path.join(config.PATH_MODULES, str(id_mod), 'variantes', str(id_variante), 'dicos')
        if not os.path.isdir(var_dir):
            os.makedirs(var_dir)
        var_paq_dir = os.path.join(config.PATH_MODULES, str(id_mod), 'variantes', str(id_variante), 'package')
        if not os.path.isdir(var_paq_dir):
            os.makedirs(var_paq_dir)
        # suppression et regénération de tous les liens symboliques
        old_dicts = glob(os.path.join(var_dir, '*.xml'))
        old_dicts.extend(glob(os.path.join(var_paq_dir, '*.xml')))
        # XXX FIXME gérer les erreurs
        for old_dict in old_dicts:
            os.unlink(old_dict)
        for new_dict in dicts:
            dirname = new_dict.split(os.sep)[-2]
            if dirname in ('eole', 'local'):
                # on a affaire à un dictionnaire isolé
                dest_dir = var_dir
            else:
                # dictionnaire de paquet, on ne l'ajoute pas local pour éviter
                # les doublons avec ceux installés par le paquet
                dest_dir = var_paq_dir
            if not os.path.isdir(dest_dir):
                os.makedirs(dest_dir)
            os.symlink(new_dict, os.path.join(dest_dir, os.path.basename(new_dict)))
        # self.sync_variante_paqs(id_variante)

    def sync_variante_paqs(self, id_variante):
        """synchronise les paquets additonnels et les paquets de dictonnaires d'une variante
        """
        module, module_version = self.get_var_mod(int(id_variante))
        f_perso = os.path.join(config.PATH_MODULES, str(module), 'variantes', str(id_variante), 'fichiers_zephir', 'fichiers_variante')
        variantes_files, variante_paqs = self.serveur_pool.get_fic_perso(f_perso)
        # paquets définis en base
        var_dicts = self.list_variante(id_variante, False)
        # on ne garde que les paquets (pas les dictionnaires isolés) ?
        paq_dicts = set([ paq for paq in var_dicts if not paq.endswith('.xml') ])
        # récupération de l'ensemble des paquets de dictionnaires connus pour
        # la distribution de ce serveur
        dict_paqs_eole = set(self.paqs[module_version]['eole'].keys())
        dict_paqs_local = set(self.paqs[module_version]['local'].keys())
        # recherche des paquets de dictionnaires non mis en base
        local_paqs = dict_paqs_local.intersection(variante_paqs)
        for paq in local_paqs:
            if paq not in paq_dicts:
                try:
                    self.add_variante_dict(id_variante, 'local', paq, False)
                except:
                    traceback.print_exc()
        eole_paqs = dict_paqs_eole.intersection(variante_paqs)
        for paq in eole_paqs:
            if paq not in paq_dicts:
                try:
                    self.add_variante_dict(id_variante, 'eole', paq, False)
                except:
                    traceback.print_exc()
        # update final des liens de la variante (désactivé dans les "add")
        self.update_variante(int(id_variante), module)
        # recherche des paquets en base non installés par la variante
        new_paqs = []
        for paq in paq_dicts:
            if paq not in variante_paqs:
                new_paqs.append(paq)
        if new_paqs:
            variante_paqs.extend(new_paqs)
            self.serveur_pool.save_fic_perso(variantes_files, variante_paqs, f_perso)

    def add_variante_dict(self, id_variante, dict_type, dict_path, update=True):
        """
        ajoute un dictionnaire à une variante
        """
        # récupération de l'identifiant du module
        id_mod, module_version = self.get_var_mod(id_variante)
        self.set_database_dict(module_version, 'variante', id_variante, dict_type, dict_path)
        # mise à jour des liens
        if update:
            self.update_variante(id_variante, id_mod)
        # si des serveurs de la variante ont ce dictionnaire, on supprime le doublon
        for serv in list(self.serveur_pool.values()):
            if serv.id_var == id_variante:
                serv_paths = self.get_link_path(serv.module_version, serv.id_s, 'serveur', dict_type, dict_path)
                if os.path.islink(serv_paths[0]):
                    # lien détecté sur le serveur, suppression
                    log.msg("Serveur %d : désactivation de %s (géré par la variante)" % (serv.id_s, dict_path))
                    self.del_serveur_dict(serv.id_s, dict_type, dict_path)
                else:
                    self.check_inactive_packages(serv)

    def del_variante_dict(self, id_variante, dict_type, dict_path):
        """
        supprime un dictionnaire d'une variante
        """
        # vérification du n° de module/variante
        id_mod, module_version = self.get_var_mod(id_variante)
        # on supprime toutes les entrées dans la base
        self.del_database_dict(module_version, 'variante', id_variante, dict_type, dict_path)
        # si paquet de dictionnaires supprimé, on le supprime aussi de fichiers_variante
        f_perso = os.path.join(config.PATH_MODULES, str(id_mod), 'variantes', str(id_variante), 'fichiers_zephir', 'fichiers_variante')
        fic_var, paq_var = self.serveur_pool.get_fic_perso(f_perso)
        if dict_path in paq_var:
            paq_var.remove(dict_path)
            self.serveur_pool.save_fic_perso(fic_var, paq_var, f_perso)
        # mise à jour des liens sur les dictionnaires du pool
        self.update_variante(id_variante, id_mod)
        # mise à jour des serveurs affectés
        for serv in list(self.serveur_pool.values()):
            if serv.id_var == id_variante:
                self.check_inactive_packages(serv)

    def copy_variante(self, var_src, var_dest):
        """copie des dictionnaires utilisés entre 2 variantes gérées
        """
        try:
            self.get_var_mod(var_src)
            mod_dest, version_dest = self.get_var_mod(var_dest)
        except:
            # une des 2 variantes n'est pas gérée
            return False
        # supression des données précédentes si besoin
        req = """delete from dict_variante where id_resource=%s"""
        cursor = cx_pool.create()
        try:
            cursor.execute(req, (int(var_dest),))
            cx_pool.commit(cursor)
        except:
            traceback.print_exc()
            cx_pool.rollback(cursor)
            return False
        # copie des entrées de la variante source
        req = """select dict_type, dict_name from dict_variante where id_resource=%s"""
        cursor = cx_pool.create()
        try:
            cursor.execute(req, (int(var_src),))
            data = cursor.fetchall()
        except:
            # pas de données pour cette variante
            data = []
        cx_pool.close(cursor)
        if data:
            cursor = cx_pool.create()
            try:
                req = """insert into dict_variante values (%s, %s, %s)"""
                for dict_type, dict_name in data:
                    cursor.execute(req, (int(var_dest), dict_type, dict_name))
                cx_pool.commit(cursor)
            except:
                traceback.print_exc()
                cx_pool.rollback(cursor)
                return False
        # recréation des liens vers les dictionnaires (nécessaire si copie de variante entre 2 releases)
        self.update_variante(var_dest, mod_dest)
        # si des serveurs de la variante ont ces dictionnaire, on supprime le doublon
        for serv in list(self.serveur_pool.values()):
            if serv.id_var == var_dest:
                for dict_type, dict_name in data:
                    serv_paths = self.get_link_path(serv.module_version, serv.id_s, 'serveur', dict_type, dict_name)
                    if os.path.islink(serv_paths[0]):
                        # lien détecté sur le serveur, suppression
                        log.msg("Serveur %d : désactivation de %s (géré par la variante)" % (serv.id_s, dict_name))
                        self.del_serveur_dict(serv.id_s, dict_type, dict_name)
                self.check_inactive_packages(serv)
        return True

    ##############################################
    # gestion des dictionnaires au niveau serveur

    def get_serveur(self, id_serveur):
        """
        vérifie que la distribution du serveur est gérée et renvoie l'objet Serveur associé
        """
        try:
            serveur = self.serveur_pool[int(id_serveur)]
            # on vérifie la version du serveur
            assert serveur.id_mod in self.modules
        except:
            raise KeyError('Serveur incompatible (dictionnaires de la distribution non gérés)')
        return serveur

    def list_serveur(self, id_serveur, full_path=True):
        """
        liste les dictionnaires/paquets associés
        à un serveur dans la base de données
        """
        serveur = self.get_serveur(id_serveur)
        return self.get_database_dict(self.modules[serveur.id_mod][1], 'serveur', serveur.id_s, full_path)

    def update_serveur(self, id_serveur):
        """
        met à jour la liste les liens vers les dictionnaires
        associés à un serveur
        """
        serveur = self.get_serveur(id_serveur)
        self.check_dirs(serveur)
        # ménage des dictionnaires 'locaux' indésirables
        serveur.backup_dicts()
        # lecture de la liste des dictionnaires du serveur
        dicts = self.get_database_dict(self.modules[serveur.id_mod][1], 'serveur', serveur.id_s)
        serveur_dir = os.path.join(serveur.get_confdir(), 'dicos', 'local')
        paq_dir = os.path.join(serveur.get_confdir(), 'dicos', 'package')
        # suppression et regénération de tous les liens symboliques
        old_dicts = glob(os.path.join(serveur_dir, '*.xml'))
        old_dicts.extend(glob(os.path.join(paq_dir, '*.xml')))
        for old_dict in old_dicts:
            # on ne touche pas aux dictionnaires stockés 'physiquement' dans 'dicos/local' du serveur
            if os.path.islink(old_dict):
                os.unlink(old_dict)
        for new_dict in dicts:
            dirname = new_dict.split(os.sep)[-2]
            if dirname in ('eole', 'local'):
                # on a affaire à un dictionnaire isolé
                dest_dir = serveur_dir
            else:
                # dictionnaire de paquet, on ne l'ajoute pas à local pour éviter
                # les doublons avec ceux installés par le paquet
                dest_dir = paq_dir
            if os.path.isfile(os.path.join(dest_dir, os.path.basename(new_dict))):
                log.msg('serveur %s : lien vers %s non créé, dictionnaire local existant' % (str(id_serveur), new_dict))
            elif not os.path.isdir(dest_dir):
                os.makedirs(dest_dir)
            os.symlink(new_dict, os.path.join(dest_dir, os.path.basename(new_dict)))

    def check_dirs(self, serveur):
        """vérifie qu'un serveur possède bien tous les liens nécessaires
        à la gestion des dictionnaires par dictpool

        serveur: objet serveur du pool de serveur Zéphir

        les répertoires de dictionnaires sont :
        local       : dictionnaires particuliers au serveur
        package     : dictionnaires livrés par des paquets particulier au serveur
        variante    : dictionnaires particuliers à la variante
        var_package : dictionnaires livrés par des paquets définis dans la variante
        module      : dictionnaires définis au niveau module (et paquets du module)
        (pour le module, on ne fait pas de distinction pour les dictionnaire livrés
        par des paquets, aucun de ces dictionnaires n'étant envoyés au serveur)
        """
        # vérification des liens sur le module et la variante
        mod_dir = os.path.join(config.PATH_MODULES, str(serveur.id_mod))
        var_dir = os.path.join(mod_dir, 'variantes', str(serveur.id_var))
        if not os.path.isdir(os.path.join(serveur.get_confdir(), 'dicos', 'package')):
            os.makedirs(os.path.join(serveur.get_confdir(), 'dicos', 'package'))
        if not os.path.islink(os.path.join(serveur.get_confdir(), 'dicos', 'module')):
            os.symlink(os.path.join(mod_dir, 'dicos'), os.path.join(serveur.get_confdir(), 'dicos', 'module'))
        if not os.path.islink(os.path.join(serveur.get_confdir(), 'dicos', 'variante')):
            os.symlink(os.path.join(var_dir, 'dicos'), os.path.join(serveur.get_confdir(), 'dicos', 'variante'))
        if not os.path.islink(os.path.join(serveur.get_confdir(), 'dicos', 'var_package')):
            os.symlink(os.path.join(var_dir, 'package'), os.path.join(serveur.get_confdir(), 'dicos', 'var_package'))

    def sync_serveur_packages(self, id_serveur):
        """synchronise l'état des paquets installés avec
        les dictionnaires activés
        """
        serveur = self.get_serveur(id_serveur)
        module_version = self.modules[serveur.id_mod][1]
        # liste des paquets de dictionnaires installés par défaut sur ce module
        module_paqs = set(self.get_module_defaults(serveur.id_mod, False))
        # récupération de l'ensemble des paquets de dictionnaires connus pour
        # la distribution de ce serveur
        dict_paqs_eole = set(self.paqs[module_version]['eole'].keys())
        dict_paqs_local = set(self.paqs[module_version]['local'].keys())
        # vérification des paquets installés remontés par le serveur
        pkg_file = os.path.join(os.path.abspath(config.PATH_ZEPHIR),'data','packages%s.list' % serveur.id_s)
        serveur_paqs = []
        serveur_dicts = []
        new_paqs = []
        new_dicts = []
        # if not os.path.isfile(pkg_file):
        # le serveur n'a jamais remonté sa liste de paquets
        # on considère qu'il possède les paquets de base du module et de la variante
        # les paquets additionnels au niveau module sont ajoutés aux paquets du serveur
        for dict_name in self.get_database_dict(module_version, 'variante', serveur.id_var, False):
            if not dict_name.endswith('.xml'):
                serveur_paqs.append(dict_name)
        serveur_paqs = set(serveur_paqs)
        # si des paquets sont en attente d'installation au niveau serveur, on vérifie qu'ils sont activés
        f_perso = os.path.join(serveur.get_confdir(), 'fichiers_zephir', 'fichiers_zephir')
        fic_serv, paq_serv = self.serveur_pool.get_fic_perso(f_perso)
        for dict_name in dict_paqs_eole.intersection(set(paq_serv)):
            if dict_name not in serveur_paqs:
                serveur_dicts.append(('eole', dict_name))
        for dict_name in dict_paqs_local.intersection(set(paq_serv)):
            if dict_name not in serveur_paqs:
                serveur_dicts.append(('local', dict_name))
        # liste des paquets de dictionnaires sélectionnés
        # et ajout de paquets à installer sur le serveur si besoin
        dict_paqs_serveur = self.get_database_dict(module_version, 'serveur', serveur.id_s, False)
        # pas de prise en compte des paquets ajoutés au niveau module : à calculer
        # dynamiquement au moment de l'envoi de conf au serveur
        # dict_paqs_serveur.extend(self.get_database_dict(module_version, 'module', serveur.id_mod,False))
        dict_paqs_serveur = set([paq for paq in dict_paqs_serveur if not paq.endswith('.xml')])
        # recherche des paquets référencés en base pour le serveur et non déclarés dans fichiers_zephir
        for eole_paq in dict_paqs_serveur.difference(serveur_paqs):
            new_paqs.append(eole_paq)
        # ajout des nouvelles entrées en base si besoin
        for dict_type, dict_path in serveur_dicts:
            # les paquets de base du module sont ignorés
            if dict_path not in dict_paqs_serveur and dict_path not in module_paqs:
                self.add_serveur_dict(serveur.id_s, dict_type, dict_path, False)
                new_dicts.append(dict_path)
        # mise à jour des liens vers les dictionnaires
        self.update_serveur(id_serveur)
        # ajout de paquets supplémentaires à installer sur le serveur
        added_paqs = serveur.add_packages(new_paqs)
        return added_paqs, new_dicts

    def update_serveur_packages(self, serveur, extra_paqs=None):
        """initialise la liste des paquets de dictionnaires installés sur le client
        calculé à la première vérification et mis à jour à la synchronisation du client.
        """
        packages = []
        if extra_paqs is None:
            extra_paqs = self.get_extra_packages(serveur)
        f_paqslist = os.path.join(config.PATH_ZEPHIR, 'data', 'packages%d.list' % serveur.id_s)
        if os.path.isfile(f_paqslist):
            known_dictionaries = set(self.paqs[serveur.module_version]['local'].keys()) | set(self.paqs[serveur.module_version]['eole'].keys()) | set(extra_paqs)
            for paq_line in open(f_paqslist):
                if paq_line:
                    # croisement avec les paquets de dictionnaires connus
                    paq_name = paq_line.split()[0]
                    if paq_name in known_dictionaries:
                        packages.append(paq_name)
        serveur.installed_packages = packages

    def get_extra_packages(self, serveur):
        # recherche des paquets supplémentaires
        f_perso = os.path.join(serveur.get_confdir(), 'fichiers_zephir', 'fichiers_zephir')
        files, paqs = self.serveur_pool.get_fic_perso(f_perso)
        f_var = os.path.join(serveur.get_confdir(), 'fichiers_zephir', 'variante', 'fichiers_variante')
        if os.path.isfile(f_var):
            variantes_files, variante_paqs = self.serveur_pool.get_fic_perso(f_var)
        else:
            variante_paqs = []
        return [paq for paq in paqs + variante_paqs if not paq.startswith('#')]

    def check_inactive_packages(self, serveur):
        """calcule la liste des paquets installés sur le serveur mais non activés
        serveur: objet serveur du pool de serveur (son module doit être supporté)
        """
        # paquets activés pour ce  serveur (au niveau serveur/variante/module)
        id_var = serveur.id_var
        liste_paqs = [paq for paq in self.list_serveur(serveur.id_s, False) if not paq.endswith('.xml')]
        liste_paqs.extend([paq for paq in self.list_variante(id_var, False) if not paq.endswith('.xml')])
        liste_paqs.extend(self.get_module_defaults(serveur.id_mod, False))
        extra_paqs = self.get_extra_packages(serveur)
        # recherche paquets de dictionnaires installés sur le serveur et non activés
        dict_paqs = []
        if serveur.installed_packages is None:
            # parcours initial de la liste des paquets installés
            # sera ensuite mis à jour seulement à la synchronisation du serveur
            self.update_serveur_packages(serveur, extra_paqs)
        for paq_name in serveur.installed_packages:
            if paq_name in extra_paqs:
                extra_paqs.remove(paq_name)
            elif paq_name not in liste_paqs:
                # paquet non activé dans l'application Zéphir!
                dict_paqs.append(paq_name)
        serveur.dict_packages = dict_paqs
        if dict_paqs or extra_paqs:
            params = {"dictpaqs_ok":[0, dict_paqs, extra_paqs]}
        else:
            params = {"dictpaqs_ok":[1, '', '']}
        # sauvegarde en base
        serveur.maj_params(params)
        return dict_paqs

    def get_inactive_packages(self, id_serveur):
        """renvoie la liste des paquets de dictionnaires non activés sur un serveur
        """
        serv = self.serveur_pool[int(id_serveur)]
        packages = serv.dict_packages
        if packages is None:
            # liste jamais calculée, on lance la vérification
            if serv.id_mod in self.modules:
                return self.check_inactive_packages(serv)
            else:
                return []
        return packages

    def add_serveur_dict(self, id_serveur, dict_type, dict_path, update=True):
        """
        ajoute un dictionnaire à un serveur
        update: pas de mise à jour des liens du serveur si False
                 (à utiliser pour les traitements par lot)
        """
        serveur = self.get_serveur(id_serveur)
        module_version = self.modules[serveur.id_mod][1]
        self.set_database_dict(module_version, 'serveur', serveur.id_s, dict_type, dict_path)
        # mise à jour de la liste des paquets non pris en compte
        if update:
            # mise à jour des liens
            self.sync_serveur_packages(id_serveur)
            self.check_inactive_packages(serveur)

    def del_serveur_dict(self, id_serveur, dict_type, dict_path, update=True):
        """
        supprime un dictionnaire d'un serveur
        update: pas de mise à jour des liens du serveur si False
                 (à utiliser pour les traitements par lot)
        """
        serveur = self.get_serveur(id_serveur)
        module_version = self.modules[serveur.id_mod][1]
        # on ne gère que les dictionnaires du pool (les dictionnaires stockés
        # au niveau serveur sont gérés indépendamment)
        # on supprime toutes les entrées dans la base
        self.del_database_dict(module_version, 'serveur', id_serveur, dict_type, dict_path)
        # si paquet de dictionnaires supprimé, on le supprime aussi de fichiers_du serveur
        f_perso = os.path.join(serveur.get_confdir(), 'fichiers_zephir', 'fichiers_zephir')
        fic_serv, paq_serv = self.serveur_pool.get_fic_perso(f_perso)
        if dict_path in paq_serv:
            paq_serv.remove(dict_path)
            self.serveur_pool.save_fic_perso(fic_serv, paq_serv, f_perso)
        if update:
            # mise à jour des liens sur les dictionnaires du pool
            self.sync_serveur_packages(id_serveur)
            self.check_inactive_packages(serveur)

    def list_local_serveur(self, id_serveur):
        """
        Renvoie la liste des dictionnaire à envoyer et des paquets à installer pour un serveur
        """
        serveur = self.get_serveur(id_serveur)
        module_version = self.modules[serveur.id_mod][1]
        local_serv = {'dicos':[], 'paquets':[]}
        for type_res, id_res in (('module',serveur.id_mod), ('variante',serveur.id_var), ('serveur',serveur.id_s)):
            # création de la liste des dictionnaires 'isolés'
            for dict_res in self.get_database_dict(module_version, type_res, id_res):
                if dict_res.split(os.sep)[-2] in ('local', 'eole'):
                    # dictionnaire isolé : à envoyer au serveur
                    local_serv['dicos'].append(dict_res)
                else:
                    paq_name = dict_res.split(os.sep)[-2]
                    if paq_name not in local_serv['paquets']:
                        local_serv['paquets'].append(paq_name)
        # ajout des autres dictionnaires stockés dans le répertoire 'dicos' du serveur
        for dict_serveur in glob(os.path.join(serveur.get_confdir(), 'dicos', 'local' ,'*.xml')):
            if not os.path.islink(dict_serveur):
                local_serv['dicos'].append(dict_serveur)
        return local_serv

    def check_variante_conflicts(self, id_serveur):
        """
        Vérifie qu'il n'existe pas des doublons entre les dictionnaires
        locaux du serveur et ceux de la variante.
        Si doublons, on les désactive au niveau serveur
        """
        serv = self.get_serveur(id_serveur)
        # dictionnaires activés au niveau serveur
        local = self.list_serveur(serv.id_s)
        # dictionnaires activés au niveau variante
        variante = self.list_variante(serv.id_var)
        # on désactive au niveau serveur les éventuels doublons
        dicts = {'local':[],'eole':[]}
        for dict_path in local:
            if dict_path in variante:
                dirname = dict_path.split(os.sep)[-2]
                if dirname in ('eole', 'local'):
                    # on a affaire à un dictionnaire isolé
                    dicts[dirname].append(os.path.basename(dict_path))
                else:
                    # dictionnaire de paquet, on recherche le nom du paquet et son type
                    dict_type = dict_path.split(os.sep)[-3]
                    if dirname not in dicts[dict_type]:
                        # (test car un paquet peut apparaître plusieurs
                        # fois si il contient plusieurs dictionnaires)
                        dicts[dict_type].append(dirname)
        for dict_type, dict_names in list(dicts.items()):
            for dict_name in dict_names:
                self.del_serveur_dict(id_serveur, dict_type, dict_name, False)
                if dict_name.endswith('.xml'):
                    log.msg(" - Serveur %s: désactivation du dictionnaire %s au niveau serveur (présent dans la variante)" % (str(id_serveur), dict_name))
                else:
                    log.msg(" - Serveur %s: désactivation du paquet %s au niveau serveur (présent dans la variante)" % (str(id_serveur), dict_name))
        # mise à jour des liens sur les dictionnaires du pool
        self.update_serveur(id_serveur)
        # vérification des paquets non pris en compte (peut changer si modification de variante)
        self.check_inactive_packages(serv)

    ####################################
    # fonctions utilitaires pour Zéphir

    def reset_modules(self):
        """
        met à jour les liens des dictionnaires pour l'ensemble des modules/variantes gérés
        """
        for id_mod, variantes in list(self.variantes.items()):
            if id_mod in self.modules:
                log.msg("Réinitialisation des dictionnaires de module / variantes : %s" % self.modules[id_mod][0])
                self.update_module(id_mod)
                for id_var in variantes:
                    self.update_variante(id_var, id_mod)


def init_dictpool(serveur_pool):
    """
    fonction d'initialisation du pool de dictionnaire
    """
    # détection des version de distribution disponibles dans le pool
    versions = []
    for eole_version in config.DISTRIBS:
        dict_path = os.path.join(config.ROOT_DIR, 'dictionnaires', config.DISTRIBS[int(eole_version)][1])
        if os.path.isdir(os.path.join(dict_path, 'eole')):
            # répertoire de paquets présents, on gère les dictionnaires de cette distribution
            # on ajoute le répertoire 'local' si il n'existe pas
            if not os.path.isdir(os.path.join(dict_path, 'local')):
                os.makedirs(os.path.join(dict_path, 'local'))
            versions.append(eole_version)
    return DictPool(versions, serveur_pool)
