#!/usr/bin/python2
# coding=utf-8
#
# The Qubes OS Project, https://www.qubes-os.org/
#
# Copyright (C) 2015
#       Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from argparse import ArgumentParser
import os
import subprocess
import sys
import re
import requests

github_issues_repo = "QubesOS/qubes-issues"
github_api_prefix = "https://api.github.com"
github_repo_prefix = "QubesOS/qubes-"
github_baseurl = "https://github.com/"

fixes_re = re.compile(r"(fixes|closes)( (https://github.com/[^ ]+/|"
                      r"QubesOS/Qubes-issues#)[0-9]+)",
                      re.IGNORECASE)
cleanup_re = re.compile(r"[^ ]+[#/]")
release_name_re = re.compile("r[0-9.]+")
number_re = re.compile(r"[0-9]+")

auth_headers = {}


def comment_issue(issue, message, add_label, delete_label,
        github_repo=github_issues_repo):
    resp = requests.post("{}/repos/{}/issues/{}/comments".format(
        github_api_prefix,
        github_repo,
        issue),
        json={'body': message},
        headers=auth_headers)
    if not resp.ok:
        print "WARNING: failed to create comment on {}: {} {}".format(
            issue, resp.status_code, resp.content)
    if delete_label:
        resp = requests.delete("{}/repos/{}/issues/{}/labels/{}".format(
            github_api_prefix,
            github_repo,
            issue,
            delete_label
        ),
            headers=auth_headers)
        # ignore 404 error - most likely package was uploaded to testing
        # before introducing this mechanism
        if not resp.ok and resp.status_code != 404:
            print "WARNING: failed to delete {} label from issue {}: " \
                  "{} {}".format(delete_label, issue,
                resp.status_code, resp.content)
    if add_label:
        resp = requests.post("{}/repos/{}/issues/{}/labels".format(
            github_api_prefix, github_repo, issue),
            json=[add_label],
            headers=auth_headers)
        if not resp.ok:
            print "WARNING: failed to add {} label to issue {}: {} {}". \
                format(add_label, issue,
                resp.status_code, resp.content)

def get_issue_no_from_http_resp(resp):
    # search for the first line with '"number":' and return that number
    # this intentionally avoids throwing json parser at the response (one
    #  parser less to trust, even if simple one)
    for line in resp.content.splitlines():
        if '"number":' in line:
            match = number_re.search(line)
            if match:
                return int(match.group(0))
            # don't look at other such lines
            break
    return None

def search_or_create_issue(github_repo, release, component,
        version, commit_sha,
        create=True):
    # first look into cache
    try:
        cachedir = os.environ['GITHUB_ISSUES_CACHE']
    except KeyError:
        try:
            cachedir = os.path.join(
                os.environ['HOME'], '.cache', 'builder-github-issues'
            )
        except KeyError:
            cachedir = None
    if not os.path.exists(cachedir):
        os.makedirs(cachedir)
    cachefile_path = os.path.join(cachedir, '{}-{}-{}'.format(
        component, version, release))
    if os.path.exists(cachefile_path):
        with open(cachefile_path) as cachefile:
            issue_no = int(cachefile.read().strip())
        return issue_no

    # then search
    issue_title = '{component} {version} ({release})'.format(
        component=component,
        version=version,
        release=release
    )
    issue_no = None
    search_url = 'https://api.github.com/search/issues/' \
                 'q=repo:{github_repo}+"{issue_title}"&sort=created'.format(
                    github_repo=github_repo,
                    issue_title=issue_title.replace(' ', '+'))
    resp = requests.get(search_url, headers=auth_headers)
    if resp.ok:
        issue_no = get_issue_no_from_http_resp(resp)

    # if still nothing, create new issue
    if issue_no is None:
        # don't create if requested so
        if not create:
            return None

        message_template_path = os.path.join(
            os.path.dirname(sys.argv[0]),
            'message-build-report'
        )
        if not os.path.exists(message_template_path):
            print "WARNING: {} does not exist".format(message_template_path)
            return None

        with open(message_template_path) as f:
            message_template = f.read()

        git_url_var = 'GIT_URL_' + component.replace('-', '_')
        if git_url_var in os.environ:
            git_url = git_url_var
        else:
            git_url = '{github}{prefix}{component}'.format(
                github=github_baseurl,
                prefix=github_repo_prefix,
                component=component
            )

        message = message_template. \
            replace("@COMPONENT@", component). \
            replace("@RELEASE_NAME@", release). \
            replace("@VERSION@", version). \
            replace("@COMMIT_SHA@", commit_sha). \
            replace("@GIT_URL@", git_url)

        # and actually create the issue
        resp = requests.post(
            'https://api.github.com/repos/{}/issues'.format(github_repo),
            json={'title': issue_title, 'body': message},
            headers=auth_headers)
        if resp.ok:
            issue_no = get_issue_no_from_http_resp(resp)
        else:
            print "WARNING: failed to create issue: {}".format(resp.reason)

    # if anything found/created - cache this value
    if issue_no:
        with open(cachefile_path, 'w') as cachefile:
            cachefile.write(str(issue_no))

    return issue_no

def notify_closed_issues(args, repo_type,
        current_commit, previous_commit,
        add_label, delete_label):
    message_template_path = "{}/message-{}-{}".format(
        os.path.dirname(sys.argv[0]),
        repo_type,
        args.package_set)

    if os.path.exists(message_template_path + "-" + args.dist):
        message_template_path = message_template_path + "-" + args.dist

    if not os.path.exists(message_template_path):
        print "ERROR: message template {} doesn't exist".format(
            message_template_path)
        sys.exit(1)

    git_log_proc = subprocess.Popen(
        ['git', '-C', args.src_dir, 'log', '{}..{}'.format(previous_commit,
                                                           current_commit)],
        stdout=subprocess.PIPE)
    (git_log, _) = git_log_proc.communicate()
    closed_issues = []
    for line in git_log.splitlines():
        match = fixes_re.search(line)
        if match:
            issues_string = match.group(0)
            issues_numbers = map(lambda s: int(cleanup_re.sub("", s)),
                                 issues_string.split()[1:])
            closed_issues.extend(issues_numbers)

    closed_issues = set(closed_issues)

    git_log_proc = subprocess.Popen(
        ['git', '-C', args.src_dir, 'log',
         '--pretty=format:{}-{}@%h %s'.format(
             github_repo_prefix,
             os.path.basename(args.src_dir)),
         '{}..{}'.format(previous_commit, current_commit)],
        stdout=subprocess.PIPE)
    (shortlog, _) = git_log_proc.communicate()

    git_url_var = 'GIT_URL_' + os.path.basename(args.src_dir).replace('-', '_')
    if git_url_var in os.environ:
        git_url = git_url_var
    else:
        git_url = "{base}/{prefix}{repo}".format(
            base=github_baseurl,
            prefix=github_repo_prefix,
            repo=os.path.basename(args.src_dir))
    git_log_url = \
        "{git_url}/compare/{prev_commit}...{curr_commit}".format(
            git_url=git_url,
            prev_commit=previous_commit,
            curr_commit=current_commit
        )

    for issue in closed_issues:
        print "Adding a comment to issue #{}".format(issue)
        message = open(message_template_path, 'r').read().\
            replace("@DIST@", args.dist).\
            replace("@PACKAGE_SET@", args.package_set).\
            replace("@PACKAGE_NAME@", args.package_name).\
            replace("@COMPONENT@", os.environ['COMPONENT']).\
            replace("@REPOSITORY@", repo_type).\
            replace("@RELEASE_NAME@", args.release_name).\
            replace("@GIT_LOG@", shortlog).\
            replace("@GIT_LOG_URL@", git_log_url)

        comment_issue(issue, message, add_label, delete_label)


def notify_build_report(args, dist_label, repo_type, commit_sha,
        add_label, delete_label):
    if 'GITHUB_BUILD_REPORT_REPO' not in os.environ:
        return

    github_report_repo = os.environ['GITHUB_BUILD_REPORT_REPO']
    if args.build_log:
        report_message = \
            "Package for {dist} was built ([build log]({build_log})) and " \
            "uploaded to {repo_type} repository".format(
                dist=dist_label,
                build_log=args.build_log,
                repo_type=repo_type
            )
    else:
        report_message = \
            "Package for {dist} was uploaded to {repo_type} repository".format(
                dist=dist_label, repo_type=repo_type)

    git_proc = subprocess.Popen(
        ['git', '-C', args.src_dir, 'tag', '--list', '--points-at=HEAD', 'v*'],
        stdout=subprocess.PIPE)
    (version_tags, _) = git_proc.communicate()
    version = version_tags.splitlines()[0]

    report_issue_no = search_or_create_issue(
        github_report_repo, args.release_name, os.environ['COMPONENT'],
        version=version, commit_sha=commit_sha,
        create=bool(args.build_log))
    if report_issue_no:
        comment_issue(report_issue_no, report_message,
            add_label, delete_label, github_repo=github_report_repo)


def main():
    global auth_headers
    epilog = "When state_file doesn't exists, no notify is sent, but the " \
             "current state is recorded"

    parser = ArgumentParser(epilog=epilog)
    parser.add_argument("release_name",
                        help="Release name, for example r3.0")
    parser.add_argument("repo_type",
                        help="Repository type",
                        choices=['current', 'current-testing',
                                 'security-testing', 'unstable'])
    parser.add_argument("src_dir",
                        help="Component sources path")
    parser.add_argument("package_name",
                        help="Binary package name")
    parser.add_argument("dist",
                        help="Distribution release code name")
    parser.add_argument("package_set",
                        choices=['dom0', 'vm'])
    parser.add_argument("state_file",
                        help="File to store internal state (previous commit "
                             "id)")
    parser.add_argument("--auth-token",
                        help="Github authentication token (OAuth2)")
    parser.add_argument("--build-log",
                        help="Build log name in build-logs repository")
    args = parser.parse_args()

    if args.auth_token:
        auth_headers['Authorization'] = \
            'token {}'.format(args.auth_token)
    elif 'GITHUB_API_KEY' in os.environ:
        auth_headers['Authorization'] = \
            'token {}'.format(os.environ['GITHUB_API_KEY'])

    delete_label = None
    add_label = None
    dist_label = args.dist
    if args.package_set == 'dom0':
        dist_label = 'dom0'
    if args.repo_type == 'current':
        repo_type = 'stable'
        delete_label = "{}-{}-testing".format(args.release_name, dist_label)
        add_label = "{}-{}-stable".format(args.release_name, dist_label)
    elif args.repo_type == 'current-testing':
        add_label = "{}-{}-testing".format(args.release_name, dist_label)
        repo_type = 'testing'
    elif args.repo_type == 'security-testing':
        repo_type = 'security-testing'
    else:
        print "Ignoring {}".format(args.repo_type)
        return

    if not release_name_re.match(args.release_name):
        print "Ignoring release {}".format(args.release_name)
        return

    git_proc = subprocess.Popen(
        ['git', '-C', args.src_dir, 'log', '-n', '1', '--pretty=format:%H'],
        stdout=subprocess.PIPE)
    (current_commit, _) = git_proc.communicate()
    current_commit = current_commit.strip()
    if not os.path.exists(args.state_file):
        print "WARNING: {} does not exist, initializing with the " \
              "current state".format(args.state_file)
        previous_commit = None
    else:
        with open(args.state_file, 'r') as f:
            previous_commit = f.readline().strip()

    if previous_commit is not None:
        if repo_type in ['stable', 'testing']:
            notify_closed_issues(args, repo_type, current_commit,
                previous_commit, add_label, delete_label)

    with open(args.state_file, 'w') as f:
        f.write(current_commit)

    notify_build_report(args, dist_label, repo_type, current_commit,
        add_label, delete_label)

if __name__ == '__main__':
    main()
