#-*-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
#
# eoleldap.py
#
# librairie pour la connexion à un serveur ldap
#
###########################################################################
"""
Librairie Ldap pour Scribe
"""
import sys
from .ldapconf import SUFFIX, ROOT_DN, USER_FILTER, GROUP_FILTER, SHARE_FILTER, \
    SUPPORT_ETAB, ldap_server, ldap_passwd, num_etab, BRANCHE_GROUP_ETAB, LDAP_MODE, acad
from scribe.errors import LdapExistingGroup, LdapExistingUser, \
SystemExistingUser, NiveauNotFound
from .eoletools import to_list
import ldap
from ldap import SCOPE_ONELEVEL


def is_system_user(user):
    """
        indique si le login proposé est déjà un utilisateur système
    """
    user = user.lower()
    passfile = open('/etc/passwd','r')
    passbuffer = passfile.readlines()
    passfile.close()
    for ligne in passbuffer:
        if user == ligne.split(':')[0]:
            return True
    return False

def get_etabs():
    conn = Ldap()
    conn.connect()
    if LDAP_MODE == 'ad' and SUPPORT_ETAB:
        list_etabs = set()
        for share in conn.connexion.search_s(SUFFIX, ldap.SCOPE_SUBTREE, '(objectClass=sambaFileShare)'):
            if 'rne' in share[1]:
                list_etabs.add(share[1]['rne'][0].decode())
        conn.close()
        list_etabs = list(list_etabs)
        if num_etab in list_etabs:
            list_etabs.remove(num_etab)
    else:
        suffix = "ou=%s,ou=education,%s" % (acad, SUFFIX)
        etabs = conn.connexion.search_s(suffix, SCOPE_ONELEVEL, '(ou=*)', ['ou'])
        conn.close()
        if etabs is None:
            raise Exception('Impossible de récuperer des établissements dans %s' % suffix)
        list_etabs = []
        for etab in etabs:
            for n_etab in etab[1]['ou']:
                list_etabs.append(n_etab.decode())

        if num_etab not in list_etabs:
            raise Exception('Pas de branche LDAP pour le numéro établissement source %s ' % num_etab)
        list_etabs.remove(num_etab)

    return (num_etab, list_etabs)

class _Ldap(object):
    """
        Interface de connection à un serveur ldap
    """

    def __init__(self, serveur=None, passwd=None, binddn=None):
        if serveur is None:
            self.serveur = ldap_server
        else:
            self.serveur = serveur
        if passwd is None:
            self.passwd = ldap_passwd
            self.local_passwd = True
        else:
            self.passwd = passwd
            self.local_passwd = False
        if binddn == None:
            self.binddn = ROOT_DN
        else:
            self.binddn = binddn
        self.connexion = None

    def reload_pwd(self):
        """
            recharge le mot de passe
        """
        if self.local_passwd:
            #reload(backend_conf)
            self.passwd = ldap_passwd

    def _search_one(self, filtre, attrlist=None, suffix=None):
        """
           recherche une entrée dans l'annuaire
        """
        result = self._search(filtre, attrlist=attrlist, suffix=suffix)
        if len(result) > 0 and len(result[0]) == 2:
            result = result[0][1]
            if 'name' in result:
                del result['name']
            if 'forname' in result:
                del result['forname']
            return result
        else:
            return {}

    def _delete(self, dn):
        """
            suppression d'une entrée ldap
        """
        self.connexion.delete(dn)


class _LdapEntry(object):
    """
        classe de base pour gérer les entrées ldap
    """
    def __init__(self, serveur=None, passwd=None):
        self.serveur = serveur
        self.passwd = passwd
        self.ldap_admin = Ldap(serveur, passwd)
        self.cache_etab = {'login': {}, 'group': {}}

    def _is_group(self, name):
        """
            test si le groupe existe dans l'annuaire
        """
        cnfilter = "(&%s(cn=%s))" % (GROUP_FILTER, name)
        if self.ldap_admin._search_one(cnfilter):
            return True
        return False

    def _is_user(self, name):
        """
            test si l'utilisateur existe dans l'annuaire
        """
        uidfilter = "(&%s(uid=%s))" % (USER_FILTER, name)
        if self.ldap_admin._search_one(uidfilter):
            return True
        return False

    def _is_share(self, name):
        """
            test si le partage existe dans l'annuaire
        """
        shfilter = "(&%s(sambaShareName=%s))" % (SHARE_FILTER, name)
        if self.ldap_admin._search_one(shfilter):
            return True
        return False

    def is_available_name(self, name):
        self.ldap_admin.connect()
        res = self._is_available_name(name)
        self.ldap_admin.close()
        return res

    def _is_available_name(self, name):
        """
            teste la disponibilité d'un uid ou un cn
        """
        if self._is_group(name):
            return False
        elif self._is_user(name):
            return False
        elif is_system_user(name):
            return False
        return True

    def _test_available_name(self, name):
        """
            Test la disponibilité d'un nom
            raise une exception si pas disponible
        """
        if self._is_group(name):
            raise LdapExistingGroup
        elif self._is_user(name):
            raise LdapExistingUser
        elif is_system_user(name):
            raise SystemExistingUser
        return True

    def get_niveau(self, classe):
        """
            Retourne le niveau associé à la classe
        """
        self.ldap_admin.connect()
        res = self._get_niveau(classe)
        self.ldap_admin.close()
        return res

    def _get_niveau(self, classe):
        res = self.ldap_admin._search_one("(&%s(cn=%s))" % (GROUP_FILTER,
            classe), ['niveau'])
        try:
            niveau = res['niveau'][0]
        except KeyError:
            raise NiveauNotFound("Impossible de trouver le niveau associé à la classe %s" % classe)
        return niveau

    def get_group_sharedirs(self, group):
        """
            renvoie la liste des partages associés au groupe (mode déconnecté)
        """
        self.ldap_admin.connect()
        res = self._get_group_sharedirs(group)
        self.ldap_admin.close()
        return res

    def _get_group_sharedirs(self, group):
        """
            renvoie la liste des partages associés au groupe
        """
        res = self.ldap_admin._search("(&%s(sambaShareGroup=%s))" % (SHARE_FILTER, group),
                                ['sambaShareName', 'sambaFilePath'])
        list_shares = []
        for share in res:
            list_shares.append([share[1]['sambaShareName'][0],
                                share[1]['sambaFilePath'][0],])
        return list_shares

    def _get_group_logon_shares(self, group):
        """
            renvoie la liste des partages associés au groupe
            - pour la génération du script de logon -
        """
        res = self.ldap_admin._search("(&%s(sambaShareGroup=%s))" % (SHARE_FILTER, group),
                ['sambaShareName', 'sambaFilePath', 'sambaShareURI', 'sambaShareDrive', ])
        list_shares = []
        for share in res:
            if 'sambaShareDrive' in share[1]:
                drive = share[1]['sambaShareDrive'][0]
            else:
                drive = ''
            list_shares.append({'name':share[1]['sambaShareName'][0],
                                'path':share[1]['sambaFilePath'][0],
                                'uri':share[1]['sambaShareURI'][0],
                                'drive':drive,
                              })
        return list_shares

    def get_members(self, group, etab=None):
        """
        renvoie la listes des membres d'un groupe
        """
        self.ldap_admin.connect()
        res = self._get_members(group, etab=etab)
        self.ldap_admin.close()
        return res

    def _get_members(self, group, etab=None):
        """
        renvoie la listes des membres d'un groupe
        """
        if LDAP_MODE == 'ad' or etab is None:
            suffix = None
        else:
            suffix = BRANCHE_GROUP_ETAB % {'etab': etab}
        res = self.ldap_admin._search_one("(&%s(cn=%s))" % (GROUP_FILTER,
                    group), ['memberUid'], suffix=suffix)
        if 'memberUid' in res:
            res = res['memberUid']
            res.sort()
            return res
        else:
            return []

    def _get_user_groups(self, login, etab=None):
        """
        renvoit la liste des groupes d'un utilisateur
        """
        res = self.ldap_admin._search("(&%s(memberUid=%s))" % (
                                GROUP_FILTER, login), 'cn')
        groups = []
        for group in res:
            if etab is not None:
                grp_etab = group[0].split(',ou=')[-3]
                if etab != grp_etab:
                    continue
            groups.append(group[1]['cn'][0])
        groups.sort()
        return groups

    def _get_users(self, filtre='', attrs=['uid']):
        """
        recherche d'utilisateurs
        """
        users = []
        res = self.ldap_admin._search("(&%s%s)" % (USER_FILTER, filtre), attrs)
        for user in res:
            if len(attrs) == 1:
                users.append(user[1][attrs[0]][0])
            else:
                users.append(user[1])
        return users

    def get_etab_from_group(self, group):
        try:
            return self._get_etab('group', group, "(&%s(cn=%s))" % (GROUP_FILTER,
                    group), 'cn')
        except:
            return num_etab


if LDAP_MODE == 'ad':
    import ldap

    LDIF_HEADER = """
dn: {dn}
changeType: {changeType}
{changeOp}: {attribute}
"""
    LDIF_VALUE = """{attribute}: {value}
"""
    CHANGE_TYPE = {
            ldap.MOD_ADD: 'add',
            ldap.MOD_DELETE: 'delete',
            ldap.MOD_REPLACE: 'replace',
            }

    class Ldap(_Ldap):
        """
            Interface de connection à un serveur ldap
        """

        def connect(self):
            """
                binding ldap si nécessaire
            """
            if not self.connexion:
                # FIXME : inutile avec execfile
                #self.reload_pwd()
                self.connexion = ldap.initialize('ldap://{}'.format(self.serveur))
                self.connexion.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
                self.connexion.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
                self.connexion.set_option(ldap.OPT_REFERRALS, False)
                self.connexion.start_tls_s()
                self.connexion.simple_bind_s(self.binddn, self.passwd)

        def close(self):
            """
                fermeture de la connexion ldap
            """
            if self.connexion:
                self.connexion.unbind()
                self.connexion = None

        def _add(self, dn, data):
            """
                ajout d'une entrée ldap
            """
            for idx, d in enumerate(data):
                k, v = d
                if k.lower() == 'objectclass' and v.lower() == 'sambafileshare':
                    v = ['sambafileshare', 'container']
                if d[0] == 'cn' and dn.startswith('cn='):
                    v = dn.split(',')[0][3:]
                if isinstance(v, str):
                    v = v.encode()
                if isinstance(v, list):
                    v = [w.encode() for w in v]
                data[idx] = (k, v)
            self.connexion.add_s(dn, data)

        def _modify(self, dn, data):
            """
                modification d'une entrée ldap
                ('type de modification', 'attribut', 'valeur')
            """
            # everything in bytes
            new_data = []
            for d in data:
                val = d[2]
                if not isinstance(val, list):
                    val = [val]
                new_val = []
                for v in val:
                    if not isinstance(v, bytes):
                        v = v.encode()
                    new_val.append(v)
                new_data.append((d[0], d[1], new_val))

            # import objectclass first
            final_data = []
            for d in new_data:
                if d[1].lower() == 'objectclass':
                    self.connexion.modify_s(dn, [d])
                if d[1].lower() != 'cn':
                    final_data.append(d)
            if final_data:
                self.connexion.modify_s(dn, final_data)

        def _search(self, filtre, attrlist=None, suffix=None):
            """
                recherche dans l'annuaire
            """
            if suffix == None:
                suffix = SUFFIX
            attrlist = to_list(attrlist)
            data = self.connexion.search_s(suffix, ldap.SCOPE_SUBTREE,
                                           filtre, attrlist)
            new_data = []
            for d in data:
                if d[0] is None:
                    continue
                new_d = {}
                for key, val in d[1].items():
                    if key in ['objectGUID', 'objectSid']:
                        continue
                    new_val = []
                    for v in val:
                        if isinstance(v, bytes):
                            v = v.decode()
                        new_val.append(v)
                    new_d[key] = new_val
                new_data.append((d[0], new_d))
            return new_data

    class LdapEntry(_LdapEntry):
        def _get_user_groups(self, login, etab=None):
            """
            renvoit la liste des groupes d'un utilisateur
            """
            uidfilter = "(&%s(cn=%s))" % (USER_FILTER, login)
            ldap_entry = self.ldap_admin._search_one(uidfilter, 'memberOf')
            if not ldap_entry:
                raise Exception('unknown user {}'.format(login))
            user_groups = []
            for grp in ldap_entry['memberOf']:
                if etab:
                    sgrp = grp.split(',OU=')
                    if len(sgrp) > 3:
                        grp_etab = grp.split(',OU=')[-3]
                        if etab != grp_etab:
                            continue
                groupname = grp.split(',', 1)[0].split('=', 1)[1]
                user_groups.append(groupname)
            user_groups.append('domain users')
            return user_groups

        def _get_members(self, group, etab=None):
            """
            renvoie la listes des membres d'un groupe
            """
            if etab is None:
                suffix = None
            else:
                suffix = BRANCHE_GROUP_ETAB % {'etab': etab}
            res = self.ldap_admin._search_one("(&%s(cn=%s))" % (GROUP_FILTER,
                        group), ['member'], suffix=suffix)
            if 'member' in res:
                members = []
                for member in res['member']:
                    members.append(member.split('=', 1)[1].split(',', 1)[0])
                members.sort()
                return members
            else:
                return []

        def _get_etab(self, _type, name, ldap_filter, attr, multi_etabs=False):
            """
                récupère le numéro d'etab du cache ou requète LDAP
            """
            if not SUPPORT_ETAB:
                return None
            if not multi_etabs and name in self.cache_etab[_type]:
                return self.cache_etab[_type][name]
            need_close = False
            if self.ldap_admin.connexion == None:
                self.ldap_admin.connect()
                need_close = True
            try:
                if _type == 'group':
                    attr = 'rne'
                result = self.ldap_admin._search(ldap_filter, [attr])[0]
                if _type == 'login':
                    dn = result[0]
                    etab = [rne[1]['rne'][0] for rne in self.ldap_admin._search(f'(&(type=Etablissement)(member={dn}))', ['rne'])]
                else:
                    etab = result[1]['rne']
                if not multi_etabs:
                    etab = etab[0]
            except:
                etab = None
            if need_close:
                self.ldap_admin.close()
            if not multi_etabs:
                self.cache_etab[_type][name] = etab
            return etab

elif LDAP_MODE == 'openldap':


    class Ldap(_Ldap):
        """
            Interface de connection à un serveur ldap
        """

        def connect(self):
            """
                binding ldap si nécessaire
            """
            if not self.connexion:
                # FIXME : inutile avec execfile
                #self.reload_pwd()
                self.connexion = ldap.initialize('ldap://{}'.format(self.serveur))
                self.connexion.simple_bind_s(self.binddn, self.passwd)

        def close(self):
            """
                fermeture de la connexion ldap
            """
            if self.connexion:
                self.connexion.unbind()
                self.connexion = None

        def _manage_data(self, data):
            new_data = []
            for dat in data:
                ldat = list(dat)
                if isinstance(dat[-1], list):
                    new_list = []
                    for d in dat[-1]:
                        if isinstance(d, str):
                            d = d.encode()
                        new_list.append(d)
                    ldat[-1] = new_list
                elif isinstance(ldat[-1], str):
                    ldat[-1] = ldat[-1].encode()
                new_data.append(tuple(ldat))
            return new_data

        def _add(self, dn, data):
            """
                ajout d'une entrée ldap
            """
            self.connexion.add_s(dn, self._manage_data(data))

        def _modify(self, dn, data):
            """
                modification d'une entrée ldap
            """
            self.connexion.modify_s(dn, self._manage_data(data))

        def _search(self, filtre, attrlist=None, suffix=None):
            """
                recherche dans l'annuaire
            """
            if suffix == None:
                suffix = SUFFIX
            attrlist = to_list(attrlist)
            results =  self.connexion.search_s(suffix, ldap.SCOPE_SUBTREE,
                                               filtre, attrlist)
            if sys.version_info[0] < 3:
                return results
            ret = []
            for key, values in results:
                sub = {}
                for subkey, lst in values.items():
                    sub[subkey] = [l.decode() for l in lst]
                ret.append((key, sub))
            return ret

    class LdapEntry(_LdapEntry):
        def _get_etab(self, _type, name, ldap_filter, attr, multi_etabs=False):
            """
                récupère le numéro d'etab du cache ou requète LDAP
            """
            if not SUPPORT_ETAB:
                return None
            if not multi_etabs and name in self.cache_etab[_type]:
                return self.cache_etab[_type][name]
            need_close = False
            if self.ldap_admin.connexion == None:
                self.ldap_admin.connect()
                need_close = True
            dn = self.ldap_admin._search(ldap_filter, [attr])[0][0]
            if need_close:
                self.ldap_admin.close()
            etab = dn.split(',ou=')[-3]
            if multi_etabs:
                etab = [etab]
            else:
                self.cache_etab[_type][name] = etab
            return etab
else:
    raise Exception('Unknown mode')
