#!/usr/bin/env python

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

import argparse
import logging
import sys

from gsh import Gsh
from gsh import __version__
from gsh.plugin import get_loaders, get_hooks
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), 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, _hook_arg_to_plugin(hook), None)
        if hook is not None:
            hooks.add(hook)
    return hooks


def _hook_arg_to_plugin(string):
    """ Transforms hook name from argument form to plugin form."""
    return "%sHook" % string.title().replace("_", "")


def _hook_plugin_to_arg(plugin):
    """ Returns a hook argument name from a hook plugin."""
    name = plugin.__name__
    new_string = [name[0].lower()]
    for char in name[1:-4]:
        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)

    # 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("-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.")

    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")

    hooks = get_hooks(config.plugin_dirs)
    _setup_loader_options(parser, get_loaders(config.plugin_dirs))

    parser.add_argument("--hooks", default=[], action="append",
                        help=("Hooks to execute during run-time. Available hooks: %s" %
                              ", ".join([_hook_plugin_to_arg(hook) for hook in hooks])
                        ))

    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))

    if not args.command or not any(args.command):
        if hosts:
            print "\n".join(hosts)
        sys.exit()

    specified_hooks = _get_specified_hooks(hooks, config.hooks)
    printer = hooks.PrinterHook
    if config.print_machines:
        printer = hooks.MachinePrinterHook
    specified_hooks.add(printer)
    specified_hooks = [hook() for hook in specified_hooks]

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

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


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