#!/usr/bin/python3
from os import readlink, walk, chdir, getcwd, makedirs
from os.path import join, islink, isdir
from typing import Dict, Any
from shutil import rmtree, copy2
import tarfile
from ansible.module_utils._text import to_text
from ansible import constants

from rougail.template import base
from rougail.error import TemplateError
from risotto.machine import build_files, INSTALL_DIR, INSTALL_CONFIG_DIR, INSTALL_TMPL_DIR, INSTALL_IMAGES_DIR, INSTALL_TESTS_DIR
from risotto.utils import custom_filters


try:
    from ansible.plugins.action import ActionBase
    from ansible.module_utils.basic import AnsibleModule
    class FakeModule(AnsibleModule):
        def __init__(self):
            pass
    from ansible.plugins.action.template import ActionModule as TmplActionModule
except:
    class ActionBase():
        def __init__(self, *args, **kwargs):
            raise Exception('works only with ansible')


ARCHIVES_DIR = '/tmp/new_configurations'


def is_diff(server_name,
            remote_directories,
            certificates,
            ):
    ret = {}
    module = FakeModule()
    current_path = getcwd()
    root = join(INSTALL_DIR, INSTALL_CONFIG_DIR, server_name)
    chdir(root)
    search_paths = [join(directory[2:], f) for directory, subdirectories, files in walk('.') for f in files]
    chdir(current_path)
    for path in search_paths:
        if path not in remote_directories:
            return True
        full_path = join(root, path)
        if not islink(full_path):
            if remote_directories[path] != module.digest_from_file(full_path, 'sha256'):
                return True
        elif remote_directories[path] != readlink(full_path):
            return True
        remote_directories.pop(path)
    if remote_directories:
        for certificate in certificates:
            for typ in ['name', 'private', 'authority']:
                if not typ in certificate:
                    continue
                name = certificate[typ][1:]
                if name in remote_directories:
                    remote_directories.pop(name)
        if remote_directories:
            return True
    return False


class ActionModule(ActionBase):
    def run(self, tmp=None, task_vars=None):
        super(ActionModule, self).run(tmp, task_vars)
        module_args = self._task.args.copy()
        hostname = module_args.pop('hostname')
        only_machine = module_args.pop('only_machine')
        configure_host = module_args.pop('configure_host')
        copy_tests = module_args.pop('copy_tests')
        # define ansible engine
        base.ENGINES['ansible'] = Tmpl(task_vars,
                                       self._task,
                                       self._connection,
                                       self._play_context,
                                       self._loader,
                                       self._templar,
                                       self._shared_loader_obj,
                                       )
        if 'copy_templates' in module_args:
            copy_templates = module_args.pop('copy_templates')
        else:
            copy_templates = False
        directories, certificates = build_files(hostname,
                                                only_machine,
                                                False,
                                                copy_tests,
                                                )
        module_args['directories'] = list(directories.values())
        module_args['directories'].append('/var/lib/risotto/images_files')
        remote = self._execute_module(module_name='compare',
                                      module_args=module_args,
                                      task_vars=task_vars,
                                      )
        if remote.get('failed'):
            if 'module_stdout' in remote:
                msg = remote['module_stdout']
            else:
                msg = remote['msg']
            raise Exception(f'error in remote action: {msg}')
        if copy_templates:
            build_files(hostname,
                        only_machine,
                        True,
                        copy_tests,
                        )

        machines_changed = []
        for machine, directory in directories.items():
            if directory not in remote['directories']:
                machines_changed.append(machine)
                continue
            if is_diff(machine,
                       remote['directories'][directory],
                       certificates['certificates'].get(machine, []),
                       ):
                machines_changed.append(machine)
        current_path = getcwd()
        if isdir(ARCHIVES_DIR):
            rmtree(ARCHIVES_DIR)
        makedirs(ARCHIVES_DIR)
        if machines_changed:
            self._execute_module(module_name='file',
                                 module_args={'path': ARCHIVES_DIR,
                                              'state': 'absent',
                                              },
                                 task_vars=task_vars,
                                 )
            self._execute_module(module_name='file',
                                 module_args={'path': ARCHIVES_DIR,
                                              'state': 'directory',
                                              },
                                 task_vars=task_vars,
                                 )
            machines = machines_changed.copy()
            if self._task.args['hostname'] in machines_changed:
                machine = self._task.args['hostname']
                machines.remove(machine)
                chdir(f'{task_vars["host_install_dir"]}/{INSTALL_CONFIG_DIR}/{machine}')
                tar_filename = f'{ARCHIVES_DIR}/host.tar'
                with tarfile.open(tar_filename, 'w') as archive:
                    archive.add('.')
                chdir(current_path)
                self._transfer_file(tar_filename, tar_filename)

            # archive and send
            if machines:
                chdir(f'{task_vars["host_install_dir"]}/{INSTALL_CONFIG_DIR}')
                tar_filename = f'{ARCHIVES_DIR}/machines.tar'
                with tarfile.open(tar_filename, 'w') as archive:
                    for machine in machines:
                        if machine == self._task.args['hostname']:
                            continue
                        archive.add(f'{machine}')
                self._transfer_file(tar_filename, tar_filename)
        else:
            machines = []
        # archive and send
        chdir(f'{task_vars["host_install_dir"]}/{INSTALL_IMAGES_DIR}/')
        tar_filename = f'{ARCHIVES_DIR}/{INSTALL_IMAGES_DIR}.tar'
        with tarfile.open(tar_filename, 'w') as archive:
            archive.add('.')
        self._execute_module(module_name='file',
                             module_args={'path': '/tmp/new_configurations',
                                          'state': 'directory',
                                          },
                             task_vars=task_vars,
                             )
        self._transfer_file(tar_filename, tar_filename)
        # tests
        self._execute_module(module_name='file',
                             module_args={'path': '/var/lib/risotto/tests',
                                          'state': 'absent',
                                          },
                             task_vars=task_vars,
                             )
        if copy_tests:
            chdir(f'{task_vars["host_install_dir"]}/{INSTALL_TESTS_DIR}/')
            tar_filename = f'{ARCHIVES_DIR}/{INSTALL_TESTS_DIR}.tar'
            with tarfile.open(tar_filename, 'w') as archive:
                archive.add('.')
            self._transfer_file(tar_filename, tar_filename)
        # templates
        self._execute_module(module_name='file',
                             module_args={'path': '/var/lib/risotto/templates',
                                          'state': 'absent',
                                          },
                             task_vars=task_vars,
                             )
        if copy_templates:
            chdir(f'{task_vars["host_install_dir"]}/')
            tar_filename = f'{ARCHIVES_DIR}/{INSTALL_TMPL_DIR}.tar'
            with tarfile.open(tar_filename, 'w') as archive:
                archive.add(INSTALL_TMPL_DIR)
            self._transfer_file(tar_filename, tar_filename)
            remote = self._execute_module(module_name='unarchive',
                                          module_args={'remote_src': True,
                                                       'src': '/tmp/new_configurations/templates.tar',
                                                       'dest': '/var/lib/risotto',
                                                       },
                                          task_vars=task_vars,
                                          )
        chdir(current_path)
        changed = machines_changed != []
        return dict(ansible_facts=dict({}),
                    changed=changed,
                    machines_changed=machines,
                    host_changed=self._task.args['hostname'] in machines_changed,
                    )


class FakeCopy:
    def __init__(self, task):
        self.task = task

    def run(self, *args, **kwargs):
        copy2(self.task.args['src'], self.task.args['dest'])
        return {}


class FakeGet:
    def __init__(self, klass):
        self.klass = klass

    def fake_get(self, action, *args, task, **kwargs):
        if action == 'ansible.legacy.copy':
            return FakeCopy(task)
        return self.klass.ori_get(action, *args, task=task, **kwargs)


class Tmpl(TmplActionModule):
    def __init__(self, task_vars, *args):
        super().__init__(*args)
        self.task_vars = task_vars

    def _early_needs_tmp_path(self):
        # do not create tmp remotely
        return False

    def process(self,
                filename: str,
                source: str,
                true_destfilename: str,
                destfilename: str,
                destdir: str,
                variable: Any,
                index: int,
                rougail_variables_dict: Dict,
                eosfunc: Dict,
                extra_variables: Any=None,
                ):
        if source is not None:  # pragma: no cover
            raise TemplateError(_('source is not supported for ansible'))
        task_vars = rougail_variables_dict | self.task_vars
        if variable is not None:
            task_vars['rougail_variable'] = variable
        if index is not None:
            task_vars['rougail_index'] = index
        if extra_variables:
            task_vars['extra_variables'] = extra_variables
        task_vars['rougail_filename'] = true_destfilename
        task_vars['rougail_destination_dir'] = destdir
        self._task.args['src'] = filename
        self._task.args['dest'] = destfilename
        # add custom filter
        custom_filters.update(eosfunc)
        # do not copy file in host but stay it locally
        self._shared_loader_obj.action_loader.ori_get = self._shared_loader_obj.action_loader.get
        self._shared_loader_obj.action_loader.get = FakeGet(self._shared_loader_obj.action_loader).fake_get
        # template
        ret = self.run(task_vars=task_vars)
        # restore get function
        self._shared_loader_obj.action_loader.get = self._shared_loader_obj.action_loader.ori_get
        # remove custom filter
        custom_filters.clear()
        if ret.get('failed'):
            raise TemplateError(f'error while templating "{filename}": {ret["msg"]}')