# -*- coding: utf-8 -*-
###########################################################################
# Eole NG - 2009
# Copyright Pole de Competence Eole  (Ministere Education - Academie Dijon)
# Licence CeCill  cf /root/LicenceEole.txt
# eole@ac-dijon.fr
#
# eolegroup.py
#
# librairie pour la gestion des groupes scribe
#
###########################################################################
"""
    librairies objets de gestion des groupes de Scribe
"""
from time import ctime
from os.path import isdir, join
from os import makedirs, unlink
from shutil import rmtree
from scribe.eoleldap import LdapEntry
from scribe.ldapconf import GROUP_DN, GROUP_FILTER, MAIL_DOMAIN, \
    USERS_DN, MAIL_ADMIN, OPT_PATH, CONTAINER_PATH_MAIL, READER_PWD, ldap_server, \
    num_etab, RACINE, BRANCHE_GROUP_ETAB, ADMIN_GROUPS, HAS_SYMPA, ROOT_DN, ldap_passwd, LDAP_MODE, EDITORS_GROUPS, SEND_POLICY
from scribe.eoletools import format_current_date, launch_smbldap_tool
from scribe.templates import alias_tmpl, liste_tmpl, liste_tmpl_ad, resp_list_tmpl, resp_list_tmpl_ad, creation_list_xml, tmpl_modo
from scribe.errors import NotEmptyNiveau, NiveauNotFound, EmptyGroupCreateKey, \
    NotEmptyGroup
from ldap import MOD_ADD, MOD_REPLACE
try:
    from pyeole.process import system_out
except:
    def system_out(**args):
        raise Exception('impossible de lancer system_out, vous etes certainement dans un conteneur')
try:
    from scribe.eoleshare import Share
except:
    class Share:
        def __init__(self, **args):
            raise Exception("impossible d'instancier Share, vous etes certainement dans un conteneur")

if HAS_SYMPA:
    SYMPA_PATH = {'restreint': join('/var/lib/sympa/expl', MAIL_DOMAIN['restreint']),
                  'internet': '/var/lib/sympa/expl'}
    SYMPA_DATA_SOURCE_PATH = join(CONTAINER_PATH_MAIL, "etc/sympa/data_sources")
ALIASES_FNAME = join(CONTAINER_PATH_MAIL, "etc/mail/sympa/aliases")


def _delete_maillist_aliases(name, domain):
    """
        Suppression des alias exim d'une liste de diffusion
    """
    regexp = r"^%(domain)s-%(name)s:\|^%(domain)s-%(name)s-request:\|^%(domain)s-%(name)s-editor:\|^%(domain)s-%(name)s-owner:" % dict(name=name, domain=domain)
    system_out(["sed", "-i", "/%s/d" % regexp, ALIASES_FNAME])


def _add_maillist_aliases(ldico):
    """
        Ajout des alias exim d'une liste de diffusion
    """
    _delete_maillist_aliases(ldico['groupe'], ldico['ldomaine'])
    alias = alias_tmpl % ldico
    ficp = open(ALIASES_FNAME, 'a')
    ficp.write(alias)
    ficp.close()


class Group(LdapEntry):
    """
        classe pour la gestion des groupes Eole
    """

    def add_groupe(self, name, domaine="", partage="", description=""):
        """ ajout d'un groupe de type Groupe """
        self.add('Groupe', name, domaine, partage, description)

    def add_niveau(self, name, domaine="restreint", partage="", description=""):
        """ ajout d'un groupe de type Niveau """
        self.add('Niveau', name, domaine, partage, description)

    def add_classe(self, name, niveau, domaine="restreint", description=""):
        """ ajout d'un groupe de type Classe """
        self.add('Classe', name, domaine, '', description, niveau=niveau)

    def add_option(self, name, domaine="restreint", partage="", description=""):
        """ ajout d'un groupe de type Option """
        self.add('Option', name, domaine, partage, description)

    def add_matiere(self, name, domaine="restreint", partage="", description=""):
        """ ajout d'un groupe de type Matiere """
        self.add('Matiere', name, domaine, partage, description)

    def add_service(self, name, domaine="restreint", partage="", description=""):
        """ ajout d'un groupe de type Service """
        self.add('Service', name, domaine, partage, description)

    def add(self, _type, name, domaine="", partage="", description="", niveau="", etab=None):
        """
            ajout avec initialisation de la connexion ldap
        """
        self.ldap_admin.connect()
        self._add(_type, name, domaine, partage, description, niveau, sync=True, etab=etab)
        self.ldap_admin.close()

    def _add(self, _type, name, domaine="", partage="", description="", niveau="", sync=False, etab=None, mkdir=True):
        """
            Ajoute un groupe (utilisation de la connexion existante)
        """
        if _type == 'Classe':
            etab = self.get_etab_from_group(niveau)
        self._test_available_name(name)
        self._add_samba(name, etab)
        if _type == 'Classe':
            if niveau == '':
                raise EmptyGroupCreateKey("il manque le niveau associé à la classe %s" % name)
            elif niveau not in self._get_groups('Niveau'):
                raise NiveauNotFound("le niveau %s n'existe pas" % niveau)
            self.cache_etab['group'][name] = etab
        if domaine and HAS_SYMPA:
            self._add_maillist(_type, name, domaine, etab=etab)
        self._add_scribe_group(_type, name, description, niveau, etab=etab)
        if _type == 'Classe':
            self._add_share(name, partage='classe', sync=False, etab=etab, mkdir=mkdir)
            equipe = "profs-%s" % name
            self._add('Equipe', equipe, domaine='restreint',
                    partage='rw', sync=sync, etab=etab, mkdir=mkdir)
            if domaine and HAS_SYMPA:
                # FIXME : liste obligatoire ?
                self._add_resp_maillist(name, domaine, etab=etab)
        elif _type == 'Option':
            self._add_share(name, partage='option', sync=False, etab=etab, mkdir=mkdir)
            equipe = "profs-%s" % name
            # pas de liste de diffusion pour l'option
            self._add('Equipe', equipe, partage='rw', sync=sync, etab=etab, mkdir=mkdir)
        elif partage != "":
            # création du partage
            self._add_share(name, partage, sync=sync, etab=etab, mkdir=mkdir)

    def _add_samba(self, name, etab=None):
        """
            Ajout de la partie Samba
        """
        cmd = ['/usr/sbin/smbldap-groupadd', '-a', name]
        launch_smbldap_tool(cmd, num_etab, etab)

    def add_maillist(self, _type, name, domaine='restreint', etab=None):
        """
            Création d'une lise de diffusion (mode déconnecté)
        """
        self.ldap_admin.connect()
        self._add_maillist(_type, name, domaine, etab=etab)
        self.ldap_admin.close()

    def _add_maillist(self, _type, name, domaine='restreint', etab=None):
        """
            Création d'une lise de diffusion (mode connecté)
        """
        if not HAS_SYMPA:
            raise Exception('sympa is not actived')
        _domaine = MAIL_DOMAIN[domaine]
        # Ajout sympa
        short_path = join(SYMPA_PATH[domaine], name.lower())
        path = CONTAINER_PATH_MAIL + short_path
        if isdir(path):
            #raise Exception, "La liste du groupe %s existe déjà" % name
            print("La liste du groupe %s existe déjà" % name)
#        else:
#            makedirs(path)
        if etab is not None:
            moderators_groups = [f"{etab}-{group}" for group in EDITORS_GROUPS]
        else:
            moderators_groups = EDITORS_GROUPS
        # construction du dico pour les templates sympa
        ldico = {}
        ldico['ldomaine'] = _domaine
        ldico['groupe'] = name
        ldico['date'] = ctime()
        ldico['branche'] = GROUP_DN % dict(cn=name, etab=etab)
        ldico['email'] = MAIL_ADMIN
        ldico['type'] = _type.lower()
        ldico['host'] = ldap_server
        ldico['racine'] =  RACINE
        ldico['list_type'] = SEND_POLICY[_type.lower()]
        ldico['moderation'] = '\n'.join([tmpl_modo % {'moderateur': moderateur}
                                         for moderateur in  moderators_groups])

        # creation de la liste
        config = creation_list_xml % ldico
        ficp = open(join(CONTAINER_PATH_MAIL, 'var/lib/sympa', 'sympa.xml'), 'w')
        ficp.write(config)
        ficp.close()
        code, out, err = system_out(['/usr/bin/sympa', '--create_list', '--robot={}'.format(MAIL_DOMAIN[domaine]), '--input_file=/var/lib/sympa/sympa.xml'], container='mail')
        if code != 0:
            raise Exception('erreur lors de la création de la liste sympa : {} | {}'.format(out, err))
        unlink(join(CONTAINER_PATH_MAIL, 'var/lib/sympa', 'sympa.xml'))

        if LDAP_MODE == 'ad':
            ldico['bind_dn'] = ROOT_DN
            ldico['bind_password'] = ldap_passwd
            config = liste_tmpl_ad % ldico
        else:
            config = liste_tmpl % ldico
        ficp = open(join(path, 'config'), 'w')
        ficp.write(config)
        ficp.close()

        info_file = open(join(path, 'info'), 'a')
        info_file.close()

        _add_maillist_aliases(ldico)

        # droit pour sympa
        cmd = ['/bin/chown', '-R', 'sympa:sympa', short_path]
        system_out(cmd, container='mail')

        # Ajout ldap
        group_dn = self.get_group_dn(name) #GROUP_DN % {'cn': name}
        datas = []
        mail = "%s@%s" % (name, _domaine)
        datas.append((MOD_REPLACE, 'mail', mail))
        self.ldap_admin._modify(group_dn, datas)
        return True

    def _add_resp_maillist(self, classe, domaine='restreint', etab=None):
        """
            Création d'une liste de diffusion "responsables"
        """
        name = "resp-%s" % classe
        _domaine = MAIL_DOMAIN[domaine]
        # Ajout sympa
        short_path = join(SYMPA_PATH[domaine], name)
        path = CONTAINER_PATH_MAIL + short_path
        if isdir(path):
            #raise Exception, "La liste du groupe %s existe déjà" % name
            print("La liste du groupe %s existe déjà" % name)
        else:
            makedirs(path)
        if etab is not None:
            moderators_groups = ["{etab}-{group}" for group in EDITORS_GROUPS]
        else:
            moderators_groups = EDITORS_GROUPS
        # pour ce template, il faut le nom de la classe ;)
        ldico = {}
        ldico['ldomaine'] = _domaine
        ldico['groupe'] = classe
        ldico['moderation'] = '\n'.join([tmpl_modo % {'moderateur': moderateur}
                                         for moderateur in moderators_groups])
        ldico['list_type'] = SEND_POLICY['resp-classe']
        ldico['date'] = ctime()
        ldico['branche'] = USERS_DN % dict(etab=etab)
        ldico['email'] = MAIL_ADMIN
        ldico['type'] = "responsables"
        ldico['pwd'] = READER_PWD
        ldico['host'] = ldap_server
        if LDAP_MODE == 'ad':
            ldico['bind_dn'] = ROOT_DN
            ldico['bind_password'] = ldap_passwd
            config = resp_list_tmpl_ad % ldico
        else:
            config = resp_list_tmpl % ldico
        ficp = open(join(path, 'config'), 'w')
        ficp.write(config)
        ficp.close()

        info_file = open(join(path, 'info'), 'a')
        info_file.close()

        # pour ce template, il faut le nom de la liste :)
        ldico['groupe'] = name
        _add_maillist_aliases(ldico)

        # droit pour sympa
        cmd = ['/bin/chown', '-R', 'sympa:sympa', short_path]
        system_out(cmd, container='mail')

        # FIXME : pas d'entrée ldap pour l'instant
        return True

    def _add_scribe_group(self, _type, name, description, niveau, etab=None):
        """
            Rajoute les options ldap eole d'un groupe
        """
        if not description:
            description = "%s %s" % (_type, name)
        datas = []
        obj = self._get_attr(name, 'objectClass')
        if _type == 'Classe':
            # FIXME : une classe est-elle un ENTGroupe ?
            obj.extend(['eolegroupe', 'classe', 'ENTClasse', 'ENTGroupe'])
            datas.append((MOD_ADD, 'niveau', niveau))
        else:
            obj.extend(['eolegroupe', 'ENTGroupe'])

        datas.append((MOD_REPLACE, 'objectClass', obj))
        datas.append((MOD_ADD, 'type', _type.capitalize()))
        datas.append((MOD_ADD, 'description', description))
        datas.append((MOD_ADD, 'LastUpdate', format_current_date()))
        group_dn = self.get_group_dn(name) #GROUP_DN % tmpl_group
        self.ldap_admin._modify(group_dn, datas)

    def _init_share(self):
        """
            initialise le gestionnaire de partage si ce n'est pas encore fait
        """
        if not hasattr(self, 'share'):
            # utilisation de la classe Share avec récupération de
            # la connexion ldap :)
            self.share = Share()
            self.share.ldap_admin = self.ldap_admin

    def _add_share(self, name, partage, sync, etab=None, mkdir=True):
        """
            ajout d'un partage associé à un groupe
            name : nom du groupe et de son partage
            partage : type de partage ('ro', 'rw', 'dt', 'classe')
        """
        self._init_share()
        # création du partage
        self.share._add(name, name, partage, sync=sync, etab=etab, mkdir=mkdir)

# GET METHODS
    def get_attr(self, name, attr):
        """
            Récupération d'un attribut de groupe
            avec connexion ldap
        """
        self.ldap_admin.connect()
        res = self._get_attr(name, attr)
        self.ldap_admin.close()
        return res

    def get_attrs(self, name, attrs):
        """
            Récupération des attributs ldap d'un groupe
            (initialise la connexion ldap)
            name: nom du groupe
            attrs: ["attr1","attr2"] ou "attr1"
        """
        self.ldap_admin.connect()
        res = self._get_attrs(name, attrs)
        self.ldap_admin.close()
        return res

    def _get_attrs(self, name, attrs):
        """
            renvoie la valeur des attributs attrs pour le groupe 'name'
            attrs : ["attr1","attr2"] ou "attr1"
            sans connexion ldap (préférez get_attrs)
        """
        return self.ldap_admin._search_one("(&%s(cn=%s))" % (
                                GROUP_FILTER, name), attrs)

    def _get_attr(self, name, attr):
        """
            renvoie la valeur d'UN attribut
            sans connexion ldap (préférez get_attr)
        """
        return self._get_attrs(name, [attr]).get(attr, [])

    def get_classes(self, niveau='*', etab=None):
        """
            renvoie les classes avec gestion de la connexion ldap
        """
        self.ldap_admin.connect()
        res = self._get_classes(niveau, etab=etab)
        self.ldap_admin.close()
        return res

    def _get_classes(self, niveau='*', etab=None):
        """
            Renvoie les classes d'un niveau
            niveau : *=tous les niveaux 3*=tous les niveaux commençant par 3...
        """
        filtre = "(&%s(niveau=%s))" % (GROUP_FILTER, niveau)
        if etab is None:
            suffix = None
        else:
            suffix = BRANCHE_GROUP_ETAB % {'etab': etab}
        result = self.ldap_admin._search(filtre, ['cn', 'niveau'], suffix=suffix)
        classes = {}
        for res in result:
            nivo = res[1]['niveau'][0]
            if not nivo in classes:
                classes[nivo] = []
            classes[nivo].append(res[1]['cn'][0])
        return classes

    def get_groups(self, _type='', etab=None):
        """
            Renvoie la liste des groupes de type _type
            _type : type du groupe ('' tous les groupes)
        """
        self.ldap_admin.connect()
        res = self._get_groups(_type, etab)
        self.ldap_admin.close()
        return res

    def _get_groups(self, _type='', etab=None):
        """
            renvoie les groupes
        """
        if _type == 'Administrateurs':
            return ADMIN_GROUPS
        elif _type != '':
            filtre = "(type=%s)" % (_type.capitalize())
        else:
            filtre = ""
        group_filter = GROUP_FILTER
        if LDAP_MODE == 'ad':
            suffix = None
            if etab is not None:
                if not isinstance(etab, list):
                    etabs = [etab]
                else:
                    etabs = etab
                group_filter += '(|' + ''.join([f"(rne={etb})" for etb in etabs]) + ')'
        else:
            if etab is None:
                suffix = None
            else:
                # sur Scribe on récupère forcement qu'un seul établissement
                if isinstance(etab, list):
                    etab = etab[0]
                suffix = BRANCHE_GROUP_ETAB % {'etab': etab}
        result = self.ldap_admin._search("(&%s%s)" % (group_filter, filtre), ['cn'], suffix=suffix)
        return [res[1]['cn'][0] for res in result]

    def get_group_type(self, name):
        """
            renvoie le type d'un groupe
        """
        self.ldap_admin.connect()
        res = self.c_get_group_type(name)
        self.ldap_admin.close()
        return res

    def c_get_group_type(self, name):
        """
            renvoie le type d'un groupe
        """
        if name in ADMIN_GROUPS:
            return 'Administrateurs'
        try:
            return self._get_attr(name, 'type')[0]
        except IndexError:
            raise Exception("Groupe %s inconnu" % name)

    def get_maillist(self, name):
        """
            renvoit l'adresse mail d'un groupe (déconnecté)
        """
        mail = self.get_attr(name, 'mail')
        if mail:
            return mail[0]
        else:
            return ""

    def _get_maillist(self, name):
        """
            renvoit l'adresse mail d'un groupe (connecté)
        """
        mail = self._get_attr(name, 'mail')
        if mail:
            return mail[0]
        else:
            return ""


# MODIFY METHODS

    def _set_attr(self, name, attribut, value):
        """
            met à jour un attribut d'un groupe
        """
        group_dn = self.get_group_dn(name) #GROUP_DN % {'cn':name}
        data = [((MOD_REPLACE, attribut, value))]
        self.ldap_admin._modify(group_dn, data)

    def _touch(self, name):
        """
            Mise à jour de l'attribut LastUpdate
        """
        self._set_attr(name, 'LastUpdate', format_current_date())

    def _Upgrade(self, name):
        """
            Mise à niveau d'un groupe existant
        """
        group_dn = self.group_dn(name) #GROUP_DN % {'cn':name}
        datas = []
        if name in ['eleves', 'professeurs', 'administratifs']:
            _type = 'Base'
        else:
            _type = self._get_attr(name, 'description')[0].split(' ')[0]
        obj = self._get_attr(name, 'objectClass')
        if _type == 'Classe':
            # FIXME : une classe est-elle un ENTGroupe ?
            obj.extend(['eolegroupe', 'ENTClasse', 'ENTGroupe'])
        else:
            obj.extend(['eolegroupe', 'ENTGroupe'])
        datas.append((MOD_REPLACE, 'objectClass', obj))
        datas.append((MOD_ADD, 'type', _type.capitalize()))
        datas.append((MOD_ADD, 'LastUpdate', format_current_date()))
        if self._get_attr(name, 'displayName') == []:
            datas.append((MOD_REPLACE, 'displayName', name))
        self.ldap_admin._modify(group_dn, datas)

    def _purge_groupe(self, name):
        """
            désinscrit tous les membres d'un groupe
        """
        self._set_attr(name, 'memberUid', [])

    def _purge_option(self, name):
        """
            désinscrit tous les élèves d'une option
        """
        self._purge_groupe(name)
        # suppression des liens symboliques
        cmd = ['rm', '-f', join(OPT_PATH, name, '*')]
        system_out(cmd)


# DELETE METHODS
    def delete(self, name, rmdir=False, sync=True):
        """
            supprime un groupe
            (avec connexion ldap)
        """
        self.ldap_admin.connect()
        self._delete(name, rmdir, sync)
        self.ldap_admin.close()

    def _delete(self, name, rmdir=False, sync=False):
        """
            supprime un groupe
            sans connexion ldap (préférez delete pour une suppression simple)
        """
        try:
            _type = self.c_get_group_type(name)
        except:
            _type = None
        try:
            domain = self._get_maillist(name).split('@')[1]
        except:
            domain = None
        if _type:
            if _type == 'Classe':
                self._delete_classe(name, rmdir, domain=domain)
            elif _type == 'Option':
                self._delete_option(name, rmdir)
            else:
                if _type == 'Niveau' and self._get_classes(name):
                    raise NotEmptyNiveau("Des classes sont encore associées au niveau %s." % name)
                self._delete_group(name)
                self._delete_shares(name, rmdir, sync)
            if domain is not None:
                self._delete_maillist(name, domain)
        else: # si il y a eu une erreur à la création du groupe
              # on peut avoir un groupe sans le champ description
            self._delete_group(name)
            self._delete_shares(name, rmdir, sync)

    def _delete_shares(self, name, rmdir=False, sync=False):
        """
            supprime les partages du groupe name
            name: nom du groupe
            rmdir: suppression des répertoires des partages
        """
        shares = self._get_group_sharedirs(name)
        if shares:
            self._init_share()
            for share, filepath in shares:
                self.share._delete(share, rmdir, sync=False)
            if sync:
                self.share._synchronize(restart=True)

    def _delete_maillist_dir(self, name):
        """
            Suppression du répertoire d'une liste de diffusion
        """
        for dirname in SYMPA_PATH.values():
            mail_dir = CONTAINER_PATH_MAIL + join(dirname, name.lower())
            if isdir(mail_dir):
                rmtree(mail_dir)

    def _delete_maillist(self, name, domain):
        """
            supprimer la mailinglist d'un groupe
            ne nécessite pas de connexion ldap
        """
        if HAS_SYMPA:
            self._delete_maillist_dir(name)
        _delete_maillist_aliases(name, domain)

    def _delete_group(self, name):
        """
            supprime l'entrée ldap-samba d'un groupe
        """
        etab = self.get_etab_from_group(name)
        cmd = ['/usr/sbin/smbldap-groupdel', name]
        launch_smbldap_tool(cmd, num_etab, etab)
        try:
            system_out(cmd, container='fichier')
        except Exception as err:
            raise Exception("Erreur de suppression du groupe %s : %s" % (name, err))

    def _delete_classe(self, name, rmdir=False, sync=False, domain=None):
        """
            Supprime une classe et son équipe pédagogique
            name : nom de la classe
            rmdir : supprimer les données ?
        """
        if self._get_members(name):
            raise NotEmptyGroup("Suppression impossible : \
il reste des élèves en %s" % name)
        # suppression des nomination professeur principal
        from scribe.enseignants import Enseignant
        ens = Enseignant()
        ens.ldap_admin = self.ldap_admin
        ens._purge_classes_admin(name)
        # suppression de l'équipe pédagogique
        self._delete("profs-%s" % name, rmdir=rmdir, sync=False)
        # on part du principe d'un partage unique pour la classe
        self._delete_group(name)
        self._init_share()
        self.share._delete_classe_share(name, rmdir, sync=sync)

        if domain is not None:
            # suppression de la liste de diffusion des responsables
            self._delete_maillist("resp-%s" % name, domain)

    def _delete_option(self, name, rmdir=False, sync=False):
        """
            Supprime une option et son équipe pédagogique
            name : nom de l'option
            rmdir : supprimer les données ?
        """
        # FIXME: interdire la suppression d'une option non vide ?
        #if self._get_members(name):
        #    raise NotEmptyGroup("Suppression impossible : \
#il reste des élèves dans l'option %s" % name)
        # suppression de l'équipe pédagogique
        self._delete("profs-%s" % name, rmdir=rmdir, sync=False)

        # on part du principe d'un partage unique pour l'option
        self._delete_group(name)
        self._init_share()
        self.share._delete_option_share(name, rmdir, sync=sync)

    def get_group_dn(self, group):
        etab = self.get_etab_from_group(group)
        return GROUP_DN % dict(cn=group, etab=etab)

