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

import datetime
import optparse
import os
import pipes
import pscheduler
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("--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 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([ pipes.quote(arg) for arg in task_cli ]),
            indent=2))
        print()

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

    if run_json["state"] == "nonstart":
        try:
            reason = run_json["errors"] 
        except KeyError:
            reason = "No reason provided."
        print_message("Run never started: %s" % (reason), exit_on_error)
        return

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

    run_ran = run_json["state"] == "finished"
    if not run_ran:
        print_message("Run did not complete: %s" % (run_json["state-display"]), exit_on_error)
        return

    if (not run_ran) and run_json["state"] in ["overdue", "missed", "preempted"]:
        print_message("Run did not complete: %s" % (run_json["state-display"]), exit_on_error)
        return

    if run_ran:
        try:
            result_merged = run_json['result-merged']
            if result_merged is None:
                print_message("result-merged missing from run result", False)
                return
            succeeded = result_merged['succeeded']
            if succeeded is None:
                raise KeyError()
        except KeyError:
            print_message("Data missing from run result", False)
            return
    else:
        succeeded = False

    if succeeded:
        dump_run_result(result_url, out_format, exit_on_error)

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

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


        # TODO: Does this even matter?
        try:
            err = run_json.get("errors", None)
        except (AttributeError, TypeError):
            err = None

        if err is not None:
            print()
            print("Error:")
            try:
                print("\n".join([
                    "    %s" % (string) for string in
                    err.split("\n")
                ]))
            except (KeyError, TypeError):
                print("    (No error)")

        # Participant diagnostics and errors

        parts = task_json["detail"]["participants"]
        for participant in range(0, len(parts)):

            print()
            print("Diagnostics from %s:" % (parts[participant]))

            try:
                diags = run_json["result-full"][participant]["diags"]
                if diags is None or diags == "":
                    raise KeyError
            except (KeyError, TypeError, AttributeError):
                diags = "No diagnostics."
            print(pscheduler.indent(diags, indent=2))

            print()
            print("Error from %s:" % (parts[participant]))
            try:
                err = run_json["result-full"][participant].get("error")
                if err is None or err == "":
                    raise KeyError
            except (KeyError, AttributeError, TypeError):
                err = "No error."
            print(pscheduler.indent(err, 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']['name'])
                except KeyError:
                    where_to = "To %s" % (archiving['archiver']['name'])
                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 = "" if succeeded else attempt['stdout']['error']
                    except (KeyError, TypeError):
                        succeeded = False
                        diags = attempt['stderr']
                    print("    %-25s %s" % (
                        attempt['time'],
                        "Succeeded" if succeeded else diags
                        ))
        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()
