# Copyright 2019 gRPC authors.
#
# Licensed 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.
"""Generate draft and release notes in Markdown from Github PRs.

You'll need a github API token to avoid being rate-limited. See
https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/

This script collects PRs using "git log X..Y" from local repo where X and Y are
tags or release branch names of previous and current releases respectively.
Typically, notes are generated before the release branch is labelled so Y is
almost always the name of the release branch. X is the previous release branch
if this is not a patch release. Otherwise, it is the previous release tag.
For example, for release v1.17.0, X will be origin/v1.16.x and for release v1.17.3,
X will be v1.17.2. In both cases Y will be origin/v1.17.x.

"""

from collections import defaultdict
import json
import logging
import re
import subprocess

import urllib3

logging.basicConfig(level=logging.WARNING)

content_header = """Draft Release Notes For {version}
--
Final release notes will be generated from the PR titles that have *"release notes:yes"* label. If you have any additional notes please add them below. These will be appended to auto generated release notes. Previous release notes are [here](https://github.com/grpc/grpc/releases).

**Also, look at the PRs listed below against your name.** Please apply the missing labels and make necessary corrections (like fixing the title) to the PR in Github. Final release notes will be generated just before the release on {date}.

Add additional notes not in PRs
--

Core
-


C++
-


C#
-


Objective-C
-


PHP
-


Python
-


Ruby
-


"""

rl_header = """This is release {version} ([{name}](https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md)) of gRPC Core.

For gRPC documentation, see [grpc.io](https://grpc.io/). For previous releases, see [Releases](https://github.com/grpc/grpc/releases).

This release contains refinements, improvements, and bug fixes, with highlights listed below.


"""

HTML_URL = "https://github.com/grpc/grpc/pull/"
API_URL = "https://api.github.com/repos/grpc/grpc/pulls/"


def get_commit_detail(commit):
    """Print commit and CL info for the commits that are submitted with CL-first workflow and warn the release manager to check manually."""
    glg_command = [
        "git",
        "log",
        "-n 1",
        "%s" % commit,
    ]
    output = subprocess.check_output(glg_command).decode("utf-8", "ignore")
    matches = re.search("Author:.*<(.*@).*>", output)
    author = matches.group(1)
    detail = "- " + author + " "
    title = output.splitlines()[4].strip()
    detail += "- " + title
    if not title.endswith("."):
        detail += "."
    matches = re.search("PiperOrigin-RevId: ([0-9]+)$", output)
    cl_num = matches.group(1)
    detail += (
        " ([commit](https://github.com/grpc/grpc/commit/"
        + commit
        + ")) ([CL](https://critique.corp.google.com/cl/"
        + cl_num
        + "))"
    )
    return detail


def get_commit_log(prevRelLabel, relBranch):
    """Return the output of 'git log prevRelLabel..relBranch'"""

    import subprocess

    glg_command = [
        "git",
        "log",
        "--pretty=oneline",
        "%s..%s" % (prevRelLabel, relBranch),
    ]
    print(("Running ", " ".join(glg_command)))
    return subprocess.check_output(glg_command).decode("utf-8", "ignore")


def get_pr_data(pr_num):
    """Get the PR data from github. Return 'error' on exception"""
    http = urllib3.PoolManager(
        retries=urllib3.Retry(total=7, backoff_factor=1), timeout=4.0
    )
    url = API_URL + pr_num
    try:
        response = http.request(
            "GET", url, headers={"Authorization": "token %s" % TOKEN}
        )
    except urllib3.exceptions.HTTPError as e:
        print("Request error:", e.reason)
        return "error"
    return json.loads(response.data.decode("utf-8"))


def get_pr_titles(gitLogs):
    import re

    # All commits
    match_commit = "^([a-fA-F0-9]+) "
    all_commits_set = set(re.findall(match_commit, gitLogs, re.MULTILINE))

    error_count = 0
    # PRs with merge commits
    match_merge_pr = "^([a-fA-F0-9]+) .*Merge pull request #(\d+)"
    matches = re.findall(match_merge_pr, gitLogs, re.MULTILINE)
    merge_commits = []
    prlist_merge_pr = []
    if matches:
        merge_commits, prlist_merge_pr = zip(*matches)
    merge_commits_set = set(merge_commits)
    print("\nPRs matching 'Merge pull request #<num>':")
    print(prlist_merge_pr)
    print("\n")

    # PRs using Github's squash & merge feature
    match_sq = "^([a-fA-F0-9]+) .*\(#(\d+)\)$"
    matches = re.findall(match_sq, gitLogs, re.MULTILINE)
    if matches:
        sq_commits, prlist_sq = zip(*matches)
    sq_commits_set = set(sq_commits)
    print("\nPRs matching '[PR Description](#<num>)$'")
    print(prlist_sq)
    print("\n")
    prlist = list(prlist_merge_pr) + list(prlist_sq)
    langs_pr = defaultdict(list)
    for pr_num in prlist:
        pr_num = str(pr_num)
        print(("---------- getting data for PR " + pr_num))
        pr = get_pr_data(pr_num)
        if pr == "error":
            print(
                ("\n***ERROR*** Error in getting data for PR " + pr_num + "\n")
            )
            error_count += 1
            continue
        rl_no_found = False
        rl_yes_found = False
        lang_found = False
        for label in pr["labels"]:
            if label["name"] == "release notes: yes":
                rl_yes_found = True
            elif label["name"] == "release notes: no":
                rl_no_found = True
            elif label["name"].startswith("lang/"):
                lang_found = True
                lang = label["name"].split("/")[1].lower()
                # lang = lang[0].upper() + lang[1:]
        body = pr["title"]
        if not body.endswith("."):
            body = body + "."

        prline = (
            "-  " + body + " ([#" + pr_num + "](" + HTML_URL + pr_num + "))"
        )
        detail = "- " + pr["user"]["login"] + "@ " + prline
        print(detail)
        # if no RL label
        if not rl_no_found and not rl_yes_found:
            print(("Release notes label missing for " + pr_num))
            langs_pr["nolabel"].append(detail)
        elif rl_yes_found and not lang_found:
            print(("Lang label missing for " + pr_num))
            langs_pr["nolang"].append(detail)
        elif rl_no_found:
            print(("'Release notes:no' found for " + pr_num))
            langs_pr["notinrel"].append(detail)
        elif rl_yes_found:
            print(
                (
                    "'Release notes:yes' found for "
                    + pr_num
                    + " with lang "
                    + lang
                )
            )
            langs_pr["inrel"].append(detail)
            langs_pr[lang].append(prline)
    commits_wo_pr = all_commits_set - merge_commits_set - sq_commits_set
    for commit in commits_wo_pr:
        langs_pr["nopr"].append(get_commit_detail(commit))

    return langs_pr, error_count


def write_draft(langs_pr, file, version, date):
    file.write(content_header.format(version=version, date=date))
    file.write(
        "Commits with missing PR number - please lookup the PR info in the corresponding CL and add to the additional notes if necessary.\n"
    )
    file.write("---\n")
    file.write("\n")
    if langs_pr["nopr"]:
        file.write("\n".join(langs_pr["nopr"]))
    else:
        file.write("- None")
    file.write("\n")
    file.write("\n")
    file.write("PRs with missing release notes label - please fix in Github\n")
    file.write("---\n")
    file.write("\n")
    if langs_pr["nolabel"]:
        langs_pr["nolabel"].sort()
        file.write("\n".join(langs_pr["nolabel"]))
    else:
        file.write("- None")
    file.write("\n")
    file.write("\n")
    file.write("PRs with missing lang label - please fix in Github\n")
    file.write("---\n")
    file.write("\n")
    if langs_pr["nolang"]:
        langs_pr["nolang"].sort()
        file.write("\n".join(langs_pr["nolang"]))
    else:
        file.write("- None")
    file.write("\n")
    file.write("\n")
    file.write(
        "PRs going into release notes - please check title and fix in Github."
        " Do not edit here.\n"
    )
    file.write("---\n")
    file.write("\n")
    if langs_pr["inrel"]:
        langs_pr["inrel"].sort()
        file.write("\n".join(langs_pr["inrel"]))
    else:
        file.write("- None")
    file.write("\n")
    file.write("\n")
    file.write("PRs not going into release notes\n")
    file.write("---\n")
    file.write("\n")
    if langs_pr["notinrel"]:
        langs_pr["notinrel"].sort()
        file.write("\n".join(langs_pr["notinrel"]))
    else:
        file.write("- None")
    file.write("\n")
    file.write("\n")


def write_rel_notes(langs_pr, file, version, name):
    file.write(rl_header.format(version=version, name=name))
    if langs_pr["core"]:
        file.write("Core\n---\n\n")
        file.write("\n".join(langs_pr["core"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["c++"]:
        file.write("C++\n---\n\n")
        file.write("\n".join(langs_pr["c++"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["c#"]:
        file.write("C#\n---\n\n")
        file.write("\n".join(langs_pr["c#"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["go"]:
        file.write("Go\n---\n\n")
        file.write("\n".join(langs_pr["go"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["Java"]:
        file.write("Java\n---\n\n")
        file.write("\n".join(langs_pr["Java"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["node"]:
        file.write("Node\n---\n\n")
        file.write("\n".join(langs_pr["node"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["objc"]:
        file.write("Objective-C\n---\n\n")
        file.write("\n".join(langs_pr["objc"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["php"]:
        file.write("PHP\n---\n\n")
        file.write("\n".join(langs_pr["php"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["python"]:
        file.write("Python\n---\n\n")
        file.write("\n".join(langs_pr["python"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["ruby"]:
        file.write("Ruby\n---\n\n")
        file.write("\n".join(langs_pr["ruby"]))
        file.write("\n")
        file.write("\n")
    if langs_pr["other"]:
        file.write("Other\n---\n\n")
        file.write("\n".join(langs_pr["other"]))
        file.write("\n")
        file.write("\n")


def build_args_parser():
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument(
        "release_version", type=str, help="New release version e.g. 1.14.0"
    )
    parser.add_argument(
        "release_name", type=str, help="New release name e.g. gladiolus"
    )
    parser.add_argument(
        "release_date", type=str, help="Release date e.g. 7/30/18"
    )
    parser.add_argument(
        "previous_release_label",
        type=str,
        help="Previous release branch/tag e.g. v1.13.x",
    )
    parser.add_argument(
        "release_branch",
        type=str,
        help="Current release branch e.g. origin/v1.14.x",
    )
    parser.add_argument(
        "draft_filename", type=str, help="Name of the draft file e.g. draft.md"
    )
    parser.add_argument(
        "release_notes_filename",
        type=str,
        help="Name of the release notes file e.g. relnotes.md",
    )
    parser.add_argument(
        "--token",
        type=str,
        default="",
        help="GitHub API token to avoid being rate limited",
    )
    return parser


def main():
    import os

    global TOKEN

    parser = build_args_parser()
    args = parser.parse_args()
    version, name, date = (
        args.release_version,
        args.release_name,
        args.release_date,
    )
    start, end = args.previous_release_label, args.release_branch

    TOKEN = args.token
    if TOKEN == "":
        try:
            TOKEN = os.environ["GITHUB_TOKEN"]
        except:
            pass
    if TOKEN == "":
        print(
            "Error: Github API token required. Either include param"
            " --token=<your github token> or set environment variable"
            " GITHUB_TOKEN to your github token"
        )
        return

    langs_pr, error_count = get_pr_titles(get_commit_log(start, end))

    draft_file, rel_file = args.draft_filename, args.release_notes_filename
    filename = os.path.abspath(draft_file)
    if os.path.exists(filename):
        file = open(filename, "r+")
    else:
        file = open(filename, "w")

    file.seek(0)
    write_draft(langs_pr, file, version, date)
    file.truncate()
    file.close()
    print(("\nDraft notes written to " + filename))

    filename = os.path.abspath(rel_file)
    if os.path.exists(filename):
        file = open(filename, "r+")
    else:
        file = open(filename, "w")

    file.seek(0)
    write_rel_notes(langs_pr, file, version, name)
    file.truncate()
    file.close()
    print(("\nRelease notes written to " + filename))
    if error_count > 0:
        print("\n\n*** Errors were encountered. See log. *********\n")


if __name__ == "__main__":
    main()