from typing import Any
from pytest import fixture, mark
from os import makedirs, listdir, link, environ
from os.path import isfile, isdir, join, abspath, basename
from yaml import load, SafeLoader
from json import dump
from shutil import rmtree
from traceback import print_exc

from tiramisu import Config
from tiramisu.error import PropertiesOptionError, LeadershipError
from rougail import RougailConvert, RougailSystemdTemplate, RougailConfig


FUNCTIONS = b"""from tiramisu import valid_network_netmask, valid_ip_netmask, valid_broadcast, valid_in_network, valid_not_equal as valid_differ, valid_not_equal, calc_value
# =============================================================
# fork of risotto-setting/src/risotto_setting/config/config.py
def get_password(**kwargs):
    return 'password'


def get_ip(**kwargs):
    return '1.1.1.1'


def get_chain(**kwargs):
    return 'chain'


def get_certificate(**kwargs):
    return 'certificate'


def get_private_key(**kwargs):
    return 'private_key'


def get_linked_configuration(**kwargs):
    if 'test' in kwargs and kwargs['test']:
        return kwargs['test'][0]
    if kwargs.get('multi', False) is True:
        return ['configuration']
    return 'configuration'


def get_certificates(**kwargs):
    return ["XXXX", "YYYY", "ZZZZ"]


def zone_information(*args, **kwargs):
    if args[1] == 'network':
        val = '192.168.0.0/24'
    else:
        val = '192.168.0.1'
    if kwargs.get('multi', False):
        val = [val]
    return val
# =============================================================

"""


def tiramisu_display_name(kls,
                          dyn_name: 'Base'=None,
                          suffix: str=None,
                          ) -> str:
    if dyn_name is not None:
        name = dyn_name
    else:
        name = kls.impl_getname()
    return name


GENERATED_DIR = 'generated'
if isdir(GENERATED_DIR):
    rmtree(GENERATED_DIR)


applications = {}


test_ok = []
for distrib in listdir('seed'):
    distrib_dir = join('seed', distrib, 'applicationservice')
    if isdir(distrib_dir):
        for release in listdir(distrib_dir):
            release_dir = join(distrib_dir, release)
            if isdir(release_dir):
                for applicationservice in listdir(release_dir):
                    applicationservice_dir = join(release_dir, applicationservice)
                    if isdir(applicationservice_dir):
                        if applicationservice in applications:
                            raise Exception('multi applicationservice')
                        applications[applicationservice] = applicationservice_dir
                        if isdir(join(applicationservice_dir, 'dictionaries')):
                            test_ok.append(applicationservice_dir)
if 'APPLICATIONSERVICE_DIR' in environ:
    test_ok = []
    if isdir(join(applicationservice_dir, 'dictionaries')):
        test_ok.append(applicationservice_dir)


print(test_ok)


@fixture(scope="module", params=test_ok)
def test_dir(request):
    return request.param


async def get_value(type: str,
                    properties: list,
                    test_values: Any,
                    multi: bool,
                    has_dependency: bool,
                    path,
                    min_number,
                    ) -> list:
    if test_values is not None:
        values = list(test_values)
        has_dependency = True
    elif type == 'hostname':
        values = ['test']
    elif type == 'domainname':
        values = ['test.test.com']
    elif type == 'filename':
        values = ['/a']
    elif type == 'ip':
        values = ['192.168.1.1']
    elif type == 'ip_cidr':
        values = ['192.168.1.1/24']
    elif type == 'network':
        values = ['192.168.1.0']
    elif type == 'network_cidr':
        values = ['192.168.1.0/24']
    elif type == 'netmask':
        values = ['255.255.255.0']
    elif type in ['string', 'password']:
        values = ['string']
    elif type == 'integer':
        if min_number is None:
            min_number = 0
        values = [min_number, min_number + 1]
    elif type in 'port':
        values = ["80"]
    elif type == 'boolean':
        values = [True, False]
    elif type == 'url':
        values = ['http://value']
    elif type == 'username':
        values = ['myname']
    elif type == 'email':
        values = ['a@a.com']
    elif type == 'date':
        values = ['2011-11-11']
    elif type == 'float':
        values = [1.1]
    else:
        raise Exception(f'Unknown type {type}')
    if not has_dependency:
        values = [values[0]]
    if multi:
        values = [values]
    if 'mandatory' not in properties:
        if not multi:
            values.insert(0, None)
        else:
            values.insert(0, [])
    return values


async def build_values(paths: list,
                       paths_values: dict,
                       ):
    if paths:
        path, option, values, _ = paths[0]
        for val in values:
            new_paths_values = paths_values.copy()
            new_paths_values[path] = val
            next_paths = paths[1:]
            if next_paths:
                async for value in build_values(next_paths,
                                                new_paths_values,
                                                ):
                    yield value
            else:
                yield new_paths_values


async def set_dep_paths(new_paths_no_deps, config, old_paths_values, new_paths_values, leader_followers):
    n = new_paths_no_deps.copy()
    for path, values in n.items():
        dico = await config.value.dict()
        if path in dico:
            value = new_paths_no_deps[path][0]
            await set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps, is_dep=True)


async def set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps, is_dep=False):
    setted = False
    if path not in old_paths_values or old_paths_values[path] != value:
        if isinstance(value, tuple):
            value = list(value)
        if not await config.option(path).option.isfollower():
            if await config.option(path).option.isleader():
                old_values = await config.unrestraint.option(path).value.get()
                if len(old_values) > len(value):
                    await config.unrestraint.option(path).value.reset()
                    leadership_path = path.rsplit('.', 1)[0]
                    if leadership_path in leader_followers:
                        for follower, values in leader_followers[leadership_path]:
                            new_paths_no_deps[follower] = values.copy()
            await config.unrestraint.option(path).value.set(value)
            setted = True
        else:
            length = await config.unrestraint.option(path).value.len()
            if length:
                setted = True
                await config.unrestraint.option(path, length-1).value.set(value)
    if setted and is_dep:
        new_paths_no_deps[path].pop(0)
        if not new_paths_no_deps[path]:
            del new_paths_no_deps[path]


async def get_values(config, new_paths, new_paths_no_deps, root_dir, leader_followers):
    old_paths_values = {}
    if not new_paths:
        yield await config.value.dict()
    async for new_paths_values in build_values(new_paths, {}):
        try:
            await config.property.read_write()
            copy_paths_values = new_paths_values.copy()
            for path, value in new_paths_values.items():
                await set_value(config, path, value, old_paths_values, new_paths_values, leader_followers, new_paths_no_deps)
            if new_paths_no_deps:
                await set_dep_paths(new_paths_no_deps, config, old_paths_values, new_paths_values, leader_followers)
            await config.property.read_only()
            dico = await config.value.dict()
            old_paths_values = dico
            yield dico
        except Exception as err:
            print_exc()
            print('Tiramisu file ' + join(root_dir, 'dictionary.py'))
            print(f'test with {new_paths_values}')
            raise Exception(f'error {err}')


def calc_depends(xmls, appname):
    application = join(applications[appname], 'applicationservice.yml')
    with open(application) as yaml:
        app = load(yaml, Loader=SafeLoader)

    if 'depends' in app and app['depends']:
        for xml in app['depends']:
            if xml not in xmls:
                xmls.insert(0, xml)
            calc_depends(xmls, xml)


def reorder_new_paths(paths, new_paths=None, not_added_paths=None, added_paths=None):
    root = new_paths is None
    if root:
        new_paths = []
        added_paths = []
    new_not_added_paths = set()
    has_old_path = False
    for path, option, values, option_dependencies in paths:
        if path not in added_paths:
            if not_added_paths is not None and not option_dependencies & not_added_paths:
                #all dependencies are already managed, so added this paths
                new_paths.append((path, option, values, option_dependencies))
                added_paths.append(path)
            else:
                has_old_path = True
                new_not_added_paths.add(path)
    if has_old_path:
        reorder_new_paths(paths, new_paths, new_not_added_paths, added_paths)
    if root:
        return new_paths


async def populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config):
    for path in await unrestraint.value.dict():
        if path in paths_done:
            continue
        properties = await config.option(path).property.get(only_raises=True,
                                                            uncalculated=True,
                                                            )
        if properties:
            # unmodified path
            continue
        for unaccessible_path in unaccessible_paths:
            if path.startswith(unaccessible_path + '.'):
                break
        else:
            # this variable is not un unaccessible path
            option = unrestraint.option(path)
            current_dependencies = await option.option.dependencies()
            test_values = await option.information.get('test', None)
            if await option.option.isfollower():
                ismulti = await option.option.issubmulti()
            else:
                ismulti = await option.option.ismulti()
            min_number = None
            type = await option.option.type()
            if test_values is None:
                if type == 'choice':
                    test_values = await option.value.list()
                    if not test_values:
                        test_values = [None]
                elif type == 'integer':
                    real_option = await option.option.get()
                    min_number = real_option.impl_get_extra('min_number')
                elif type == 'domainname':
                    real_option = await option.option.get()
                    if real_option.impl_get_extra('_dom_type') in ['netbios', 'hostname']:
                        type = 'hostname'
                elif type == 'ip':
                    true_option = await option.option.get()
                    if true_option.impl_get_extra('_cidr'):
                        type = 'ip_cidr'
                elif type == 'network':
                    true_option = await option.option.get()
                    if true_option.impl_get_extra('_cidr'):
                        type = 'network_cidr'
            else:
                test_values = tuple(test_values)
            full_properties = await option.property.get()
            for dependency in current_dependencies:
                dependencies.add(await dependency.option.path())
            paths.append((path,
                          full_properties,
                          test_values,
                          ismulti,
                          type,
                          option,
                          min_number,
                          ))
            paths_done.append(path)


@mark.asyncio
async def test_template(test_dir):
    if test_dir.startswith('seed/eole'):
        return
#    if test_dir != 'seed/eole/applicationservice/2020.1.1/eole-lemonldap-ng':
#        return
#    if test_dir in [
#                    'seed/eole/applicationservice/2020.1.1/eole-proxy',
#                    'seed/eole/applicationservice/2020.1.1/eole-bareos',
#                    ]:
#        return
    if not isdir(GENERATED_DIR):
        makedirs(GENERATED_DIR)
    test_name = basename(test_dir)
    root_dir = f'{GENERATED_DIR}/{test_name}'
    tmp_dir = f'{root_dir}/tmp'
    funcs_dir = f'{root_dir}/funcs'
    for dirname in [root_dir, tmp_dir, funcs_dir]:
        if isdir(dirname):
            rmtree(dirname)
        makedirs(dirname)
    print()
    print('>', test_dir)
    test_file = test_dir.rsplit('/', 1)[-1]
    xmls = [test_file]
    calc_depends(xmls, test_file)
    print('  o dependencies: ' + ', '.join(xmls))
    with open(f'{root_dir}/dependencies.txt', 'w') as fh:
        dump(xmls, fh)
    dictionaries = [join(applications[xml], 'dictionaries') for xml in xmls if isdir(join(applications[xml], 'dictionaries'))]
    if 'risotto_setting.rougail' not in RougailConfig['extra_annotators']:
        RougailConfig['extra_annotators'].append('risotto_setting.rougail')
    RougailConfig['dictionaries_dir'] = dictionaries
    print('  o dictionaries: ' + ', '.join(dictionaries))
    RougailConfig['extra_dictionaries'] = {}
    extra_dictionaries = []
    for xml in xmls:
        extras_dir = join(applications[xml], 'extras')
        if isdir(extras_dir):
            for extra in listdir(extras_dir):
                extra_dir = join(extras_dir, extra)
                if isdir(extra_dir):
                    RougailConfig['extra_dictionaries'].setdefault(extra, []).append(extra_dir)
                    extra_dictionaries.append(extra_dir)
    if RougailConfig['extra_dictionaries']:
        print('  o extra dictionaries: ' + ', '.join(extra_dictionaries))
    if isdir('templates'):
        rmtree('templates')
    makedirs('templates')
    func = f'{funcs_dir}/funcs.py'
    with open(func, 'wb') as fh:
        fh.write(FUNCTIONS)
        for xml in xmls:
            func_dir = join(applications[xml], 'funcs')
            if isdir(func_dir):
                for f in listdir(func_dir):
                    if not f.startswith('__'):
                        with open(join(func_dir, f), 'rb') as rfh:
                            fh.write(rfh.read())
            templates_dir = join(applications[xml], 'templates')
            if isdir(templates_dir):
                for f in listdir(templates_dir):
                    template_file = join(templates_dir, f)
                    dest_template_file = join('templates', f)
                    if isfile(dest_template_file):
                        raise Exception(f'duplicate template {f}')
                    link(template_file, dest_template_file)

    RougailConfig['functions_file'] = func
    eolobj = RougailConvert()
    xml = eolobj.save(join(root_dir, 'dictionary.py'))
    optiondescription = {}
    try:
        exec(xml, None, optiondescription)
    except Exception as err:
        print('Tiramisu file ' + join(root_dir, 'dictionary.py'))
        print_exc()
        raise Exception(f'unknown error when load tiramisu object {err}') from err
    config = await Config(optiondescription['option_0'], display_name=tiramisu_display_name)
    has_services = False
    for option in await config.option.list('optiondescription'):
        if await option.option.name() == 'services':
            has_services = True
            break
    if not has_services:
        print(f'  o number of tests: any template')
        rmtree(root_dir)
        return
    await config.property.read_write()
    paths = []
    unrestraint = config.unrestraint
    # this paths in unaccessible, so variable inside are unaccessible too
    unaccessible_paths = []
    dependencies = set()
    for option in await unrestraint.option.list('optiondescription',
                                                recursive=True,
                                                ):
        path = await option.option.path()
        properties = await config.option(path).property.get(only_raises=True,
                                                            uncalculated=True,
                                                            )
        if properties:
            unaccessible_paths.append(path)
        for dependency in await option.option.dependencies():
            dependencies.add(await dependency.option.path())
    paths_done = []
    await populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config)

    all_values = {}
    for path, full_properties, test_values, ismulti, type, option, min_number in paths:
        values = await get_value(type,
                                 full_properties,
                                 test_values,
                                 ismulti,
                                 path in dependencies,
                                 path,
                                 min_number,
                                 )
        all_values[path] = values
        if await option.option.isfollower() or len(values) > 1:
            continue
        if await config.unrestraint.option(path).option.isleader():
            old_values = await config.unrestraint.option(path).value.get()
            for idx in reversed(range(len(old_values))):
                await config.unrestraint.option(path).value.pop(idx)
        await config.unrestraint.option(path).value.set(values[0])
    await populate_paths(paths_done, paths, dependencies, unrestraint, unaccessible_paths, config)
    new_paths = []
    number_test = 1
    nb_var = 0
    for path, full_properties, test_values, ismulti, type, option, min_number in paths:
        if path in all_values:
            values = all_values[path]
            has_values = True
        else:
            values = await get_value(type,
                                     full_properties,
                                     test_values,
                                     ismulti,
                                     path in dependencies,
                                     path,
                                     min_number,
                                     )
            has_values = False
        number_test *= len(values)
        if await option.option.isfollower() or len(values) > 1:
            option_dependencies = {await subopt.option.path() for subopt in await option.option.dependencies()}
            new_paths.append((path, option, values, option_dependencies))
            nb_var += 1
        elif not has_values:
            if await config.unrestraint.option(path).option.isleader():
                old_values = await config.unrestraint.option(path).value.get()
                for idx in reversed(range(len(old_values))):
                    await config.unrestraint.option(path).value.pop(idx)
            await config.unrestraint.option(path).value.set(values[0])
    print('  o number of variables: {}'.format(len(paths)))
    new_paths = reorder_new_paths(new_paths)
    print(f'  o number of tested variables: {nb_var}')
    print(f'  o estimate number of tests: {number_test}')
    new_paths_no_deps = {}
    leader_followers = {}
    if number_test > 1001:
        number_test = 1
        nb2 = 0
        print(f'      degraded mode...')
        new_paths_2 = []
        for path, option, values, option_dependencies in new_paths:
            isfollower = await option.option.isfollower()
            if isfollower:
                has_dependencies = len(option_dependencies) > 1
            else:
                has_dependencies = len(option_dependencies) > 0
            if not has_dependencies:
                if isfollower:
                    leadership_path = path.rsplit('.', 1)[0]
                    leader_followers.setdefault(leadership_path, []).append((path, values.copy()))
                new_paths_no_deps[path] = values
                nb2 += len(values) - 1
            else:
                number_test *= len(values)
                new_paths_2.append((path, option, values, option_dependencies))
        new_paths = new_paths_2
        print('      > degraded variables: {}'.format(len(new_paths_no_deps)))
        print(f'      > new estimated number of tests: {number_test + nb2}')
    idx = 0
    real_idx = 0
    old_dico = {}
    async for dico in get_values(config, new_paths, new_paths_no_deps, root_dir, leader_followers):
        try:
            real_idx += 1
            if str(real_idx).endswith('0'):
                print(f'   ... {real_idx}')
            build_dir = f'{root_dir}/{idx}'
            dest_dir = f'{build_dir}/dest'
            mandatories = await config.value.mandatory()
            if old_dico == dico:
                continue
            for dirname in [build_dir, dest_dir]:
                makedirs(dirname)
            old_dico = dico
            if mandatories:
                print('Tiramisu file ' + join(root_dir, 'dictionary.py'))
                raise Exception(f'Mandatory option not configured {mandatories}')
            RougailConfig['functions_file'] = func
            RougailConfig['tmp_dir'] = tmp_dir
            RougailConfig['templates_dir'] = 'templates'
            RougailConfig['destinations_dir'] = dest_dir
            await launch(config, root_dir, dico)
            await config.value.dict()
            with open(f'{build_dir}/variables.txt', 'w') as fh:
                dump(await config.value.dict(leader_to_list=True), fh, indent=4, sort_keys=True)
            idx += 1
        except Exception as err:
            print_exc()
            print('Tiramisu file ' + join(root_dir, 'dictionary.py'))
            print(f'test with {dico}')
            raise Exception(f'unknown error {err}') from err
    print(f'  o number of tests: {idx}')

    if isdir('templates'):
        rmtree('templates')
    rmtree(tmp_dir)
    rmtree(funcs_dir)


async def launch(config, root_dir, dico):
    try:
        await config.property.read_only()
        engine = RougailSystemdTemplate(config)
        await engine.instance_files()
    except Exception as err:
        print_exc()
        print('Tiramisu file ' + join(root_dir, 'dictionary.py'))
        print(f'test with {dico}')
        raise Exception(f'unknown error {err}') from err
