#!/usr/bin/env python
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:
- user
- public_repo
- repo
- repo_deployment
- repo:status
- read:repo_hook
- read:org
- read:public_key
- read:gpg_key
''')
    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()
