#!/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, re, email
from glob import glob

import apport
from launchpadlib.errors import HTTPError
from apport.crashdb_impl.launchpad import CrashDatabase

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

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='', 
            description=part.get_filename(),
            content_type=None,
            data=part.get_payload(decode=True),
            filename=part.get_filename(), is_patch=False)

    # change bug status from "Incomplete"
    for task in bug.bug_tasks:
        if task.status == 'Incomplete':
            task.transitionToStatus(status='New')

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(None)

#
# 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)

try:
    bug_number = int(args[0])
except ValueError:
    optparser.error('invalid bug report number')
    sys.exit(1)

print 'Logging into Launchpad... You have to allow "Change anything" privileges.'
crashdb = CrashDatabase(None, None, {'distro': 'ubuntu'})

print 'Downloading bug information...'

try:
    bug = crashdb.launchpad.bugs[int(bug_number)]
except KeyError:
    print 'The bug number %s does not exist in Launchpad.' % bug_number
    sys.exit(1)

if bug.owner.name != crashdb.launchpad.me.name:
    print 'You are not the reporter of bug %s.' % bug_number
    while True:
        val = raw_input('Is that really the bug you want to update? [y/N]')
        if val.lower() in ('y', 'yes'):
    	    break
        elif val.lower() in ('n', 'no', ''):
    	    sys.exit(1)
        else:
    	    print 'Invalid answer.'

report = apport.Report('Bug')

print 'Bug title:', bug.title
info_collected = False
if opts.package:
    collect(report, opts.package)
    info_collected = True
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
        # we either must have the package installed or a source package hook
        # available to collect sensible information
        try:
            apport.packaging.get_version(report['Package'])
            info_collected = True
        except ValueError:
            if os.path.exists('/usr/share/apport/package-hooks/source_%s.py' %
                    report['SourcePackage']):
                info_collected = True
            else:
                print 'Package %s not installed and no hook available, ignoring' % src
                continue
        collect(report, src)

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

if not info_collected:
    print 'No additional information collected.'
    sys.exit(0)

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

try:
    upload(report, bug)

    bug = crashdb.launchpad.bugs[int(bug_number)] # LP#336866 workaround
    x = bug.tags[:] # LP#254901 workaround
    x.append('apport-collected')
    bug.tags = x
    bug.lp_save()
except HTTPError, e:
    print >> sys.stderr, 'Error connecting to Launchpad: %s\nYou have to allow "Change anything" privileges.\nYou can reset the credentials by removing the file "~/.cache/apport/launchpad.credentials."' % str(e)
    sys.exit(1)
