#!/usr/bin/env python

""" Command-line interface to using GSH."""

import argparse
from argparse import RawDescriptionHelpFormatter
import logging
import sys
import warnings
warnings.simplefilter("ignore")

from gsh import Gsh
from gsh import __version__
from gsh.plugin import get_loaders, get_hooks, get_executors
from gsh.config import Config


def _create_loader_action(plugin):
    """ Used to pass a plugin into a custom Action via a closure."""
    class _LoaderAction(argparse.Action):
        """ Custom Action for argparse to grab loaders by name."""
        def __call__(self, parser, namespace, values, option_string=None):
            if not getattr(namespace, self.dest, None):
                setattr(namespace, self.dest, {})
            loaders = getattr(namespace, self.dest)
            loaders.setdefault(plugin, []).append(values)
    return _LoaderAction


def _setup_loader_options(parser, plugins):
    """ Dynamically add options based on what loaders exist."""
    loader_group = parser.add_argument_group("Loaders")
    for plugin in plugins:
        args = []

        if plugin.opt_short:
            args.append(plugin.opt_short)
        if plugin.opt_long:
            args.append(plugin.opt_long)

        if args and plugin.opt_help and isinstance(plugin.opt_help, basestring):
            loader_group.add_argument(
                *args, default={}, dest="loaders", metavar=plugin.opt_metavar,
                action=_create_loader_action(plugin), nargs=plugin.opt_nargs,
                help=plugin.opt_help)


def _get_specified_hooks(plugins, specified_hooks):
    """ Given a list of specified hook names, return a set of hooks.

    Args:
        plugins: An iterable of available hook type plugins.
        specified_hooks: An interable of hook names in argument form.

    Returns:
        A set of hooks that were specified.

    """
    hooks = set()
    for hook in specified_hooks:
        hook = getattr(plugins, _arg_to_plugin(hook, "Hook"), None)
        if hook is not None:
            hooks.add(hook)
    return hooks


def _arg_to_plugin(string, suffix):
    """ Transforms name from argument form to plugin form."""
    return "%s%s" % (string.title().replace("_", ""), suffix)


def _plugin_to_arg(plugin, suffix):
    """ Returns an argument name from a plugin."""
    name = plugin.__name__
    new_string = [name[0].lower()]
    for char in name[1:-len(suffix)]:
        if char.isupper():
            new_string.append("_")
        new_string.append(char.lower())
    return "".join(new_string)


def main():

    config = Config()
    config.load_default_files()

    description_msg = "Run a command across many machines."
    parser = argparse.ArgumentParser(description=description_msg, add_help=False,
                                     formatter_class=RawDescriptionHelpFormatter)

    # Do a first pass of parsing where we ignore unknown options so that we can
    # make use of options which will discover plugins and potentially load more
    # options.
    parser.add_argument("-p", "--plugin_dirs", default=[], action="append",
                        help="Directories containing additional plugins.")
    args, unknown = parser.parse_known_args()
    config.update_from_args(args)

    # Delay parsing of most options until the second pass due to a bug in argparse that
    # treats -abc differently from -a -b -c when parsing unknown args.
    parser.add_argument("command", nargs=argparse.REMAINDER, default=None,
                        help="Command to execute remotely.")
    parser.add_argument("-t", "--timeout", default=None, type=int,
                        help="How long to allow a command to run before timeout.")
    parser.add_argument("-F", "--forklimit", default=None,
                        help="Limit on concurrenct processes.")

    parser.add_argument("-M", "--show-machine-names", dest="print_machines",
                        action="store_true", default=None,
                        help="Prepend the hostname to output.")
    parser.add_argument("-H", "--hide-machine-names", dest="print_machines",
                        action="store_false", default=None,
                        help="Do not prepend hostname to output.")

    parser.add_argument("-P", "--print-output", dest="print_output",
                        action="store_true", default=None,
                        help="Print output from the commands executed.")
    parser.add_argument("-N", "--no-print-output", dest="print_output",
                        action="store_false", default=None,
                        help="Don't print output from the commands executed.")

    parser.add_argument("--show-percent", dest="show_percent",
                        action="store_true", default=None,
                        help="Prepend percent complete to output.")
    parser.add_argument("--hide-percent", dest="show_percent",
                        action="store_false", default=None,
                        help="Do not prepend percent complete to output.")


    parser.add_argument("-c", "--concurrent-shell", dest="concurrent",
                        action="store_true", default=None,
                        help="Execute commands concurrently.")
    parser.add_argument("-w", "--wait-shell", dest="concurrent",
                        action="store_false", default=None,
                        help="Force sequentially execution.")

    # Not supporting backwards compatibility with -o because of the
    # complication it adds to argument parsing.
    parser.add_argument("--remoteshellopt", action="append", default=[],
                        help="Pass options to SSH (SSHExecutor Only).")

    parser.add_argument("-V", "--version", action="store_true", default=False,
                        help="Display version information.")

    # We specify add_help=False and explicitly define it here so it gets picked up in the second
    # pass of parsing and displays the loader plugin help.
    parser.add_argument("-h", "--help", action='help', default=argparse.SUPPRESS,
                        help="show this help message and exit")

    _setup_loader_options(parser, get_loaders(config.plugin_dirs))

    hooks = get_hooks(config.plugin_dirs)
    avail_hooks = ", ".join(set([_plugin_to_arg(hook, "Hook") for hook in hooks if hook.show_cli]))
    hooks_group = parser.add_argument_group("Hooks", description=(
        "Loaded Hooks: %s\n"
        "Available Hooks: %s\n"
        % (", ".join(config.hooks), avail_hooks)
    ))
    hooks_group.add_argument("--hooks", default=[], action="append",
                             help="Hooks to execute during run-time.")

    executors = get_executors(config.plugin_dirs)
    avail_executors = ", ".join(set([_plugin_to_arg(executor, "Executor") for executor in executors]))
    executors_group = parser.add_argument_group("Executors", description=(
        "Executors behave as the layer for doing backend execution.\n"
        "Currently only the ssh executor is fully support, though other\n"
        "experimental executors exist, specifically for working with\n"
        "password-based and interactive shells. This executors will change\n"
        "often.\n\n"
        "Executor's can be passed positional and keyword arguments via\n"
        "the command line using the syntax: executor:arg1,arg2,kwarg1=foo\n\n"
        "Loaded Executor: %s\n"
        "Available Executors: %s"
        % (config.executor, avail_executors)
    ))
    executors_group.add_argument("-e", "--executor", default=None,
                                 help="Which execution plugin to use.")

    args = parser.parse_args()
    config.update_from_args(args)

    if args.version:
        print "Gary's Shell / Version: %s" % __version__
        sys.exit()

    if not args.loaders:
        parser.print_help()
        sys.exit()

    hosts = set()
    for plugin, options in args.loaders.iteritems():
        hosts.update(plugin(*options))

    command = args.command

    if len(command) == 1 and command[0] == "-":
        command = [sys.stdin.read()]

    if not command or not any(command):
        if not sys.stdin.isatty():
            command = [sys.stdin.read()]
        else:
            if hosts:
                print "\n".join(hosts)
            sys.exit()


    specified_hooks = _get_specified_hooks(hooks, config.hooks)
    specified_hooks = [hook() for hook in specified_hooks]

    printer_config = {
        "prepend_host": config.print_machines,
        "show_percent": config.show_percent,
    }

    if config.print_output:
        specified_hooks.append(hooks.PrinterHook(**printer_config))

    forklimit = config.forklimit
    if not config.concurrent:
        forklimit = 1

    executor = getattr(executors, _arg_to_plugin(config.executor, "Executor"))

    try:
        gsh = Gsh(hosts, command, fork_limit=forklimit,
                  timeout=config.timeout, hooks=specified_hooks,
                  executor=executor(config.executor_args, config.executor_kwargs))
        gsh.run_async()
        sys.exit(gsh.wait())
    except KeyboardInterrupt:
        sys.exit("Bye")


if __name__ == "__main__":
    logging.basicConfig()
    main()
