# -*- coding: utf-8 -*-
'''
Execute salt convenience routines
'''

# Import python libs
from __future__ import print_function
import collections
import logging
import time

# Import salt libs
import salt.exceptions
import salt.loader
import salt.minion
import salt.utils
import salt.utils.args
import salt.utils.event
from salt.client import mixins
from salt.output import display_output
from salt.utils.error import raise_error
from salt.utils.event import tagify

log = logging.getLogger(__name__)


class RunnerClient(mixins.SyncClientMixin, mixins.AsyncClientMixin, object):
    '''
    The interface used by the :command:`salt-run` CLI tool on the Salt Master

    It executes :ref:`runner modules <all-salt.runners>` which run on the Salt
    Master.

    Importing and using ``RunnerClient`` must be done on the same machine as
    the Salt Master and it must be done using the same user that the Salt
    Master is running as.

    Salt's :conf_master:`external_auth` can be used to authenticate calls. The
    eauth user must be authorized to execute runner modules: (``@runner``).
    Only the :py:meth:`master_call` below supports eauth.
    '''
    client = 'runner'
    tag_prefix = 'run'

    def __init__(self, opts):
        self.opts = opts
        self.functions = salt.loader.runner(opts)

    def cmd(self, fun, arg, pub_data=None, kwarg=None):
        '''
        Execute a runner function

        .. code-block:: python

            >>> opts = salt.config.master_config('/etc/salt/master')
            >>> runner = salt.runner.RunnerClient(opts)
            >>> runner.cmd('jobs.list_jobs', [])
            {
                '20131219215650131543': {
                    'Arguments': [300],
                    'Function': 'test.sleep',
                    'StartTime': '2013, Dec 19 21:56:50.131543',
                    'Target': '*',
                    'Target-type': 'glob',
                    'User': 'saltdev'
                },
                '20131219215921857715': {
                    'Arguments': [300],
                    'Function': 'test.sleep',
                    'StartTime': '2013, Dec 19 21:59:21.857715',
                    'Target': '*',
                    'Target-type': 'glob',
                    'User': 'saltdev'
                },
            }

        '''
        if kwarg is None:
            kwarg = {}
        if not isinstance(kwarg, dict):
            raise salt.exceptions.SaltInvocationError(
                'kwarg must be formatted as a dictionary'
            )

        if pub_data is None:
            pub_data = {}
        if not isinstance(pub_data, dict):
            raise salt.exceptions.SaltInvocationError(
                'pub_data must be formatted as a dictionary'
            )

        arglist = salt.utils.args.parse_input(arg)

        def _append_kwarg(arglist, kwarg):
            '''
            Append the kwarg dict to the arglist
            '''
            kwarg['__kwarg__'] = True
            arglist.append(kwarg)

        if kwarg:
            try:
                if isinstance(arglist[-1], dict) \
                        and '__kwarg__' in arglist[-1]:
                    for key, val in kwarg.iteritems():
                        if key in arglist[-1]:
                            log.warning(
                                'Overriding keyword argument {0!r}'.format(key)
                            )
                        arglist[-1][key] = val
                else:
                    # No kwargs yet present in arglist
                    _append_kwarg(arglist, kwarg)
            except IndexError:
                # arglist is empty, just append
                _append_kwarg(arglist, kwarg)

        self._verify_fun(fun)
        args, kwargs = salt.minion.load_args_and_kwargs(
            self.functions[fun], arglist, pub_data
        )
        return self.functions[fun](*args, **kwargs)

    def master_call(self, **kwargs):
        '''
        Execute a runner function through the master network interface (eauth).
        '''
        load = kwargs
        load['cmd'] = 'runner'
        sreq = salt.transport.Channel.factory(self.opts,
                                              crypt='clear',
                                              usage='master_call')
        ret = sreq.send(load)
        if isinstance(ret, collections.Mapping):
            if 'error' in ret:
                raise_error(**ret['error'])
        return ret

    def _reformat_low(self, low):
        '''
        Format the low data for RunnerClient()'s master_call() function

        The master_call function here has a different function signature than
        on WheelClient. So extract all the eauth keys and the fun key and
        assume everything else is a kwarg to pass along to the runner function
        to be called.
        '''
        auth_creds = dict([(i, low.pop(i)) for i in [
                'username', 'password', 'eauth', 'token', 'client',
            ] if i in low])
        reformatted_low = {'fun': low.pop('fun')}
        reformatted_low.update(auth_creds)
        reformatted_low['kwarg'] = low
        return reformatted_low

    def cmd_async(self, low):
        '''
        Execute a runner function asynchronously; eauth is respected

        This function requires that :conf_master:`external_auth` is configured
        and the user is authorized to execute runner functions: (``@runner``).

        .. code-block:: python

            runner.eauth_async({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        '''
        reformatted_low = self._reformat_low(low)
        return self.master_call(**reformatted_low)

    def cmd_sync(self, low, timeout=None):
        '''
        Execute a runner function synchronously; eauth is respected

        This function requires that :conf_master:`external_auth` is configured
        and the user is authorized to execute runner functions: (``@runner``).

        .. code-block:: python

            runner.eauth_sync({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        '''
        sevent = salt.utils.event.get_event('master',
                                            self.opts['sock_dir'],
                                            self.opts['transport'],
                                            opts=self.opts)

        reformatted_low = self._reformat_low(low)
        job = self.master_call(**reformatted_low)
        ret_tag = tagify('ret', base=job['tag'])

        timelimit = time.time() + (timeout or 300)
        while True:
            ret = sevent.get_event(full=True)
            if ret is None:
                if time.time() > timelimit:
                    raise salt.exceptions.SaltClientTimeout(
                        "RunnerClient job '{0}' timed out".format(job['jid']),
                        jid=job['jid'])
                else:
                    continue

            if ret['tag'] == ret_tag:
                return ret['data']['return']


class Runner(RunnerClient):
    '''
    Execute the salt runner interface
    '''
    def _print_docs(self):
        '''
        Print out the documentation!
        '''
        arg = self.opts.get('fun', None)
        docs = super(Runner, self).get_docs(arg)
        for fun in sorted(docs):
            display_output('{0}:'.format(fun), 'text', self.opts)
            print(docs[fun])

    def run(self):
        '''
        Execute the runner sequence
        '''
        if self.opts.get('doc', False):
            self._print_docs()
        else:
            try:
                return super(Runner, self).cmd(
                        self.opts['fun'], self.opts['arg'], self.opts)
            except salt.exceptions.SaltException as exc:
                ret = str(exc)
                print(ret)
                return ret
