#!/usr/bin/env python
#
# Commit: $Format:%H$
# Date:   $Format:%cD$

import os
import sys
import glob
import argparse
import shutil
import time
import Queue
from datetime import datetime
from subprocess import check_call

from termcolor import colored
from script import Script

#
# For now, we are using the git SHA as the version number for inspector, with the plan
# that eventually this evolve to a regular n.n.n version number once we have inspector
# on a formal release train.
#
# Note that the Format variable will be replaced with the git SHA whenever a ZIP is created
# with the git-archive command.
#
VERSION = "$Format:%H$"
PATH = os.path.dirname(os.path.abspath(__file__))
SCRIPTS = os.environ.get("ZI_SCRIPTS") or os.path.join(PATH, "scripts")
RESULTS = os.environ.get("ZI_RESULTS") or "inspected-%s" % datetime.today().isoformat().replace(":", "-")
LOGFILE = os.path.join(RESULTS, "inspector.log")

def log(msg, color=None, attrs=None, end=True, tty=True):
    # Logic here could be cleaner, but I'm in a hurry.
    if sys.stdout.isatty():
        fmtd = colored(msg, color, attrs=attrs)
    else:
        fmtd = msg
    if end and tty:
        print fmtd
    elif tty:
        print fmtd,
    with open(LOGFILE, 'a') as f:
        if end:
            msg += "\n"
        f.write(msg.encode("utf-8"))
    sys.stdout.flush()

def get_scripts():
    paths = glob.glob(os.path.join(SCRIPTS, "*"))
    return [Script(s) for s in paths if os.path.isfile(s)]

def get_args():
    parser = argparse.ArgumentParser(description="Inspect a Zenoss installation.")
    parser.add_argument("-v", "--version", action="store_true", default=False, help="Print version and exit.")
    parser.add_argument("--print-tags", action="store_true", default=False, help="Print tags and exit.")
    parser.add_argument("--no-remove", action="store_true", default=False,
                        help="Don't remove working directory.")
    parser.add_argument("--no-save", action="store_true", default=False,
                        help="Don't compress results.")
    parser.add_argument("-j", "--jobs", default=16, type=int, help="Maximum number of jobs to run in parallel. Default is 16.")
    parser.add_argument("-t", "--timeout", default=300, type=int, help="Timeout in seconds for entire inspection. Set to 0 to wait forever. Default is 300.")
    parser.add_argument("-l", "--lines", default=100000, type=int, help="Limit output of each result file to LINES lines. Set to 0 to collect all lines. Default is 100000.")
    tags = parser.add_mutually_exclusive_group()
    tags.add_argument("-w", "--whitelist", nargs="+", help="Use only these tags.")
    tags.add_argument("-b", "--blacklist", nargs="+", help="Don't use these tags.")
    return parser.parse_args()

# Get a unique, sorted list of tags from all scripts
def get_tags(scripts):
    allTags = []
    for s in scripts:
        if s.tags:
            allTags.extend(s.tags)
    uniqueTags = set(allTags)
    tags = list(uniqueTags)
    tags.sort()
    return tags

# Log an error if any tag in tagList is NOT found in allTags
def invalid_tags_found(description, tagList, allTags):
    if not tagList:
        return False
    err = False
    for tag in tagList:
        if tag in allTags:
            continue
        err = True
        log("ERROR: %s tag %s is not defined" % (description, tag))

    return err

def main():
    args = get_args()
    if args.version:
        print VERSION
        return

    # Create the results directory so we can write log messages from this point on
    os.mkdir(RESULTS)

    scripts = get_scripts()
    if len(scripts) == 0:
        log("ERROR: no scripts found in %s" % SCRIPTS)
        return

    allTags = get_tags(scripts)
    if args.print_tags:
        log("Found these tags = [%s]" % ', '.join(allTags))
        return

    # Bail if someone specified invalid tags; otherwise they may incorrectly
    #    assume that something happened for the wrong reason
    if invalid_tags_found("whitelist", args.whitelist, allTags) or invalid_tags_found("blacklist", args.blacklist, allTags):
        return

    if args.whitelist:
        scripts = [s for s in scripts if any(t in args.whitelist for t in s.tags)]
    elif args.blacklist:
        scripts = [s for s in scripts if not any(t in args.blacklist for t in s.tags)]

    if len(scripts) == 0:
        if args.blacklist:
            log("ERROR: no scripts to run after applying the blacklist [%s]" % ', '.join(args.blacklist))
        return

    log("Starting inspector version %s" % VERSION, "blue", attrs=["bold"])
    if args.whitelist:
        log("Using whitelist of tags: [%s]" % ', '.join(args.whitelist))
    elif args.blacklist:
        log("Using blacklist of tags: [%s]" % ', '.join(args.blacklist))

    to_run = scripts
    have_run = []
    running = []

    last_len = None
    last_wait_indicate = time.time()
    start_time = time.time()

    queue = Queue.Queue()
    padding = max([len(s.basename) for s in scripts]) + 4
    while len(to_run) + len(running) > 0:

        if args.timeout > 0 and time.time() - start_time >= args.timeout:
            cancelled = ' '.join(s.basename for s in running)
            norun = ' '.join(s.basename for s in to_run)
            log("Timed out after %d seconds." % args.timeout, "red", attrs=["bold"])
            if len(cancelled) > 0:
                log("The following scripts were cancelled: %s" % cancelled, "red")
            if len(norun) > 0:
                log("The following scripts never got a chance to start: %s" % norun, "red")
            break

        for script in to_run[:]:

            if args.jobs > 0 and len(running) >= args.jobs:
                break

            # Make a list of all scripts that have or might resolve.
            available = have_run + to_run + running

            # If the deps for this script will never be met, remove it from the list and continue
            # to the next.
            available_names = [s.basename for s in available]
            if not all(d in available_names for d in script.deps):
                log("Skipping script %s because one or more its dependencies are not available" % script.basename)
                to_run.remove(script)
                continue

            # If we haven't met the deps for this script yet, continue to the next one.
            completed_names = [s.basename for s in have_run]
            if not all(d in completed_names for d in script.deps):
                continue

            # All deps met, let's run this script. No matter what happens next, we don't want this
            # in our to-exectute script list anymore.
            to_run.remove(script)

            # Run the script.
            script.run(RESULTS, queue)
            running.append(script)

        # Detect circular dependencies.
        if len(to_run) == last_len and len(running) == 0:
            log("Circular dependency detected, aborting.", "red", attrs=["bold"])
            # Sort the names for test predictability
            names = [s.basename for s in scripts]
            names.sort()
            log(' '.join(names), "red")
            break
        last_len = len(to_run)

        # Don't spin.
        if queue.empty():
            time.sleep(0.1)

        # Handle any available results.
        while not queue.empty():
            result = queue.get()
            script = result["script"]
            retcode = result["retcode"]
            running.remove(script)
            if script.info:
                stdout = result["stdout"]
                stderr = result["stderr"]
                if len(stdout) > 0:
                    log(script.basename, "red", attrs=["bold"])
                    log(stdout.strip(), "red")
                if len(stderr) > 0:
                    log("Error running %s" % script.basename, "red", attrs=["bold"])
                    log(stderr.strip(), "red")
            if retcode == 0:
                log(script.basename.ljust(padding) + "done", "blue")
                have_run.append(script)
            else:
                log(script.basename.ljust(padding) + "X", "red")
            last_wait_indicate = time.time()

        if time.time() - last_wait_indicate > 5.0:
            if (args.timeout > 0):
                delta = args.timeout - (time.time() - start_time)
                log("Waiting %d seconds for %s..." % (delta, ' '.join(s.basename for s in running)), "white")
            else:
                log("Waiting forever for %s..." % ' '.join(s.basename for s in running), "white")
            last_wait_indicate = time.time()

    if args.lines > 0:
        log("Limiting result logs to %d lines." % args.lines, "blue", attrs=["bold"])
        paths = glob.glob(os.path.join(RESULTS, "*"))
        for p in  [s for s in paths if os.path.isfile(s)]:
            if p.endswith('inspector.log'):
                continue
            os.system("tail -n%d %s > inspector_temp_file" % (args.lines, p))
            os.system("mv inspector_temp_file %s" % p)

    if not args.no_save:
        log("Compressing results: %s" % RESULTS + ".tar.gz", "blue", attrs=["bold"])
        check_call(["tar", "-czf", RESULTS + ".tar.gz", RESULTS])

    if not args.no_remove:
        log("Cleaning up...", "blue", attrs=["bold"])
        shutil.rmtree(RESULTS)

    print "Done.\n"

if __name__ == "__main__":
    main()
