#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: E402

import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any

# Hackery to enable importing of utils from ci/scripts/jenkins
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
sys.path.append(str(REPO_ROOT / "ci" / "scripts" / "jenkins"))

from git_utils import GitHubRepo, git, parse_remote

_commit_query_fields = """
    messageHeadline
    oid
    statusCheckRollup {
        contexts(last:100) {
            nodes {
                ... on CheckRun {
                    conclusion
                    status
                    name
                    checkSuite {
                        workflowRun {
                            workflow {
                                name
                            }
                        }
                    }
                }
                ... on StatusContext {
                    context
                    state
                }
            }
        }
    }
"""


def commits_query(user: str, repo: str, cursor: str | None = None):
    """
    Create a GraphQL query to find the last N commits along with their statuses
    and some metadata (paginated after 'cursor')
    """
    after = ""
    if cursor is not None:
        after = f', after:"{cursor}"'

    return f"""
    {{
    repository(name: "{repo}", owner: "{user}") {{
        defaultBranchRef {{
        target {{
            ... on Commit {{
            history(first: 15{after}) {{
                edges {{ cursor }}
                nodes {{
                    {_commit_query_fields}
                }}
            }}
            }}
        }}
        }}
    }}
    }}
    """


EXPECTED_CI_JOBS = [
    "cross-isa-minimal/branch",
    "gpu/branch",
    "arm/branch",
    "cortexm/branch",
    "cpu/branch",
    "docker/branch",
    "lint/branch",
    "minimal/branch",
    "riscv/branch",
    "wasm/branch",
]


def commit_passed_ci(commit: dict[str, Any]) -> bool:
    """
    Returns true if all of a commit's statuses are SUCCESS
    """
    statuses = commit["statusCheckRollup"]["contexts"]["nodes"]

    # GitHub Actions statuses are different from external GitHub statuses, so
    # unify them into 1 representation
    # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads
    unified_statuses = []
    for status in statuses:
        if "context" in status:
            # Parse non-GHA status
            unified_statuses.append((status["context"], status["state"] == "SUCCESS"))
        else:
            # Parse GitHub Actions item
            workflow = status["checkSuite"]["workflowRun"]["workflow"]["name"]
            name = f"{workflow} / {status['name']}"
            unified_statuses.append((name, status["conclusion"] == "SUCCESS"))

    print(f"Statuses on {commit['oid']}:", json.dumps(unified_statuses, indent=2))

    # Assert that specific jobs are present in the commit statuses (i.e. don't
    # approve if CI was broken and didn't schedule a job)
    job_names = {name for name, status in unified_statuses}
    for job in EXPECTED_CI_JOBS:
        if job not in job_names:
            # Did not find expected job name
            return False

    passed_ci = all(status for name, status in unified_statuses)
    return passed_ci


def update_branch(user: str, repo: str, sha: str, branch_name: str) -> None:
    git(["fetch", "origin", sha])
    git(["reset", "--hard", "FETCH_HEAD"])
    try:
        git(["branch", "-D", branch_name])
    except RuntimeError:
        # Ignore failures (i.e. the branch did not exist in the first place)
        pass
    git(["checkout", "-b", branch_name])

    # Create and push the branch
    git(["push", "origin", "--force", branch_name])
    print(f"Pushed branch {branch_name} with commit {sha}")


if __name__ == "__main__":
    help = "Push the a branch to the last commit that passed all CI runs"
    parser = argparse.ArgumentParser(description=help)
    parser.add_argument("--remote", default="origin", help="ssh remote to parse")
    parser.add_argument("--dry-run", action="store_true", help="don't submit to GitHub")
    parser.add_argument("--branch", default="last-successful", help="branch name")
    parser.add_argument(
        "--testonly-json", help="(testing) data to use instead of fetching from GitHub"
    )
    args = parser.parse_args()

    remote = git(["config", "--get", f"remote.{args.remote}.url"])
    user, repo = parse_remote(remote)

    if args.testonly_json:
        r = json.loads(args.testonly_json)
    else:
        github = GitHubRepo(token=os.environ["GITHUB_TOKEN"], user=user, repo=repo)
        q = commits_query(user, repo)
        r = github.graphql(q)

    commits = r["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"]

    # Limit GraphQL pagination
    MAX_COMMITS_TO_CHECK = 50
    i = 0

    while i < MAX_COMMITS_TO_CHECK:
        # Check each commit
        for commit in commits:
            if commit_passed_ci(commit):
                print(f"Found last good commit: {commit['oid']}: {commit['messageHeadline']}")
                if not args.dry_run:
                    update_branch(
                        user=user,
                        repo=repo,
                        sha=commit["oid"],
                        branch_name=args.branch,
                    )
                # Nothing to do after updating the branch, exit early
                exit(0)

        # No good commit found, proceed to next page of results
        edges = r["data"]["repository"]["defaultBranchRef"]["target"]["history"]["edges"]
        if len(edges) == 0:
            break
        else:
            q = commits_query(user, repo, cursor=edges[-1]["cursor"])
            r = github.graphql(q)
            commits = r["data"]["repository"]["defaultBranchRef"]["target"]["history"]["nodes"]

        # Backstop to prevent looking through all the past commits
        i += len(commits)

    print(f"No good commits found in the last {len(commits)} commits")
    exit(1)
