#!/usr/bin/python
# -*- coding: utf-8 -*-
# ##################################################################
#
# Copyright (C) 2010-2011, 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 glob
import optparse
import os
import shutil
import sys
import tempfile

import lsb_release
from httplib2 import Http, HttpLib2Error

from ubuntutools.archive import (SourcePackage, DebianSourcePackage,
                                 UbuntuSourcePackage, DownloadError)
from ubuntutools.config import UDTConfig, ubu_email
from ubuntutools.builder import get_builder
from ubuntutools.lp.lpapicache import (Launchpad, Distribution,
                                       SeriesNotFoundException,
                                       PackageNotFoundException)
from ubuntutools.logger import Logger
from ubuntutools.misc import (system_distribution, vendor_to_distroinfo,
                              codename_to_distribution)
from ubuntutools.question import YesNoQuestion
from ubuntutools import subprocess

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[0], ret))


def check_program_exists(name, package=None):
    paths = set(os.environ['PATH'].split(':'))
    paths |= set(('/sbin', '/usr/sbin', '/usr/local/sbin'))
    if not any(os.path.exists(os.path.join(p, name)) for p in paths):
        Logger.error('Could not find "%s". Please install the package "%s" '
                     'to use this functionality.',
                     name, package or name)
        sys.exit(1)


def parse(args):
    usage = 'Usage: %prog [options] <source package name or .dsc URL/file>'
    parser = optparse.OptionParser(usage)
    parser.add_option('-d', '--destination',
                      metavar='DEST',
                      dest='dest_releases',
                      default=[],
                      action='append',
                      help='Backport to DEST release '
                           '(default: current release)')
    parser.add_option('-s', '--source',
                      metavar='SOURCE',
                      dest='source_release',
                      help='Backport from SOURCE release '
                           '(default: devel release)')
    parser.add_option('-S', '--suffix',
                      metavar='SUFFIX',
                      help='Suffix to append to version number '
                           '(default: ~ppa1 when uploading to a PPA)')
    parser.add_option('-b', '--build',
                      default=False,
                      action='store_true',
                      help='Build the package before uploading '
                           '(default: %default)')
    parser.add_option('-B', '--builder',
                      metavar='BUILDER',
                      help='Specify the package builder (default: pbuilder)')
    parser.add_option('-U', '--update',
                      default=False,
                      action='store_true',
                      help='Update the build environment before '
                           'attempting to build')
    parser.add_option('-u', '--upload',
                      metavar='UPLOAD',
                      help='Specify an upload destination')
    parser.add_option("-k", "--key",
                      dest='keyid',
                      help="Specify the key ID to be used for signing.")
    parser.add_option('--dont-sign',
                      dest='keyid', action='store_false',
                      help='Do not sign the upload.')
    parser.add_option('-y', '--yes',
                      dest='prompt',
                      default=True,
                      action='store_false',
                      help='Do not prompt before uploading to a PPA')
    parser.add_option('-v', '--version',
                      metavar='VERSION',
                      help='Package version to backport (or verify)')
    parser.add_option('-w', '--workdir',
                      metavar='WORKDIR',
                      help='Specify a working directory '
                           '(default: temporary dir)')
    parser.add_option('-r', '--release-pocket',
                      default=False,
                      action='store_true',
                      help='Target the release pocket in the .changes file. '
                           'Necessary (and default) for uploads to PPAs')
    parser.add_option('-c', '--close',
                      metavar='BUG',
                      help='Bug to close in the changelog entry.')
    parser.add_option('-m', '--mirror',
                      metavar='URL',
                      help='Preferred mirror (default: Launchpad)')
    parser.add_option('-l', '--lpinstance',
                      metavar='INSTANCE',
                      help='Launchpad instance to connect to '
                           '(default: production)')
    parser.add_option('--no-conf',
                      default=False,
                      action='store_true',
                      help="Don't read config files or environment variables")

    opts, args = parser.parse_args(args)
    if len(args) != 1:
        parser.error('You must specify a single source package or a .dsc '
                     'URL/path.')
    config = UDTConfig(opts.no_conf)
    if opts.builder is None:
        opts.builder = config.get_value('BUILDER')
    if not opts.update:
        opts.update = config.get_value('UPDATE_BUILDER', boolean=True)
    if opts.workdir is None:
        opts.workdir = config.get_value('WORKDIR')
    if opts.lpinstance is None:
        opts.lpinstance = config.get_value('LPINSTANCE')
    if opts.upload is None:
        opts.upload = config.get_value('UPLOAD')
    if opts.keyid is None:
        opts.keyid = config.get_value('KEYID')
    if not opts.upload and not opts.workdir:
        parser.error('Please specify either a working dir or an upload target!')
    if opts.upload and opts.upload.startswith('ppa:'):
        opts.release_pocket = True
    if opts.upload:
        check_program_exists('dput')

    return opts, args, config


def find_release_package(mirror, workdir, package, version, source_release,
                         config):
    srcpkg = None

    if source_release:
        distribution = codename_to_distribution(source_release)
        if not distribution:
            error('Unknown release codename %s' % source_release)
        info = vendor_to_distroinfo(distribution)()
        source_release = info.codename(source_release, default=source_release)
    else:
        distribution = system_distribution()
    mirrors = [mirror] if mirror else []

    mirrors.append(config.get_value('%s_MIRROR' % distribution.upper()))

    if not version:
        archive = Distribution(distribution.lower()).getArchive()
        try:
            spph = archive.getSourcePackage(package, source_release)
        except (SeriesNotFoundException, PackageNotFoundException), e:
            error(str(e))
        version = spph.getVersion()

    if distribution == 'Debian':
        srcpkg = DebianSourcePackage(package,
                                     version,
                                     workdir=workdir,
                                     mirrors=mirrors)
    elif distribution == 'Ubuntu':
        srcpkg = UbuntuSourcePackage(package,
                                     version,
                                     workdir=workdir,
                                     mirrors=mirrors)

    return srcpkg

def find_package(mirror, workdir, package, version, source_release, config):
    "Returns the SourcePackage"
    if package.endswith('.dsc'):
        return SourcePackage(version=version, dscfile=package,
                             workdir=workdir, mirrors=(mirror,))

    if not source_release and not version:
        info = vendor_to_distroinfo(system_distribution())
        source_release = info().devel()

    srcpkg = find_release_package(mirror, workdir, package, version,
                                  source_release, config)
    if version and srcpkg.version != version:
        error('Requested backport of version %s but version of %s in %s is %s'
              % (version, package, source_release, srcpkg.version))

    return srcpkg

def get_backport_version(version, suffix, upload, release):
    distribution = codename_to_distribution(release)
    if not distribution:
        error('Unknown release codename %s' % release)
    series = Distribution(distribution.lower()).\
        getSeries(name_or_version=release)

    backport_version = version + ('~%s%s.1' % (distribution.lower(), series.version))
    if suffix is not None:
        backport_version += suffix
    elif upload and upload.startswith('ppa:'):
        backport_version += '~ppa1'
    return backport_version

def get_old_version(source, release):
    try:
        distribution = codename_to_distribution(release)
        archive = Distribution(distribution.lower()).getArchive()
        pkg = archive.getSourcePackage(source,
                                       release,
                                       ('Release', 'Security', 'Updates',
                                        'Proposed', 'Backports'))
        return pkg.getVersion()
    except (SeriesNotFoundException, PackageNotFoundException), e:
        pass

def get_backport_dist(release, release_pocket):
    if release_pocket:
        return release
    else:
        return '%s-backports' % release

def do_build(workdir, dsc, release, builder, update):
    builder = get_builder(builder)
    if not builder:
        return

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

    # builder.build is going to chdir to buildresult:
    workdir = os.path.realpath(workdir)
    return builder.build(os.path.join(workdir, dsc),
                         release,
                         os.path.join(workdir, "buildresult"))

def do_upload(workdir, package, bp_version, changes, 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

    check_call(['dput', upload, changes], cwd=workdir)

def orig_needed(upload, workdir, pkg):
    '''Avoid a -sa if possible'''
    if not upload or not upload.startswith('ppa:'):
        return True
    ppa = upload.split(':', 1)[1]
    user, ppa = ppa.split('/', 1)

    version = pkg.version.upstream_version

    h = Http()
    for filename in glob.glob(os.path.join(workdir,
            '%s_%s.orig*' % (pkg.source, version))):
        url = ('https://launchpad.net/~%s/+archive/%s/+files/%s'
               % (user, ppa, filename))
        try:
            headers, body = h.request(url, 'HEAD')
            if (headers.status != 200 or 
                not headers['content-location'].
                    startswith('https://launchpadlibrarian.net')):
                return True
        except HttpLib2Error, e:
            Logger.info(e)
            return True
    return False

def do_backport(workdir, pkg, suffix, close, release, release_pocket, build,
                builder, update, upload, keyid, prompt):
    dirname = '%s-%s' % (pkg.source, release)
    srcdir = os.path.join(workdir, dirname)

    if os.path.exists(srcdir):
        question = 'Working directory %s already exists. Delete it?' % srcdir
        if YesNoQuestion().ask(question, 'no') == 'no':
            sys.exit(1)
        shutil.rmtree(srcdir)

    pkg.unpack(dirname)

    bp_version = get_backport_version(pkg.version.full_version, suffix,
                                      upload, release)
    old_version = get_old_version(pkg.source, release)
    bp_dist = get_backport_dist(release, release_pocket)

    changelog = 'No-change backport to %s' % (release,)
    if close:
        changelog += ' (LP: #%s)' % (close,)
    check_call(['dch',
                '--force-bad-version',
                '--force-distribution',
                '--preserve',
                '--newversion', bp_version,
                '--distribution', bp_dist,
                changelog],
               cwd=srcdir)

    cmd = ['debuild', '--no-lintian', '-S', '-nc', '-uc', '-us']
    if orig_needed(upload, workdir, pkg):
        cmd.append('-sa')
    else:
        cmd.append('-sd')
    if old_version:
        cmd.append('-v%s' % old_version)
    env = os.environ.copy()
    # An ubuntu.com e-mail address would make dpkg-buildpackage fail if there
    # wasn't an Ubuntu maintainer for an ubuntu-versioned package. LP: #1007042
    env.pop('DEBEMAIL', None)
    check_call(cmd, cwd=srcdir, env=env)

    fn_base = pkg.source + '_' + bp_version.split(':', 1)[-1]
    changes = fn_base + '_source.changes'

    if build:
        if 0 != do_build(workdir, fn_base + '.dsc', release, builder, update):
            sys.exit(1)

    # None: sign with the default signature. False: don't sign
    if keyid is not False:
        cmd = ['debsign']
        if keyid:
            cmd.append('-k' + keyid)
        cmd.append(changes)
        check_call(cmd, cwd=workdir)
    if upload:
        do_upload(workdir, pkg.source, bp_version, changes, upload, prompt)

    shutil.rmtree(srcdir)

def main(args):
    ubu_email()

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

    Launchpad.login_anonymously(service=opts.lpinstance)

    if not opts.dest_releases:
        distinfo = lsb_release.get_distro_information()
        try:
            opts.dest_releases = [distinfo['CODENAME']]
        except KeyError:
            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:
        pkg = find_package(opts.mirror,
                           workdir,
                           package_or_dsc,
                           opts.version,
                           opts.source_release,
                           config)
        pkg.pull()

        for release in opts.dest_releases:
            do_backport(workdir,
                        pkg,
                        opts.suffix,
                        opts.close,
                        release,
                        opts.release_pocket,
                        opts.build,
                        opts.builder,
                        opts.update,
                        opts.upload,
                        opts.keyid,
                        opts.prompt)
    except DownloadError, e:
        error(str(e))
    finally:
        if not opts.workdir:
            shutil.rmtree(workdir)

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