#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
debops: run ansible-playbook with some customization
"""
# Copyright (C) 2014-2015 Hartmut Goebel <h.goebel@crazy-compilers.com>
# Part of the DebOps - https://debops.org/

# 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,
# write to the Free Software Foundation, Inc., 59
# Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# An on-line copy of the GNU General Public License can
# be downloaded from the FSF web page at:
# https://www.gnu.org/copyleft/gpl.html

from __future__ import print_function

import sys
import os
import subprocess
try:
    import configparser
except ImportError:
    import ConfigParser as configparser
from distutils.version import LooseVersion as ver
from debops import ANSIBLE_CONFIG_FILE, ENCFS_PREFIX, SECRET_NAME
from debops import DEBOPS_PY_PACKAGE
from debops import read_config
from debops import padlock_lock, padlock_unlock
from debops.cmds import INSECURE
from debops.cmds import find_debops_project, require_commands
from debops.cmds import find_playbookpath, find_inventorypath
from debops.cmds import find_monorepopath

__author__ = "Hartmut Goebel <h.goebel@crazy-compilers.com>"
__copyright__ = "Copyright 2014-2015 by Hartmut Goebel  " \
                "<h.goebel@crazy-compilers.com>"
__licence__ = "GNU General Public License version 3 (GPL v3) or later"


PATHSEP = ':'

ConfigFileHeader = """\
# Ansible configuration file generated by DebOps, all changes will be lost.
# You can manipulate the contents of this file via `.debops.cfg`.
"""

# External programms used. List here for easy substitution for
# hard-coded paths.
ANSIBLE_PLAYBOOK = 'ansible-playbook'


def write_config(filename, config):
    cfgparser = configparser.ConfigParser()
    for section, pairs in config.items():
        cfgparser.add_section(section)
        for option, value in pairs.items():
            cfgparser.set(section, option, value)

    with open(filename, "w") as fh:
        print(ConfigFileHeader, file=fh)
        cfgparser.write(fh)


def gen_ansible_cfg(filename, config, project_root, playbooks_path,
                    monorepo_path, inventory_path):
    # Generate Ansible configuration file

    def custom_paths(type):
        if type in defaults:
            # prepend value from .debops.cfg
            yield defaults[type]
        yield os.path.join(project_root, "ansible", "playbooks", type)
        yield os.path.join(project_root, "ansible", "plugins", type)
        yield os.path.join(project_root, "ansible", type)
        yield os.path.join(project_root,
                           "debops", "ansible", "playbooks", "playbooks", type)
        yield os.path.join(project_root,
                           "debops", "ansible", "playbooks", type)
        yield os.path.join(project_root, "debops", "ansible", "plugins", type)
        yield os.path.join(project_root, "debops", "ansible", type)
        if monorepo_path:
            yield os.path.join(monorepo_path,
                               "ansible", "playbooks", "playbooks", type)
            yield os.path.join(monorepo_path, "ansible", "playbooks", type)
            yield os.path.join(monorepo_path, "ansible", "plugins", type)
            yield os.path.join(monorepo_path, "ansible", type)
        yield os.path.join(DEBOPS_PY_PACKAGE, "plugins", type)
        yield os.path.join(DEBOPS_PY_PACKAGE, "playbooks", type)
        yield os.path.join(playbooks_path, type)
        yield os.path.join("/usr/share/ansible/", type)

    # Add custom configuration options to ansible.cfg: Take values
    # from [ansible ...] sections of the .debops.cfg file
    # Note: To set debops default values, use debops.config.DEFAULTS
    cfg = dict((sect.split(None, 1)[1], pairs)
               for sect, pairs in config.items()
               if sect.startswith('ansible '))

    defaults = cfg.setdefault('defaults', {})
    defaults['inventory'] = inventory_path
    if monorepo_path:
        defaults['roles_path'] = PATHSEP.join(filter(None, (
            defaults.get('roles_path'),  # value from .debops.cfg or None
            os.path.join(project_root, "roles"),
            os.path.join(project_root, "ansible", "roles"),
            os.path.join(project_root, "debops", "ansible", "roles"),
            os.path.join(monorepo_path, "ansible", "roles"),
            os.path.join(DEBOPS_PY_PACKAGE, "roles"),
            os.path.join(playbooks_path, "..", "roles"),
            os.path.join(playbooks_path, "roles"),
            "/etc/ansible/roles")))
    else:
        defaults['roles_path'] = PATHSEP.join(filter(None, (
            defaults.get('roles_path'),  # value from .debops.cfg or None
            os.path.join(project_root, "roles"),
            os.path.join(project_root, "ansible", "roles"),
            os.path.join(project_root, "debops", "ansible", "roles"),
            os.path.join(DEBOPS_PY_PACKAGE, "roles"),
            os.path.join(playbooks_path, "..", "roles"),
            os.path.join(playbooks_path, "roles"),
            "/etc/ansible/roles")))

    ansible_version_out = subprocess.check_output([ANSIBLE_PLAYBOOK,
                                                   "--version"]).decode()

    # Get first line and split by spaces to get second 'word'.
    ansible_version = str(ansible_version_out.splitlines()[0].split()[1])

    for plugin_type in ('action', 'callback', 'connection',
                        'filter', 'lookup', 'vars'):
        plugin_type = plugin_type+"_plugins"
        defaults[plugin_type] = PATHSEP.join(custom_paths(plugin_type))

        if (ver(ansible_version) >= ver("1.7") and
                ver(ansible_version) < ver("2.0")):
            # work around a bug obviously introduced in 1.7, see
            # https://github.com/ansible/ansible/issues/8555
            if ' ' in defaults[plugin_type]:
                defaults[plugin_type] = PATHSEP.join(
                    '"%s"' % p for p in defaults[plugin_type].split(PATHSEP))

    defaults['library'] = PATHSEP.join(custom_paths('library'))

    write_config(filename, cfg)


def find_playbook(config,
                  playbook,
                  project_root,
                  monorepo_path,
                  package_path,
                  playbooks_path):
    tries = [
        (project_root, "playbooks", playbook),
        (project_root, "ansible", "playbooks", playbook),
        (project_root, "debops", "ansible", "playbooks", playbook),
        (project_root, "debops", "ansible", "playbooks", "playbooks",
         playbook),
        (monorepo_path, "ansible", "playbooks", playbook),
        (monorepo_path, "ansible", "playbooks", "playbooks", playbook),
        (package_path, "playbooks", playbook),
        (playbooks_path, playbook),
        ]

    if 'playbooks_path' in config['paths']:
        tries += [(custom_path, playbook) for custom_path in
                  config['paths']['playbooks_path'].split(PATHSEP)]

    for parts in tries:
        try:
            play = os.path.join(*parts)
        except Exception:
            continue
        if os.path.isfile(play):
            return play


def main(cmd_args):
    project_root = find_debops_project(required=True)
    config = read_config(project_root)
    playbooks_path = find_playbookpath(config, project_root, required=False)
    monorepo_path = find_monorepopath(config, project_root, required=False)

    # Ensure that script works with least amount of changes
    if playbooks_path is None:
        playbooks_path = '/nonexistent'

    # Make sure required commands are present
    require_commands(ANSIBLE_PLAYBOOK)

    # Check if user specified a potential playbook name as the first
    # argument. If yes, use it as the playbook name and remove it from
    # the argument list
    play_list = []
    arg_list = []
    play = None
    if len(cmd_args) > 0:
        for index, element in enumerate(cmd_args):
            maybe_play = element
            if os.path.isfile(maybe_play):
                play = maybe_play
            else:
                play = find_playbook(config,
                                     maybe_play + ".yml",
                                     project_root,
                                     monorepo_path,
                                     DEBOPS_PY_PACKAGE,
                                     playbooks_path)
            if play:
                play_list.append(play)
            else:
                arg_list.append(element)
            del maybe_play

    if not play_list:
        play = find_playbook(config,
                             "site.yml",
                             project_root,
                             monorepo_path,
                             DEBOPS_PY_PACKAGE,
                             playbooks_path)
        if play:
            play_list.append(play)

    inventory_path = find_inventorypath(project_root)
    os.environ['ANSIBLE_INVENTORY'] = inventory_path

    global_vars_file = os.path.join(inventory_path + '/../global-vars.yml')
    if os.path.isfile(global_vars_file):
        print('Including variables from ' + os.path.relpath(global_vars_file))
        arg_list.extend(['--extra-vars',
                         '@' + os.path.relpath(global_vars_file)])

    ansible_config_file = os.path.join(project_root, ANSIBLE_CONFIG_FILE)
    os.environ['ANSIBLE_CONFIG'] = os.path.abspath(ansible_config_file)
    gen_ansible_cfg(ansible_config_file, config, project_root, playbooks_path,
                    monorepo_path, inventory_path)

    # Allow insecure SSH connections if requested
    if INSECURE:
        os.environ['ANSIBLE_HOST_KEY_CHECKING'] = 'False'

    # Create path to EncFS encrypted directory, based on inventory name
    encfs_encrypted = os.path.join(os.path.dirname(inventory_path),
                                   ENCFS_PREFIX + SECRET_NAME)

    # Check if encrypted secret directory exists and use it
    revert_unlock = padlock_unlock(encfs_encrypted)
    try:
        # Run ansible-playbook with custom environment
        print("Running Ansible playbooks:")
        for element in play_list:
            print(element)
        return subprocess.call([ANSIBLE_PLAYBOOK] + play_list + arg_list)
    finally:
        if revert_unlock:
            padlock_lock(encfs_encrypted)


try:
    sys.exit(main(sys.argv[1:]))
except KeyboardInterrupt:
    raise SystemExit('... aborted')
