# Copyright 2009-2014 Justin Riley
#
# This file is part of TethysCluster.
#
# TethysCluster is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# TethysCluster is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with TethysCluster. If not, see <http://www.gnu.org/licenses/>.

"""
TethysCluster Command Line Interface:

tethyscluster [global-opts] action [action-opts] [<action-args> ...]
"""
import os
import sys
import shlex
import socket
import optparse
import platform

from boto.exception import BotoServerError, EC2ResponseError, S3ResponseError

from tethyscluster import config
from tethyscluster import static
from tethyscluster import logger
from tethyscluster import commands
from tethyscluster import exception
from tethyscluster import completion
from tethyscluster.logger import log, console
from tethyscluster import __version__

__description__ = """
TethysCluster - (https://github.com/CI-WATER/TethysCluster/wiki) (v. %s)
An adaptation of StarCluster (http://star.mit.edu/cluster)
""" % __version__


class TethysClusterCLI(object):
    """
    TethysCluster Command Line Interface
    """
    def __init__(self):
        self._gparser = None
        self.subcmds_map = {}

    @property
    def gparser(self):
        if not self._gparser:
            self._gparser = self.create_global_parser()
        return self._gparser

    def print_header(self):
        print >> sys.stderr, __description__.replace('\n', '', 1)

    def parse_subcommands(self, gparser=None):
        """
        Parse global arguments, find subcommand from list of subcommand
        objects, parse local subcommand arguments and return a tuple of
        global options, selected command object, command options, and
        command arguments.

        Call execute() on the command object to run. The command object has
        members 'gopts' and 'opts' set for global and command options
        respectively, you don't need to call execute with those but you could
        if you wanted to.
        """
        gparser = gparser or self.gparser
        # parse global options.
        gopts, args = gparser.parse_args()
        if not args:
            gparser.print_help()
            raise SystemExit("\nError: you must specify an action.")
        # set debug level if specified
        if gopts.DEBUG:
            console.setLevel(logger.DEBUG)
            config.DEBUG_CONFIG = True
        # load TethysClusterConfig into global options
        try:
            cfg = config.TethysClusterConfig(gopts.CONFIG)
            cfg.load()
        except exception.ConfigNotFound, e:
            log.error(e.msg)
            e.display_options()
            sys.exit(1)
        except exception.ConfigError, e:
            log.error(e.msg)
            sys.exit(1)
        gopts.CONFIG = cfg
        # Parse command arguments and invoke command.
        subcmdname, subargs = args[0], args[1:]
        try:
            sc = self.subcmds_map[subcmdname]
            lparser = optparse.OptionParser(sc.__doc__.strip())
            sc.gopts = gopts
            sc.parser = lparser
            sc.gparser = gparser
            sc.subcmds_map = self.subcmds_map
            sc.addopts(lparser)
            sc.opts, subsubargs = lparser.parse_args(subargs)
        except KeyError:
            raise SystemExit("Error: invalid command '%s'" % subcmdname)
        return gopts, sc, sc.opts, subsubargs

    def create_global_parser(self, subcmds=None, no_usage=False,
                             add_help=True):
        if no_usage:
            gparser = optparse.OptionParser(usage=optparse.SUPPRESS_USAGE,
                                            add_help_option=add_help)
        else:
            gparser = optparse.OptionParser(__doc__.strip(),
                                            version=__version__,
                                            add_help_option=add_help)
            # Build map of name -> command and docstring.
            cmds_header = 'Available Commands:'
            gparser.usage += '\n\n%s\n' % cmds_header
            gparser.usage += '%s\n' % ('-' * len(cmds_header))
            gparser.usage += "NOTE: Pass --help to any command for a list of "
            gparser.usage += 'its options and detailed usage information\n\n'
            subcmds = subcmds or commands.all_cmds
            for sc in subcmds:
                helptxt = sc.__doc__.splitlines()[3].strip()
                gparser.usage += '- %s: %s\n' % (', '.join(sc.names), helptxt)
                for n in sc.names:
                    assert n not in self.subcmds_map
                    self.subcmds_map[n] = sc
        gparser.add_option("-d", "--debug", dest="DEBUG",
                           action="store_true", default=False,
                           help="print debug messages (useful for "
                           "diagnosing problems)")
        gparser.add_option("-c", "--config", dest="CONFIG", action="store",
                           metavar="FILE",
                           help="use alternate config file (default: %s)" %
                           static.TETHYSCLUSTER_CFG_FILE)
        gparser.add_option("-r", "--region", dest="REGION", action="store",
                           help="specify a region to use (default: us-east-1)")
        gparser.disable_interspersed_args()
        return gparser

    def __write_module_version(self, modname, fp):
        """
        Write module version information to a file
        """
        try:
            mod = __import__(modname)
            fp.write("%s: %s\n" % (mod.__name__, mod.__version__))
        except Exception, e:
            print "error getting version for '%s' module: %s" % (modname, e)

    def bug_found(self):
        """
        Builds a crash-report when TethysCluster encounters an unhandled
        exception. Report includes system info, python version, dependency
        versions, and a full debug log and stack-trace of the crash.
        """
        dashes = '-' * 10
        header = dashes + ' %s ' + dashes + '\n'
        crashfile = open(static.CRASH_FILE, 'w')
        argv = sys.argv[:]
        argv[0] = os.path.basename(argv[0])
        argv = ' '.join(argv)
        crashfile.write(header % "SYSTEM INFO")
        crashfile.write("TethysCluster: %s\n" % __version__)
        crashfile.write("Python: %s\n" % sys.version.replace('\n', ' '))
        crashfile.write("Platform: %s\n" % platform.platform())
        dependencies = ['boto', 'paramiko', 'Crypto']
        for dep in dependencies:
            self.__write_module_version(dep, crashfile)
        crashfile.write("\n" + header % "CRASH DETAILS")
        crashfile.write('Command: %s\n\n' % argv)
        for line in logger.get_session_log():
            crashfile.write(line)
        crashfile.close()
        print
        log.error("Oops! Looks like you've found a bug in TethysCluster")
        log.error("Crash report written to: %s" % static.CRASH_FILE)
        # log.error("Please remove any sensitive data from the crash report")
        # log.error("and submit it to tethyscluster@mit.edu")
        sys.exit(1)

    def get_global_opts(self):
        """
        Parse and return global options. This method will silently return None
        if any errors are encountered during parsing.
        """
        gparser = self.create_global_parser(no_usage=True, add_help=False)
        try:
            sys.stdout = open(os.devnull, 'w')
            sys.stderr = open(os.devnull, 'w')
            gopts, _ = gparser.parse_args()
            return gopts
        except SystemExit:
            pass
        finally:
            sys.stdout = sys.__stdout__
            sys.stderr = sys.__stderr__

    def is_completion_active(self):
        return 'OPTPARSE_AUTO_COMPLETE' in os.environ

    def _init_completion(self):
        """
        Restore original sys.argv from COMP_LINE in the case that tethyscluster
        is being called by Bash/ZSH for completion options. Bash/ZSH will
        simply call 'tethyscluster' with COMP_LINE environment variable set to
        the current (partial) argv for completion.

        TethysCluster's Bash/ZSH completion code needs to read the global config
        option in case an alternate config is specified at the command line
        when completing options. TethysCluster's completion code uses the config
        to generate completion options. Setting sys.argv to $COMP_LINE in this
        case allows the global option parser to be used to extract the global
        -c option (if specified) and load the proper config in the completion
        code.
        """
        if 'COMP_LINE' in os.environ:
            newargv = shlex.split(os.environ.get('COMP_LINE'))
            for i, arg in enumerate(newargv):
                arg = os.path.expanduser(arg)
                newargv[i] = os.path.expandvars(arg)
            sys.argv = newargv

    def handle_completion(self):
        if self.is_completion_active():
            gparser = self.create_global_parser(no_usage=True, add_help=False)
            # set sys.path to COMP_LINE if it exists
            self._init_completion()
            # fetch the global options
            gopts = self.get_global_opts()
            # try to load TethysClusterConfig into global options
            if gopts:
                try:
                    cfg = config.TethysClusterConfig(gopts.CONFIG)
                    cfg.load()
                except exception.ConfigError:
                    cfg = None
                gopts.CONFIG = cfg
            scmap = {}
            for sc in commands.all_cmds:
                sc.gopts = gopts
                for n in sc.names:
                    scmap[n] = sc
            listcter = completion.ListCompleter(scmap.keys())
            subcter = completion.NoneCompleter()
            completion.autocomplete(gparser, listcter, None, subcter,
                                    subcommands=scmap)
            sys.exit(1)

    def main(self):
        """
        TethysCluster main
        """
        # Handle Bash/ZSH completion if necessary
        self.handle_completion()
        # Show TethysCluster header
        self.print_header()
        # Parse subcommand options and args
        gopts, sc, opts, args = self.parse_subcommands()
        if args and args[0] == 'help':
            # make 'help' subcommand act like --help option
            sc.parser.print_help()
            sys.exit(0)
        # run the subcommand and handle exceptions
        try:
            sc.execute(args)
        except (EC2ResponseError, S3ResponseError, BotoServerError), e:
            log.error("%s: %s" % (e.error_code, e.error_message),
                      exc_info=True)
            sys.exit(1)
        except socket.error, e:
            log.exception("Connection error:")
            log.error("Check your internet connection?")
            sys.exit(1)
        except exception.ThreadPoolException, e:
            log.error(e.format_excs())
            self.bug_found()
        except exception.ClusterDoesNotExist, e:
            cm = gopts.CONFIG.get_cluster_manager()
            cls = ''
            try:
                cls = cm.get_clusters(load_plugins=False, load_receipt=False)
            except:
                log.debug("Error fetching cluster list", exc_info=True)
            log.error(e.msg)
            if cls:
                taglist = ', '.join([c.cluster_tag for c in cls])
                active_clusters = "(active clusters: %s)" % taglist
                log.error(active_clusters)
            sys.exit(1)
        except exception.BaseException, e:
            log.error(e.msg, extra={'__textwrap__': True})
            log.debug(e.msg, exc_info=True)
            sys.exit(1)
        except SystemExit:
            # re-raise SystemExit to avoid the bug-catcher below
            raise
        except Exception:
            log.error("Unhandled exception occured", exc_info=True)
            self.bug_found()


def warn_debug_file_moved():
    old_file = os.path.join(static.TMP_DIR, 'tethyscluster-debug-%s.log' %
                            static.CURRENT_USER)
    if os.path.exists(old_file):
        stars = '*' * 50
        log.warn(stars)
        log.warn("The default log file location is now:")
        log.warn("")
        log.warn(static.DEBUG_FILE)
        log.warn("")
        log.warn("Please delete or move the old log file located at:")
        log.warn("")
        log.warn(old_file)
        log.warn(stars)


def main():
    try:
        static.create_tc_config_dirs()
        logger.configure_sc_logging()
        warn_debug_file_moved()
        TethysClusterCLI().main()
    except KeyboardInterrupt:
        print "Interrupted, exiting."
        sys.exit(1)

if __name__ == '__main__':
    main()
