#!/usr/bin/python
#
# Attach to a task and watch runs as they happen
#

# General syntax:
#     pscheduler attach [options] task-url

import optparse
import os
import pscheduler
import subprocess
import sys
import time


pscheduler.set_graceful_exit()

#
# Gargle the arguments
#

usage = "Usage: %prog [options] task-url"
opt_parser = optparse.OptionParser(usage = usage)
opt_parser.disable_interspersed_args()

# GENERAL OPTIONS

opt_parser.add_option("--bind",
                      help="Make the request from the provided address",
                      default=None,
                      action="store", type="string",
                      dest="bind")

opt_parser.add_option("--format",
                      help="Result format (text, html, json, none)",
                      action="store", type="string", default="text",
                      dest="format")

opt_parser.add_option("--quiet",
                      help="Operate quietly",
                      action="store_true", default=False,
                      dest="quiet")

opt_parser.add_option("--runs",
                      help="Exit after this many runs (default 0=infinite)",
                      action="store", type="int", default=0,
                      dest="runs")

opt_parser.add_option("--debug", action="store_true", dest="debug")


(options, remaining_args) = opt_parser.parse_args()

if len(remaining_args) < 1:
    opt_parser.print_usage()
    pscheduler.fail()

#
# Validate the command line
#

if options.runs < 0:
    pscheduler.fail("Invalid --runs; must be 0 or more")
max_runs = options.runs

formats = {
    'html': 'text/html',
    'json': 'application/json',
    'none': None,
    'text': 'text/plain',
    # Not "officially" supported, but here for completeness
    'text/html': 'text/html',
    'application/json': 'application/json',
    'text/plain': 'text/plain',
    }

try:
    out_format = formats[options.format]
except KeyError:
    pscheduler.fail("Invalid --format '%s'; must be text, html, json or none"
                    % (options.format) )



if len(remaining_args) != 1:
    opt_parser.print_usage()
    pscheduler.fail()

[task_url] = remaining_args

if not pscheduler.api_is_task(task_url):
    pscheduler.fail("Invalid task URL.")



verbose = (not options.quiet) and (out_format == "text/plain")


log = pscheduler.Log(verbose=verbose, debug=options.debug, quiet=True)


# Get the task with details and find out its class.

log.debug("Fetching %s", task_url)
status, task = pscheduler.url_get(task_url, params={"detail":True},
                                  bind=options.bind, throw=False)

if status != 200:
    pscheduler.fail("Unable to fetch task: " + task)

# TODO: This is in anticipation of multi-result.
try:
    multi_result = task["detail"]["multi-result"]
    multi_duration = task["detail"]["duration"]
except KeyError:
    pscheduler.fail("Server returned malformed test specification.")

# TODO: Might be nice to dump the test parameters, but need to add a
# 'format' parameter to the REST API like was done for runs.

run_count = 0

last_url = ""

# TODO: This maintains backward compatibility with older versions and
# can be backed off to just next-run-href after the 1.0 release.

try:
    next_run_url = task["detail"]["next-run-href"]
    next_old_style = False
except KeyError:
    try:
        next_old_style = True
    except KeyError:
        pscheduler.fail("Server returned malformed task information.")


while True:

    # TODO: Remove the old_style decision after the 1.0 release

    if next_old_style:

        status, runs = pscheduler.url_get(
            "%s/runs" % (task_url),
            params={
                'upcoming': True,
                'limit' : 1
            },
            bind=options.bind,
            throw=False
        )

        if status != 200:
            pscheduler.fail("Unable to fetch runs.")

        if len(runs) == 0:
            if run_count == 0:
                log.debug("No upcoming runs scheduled")
                pscheduler.fail("No runs scheduled for this task.")
            else:
                pscheduler.succeed()

        run_url = runs[0]

    else:

        status, run_data = pscheduler.url_get(next_run_url,
                                              params={ "wait": 0 },
                                              bind=options.bind,
                                              throw=False)
        if status == 404:
            if run_count == 0:
                pscheduler.fail("No runs scheduled for this task.")
            else:
                pscheduler.succeed("No further runs scheduled.")
        if status != 200:
            pscheduler.fail("Unable to fetch runs: %s", run_data)

        run_url = run_data['href']


    if run_url == last_url:
        # This will get us away from a just-finished run because
        # everything is normalized out to one-second boundaries.
        log.debug("Still working in last task's time slot.  Sleeping.")
        time.sleep(1.0)
        continue

    last_url = run_url

    if verbose:
        print
        print "Next scheduled run:"
        print run_url

    try:
        status, run_json = pscheduler.url_get(run_url, bind=options.bind)
    except pscheduler.psurl.URLException as ex:
        pscheduler.fail(str(ex))


    # Wait out non-starters

    if run_json['state'] == 'nonstart':

        if verbose:
            print "Run scheduled at", run_json['start-time'], "is a non-starter:"
            print run_json['errors']
            print

        # If the task wasn't a repeater, don't hang around.
        try:
            repeating = task['schedule']['repeat']
        except KeyError:
            pscheduler.succeed()

        end_time = run_json['end-time']
        wait_time = pscheduler.time_until_seconds(
            pscheduler.iso8601_as_datetime(end_time))
        if verbose:
            print "Waiting until this run would have ended (%s, ~%s seconds)" \
                % (end_time, int(wait_time))
        time.sleep(wait_time)
        run_count += 1
        continue

    #
    # Wait for the run to start and finish and fetch the results
    #

    start_time = run_json['start-time']
    wait_time = pscheduler.time_until_seconds(
        pscheduler.iso8601_as_datetime(start_time))
    if verbose:
        print "Starts %s (~%s seconds)" % (start_time, int(wait_time))
    time.sleep(wait_time)

    end_time = run_json['end-time']
    wait_time = pscheduler.time_until_seconds(
        pscheduler.iso8601_as_datetime(end_time))
    if verbose:
        print "Ends   %s (~%s seconds)" % (end_time, int(wait_time))
    time.sleep(wait_time)

    if verbose:
        print "Waiting for result..."

    status, result = pscheduler.url_get( run_url,
                                         params={ 'wait-merged': True },
                                         bind=options.bind,
                                         throw=False )

    if status == 404:
        pscheduler.succeed("Run not found; task may have been canceled.")
    if status != 200:
        pscheduler.fail("Failed to fetch run: %d: %s" % (status, result))

    #
    # If failed, dump diags.
    #

    # TODO: Need to do a better job picking this apart.  The full-result
    # member needs to be populated whether the test succeeded or not.

    try:
        status = result['result-merged']['succeeded']
    except (KeyError, TypeError):
        status = False
        run_count += 1

    if not status:

        participants = result['participants']
        full = result['result-full']

        if verbose:

            if "state" not in result:
                pscheduler.fail("Data returned from the server doesn't look like a run result.")

            if result["state"] in ["nonstart", "failed"]:
                try:
                    reason = result["errors"] 
                except KeyError:
                    reason = "No reason provided."

                text = "Run never started"
                if result["state"] == "failed":
                    text = "Run failed"
            
                # super awesome 3.5 backwards compatibility hack check
                # TODO: remove this in 4.1 or whenever support for 4->3.5 testing is removed
                # HACK: BWCTLBC
                # change back to just this ----v 
                # pscheduler.fail("%s: %s" % (text, reason))
                message = "%s: %s" % (text, reason)

                try:
                    dest = task["test"]["spec"].get("dest")
                    source = task["test"]["spec"].get("source")
                    
                    if (dest and pscheduler.api_has_bwctl(dest)) or (source and pscheduler.api_has_bwctl(source)):
                        message += "\nPlease note that if you are testing with a mixture of pScheduler enabled hosts and legacy BWCTL hosts, certain options such as --tool may result in this error message. To ensure backwards compatibility mode is used, do not specify --tool if either end is a BWCTL based host. If possible, the perfSONAR team strongly recommends upgrading as backwards compatibility support will be dropped in a future release."
                except KeyError:
                    pass

                pscheduler.fail(message)
                # end super awesome 3.5 hack

            if result["state"] in ["pending", "on-deck", "running", "cleanup"]:
                pscheduler.fail("Run has not completed.")

            if result["state"] in ["overdue", "missed", "preempted"]:
                pscheduler.fail("Run did not complete: %s" % result["state-display"])

            if result["state"] == "finished" and "result-merged" not in result:
                pscheduler.fail("Run completed but is missing merged result.")

            print
            print "Run failed.  The following errors were reported:"

            for participant in range(0, len(result['participants'])):
                print "By %s:" % participants[participant]
                try:
                    print '   ',
                    print '\n    '.join(full[participant]['error'].split('\n'))
                except (KeyError, TypeError, AttributeError):
                    # TODO: There should be an 'error' in the JSON, but don't
                    # punish the user if there isn't.
                    print "  (No error reported)"
                print


        continue


    #
    # Print the final result.
    #

    status, formatted = pscheduler.url_get(
        result['result-href'],
        params={ 'wait': True,
                 'format': out_format },
        bind=options.bind,
        json=False,
        throw=False)

    if status != 200:
        pscheduler.fail("Unable to retrieve formatted result: " + formatted)

    # TODO: Remove the extra print if not narrating and on a TTY
    if verbose:
        print
    print formatted.rstrip()
    if verbose:
        print

    run_count += 1

    if run_count == max_runs:
        log.debug("Last run")
        break



pscheduler.succeed()
