#!/usr/bin/python
# -*- coding: utf-8 -*-
# ##################################################################
#
# Copyright (C) 2010, Evan Broder <evan@ebroder.net>
# Copyright (C) 2010, Benjamin Drung <bdrung@ubuntu.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# See file /usr/share/common-licenses/GPL-2 for more details.
#
# ##################################################################

import optparse
import os
import shutil
import subprocess
import sys
import tempfile
import urllib

from debian.deb822 import Dsc
import launchpadlib.launchpad
import lsb_release

from ubuntutools.config import get_value, ubu_email
from ubuntutools.builder import getBuilder
from ubuntutools.logger import Logger
from ubuntutools.question import YesNoQuestion

def error(msg):
    Logger.error(msg)
    sys.exit(1)

def check_call(cmd, *args, **kwargs):
    Logger.command(cmd)
    ret = subprocess.call(cmd, *args, **kwargs)
    if ret != 0:
        error('%s returned %d.' % (cmd, ret))

def parse(args):
    usage = 'Usage: %prog [options] <source package name or .dsc URL/file>'
    p = optparse.OptionParser(usage)
    p.add_option('-d', '--destination',
                 dest='dest_releases',
                 default=[],
                 action='append',
                 help='Backport to DEST release (default: current release)',
                 metavar='DEST')
    p.add_option('-s', '--source',
                 dest='source_release',
                 default=None,
                 help='Backport from SOURCE release (default: devel release)',
                 metavar='SOURCE')
    p.add_option('-S', '--suffix',
                 dest='suffix',
                 default=None,
                 help='Suffix to append to version number (default: ~ppa1)',
                 metavar='SUFFIX')
    p.add_option('-b', '--build',
                 dest='build',
                 default=False,
                 action='store_true',
                 help='Build the package before uploading (default: %default)')
    p.add_option('-B', '--builder',
                 dest='builder',
                 default=get_value('BUILDER'),
                 help='Specify the package builder (default: %default)',
                 metavar='BUILDER')
    p.add_option('-U', '--update',
                 dest='update',
                 default=get_value('UPDATE_BUILDER'),
                 action='store_true',
                 help='Update the build environment before attempting to build')
    p.add_option('-u', '--upload',
                 dest='upload',
                 help='Specify an upload destination',
                 metavar='UPLOAD')
    p.add_option('-y', '--yes',
                 dest='prompt',
                 default=True,
                 action='store_false',
                 help='Do not prompt before uploading to a PPA')
    p.add_option('-v', '--version',
                 dest='version',
                 default=None,
                 help='Package version to backport (or verify)',
                 metavar='VERSION')
    p.add_option('-w', '--workdir',
                 dest='workdir',
                 default=get_value('WORKDIR'),
                 help='Specify a working directory (default: temporary dir)',
                 metavar='WORKDIR')
    p.add_option('-l', '--launchpad',
                 dest='launchpad',
                 default=get_value('LPINSTANCE'),
                 help='Launchpad instance to connect to (default: %default)',
                 metavar='INSTANCE')
    p.add_option('--no-conf', '--noconf',
                 dest='no_configuration',
                 default=False,
                 help="Don't read config files, must be the first option given",
                 action='store_true')

    opts, args = p.parse_args(args)
    if len(args) != 1:
        p.error('You must specify a single source package or a .dsc URL/path.')
    if not opts.upload and not opts.workdir:
        p.error('Please specify either a working dir or an upload target!')

    return opts, args

def find_release_package(lp, package, version, source_release):
    ubuntu = lp.distributions['ubuntu']
    archive = ubuntu.main_archive
    series = ubuntu.getSeries(name_or_version=source_release)
    status = 'Published'
    for pocket in ('Updates', 'Security', 'Release'):
        try:
            srcpkg = archive.getPublishedSources(source_name=package,
                                                 distro_series=series,
                                                 pocket=pocket,
                                                 status=status,
                                                 exact_match=True)[0]
            break
        except IndexError:
            continue
    else:
        error('Unable to find package %s in release %s.' %
              (package, source_release))

    if version and version != srcpkg.source_package_version:
        error('Requested backport of version %s but %s is at version %s.' %
              (version, package, srcpkg.source_package_version))

    return srcpkg

def find_version_package(lp, package, version):
    ubuntu = lp.distributions['ubuntu']
    archive = ubuntu.main_archive
    try:
        # Might get more than one (i.e. same version in multiple
        # releases), but they should all be identical
        return archive.getPublishedSources(source_name=package,
                                           version=version)[0]
    except IndexError:
        error('Version %s of package %s was never published in Ubuntu.' %
              (version, package))

def dscurl_from_package(lp, workdir, package, version, source_release):
    if not source_release and not version:
        source_release = lp.distributions['ubuntu'].current_series.name

    # If source_release is specified, then version is just for verification
    if source_release:
        srcpkg = find_release_package(lp, package, version, source_release)
    else:
        srcpkg = find_version_package(lp, package, version)

    for f in srcpkg.sourceFileUrls():
        if f.endswith('.dsc'):
            return urllib.unquote(f)
    else:
        error('Package %s contains no .dsc file.' % package)

def dscurl_from_dsc(package):
    path = os.path.abspath(os.path.expanduser(package))
    if os.path.exists(path):
        return 'file://%s' % path
    else:
        # Can't resolve it as a local path? Let's just hope it's good as-is
        return package

def fetch_package(lp, workdir, package, version, source_release):
    # Returns the path to the .dsc file that was fetched

    if package.endswith('.dsc'):
        dsc = dscurl_from_dsc(package)
    else:
        dsc = dscurl_from_package(lp, workdir, package, version, source_release)

    check_call(['dget', '--download-only', '--allow-unauthenticated', dsc],
               cwd=workdir)
    return os.path.join(workdir, os.path.basename(dsc))

def get_backport_version(version, suffix, upload, release):
    v = version + ('~%s1' % release)
    if suffix is not None:
        v += suffix
    elif upload and upload.startswith('ppa:'):
        v += '~ppa1'
    return v

def get_backport_dist(upload, release):
    if not upload or upload == 'ubuntu':
        return '%s-backports' % release
    else:
        return release

def do_build(workdir, package, release, bp_version, builder, update):
    builder = getBuilder(builder)
    if not builder:
        return

    if update:
        if 0 != builder.update(release):
            sys.exit(1)

    return builder.build(os.path.join(workdir,
                                      '%s_%s.dsc' % (package, bp_version)),
                         release,
                         os.path.join(workdir, "buildresult"))

def do_upload(workdir, package, bp_version, upload, prompt):
    print 'Please check %s %s in file://%s carefully!' % \
          (package, bp_version, workdir)
    if prompt or upload == 'ubuntu':
        question = 'Do you want to upload the package to %s' % upload
        answer = YesNoQuestion().ask(question, "yes")
        if answer == "no":
            return

    changes_file = '%s_%s_source.changes' % (package, bp_version)
    check_call(['dput', upload, changes_file], cwd=workdir)


def do_backport(workdir, package, dscfile, version, suffix, release, build,
                builder, update, upload, prompt):
    check_call(['dpkg-source', '-x', dscfile, package], cwd=workdir)
    srcdir = os.path.join(workdir, package)

    bp_version = get_backport_version(version, suffix, upload, release)
    bp_dist = get_backport_dist(upload, release)

    ubu_email()
    check_call(['dch',
                '--force-bad-version',
                '--preserve',
                '--newversion', bp_version,
                '--distribution', bp_dist,
                'No-change backport to %s' % release],
               cwd=srcdir)
    check_call(['debuild', '--no-lintian', '-S', '-sa'], cwd=srcdir)

    if ':' in bp_version:
        bp_version = bp_version[bp_version.find(':')+1:]

    if build:
        if 0 != do_build(workdir, package, release, bp_version, builder, update):
            sys.exit(1)
    if upload:
        do_upload(workdir, package, bp_version, upload, prompt)

    shutil.rmtree(srcdir)

def main(args):
    os.environ['DEB_VENDOR'] = 'Ubuntu'

    opts, (package_or_dsc,) = parse(args[1:])

    script_name = os.path.basename(sys.argv[0])
    lp = launchpadlib.launchpad.Launchpad.login_anonymously(script_name,
                                                            opts.launchpad)

    if not opts.dest_releases:
        try:
            distinfo = lsb_release.get_distro_information()
            opts.dest_releases = [distinfo['CODENAME']]
        except:
            error('No destination release specified and unable to guess yours.')

    if opts.workdir:
        workdir = os.path.expanduser(opts.workdir)
    else:
        workdir = tempfile.mkdtemp(prefix='backportpackage-')

    if not os.path.exists(workdir):
        os.makedirs(workdir)

    try:
        dscfile = fetch_package(lp,
                                workdir,
                                package_or_dsc,
                                opts.version,
                                opts.source_release)

        dsc = Dsc(open(os.path.join(workdir, dscfile)))
        package = dsc['Source']
        version = dsc['Version']

        for release in opts.dest_releases:
            do_backport(workdir,
                        package,
                        dscfile,
                        version,
                        opts.suffix,
                        release,
                        opts.build,
                        opts.builder,
                        opts.update,
                        opts.upload,
                        opts.prompt)
    finally:
        if not opts.workdir:
            shutil.rmtree(workdir)

if __name__ == '__main__':
    sys.exit(main(sys.argv))