#!/usr/bin/python3

# Close issues on updates-status:
#  - updates fully uploaded to current (aka stable)
#  - updates superseded by a newer version

import argparse
import requests
import json
updates_repo = "QubesOS/updates-status"

q_template = """query {{
    resource(url:"{repo}") {{
        ... on Repository {{
            issues({pagination} states:OPEN) {{
                edges {{
                    node {{
                        body
                        number
                        createdAt
                        title
                        id
                        labels(first:100) {{
                            nodes {{
                                name
                            }}
                        }}
                    }}
                    cursor
                }}
                pageInfo {{
                    hasNextPage
                }}
            }}
        }}
    }}
}}"""

comment_mutation = """mutation {{
    addComment(input:{{subjectId:"{subjectid!s}",body:"{body!s}"}}) {{
        subject
    }}
}}"""


def graphql(query):
    r = requests.post("https://api.github.com/graphql",
            json={'query': query},
            auth=())
    if not r.ok:
        raise Exception('API call failed: %s' % r.text)
    return r.text


def close_issue(issue_id, issue_no, repo=updates_repo):
    # Github API v4 lack closeIssue mutation
    # https://platform.github.community/t/add-closeissue-mutation/3250/3

    r = requests.patch('https://api.github.com/repos/{}/issues/{}'.format(
                repo, issue_no),
            json={'state': 'closed'}, auth=())
    if not r.ok:
        raise Exception('API call failed: %d %s' % (r.status_code, r.text))

def comment_issue(issue_id, issue_no, body, repo=updates_repo):
    r = requests.post('https://api.github.com/repos/{}/issues/{}/comments'.format(
                repo, issue_no),
            json={'body': body}, auth=())
    if not r.ok:
        raise Exception('API call failed: %d %s' % (r.status_code, r.text))
    return
    # use this when fully switching to API v4:
    comment_graphql = comment_mutation.format(subjectid=issue_id, body=body)
    print(comment_graphql)
    return graphql(comment_graphql)

def add_label(issue_no, label, repo=updates_repo):
    resp = requests.post("https://api.github.com/repos/{}/issues/{}/labels".format(
        repo, issue_no),
        json=[label],
        auth=())
    if not resp.ok:
        print("WARNING: failed to add {} label to issue {}: {} {}".
            format(label, issue_no,
            resp.status_code, resp.content))

parser = argparse.ArgumentParser("cleanup-updates-status")

parser.add_argument('--dry-run', '-n', action='store_true', help='only print what would be done')


def main():
    args = parser.parse_args()

    q = q_template.format(repo=updates_repo, pagination="first:100")

    data = graphql(q)
    data = json.loads(data)
    issues = [e['node'] for e in data['data']['resource']['issues']['edges']]
    while data['data']['resource']['issues']['pageInfo']['hasNextPage']:
        cursor = data['data']['resource']['issues']['edges'][-1]['cursor']
        q = q_template.format(repo=updates_repo, pagination="first:100 after:\"{}\"".format(cursor))
        data = graphql(q)
        data = json.loads(data)
        issues.extend(e['node'] for e in data['data']['resource']['issues']['edges'])

    ### cleanup updates fully uploaded to stable

    for issue in issues:
        issue_no = issue['number']
        labels = [l['name'] for l in issue['labels']['nodes']]
        print('Issue {} labels: {!r}'.format(issue_no, labels))
        # there is at least one -stable label and no -cur-test label is left
        if any(l.endswith('-stable') for l in labels) and \
                all(not l.endswith('-cur-test') for l in labels):
            print('Closing {} (stable): {}'.format(issue_no, issue['title']))
            if not args.dry_run:
                close_issue(issue['id'], issue_no)
        elif any(l.endswith('-stable') for l in labels):
            # some -cur-test left
            print('Partial upload {}: {} ({})'.format(issue_no, issue['title'],
                ' '.join(l for l in labels if l.endswith('-cur-test'))))

    ### cleanup updates superseded by later version

    # dict of component"-"release -> ( number, title, [ labels ] )
    latest_updates = {}
    for issue in sorted(issues, key=lambda x: -int(x['number'])):
        issue_no = issue['number']
        component = issue['title'].split(' ')[0]
        release = issue['title'].split('(')[-1].rstrip(')')
        labels = [l['name'].replace('-cur-test', '').replace('-stable', '') for l in issue['labels']['nodes']
                  if l['name'] != 'buggy']
        key = '{}-{}'.format(component, release)
        if key in latest_updates:
            latest_issue, latest_title, latest_labels = latest_updates[key]
            if all(label in latest_labels for label in labels
                    if label.endswith('cur-test') or label.endswith('-testing')):
                print('Closing {} (superseded by {}): {}'.format(
                    issue_no, latest_issue, issue['title']))
                if not args.dry_run:
                    comment_issue(issue['id'], issue_no, 'Superseded by #{}'.format(latest_issue))
                    close_issue(issue['id'], issue_no)
            else:
                print('Update {} ({}) newer than {} ({}) lack {} labels'.format(
                            latest_issue, latest_title, issue_no, issue['title'],
                            [label for label in labels if label not in latest_labels]))
        else:
            latest_updates[key] = (issue_no, issue['title'], labels)


if __name__ == '__main__':
    main()
