#!/usr/bin/python3 -s
import argparse
import json
import os
import sys
import re
import subprocess
from collections import namedtuple
from typing import Optional

import colored
from colored import stylize
import requests


def make_pr_factory(current_user: str):
    def make_pr(pull, with_author=False, with_number=True, with_reviews=False, with_branch=False, with_status=True,
                with_conflicts=False):
        author = ''
        number = ''
        reviews = ''
        status = ''

        if with_author:
            author = '({}) '.format(stylize(pull['author']['login'], colored.fg('red')))

        if with_number:
            number = '({}) '.format(stylize('#{}'.format(pull['number']), colored.fg('green')))

        commits = pull.get('commits', {}).get('nodes', [])
        if with_status and len(commits) > 0:
            last_commit = commits[0]['commit']
            status_obj = last_commit['status']
            if status_obj:
                state = status_obj['state']
                if state == 'SUCCESS':
                    status = stylize('✓', colored.fg('green'))
                elif state in ['FAILURE', 'ERROR']:
                    status = stylize('✘', colored.fg('red'))
                elif state == 'PENDING':
                    status = stylize('●', colored.fg('yellow'))

                status = '' + status

        if with_conflicts and pull['mergeable'] == 'CONFLICTING':
            status = ' ({}) {}'.format(stylize('conflicting', colored.fg('red')), status)

        url = stylize(pull['url'], colored.fg('blue'))

        line = [
            ' - {title} {number}{author}{status}'.format(number=number, title=pull['title'], author=author,
                                                         status=status),
            '   {url} {reviews}'.format(url=url, reviews=reviews)
        ]
        if with_reviews:
            user_review_states = []

            for review in pull['reviews']['nodes']:
                user_review_states.append((review['author']['login'], review['state']))

            for request in pull['reviewRequests']['nodes']:
                user_review_states.append((request['requestedReviewer']['login'], 'REQUESTED'))

            user_states = {}
            for (user, state) in reversed(user_review_states):
                if user == current_user:
                    continue

                if user in user_states and user_states[user] != 'COMMENTED':
                    continue

                user_states[user] = state

            approvals = [k for (k, v) in user_states.items() if v == 'APPROVED']
            pending = [k for (k, v) in user_states.items() if v != 'APPROVED']
            changes_required = 'CHANGES_REQUESTED' in user_states.values()

            components = []
            if (len(approvals)):
                components.append('{} approvals ({})'.format(len(approvals), ', '.join(approvals)))
            if (len(pending)):
                components.append('{} pending ({})'.format(len(pending), ', '.join(pending)))
            if changes_required:
                components.append('changes required')

            style = None
            components_str = ', '.join(components)
            if len(pending) == 0:
                components_str += ' 🎉'
                style = colored.fg('green') + colored.attr('bold')
            elif changes_required:
                components_str += ' 😭'
                style = colored.fg('red') + colored.attr('bold')
            else:
                style = colored.attr('dim')

            components_str = stylize(components_str, style)
            line.insert(1, '   {}'.format(components_str))

        if with_branch:
            line.append('   {}'.format(stylize(pull['headRefName'], colored.fg('blue') + colored.attr('dim'))))

        return '\n'.join(line)

    return make_pr


def make_title(emoji, text):
    return '{}  '.format(emoji) + stylize(text, colored.fg('white') + colored.attr('underlined')) + '\n'


def check_pending_reviews(data, make_pr):
    pull_requests = data['reviewRequests']['nodes']
    if len(pull_requests) > 0:
        print(make_title('👋', 'You have pending review requests ({})'.format(len(pull_requests))))
        for pull in pull_requests:
            print(make_pr(pull, with_author=True))

        print("")


def check_open_prs(data, repo_namespace, make_pr):
    pull_requests = data['user']['openPRs']['nodes']
    pull_requests = list(filter(lambda req: repo_namespace in req['url'], pull_requests))
    if len(pull_requests) > 0:
        print(make_title('😡', 'Your open pull requests for this repo ({})'.format(len(pull_requests))))
        for pull in pull_requests:
            print(make_pr(pull, with_reviews=True, with_author=False, with_branch=True, with_conflicts=True))
            print("")


def check_current_branch(data, repo_namespace, repo_url, current_branch, make_pr, base_branch='master'):
    print(make_title('🤑', 'Branch Pull Requests:'))
    pull_requests = data['user']['branchPR']['nodes']
    pull_requests = list(filter(lambda req: req['repository']['nameWithOwner'] == repo_namespace, pull_requests))
    if len(pull_requests) > 0:
        for pull in pull_requests:
            print(make_pr(pull))
    else:
        compare_link = '{}/compare/{}...{}'.format(repo_url, base_branch, current_branch)
        compare_link = stylize(compare_link, colored.fg('blue'))

        print(
            '   No pull requests were found for this branch. {}:\n'.format(stylize('Create one', colored.attr('bold'))))
        print('   {}'.format(compare_link))

    print("")


def make_request(user, head_ref, token):
    query = get_query()
    query = query.replace('{username}', user)
    query = query.replace('{branchname}', head_ref)

    resp = requests.post('https://api.github.com/graphql',
                         headers={
                             'Authorization': 'bearer {}'.format(token)
                         },
                         json={'query': query})

    if not resp.ok:
        raise Exception("Request failed ({}): {}".format(resp.status_code, resp.text))

    return resp.json()


def get_query():
    return '''
{
  user(login: "{username}") {
    branchPR: pullRequests(first: 10, headRefName: "{branchname}") {
      nodes {
        number
        title
        url
        repository {
          nameWithOwner
        }
      }
    }
    openPRs: pullRequests(last: 50, states: [OPEN]) {
      totalCount
      nodes {
        number
        url
        title
        headRefName
        mergeable
        commits(last:1) {
          nodes {
            commit {
              oid
              status {
                state
              }
            }
          }
        }
        reviewRequests(last: 50) {
          nodes {
            id
            requestedReviewer {
              ... on User {
                login
                name
              }
            }
          }
        }
        reviews(last: 50, states: [PENDING, COMMENTED, APPROVED, CHANGES_REQUESTED, DISMISSED]) {
          nodes {
            id
            state
            author {
              avatarUrl
              login
              resourcePath
              url
            }
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
  reviewRequests: search(query: "is:open is:pr review-requested:{username} archived:false", type: ISSUE, first: 10) {
    nodes {
      ... on PullRequest {
        number
        title
        url
        author {
          login
        }
      }
    }
  }
} 
'''.strip()


def get_repo_current_branch():
    try:
        return subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode('UTF-8').strip()
    except:
        print("Could not get the repo's current branch")
        exit(1)


namespace_re = re.compile(r'(.*)\.git')


def get_repo_namespace():
    try:
        sp = subprocess.Popen(['git', 'remote', 'get-url', 'origin'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        origin, _ = sp.communicate()
        origin = origin.decode('UTF-8').strip()
        domain, path = origin.split(':')
        result = namespace_re.match(path)
        return result.group(1)
    except:
        print("Could not determine current repo's github address")
        exit(1)


Config = namedtuple('Config', ['username', 'token'])


def get_config_path():
    return os.path.join(os.path.expanduser("~"), '.github_info.json')


def read_config() -> Optional[Config]:
    try:
        with open(get_config_path()) as fh:
            conf = json.load(fh)
            return Config(username=conf['username'], token=conf['token'])
    except:
        return None


def write_config(config: Config):
    with open(get_config_path(), 'w+') as fh:
        json.dump({
            'username': config.username,
            'token': config.token,
        }, fh)


def configure(args):
    print(f'''{stylize('Please acquire a Github personal access token from:', colored.fg('white'))}
    
{stylize('https://github.com/settings/tokens', colored.fg('blue'))}

The following scopes should be sufficient:
- repo (Full control of private repositories)

''')
    token = input("Access Token: ")
    if token.strip() == '':
        print("No access token provided")
        exit(1)

    username = input("Github Username: ")
    if username.strip() == '':
        print("No username provided")
        exit(1)

    write_config(Config(token=token, username=username))
    print("All done! Thanks!")


def run(args):
    config = read_config()
    if config is None:
        print(f"Please run '{sys.argv[0]} configure' command first.")
        exit(1)

    current_branch = get_repo_current_branch()
    repo_namespace = get_repo_namespace()
    make_pr = make_pr_factory(config.username)

    response = make_request(config.username, current_branch, config.token)
    data = response['data']
    check_open_prs(data, repo_namespace, make_pr)
    check_pending_reviews(data, make_pr)
    check_current_branch(data, repo_namespace, 'https://github.com/{}'.format(repo_namespace), make_pr=make_pr,
                         current_branch=current_branch)


def main():
    parser = argparse.ArgumentParser(description='Show Github information for a repo.')
    subparsers = parser.add_subparsers()
    configure_parser = subparsers.add_parser('configure', help='Provide credentials and configure the tool')
    configure_parser.set_defaults(func=configure)

    args = parser.parse_args()
    if hasattr(args, 'func'):
        args.func(args)
    else:
        run(args)
main()
