# -*- 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
#
# eoleshare.py
#
# librairie de fontions
#
###########################################################################
"""
    librairies de fonctions communes
"""
import codecs
from os import system, makedirs, unlink, close, pardir, environ
from os.path import isdir, join, basename, getsize, abspath
from tempfile import mkstemp, mktemp
from datetime import datetime, timedelta
from shutil import move, rmtree
from random import SystemRandom
import secrets
import string

from scribe.ldapconf import MIN_PASSWORD_LENGTH, MIN_PASSWORD_CLASS, \
     HOME_PATH, AD_HOME_PATH, EOLE_AD, RECYCLAGE_PATH, MAILDIR_PATH, CONTAINER_NAME
from pyeole.process import system_code, system_out
from pyeole.service import unmanaged_service
from pyeole.service.error import ServiceError
from pyeole.deprecation import deprecated
from fichier.passwd import check_nb_min_classes
from fichier.acl import set_owner

try:
    from creole.eosfunc import load_container_var
    conteneurs = load_container_var()
except:
    conteneurs = {}

random = SystemRandom()

char_classes = [
        string.ascii_uppercase,
        string.ascii_lowercase,
        string.digits,
        # string.punctuation, # contient des caractères potentiellement problématiques
        '&$@%=',
        ]

def add_dir(dirname):
    """
    wrapper pour mkdir -p
    """
    if not isdir(dirname):
        makedirs(dirname)

@deprecated
def set_acl(dirname, groupname, mode='rwx', recursive=True):
    """
        Mets en place des acls sur dirname pour le groupe groupname
        :dirname: nom du répertoire (absolu)
        :groupname: nom du groupe
        :mode: type de droit de la forme rwx /r-x ...
        :rec: Mise en place récursive
    """
    # l'option -P de setfacl permet d'ignorer les liens symboliques
    if recursive:
        cmd = ['setfacl', '-PRm', 'g:%s:%s' % (groupname, mode), '%s' % dirname]
    else:
        cmd = ['setfacl', '-Pm', 'g:%s:%s' % (groupname, mode), '%s' % dirname]
    ret = system_out(cmd)
    if ret[0] != 0:
        print('error in set_acl: {} - {}'.format(' '.join(cmd), join(' - '.join(ret[1:]))))
        print('return code: {}'.format(ret[0]))
        if 'VM_DIR_EOLE_CI_TEST' in environ and ret[0] == 2:
            # on est dans le serveur de test EOLE
            # a priori il ne retrouve pas l'utilisateur
            print()
            print('* wbinfo -u')
            system_code(['wbinfo', '-u'])
            print()
            print('* wbinfo -g')
            system_code(['wbinfo', '-g'])
            print()
            print('* systemctl status ')
            system_code(['systemctl', 'status', 'winbind.service', '--no-pager'])
    elif ret[1]:
        print(ret[1])

@deprecated
def set_default_acl(dirname, recursive=True):
    """
        Met les acls par défaut récursivement sur dirname
    """
    if recursive:
        system("""getfacl --access --absolute-names "%s" | setfacl -Rd -M- "%s" """ % (
                                                        dirname, dirname))
    else:
        system("""getfacl --access --absolute-names "%s" | setfacl -d -M- "%s" """ % (
                                                        dirname, dirname))


def add_symlink(source, destination):
    """
        Ajoute un lien symbolique destination pointant vers source
    """
    system("""ln -nsf "%s" "%s" """ % (source, destination))

def convert_file(filename):
    """
    conversion du fichier en UTF-8
    (si nécessaire)
    retourne le type de format détecté (utf8 ou utf8 avec bom)
    """
    begin = min(32, getsize(filename))
    raw = open(filename, 'rb').read(begin)

    if raw.startswith(codecs.BOM_UTF8):
        return "utf-8-sig"

    cmd = '/usr/bin/iconv'
    cont = open(filename, 'rb').read()
    try:
        cont.decode('utf8')
        # déjà en UTF-8
        return "utf-8"
    except:
        pass
    try:
        # fichier en ISO
        cont.decode('iso8859-1')
        tmpf = mktemp()
        system('%s -f iso8859-1 -t utf8 "%s" > "%s"' % (cmd, filename, tmpf))
        system('/bin/mv -f "%s" "%s"' % (tmpf, filename))
        return "utf-8"
    except:
        raise UnicodeError("Tentative de conversion de fichier impossible !")


def ok_groupe(groupe, gtype='g'):
    """
    portage de la fonction PHP originale
    """
    groupe = groupe.lower()
    if gtype == 'n':
        # niveau coupé au 1er espace
        groupe = groupe.split()[0]
    groupe = groupe.split('(')[0]     #parenthèse ouvrante
    groupe = groupe.split(',')[0]     #virgule
    groupe = groupe.replace(' ', '')  #espaces
    groupe = groupe.replace('*', 'e') #etoile, ex : mp*
    groupe = groupe.replace('.', '')  #point
    groupe = groupe.replace(':', '')  #2points
    groupe = groupe.replace(';', '')  #point-virgule
    groupe = groupe.replace('=', '')  #égal
    groupe = groupe.replace('"', '')  #double-quotes
    groupe = groupe.replace("'", '')  #apostrophes
    groupe = groupe.replace('$', '')  #dollar
    groupe = groupe.replace('+', '')  #plus
    groupe = groupe.replace(')', '')  #parenthèse fermante
    groupe = groupe.replace('&', '')  #esperluette
    groupe = groupe.replace('/', '-') #slash
    # caractères interdits en début de nom
    while groupe.startswith('-') or groupe.startswith('_'):
        groupe = groupe[1:]
    # groupes 100% numériques
    if groupe.isdigit():
        # ajout du prefixe "type"
        groupe = gtype+groupe
    # FIXME
    if len(groupe) > 20:
        return groupe[0:20]
    return groupe

def replace_cars(chaine):
    """
    fonction de suppression des caractères spéciaux les plus courants
    """
    if chaine is None:
        return ''
    chars = {'¥' : 'Y', 'µ' : 'u', 'À' : 'A', 'Á' : 'A',
             'Â' : 'A', 'Ã' : 'A', 'Ä' : 'A', 'Å' : 'A',
             'Æ' : 'A', 'Ç' : 'C', 'È' : 'E', 'É' : 'E',
             'Ê' : 'E', 'Ë' : 'E', 'Ì' : 'I', 'Í' : 'I',
             'Î' : 'I', 'Ï' : 'I', 'Ð' : 'D', 'Ñ' : 'N',
             'Ò' : 'O', 'Ó' : 'O', 'Ô' : 'O', 'Õ' : 'O',
             'Ö' : 'O', 'Ø' : 'O', 'Ù' : 'U', 'Ú' : 'U',
             'Û' : 'U', 'Ü' : 'U', 'Ý' : 'Y', 'ß' : 's',
             'à' : 'a', 'á' : 'a', 'â' : 'a', 'ã' : 'a',
             'ä' : 'a', 'å' : 'a', 'æ' : 'a', 'ç' : 'c',
             'è' : 'e', 'é' : 'e', 'ê' : 'e', 'ë' : 'e',
             'ì' : 'i', 'í' : 'i', 'î' : 'i', 'ï' : 'i',
             'ð' : 'o', 'ñ' : 'n', 'ò' : 'o', 'ó' : 'o',
             'ô' : 'o', 'õ' : 'o', 'ö' : 'o', 'ø' : 'o',
             'ù' : 'u', 'ú' : 'u', 'û' : 'u', 'ü' : 'u',
             'ý' : 'y', 'ÿ' : 'y', '~B': 'e', '°' : '.',
             'Ō' : 'O', 'Ÿ' : 'y',
             '\\': '|', 'Œ' : 'OE', 'œ' : 'oe',
            }
    for old, new in list(chars.items()):
        chaine = chaine.replace(old, new)
    return chaine

def replace_more_cars(chaine):
    """
    fonction de suppression des caractères spéciaux les plus courants
    """
    return replace_cars(chaine).replace('(', '').replace(')', '')

def ok_name(uid):
    """
    suppression des caractères interdits pour un uid
    du type prenom.nom
    """
    uid = replace_cars(uid)
    uid = uid.replace('.', '')
    uid = uid.replace(',', '')
    uid = uid.replace('-', '')
    uid = uid.replace(';', '')
    uid = uid.replace("'", '')
    uid = uid.replace('"', '')
    uid = uid.replace('/', '')
    uid = uid.replace('+', '')
    uid = uid.replace('(', '')
    uid = uid.replace(')', '')
    uid = uid.replace('&', '')
    return uid

def manage_nom(nom):
    """ gestion des noms à particule """
    nom = nom.lower().replace("'", '')
    particules = ['de', 'du', 'dela', 'desa', 'dos', 'di', 'es', 'el', 'le',
                  'la', 'da', 'van', 'von', 'ben', 'saint', 'ez', 'ait', 'sa',
                  'del', 'et']
    for particule in particules:
        if nom.startswith('%s ' % particule):
            nom = nom.replace('%s ' % particule, particule, 1)
        elif nom.startswith('%s-' % particule):
            nom = nom.replace('%s-' % particule, particule, 1)
    nom = nom.split(' ')[0]
    nom = nom.split('-')[0]
    return nom

def manage_prenom(prenom):
    """ gestion des prénoms à particule """
    prenom = prenom.lower().replace("'", '')
    particules = ['el', 'my']
    for particule in particules:
        if prenom.startswith('%s ' % particule):
            prenom = prenom.replace('%s ' % particule, particule, 1)
    prenom = prenom.split(' ')[0]
    return prenom

def gen_login(pref, prenom, nom):
    """
    génération d'un login acceptable :)
    @pref : standard/pnom/nomp/p.nnn/prenom.n
    @prenom : prénom de l'utilisateur
    @nom : nom de l'utilisateur
    """
    prenom = ok_name(manage_prenom(prenom))
    nom = ok_name(manage_nom(nom))
    if prenom != '':
        if pref == 'standard':
            login = "%s.%s" % (prenom, nom)
            if len(login) > 19:
                login = "%s.%s" % (prenom[0], nom)
                if len(login) > 19:
                    login = "%s.%s" % (prenom, nom[0])
                    if len(login) > 19:
                        login = "%s.%s" % (prenom[0], nom[0])
        elif pref == 'pnom':
            login = "%s%s" % (prenom[0], nom)
            if len(login) > 19:
                login = "%s%s" % (prenom[0], nom[0])
        elif pref == 'nomp':
            login = "%s%s" % (nom, prenom[0])
            if len(login) > 19:
                login = "%s%s" % (nom[0], prenom[0])
        elif pref == 'p.nnn':
            login = "%s.%s" % (prenom[0], nom[0:3])
        elif pref == 'prenom.n':
            login = "%s.%s" % (prenom, nom[0])
            if len(login) > 19:
                login = "%s.%s" % (prenom[0], nom[0])
        else:
            raise Exception("Préférence %s inconnue pour le format de login")
    else:
        login = nom
    if len(login) > 19:
        login = login[0:19]
    return login


def valid_password(pwd):
    """
    Vérifie la validité d'un mot de passe
    selon les critères définis dans la configuration du module
    """
    return len(pwd) >= MIN_PASSWORD_LENGTH and check_nb_min_classes(pwd, MIN_PASSWORD_CLASS)

def shuffle(pool):
    for i in range(len(pool) -1, 0, -1):
        j = secrets.choice(range(0, i))
        pool[i], pool[j] = pool[j], pool[i]
    return pool

def gen_stronger_password(min_length=8, max_length=8, classes=4):
    if classes > len(char_classes):
        raise Exception("Trop de classes demandées")
    if max_length < classes:
        raise Exception("Pas assez de caractères par rapport au nombre de classes")
    if min_length < classes:
        min_length = classes
    if max_length < min_length:
        max_length = min_length
    nbtarget = secrets.choice(range(min_length, max_length + 1))

    frequences = []
    for i in range(classes - 1):
        residu = max(nbtarget - sum(frequences) - classes, 1)
        frequences.append(secrets.randbelow(residu))
    frequences.append(nbtarget - sum(frequences) - classes)
    frequences = [i + 1 for i in frequences]
    frequences = shuffle(frequences)

    pool = []
    for classe, frequence in zip(random.sample(char_classes, classes), frequences):
        pool += [secrets.choice(classe) for nb in range(frequence)]
    pool = shuffle(pool)
    return "".join(pool)

def gen_strong_password():
    """
    Génération d'un mot de passe aléatoire solide
    (8 caractères + 3 classes minimum)
    """
    def random_letter(upper=False):
        "[a-z] ou [A-Z]"
        letter = random.choice('abcdefghijklmnopqrstuvwxyz')
        return letter.upper() if upper else letter
    def random_upper():
        "[A-Z]"
        return random_letter(upper=True)
    def random_number():
        "[0-9]"
        return str(random.randint(0, 9))
    def random_special_cars():
        "our special chars"
        return random.choice('&$@%=')
    random_factory = {
            1: random_letter,
            2: random_upper,
            3: random_number,
            4: random_special_cars}
    # let's decide how to populate the passwd string
    # default weighting ([a-z] : 4, [A-Z] : 3, [0-9] : 2, other : 2)
    weighting = [1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4]
    return "".join([random_factory[n]() for n in random.sample(weighting, 8)])

def format_current_date(format="%Y%m%d"):
    """
    renvoie la date du jour formatée
    """
    now = datetime.today()
    return datetime.strftime(now, format)

def format_old_date(format="%Y%m%d"):
    """
    renvoie une date un peu ancienne
    (utilisée pour la mise à niveau de l'annuaire)
    """
    # 8 -> il y a 8 jours
    date = datetime.today() - timedelta(8)
    return datetime.strftime(date, format)

def is_empty(dico, cle):
    """
    pas de clé ou valeur vide
    """
    if cle in dico and dico[cle] != '':
        return False
    return True

def not_empty(dico, cle):
    """
    verifie que la cle existe
    et que la valeur associee n'est pas vide
    """
    return not is_empty(dico, cle)

def formate_civilite(titre):
    """
        civilité au format Eole
    """
    if titre.lower() == 'mlle':
        return '3'
    elif titre.lower() == 'mme':
        return '2'
    return '1'

def to_list(data):
    """
        formatage chaine ou liste en liste sans doublons ;)
    """
    if data is None:
        return data
    elif data == '':
        return []
    elif not hasattr(data, '__iter__') or isinstance(data, str):
        return list(set([data]))
    return list(set(data))

def formate_date(date, sep='/'):
    """
    formate une date "américaine" (ex : attribut dateNaissance)
    en date "française" avec ou sans séparateur
    """
    try:
        if '/' in date:
            year, month, day = date.split('/', 2)
        elif '-' in date:
            year, month, day = date.split('-', 2)
        elif len(date) == 8:
            year, month, day = (date[6:8], date[4:6], date[0:4])
        else:
            return date
    except ValueError:
        return date
    if len(year) == 4:
        return sep.join((day, month, year))
    elif len(day) == 4:
        # la date était déjà à l'anglaise !
        return sep.join((year, month, day))
    return date

def clean_date(date):
    """
    supprime les éventuels séparateurs d'une date
    """
    return date.replace('/','').replace('-','').replace(' ', '')

def get_days_from_epoch(date):
    """
    donne le nombre de jour entre epoch et une date au format
    jj/mm/aaaa
    """
    target_date = datetime(year=int(date[6:10]), month=int(date[3:5]), day=int(date[0:2]))
    return (target_date - datetime(1970, 1, 1)).days

def get_date_from_epoch(unix_time):
    """
    donne la donne epoch + days au format jj/mm/aaaa
    """
    date = datetime.fromtimestamp(unix_time)
    return date.strftime('%d/%m/%Y')

def get_nt_time_from_date(date):
    """
    donne la durée en centaine de ns depuis une date
    jj/mm/aaaa
    """
    target_date = datetime(year=int(date[6:10]), month=int(date[3:5]), day=int(date[0:2]))
    return (target_date - datetime(1601, 1, 1)).days * 24 * 3600 * 10000000

def get_date_from_nt_time(nt_time):
    """
    donne la date 1/1/1601 + centaines de ns au format jj/mm/aaaa
    """
    date = datetime(year=1601, month=1, day=1) + timedelta(seconds=nt_time/10000000)
    return date.strftime('%d/%m/%Y')

def deformate_date(date):
    """
    formate une date en aaaammjj pour l'annuaire
    (historique)
    formats acceptés : jj/mm/aa[aa], jj-mm-aa[aa], jjmmaa[aa]
    """
    date = clean_date(date)
    if len(date) == 8:
        return date[4:8]+date[2:4]+date[0:2]
    elif len(date) == 6:
        return date[4:6]+date[2:4]+date[0:2]
    raise Exception("format de date incorrect : %s" % date)

def fixdate(date):
    """
    renvoie une date aaaammjj valide
    """
    if len(date) == 8 and int(date[0:4]) > 1:
        day = int(date[6:8])
        month = int(date[4:6])
        if month in [1, 3, 5, 7, 8, 10, 12] and day in range(1, 32):
            # mois à 31 jours
            return date
        elif month in [4, 6, 9, 11] and day in range(1, 31):
            # mois à 30 jours
            return date
        elif month == 2 and day in range(1, 30):
            # février
            return date
    return '10010101'

def strip_adresse(adresse):
    """
    Transformation d'une adresse en vue d'en faciliter la comparaison
    """
    adresse = adresse.replace('\n', '').replace(' ', '').replace(',', '')
    return adresse.replace('.', '').lower()

def actualise_cache():
    """ démarrage de LSC """
    if EOLE_AD:
        cmd = ['/usr/bin/actualise_cache']
        ret = system_out(cmd)
        if ret[0] != 0:
            print('error in actualise_cache: {} - {}'.format(' '.join(cmd), ' - '.join(ret[1:])))
            print('return code: {}'.format(ret[0]))

def nscd_start():
    """ démarrage de LSC """
    if EOLE_AD:
        actualise_cache()
        unmanaged_service('start', 'eole-lsc', 'service')

def nscd_stop():
    """ arrêt de LSC """
    if EOLE_AD:
        unmanaged_service('stop', 'eole-lsc', 'service')

def lsc_sync():
    if EOLE_AD:
        cmd = ['lsc', '-f', '/etc/lsc', '-s', 'all', '-t1']
        ret = system_out(cmd)
        if ret[0] != 0:
            print('error in lsc_sync: {} - {}'.format(' '.join(cmd), ' - '.join(ret[1:])))
            print('return code: {}'.format(ret[0]))
        else:
            try:
                unmanaged_service('status', 'eole-lsc', 'service')
                nscd_stop()
                nscd_start()
            except ServiceError:
                # ne pas redémarrer lsc
                actualise_cache()

def lsc_full_sync():
    if EOLE_AD:
        cmd = ['lsc', '-f', '/etc/lsc', '-s', 'all', '-c', 'all']
        ret = system_out(cmd)
        if ret[0] != 0:
            print('error in lsc_full_sync: {} - {}'.format(' '.join(cmd), ' - '.join(ret[1:])))
            print('return code: {}'.format(ret[0]))
        else:
            try:
                unmanaged_service('status', 'eole-lsc', 'service')
                nscd_stop()
                nscd_start()
            except ServiceError:
                # ne pas redémarrer lsc
                actualise_cache()

def calc_recycle_dir(subdir):
    """
    calcul et création du répertoire de recyclage
    /home/recyclage/<année>/<subdir>
    """
    current_year = format_current_date(format="%Y")
    recycle_dir = join(RECYCLAGE_PATH, current_year, subdir)
    add_dir(recycle_dir)
    return recycle_dir

def calc_dest_dir(base_dir):
    """
    calcul du dossier de destination
    si existant, on renomme en .1, .2,...
    """
    dest_dir = base_dir
    num = 1
    while isdir(dest_dir):
        dest_dir = "%s.%d" % (base_dir, num)
        num += 1
    return dest_dir

def create_ad_home(login, home):
    """
    Déplace le home et crée le lien
    """
    ad_home = join(AD_HOME_PATH, login)
    abc = abspath(join(home, pardir))
    add_dir(abc)
    if not isdir(ad_home):
        if isdir(home):
            move(home, ad_home)
        else:
            makedirs(ad_home, 0o700)
        set_owner(ad_home, login)
        system('chmod u+w {}'.format(ad_home))
        add_symlink(ad_home, home)
    # else: #FIXME

def move_user_datas(user):
    """
    déplacement des données d'un utilisateur supprimé
    sans demande de suppression de ses données
    """
    recycle_dir = calc_recycle_dir(user[0])
    dest_dir = calc_dest_dir(join(recycle_dir, user))
    old_user_dir = join(HOME_PATH, user[0], user)
    ad_user_dir = join(AD_HOME_PATH, user)
    if isdir(ad_user_dir):
        real_user_dir = ad_user_dir
        # remove symlink
        unlink(old_user_dir)
    elif isdir(old_user_dir):
        real_user_dir = old_user_dir
    else:
        return
    # d'abord un peu de ménage (#2101)
    rmtree(join(real_user_dir, '.ftp'), ignore_errors=True)
    rmtree(join(real_user_dir, 'groupes'), ignore_errors=True)
    system('find "%s" -type l -delete' % real_user_dir)
    move(real_user_dir, dest_dir)

def move_mail_datas(user):
    """
    déplacement des données d'un compte Mail supprimé
    sans demande de suppression de ses données
    """
    old_user_dir = join(MAILDIR_PATH, user)
    if isdir(old_user_dir):
        recycle_dir = calc_recycle_dir('mail')
        dest_dir = calc_dest_dir(join(recycle_dir, user))
        move(old_user_dir, dest_dir)

def move_share_datas(filepath):
    """
    déplacement des données d'un partage supprimé
    sans demande de suppression des données
    """
    old_share_dir = filepath
    # suppression des liens symobliques (#2101)
    system('find "%s" -type l -delete' % old_share_dir)
    recycle_dir = calc_recycle_dir('workgroups')
    dest_dir = calc_dest_dir(join(recycle_dir, basename(filepath)))
    move(old_share_dir, dest_dir)

def launch_smbldap_tool(cmd, ref_etab, etab, force_dn={}):
    tempfile = None
    if etab != None and ref_etab != etab:
        filename = '%s/etc/smbldap-tools/smbldap.conf' % conteneurs.get('container_path_fichier', '')
        confdir = '/etc/smbldap-tools/'
        fd, tempfile = mkstemp(suffix='.temp', dir='%s%s' % (conteneurs.get('container_path_fichier', ''), confdir))
        container_file = '%s%s' % (confdir, basename(tempfile))
        fh = open(filename, 'r')
        filecontent = fh.read()
        fh.close()
        newcontent = filecontent.replace('num_etab="{0}"'.format(ref_etab), 'num_etab="{0}"'.format(etab))
        for old, new in list(force_dn.items()):
            newcontent = newcontent.replace(old, new)
        fh = open(tempfile, 'w')
        fh.write(newcontent)
        fh.close()
        env = {'SMBLDAP_CONF': container_file}
    else:
        env = None
    res = system_out(cmd, container=CONTAINER_NAME, env=env)
    if tempfile is not None:
        close(fd)
        unlink(tempfile)
    if res[0] != 0:
        raise Exception("Erreur à l'exécution de la commande %s : %s" % (' '.join(cmd), ''.join(res[1:])))


def get_reverse_login(login):
    if '.' not in login:
        return login
    rev_login = login.split('.', 1)
    rev_login.reverse()
    return '.'.join(rev_login)
