#!/usr/bin/env python
"""Simple tools to query github.com and gather repo information.

Taken from ipython

"""
# ----------------------------------------------------------------------------
# Imports
# ----------------------------------------------------------------------------
from distutils.version import LooseVersion

import json
import re
import sys
import os
import argparse
import operator

from datetime import datetime, timedelta
from subprocess import check_output

from urllib.request import urlopen

# ----------------------------------------------------------------------------
# Globals
# ----------------------------------------------------------------------------

ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
PER_PAGE = 100

element_pat = re.compile(r'<(.+?)>')
rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')

LAST_RELEASE = datetime(2015, 3, 18)
CONTRIBUTORS_FILE = "contributors.json"
GH_TOKEN = os.environ.get('GH_TOKEN', '')
TOKEN_URL = "access_token={}".format(GH_TOKEN) if GH_TOKEN else ''


# ----------------------------------------------------------------------------
# Functions
# ----------------------------------------------------------------------------


def fetch_url(url):
    if TOKEN_URL:
        if "?" in url:
            url += "&{0}".format(TOKEN_URL)
        else:
            url += "?{0}".format(TOKEN_URL)
    try:
        print("fetching %s" % url, file=sys.stderr)
        # url = Request(url,
        #               headers={'Accept': 'application/vnd.github.v3+json',
        #                        'User-agent': 'Defined'})
        f = urlopen(url)
    except Exception as e:
        print(e)
        print("return Empty data", file=sys.stderr)
        return {}

    return f


def parse_link_header(headers):
    link_s = headers.get('link', '')
    urls = element_pat.findall(link_s)
    rels = rel_pat.findall(link_s)
    d = {}
    for rel, url in zip(rels, urls):
        d[rel] = url
    return d


def get_json_from_url(url):
    """Fetch and read url."""
    f = fetch_url(url)
    return json.load(f) if f else {}


def get_paged_request(url):
    """Get a full list, handling APIv3's paging."""
    results = []
    while url:
        f = fetch_url(url)
        if not f:
            continue
        results.extend(json.load(f))
        links = parse_link_header(f.headers)
        url = links.get('next')
    return results


def get_issues(project="skoudoro/mailerlite-api-python", state="closed",
               pulls=False):
    """Get a list of the issues from the Github API."""
    which = 'pulls' if pulls else 'issues'
    url = "https://api.github.com/repos/%s/%s?state=%s&per_page=%i" % \
        (project, which, state, PER_PAGE)
    return get_paged_request(url)


def get_tags(project="skoudoro/mailerlite-api-python"):
    """Get a list of the tags from the Github API."""
    url = "https://api.github.com/repos/{0}/tags".format(project)
    return get_paged_request(url)


def fetch_basic_stats(project="skoudoro/mailerlite-api-python"):
    """Fetch the basic stats.

    Returns
    -------
    basic_stats : dict
        A dictionary containing basic statistics. For example:
        {'subscribers': 41,
         'forks': 142,
         'forks_url': 'https://github.com/skoudoro/mailerlite-api-python/network'
         'watchers': 94,
         'open_issues': 154,
         'stars': 94,
         'stars_url': 'https://github.com/skoudoro/mailerlite-api-python/stargazers'
        }

    """
    desired_keys = ["stargazers_count", "stargazers_url", "watchers_count",
                    "watchers_url", "forks_count", "forks_url", "open_issues",
                    "issues_url", "subscribers_count", "subscribers_url"]
    url = "https://api.github.com/repos/{0}".format(project)
    r_json = get_json_from_url(url)
    basic_stats = dict((k, r_json[k]) for k in desired_keys if k in r_json)
    return basic_stats


def fetch_contributor_stats(project="skoudoro/mailerlite-api-python"):
    """Fetch stats of contributors.

    Returns
    -------
    contributor_stats : dict
        A dictionary containing contributor statistics. For example:
        {'total_contributors': 50,
         'total_commits': 6031,
         'contributors': [ {
            "user_name": "Garyfallidis"
            "avatar_url":"https://avatars.githubusercontent.com/u/134276?v=3",
            "html_url": "https://github.com/Garyfallidis",
            "total_commits": 1389,
            "total_additions": 116712,
            "total_deletions": 70340,
            "weekly_commits": [
                        {
                        "w": "1367712000",
                        "a": 6898,
                        "d": 77,
                        "c": 10
                        },
                    ]
            },
            ]
        }

    """
    url = "https://api.github.com/repos/{0}/stats/contributors".format(project)
    r_json = get_json_from_url(url)

    contributor_stats = {}
    contributor_stats["total_contributors"] = len(r_json)
    contributor_stats["contributors"] = []

    cumulative_commits = 0
    desired_keys = ["login", "avatar_url", "html_url"]
    with open(os.path.join(os.path.dirname(__file__), "..",
                           CONTRIBUTORS_FILE)) as f:
        extra_info = json.load(f)
        extra_info = extra_info["team"] + extra_info["core_team"]

    for contributor in r_json:
        contributor_dict = dict((k, contributor["author"][k])
                                for k in desired_keys
                                if k in contributor["author"])

        # check if "author" is null
        if not contributor_dict["login"]:
            continue

        # Replace key name
        contributor_dict["username"] = usrname = contributor_dict.pop("login")
        contributor_dict["nb_commits"] = contributor["total"]

        # Add extra information like fullname and affiliation
        l_extra_info = [e for e in extra_info
                        if e["username"].lower() == usrname.lower()]

        l_extra_info = l_extra_info[0] if l_extra_info else {}
        contributor_dict.update(l_extra_info)

        # Update total commits
        cumulative_commits += contributor_dict["nb_commits"]

        total_additions = 0
        total_deletions = 0
        for week in contributor["weeks"]:
            total_additions += week['a']
            total_deletions += week['d']

        contributor_dict["total_additions"] = total_additions
        contributor_dict["total_deletions"] = total_deletions
        # contributor_dict["weekly_commits"] = contributor["weeks"]
        contributor_stats["contributors"].insert(0, contributor_dict)

    contributor_stats["total_commits"] = cumulative_commits
    return contributor_stats


def _parse_datetime(s):
    """Parse dates in the format returned by the Github API."""
    if s:
        return datetime.strptime(s, ISO8601)
    else:
        return datetime.fromtimestamp(0)


def issues2dict(issues):
    """Convert a list of issues to a dict, keyed by issue number."""
    idict = {}
    for i in issues:
        idict[i['number']] = i
    return idict


def is_pull_request(issue):
    """Return True if the given issue is a pull request."""
    return 'pull_request_url' in issue


def issues_closed_since(period=LAST_RELEASE,
                        project="skoudoro/mailerlite-api-python",
                        pulls=False):
    """Get all issues closed since a particular point in time.

    period can either be a datetime object, or a timedelta object. In the
    latter case, it is used as a time before the present.

    """
    which = 'pulls' if pulls else 'issues'

    if isinstance(period, timedelta):
        period = datetime.now() - period
    url = ("https://api.github.com/repos/%s/%s?state=closed&sort=updated&"
           "since=%s&per_page=%i") % (project, which,
                                      period.strftime(ISO8601), PER_PAGE)
    allclosed = get_paged_request(url)
    # allclosed = get_issues(project=project, state='closed', pulls=pulls,
    #                        since=period)
    filtered = [i for i in allclosed
                if _parse_datetime(i['closed_at']) > period]

    # exclude rejected PRs
    if pulls:
        filtered = [pr for pr in filtered if pr['merged_at']]

    return filtered


def sorted_by_field(issues, field='closed_at', reverse=False):
    """Return a list of issues sorted by closing date date."""
    return sorted(issues, key=lambda i: i[field], reverse=reverse)


def report(issues, show_urls=False):
    """Summary report about a list of issues, printing number and title."""
    # titles may have unicode in them, so we must encode everything below
    if show_urls:
        for i in issues:
            role = 'ghpull' if 'merged_at' in i else 'ghissue'
            print('* :%s:`%d`: %s' % (role, i['number'],
                                      i['title']))
    else:
        for i in issues:
            print('* %d: %s' % (i['number'], i['title']))


def get_all_versions(ignore='', project="skoudoro/mailerlite-api-python"):
    """Return all releases version.

    Parameters
    ----------
    ignore: str
        skip a version number between (default: '')
        you can skip minor, or micro version number, it
        will be replace by x
    project: str
        repo path

    Returns
    -------
    l_version: list of str
        versions number list

    """
    tags = get_tags(project=project)
    l_version = [t['name'] for t in tags]

    if ignore.lower() in ['micro', 'minor']:
        l_version = list(set([re.sub(r'(\d+)$', 'x', v) for v in l_version]))

    if ignore.lower() == 'minor':
        l_version = list(set([re.sub(r'\.(\d+)\.', '.x.', v)
                              for v in l_version]))

    return l_version


def version_compare(current_version, version_number, op='eq',
                    all_versions=None):
    """Compare doc version. This is afilter for sphinx template."""
    p = re.compile(r'(\d+)\.(\d+)')
    d_operator = {'<': operator.lt,
                  'lt': operator.lt,
                  '<=': operator.le,
                  'le': operator.le,
                  '>': operator.gt,
                  'gt': operator.gt,
                  '>=': operator.ge,
                  'ge': operator.ge,
                  '==': operator.eq,
                  '=': operator.eq,
                  'eq': operator.eq,
                  '!=': operator.ne,
                  }
    # Setup default value to op
    if op not in d_operator.keys():
        op = 'eq'

    # check dev page
    if current_version.lower() == 'dev':
        return 'post' in version_number

    # major and minor extraction
    current = p.search(current_version)
    ref = p.search(version_number)

    if current_version.lower() == 'latest':
        # Check if it is the latest release
        all_versions = all_versions or get_all_versions()
        if not all_versions:
            return False
        last_version = sorted(all_versions)[0]
        last_version = p.search(last_version)
        if LooseVersion(last_version.group()) ==  \
           LooseVersion(ref.group()) and \
           'post' not in version_number:
            return True
        return False

    if 'post' in version_number:
        return False

    return d_operator[op](LooseVersion(current.group()),
                          LooseVersion(ref.group()))


def username_to_fullname(all_authors):
    contributors_file = os.path.join(os.path.dirname(__file__), "..",
                                     CONTRIBUTORS_FILE)

    if not os.path.isfile(contributors_file):
        return all_authors

    with open(contributors_file) as f:
        extra_info = json.load(f)
        extra_info = extra_info["team"] + extra_info["core_team"]
        extra_info = {i["username"]: i['fullname'] for i in extra_info}

    curent_authors = all_authors
    for i, author in enumerate(all_authors):
        if author[2:] in extra_info.keys():
            curent_authors[i] = '* ' + extra_info[author[2:]]

    return curent_authors


def github_stats(**kwargs):
    """Get release github stats."""
    # Whether to add reST urls for all issues in printout.
    show_urls = True
    save = kwargs.get('save', None)
    if save:
        version = kwargs.get('version', 'vx.x.x')
        fname = "releasev" + version + '.rst'
        fpath = os.path.join(os.path.dirname(__file__), '..', 'release_notes',
                             fname)
        f = open(fpath, 'w')
        orig_stdout = sys.stdout
        sys.stdout = f
        print(".. _{}".format(fname))
        print()
        print(("==============================\n"
               " Release notes version {}\n"
               "==============================").format(version))

    # By default, search one month back
    tag = kwargs.get('tag', None)
    if tag is None:
        if len(sys.argv) > 1:
            try:
                days = int(sys.argv[1])
            except Exception:
                tag = sys.argv[1]
        else:
            tag = check_output(['git', 'describe', '--abbrev=0'],
                               universal_newlines=True).strip()

    if tag:
        cmd = ['git', 'log', '-1', '--format=%ai', tag]
        tagday, _ = check_output(cmd,
                                 universal_newlines=True).strip().rsplit(' ',
                                                                         1)
        since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S")
    else:
        since = datetime.now() - timedelta(days=days)

    print("fetching GitHub stats since %s (tag: %s)" % (since, tag),
          file=sys.stderr)

    issues = issues_closed_since(since, pulls=False)
    pulls = issues_closed_since(since, pulls=True)

    # For regular reports, it's nice to show them in reverse chronological
    # order
    issues = sorted_by_field(issues, reverse=True)
    pulls = sorted_by_field(pulls, reverse=True)

    n_issues, n_pulls = map(len, (issues, pulls))
    n_total = n_issues + n_pulls

    # Print summary report we can directly include into release notes.
    print()
    since_day = since.strftime("%Y/%m/%d")
    today = datetime.today().strftime("%Y/%m/%d")
    print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag))
    print()
    print("These lists are automatically generated, and may be incomplete or"
          " contain duplicates.")
    print()
    if tag:
        # print git info, in addition to GitHub info:
        since_tag = tag+'..'
        cmd = ['git', 'log', '--oneline', since_tag]
        ncommits = check_output(cmd, universal_newlines=True).splitlines()
        ncommits = len(ncommits)

        author_cmd = ['git', 'log', '--format=* %aN', since_tag]
        all_authors = check_output(author_cmd, universal_newlines=True) \
            .splitlines()
        # Replace username by author name
        all_authors = username_to_fullname(all_authors)
        unique_authors = sorted(set(all_authors))

        if len(unique_authors) == 0:
            print("No commits during this period.")
        else:
            print("The following %i authors contributed %i commits." %
                  (len(unique_authors), ncommits))
            print()
            print('\n'.join(unique_authors))
            print()

            print()
            print("We closed a total of %d issues, %d pull requests and %d"
                  " regular issues;\nthis is the full list (generated with"
                  " the script \n:file:`tools/github_stats.py`):" %
                  (n_total, n_pulls, n_issues))
            print()
            print('Pull Requests (%d):\n' % n_pulls)
            report(pulls, show_urls)
            print()
            print('Issues (%d):\n' % n_issues)
            report(issues, show_urls)

    if save:
        sys.stdout = orig_stdout
        f.close()


# ----------------------------------------------------------------------------
# Sphinx connection
# ----------------------------------------------------------------------------
def add_jinja_filters(app):
    app.builder.templates.environment.filters['version_compare'] = \
        version_compare


def setup(app):
    """
    - Create releases information
    - Collect and clean authors
    - Adds extra jinja filters.
    """
    app.connect("builder-inited", add_jinja_filters)
    app.add_stylesheet("css/custom_github.css")


# ----------------------------------------------------------------------------
# Main script
# ----------------------------------------------------------------------------


if __name__ == "__main__":
    # e.g github_tools.py --tag=v0.1.3 --save --version=0.1.4
    parser = argparse.ArgumentParser()
    parser.add_argument("--tag", type=str, default=None,
                        help='from which tag version to get information')
    parser.add_argument("--version", type=str, default='',
                        help='current release version')
    parser.add_argument("--save", dest='save', action='store_true',
                        default=False, help=("Save in the release folder"
                                             "and add rst header")
                        )

    args = parser.parse_args()
    github_stats(**vars(args))
