# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from datetime import datetime
import json
import logging
import multiprocessing
import os
from pathlib import Path
import shutil
import six
from six.moves import configparser
from six.moves import cStringIO as StringIO
import sys
import tempfile
import yaml

from oslo_concurrency import processutils

from tripleo_common import constants

LOG = logging.getLogger(__name__)


def write_default_ansible_cfg(work_dir,
                              remote_user,
                              ssh_private_key=None,
                              transport=None,
                              base_ansible_cfg='/etc/ansible/ansible.cfg',
                              override_ansible_cfg=None):
    ansible_config_path = os.path.join(work_dir, 'ansible.cfg')
    shutil.copy(base_ansible_cfg, ansible_config_path)

    modules_path = (
        '/root/.ansible/plugins/modules:'
        '/usr/share/ansible/tripleo-plugins/modules:'
        '/usr/share/ansible/plugins/modules:'
        '/usr/share/ansible-modules:'
        '{}/library:'
        '{}/library'.format(constants.DEFAULT_VALIDATIONS_BASEDIR,
                            constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR))
    lookups_path = (
        '/root/.ansible/plugins/lookup:'
        '/usr/share/ansible/tripleo-plugins/lookup:'
        '/usr/share/ansible/plugins/lookup:'
        '{}/lookup_plugins:'
        '{}/lookup_plugins'.format(
            constants.DEFAULT_VALIDATIONS_BASEDIR,
            constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR))
    callbacks_path = (
        '~/.ansible/plugins/callback:'
        '/usr/share/ansible/tripleo-plugins/callback:'
        '/usr/share/ansible/plugins/callback:'
        '{}/callback_plugins:'
        '{}/callback_plugins'.format(
            constants.DEFAULT_VALIDATIONS_BASEDIR,
            constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR))

    callbacks_whitelist = ','.join(['tripleo_dense', 'tripleo_profile_tasks',
                                    'tripleo_states'])
    action_plugins_path = (
        '~/.ansible/plugins/action:'
        '/usr/share/ansible/plugins/action:'
        '/usr/share/ansible/tripleo-plugins/action:'
        '{}/action_plugins:'
        '{}/action_plugins'.format(
            constants.DEFAULT_VALIDATIONS_BASEDIR,
            constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR))
    filter_plugins_path = (
        '~/.ansible/plugins/filter:'
        '/usr/share/ansible/plugins/filter:'
        '/usr/share/ansible/tripleo-plugins/filter:'
        '{}/filter_plugins:'
        '{}/filter_plugins'.format(
            constants.DEFAULT_VALIDATIONS_BASEDIR,
            constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR))
    roles_path = ('%(work_dir)s/roles:'
                  '/root/.ansible/roles:'
                  '/usr/share/ansible/tripleo-roles:'
                  '/usr/share/ansible/roles:'
                  '/etc/ansible/roles:'
                  '%(ooo_val_path)s/roles:'
                  '%(work_dir)s' % {
                      'work_dir': work_dir,
                      'ooo_val_path':
                          constants.DEFAULT_VALIDATIONS_LEGACY_BASEDIR
                  })

    config = configparser.ConfigParser()
    config.read(ansible_config_path)

    config.set('defaults', 'retry_files_enabled', 'False')
    config.set('defaults', 'roles_path', roles_path)
    config.set('defaults', 'library', modules_path)
    config.set('defaults', 'callback_plugins', callbacks_path)
    config.set('defaults', 'callback_whitelist', callbacks_whitelist)
    config.set('defaults', 'stdout_callback', 'tripleo_dense')
    config.set('defaults', 'action_plugins', action_plugins_path)
    config.set('defaults', 'lookup_plugins', lookups_path)
    config.set('defaults', 'filter_plugins', filter_plugins_path)

    log_path = os.path.join(work_dir, 'ansible.log')
    config.set('defaults', 'log_path', log_path)
    if os.path.exists(log_path):
        new_path = (log_path + '-' +
                    datetime.now().strftime("%Y-%m-%dT%H:%M:%S"))
        os.rename(log_path, new_path)

    # Create the log file, and set some rights on it in order to prevent
    # unwanted accesse
    Path(log_path).touch()
    os.chmod(log_path, 0o640)

    config.set('defaults', 'forks', str(min(
        multiprocessing.cpu_count() * 4, 100)))
    config.set('defaults', 'timeout', '30')
    config.set('defaults', 'gather_timeout', '30')

    # Setup fact cache to improve playbook execution speed
    config.set('defaults', 'gathering', 'smart')
    config.set('defaults', 'fact_caching', 'jsonfile')
    config.set('defaults', 'fact_caching_connection',
               '~/.ansible/fact_cache')
    # NOTE(mwhahaha): only gather the bare minimum facts because this has
    # direct impact on how fast ansible can go.
    config.set('defaults', 'gather_subset', '!all,min')
    # NOTE(mwhahaha): this significantly affects performation per ansible#73654
    config.set('defaults', 'inject_facts_as_vars', 'false')

    # Set the pull interval to lower CPU overhead
    config.set('defaults', 'internal_poll_interval', '0.01')

    # Set the interpreter discovery to auto mode.
    config.set('defaults', 'interpreter_python', 'auto')

    # Expire facts in the fact cache after 7200s (2h)
    config.set('defaults', 'fact_caching_timeout', '7200')

    # mistral user has no home dir set, so no place to save a known hosts file
    config.set('ssh_connection', 'ssh_args',
               '-o UserKnownHostsFile=/dev/null '
               '-o StrictHostKeyChecking=no '
               '-o ControlMaster=auto '
               '-o ControlPersist=30m '
               '-o ServerAliveInterval=5 '
               '-o ServerAliveCountMax=5 '
               '-o PreferredAuthentications=publickey')
    config.set('ssh_connection', 'control_path_dir',
               os.path.join(work_dir, 'ansible-ssh'))
    config.set('ssh_connection', 'retries', '8')
    config.set('ssh_connection', 'pipelining', 'True')
    # Related to https://github.com/ansible/ansible/issues/22127
    config.set('ssh_connection', 'scp_if_ssh', 'True')

    if override_ansible_cfg:
        sio_cfg = StringIO()
        sio_cfg.write(override_ansible_cfg)
        sio_cfg.seek(0)
        config.read_file(sio_cfg)
        sio_cfg.close()

    with open(ansible_config_path, 'w') as configfile:
        config.write(configfile)

    return ansible_config_path


def _get_inventory(inventory, work_dir):
    if not inventory:
        return None

    if (isinstance(inventory, six.string_types) and
            os.path.exists(inventory)):
        return inventory
    if not isinstance(inventory, six.string_types):
        inventory = yaml.safe_dump(inventory)

    path = os.path.join(work_dir, 'inventory.yaml')

    with open(path, 'w') as inv:
        inv.write(inventory)

    return path


def _get_ssh_private_key(ssh_private_key, work_dir):
    if not ssh_private_key:
        return None

    if (isinstance(ssh_private_key, six.string_types) and
            os.path.exists(ssh_private_key)):
        os.chmod(ssh_private_key, 0o600)
        return ssh_private_key

    path = os.path.join(work_dir, 'ssh_private_key')

    with open(path, 'w') as ssh_key:
        ssh_key.write(ssh_private_key)
    os.chmod(path, 0o600)

    return path


def _get_playbook(playbook, work_dir):
    if not playbook:
        return None

    if (isinstance(playbook, six.string_types) and
            os.path.exists(playbook)):
        return playbook
    if not isinstance(playbook, six.string_types):
        playbook = yaml.safe_dump(playbook)

    path = os.path.join(work_dir, 'playbook.yaml')

    with open(path, 'w') as pb:
        pb.write(playbook)

    return path


def run_ansible_playbook(playbook, work_dir=None, **kwargs):
    verbosity = kwargs.get('verbosity', 5)
    remove_work_dir = False
    if not work_dir:
        work_dir = tempfile.mkdtemp(prefix='tripleo-ansible')
        remove_work_dir = True

    playbook = _get_playbook(playbook, work_dir)
    python_version = sys.version_info.major
    ansible_playbook_cmd = "ansible-playbook-{}".format(python_version)
    if 1 < verbosity < 6:
        verbosity_option = '-' + ('v' * (verbosity - 1))
        command = [ansible_playbook_cmd, verbosity_option,
                   playbook]
    else:
        command = [ansible_playbook_cmd, playbook]

    limit_hosts = kwargs.get('limit_hosts', None)
    if limit_hosts:
        command.extend(['--limit', limit_hosts])

    module_path = kwargs.get('module_path', None)
    if module_path:
        command.extend(['--module-path', module_path])

    become = kwargs.get('become', False)
    if become:
        command.extend(['--become'])

    become_user = kwargs.get('become_user', None)
    if become_user:
        command.extend(['--become-user', become_user])

    extra_vars = kwargs.get('extra_vars', None)
    if extra_vars:
        extra_vars = json.dumps(extra_vars)
        command.extend(['--extra-vars', extra_vars])

    flush_cache = kwargs.get('flush_cache', False)
    if flush_cache:
        command.extend(['--flush-cache'])

    forks = kwargs.get('forks', None)
    if forks:
        command.extend(['--forks', forks])

    ssh_common_args = kwargs.get('ssh_common_args', None)
    if ssh_common_args:
        command.extend(['--ssh-common-args', ssh_common_args])

    ssh_extra_args = kwargs.get('ssh_extra_args', None)
    if ssh_extra_args:
        command.extend(['--ssh-extra-args', ssh_extra_args])

    timeout = kwargs.get('timeout', None)
    if timeout:
        command.extend(['--timeout', timeout])

    inventory = _get_inventory(kwargs.get('inventory', None),
                               work_dir)
    if inventory:
        command.extend(['--inventory-file', inventory])

    tags = kwargs.get('tags', None)
    if tags:
        command.extend(['--tags', tags])

    skip_tags = kwargs.get('skip_tags', None)
    if skip_tags:
        command.extend(['--skip-tags', skip_tags])

    extra_env_variables = kwargs.get('extra_env_variables', None)
    override_ansible_cfg = kwargs.get('override_ansible_cfg', None)
    remote_user = kwargs.get('remote_user', None)
    ssh_private_key = kwargs.get('ssh_private_key', None)

    if extra_env_variables:
        if not isinstance(extra_env_variables, dict):
            msg = "extra_env_variables must be a dict"
            raise RuntimeError(msg)
        for key, value in extra_env_variables.items():
            extra_env_variables[key] = six.text_type(value)

    try:
        ansible_config_path = write_default_ansible_cfg(
            work_dir,
            remote_user,
            ssh_private_key=_get_ssh_private_key(
                ssh_private_key, work_dir),
            override_ansible_cfg=override_ansible_cfg)
        env_variables = {
            'HOME': work_dir,
            'ANSIBLE_LOCAL_TEMP': work_dir,
            'ANSIBLE_CONFIG': ansible_config_path,
        }

        profile_tasks = kwargs.get('profile_tasks', True)
        if profile_tasks:
            profile_tasks_limit = kwargs.get('profile_tasks_limit', 20)
            env_variables.update({
                # the whitelist could be collected from multiple
                # arguments if we find a use case for it
                'ANSIBLE_CALLBACK_WHITELIST':
                    'tripleo_dense,tripleo_profile_tasks,tripleo_states',
                'ANSIBLE_STDOUT_CALLBACK': 'tripleo_dense',
                'PROFILE_TASKS_TASK_OUTPUT_LIMIT':
                    six.text_type(profile_tasks_limit),
            })

        if extra_env_variables:
            env_variables.update(extra_env_variables)

        command = [str(c) for c in command]

        reproduce_command = kwargs.get('reproduce_command', None)
        command_timeout = kwargs.get('command_timeout', None)
        trash_output = kwargs.get('trash_output', None)
        if reproduce_command:
            command_path = os.path.join(work_dir,
                                        "ansible-playbook-command.sh")
            with open(command_path, 'w') as f:
                f.write('#!/bin/bash\n')
                f.write('\n')
                for var in env_variables:
                    f.write('%s="%s"\n' % (var, env_variables[var]))
                f.write('\n')
                f.write(' '.join(command))
                f.write(' "$@"')
                f.write('\n')

            os.chmod(command_path, 0o750)

        if command_timeout:
            command = ['timeout', '-s', 'KILL',
                       str(command_timeout)] + command

        LOG.info('Running ansible-playbook command: %s', command)

        stderr, stdout = processutils.execute(
            *command, cwd=work_dir,
            env_variables=env_variables,
            log_errors=processutils.LogErrors.ALL)
        if trash_output:
            stdout = ""
            stderr = ""
        return {"stderr": stderr, "stdout": stdout,
                "log_path": os.path.join(work_dir, 'ansible.log')}
    finally:
        try:
            if remove_work_dir:
                shutil.rmtree(work_dir)
        except Exception as e:
            msg = "An error happened while cleaning work directory: " + e
            raise RuntimeError(msg)
