import copy
import logging
import os
import random

import salt.client.ssh
import salt.config
import salt.syspaths
import salt.utils.args
from salt.exceptions import SaltClientError

log = logging.getLogger(__name__)


class SSHClient:
    """
    Create a client object for executing routines via the salt-ssh backend

    .. versionadded:: 2015.5.0
    """

    def __init__(
        self,
        c_path=os.path.join(salt.syspaths.CONFIG_DIR, "master"),
        mopts=None,
        disable_custom_roster=False,
    ):
        if mopts:
            self.opts = mopts
        else:
            if os.path.isdir(c_path):
                log.warning(
                    "%s expects a file path not a directory path(%s) to "
                    "its 'c_path' keyword argument",
                    self.__class__.__name__,
                    c_path,
                )
            self.opts = salt.config.client_config(c_path)

        # Salt API should never offer a custom roster!
        self.opts["__disable_custom_roster"] = disable_custom_roster
        # Pillar compilation and nested SSH calls require the correct config_dir
        # in __opts__, otherwise we will use the SSH minion's one later.
        if "config_dir" not in self.opts:
            self.opts["config_dir"] = os.path.dirname(c_path)

    def sanitize_kwargs(self, kwargs):
        roster_vals = [
            ("host", str),
            ("ssh_user", str),
            ("ssh_passwd", str),
            ("ssh_port", int),
            ("ssh_sudo", bool),
            ("ssh_sudo_user", str),
            ("ssh_priv", str),
            ("ssh_priv_passwd", str),
            ("ssh_identities_only", bool),
            ("ssh_remote_port_forwards", str),
            ("ssh_keepalive", bool),
            ("ssh_keepalive_interval", int),
            ("ssh_keepalive_count_max", int),
            ("ssh_options", list),
            ("ssh_max_procs", int),
            ("ssh_askpass", bool),
            ("ssh_key_deploy", bool),
            ("ssh_update_roster", bool),
            ("ssh_scan_ports", str),
            ("ssh_scan_timeout", int),
            ("ssh_timeout", int),
            ("ssh_log_file", str),
            ("ssh_pre_hook", str),
            ("raw_shell", bool),
            ("refresh_cache", bool),
            ("roster", str),
            ("roster_file", str),
            ("rosters", list),
            ("ignore_host_keys", bool),
            ("raw_shell", bool),
            ("extra_filerefs", str),
            ("min_extra_mods", str),
            ("thin_extra_mods", str),
            ("verbose", bool),
            ("static", bool),
            ("ssh_wipe", bool),
            ("rand_thin_dir", bool),
            ("regen_thin", bool),
            ("ssh_run_pre_flight", bool),
            ("no_host_keys", bool),
            ("saltfile", str),
        ]
        sane_kwargs = {}
        for name, kind in roster_vals:
            if name not in kwargs:
                continue
            try:
                val = kind(kwargs[name])
            except ValueError:
                log.warning("Unable to cast kwarg %s", name)
                continue
            if kind is bool or kind is int:
                sane_kwargs[name] = val
            elif kind is str:
                if val.find("ProxyCommand") != -1:
                    log.warning("Filter unsafe value for kwarg %s", name)
                    continue
                sane_kwargs[name] = val
            elif kind is list:
                sane_val = []
                for item in val:
                    # This assumes the values are strings
                    if item.find("ProxyCommand") != -1:
                        log.warning("Filter unsafe value for kwarg %s", name)
                        continue
                    sane_val.append(item)
                sane_kwargs[name] = sane_val
        return sane_kwargs

    def _prep_ssh(
        self, tgt, fun, arg=(), timeout=None, tgt_type="glob", kwarg=None, **kwargs
    ):
        """
        Prepare the arguments
        """
        kwargs = self.sanitize_kwargs(kwargs)
        opts = copy.deepcopy(self.opts)
        opts.update(kwargs)
        if timeout:
            opts["timeout"] = timeout
        arg = salt.utils.args.condition_input(arg, kwarg)
        opts["argv"] = [fun] + arg
        opts["selected_target_option"] = tgt_type
        opts["tgt"] = tgt
        opts["arg"] = arg
        return salt.client.ssh.SSH(opts)

    def cmd_iter(
        self,
        tgt,
        fun,
        arg=(),
        timeout=None,
        tgt_type="glob",
        ret="",
        kwarg=None,
        **kwargs,
    ):
        """
        Execute a single command via the salt-ssh subsystem and return a
        generator

        .. versionadded:: 2015.5.0
        """
        ssh = self._prep_ssh(tgt, fun, arg, timeout, tgt_type, kwarg, **kwargs)
        yield from ssh.run_iter(jid=kwargs.get("jid", None))

    def cmd(
        self, tgt, fun, arg=(), timeout=None, tgt_type="glob", kwarg=None, **kwargs
    ):
        """
        Execute a single command via the salt-ssh subsystem and return all
        routines at once

        .. versionadded:: 2015.5.0
        """
        ssh = self._prep_ssh(tgt, fun, arg, timeout, tgt_type, kwarg, **kwargs)
        final = {}
        for ret in ssh.run_iter(jid=kwargs.get("jid", None)):
            final.update(ret)
        return final

    def cmd_sync(self, low):
        """
        Execute a salt-ssh call synchronously.

        .. versionadded:: 2015.5.0

        WARNING: Eauth is **NOT** respected

        .. code-block:: python

            client.cmd_sync({
                'tgt': 'silver',
                'fun': 'test.ping',
                'arg': (),
                'tgt_type'='glob',
                'kwarg'={}
                })
            {'silver': {'fun_args': [], 'jid': '20141202152721523072', 'return': True, 'retcode': 0, 'success': True, 'fun': 'test.ping', 'id': 'silver'}}
        """

        kwargs = copy.deepcopy(low)

        for ignore in ["tgt", "fun", "arg", "timeout", "tgt_type", "kwarg"]:
            if ignore in kwargs:
                del kwargs[ignore]

        return self.cmd(
            low["tgt"],
            low["fun"],
            low.get("arg", []),
            low.get("timeout"),
            low.get("tgt_type"),
            low.get("kwarg"),
            **kwargs,
        )

    def cmd_async(self, low, timeout=None):
        """
        Execute aa salt-ssh asynchronously

        WARNING: Eauth is **NOT** respected

        .. code-block:: python

            client.cmd_sync({
                'tgt': 'silver',
                'fun': 'test.ping',
                'arg': (),
                'tgt_type'='glob',
                'kwarg'={}
                })
            {'silver': {'fun_args': [], 'jid': '20141202152721523072', 'return': True, 'retcode': 0, 'success': True, 'fun': 'test.ping', 'id': 'silver'}}
        """
        # TODO Not implemented
        raise SaltClientError

    def cmd_subset(
        self,
        tgt,
        fun,
        arg=(),
        timeout=None,
        tgt_type="glob",
        ret="",
        kwarg=None,
        subset=3,
        **kwargs,
    ):
        """
        Execute a command on a random subset of the targeted systems

        The function signature is the same as :py:meth:`cmd` with the
        following exceptions.

        :param subset: The number of systems to execute on

        .. code-block:: python

            >>> import salt.client.ssh.client
            >>> sshclient= salt.client.ssh.client.SSHClient()
            >>> sshclient.cmd_subset('*', 'test.ping', subset=1)
            {'jerry': True}

        .. versionadded:: 2017.7.0
        """
        minion_ret = self.cmd(tgt, "sys.list_functions", tgt_type=tgt_type, **kwargs)
        minions = list(minion_ret)
        random.shuffle(minions)
        f_tgt = []
        for minion in minions:
            if fun in minion_ret[minion]["return"]:
                f_tgt.append(minion)
            if len(f_tgt) >= subset:
                break
        return self.cmd_iter(
            f_tgt, fun, arg, timeout, tgt_type="list", ret=ret, kwarg=kwarg, **kwargs
        )

    def destroy(self):
        """
        API compatibility method with salt.client.LocalClient
        """

    def __enter__(self):
        """
        API compatibility method with salt.client.LocalClient
        """
        return self

    def __exit__(self, *args):
        """
        API compatibility method with salt.client.LocalClient
        """
        self.destroy()
