#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Performs bisection on LLVM based off a .JSON file."""

import enum
import json
import os
import subprocess
import sys
import time
import traceback

import chroot
import llvm_bisection
import update_tryjob_status


# Used to re-try for 'llvm_bisection.py' to attempt to launch more tryjobs.
BISECTION_RETRY_TIME_SECS = 10 * 60

# Wait time to then poll each tryjob whose 'status' value is 'pending'.
POLL_RETRY_TIME_SECS = 30 * 60

# The number of attempts for 'llvm_bisection.py' to launch more tryjobs.
#
# It is reset (break out of the `for` loop/ exit the program) if successfully
# launched more tryjobs or bisection is finished (no more revisions between
# start and end of the bisection).
BISECTION_ATTEMPTS = 3

# The limit for updating all tryjobs whose 'status' is 'pending'.
#
# If the time that has passed for polling exceeds this value, then the program
# will exit with the appropriate exit code.
POLLING_LIMIT_SECS = 18 * 60 * 60


class BuilderStatus(enum.Enum):
    """Actual values given via 'cros buildresult'."""

    PASS = "pass"
    FAIL = "fail"
    RUNNING = "running"


# Writing a dict with `.value`s spelled out makes `black`'s style conflict with
# `cros lint`'s diagnostics.
builder_status_mapping = {
    a.value: b.value
    for a, b in (
        (BuilderStatus.PASS, update_tryjob_status.TryjobStatus.GOOD),
        (BuilderStatus.FAIL, update_tryjob_status.TryjobStatus.BAD),
        (BuilderStatus.RUNNING, update_tryjob_status.TryjobStatus.PENDING),
    )
}


def GetBuildResult(chromeos_path, buildbucket_id):
    """Returns the conversion of the result of 'cros buildresult'."""

    # Calls 'cros buildresult' to get the status of the tryjob.
    try:
        tryjob_json = subprocess.check_output(
            [
                "cros",
                "buildresult",
                "--buildbucket-id",
                str(buildbucket_id),
                "--report",
                "json",
            ],
            cwd=chromeos_path,
            stderr=subprocess.STDOUT,
            encoding="utf-8",
        )
    except subprocess.CalledProcessError as err:
        if "No build found. Perhaps not started" not in err.output:
            raise
        return None

    tryjob_content = json.loads(tryjob_json)

    build_result = str(tryjob_content["%d" % buildbucket_id]["status"])

    # The string returned by 'cros buildresult' might not be in the mapping.
    if build_result not in builder_status_mapping:
        raise ValueError(
            '"cros buildresult" return value is invalid: %s' % build_result
        )

    return builder_status_mapping[build_result]


def main():
    """Bisects LLVM using the result of `cros buildresult` of each tryjob.

    Raises:
        AssertionError: The script was run inside the chroot.
    """

    chroot.VerifyOutsideChroot()

    args_output = llvm_bisection.GetCommandLineArgs()

    chroot.VerifyChromeOSRoot(args_output.chromeos_path)

    if os.path.isfile(args_output.last_tested):
        print("Resuming bisection for %s" % args_output.last_tested)
    else:
        print("Starting a new bisection for %s" % args_output.last_tested)

    while True:
        # Update the status of existing tryjobs
        if os.path.isfile(args_output.last_tested):
            update_start_time = time.time()
            with open(args_output.last_tested, encoding="utf-8") as json_file:
                json_dict = json.load(json_file)
            while True:
                print(
                    '\nAttempting to update all tryjobs whose "status" is '
                    '"pending":'
                )
                print("-" * 40)

                completed = True
                for tryjob in json_dict["jobs"]:
                    if (
                        tryjob["status"]
                        == update_tryjob_status.TryjobStatus.PENDING.value
                    ):
                        status = GetBuildResult(
                            args_output.chromeos_path, tryjob["buildbucket_id"]
                        )
                        if status:
                            tryjob["status"] = status
                        else:
                            completed = False

                print("-" * 40)

                # Proceed to the next step if all the existing tryjobs have
                # completed.
                if completed:
                    break

                delta_time = time.time() - update_start_time

                if delta_time > POLLING_LIMIT_SECS:
                    # Something is wrong with updating the tryjobs's 'status'
                    # via `cros buildresult` (e.g. network issue, etc.).
                    sys.exit("Failed to update pending tryjobs.")

                print("-" * 40)
                print("Sleeping for %d minutes." % (POLL_RETRY_TIME_SECS // 60))
                time.sleep(POLL_RETRY_TIME_SECS)

            # There should always be update from the tryjobs launched in the
            # last iteration.
            temp_filename = "%s.new" % args_output.last_tested
            with open(temp_filename, "w", encoding="utf-8") as temp_file:
                json.dump(
                    json_dict, temp_file, indent=4, separators=(",", ": ")
                )
            os.rename(temp_filename, args_output.last_tested)

        # Launch more tryjobs.
        bisection_complete = (
            llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value
        )
        for cur_try in range(1, BISECTION_ATTEMPTS + 1):
            try:
                print("\nAttempting to launch more tryjobs if possible:")
                print("-" * 40)

                bisection_ret = llvm_bisection.main(args_output)

                print("-" * 40)

                # Stop if the bisection has completed.
                if bisection_ret == bisection_complete:
                    sys.exit(0)

                # Successfully launched more tryjobs.
                break
            except Exception:
                traceback.print_exc()

                print("-" * 40)

                # Exceeded the number of times to launch more tryjobs.
                if cur_try == BISECTION_ATTEMPTS:
                    sys.exit("Unable to continue bisection.")

                num_retries_left = BISECTION_ATTEMPTS - cur_try

                print(
                    "Retries left to continue bisection %d." % num_retries_left
                )

                print(
                    "Sleeping for %d minutes."
                    % (BISECTION_RETRY_TIME_SECS // 60)
                )
                time.sleep(BISECTION_RETRY_TIME_SECS)


if __name__ == "__main__":
    main()
