# Copyright 2018 The 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.

from __future__ import print_function

import datetime
import json
import os
import sys
import time
import traceback

import jwt
import requests

_GITHUB_API_PREFIX = "https://api.github.com"
_GITHUB_REPO = "grpc/grpc"
_GITHUB_APP_ID = 22338
_INSTALLATION_ID = 519109

_ACCESS_TOKEN_CACHE = None
_ACCESS_TOKEN_FETCH_RETRIES = 6
_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S = 15

_CHANGE_LABELS = {
    -1: "improvement",
    0: "none",
    1: "low",
    2: "medium",
    3: "high",
}

_INCREASE_DECREASE = {
    -1: "decrease",
    0: "neutral",
    1: "increase",
}


def _jwt_token():
    github_app_key = open(
        os.path.join(
            os.environ["KOKORO_KEYSTORE_DIR"], "73836_grpc_checks_private_key"
        ),
        "rb",
    ).read()
    return jwt.encode(
        {
            "iat": int(time.time()),
            "exp": int(time.time() + 60 * 10),  # expire in 10 minutes
            "iss": _GITHUB_APP_ID,
        },
        github_app_key,
        algorithm="RS256",
    )


def _access_token():
    global _ACCESS_TOKEN_CACHE
    if _ACCESS_TOKEN_CACHE == None or _ACCESS_TOKEN_CACHE["exp"] < time.time():
        for i in range(_ACCESS_TOKEN_FETCH_RETRIES):
            resp = requests.post(
                url="https://api.github.com/app/installations/%s/access_tokens"
                % _INSTALLATION_ID,
                headers={
                    "Authorization": "Bearer %s" % _jwt_token(),
                    "Accept": "application/vnd.github.machine-man-preview+json",
                },
            )

            try:
                _ACCESS_TOKEN_CACHE = {
                    "token": resp.json()["token"],
                    "exp": time.time() + 60,
                }
                break
            except (KeyError, ValueError):
                traceback.print_exc()
                print("HTTP Status %d %s" % (resp.status_code, resp.reason))
                print("Fetch access token from Github API failed:")
                print(resp.text)
                if i != _ACCESS_TOKEN_FETCH_RETRIES - 1:
                    print(
                        "Retrying after %.2f second."
                        % _ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S
                    )
                    time.sleep(_ACCESS_TOKEN_FETCH_RETRIES_INTERVAL_S)
        else:
            print("error: Unable to fetch access token, exiting...")
            sys.exit(0)

    return _ACCESS_TOKEN_CACHE["token"]


def _call(url, method="GET", json=None):
    if not url.startswith("https://"):
        url = _GITHUB_API_PREFIX + url
    headers = {
        "Authorization": "Bearer %s" % _access_token(),
        "Accept": "application/vnd.github.antiope-preview+json",
    }
    return requests.request(method=method, url=url, headers=headers, json=json)


def _latest_commit():
    resp = _call(
        "/repos/%s/pulls/%s/commits"
        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"])
    )
    return resp.json()[-1]


def check_on_pr(name, summary, success=True):
    """Create/Update a check on current pull request.

    The check runs are aggregated by their name, so newer check will update the
    older check with the same name.

    Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
    should be updated.

    Args:
      name: The name of the check.
      summary: A str in Markdown to be used as the detail information of the check.
      success: A bool indicates whether the check is succeed or not.
    """
    if "KOKORO_GIT_COMMIT" not in os.environ:
        print("Missing KOKORO_GIT_COMMIT env var: not checking")
        return
    if "KOKORO_KEYSTORE_DIR" not in os.environ:
        print("Missing KOKORO_KEYSTORE_DIR env var: not checking")
        return
    if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ:
        print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking")
        return
    MAX_SUMMARY_LEN = 65400
    if len(summary) > MAX_SUMMARY_LEN:
        # Drop some hints to the log should someone come looking for what really happened!
        print("Clipping too long summary")
        print(summary)
        summary = summary[:MAX_SUMMARY_LEN] + "\n\n\n... CLIPPED (too long)"
    completion_time = (
        str(datetime.datetime.utcnow().replace(microsecond=0).isoformat()) + "Z"
    )
    resp = _call(
        "/repos/%s/check-runs" % _GITHUB_REPO,
        method="POST",
        json={
            "name": name,
            "head_sha": os.environ["KOKORO_GIT_COMMIT"],
            "status": "completed",
            "completed_at": completion_time,
            "conclusion": "success" if success else "failure",
            "output": {
                "title": name,
                "summary": summary,
            },
        },
    )
    print(
        "Result of Creating/Updating Check on PR:",
        json.dumps(resp.json(), indent=2),
    )


def label_significance_on_pr(name, change, labels=_CHANGE_LABELS):
    """Add a label to the PR indicating the significance of the check.

    Requires environment variable 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' to indicate which pull request
    should be updated.

    Args:
      name: The name of the label.
      value: A str in Markdown to be used as the detail information of the label.
    """
    if change < min(list(labels.keys())):
        change = min(list(labels.keys()))
    if change > max(list(labels.keys())):
        change = max(list(labels.keys()))
    value = labels[change]
    if "KOKORO_GIT_COMMIT" not in os.environ:
        print("Missing KOKORO_GIT_COMMIT env var: not checking")
        return
    if "KOKORO_KEYSTORE_DIR" not in os.environ:
        print("Missing KOKORO_KEYSTORE_DIR env var: not checking")
        return
    if "KOKORO_GITHUB_PULL_REQUEST_NUMBER" not in os.environ:
        print("Missing KOKORO_GITHUB_PULL_REQUEST_NUMBER env var: not checking")
        return
    existing = _call(
        "/repos/%s/issues/%s/labels"
        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]),
        method="GET",
    ).json()
    print("Result of fetching labels on PR:", existing)
    new = [x["name"] for x in existing if not x["name"].startswith(name + "/")]
    new.append(name + "/" + value)
    resp = _call(
        "/repos/%s/issues/%s/labels"
        % (_GITHUB_REPO, os.environ["KOKORO_GITHUB_PULL_REQUEST_NUMBER"]),
        method="PUT",
        json=new,
    )
    print("Result of setting labels on PR:", resp.text)


def label_increase_decrease_on_pr(name, change, significant):
    if change <= -significant:
        label_significance_on_pr(name, -1, _INCREASE_DECREASE)
    elif change >= significant:
        label_significance_on_pr(name, 1, _INCREASE_DECREASE)
    else:
        label_significance_on_pr(name, 0, _INCREASE_DECREASE)