#!/usr/bin/python
#
# Copyright (C) 2007, Canonical Ltd.
# Copyright (C) 2010, Benjamin Drung <bdrung@ubuntu.com>
# Copyright (C) 2010, Stefano Rivera <stefanor@ubuntu.com>
#
# It was initial written by Daniel Holbach.
#
# 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 3.
#
# 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-3 for more details.

import csv
import getopt
import lazr.restfulclient
import os
import re
import subprocess
import sys
import logging
import glob
import fnmatch

from launchpadlib.launchpad import Launchpad

from ubuntutools.config import UDTConfig

COMMAND_LINE_SYNTAX_ERROR = 1
VERSION_DETECTION_FAILED = 2
PRIVATE_USER_EMAIL = 3
UPLOAD_NOT_PERMITTED = 4

def get_version(title):
    m = re.search("[() ][0-9][0-9a-zA-Z.:+-~]*", title)
    if m is None:
        print >> sys.stderr, ("Version could not be detected. Please specify "
                              "it with -V.")
        sys.exit(VERSION_DETECTION_FAILED)
    return m.group(0).strip("() ")

def strip_epoch(version):
    parts = version.split(':')
    if len(parts) > 1:
        del parts[0]
    version = ':'.join(parts)
    return version

def log_call(command):
    command = map(str, command)
    logging.info("Running %s", " ".join(command))
    return command

def get_source(package, version, section, dist, uploader_name, uploader_email,
               bug, key, upload):
    if os.path.isdir("/tmpfs"):
        workdir = "/tmpfs/ack-sync"
    else:
        workdir = "/tmp/ack-sync"
    if not os.path.isdir(workdir):
        os.makedirs(workdir)
    os.chdir(workdir)

    cmd = ["syncpackage", package, "-r", dist, "-V", version, "-b", str(bug)]
    if section is not None:
        cmd += ["-c", section]
    if uploader_email is not None:
        cmd += ["-e", uploader_email]
    if uploader_name is not None:
        cmd += ["-n", uploader_name]
    if not upload:
        cmd += ['--dont-sign',]
    if upload and key is not None:
        cmd += ["-k", key]
    subprocess.check_call(cmd)

    dsc_file = package + "_" + strip_epoch(version) + "fakesync1.dsc"
    if not os.path.exists(os.path.join(workdir, dsc_file)):
        dsc_file = package + "_" + strip_epoch(version) + ".dsc"
    if not os.path.exists(os.path.join(workdir, dsc_file)):
        print >> sys.stderr, ("E: Failed to find .dsc file created by "
                              "syncpackage.")
        sys.exit(1)
    return dsc_file

def build_source(dist, dsc_file, pbuilder, sbuild):
    try:
        if sbuild:
            cmd = ["sbuild", "-d", dist, "-A", dsc_file]
            subprocess.check_call(log_call(cmd))
        else:
            if not os.path.isdir("buildresult"):
                os.makedirs("buildresult")
            cmd = ["sudo", "-E", "DIST=" + dist, pbuilder, "--build",
                   "--buildresult", "buildresult", dsc_file]
            subprocess.check_call(log_call(cmd))
    except subprocess.CalledProcessError:
        print >> sys.stderr, "E: %s failed to build." % (dsc_file)
        sys.exit(1)

def test_install(dist, dsc_file, sbuild, lvm):
    changes_files = glob.glob(os.path.splitext(dsc_file)[0]+"_*.changes")
    changes_file = ""

    for temp_file in changes_files:
        if not fnmatch.fnmatch(temp_file, '*_source.changes'):
            changes_file = temp_file

    if not (os.path.isfile(changes_file)): # if no file exists at all => exit
        print >> sys.stderr, "E: No .changes file has been generated."
        sys.exit(1)

    try:
        cmd = ["sudo", "piuparts", "-N", "-W", "--single-changes-list",
               "--log-level=info", "--ignore=/var/log/apt/history.log",
               "--mirror=http://archive.ubuntu.com/ubuntu main universe "
               "restricted multiverse", changes_file]
        if sbuild:
            lvm_volume = lvm + "/" + dist + "_chroot"
            subprocess.check_call(log_call(cmd + ["--lvm-volume="+lvm_volume]))
        else:
            subprocess.check_call(log_call(cmd + ["--pbuilder"]))
    except subprocess.CalledProcessError:
        print >> sys.stderr, "E: %s failed to install. Please check log" % \
                             (changes_file)

def get_email_from_file(name):
    filename = os.path.expanduser("~/.ack-sync-email.list")
    if os.path.isfile(filename):
        csvfile = open(filename)
        csv_reader = csv.reader(csvfile)
        for row in csv_reader:
            if row and row[0] == name:
                return row[1]
    return None

def unsubscribe_sponsors(launchpad, bug):
    us = launchpad.people['ubuntu-sponsors']
    bug.unsubscribe(person=us)
    print "ubuntu-sponsors unsubscribed"


def ack_sync(bug_numbers, all_package, all_version, all_section, update,
             all_uploader_email, key, upload, lpinstance, pbuilder, sbuild, lvm,
             piuparts, verbose=False, silent=False):
    launchpad = Launchpad.login_with("ubuntu-dev-tools", lpinstance)
    # TODO: use release-info (once available)
    series = launchpad.distributions["ubuntu"].current_series
    dist = series.name

    # update pbuilder
    if update:
        if sbuild:
            subprocess.call(log_call(["sbuild-update", dist]))
        else:
            cmd = ["sudo", "-E", "DIST=" + dist, pbuilder, "--update"]
            subprocess.call(log_call(cmd))

    for bug_number in bug_numbers:
        bug = launchpad.bugs[bug_number]
        uploader = bug.owner
        uploader_name = uploader.display_name
        if all_uploader_email is not None:
            uploader_email = all_uploader_email
        elif launchpad.people['ubuntumembers'] in uploader.super_teams:
            uploader_email = uploader.name + '@ubuntu.com'
        else:
            try:
                uploader_email =  uploader.preferred_email_address.email
            except ValueError:
                uploader_email = get_email_from_file(uploader.name)
                if uploader_email is None:
                    if not silent:
                        print >> sys.stderr, ("E: Bug owner '%s' does not have "
                                              "a public email address. Specify "
                                              "uploader with '-e'.") % \
                                             (uploader_name)
                    sys.exit(PRIVATE_USER_EMAIL)
                elif not silent:
                    print "Taking email address from local file: " + \
                          uploader_email

        # Try to find a Ubuntu bug task, against which the package is raised.
        for t in bug.bug_tasks:
            if t.bug_target_name.endswith(' (Ubuntu)'):
                task = t
                break
        try:
            print "Using Ubuntu bug task: %s" % task.bug_target_name
        except NameError:
            # We failed, use the first task (revert to previous behavior)
            task = bug.bug_tasks[0]
            print ("W: Could not find bug task for a Ubuntu package."
                   "  Using task '%s'" % task.bug_target_name)

        if all_package is not None:
            package = all_package
        else:
            package = task.bug_target_name.split(" ")[0]
            if package == "ubuntu":
                words = bug.title.split(" ")
                # no source package was defined. Guessing that the second or
                # third word in the title is the package name, because most
                # titles start with "Please sync <package>" or "Sync <package>"
                if words[0].lower() == "please":
                    package = words[2]
                else:
                    package = words[1]
        if all_version is not None:
            version = all_version
        else:
            version = get_version(bug.title)

        src_pkg = series.getSourcePackage(name=package)
        if src_pkg is None:
            print "%s is NEW in %s." % (package, dist)
        if src_pkg is not None:
            # TODO: Port ack-sync to use lpapicache and reduce code-duplication.
            can_upload = None
            try:
                series.main_archive.checkUpload(
                    component=src_pkg.latest_published_component_name,
                    distroseries=series, person=launchpad.me, pocket='Release',
                    sourcepackagename=package)
                can_upload = True
            except lazr.restfulclient.errors.HTTPError, e:
                if e.response.status == 403:
                    can_upload = False
                elif e.response.status == 400:
                    print ("W: Package is probably not in Ubuntu. Can't check "
                           "upload rights.")
                    can_upload = True
                else:
                    raise e
            if not can_upload:
                print >> sys.stderr, ("E: Sorry, you are not allowed to upload "
                                      'package "%s" to %s.' % (package, dist))
                sys.exit(UPLOAD_NOT_PERMITTED)

        if task.assignee == None:
            task.assignee = launchpad.me
            print "assigned me"
            task.lp_save()
        if task.assignee != launchpad.me:
            print >> sys.stderr, ("E: %s is already assigned to "
                                  "https://launchpad.net/bugs/%i") % \
                                 (task.assignee.display_name, bug.id)
            sys.exit(1)
        old_status = task.status
        task.status = 'In Progress'
        unsubscribe_sponsors(launchpad, bug)
        if task.importance == "Undecided":
            task.importance = "Wishlist"
            print "importance set to Wishlist"
        task.lp_save()

        print "package:", package
        print "version:", version
        dsc_file = get_source(package, version, all_section, dist,
                              uploader_name, uploader_email, bug_number, key,
                              upload)

        if dsc_file.endswith('fakesync1.dsc'):
            upload = True

        # extract source
        env = os.environ
        env['DEB_VENDOR'] = 'Ubuntu'
        subprocess.check_call(["dpkg-source", "-x", dsc_file], env=env)

        build_source(dist, dsc_file, pbuilder, sbuild)

        if piuparts:
            test_install(dist, dsc_file, sbuild, lvm)

        print bug.title
        print '%s (was %s)' % (task.status, old_status)
        print "Uploader:", uploader_name + " <" + uploader_email + ">"
        if upload:
            print ("Will upload sync directly, rather than subscribing "
                   "ubuntu-archive")
        try:
            raw_input('Press [Enter] to continue or [Ctrl-C] to abort.')
        except KeyboardInterrupt:
            continue

        bug.subscribe(person=launchpad.me)
        print "subscribed me"
        if upload:
            task.status = 'Fix Committed'
            task.assignee = None
            print "unassigned me"
            task.lp_save()
            changes_file = dsc_file[:-4] + "_source.changes"
            subprocess.check_call(["dput", "ubuntu", changes_file])
        else:
            task.status = 'Confirmed'
            task.assignee = None
            print "unassigned me"
            task.lp_save()
            cmd = ["dpkg-architecture", "-qDEB_BUILD_ARCH"]
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
            architecture = process.communicate()[0].strip()
            content = "%s %s builds on %s. Sync request ACK'd." % \
                      (package, version, architecture)
            bug.newMessage(content=content, subject="ack-sync")
            bug.subscribe(person=launchpad.people['ubuntu-archive'])
            print "subscribed ubuntu-archive"


def usage():
    print """ack-sync <bug numbers>

  -e,                       specify uploader email address
  -h, --help                displays this help
  -k, --key                 key used to sign the package (in case of sponsoring)
      --lpinstance=<instance> Launchpad instance to connect to
                            (default: production)
  -l, --lvm                 lvm root dev directory, used for sbuild and piuparts
                            default is /dev/vg
      --no-conf             Don't read config files or environment variables
  -p, --package=<package>   set the package
  -P, --with-piuparts       use piuparts to check the instalability
      --section=<section>   Debian section (one of main, contrib, non-free)
  -s, --silent              be more silent
  -S, --with-sbuild         use sbuild instead of pbuilder
  -C, --pbuilder=<command>  use <command> as pbuilder
  -u, --update              updates pbuilder before building
  -U, --upload              upload the sync immediately rather than ACK-ing it
                            for the archive admins
  -v, --verbose             be more verbosive
  -V, --version=<version>   set the version"""

def main():
    try:
        long_opts = ["help", "key=", "lvm=", "package=", "section=", "silent",
                     "update", "upload", "verbose", "version=", "with-sbuild",
                     "pbuilder=", "with-piuparts", "lpinstance=", "no-conf"]
        opts, args = getopt.gnu_getopt(sys.argv[1:], "e:hk:p:PsSC:uUvV:",
                                       long_opts)
    except getopt.GetoptError, e:
        # will print something like "option -a not recognized"
        print >> sys.stderr, str(e)
        sys.exit(COMMAND_LINE_SYNTAX_ERROR)

    upload = False
    package = None
    sbuild = False
    section = None
    silent = False
    update = False
    uploader_email = None
    verbose = False
    version = None
    piuparts = False
    pbuilder = None
    lvm = "/dev/vg"
    key = None
    lpinstance = None
    no_conf = False

    for o, a in opts:
        if o in ("-h", "--help"):
            usage()
            sys.exit()
        elif o in ("-e"):
            uploader_email = a
        elif o in ("-k", "--key"):
            key = a
        elif o in ("--lpinstance"):
            lpinstance = a
        elif o in ("-l", "--lvm"):
            lvm = a
        elif o in ("--no-conf"):
            no_conf = True
        elif o in ("-p", "--package"):
            package = a
        elif o in ("-P", "--with-piuparts"):
            piuparts = True
        elif o in ("--section"):
            section = a
        elif o in ("-s", "--silent"):
            silent = True
        elif o in ("-S", "--with-sbuild"):
            sbuild = True
        elif o in ("-C", "--pbuilder"):
            pbuilder = a
        elif o in ("-u", "--update"):
            update = True
        elif o in ("-U", "--upload"):
            upload = True
        elif o in ("-v", "--verbose"):
            verbose = True
        elif o in ("-V", "--version"):
            version = a
        else:
            assert False, "unhandled option"

    if len(args) == 0:
        if not silent:
            print >> sys.stderr, "E: You must specify at least one bug number."
        sys.exit(COMMAND_LINE_SYNTAX_ERROR)

    bug_numbers = []
    for arg in args:
        try:
            number = int(arg)
        except:
            if not silent:
                print >> sys.stderr, "E: '%s' is not a valid bug number." % arg
            sys.exit(COMMAND_LINE_SYNTAX_ERROR)
        bug_numbers.append(number)

    config = UDTConfig(no_conf)
    if lpinstance is None:
        lpinstance = config.get_value('LPINSTANCE')
    if pbuilder is None and not sbuild:
        builder = config.get_value('BUILDER')
        if builder == 'pbuilder':
            pbuilder = 'pbuilder'
        elif builder == 'sbuild':
            sbuild = True
        else:
            print >> sys.stderr, "E: Unsupported build-system: %s" % builder
            sys.exit(COMMAND_LINE_SYNTAX_ERROR)
    if not update:
        update = config.get_value('UPDATE_BUILDER', boolean=True)
    #TODO: Support WORKDIR

    ack_sync(bug_numbers, package, version, section, update, uploader_email,
             key, upload, lpinstance, pbuilder, sbuild, lvm, piuparts, verbose,
             silent)

if __name__ == '__main__':
    main()