# -*- coding: utf-8 -*-
from __future__ import absolute_import

import sys
import optparse  # pylint: disable=deprecated-module
import multiprocessing

import salt_sproxy.version

import six
from six.moves import map  # pylint: disable=W8410
from six.moves import range  # pylint: disable=W8410
import salt.version
import salt.utils.args
import salt.utils.parsers
import salt.config as config

try:
    from jnpr.junos import __version__ as jnpr_version

    # Ain't Juniper awkward?
except ImportError:
    jnpr_version = None

CPU_COUNT = multiprocessing.cpu_count()


def salt_information():
    """
    Return version of Salt and salt-sproxy.
    """
    yield "Salt", salt.version.__version__
    yield "Salt SProxy", salt_sproxy.version.__version__


def dependency_information(include_salt_cloud=False):
    """
    Report versions of library dependencies.

    This function has been ported from
    https://github.com/saltstack/salt/blob/develop/salt/version.py
    and extended here to collect the version information for several more
    libraries that may be necessary for various Proxy (or Execution) Modules.
    """
    libs = [
        ("Python", None, sys.version.rsplit("\n", maxsplit=1)[0].strip()),
        ("NAPALM", "napalm", "__version__"),
        ("Netmiko", "netmiko", "__version__"),
        ("junos-eznc", None, jnpr_version),
        ("ncclient", "ncclient", "__version__"),
        ("paramiko", "paramiko", "__version__"),
        ("pyeapi", "pyeapi", "__version__"),
        ("textfsm", "textfsm", "__version__"),
        ("jxmlease", "jxmlease", "__version__"),
        ("scp", "scp", "__version__"),
        ("PyNSO", "pynso", "__version__"),
        ("Ansible", "ansible", "__version__"),
        ("PyNetBox", "pynetbox", "__version__"),
        ("Jinja2", "jinja2", "__version__"),
        ("M2Crypto", "M2Crypto", "version"),
        ("msgpack-python", "msgpack", "version"),
        ("msgpack-pure", "msgpack_pure", "version"),
        ("pycrypto", "Crypto", "__version__"),
        ("pycryptodome", "Cryptodome", "version_info"),
        ("PyYAML", "yaml", "__version__"),
        ("PyZMQ", "zmq", "__version__"),
        ("ZMQ", "zmq", "zmq_version"),
        ("Mako", "mako", "__version__"),
        ("Tornado", "tornado", "version"),
        ("timelib", "timelib", "version"),
        ("dateutil", "dateutil", "__version__"),
        ("pygit2", "pygit2", "__version__"),
        ("libgit2", "pygit2", "LIBGIT2_VERSION"),
        ("smmap", "smmap", "__version__"),
        ("cffi", "cffi", "__version__"),
        ("pycparser", "pycparser", "__version__"),
        ("gitdb", "gitdb", "__version__"),
        ("gitpython", "git", "__version__"),
        ("python-gnupg", "gnupg", "__version__"),
        ("docker-py", "docker", "__version__"),
    ]

    if include_salt_cloud:
        libs.append(("Apache Libcloud", "libcloud", "__version__"))

    for name, imp, attr in libs:
        if imp is None:
            yield name, attr
            continue
        try:
            imp = __import__(imp)
            version = getattr(imp, attr)
            if callable(version):
                version = version()
            if isinstance(version, (tuple, list)):
                version = ".".join(map(str, version))
            yield name, version
        except Exception:
            yield name, None


salt.version.salt_information = salt_information
salt.version.dependency_information = dependency_information


class SaltStandaloneProxyOptionParser(
    six.with_metaclass(
        salt.utils.parsers.OptionParserMeta,
        salt.utils.parsers.OptionParser,
        salt.utils.parsers.ConfigDirMixIn,
        salt.utils.parsers.MergeConfigMixIn,
        salt.utils.parsers.TimeoutMixIn,
        salt.utils.parsers.LogLevelMixIn,
        salt.utils.parsers.HardCrashMixin,
        salt.utils.parsers.SaltfileMixIn,
        salt.utils.parsers.ExtendedTargetOptionsMixIn,
        salt.utils.parsers.OutputOptionsMixIn,
        salt.utils.parsers.ArgsStdinMixIn,
        salt.utils.parsers.ProfilingPMixIn,
        salt.utils.parsers.EAuthMixIn,
        salt.utils.parsers.NoParseMixin,
    )
):
    default_timeout = 60

    description = (
        r"""
  ___          _   _       ___   ___
 / __|  __ _  | | | |_    / __| | _ \  _ _   ___  __ __  _  _
 \__ \ / _` | | | |  _|   \__ \ |  _/ | '_| / _ \ \ \ / | || |
 |___/ \__,_| |_|  \__|   |___/ |_|   |_|   \___/ /_\_\  \_, |
                                                         |__/

"""
        "salt-sproxy is a tool to invoke arbitrary Salt functions on a group\n"
        "of (network) devices connecting through a Salt Proxy Minion, without\n"
        "having the Proxy Minion services up and running (or the Master).\n"
    )
    VERSION = salt_sproxy.version.__version__

    usage = "%prog [options] <target> <function> [arguments]"

    epilog = (
        "You can find additional help about %prog at "
        "https://salt-sproxy.readthedocs.io/en/latest/"
    )

    # ConfigDirMixIn config filename attribute
    _config_filename_ = "master"

    # LogLevelMixIn attributes
    _default_logging_level_ = config.DEFAULT_MASTER_OPTS["log_level"]
    _default_logging_logfile_ = config.DEFAULT_MASTER_OPTS["log_file"]

    def format_description(self, formatter):
        return self.description

    def _mixin_setup(self):
        self.add_option(
            "-r", "--roster", default=False, help="The name of the Salt Roster to use."
        )
        self.add_option(
            "--roster-file",
            dest="roster_file",
            help="Absolute path to the Roster file to use.",
        )
        self.add_option(
            "-s",
            "--static",
            default=False,
            action="store_true",
            help=("Return the data from devices as a group after they all return."),
        )
        self.add_option(
            "--async",
            default=False,
            dest="async",
            action="store_true",
            help=("Run the salt-sproxy command but don't wait for a reply."),
        )
        self.add_option(
            "--dont-cache-grains",
            default=False,
            action="store_true",
            help=("Do not cache the collected Grains for the sproxy devices."),
        )
        self.add_option(
            "--dont-cache-pillar",
            default=False,
            action="store_true",
            help=("Do not cache the compiled Pillar for the sproxy devices."),
        )
        self.add_option(
            "--no-cached-grains",
            default=False,
            action="store_true",
            help="Do not use the available cached Grains (if any).",
        )
        self.add_option(
            "--no-cached-pillar",
            default=False,
            action="store_true",
            help="Do not use the available cached Pillar (if any)",
        )
        self.add_option(
            "--no-grains",
            default=False,
            action="store_true",
            help=(
                "Do not attempt to collect Grains at all. Use with care, it "
                "may lead to unexpected results."
            ),
        )
        self.add_option(
            "-i",
            "--ignore-host-keys",
            default=False,
            action="store_true",
            dest="ignore_host_keys",
            help=(
                "By default ssh host keys are honored and connections will ask "
                "for approval. Use this option to disable StrictHostKeyChecking."
            ),
        )
        self.add_option(
            "--no-host-keys",
            default=False,
            action="store_true",
            dest="no_host_keys",
            help=(
                "Fully ignores ssh host keys which by default are honored and "
                "connections would ask for approval. Useful if the host key of "
                "a remote server has changed and would still error with "
                "--ignore-host-keys."
            ),
        )
        self.add_option(
            "--identities-only",
            default=False,
            action="store_true",
            dest="identities_only",
            help=(
                "Use the only authentication identity files configured in the "
                "ssh_config files. See ``IdentitiesOnly`` flag in man ssh_config."
            ),
        )
        self.add_option(
            "--priv",
            dest="priv",
            help=("Specify the SSH private key file to be used for authentication."),
        )
        self.add_option(
            "--priv-passwd",
            dest="priv_passwd",
            help=("Specify the SSH private key file's passphrase when required."),
        )
        self.add_option(
            "--preload-targeting",
            default=False,
            action="store_true",
            help=(
                "Preload Grains for all the devices before targeting."
                "This is useful to match the devices on dynamic Grains that "
                "do not require the connection with the remote device - e.g., "
                "Grains collected from an external API, etc."
            ),
        )
        self.add_option(
            "--invasive-targeting",
            default=False,
            action="store_true",
            help=(
                "Collect all the possible data from every device salt-sproxy "
                "is aware of, before distributing the commands. "
                "In other words, this option tells salt-sproxy to connect to "
                "every possible device defined in the Roster of choice, collect "
                "Grains, compile Pillars, etc., and only then execute the "
                "command against the devices matching the target expression."
                "Use with care, as this may significantly reduce the "
                "performances, but this is the price paid to be able to target "
                "using device properties. "
                "Consider using this option in conjunction with --cache-grains "
                "and / or --cache-pillar to cache the Grains and the Pillars to "
                "re-use them straight away next time."
            ),
        )
        self.add_option(
            "--no-pillar",
            default=False,
            action="store_true",
            help=(
                "Do not compile Pillar at all. Use with care, it may lead to "
                "unexpected results."
            ),
        )
        self.add_option(
            "-b",
            "--batch",
            "--batch-size",
            dest="batch_size",
            help=(
                "The number of devices to connect to in parallel. "
                "Default: {} (number of CPUs on your machine)".format(CPU_COUNT)
            ),
        )
        self.add_option(
            "--preview-target",
            dest="preview_target",
            action="store_true",
            help="Show the devices expected to match the target.",
        )
        self.add_option(
            "--sync-all",
            dest="sync_all",
            action="store_true",
            help=(
                "Load the all extension modules provided with salt-sproxy, as "
                "well as the extension modules from your own environment."
            ),
        )
        self.add_option(
            "--sync-grains",
            dest="sync_grains",
            action="store_true",
            help=(
                "Re-sync the Grains modules. Useful if you have custom Grains "
                "modules in your own environment."
            ),
        )
        self.add_option(
            "--sync-modules",
            dest="sync_modules",
            action="store_true",
            help=("Load the salt-sproxy Execution modules."),
        )
        self.add_option(
            "--sync-roster",
            dest="sync_roster",
            action="store_true",
            help=(
                "Synchronise the Roster modules (both salt-sproxy native "
                "and provided by the user in their own environment)."
            ),
        )
        self.add_option(
            "--sync-proxy",
            dest="sync_proxy",
            action="store_true",
            help=("Load the salt-sproxy Proxy modules."),
        )
        self.add_option(
            "--sync-executors",
            dest="sync_executors",
            action="store_true",
            help=("Load the salt-sproxy Executor modules."),
        )
        self.add_option(
            "--saltenv",
            dest="saltenv_cli",
            help="The Salt environment name to load module and files from",
        )
        self.add_option(
            "--events",
            dest="events",
            action="store_true",
            help=(
                "Whether should put the events on the Salt bus (mostly "
                "useful when having a Master running)."
            ),
        )
        self.add_option(
            "--use-proxy",
            "--use-existing-proxy",
            "--use-existing-minion",
            dest="use_existing_proxy",
            action="store_true",
            help=(
                "Use the existing Proxy Minions to execute the commands, "
                "whenever available."
            ),
        )
        self.add_option(
            "--pillar-root",
            default=None,
            help="Set this directory as the base pillar root.",
        )
        self.add_option(
            "--file-root",
            default=None,
            help="Set this directory as the base file root.",
        )
        self.add_option(
            "--states-dir",
            default=None,
            help="Set this directory to search for additional states.",
        )
        self.add_option(
            "-m",
            "--module-dirs",
            dest="module_dirs_cli",
            default=[],
            action="append",
            help=(
                "Specify an additional directory to pull modules from. "
                "Multiple directories can be provided by passing "
                "`-m/--module-dirs` multiple times."
            ),
        )
        self.add_option(
            "--installation-path",
            dest="installation_path",
            action="store_true",
            help=("Display the absolute path to where salt-sproxy is installed."),
        )
        self.add_option(
            "--display-file-roots",
            dest="display_file_roots",
            action="store_true",
            help=(
                "Display the file_roots option you would need to configure "
                "in order to use the salt-sproxy extension modules directly, "
                "and, implicitly, leverage the event-driven methodology and "
                "the Salt REST API."
            ),
        )
        self.add_option(
            "--save-file-roots",
            dest="save_file_roots",
            action="store_true",
            help=(
                "Saves the file_roots configuration so you can start "
                "leveraging the event-driven automation and the Salt REST API."
            ),
        )
        self.add_option(
            "--config-dump",
            dest="config_dump",
            default=False,
            action="store_true",
            help="Dump the salt-sproxy configuration values",
        )
        self.add_option(
            "--no-connect",
            dest="no_connect",
            action="store_true",
            default=False,
            help=(
                "Do not initiate the connection with the device, only use "
                "cached data to compile data and execute Salt functions that "
                "do not require the actual connection with the device."
            ),
        )
        self.add_option(
            "--test-ping",
            dest="test_ping",
            action="store_true",
            default=False,
            help=(
                "When using together with --use-existing-proxy, this option can"
                " help to ensure the existing Proxy Minion is responsive (not "
                "only up and running, by executing a ping test."
            ),
        )
        self.add_option(
            "--failhard",
            dest="failhard",
            action="store_true",
            default=False,
            help="Stop execution at the first execution error.",
        )
        self.add_option(
            "--target-cache",
            dest="target_cache",
            action="store_true",
            default=False,
            help=("Cache the list of devices matched by your target expression."),
        )
        group = self.output_options_group = optparse.OptionGroup(
            self, "Output Options", "Configure your preferred output format."
        )
        self.add_option_group(group)

        group.add_option(
            "-q",
            "--quiet",
            default=False,
            action="store_true",
            help="Do not display the results of the run.",
        )
        self.add_option(
            "--summary",
            default=False,
            action="store_true",
            help="Display salt execution summary information.",
        )
        self.add_option(
            "-v",
            "--verbose",
            default=False,
            action="store_true",
            help="Turn on command verbosity, display jid and detailed summary.",
        )
        self.add_option(
            "--show-jid",
            default=False,
            action="store_true",
            help="Display jid without the additional output of --verbose.",
        )
        self.add_option(
            "--hide-timeout",
            default=False,
            action="store_true",
            help="Hide devices that timeout.",
        )
        self.add_option(
            "--batch-wait",
            default=0,
            type=float,
            help=(
                "Wait the specified time in seconds after each batch is done"
                "before executing the next one."
            ),
        )
        self.add_option(
            "-p",
            "--progress",
            default=False,
            action="store_true",
            help="Display a progress graph.",
        )
        self.add_option(
            "--return",
            dest="returner",
            default="",
            metavar="RETURNER",
            help=(
                "The name of the Returner module to use for sending data to "
                "various external systems."
            ),
        )
        self.add_option(
            "--return-config",
            dest="returner_config",
            default="",
            metavar="RETURNER_CONF",
            help="Specify an alternative Returner config.",
        )
        self.add_option(
            "--return-kwargs",
            dest="returner_kwargs",
            default={},
            metavar="RETURNER_KWARGS",
            help="Set Returner options at the command line.",
        )
        self.add_option(
            "-d",
            "--doc",
            "--documentation",
            dest="doc",
            default=False,
            action="store_true",
            help=(
                "Return the documentation for the specified module or for "
                "all modules if none are specified."
            ),
        )

    # Everything else that follows here is verbatim copy from
    # https://github.com/saltstack/salt/blob/develop/salt/utils/parsers.py
    def _mixin_after_parsed(self):
        if (
            self.options.display_file_roots
            or self.options.installation_path
            or self.options.save_file_roots
            or self.options.config_dump
        ):
            # Insert dummy arg when displaying the file_roots
            self.args.append("not_a_valid_target")
            self.args.append("not_a_valid_command")
        elif self.options.doc:
            if len(self.args) == 1:
                self.args.insert(0, "not_a_valid_target")
            elif len(self.args) == 0:
                self.args.append("not_a_valid_target")
                self.args.append("*")

        if self.options.list:
            try:
                if "," in self.args[0]:
                    self.config["tgt"] = self.args[0].replace(" ", "").split(",")
                else:
                    self.config["tgt"] = self.args[0].split()
            except IndexError:
                self.exit(42, "\nCannot execute command without defining a target.\n\n")
        else:
            try:
                self.config["tgt"] = self.args[0]
            except IndexError:
                self.exit(42, "\nCannot execute command without defining a target.\n\n")

        if self.options.preview_target:
            # Insert dummy arg which won't be used
            self.args.append("not_a_valid_command")

        # Detect compound command and set up the data for it
        if self.args:
            try:
                if "," in self.args[1]:
                    self.config["fun"] = self.args[1].split(",")
                    self.config["arg"] = [[]]
                    cmd_index = 0
                    if (
                        self.args[2:].count(self.options.args_separator)
                        == len(self.config["fun"]) - 1
                    ):
                        # new style parsing: standalone argument separator
                        for arg in self.args[2:]:
                            if arg == self.options.args_separator:
                                cmd_index += 1
                                self.config["arg"].append([])
                            else:
                                self.config["arg"][cmd_index].append(arg)
                    else:
                        # old style parsing: argument separator can be inside args
                        for arg in self.args[2:]:
                            if self.options.args_separator in arg:
                                sub_args = arg.split(self.options.args_separator)
                                for sub_arg_index, sub_arg in enumerate(sub_args):
                                    if sub_arg:
                                        self.config["arg"][cmd_index].append(sub_arg)
                                    if sub_arg_index != len(sub_args) - 1:
                                        cmd_index += 1
                                        self.config["arg"].append([])
                            else:
                                self.config["arg"][cmd_index].append(arg)
                        if len(self.config["fun"]) > len(self.config["arg"]):
                            self.exit(
                                42,
                                "Cannot execute compound command without "
                                "defining all arguments.\n",
                            )
                        elif len(self.config["fun"]) < len(self.config["arg"]):
                            self.exit(
                                42,
                                "Cannot execute compound command with more "
                                "arguments than commands.\n",
                            )
                    # parse the args and kwargs before sending to the publish
                    # interface
                    for i in range(len(self.config["arg"])):
                        self.config["arg"][i] = salt.utils.args.parse_input(
                            self.config["arg"][i], no_parse=self.options.no_parse
                        )
                else:
                    self.config["fun"] = self.args[1]
                    self.config["arg"] = self.args[2:]
                    # parse the args and kwargs before sending to the publish
                    # interface
                    self.config["arg"] = salt.utils.args.parse_input(
                        self.config["arg"], no_parse=self.options.no_parse
                    )
            except IndexError:
                self.exit(42, "\nIncomplete options passed.\n\n")

    def setup_config(self):
        defaults = config.DEFAULT_MASTER_OPTS.copy()
        defaults["timeout"] = 60
        return config.client_config(self.get_config_file_path(), defaults=defaults)
