#!/usr/bin/python

# Download a Launchpad bug report, get its source package, check if it has
# apport hooks, and if so, run and upload them.
#
# Copyright (c) 2009 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@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; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

import sys, os.path, optparse, tempfile, atexit, shutil, re, email
from glob import glob

import apport

bug_target_re = re.compile(
    r'/ubuntu/(?:(?P<suite>[^/]+)/)?\+source/(?P<source>[^/]+)$')

try:
    import launchpadlib.errors
    from launchpadlib.launchpad import Launchpad, STAGING_SERVICE_ROOT, EDGE_SERVICE_ROOT
    from launchpadlib.credentials import Credentials
except ImportError:
    print >> sys.stderr, 'Please install the package "python-launchpadlib".'
    sys.exit(1)

def login(launchpad_instance=EDGE_SERVICE_ROOT):
    '''Log into Launchpad.
    
    This reads/saves credentials, and returns a Launchpad instance.
    '''
    cache_dir = tempfile.mkdtemp()
    atexit.register(shutil.rmtree, cache_dir)

    cred_dir = os.path.expanduser('~/.cache/apport')
    if not os.path.isdir(cred_dir):
        os.makedirs(cred_dir)
    cred = os.path.join(cred_dir, 'launchpad.credentials')

    if os.path.exists(cred):
        # use existing credentials
        credentials = Credentials()
        credentials.load(open(cred))
        launchpad = Launchpad(credentials, launchpad_instance, cache_dir)
    else:
        # get credentials and save them
        try:
            launchpad = Launchpad.get_token_and_login('apport-collect',
                    launchpad_instance, cache_dir)
        except launchpadlib.errors.HTTPError, e:
            print >> sys.stderr, 'Error connecting to Launchpad: %s\nYou have to allow "Change anything" privileges.' % str(e)
            sys.exit(1)
        f = open(cred, 'w')
        os.chmod(cred, 0600)
        launchpad.credentials.save(f)
        f.close()

    return launchpad

def upload(report, bug):
    '''Upload collected information to Launchpad bug.'''

    print 'Uploading additional information to Launchpad bug...'

    # we want to reuse the knowledge of write_mime() with all its different input
    # types and output formatting; however, we have to dissect the mime ourselves,
    # since we can't just upload it as a blob
    mime = tempfile.TemporaryFile()
    report.write_mime(mime)
    mime.flush()
    mime.seek(0)
    msg = email.message_from_file(mime)
    msg_iter = msg.walk()

    # first part is the multipart container
    part = msg_iter.next()
    assert part.is_multipart()

    # second part should be an inline text/plain attachments with all short
    # fields
    part = msg_iter.next()
    assert not part.is_multipart()
    assert part.get_content_type() == 'text/plain'
    
    print'   short text data...'
    bug.newMessage(content=part.get_payload(decode=True), 
        subject='apport-collect data')

    # other parts are the attachments:
    for part in msg_iter:
        print '   attachment: %s...' % part.get_filename()
        bug.addAttachment(comment='', data=part.get_payload(decode=True),
            filename=part.get_filename(), is_patch=False)

def collect(report, package):
    '''Collect information for given package.'''

    print 'Collecting apport information for source package %s...' % package
    try:
        report.add_package_info(package)
    except ValueError:
        # this happens for source package tasks which do not have an identical
        # binary package name
        pass
    report.add_hooks_info()

#
# main
#

optparser = optparse.OptionParser('%prog [options] <Launchpad bug number>')
optparser.add_option('-p', '--package', 
    help="Collect information for this package. If not given, it will be inferred from the bug report's source package tasks.",
    type='string', dest='package')
(opts, args) = optparser.parse_args()

if len(args) != 1:
    optparser.error('incorrect number of arguments; use --help for a short online help')
    sys.exit(1)

(bug_number,) = args

print 'Logging into Launchpad...'
if os.getenv('APPORT_STAGING'):
    lp = login(STAGING_SERVICE_ROOT)
else:
    lp = login()

print 'Downloading bug...'
bug =  lp.bugs[int(bug_number)]

report = apport.Report('Bug')

print 'Bug title:', bug.title
if opts.package:
    collect(report, opts.package)
else:
    # determine bug tasks and collect for those
    for task in bug.bug_tasks:
        match = bug_target_re.search(task.target.self_link)
        if not match:
            print 'Ignoring task', task.target
            continue
        if task.status in ('Invalid', "Won't Fix", 'Fix Released'):
            print 'Ignoring task %s because it is closed' % task.target
            continue
        src = match.group('source')
        report['SourcePackage'] = src
        report['Package'] = src # no way to find this out
        collect(report, src)

report.add_os_info()
report.add_user_info()
report.add_proc_environ()

# delete the uninteresting keys
del report['ProblemType']
del report['Date']
try:
    del report['SourcePackage']
except KeyError:
    pass

if len(report.keys()) == 0:
    print 'No additional information collected.'
    sys.exit(0)

try:
    upload(report, bug)
except launchpadlib.errors.HTTPError, e:
    print >> sys.stderr, 'Error connecting to Launchpad: %s\nYou have to allow "Change anything" privileges.' % str(e)
    sys.exit(1)
