#!/usr/bin/python
#
# Copyright (C) 2011, Stefano Rivera <stefanor@debian.org>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from collections import defaultdict
import optparse
import sys

import apt
from devscripts.logger import Logger
from distro_info import UbuntuDistroInfo

from ubuntutools.lp.lpapicache import Launchpad, Distribution
from ubuntutools.lp.udtexceptions import PackageNotFoundException
from ubuntutools.config import UDTConfig
from ubuntutools.rdepends import query_rdepends, RDependsException
from ubuntutools.requestsync.common import edit_report
from ubuntutools.question import YesNoQuestion


class DestinationException(Exception):
    pass


def determine_destinations(source, destination):
    ubuntu_info = UbuntuDistroInfo()
    if destination is None:
        destination = ubuntu_info.stable()

    if source not in ubuntu_info.all:
        raise DestinationException("Source release %s does not exist" % source)
    if destination not in ubuntu_info.all:
        raise DestinationException("Destination release %s does not exist"
                                   % destination)
    if destination not in ubuntu_info.supported():
        raise DestinationException("Destination release %s is not supported"
                                   % destination)

    found = False
    destinations = []
    support_gap = False
    for release in ubuntu_info.all:
        if release == destination:
            found = True
        if release == source:
            break
        if found:
            if support_gap:
                if ubuntu_info.is_lts(release):
                    support_gap = False
                else:
                    continue
            if release not in ubuntu_info.supported():
                support_gap = True
                continue
            destinations.append(release)

    assert found
    assert len(destinations) > 0

    return destinations


def find_rdepends(package, releases):
    published_binaries = set()
    for bpph in package._lpobject.getPublishedBinaries():
        published_binaries.add(bpph.binary_package_name)

    intermediate = defaultdict(lambda: defaultdict(list))

    # We want to display every pubilshed binary, even if it has no rdepends
    for binpkg in published_binaries:
        intermediate[binpkg]

    for arch in ('any', 'source'):
        for release in releases:
            for binpkg in published_binaries:
                try:
                    raw_rdeps = query_rdepends(binpkg, release, arch)
                except RDependsException:
                    # Not published? TODO: Check
                    continue
                for relationship, rdeps in raw_rdeps.iteritems():
                    for rdep in rdeps:
                        if rdep['Package'] in published_binaries:
                            continue
                        intermediate[binpkg][rdep['Package']] \
                                .append((release, relationship))

    output = []
    for binpkg, rdeps in intermediate.iteritems():
        output += ['', binpkg, '=' * len(binpkg)]
        for pkg, appearences in rdeps.iteritems():
            output += ['* %s' % pkg]
            for release, relationship in appearences:
                output += ['  [ ] %s (%s)' % (release, relationship)]

    return '\n'.join(output)


def locate_package(package, distribution):
    archive = Distribution('ubuntu').getArchive()
    for pass_ in ('source', 'binary'):
        try:
            package_spph = archive.getSourcePackage(package, distribution)
            return package_spph
        except PackageNotFoundException, e:
            if pass_ == 'binary':
                Logger.error(str(e))
                sys.exit(1)

        try:
            apt_pkg = apt.Cache()[package]
        except KeyError:
            continue
        package = apt_pkg.candidate.source_name
        Logger.normal("Binary package specified, considering its source "
                      "package instead: %s", package)


def request_backport(package_spph, source, destinations):
    subst = {
        'package': package_spph.getPackageName(),
        'version': package_spph.getVersion(),
        'component': package_spph.getComponent(),
        'rdepends': find_rdepends(package_spph, destinations),
        'source': source,
        'destinations': ', '.join(destinations),
    }
    subject = ("Please backport %(package)s %(version)s (%(component)s) "
               "from %(source)s" % subst)
    body = ("Please backport %(package)s %(version)s (%(component)s) "
            "from %(source)s to %(destinations)s.\n\n"
            "Reason for the backport:\n"
            "<<< Enter your reasoning here >>>\n\n"
            "Testing performed:\n"
            "<<< Mention any build & install tests you've done >>>\n\n"
            "Reverse dependencies:\n"
            "The following reverse-dependencies need to be tested against the "
            "new version of %(package)s. "
            "For reverse-build-dependencies, please test that the package "
            "still builds against the new %(package)s. "
            "For reverse-dependencies, please test that the version of the "
            "package currently in the release still works with the new "
            "libgdata installed. "
            "Mark off items in the checklist [X] as you test them, "
            "but please leave the checklist so that backporters can quickly "
            "evaluate the state of testing.\n"
            "%(rdepends)s\n"
            % subst)

    subject, body = edit_report(subject, body, changes_required=True)

    Logger.normal('The final report is:\nSummary: %s\nDescription:\n%s\n',
                  subject, body)
    if YesNoQuestion().ask("Request this backport", "yes") == "no":
        sys.exit(1)

    targets = [Launchpad.projects['%s-backports' % destination]
               for destination in destinations]
    bug = Launchpad.bugs.createBug(title=subject, description=body,
                                   target=targets[0])
    for target in targets[1:]:
        bug.addTask(target=target)

    Logger.normal("Backport request filed as %s", bug.web_link)


def main():
    parser = optparse.OptionParser('%progname [options] package')
    parser.add_option('-d', '--destination', metavar='DEST',
                      help='Backport to DEST release and necessary '
                           'intermediate releases '
                           '(default: current stable release)')
    parser.add_option('-s', '--source', metavar='SOURCE',
                      help='Backport from SOURCE release '
                           '(default: current devel release)')
    parser.add_option('-l', '--lpinstance', metavar='INSTANCE', default=None,
                      help='Launchpad instance to connect to '
                           '(default: production).')
    parser.add_option('--no-conf', action='store_true',
                      dest='no_conf', default=False,
                      help="Don't read config files or environment variables")
    options, args = parser.parse_args()

    if len(args) != 1:
        parser.error("One (and only one) package must be specified")
    package = args[0]

    config = UDTConfig(options.no_conf)

    if options.lpinstance is None:
        options.lpinstance = config.get_value('LPINSTANCE')
    Launchpad.login(options.lpinstance)

    if options.source is None:
        options.source = Distribution('ubuntu').getDevelopmentSeries().name

    try:
        destinations = determine_destinations(options.source,
                                              options.destination)
    except DestinationException, e:
        Logger.error(str(e))
        sys.exit(1)

    package_spph = locate_package(package, options.source)
    request_backport(package_spph, options.source, destinations)


if __name__ == '__main__':
    main()