# This file is part of OpenStack Ansible driver for Kostyor.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# I feel incredibly wrong about these lines but it seems like the only
# working solution right now. Celery uses its own fork of native
# multiprocessing module, which is significantly diverged from the
# version of Python 2.7. So when it come to start 'multiprocessing.Process'
# instance from within Celery task, it simply fails due to inability to
# retrieve some properties (e.g. _authkey) from '_current_process' since
# they simply don't exist in 'billiard.Process.
#
# This is essential part of this driver, since Ansible internally use
# multiprocessing.Process to do parallel execution.
#
# https://github.com/celery/billiard/pull/202
import multiprocessing
import billiard
multiprocessing.Process = billiard.Process  # noqa

import os
import glob

from ansible.cli.playbook import PlaybookCLI
from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.inventory import Inventory
from ansible.parsing.dataloader import DataLoader
from ansible.vars import VariableManager
from ansible.utils.vars import combine_vars

from kostyor.rpc.app import app

from . import base


class _setcwd(object):
    """Context manager for temporally changing current working directory.

    Some of OpenStack Ansible playbooks require to be called from some
    directory. Since Ansible doesn't support passing custom working
    directory, we need to change current working directory before calling
    this sort of playbooks.

    Usage example:

        with _setcwd('/opt/openstack-ansible/playbooks'):
            _run_playbook(...)

    :param cwd: current working directory to be set
    :type cwd: str
    """

    def __init__(self, cwd):
        self._newcwd = cwd
        self._oldcwd = None

    def __enter__(self):
        self._oldcwd = os.getcwd()

        if self._newcwd:
            os.chdir(self._newcwd)

    def __exit__(self, *args):
        if self._newcwd:
            os.chdir(self._oldcwd)


def _get_user_settings(loader):
    """Read user settings from /etc/openstack_deploy.

    OpenStack Ansible user settings are stored in /etc/openstack_deploy
    directory. We need to read, combine and pass them to variable
    manager before executing any playbook. This is what heppened under
    the hood when one calls 'openstack-ansible' wrapper in command line.

    :param loader: an instance of ansible data loader to be used
    :type loader: :class:`ansible.parsing.dataloader.DataLoader`
    """
    settings = {}

    # /etc/openstack_deploy is default and, by all means, hardcoded path
    # to deployment settings. The dir contains user settings, where each
    # file starts with 'user_' prefix and ends with '.yml' suffix.
    pattern = os.path.join('/etc', 'openstack_deploy', 'user_*.yml')

    for filename in glob.glob(pattern):
        # Ansible may use different strategies of combining variables, so
        # we need to use its function instead of '.update(...)' method.
        settings = combine_vars(settings, loader.load_from_file(filename))

    return settings


def _run_playbook_impl(playbook, hosts_fn=None, cwd=None, ignore_errors=False):
    # Unfortunately, there's no good way to get the options instance
    # with proper defaults since it's generated by argparse inside
    # PlaybookCLI. Due to the fact that the options can't be empty
    # and must contain proper values we have not choice but extract
    # them from PlaybookCLI instance.
    playbook_cli = PlaybookCLI(['to-be-stripped', playbook])
    playbook_cli.parse()
    options = playbook_cli.options

    # Get others required options.
    loader = DataLoader()
    variable_manager = VariableManager()
    inventory = Inventory(loader, variable_manager)
    variable_manager.set_inventory(inventory)
    variable_manager.extra_vars = _get_user_settings(loader)

    # Limit playbook execution to hosts returned by 'hosts_fn'.
    if hosts_fn is not None:
        inventory.subset([
            host.get_vars()['inventory_hostname']
            for host in hosts_fn(inventory)
        ])

    # Finally, we can create a playbook executor and run the playbook.
    executor = PlaybookExecutor(
        playbooks=[playbook],
        inventory=inventory,
        variable_manager=variable_manager,
        loader=loader,
        options=options,
        passwords={}
    )

    # Some playbooks may rely on current working directory, so better allow
    # to change it before execution.
    with _setcwd(cwd):
        exitcode = executor.run()

    # Celery treats exceptions from task as way to mark it failed. So let's
    # throw one to do so in case return code is not zero.
    if all([not ignore_errors, exitcode is not None, exitcode != 0]):
        raise Exception('Playbook "%s" has been finished with errors. '
                        'Exit code is "%d".' % (playbook, exitcode))

    return exitcode


@app.task
def _run_playbook(playbook, cwd=None, ignore_errors=False):
    return _run_playbook_impl(
        playbook,
        cwd=cwd,
        ignore_errors=ignore_errors
    )


@app.task
def _run_playbook_for(playbook, hosts, service, cwd=None, ignore_errors=False):
    return _run_playbook_impl(
        playbook,
        lambda inv: base.get_component_hosts_on_nodes(inv, service, hosts),
        cwd=cwd,
        ignore_errors=ignore_errors
    )


class Driver(base.Driver):

    
_run_playbook = _run_playbook
    _run_playbook_for = _run_playbook_for