#!/usr/bin/env python3
#
# Fetch the result of a run or runs by its URL
#

import datetime
import optparse
import os
import pscheduler
import shlex
import sys


pscheduler.set_graceful_exit()


#
# Gargle the arguments
#


class VerbatimParser(optparse.OptionParser):
    def format_epilog(self, formatter):
        return self.epilog

opt_parser = VerbatimParser(
    usage="Usage: %prog [options] run-url",
    epilog=
"""
Examples:

  result https://ps.foo.org/pscheduler/task/12345.../run/67890...
      Fetch a result of the specified run as plain text

  result --format text https://ps.foo.org/pscheduler/task/12345.../run/67890...
      Same as above, with explicit format

  result --format html https://ps.foo.org/pscheduler/task/12345.../run/67890...
      Fetch a result of the specified run and format as HTML

  result --format json html https://ps.foo.org/pscheduler/task/12345...
      Fetch a result of the specified run and format as JSON
"""
    )
opt_parser.disable_interspersed_args()

opt_parser.add_option("--archivings",
                      help="For text output, dump archiving status",
                      action="store_true",
                      default=False,
                      dest="archivings")

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("--diags",
                      help="For text output, dump participant diagnostics",
                      action="store_true",
                      default=False,
                      dest="diags")

opt_parser.add_option("--format",
                      help="Format for output: text (the default), html or json",
                      action="store", type="string",
                      default="text",
                      dest="format")

opt_parser.add_option("--full",
                      help="Produce full diagnostics and archiving information",
                      action="store_true",
                      default=False,
                      dest="full")

opt_parser.add_option("--output",
                      help="Path to write output",
                      action="store", type="string",
                      default=None,
                      dest="output")

opt_parser.add_option("--quiet",
                      help="For text output, don't display anything but the result",
                      action="store_true",
                      default=False,
                      dest="quiet")

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



(options, remaining_args) = opt_parser.parse_args()

if options.full:
    options.archivings = True
    options.diags = True

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

formats = {
    'html': 'text/html',
    'json': 'application/json',
    '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; must be text, html, or json")


if options.diags and out_format != "text/plain":
    pscheduler.fail("Cannot produce diagnostics in %s format" % (options.format))

if options.archivings and out_format != "text/plain":
    pscheduler.fail("Cannot produce archivings in %s format" % (options.format))

if options.output:
    try:
        out_file = open(options.output, "w")
    except Exception as ex:
        pscheduler.fail(str(ex))
else:
    out_file = sys.stdout



url = remaining_args[0]



#
# Dump a single run's result
#

def print_message(message, exit_after):
    if exit_after:
        pscheduler.fail(message)
    else:
        print(message)
    

def dump_run_result(url, out_format, exit_on_error=True):
    try:

        status, text = pscheduler.url_get(
            url,
            params={ "format": out_format },
            bind=options.bind,
            json=False)

    except Exception as ex:
        print_message("Problem fetching results: %s" % (str(ex)), exit_on_error)
        return

    out_file.write(text.strip() + "\n")

    

def dump_run(run_json, exit_on_error=False, print_run_url=False):

    try:
        result_url = run_json["result-href"]
    except KeyError:
        print_message("No result URL returned by the server.", exit_on_error)
        return

    try:
        if run_json["participant"] != 0:
            print_message("Cannot query non-lead server for run result.", exit_on_error)
            return
    except KeyError:
        print_message("Internal error:  Result is mising data.", exit_on_error)
        return


    # Non-text/plain is the easy-out case.
    if out_format != "text/plain":
        dump_run_result(result_url, out_format, exit_on_error)
        return

    # Everything below this is text/plain.

    try:
        task_url = run_json["task-href"]
    except KeyError:
        print_message("No task URL returned by the server.", exit_on_error)
        return

    try:

        # TODO: This is repetitive
        status, task_cli = pscheduler.url_get(
            "%s/cli" % task_url,
            bind=options.bind)

        status, task_json = pscheduler.url_get(task_url,
                                               params={"detail": True},
                                               bind=options.bind)

    except Exception as ex:
        print_message("Problem fetching results: %s" % (str(ex)), exit_on_error)
        return


    # Header-type stuff for text/plain if not quiet

    if not options.quiet:

        firstline = [ run_json["start-time"], "on" ]

        parts = run_json["participants"]
        if len(parts) == 1:
            firstline.append(parts[0])
        else:
            last = parts.pop()
            if len(parts) == 1:
                firstline.append(parts[0])
            else:
                firstline.extend([ "%s," % part for part in parts ])
            firstline.append("and")
            firstline.append(last)

        firstline.append("with")
        firstline.append(task_json["tool"] + ":")
        print(pscheduler.prefixed_wrap(
            "", " ".join(firstline), indent=2))

        if print_run_url:
            print()
            print(run_json.get("href", "(URL is missing from data)"))

        print()
        print(pscheduler.prefixed_wrap(
            "", " ".join([ shlex.quote(arg) for arg in task_cli ]),
            indent=2))
        print()

    # Deal with the various reasons why the run might not have happened

    state = run_json["state"]


    # Handle abject falures

    if state == "nonstart":
        print_message("Run never started: {}".format(
            run_json.get("errors", "No reason provided.")
        ), exit_on_error)
        return

    if state in ["pending", "on-deck", "running", "cleanup"]:
        print_message("Run has not completed.", exit_on_error)
        return

    if state in ["overdue", "missed", "preempted"]:
        print_message("Run did not complete: {}".format(run_json["state-display"]), exit_on_error)
        return


    try:
        result_merged = run_json['result-merged']
        if result_merged is None:
            raise KeyError()
    except KeyError:
        result_merged = {
            "succeeded": False,
            "error": "No result was produced."
        }

    succeeded = result_merged.get('succeeded', False)

    if succeeded:
        dump_run_result(result_url, out_format, exit_on_error)
    else:

        print("Run failed.")

        try:
            print("\nError:\n{}".format(pscheduler.indent(result_merged['error'])))
        except (KeyError, TypeError):
            # Nothing to print.
            pass

        try:
            print("\nDiagnostics:\n{}".format(pscheduler.indent(result_merged['diags'])))
        except (KeyError, TypeError):
            # Nothing to print.
            pass



    # Failures get a diagnostic dump no matter what.

    if options.diags or not succeeded:

        if "clock-survey" in run_json and len(run_json["clock-survey"]) > 1:

            survey_max = len(run_json["clock-survey"]) - 1

            survey = [ pscheduler.iso8601_as_datetime(entry["time"])
                       for entry in run_json["clock-survey"] ]

            max_diff = datetime.timedelta()
            for index_a in range(0, survey_max+1):
                time_a = survey[index_a]
                for time_b in survey[index_a+1:]:
                    max_diff = max(max_diff, abs(time_b - time_a))

            if max_diff > datetime.timedelta(seconds=1.0):
                print()
                print(pscheduler.prefixed_wrap("", 
                                               "This run likely failed because"
                                               " the clocks on participants differed"
                                               " by %s." % (max_diff)
                                           ))

        # Limit diagnostics

        if run_json.get("limit-diags", None) is not None:
            print()
            print("Limit system diagnostics for this run:")
            print(pscheduler.indent(run_json["limit-diags"]))
            

        # Participant diagnostics and errors

        parts = task_json["detail"]["participants"]

        full_result = run_json.get("result-full")

        for participant in range(0, len(parts)):

            try:
                part_result = None if full_result is None else full_result[participant]
            except IndexError:
                part_result = None

            if part_result is None:
                print("\nNo additional information available from {}.".format(parts[participant]))
                continue

            for what, what_text in [ ("error", "Error"), ("diags", "Diagnostics") ]:

                if part_result.get(what, None) in [None, ""]:
                    continue

                print("\n{} from {}:\n{}".format(
                    what_text,
                    parts[participant],
                    pscheduler.indent(part_result[what], indent=2)
                ))


    # Dump the archiving information

    if succeeded and options.archivings:
        print()
        print("Archivings:")
        if run_json.get('archivings', None) is not None:
            for archiving in run_json['archivings']:
                print()

                # Servers 4.3 and later add the full archive spec with a label.
                try:
                    where_to = "To %s using %s" % (archiving['spec']['label'], archiving['spec']['archiver'])
                except KeyError:
                    where_to = "To %s" % (archiving['spec']['archiver'])
                print("  %s, %s" % (
                    where_to,
                    "Finished" if archiving['archived'] else "Unfinished"
                    ))

                for attempt in archiving['diags']:
                    try:
                        if attempt['return-code'] != 0:
                            raise TypeError  # Treat this as a failure.
                        succeeded = attempt['stdout']['succeeded']
                        diags = attempt['stdout'].get('error', None)
                        if diags == "":
                            diags = None
                    except (KeyError, TypeError):
                        succeeded = False
                        diags = attempt['stderr']
                    print("    %-25s %s" % (
                        attempt['time'],
                        diags or "Succeeded"
                        ))
        else:
            print(pscheduler.indent("This task had no archivings."))





#
# Main Program
#


# Fetch the URL and see if we're dealing with a task or a run.

def fail_not_pscheduler():
    pscheduler.fail("URL does not point at a valid pScheduler task or run.")


try:
    status, json = pscheduler.url_get(url,
                                      params={ "detail": True },
                                      bind=options.bind)
    if not isinstance(json, dict):
        raise ValueError
except ValueError:
    fail_not_pscheduler()
except Exception as ex:
    pscheduler.fail("Unable to fetch URL: %s" % (str(ex)))


# Run means a single result.

if "result-href" in json:
    dump_run(json,
             exit_on_error=(out_format != "text/plain"),
             print_run_url=False
    )
    pscheduler.succeed()


# Try it as a task, dumping all runs

try:
    status, runs = pscheduler.url_get(
        json["detail"]["runs-href"],
        bind=options.bind)
except KeyError:
    fail_not_pscheduler()
    pass
except Exception as ex:
    pscheduler.fail("Problem fetching runs: %s" % (str(ex)))

if out_format != "text/plain":
    pscheduler.fail("Cannot retrieve muliple runs as %s" % (options.format))

need_newline = False
for run in runs:

    try:
        status, run_json = pscheduler.url_get(run, bind=options.bind)
    except Exception as ex:
        pscheduler.fail(str(ex))

    if need_newline:
        print()
        print()
        print()
    else:
        need_newline = True
    dump_run(run_json,
             exit_on_error=False,
             print_run_url=True)



pscheduler.succeed()
