# Copyright (c) 2017 StackHPC Ltd.
#
# 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.

import logging
import os
import os.path
import subprocess
import sys

from kayobe import utils
from kayobe import vault


DEFAULT_CONFIG_PATH = "/etc/kolla"

CONFIG_PATH_ENV = "KOLLA_CONFIG_PATH"

DEFAULT_VENV_PATH = "venvs/kolla-ansible"

VENV_PATH_ENV = "KOLLA_VENV_PATH"

LOG = logging.getLogger(__name__)


def add_args(parser):
    """Add arguments required for running Kolla Ansible to a parser."""
    # $KOLLA_CONFIG_PATH or /etc/kolla.
    default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)
    # $KOLLA_VENV_PATH or $PWD/venvs/kolla-ansible
    default_venv = os.getenv(VENV_PATH_ENV,
                             os.path.join(os.getcwd(), DEFAULT_VENV_PATH))
    parser.add_argument("--kolla-config-path", default=default_config_path,
                        help="path to Kolla configuration. "
                             "(default=$%s or %s)" %
                             (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH))
    parser.add_argument("-ke", "--kolla-extra-vars", metavar="EXTRA_VARS",
                        action="append",
                        help="set additional variables as key=value or "
                             "YAML/JSON for Kolla Ansible")
    parser.add_argument("-ki", "--kolla-inventory", metavar="INVENTORY",
                        help="specify inventory host path "
                             "(default=$%s/inventory or %s/inventory) for "
                             "Kolla Ansible" %
                             (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH),
                        action='append')
    parser.add_argument("-kl", "--kolla-limit", metavar="SUBSET",
                        help="further limit selected hosts to an additional "
                             "pattern")
    parser.add_argument("-kp", "--kolla-playbook", metavar="PLAYBOOK",
                        help="path to Ansible playbook file")
    parser.add_argument("--kolla-skip-tags", metavar="TAGS",
                        help="only run plays and tasks whose tags do not "
                             "match these values in Kolla Ansible")
    parser.add_argument("-kt", "--kolla-tags", metavar="TAGS",
                        help="only run plays and tasks tagged with these "
                             "values in Kolla Ansible")
    parser.add_argument("--kolla-venv", metavar="VENV", default=default_venv,
                        help="path to virtualenv where Kolla Ansible is "
                             "installed (default=$%s or $PWD/%s)" %
                             (VENV_PATH_ENV, DEFAULT_VENV_PATH))


def _get_inventory_paths(parsed_args, inventory_filename):
    """Return the path to the Kolla inventory."""
    if parsed_args.kolla_inventory:
        return parsed_args.kolla_inventory
    else:
        paths = [os.path.join(parsed_args.kolla_config_path, "inventory",
                              inventory_filename)]
        # Inventory in the base layer is placed in the "kayobe"
        # directory. This means that you can't have an environment
        # called kayobe as it would conflict.
        environments = ["kayobe"]
        if parsed_args.environment:
            environments.append(parsed_args.environment)
        else:
            environment_finder = utils.EnvironmentFinder(
                parsed_args.config_path, parsed_args.environment)
            for environment in environment_finder.ordered():
                environments.append(environment)
        for environment in environments:
            candidate_path = os.path.join(
                parsed_args.kolla_config_path, "extra-inventories",
                environment)
            if utils.is_readable_dir(candidate_path)["result"]:
                paths.append(candidate_path)
        return paths


def _validate_args(parsed_args, inventory_filename):
    """Validate Kayobe Ansible arguments."""
    vault.enforce_single_password_source(parsed_args)
    result = utils.is_readable_dir(parsed_args.kolla_config_path)
    if not result["result"]:
        LOG.error("Kolla configuration path %s is invalid: %s",
                  parsed_args.kolla_config_path, result["message"])
        sys.exit(1)

    inventories = _get_inventory_paths(parsed_args, inventory_filename)
    for inventory in inventories:
        result = utils.is_readable_dir(inventory)
        if not result["result"]:
            # NOTE(mgoddard): Previously the inventory was a file, now it is a
            # directory to allow us to support inventory host_vars. Support
            # both formats for now.
            result_f = utils.is_readable_file(inventory)
            if not result_f["result"]:
                LOG.error("Kolla inventory %s is invalid: %s",
                          inventory, result["message"])
                sys.exit(1)

    result = utils.is_readable_dir(parsed_args.kolla_venv)
    if not result["result"]:
        LOG.error("Kolla virtualenv %s is invalid: %s",
                  parsed_args.kolla_venv, result["message"])
        sys.exit(1)

    if parsed_args.kolla_playbook:
        result = utils.is_readable_file(parsed_args.kolla_playbook)
        if not result["result"]:
            LOG.error("Kolla Ansible playbook %s is invalid: %s",
                      parsed_args.kolla_playbook, result["message"])
            sys.exit(1)


def build_args(parsed_args, command, inventory_filename, extra_vars=None,
               tags=None, verbose_level=None, extra_args=None, limit=None):
    """Build arguments required for running Kolla Ansible."""
    venv_activate = os.path.join(parsed_args.kolla_venv, "bin", "activate")
    cmd = [".", venv_activate, "&&"]
    cmd += ["kolla-ansible", command]
    if verbose_level:
        cmd += ["-" + "v" * verbose_level]
    if parsed_args.kolla_playbook:
        cmd += ["--playbook", parsed_args.kolla_playbook]
    cmd += vault.build_args(parsed_args, "--key")
    inventories = _get_inventory_paths(parsed_args, inventory_filename)
    for inventory in inventories:
        cmd += ["--inventory", inventory]
    if parsed_args.kolla_config_path != DEFAULT_CONFIG_PATH:
        cmd += ["--configdir", parsed_args.kolla_config_path]
        cmd += ["--passwords",
                os.path.join(parsed_args.kolla_config_path, "passwords.yml")]
    if parsed_args.kolla_extra_vars:
        for extra_var in parsed_args.kolla_extra_vars:
            # Don't quote or escape variables passed via the kayobe -e CLI
            # argument, to match Ansible's behaviour.
            cmd += ["-e", extra_var]
    if extra_vars:
        for extra_var_name, extra_var_value in extra_vars.items():
            # Quote and escape variables originating within the python CLI.
            extra_var_value = utils.quote_and_escape(extra_var_value)
            cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)]
    if parsed_args.kolla_limit or limit:
        limit_arg = utils.intersect_limits(parsed_args.kolla_limit, limit)
        cmd += ["--limit", utils.quote_and_escape(limit_arg)]
    if parsed_args.kolla_skip_tags:
        cmd += ["--skip-tags", parsed_args.kolla_skip_tags]
    if parsed_args.kolla_tags or tags:
        all_tags = [t for t in [parsed_args.kolla_tags, tags] if t]
        cmd += ["--tags", ",".join(all_tags)]
    if extra_args:
        cmd += extra_args
    return cmd


def _get_environment(parsed_args):
    """Return an environment dict for executing Kolla Ansible."""
    env = os.environ.copy()
    vault.update_environment(parsed_args, env)
    # If a custom Ansible configuration file exists, use it. Allow
    # etc/kayobe/kolla/ansible.cfg or etc/kayobe/ansible.cfg.
    ansible_cfg_path = os.path.join(parsed_args.config_path, "kolla",
                                    "ansible.cfg")
    if utils.is_readable_file(ansible_cfg_path)["result"]:
        env.setdefault("ANSIBLE_CONFIG", ansible_cfg_path)
    else:
        ansible_cfg_path = os.path.join(parsed_args.config_path, "ansible.cfg")
        if utils.is_readable_file(ansible_cfg_path)["result"]:
            env.setdefault("ANSIBLE_CONFIG", ansible_cfg_path)
    # kolla-ansible allows passing additional arguments to ansible-playbook via
    # EXTRA_OPTS.
    if parsed_args.check or parsed_args.diff:
        extra_opts = env.setdefault("EXTRA_OPTS", "")
        if parsed_args.check and "--check" not in extra_opts:
            env["EXTRA_OPTS"] += " --check"
        if parsed_args.diff and "--diff" not in extra_opts:
            env["EXTRA_OPTS"] += " --diff"
    return env


def run(parsed_args, command, inventory_filename, extra_vars=None,
        tags=None, quiet=False, verbose_level=None, extra_args=None,
        limit=None):
    """Run a Kolla Ansible command."""
    _validate_args(parsed_args, inventory_filename)
    cmd = build_args(parsed_args, command,
                     inventory_filename=inventory_filename,
                     extra_vars=extra_vars, tags=tags,
                     verbose_level=verbose_level,
                     extra_args=extra_args,
                     limit=limit)
    env = _get_environment(parsed_args)
    try:
        utils.run_command(" ".join(cmd), quiet=quiet, shell=True, env=env)
    except subprocess.CalledProcessError as e:
        LOG.error("kolla-ansible %s exited %d", command, e.returncode)
        sys.exit(e.returncode)


def run_seed(*args, **kwargs):
    """Run a Kolla Ansible command using the seed inventory."""
    return run(*args, inventory_filename="seed", **kwargs)


def run_overcloud(*args, **kwargs):
    """Run a Kolla Ansible command using the overcloud inventory."""
    return run(*args, inventory_filename="overcloud", **kwargs)
