"""
Execute salt convenience routines
"""

import logging
import os

import salt.exceptions
import salt.loader
import salt.minion
import salt.utils.args
import salt.utils.event
import salt.utils.files
import salt.utils.user
from salt.client import mixins
from salt.output import display_output
from salt.utils.lazy import verify_fun

log = logging.getLogger(__name__)


class RunnerClient(mixins.SyncClientMixin, mixins.AsyncClientMixin):
    """
    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, context=None):
        mixins.SyncClientMixin.__init__(self, opts, context=context)
        mixins.AsyncClientMixin.__init__(self, opts, context=context)
        self.opts = opts
        self.context = context or {}
        self.event = None
        self.salt_user = salt.utils.user.get_specific_user()
        self.event = salt.utils.event.get_event(
            "master", self.opts["sock_dir"], opts=self.opts, listen=False
        )

    def destroy(self):
        if self.event is not None:
            self.event.destroy()
            self.event = None
        if hasattr(self, "_functions") and self._functions is not None:
            if hasattr(self._functions, "destroy"):
                self._functions.destroy()
            self._functions = {}
        if hasattr(self, "utils") and self.utils is not None:
            if hasattr(self.utils, "destroy"):
                self.utils.destroy()
            self.utils = {}

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.destroy()

    @property
    def functions(self):
        if not hasattr(self, "_functions"):
            if not hasattr(self, "utils"):
                self.utils = salt.loader.utils(self.opts)
            # Must be self.functions for mixin to work correctly :-/
            try:
                self._functions = salt.loader.runner(
                    self.opts, utils=self.utils, context=self.context
                )
            except AttributeError:
                # Just in case self.utils is still not present (perhaps due to
                # problems with the loader), load the runner funcs without them
                self._functions = salt.loader.runner(self.opts, context=self.context)

        return self._functions

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

        This also normalizes the following low data formats to a single, common
        low data structure.

        Old-style low: ``{'fun': 'jobs.lookup_jid', 'jid': '1234'}``
        New-style: ``{'fun': 'jobs.lookup_jid', 'kwarg': {'jid': '1234'}}``
        CLI-style: ``{'fun': 'jobs.lookup_jid', 'arg': ['jid="1234"']}``
        """
        fun = low.pop("fun")
        verify_fun(self.functions, fun)

        eauth_creds = {
            i: low.pop(i)
            for i in [
                "username",
                "password",
                "eauth",
                "token",
                "client",
                "user",
                "key",
            ]
            if i in low
        }

        # Run name=value args through parse_input. We don't need to run kwargs
        # through because there is no way to send name=value strings in the low
        # dict other than by including an `arg` array.
        _arg, _kwarg = salt.utils.args.parse_input(low.pop("arg", []), condition=False)
        _kwarg.update(low.pop("kwarg", {}))

        # If anything hasn't been pop()'ed out of low by this point it must be
        # an old-style kwarg.
        _kwarg.update(low)

        # Finally, mung our kwargs to a format suitable for the byzantine
        # load_args_and_kwargs so that we can introspect the function being
        # called and fish for invalid kwargs.
        munged = []
        munged.extend(_arg)
        munged.append(dict(__kwarg__=True, **_kwarg))
        arg, kwarg = salt.minion.load_args_and_kwargs(
            self.functions[fun], munged, ignore_invalid=True
        )

        return dict(fun=fun, kwarg={"kwarg": kwarg, "arg": arg}, **eauth_creds)

    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.cmd_async({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        """
        reformatted_low = self._reformat_low(low)

        return mixins.AsyncClientMixin.cmd_async(self, reformatted_low)

    def cmd_sync(self, low, timeout=None, full_return=False):
        """
        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.cmd_sync({
                'fun': 'jobs.list_jobs',
                'username': 'saltdev',
                'password': 'saltdev',
                'eauth': 'pam',
            })
        """
        reformatted_low = self._reformat_low(low)
        return mixins.SyncClientMixin.cmd_sync(
            self, reformatted_low, timeout, full_return
        )

    def cmd(
        self,
        fun,
        arg=None,
        pub_data=None,
        kwarg=None,
        print_event=True,
        full_return=False,
    ):  # pylint: disable=useless-super-delegation
        """
        Execute a 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'
                },
            }

        """
        return super().cmd(fun, arg, pub_data, kwarg, print_event, full_return)


class Runner(RunnerClient):
    """
    Execute the salt runner interface
    """

    def __init__(self, opts, context=None):
        super().__init__(opts, context=context)
        self.returners = salt.loader.returners(opts, self.functions, context=context)
        self.outputters = salt.loader.outputters(opts)

    def destroy(self):
        if hasattr(self, "returners") and self.returners is not None:
            if hasattr(self.returners, "destroy"):
                self.returners.destroy()
            self.returners = {}
        if hasattr(self, "outputters") and self.outputters is not None:
            if hasattr(self.outputters, "destroy"):
                self.outputters.destroy()
            self.outputters = {}
        super().destroy()

    def print_docs(self):
        """
        Print out the documentation!
        """
        arg = self.opts.get("fun", None)
        docs = super().get_docs(arg)
        for fun in sorted(docs):
            display_output(f"{fun}:", "text", self.opts)
            print(docs[fun])

    # TODO: move to mixin whenever we want a salt-wheel cli
    def run(self, full_return=False):
        """
        Execute the runner sequence
        """
        import salt.minion

        ret = {}
        if self.opts.get("doc", False):
            self.print_docs()
        else:
            low = {"fun": self.opts["fun"]}
            try:
                # Allocate a jid
                async_pub = self._gen_async_pub(jid=self.opts.get("jid"))
                self.jid = async_pub["jid"]

                fun_args = salt.utils.args.parse_input(
                    self.opts["arg"], no_parse=self.opts.get("no_parse", [])
                )

                verify_fun(self.functions, low["fun"])
                args, kwargs = salt.minion.load_args_and_kwargs(
                    self.functions[low["fun"]], fun_args
                )
                low["arg"] = args
                low["kwarg"] = kwargs

                if self.opts.get("eauth"):
                    if "token" in self.opts:
                        try:
                            with salt.utils.files.fopen(
                                os.path.join(self.opts["cachedir"], ".root_key"), "r"
                            ) as fp_:
                                low["key"] = salt.utils.stringutils.to_unicode(
                                    fp_.readline()
                                )
                        except OSError:
                            low["token"] = self.opts["token"]

                    # If using eauth and a token hasn't already been loaded into
                    # low, prompt the user to enter auth credentials
                    if "token" not in low and "key" not in low and self.opts["eauth"]:
                        # This is expensive. Don't do it unless we need to.
                        import salt.auth

                        resolver = salt.auth.Resolver(self.opts)
                        res = resolver.cli(self.opts["eauth"])
                        if self.opts["mktoken"] and res:
                            tok = resolver.token_cli(self.opts["eauth"], res)
                            if tok:
                                low["token"] = tok.get("token", "")
                        if not res:
                            log.error("Authentication failed")
                            return ret
                        low.update(res)
                        low["eauth"] = self.opts["eauth"]
                else:
                    user = salt.utils.user.get_specific_user()

                if low["fun"] in ["state.orchestrate", "state.orch", "state.sls"]:
                    low["kwarg"]["orchestration_jid"] = async_pub["jid"]

                # Run the runner!
                if self.opts.get("async", False):
                    if self.opts.get("eauth"):
                        async_pub = self.cmd_async(low)
                    else:
                        async_pub = self.asynchronous(
                            self.opts["fun"], low, user=user, pub=async_pub
                        )
                    # by default: info will be not enougth to be printed out !
                    log.warning(
                        "Running in asynchronous mode. Results of this execution may "
                        "be collected by attaching to the master event bus or "
                        "by examining the master job cache, if configured. "
                        "This execution is running under tag %s",
                        async_pub["tag"],
                    )
                    return async_pub["jid"]  # return the jid

                # otherwise run it in the main process
                if self.opts.get("show_jid"):
                    print(f"jid: {self.jid}")

                if self.opts.get("eauth"):
                    ret = self.cmd_sync(low)
                    if isinstance(ret, dict) and set(ret) == {"data", "outputter"}:
                        outputter = ret["outputter"]
                        ret = ret["data"]
                    else:
                        outputter = None
                    display_output(ret, outputter, self.opts)
                else:
                    ret = self._proc_function(
                        instance=self,
                        opts=self.opts,
                        fun=self.opts["fun"],
                        low=low,
                        user=user,
                        tag=async_pub["tag"],
                        jid=async_pub["jid"],
                        daemonize=False,
                        full_return=full_return,
                    )
            except salt.exceptions.SaltException as exc:
                with salt.utils.event.get_event("master", opts=self.opts) as evt:
                    evt.fire_event(
                        {
                            "success": False,
                            "return": f"{exc}",
                            "retcode": 254,
                            "fun": self.opts["fun"],
                            "fun_args": fun_args,
                            "jid": self.jid,
                        },
                        tag=f"salt/run/{self.jid}/ret",
                    )
                # Attempt to grab documentation
                if "fun" in low:
                    ret = self.get_docs("{}*".format(low["fun"]))
                else:
                    ret = None

                # If we didn't get docs returned then
                # return the `not availble` message.
                if not ret:
                    ret = f"{exc}"
                if not self.opts.get("quiet", False):
                    display_output(ret, "nested", self.opts)
            else:
                log.debug("Runner return: %s", ret)

            return ret
