#!/usr/bin/python2
#
# Copyright 2012 SUSE Linux
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import argparse
from datetime import datetime
import glob
import json
import os
import re
import sys
import tarfile
import urllib


GITHUB_CREDS = ".github_tarballs_credentials"
COMMIT_HASH_SIZE = 7
GITHUB_COMPARE = ("https://%(creds)sapi.github.com/repos/"
                  "%(owner)s/%(repo)s/compare/%(base)s...%(head)s")


def github_credentials():
    """Read github credentials from a plain text file

    This helps avoid rate limiting on the github API.
    """
    creds = ""
    try:
        with open(os.path.join(os.path.expanduser('~'), GITHUB_CREDS)) as f:
            creds = f.read().rstrip()
    except IOError:
        pass
    return creds


def download_tarball(url, filename):
    """Download an upstream tarball

    :url: remote source of the tarball
    :filename: where to save the downloaded tarball

    """
    try:
        urllib.urlretrieve(url, filename)
    except IOError as e:
        sys.exit(e)


def get_parent_dir_and_version_from_tarball(tar_name):
    try:
        tar = tarfile.open(tar_name)
        parent_dir = tar.firstmember.name
        version = parent_dir.rsplit('-', 1)[1]
        return (parent_dir, version)
    except IndexError:
        sys.exit("Could not figure out version from directory "
                 "inside tarball: " + tar.firstmember.name)
    finally:
        tar.close()


def get_commit_from_spec(package):
    """Parse the spec's Version field for a previously set commit version

    :package: name of the package

    Returns None in case no commit could be read.

    """
    try:
        f = open(package + '.spec')
        return re.search(r'^Version:\s+.*\+git\.\d+\.(.*?)(\s+#.*)?$',
                         f.read(), flags=re.MULTILINE).group(1)
    except AttributeError:
        return None
    finally:
        f.close()


def package_version(git_compare, upstream_version):
    try:
        upstream_commit = git_compare['commits'][-1]
    except IndexError:
        upstream_commit = git_compare['base_commit']

    sha = upstream_commit['sha'][:COMMIT_HASH_SIZE]

    return '%s+git.%s.%s' % (upstream_version,
                             datetime.now().strftime('%s'),
                             sha)


def update_spec_file(package_version, tarball_parent_dir, filename):
    for specfile in glob.glob('./*.spec'):
        try:
            f = open(specfile, 'r+')
            contents = f.read()
            f.seek(0)
            f.truncate()
            contents = re.sub(r'\n((Version:\s+).*)\n',
                              r'\n\g<2>%s\n' % package_version,
                              contents, count=1)
            contents = re.sub(r'\n((%setup\s+).*)\n',
                              r'\n\g<2>-q -n %s\n' % tarball_parent_dir,
                              contents, count=1)
            contents = re.sub(r'\n((Source0?:\s+).*)\n',
                              r'\n\g<2>%s\n' % filename,
                              contents, count=1)
            f.write(contents)
        finally:
            f.close()


def create_changes(git_compare, package_version, email):
    """Return a string with the new changes for the .changes file

    :git_compare: JSON data from github commit compare
    :package_version: release version string for the .changes file entry
    :email: email address used for the .changes file entry

    """
    timestamp = datetime.utcnow().strftime('%a %b %e %T UTC %Y')

    if not git_compare['commits']:
        return None

    commits = "  + " + "\n  + ".join(
        c['commit']['message'].split('\n')[0]
        for c in git_compare['commits']
        if not c['commit']['message'].startswith('Merge '))

    message = ('- Update to version %(package_version)s:\n'
               '%(commits)s\n') % locals()

    change = (
        '-----------------------------------'
        '---------------------------------\n'
        '%(timestamp)s - %(email)s\n'
        '\n%(message)s\n' % locals())

    return change


def update_changes_file(package, changes):
    try:
        f = open(package + ".changes", "r+")
        contents = f.read()
        f.seek(0)
        f.write(changes)
        f.write(contents)
    finally:
        f.close()


def get_changes(package, owner, repo, target):
    """Return a dict of new commits from github compare

    See http://developer.github.com/v3/repos/commits/#compare-two-commits

    The dict contains:
    {
     "commits": [{
         "url": ...,
         "commit": {
             "message": ...,
             ...
         },
         ...
     }, ...]
    }

    """
    current_commit = get_commit_from_spec(package)
    url = GITHUB_COMPARE % dict(owner=owner,
                                repo=repo,
                                creds=github_credentials(),
                                base=current_commit or target,
                                head=target)
    try:
        res = urllib.urlopen(url)
    except IOError as e:
        sys.exit(e)

    if res.code != 200:
        sys.exit("Could not read commits from %s. %s "
                 % (url, json.loads(res.read())['message']))

    data = json.loads(res.read())

    return data


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Git Tarballs')
    parser.add_argument('--url', required=True,
                        help='upstream tarball URL to download')
    parser.add_argument('--filename',
                        help='where to save the downloaded tarball')
    parser.add_argument('--package',
                        help='the OBS package name')
    parser.add_argument('--repo_owner',
                        help='github repository owner')
    parser.add_argument('--repo_name',
                        help='github repository name')
    parser.add_argument('--target',
                        help='git repository target commit/ref to update to')
    parser.add_argument('--email', required=True,
                        help="commit author's email (for the .changes file)")
    parser.add_argument('--outdir',
                        help='osc service parameter that does nothing')
    args = parser.parse_args()

    if not args.filename:
        args.filename = args.url.rsplit("/", 1)[1]
    if not args.package:
        args.package = os.getcwd().rsplit("/", 1)[1]

    download_tarball(args.url, args.filename)

    tarball_parent_dir, upstream_version = \
        get_parent_dir_and_version_from_tarball(args.filename)

    git_compare = get_changes(args.package, args.repo_owner,
                              args.repo_name, args.target)
    current_version = package_version(git_compare, upstream_version)

    changes = create_changes(git_compare, current_version, args.email)
    if changes:
        update_changes_file(args.package, changes)
        update_spec_file(current_version, tarball_parent_dir, args.filename)
