# -*- coding: utf-8 -*-

# Import python libs
from __future__ import absolute_import, print_function
import os
import sys

# Import Salt libs
import salt.utils.job
from salt.ext.six import string_types
from salt.utils import parsers, print_cli
from salt.utils.args import yamlify_arg
from salt.utils.verify import verify_log
from salt.exceptions import (
        SaltClientError,
        SaltInvocationError,
        EauthAuthenticationError
        )

# Import 3rd-party libs
import salt.ext.six as six


class SaltCMD(parsers.SaltCMDOptionParser):
    '''
    The execution of a salt command happens here
    '''

    def run(self):
        '''
        Execute the salt command line
        '''
        import salt.auth
        import salt.client
        self.parse_args()

        # Setup file logging!
        self.setup_logfile_logger()
        verify_log(self.config)

        try:
            # We don't need to bail on config file permission errors
            # if the CLI
            # process is run with the -a flag
            skip_perm_errors = self.options.eauth != ''

            local = salt.client.get_local_client(
                self.get_config_file_path(),
                skip_perm_errors=skip_perm_errors)
        except SaltClientError as exc:
            self.exit(2, '{0}\n'.format(exc))
            return

        if self.options.batch or self.options.static:
            self._run_batch()
        else:
            if self.options.timeout <= 0:
                self.options.timeout = local.opts['timeout']

            kwargs = {
                'tgt': self.config['tgt'],
                'fun': self.config['fun'],
                'arg': self.config['arg'],
                'timeout': self.options.timeout,
                'show_timeout': self.options.show_timeout,
                'show_jid': self.options.show_jid}

            if 'token' in self.config:
                try:
                    with salt.utils.fopen(os.path.join(self.config['cachedir'], '.root_key'), 'r') as fp_:
                        kwargs['key'] = fp_.readline()
                except IOError:
                    kwargs['token'] = self.config['token']

            kwargs['delimiter'] = self.options.delimiter

            if self.selected_target_option:
                kwargs['expr_form'] = self.selected_target_option
            else:
                kwargs['expr_form'] = 'glob'

            if getattr(self.options, 'return'):
                kwargs['ret'] = getattr(self.options, 'return')

            if getattr(self.options, 'return_config'):
                kwargs['ret_config'] = getattr(self.options, 'return_config')

            if getattr(self.options, 'return_kwargs'):
                kwargs['ret_kwargs'] = yamlify_arg(
                        getattr(self.options, 'return_kwargs'))

            if getattr(self.options, 'module_executors'):
                kwargs['module_executors'] = yamlify_arg(getattr(self.options, 'module_executors'))

            if getattr(self.options, 'metadata'):
                kwargs['metadata'] = yamlify_arg(
                        getattr(self.options, 'metadata'))

            # If using eauth and a token hasn't already been loaded into
            # kwargs, prompt the user to enter auth credentials
            if 'token' not in kwargs and 'key' not in kwargs and self.options.eauth:
                resolver = salt.auth.Resolver(self.config)
                res = resolver.cli(self.options.eauth)
                if self.options.mktoken and res:
                    tok = resolver.token_cli(
                            self.options.eauth,
                            res
                            )
                    if tok:
                        kwargs['token'] = tok.get('token', '')
                if not res:
                    sys.stderr.write('ERROR: Authentication failed\n')
                    sys.exit(2)
                kwargs.update(res)
                kwargs['eauth'] = self.options.eauth

            if self.config['async']:
                jid = local.cmd_async(**kwargs)
                print_cli('Executed command with job ID: {0}'.format(jid))
                return
            retcodes = []
            try:
                # local will be None when there was an error
                errors = []
                if local:
                    if self.options.subset:
                        cmd_func = local.cmd_subset
                        kwargs['sub'] = self.options.subset
                        kwargs['cli'] = True
                    else:
                        cmd_func = local.cmd_cli

                    if self.options.progress:
                        kwargs['progress'] = True
                        self.config['progress'] = True
                        ret = {}
                        for progress in cmd_func(**kwargs):
                            out = 'progress'
                            try:
                                self._progress_ret(progress, out)
                            except salt.exceptions.LoaderError as exc:
                                raise salt.exceptions.SaltSystemExit(exc)
                            if 'return_count' not in progress:
                                ret.update(progress)
                        self._progress_end(out)
                        self._print_returns_summary(ret)
                    elif self.config['fun'] == 'sys.doc':
                        ret = {}
                        out = ''
                        for full_ret in local.cmd_cli(**kwargs):
                            ret_, out, retcode = self._format_ret(full_ret)
                            ret.update(ret_)
                        self._output_ret(ret, out)
                    else:
                        if self.options.verbose:
                            kwargs['verbose'] = True
                        ret = {}
                        for full_ret in cmd_func(**kwargs):
                            try:
                                ret_, out, retcode = self._format_ret(full_ret)
                                retcodes.append(retcode)
                                self._output_ret(ret_, out)
                                ret.update(full_ret)
                            except KeyError:
                                errors.append(full_ret)

                    # Returns summary
                    if self.config['cli_summary'] is True:
                        if self.config['fun'] != 'sys.doc':
                            if self.options.output is None:
                                self._print_returns_summary(ret)
                                self._print_errors_summary(errors)

                    # NOTE: Return code is set here based on if all minions
                    # returned 'ok' with a retcode of 0.
                    # This is the final point before the 'salt' cmd returns,
                    # which is why we set the retcode here.
                    if retcodes.count(0) < len(retcodes):
                        sys.stderr.write('ERROR: Minions returned with non-zero exit code\n')
                        sys.exit(11)

            except (SaltInvocationError, EauthAuthenticationError, SaltClientError) as exc:
                ret = str(exc)
                out = ''
                self._output_ret(ret, out)

    def _run_batch(self):
        import salt.cli.batch
        eauth = {}
        if 'token' in self.config:
            eauth['token'] = self.config['token']

        # If using eauth and a token hasn't already been loaded into
        # kwargs, prompt the user to enter auth credentials
        if 'token' not in eauth and self.options.eauth:
            resolver = salt.auth.Resolver(self.config)
            res = resolver.cli(self.options.eauth)
            if self.options.mktoken and res:
                tok = resolver.token_cli(
                        self.options.eauth,
                        res
                        )
                if tok:
                    eauth['token'] = tok.get('token', '')
            if not res:
                sys.stderr.write('ERROR: Authentication failed\n')
                sys.exit(2)
            eauth.update(res)
            eauth['eauth'] = self.options.eauth

        if self.options.static:

            if not self.options.batch:
                self.config['batch'] = '100%'

            try:
                batch = salt.cli.batch.Batch(self.config, eauth=eauth, quiet=True)
            except salt.exceptions.SaltClientError as exc:
                sys.exit(2)

            ret = {}

            for res in batch.run():
                ret.update(res)

            self._output_ret(ret, '')

        else:
            try:
                batch = salt.cli.batch.Batch(self.config, eauth=eauth, parser=self.options)
            except salt.exceptions.SaltClientError as exc:
                # We will print errors to the console further down the stack
                sys.exit(1)
            # Printing the output is already taken care of in run() itself
            for res in batch.run():
                if self.options.failhard:
                    for ret in six.itervalues(res):
                        retcode = salt.utils.job.get_retcode(ret)
                        if retcode != 0:
                            sys.stderr.write(
                                '{0}\nERROR: Minions returned with non-zero exit code.\n'.format(
                                    res
                                )
                            )
                            sys.exit(retcode)

    def _print_errors_summary(self, errors):
        if errors:
            print_cli('\n')
            print_cli('---------------------------')
            print_cli('Errors')
            print_cli('---------------------------')
            for minion in errors:
                print_cli(self._format_error(minion))

    def _print_returns_summary(self, ret):
        '''
        Display returns summary
        '''
        return_counter = 0
        not_return_counter = 0
        not_return_minions = []
        not_response_minions = []
        not_connected_minions = []
        failed_minions = []
        for each_minion in ret:
            minion_ret = ret[each_minion].get('ret')
            if (
                    isinstance(minion_ret, string_types)
                    and minion_ret.startswith("Minion did not return")
                    ):
                if "Not connected" in minion_ret:
                    not_connected_minions.append(each_minion)
                elif "No response" in minion_ret:
                    not_response_minions.append(each_minion)
                not_return_counter += 1
                not_return_minions.append(each_minion)
            else:
                return_counter += 1
                if salt.utils.job.get_retcode(ret[each_minion]):
                    failed_minions.append(each_minion)
        print_cli('\n')
        print_cli('-------------------------------------------')
        print_cli('Summary')
        print_cli('-------------------------------------------')
        print_cli('# of minions targeted: {0}'.format(return_counter + not_return_counter))
        print_cli('# of minions returned: {0}'.format(return_counter))
        print_cli('# of minions that did not return: {0}'.format(not_return_counter))
        print_cli('# of minions with errors: {0}'.format(len(failed_minions)))
        if self.options.verbose:
            if not_connected_minions:
                print_cli('Minions not connected: {0}'.format(" ".join(not_connected_minions)))
            if not_response_minions:
                print_cli('Minions not responding: {0}'.format(" ".join(not_response_minions)))
            if failed_minions:
                print_cli('Minions with failures: {0}'.format(" ".join(failed_minions)))
        print_cli('-------------------------------------------')

    def _progress_end(self, out):
        import salt.output
        salt.output.progress_end(self.progress_bar)

    def _progress_ret(self, progress, out):
        '''
        Print progress events
        '''
        import salt.output
        # Get the progress bar
        if not hasattr(self, 'progress_bar'):
            try:
                self.progress_bar = salt.output.get_progress(self.config, out, progress)
            except Exception as exc:
                raise salt.exceptions.LoaderError('\nWARNING: Install the `progressbar` python package. '
                                                  'Requested job was still run but output cannot be displayed.\n')
        salt.output.update_progress(self.config, progress, self.progress_bar, out)

    def _output_ret(self, ret, out):
        '''
        Print the output from a single return to the terminal
        '''
        import salt.output
        # Handle special case commands
        if self.config['fun'] == 'sys.doc' and not isinstance(ret, Exception):
            self._print_docs(ret)
        else:
            # Determine the proper output method and run it
            salt.output.display_output(ret, out, self.config)
        if not ret:
            sys.stderr.write('ERROR: No return received\n')
            sys.exit(2)

    def _format_ret(self, full_ret):
        '''
        Take the full return data and format it to simple output
        '''
        ret = {}
        out = ''
        retcode = 0
        for key, data in six.iteritems(full_ret):
            ret[key] = data['ret']
            if 'out' in data:
                out = data['out']
            ret_retcode = salt.utils.job.get_retcode(data)
            if ret_retcode > retcode:
                retcode = ret_retcode
        return ret, out, retcode

    def _format_error(self, minion_error):
        for minion, error_doc in six.iteritems(minion_error):
            error = 'Minion [{0}] encountered exception \'{1}\''.format(minion, error_doc['message'])
        return error

    def _print_docs(self, ret):
        '''
        Print out the docstrings for all of the functions on the minions
        '''
        import salt.output
        docs = {}
        if not ret:
            self.exit(2, 'No minions found to gather docs from\n')
        if isinstance(ret, str):
            self.exit(2, '{0}\n'.format(ret))
        for host in ret:
            if ret[host] == 'Minion did not return. [Not connected]':
                continue
            for fun in ret[host]:
                if fun not in docs:
                    if ret[host][fun]:
                        docs[fun] = ret[host][fun]
        for fun in sorted(docs):
            salt.output.display_output(fun + ':', 'nested', self.config)
            print_cli(docs[fun])
            print_cli('')
