# -*- coding: utf-8 -*-
#
# Author: Bo Maryniuk <bo@suse.de>
#
# Copyright 2017 SUSE LLC
# 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.

r'''
Execution of Ansible modules from within states
===============================================

With `ansible.call` these states allow individual Ansible module calls to be
made via states. To call an Ansible module function use a :mod:`module.run <salt.states.ansible.call>`
state:

.. code-block:: yaml

    some_set_of_tasks:
      ansible:
        - system.ping
        - packaging.os.zypper
          - name: emacs
          - state: installed

'''
from __future__ import absolute_import, print_function, unicode_literals
import logging
import os
import sys

# Import salt modules
import salt.fileclient
import salt.ext.six as six
from salt.utils.decorators import depends
import salt.utils.decorators.path

log = logging.getLogger(__name__)
__virtualname__ = 'ansible'


@depends('ansible')
class AnsibleState(object):
    '''
    Ansible state caller.
    '''
    def get_args(self, argset):
        '''
        Get args and kwargs from the argset.

        :param argset:
        :return:
        '''
        args = []
        kwargs = {}
        for element in argset or []:
            if isinstance(element, dict):
                kwargs.update(element)
            else:
                args.append(element)
        return args, kwargs

    def __call__(self, **kwargs):
        '''
        Call Ansible module.

        :return:
        '''

        ret = {
            'name': kwargs.pop('name'),
            'changes': {},
            'comment': '',
            'result': True,
        }

        for mod_name, mod_params in kwargs.items():
            args, kwargs = self.get_args(mod_params)
            try:
                ans_mod_out = __salt__['ansible.{0}'.format(mod_name)](**{'__pub_arg': [args, kwargs]})
            except Exception as err:
                ans_mod_out = 'Module "{0}" failed. Error message: ({1}) {2}'.format(
                    mod_name, err.__class__.__name__, err)
                ret['result'] = False
            ret['changes'][mod_name] = ans_mod_out

        return ret


def __virtual__():
    '''
    Disable, if Ansible is not available around on the Minion.
    '''
    setattr(sys.modules[__name__], 'call', lambda **kwargs: AnsibleState()(**kwargs))   # pylint: disable=W0108
    return __virtualname__


def _client():
    '''
    Get a fileclient
    '''
    return salt.fileclient.get_file_client(__opts__)


def _changes(plays):
    '''
    Find changes in ansible return data
    '''
    changes = {}
    for play in plays['plays']:
        task_changes = {}
        for task in play['tasks']:
            host_changes = {}
            for host, data in six.iteritems(task['hosts']):
                if data['changed'] is True:
                    host_changes[host] = data.get('diff', data.get('changes', {}))
            if host_changes:
                task_changes[task['task']['name']] = host_changes
        if task_changes:
            changes[play['play']['name']] = task_changes
    return changes


@salt.utils.decorators.path.which('ansible-playbook')
def playbooks(name, rundir=None, git_repo=None, git_kwargs=None, ansible_kwargs=None):
    '''
    Run Ansible Playbooks

    :param name: path to playbook. This can be relative to rundir or the git repo
    :param rundir: location to run ansible-playbook from.
    :param git_repo: git repository to clone for ansible playbooks.  This is cloned
                     using the `git.latest` state, and is cloned to the `rundir`
                     if specified, otherwise it is clone to the `cache_dir`
    :param git_kwargs: extra kwargs to pass to `git.latest` state module besides
                       the `name` and `target`
    :param ansible_kwargs: extra kwargs to pass to `ansible.playbooks` execution
                           module besides the `name` and `target`

    :return: Ansible playbook output.

    .. code-block:: yaml

        run nginx install:
          ansible.playbooks:
            - name: install.yml
            - git_repo: git://github.com/gituser/playbook.git
            - git_kwargs:
                rev: master
    '''
    ret = {
        'result': False,
        'changes': {},
        'comment': 'Running playbook {0}'.format(name),
        'name': name,
    }
    if git_repo:
        if not isinstance(rundir, six.text_type) or not os.path.isdir(rundir):
            rundir = _client()._extrn_path(git_repo, 'base')
            log.trace('rundir set to %s', rundir)
        if not isinstance(git_kwargs, dict):
            log.debug('Setting git_kwargs to empty dict: %s', git_kwargs)
            git_kwargs = {}
        __states__['git.latest'](
            name=git_repo,
            target=rundir,
            **git_kwargs
        )
    if not isinstance(ansible_kwargs, dict):
        log.debug('Setting ansible_kwargs to empty dict: %s', ansible_kwargs)
        ansible_kwargs = {}
    checks = __salt__['ansible.playbooks'](name, rundir=rundir, check=True, diff=True, **ansible_kwargs)
    if all(not check['changed'] for check in six.itervalues(checks['stats'])):
        ret['comment'] = 'No changes to be made from playbook {0}'.format(name)
        ret['result'] = True
    elif __opts__['test']:
        ret['comment'] = 'Changes will be made from playbook {0}'.format(name)
        ret['result'] = None
        ret['changes'] = _changes(checks)
    else:
        results = __salt__['ansible.playbooks'](name, rundir=rundir, diff=True, **ansible_kwargs)
        ret['comment'] = 'Changes were made by playbook {0}'.format(name)
        ret['changes'] = _changes(results)
        ret['result'] = all(
            not check['failures'] and not check['unreachable']
            for check in six.itervalues(checks['stats'])
        )
    return ret
