#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from glob import glob
from itertools import product
from os import makedirs, unlink
from os.path import join, isdir, isfile
from IPy import IP, IPSet
from pyeole.service import manage_services
from creole.client import CreoleClient
from creole.eosfunc import is_ip

DHCP_DIR = 'etc/dhcp/fixed-address'
DHCP_CLASSES_DIR = 'etc/dhcp/host-classes'
CONFIG_FILE = '/var/lib/eole/config/dhcp.conf'
STATIC_IP_HOSTS_TMPL = "host %s { hardware ethernet %s; fixed-address %s; }\n"
DYNAMIC_IP_HOSTS_TMPL = "host %s { hardware ethernet %s; }\n"
DHCP_CLASS_TMPL = 'class "{0}" {{\n\tmatch hardware;\n}}\n'
DHCP_SUBCLASSES_TMPL = 'subclass "{0}" {1};\nsubclass "{0}" 1:{1};\n'
try:
    DICO = CreoleClient().get_creole()
except:
    DICO = {}


def build_dir_name(dir_name=DHCP_DIR, dico=DICO):
    """
    Calcul du chemin du répertoire en fonction du mode
    conteneur/non conteneur
    """
    return join('/', dico['container_path_dhcp'], dir_name)


def build_file_name(name, dir_name=DHCP_DIR, dico=DICO):
    """
    Calcul du chemin de l'un des fichiers de configuration
    """
    dirname = build_dir_name(dir_name=dir_name, dico=dico)
    return join(dirname, name+'.txt')


def clean():
    """
    remove old files and create empty files
    """
    for dirname in [DHCP_CLASSES_DIR, DHCP_DIR]:
        dirname = build_dir_name(dirname)
        if not isdir(dirname):
            makedirs(dirname)
        for name in glob(join(dirname, '*')):
            unlink(name)
    # fixed adresse file
    with open(build_file_name('fixed-addresses'), 'w') as fixed_addresses_file:
        fixed_addresses_file.write('')
    # pool related classes file
    for i in range(0, len(DICO['adresse_network_dhcp'])):
        ip_basse = DICO['adresse_network_dhcp.ip_basse_dhcp'][i]
        ip_haute = DICO['adresse_network_dhcp.ip_haute_dhcp'][i]
        filename = '{}-{}'.format(ip_basse, ip_haute)
        with open(build_file_name(filename, dir_name=DHCP_CLASSES_DIR),
                  'w+') as class_file:
            class_file.write('')


def parse_ead_config(ead_config=CONFIG_FILE):
    """
    Extract and structure information from ead exported list of static IP
    configurations.
    :param ead_config: static IP configurations exported from EAD.
    :type ead_config: path
    """
    tuples = []
    if isfile(ead_config):
        with open(ead_config, 'r') as ead_file:
            tuples = [l.strip().split('#') for l in ead_file.readlines()
                      if l.count('#') == 2 and not l.startswith('#')]
    return tuples


def clean_ead_file(hosts_configuration, ead_config=CONFIG_FILE):
    """
    """
    confs = ['#'.join(c)
             for mac, conf in hosts_configuration.items()
             for c in product([conf['name']], [ip.strNormal()
                                               for ip in conf['ip']], [mac])]
    with open(ead_config, 'w') as ead_file:
        ead_file.write('\n'.join(confs))
    return hosts_configuration


# erreurs optionnelles
CRITICAL_ALLOC_ERRORS = set([
    'incohérence couples MAC, nom',
    'format IP invalide',
    'IP hors sous-réseau déclaré',
])


def clean_hosts_configuration(raw_configuration, critical_errors=CRITICAL_ALLOC_ERRORS, dico=DICO, ead_config=CONFIG_FILE):
    """
    Return filtered IP allocations list and trigger /var/lib/eole/config/dhcp.conf
    file cleaning accordingly
    :param raw_configuration: configuration exported by EAD
    :type raw_configuration: `list`  (`str` name, `str` ip, `str` mac)
    """

    subnets = []
    for idx, adresse_network_dhcp in enumerate(dico['adresse_network_dhcp']):
        if dico['adresse_network_dhcp.adressage_statique'][idx] == 'non':
            subnets.append(IP('{0}/{1}'.format(adresse_network_dhcp, dico['adresse_network_dhcp.adresse_netmask_dhcp'][idx])))
    pools = []
    for ip_basse, ip_haute, adressage_statique in zip(dico['adresse_network_dhcp.ip_basse_dhcp'],
                                                      dico['adresse_network_dhcp.ip_haute_dhcp'],
                                                      dico['adresse_network_dhcp.adressage_statique'],
                                                      ):
        if adressage_statique != 'non':
            continue
        pool = IPSet()
        base_network, low_host_bit = ip_basse.rsplit('.', 1)
        high_host_bit = ip_haute.rsplit('.', 1)[-1]
        for host_bit in range(int(low_host_bit), int(high_host_bit) + 1):
            ip = IP('{0}.{1}'.format(base_network, host_bit))
            pool.add(ip)
        pools.append(('{}-{}'.format(ip_basse, ip_haute), pool))
    conf = {'static': [], 'dynamic': {}}
    conf['dynamic'].update({subnet: {} for subnet in subnets})
    for pool in pools:
        for subnet in conf['dynamic']:
            if not pool[1].isdisjoint(IPSet([subnet])):
                conf['dynamic'][subnet].setdefault(pool[0], [])
    hosts = {}
    for name, ip, mac in raw_configuration:

        # consignation des erreurs rencontrées lors des validations optionnelles
        errors = []

        if (mac in hosts and hosts[mac]['name'] != name or
            ((len([h for h in hosts.values() if h['name'] == name]) != 0) and
             (mac not in [m for m, h in hosts.items() if h['name'] == name]))):
            print("Un nom donné ({}) doit être associé à une seule adresse MAC".format(name))
            errors.append('incohérence couples MAC, nom')

        if not is_ip(ip):
            print("Format de l’IP fournie invalide")
            errors.append('format IP invalide')

        elif IP(ip) not in IPSet(list(conf['dynamic'].keys())):  # IP defined outside declared subnets
            print("Aucun subnet n’est déclaré pour cette adresse IP : {0} ({1})".format(ip, name))
            errors.append('IP hors sous-réseau déclaré')

        if not set(errors).isdisjoint(critical_errors):
            continue
        else:
            current_subnet = [s for s in subnets if IP(ip) in s]
            current_subnet = current_subnet[0] if len(current_subnet) != 0 else None
            current_pool = [p for p in pools if IP(ip) in p[1]]
            current_pool = current_pool[0] if len(current_pool) != 0 else None

            if current_pool is None:
                if IP(ip) in IPSet([p for m in hosts.values() for p in m['ip']]):  # ip already allocated
                    print("IP déjà attribuée")
                elif (mac in hosts and  # mac already used in conf
                      current_subnet in hosts[mac]['tree'] and  # mac already used in same subnet
                      len(list(hosts[mac]['tree'][current_subnet].keys())) > 0):  # mac either associated with static address or pool
                    print("Allocation statique de l’IP {} pour l’adresse MAC {} ({}) incompatible avec une allocation pré-existante (même sous-réseau).".format(ip, mac, name))
                else:
                    # add IP to static addresses list
                    conf['static'] = conf.get('static', []) + [(name, ip, mac)]
                    hosts.setdefault(mac, {'name': name, 'ip': [], 'tree': {}})
                    hosts[mac]['tree'].setdefault(current_subnet, {})
                    hosts[mac]['ip'].append(IP(ip))
                    hosts[mac]['tree'][current_subnet]['static'] = IP(ip)

            else:
                if (mac in hosts and  # mac already used in conf
                    current_subnet in hosts[mac]['tree'] and  # mac already used in same subnet
                    len(set(['static', current_pool]).intersection(set(hosts[mac]['tree'][current_subnet].keys()))) is not None):  # mac either associated with static address or same pool
                    print("Association au pool {} de l’adresse MAC {} ({}) incompatible avec une allocation pré-existante (adresse statique dans le même sous-réseau voire le même pool).".format(ip, mac, name))
                else:
                    # add IP to pool
                    conf['dynamic'][current_subnet][current_pool[0]] = conf['dynamic'][current_subnet].get(current_pool[0], []) + [(name, mac)]
                    hosts.setdefault(mac, {'name': name, 'ip': [], 'tree': {}})
                    hosts[mac]['tree'].setdefault(current_subnet, {})
                    hosts[mac]['ip'].append(IP(ip))
                    hosts[mac]['tree'][current_subnet][current_pool[0]] = IP(ip)
    clean_ead_file(hosts, ead_config=ead_config)
    return conf


def gen_dhcp_config(hosts_configuration, dico=DICO, dir_name=DHCP_DIR):
    """
    Génération des fichiers de configuration (hosts et class)
    :param hosts_configuration: structure contenant l’information sur les allocations
                                d’adresses IP
    :type hosts_configuration: `dict` {'static': [(`str` name, `str` ip, `str` mac),],
                                       'dynamic': {`IPSet` subnet: {`str` range: [(`str` name, `str` mac,),]}, }}
    """
    # création du fichier d’hôtes rassemblant ceux avec les IP statiques et les
    # autres si ils ne figurent pas déjà dans la précédente liste
    with open(build_file_name('fixed-addresses', dico=dico, dir_name=dir_name), 'w') as fixed_address_file:
        hosts = {}
        for static_alloc in hosts_configuration['static']:
            hosts.setdefault((static_alloc[0], static_alloc[2]), [])
            hosts[(static_alloc[0], static_alloc[2])].append(static_alloc[1])
        for host, ips in hosts.items():
            fixed_addresses = ', '.join([ip for ip in ips])
            host = STATIC_IP_HOSTS_TMPL %(host[0], host[1], fixed_addresses)
            fixed_address_file.write(host)
        for pool, host_list in [p for pools in hosts_configuration['dynamic'].values()
                                for p in pools.items()]:
            for host in host_list:
                if host[0] not in [h[0] for h in hosts]:
                    host = DYNAMIC_IP_HOSTS_TMPL %(host[0], host[1])
                    fixed_address_file.write(host)

    # création des fichiers de classes pour la restriction des pools
    for pool, macs in [p for subnet in hosts_configuration['dynamic'].values()
                       for p in subnet.items()]:
        if dir_name != DHCP_DIR:
            sub_dir_name = dir_name
        else:
            sub_dir_name = DHCP_CLASSES_DIR
        with open(build_file_name(pool, dir_name=sub_dir_name, dico=dico), 'w') as pool_file:
            class_name = "pool-{}".format(pool)
            subclasses = [DHCP_SUBCLASSES_TMPL.format(class_name, client[1]) for client in macs]
            pool_file.write(DHCP_CLASS_TMPL.format(class_name))
            pool_file.write(''.join(subclasses))



if __name__ == '__main__':
    clean()
    hosts_configuration = parse_ead_config()
    pools_configuration = clean_hosts_configuration(hosts_configuration)
    gen_dhcp_config(pools_configuration)
    if len(sys.argv) == 1:
        # si aucun argument, on redémarre le service
        ret = manage_services('restart', 'isc-dhcp-server', 'dhcp')
        if ret != 0:
            print("Une erreur s'est produite au redémarrage du service dhcp")
        exit(ret)
