#!/usr/bin/env python

import sys
import os
import os.path
import argparse
import fnmatch
from hashlib import sha1
import requests
import json
import re

# internals
APTLY_SESSION = None

CI_JOB_NAME = 'CI_JOB_NAME'
CI_JOB_UPLOAD_PREFIX = 'upload[:/]'
ICINGA_BUILD_TYPE = 'ICINGA_BUILD_TYPE'
ICINGA_BUILD_RELEASE_TYPE = 'ICINGA_BUILD_RELEASE_TYPE'
ICINGA_BUILD_TYPE_DEFAULT = 'release'

UPLOAD_TYPE_DEB = 'DEB'
UPLOAD_TYPE_RPM = 'RPM'

def scan_dir(path, pattern):
    found = []
    for root, dirs, files in os.walk(path):
        for file in files:
            if fnmatch.fnmatch(file, pattern):
                found.append(os.path.join(root, file))
    return found

def detect_rpm(path):
    rpms = scan_dir(path, '*.rpm')
    if len(rpms) == 0:
        return []
        
    return rpms

def detect_deb(path):
    source = scan_dir(path, '*.dsc')
    tarballs = scan_dir(path, '*.tar*')
    changes = scan_dir(path, '*.changes')
    debs = scan_dir(path, '*.deb')

    if len(source) == 0:
        return []
    elif len(source) != 1:
        raise StandardError, 'There more than one source DSC in ' + path

    files = source + tarballs + changes + debs
    if len(files) < 2:
        raise StandardError, 'There should be at least 2 files in ' + path

    return files

def sha1_file(path):
    h = sha1()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(4096), b""):
            h.update(chunk)
    return h.hexdigest()

def upload_checksum(files):
    files = sorted(files)
    h = sha1()
    d = dict()
    for file in files:
        name = os.path.basename(file)
        d[name] = fh = sha1_file(file)
        h.update(fh)
    return (h.hexdigest(), d)

def ci_split_name():
    """
    Split the CI_JOB_NAME into target and release
    e.g. upload/debian/stretch -> ['debian', 'stretch']
    or upload/stack/dev/debian/stretch -> ['stack/dev/debian', 'stretch']
    """
    job_name = os.environ.get(CI_JOB_NAME)
    if job_name:
        match = re.match("^%s(.+)/([^/]+)$" % CI_JOB_UPLOAD_PREFIX, job_name)
        if match:
            return match.groups()

    return None

def get_release_type():
    """
    Get ICINGA_BUILD_RELEASE_TYPE or ICINGA_BUILD_TYPE from environ
    """
    if os.environ.has_key(ICINGA_BUILD_RELEASE_TYPE):
        return os.environ.get(ICINGA_BUILD_RELEASE_TYPE)

    if os.environ.has_key(ICINGA_BUILD_TYPE):
        return os.environ.get(ICINGA_BUILD_TYPE)

    return ICINGA_BUILD_TYPE_DEFAULT

def ci_release(upload_type, release):
    """
    Build the release name from CI_JOB_NAME and ICINGA_BUILD_TYPE

    Examples:
        DEB / stretch / release -> icinga-stretch
        DEB / stretch / snapshot -> icinga-stretch-snapshots
        DEB / stretch / Y -> icinga-stretch-Y

        RPM / 7 / release -> 7/release
        RPM / X / Y -> X/Y
    """
    build_type = get_release_type()

    if upload_type == UPLOAD_TYPE_DEB:
        publish_release = 'icinga-' + release
        if build_type != 'release':
            publish_release += '-' + build_type

        if build_type == 'snapshot':
            publish_release += 's' # snapshots

        return publish_release
    elif upload_type == UPLOAD_TYPE_RPM:
        return release + '/' + build_type
    else:
        raise StandardError, "Unknown upload type %s" % upload_type

def ci_repo(upload_type, target, release):
    """
    Build the aptly repo from target, release and ICINGA_BUILD_TYPE (only for DEB)

    Examples:
        icinga-debian-stretch-release
        icinga-debian-stretch-snapshot
    """
    if upload_type != UPLOAD_TYPE_DEB:
        raise StandardError, "Repo can only be set on DEB uploads!"

    build_type = get_release_type()

    return 'icinga-%s-%s-%s' % (target, release, build_type)

def aptly_session():
    global APTLY_SESSION
    if APTLY_SESSION is None:
        APTLY_SESSION = s = requests.Session()
        if args.username and args.password:
            s.auth = (args.username, args.password)
        if args.insecure:
            s.verify = False

    return APTLY_SESSION

def aptly_url(url):
    return args.server + url

parser = argparse.ArgumentParser(description='Uploading build results to an Aptly server')
parser.add_argument('--server', help='APTLY API service to talk to (e.g. http://127.0.0.1:8080/api)',
                    default=os.environ.get('APTLY_SERVER', 'http://127.0.0.1:8080/api'), metavar='APTLY_SERVER')
parser.add_argument('--username', help='APTLY API username',
                    default=os.environ.get('APTLY_USERNAME'), metavar='APTLY_USERNAME')
parser.add_argument('--password', help='APTLY API password',
                    default=os.environ.get('APTLY_PASSWORD'), metavar='APTLY_PASSWORD')
parser.add_argument('--result', metavar='path',
                    default='build/', help='Build result to upload')
parser.add_argument('--target', metavar='target',
                    help='Repository to install the package to (e.g. stack/dev/epel or stack/dev/ubuntu)')
parser.add_argument('--release', metavar='release',
                    help='Version of the repository to install to (e.g. 7 or icinga-xenial)')
parser.add_argument('--repo', metavar='repo',
                    help='Specific repository name in aptly')
parser.add_argument('--architectures', metavar='list',
                    default=os.environ.get('ICINGA_BUILD_DEB_DEFAULT_ARCH', 'amd64'),
                    help=('Specify list of architectures to publish the repo with,'
                          'separated by comma (e.g. amd64,i386 or armhf)'))
parser.add_argument('--insecure', action='store_true', help='Disable SSL verification')
parser.add_argument('--noop', action='store_true', help='Only prepare upload')

args = parser.parse_args()

if not args.server and not args.noop:
    raise StandardError, "Specifying an aptly server is required (--server or APTLY_SERVER)"

if not os.path.exists(args.result):
    raise StandardError, "Result path '%s' does not exist!" % (args.result)

rpms = detect_rpm(args.result)
debs = detect_deb(args.result)
files = None
if rpms:
    type = UPLOAD_TYPE_RPM
    files = rpms
elif debs:
    type = UPLOAD_TYPE_DEB
    files = debs

if not files:
    raise StandardError, 'No packages found in %s' % (args.result)

pair = ci_split_name()

if not args.target:
    if pair:
        args.target = pair[0]
    else:
        raise StandardError, "Could not detect --target from %s, please specify!" % CI_JOB_NAME

if not args.release:
    if pair:
        args.release = ci_release(type, pair[1])
    else:
        raise StandardError, "Could not detect --release from %s, please specify!" % CI_JOB_NAME

if not args.repo and type == UPLOAD_TYPE_DEB:
    if pair:
        args.repo = ci_repo(type, *pair)
    else:
        raise StandardError, "Could not detect --repo from %s, please specify!" % CI_JOB_NAME

(checksum, checksums) = upload_checksum(files)
upload_prefix = re.sub('[/\-_]+', '_', args.target + '_' + args.release)
upload_name = '%s_%s' % (upload_prefix, checksum)

# meta and upload file
upload_meta = {
    'target': args.target,
    'release': args.release,
    'type': type,
    'checksums': checksums,
}

if args.repo and type == UPLOAD_TYPE_DEB:
    upload_meta['repo'] = args.repo

if args.architectures:
    upload_meta['architectures'] = re.split(r'\s*,\s*', args.architectures)

    # always add i386 if amd64 is base arch
    if 'amd64' in upload_meta['architectures'] and not 'i386' in upload_meta['architectures']:
        upload_meta['architectures'].append('i386')

print "Prepared upload to: %s" % (aptly_url('/files/' + upload_name))
print "Metadata is:" + json.dumps(upload_meta, sort_keys=True, indent=4)
print

if args.noop:
    print "Running in noop mode, stopping here!"
    sys.exit(0)

# ensure target is absent
r = aptly_session().delete(aptly_url('/files/' + upload_name))
if r.status_code == requests.codes.ok:
    print "Deleted existing upload %s" % (upload_name)
elif r.status_code != requests.codes.not_found:
    raise StandardError, 'Unexpected result code: %s' % (r.status_code)

# uploading files
upload_url = aptly_url('/files/' + upload_name)
print "Uploading %d files to %s" % (len(files), upload_name)
for file in files:
    file_data = [('file', (file, open(file, 'rb')))]
    r = aptly_session().post(upload_url, files=file_data)
    if r.status_code == requests.codes.ok:
        print "Upload successful: %s" % (file)
    else:
        raise StandardError, "Upload failed for %s - http status: %s - message:\n%s" % (upload_name, r.status_code, r.text[:30])

file_data = [('file', ('upload.json', json.dumps(upload_meta)))]
r = aptly_session().post(upload_url, files=file_data)
if r.status_code == requests.codes.ok:
    print 'Metadata upload successful.'
else:
    raise StandardError, "Upload metadata failed for %s - http status: %s - message:\n%s" % (upload_name, r.status_code, r.text[:30])