#!/usr/bin/python
# bugzilla - a commandline frontend for the python bugzilla module
#
# Copyright (C) 2007 Red Hat Inc.
# Author: Will Woods <wwoods@redhat.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 bugzilla, optparse
import os, sys, glob, re
import logging

version = '0.2'
default_bz = 'https://bugzilla.redhat.com/xmlrpc.cgi'

# Initial simple logging stuff
logging.basicConfig()
log = logging.getLogger("bugzilla")
if '--debug' in sys.argv:
    log.setLevel(logging.DEBUG)
elif '--verbose' in sys.argv:
    log.setLevel(logging.INFO)
        
def findcookie():
    globs = ['~/.mozilla/firefox/*default*/cookies.txt']
    for g in globs:
        log.debug("Looking for cookies.txt in %s", g)
        cookiefiles = glob.glob(os.path.expanduser(g))
        if cookiefiles:
            # return the first one we find.
            # TODO: find all cookiefiles, sort by age, use newest
            return cookiefiles[0]
cookiefile = None

cmdlist = ('info','query','new','modify')
def setup_parser():
    u =   "usage: %prog [global options] COMMAND [options]"
    u +=  "\nCommands: %s" % ', '.join(cmdlist)
    p = optparse.OptionParser(usage=u)
    p.disable_interspersed_args()
    # General bugzilla connection options
    p.add_option('--bugzilla',default=default_bz,
            help="bugzilla XMLRPC URI. default: %s" % default_bz)
    p.add_option('--user',
            help="username. Will attempt to use browser cookie if not specified.")
    p.add_option('--password',
            help="password. Will attempt to use browser cookie if not specified.")
    p.add_option('--cookiefile',
            help="cookie file to use for bugzilla authentication")
    p.add_option('--verbose',action='store_true',
            help="give more info about what's going on")
    p.add_option('--debug',action='store_true',
            help="output bunches of debugging info")
    return p

def setup_action_parser(action):
    p = optparse.OptionParser(usage="usage: %%prog %s [options]" % action)
    # TODO: product and version could default to current system
    # info (read from /etc/redhat-release?)
    if action == 'new':
        p.add_option('-p','--product',
                help="REQUIRED: product name (list with 'bugzilla info -p')")
        p.add_option('-v','--version',
                help="REQUIRED: product version")
        p.add_option('-c','--component',
                help="REQUIRED: component name (list with 'bugzilla info -c PRODUCT')")
        p.add_option('-l','--comment',
                help="REQUIRED: initial bug comment")
        p.add_option('-s','--summary',dest='short_desc',
                help="REQUIRED: bug summary")
        p.add_option('-o','--os',default='Linux',dest='op_sys',
                help="OPTIONAL: operating system (default: Linux)")
        p.add_option('-a','--arch',default='All',dest='rep_platform',
                help="OPTIONAL: arch this bug occurs on (default: All)")
        p.add_option('--severity',default='medium',dest='bug_severity',
                help="OPTIONAL: bug severity (default: medium)")
        p.add_option('--priority',default='medium',dest='priority',
                help="OPTIONAL: bug priority (default: medium)")
        p.add_option('-u','--url',dest='bug_file_loc',default='http://',
                help="OPTIONAL: URL for further bug info")
        p.add_option('--cc',
                help="OPTIONAL: add emails to initial CC list")
        # TODO: alias, assigned_to, reporter, qa_contact, dependson, blocked
    elif action == 'query':
        p.add_option('-p','--product',
                help="product name (list with 'bugzilla info -p')")
        p.add_option('-v','--version',
                help="product version")
        p.add_option('-c','--component',
                help="component name (list with 'bugzilla info -c PRODUCT')")
        p.add_option('-l','--long_desc',
                help="search inside bug comments")
        p.add_option('-s','--short_desc',
                help="search bug summaries")
        p.add_option('-o','--cc',
                help="search cc lists for given address")
        p.add_option('-a','--assigned_to',
                help="search for bugs assigned to this address")
        p.add_option('--blocked',
                help="search for bugs that block this bug ID")
        p.add_option('--dependson',
                help="search for bugs that depend on this bug ID")
        p.add_option('-b','--bug_id',
                help="specify individual bugs by IDs, separated with commas")
        p.add_option('-t','--bug_status',default="NEW,VERIFIED,ASSIGNED,NEEDINFO,ON_DEV,FAILS_QA,REOPENED",
                help="comma-separated list of bug statuses to accept")
    elif action == 'info':
        p.add_option('-p','--products',action='store_true',
                help='Get a list of products')
        p.add_option('-c','--components',metavar="PRODUCT",
                help='List the components in the given product')
        p.add_option('-o','--component_owners',metavar="PRODUCT",
                help='List components (and their owners)')
        p.add_option('-v','--versions',metavar="PRODUCT",
                help='List the versions for the given product')
    elif action == 'modify':
        p.set_usage("usage: %prog modify [options] BUGID BUGID ...")
        p.add_option('-l','--comment',
                help='Add a comment')
        # FIXME: check value for resolution
        p.add_option('-k','--close',metavar="RESOLUTION",
                help='Close with the given resolution')
        # TODO: --keyword, --flag, --tag, --status, --assignee, --cc, ...

    if action in ('new','query'):
        # output modifiers
        p.add_option('-f','--full',action='store_const',dest='output',
                const='full',default='normal',help="output detailed bug info")
        p.add_option('-i','--ids',action='store_const',dest='output',
                const='ids',help="output only bug IDs")
        p.add_option('--outputformat',
                help="Print output in the form given. You can use RPM-style "+
                "tags that match bug fields, e.g.: '%{bug_id}: %{short_desc}'")
    return p

if __name__ == '__main__':
    # Set up parser for global args
    parser = setup_parser()
    # Parse the commandline, woo
    (global_opt,args) = parser.parse_args()
    # Get our action from these args
    if len(args) and args[0] in cmdlist:
        action = args.pop(0)
    else:
        parser.error("command must be one of: %s" % ','.join(cmdlist))
    # Parse action-specific args
    action_parser = setup_action_parser(action)
    (opt, args) = action_parser.parse_args(args)

    # Connect to bugzilla
    log.info('Connecting to %s',global_opt.bugzilla)
    bz=bugzilla.Bugzilla(url=global_opt.bugzilla)
    if global_opt.user and global_opt.password:
        log.info('Using username/password for authentication')
        bz.login(global_opt.user,global_opt.password)
    elif global_opt.cookiefile:
        log.info('Using cookies in %s for authentication', global_opt.cookiefile)
        bz.readcookiefile(global_opt.cookiefile)
    else:
        cookiefile = findcookie()
        if cookiefile:
            log.info('Using cookies in %s for authentication', cookiefile)
            bz.readcookiefile(cookiefile)
        else:
            parser.error("Could not find a Firefox cookie file. Try --user/--password.")
    
    # And now we actually execute the given command
    buglist = list() # save the results of query/new/modify here
    if action == 'info':
        if opt.products:
            for k in sorted(bz.products):
                print k

        if opt.components:
            for c in sorted(bz.getcomponents(opt.components)):
                print c

        if opt.component_owners:
            component_details = bz.getcomponentsdetails(opt.component_owners)
            for c in sorted(component_details):
                print "%s: %s" % (c, component_details[c]['initialowner'])

        if opt.versions:
            for p in bz.querydata['product']:
                if p['name'] == opt.versions:
                    for v in p['versions']:
                        print v

    elif action == 'query':
        # shortcut for specific bug_ids
        if opt.bug_id:
            log.debug("bz.getbugs(%s)", opt.bug_id.split(','))
            buglist=bz.getbugs(opt.bug_id.split(','))
        else:
            # Construct the query from the list of queryable options
            q = dict()
            email_count = 1
            chart_id = 0
            for a in ('product','component','version','long_desc','bug_id',
                      'short_desc','cc','assigned_to','bug_status',
                      'blocked','dependson'):
                if hasattr(opt,a):
                    i = getattr(opt,a)
                    if i:
                        if a in ('bug_status'): # list args
                            q[a] = i.split(',')
                        elif a in ('cc','assigned_to'):
                            # the email query fields are kind of weird - thanks
                            # to Florian La Roche for figuring this bit out.
                            # ex.: {'email1':'foo@bar.com','emailcc1':True}
                            q['email%i' % email_count] = i
                            q['email%s%i' % (a,email_count)] = True
                            email_count += 1
                        elif a in ('blocked','dependson'): 
                            # Chart args are weird.
                            q['field%i-0-0' % chart_id] = a
                            q['type%i-0-0' % chart_id]  = 'equals'
                            q['value%i-0-0' % chart_id] = i
                            chart_id += 1
                        else:
                            q[a] = i
            log.debug("bz.query: %s", q)
            buglist = bz.query(q)

    elif action == 'new':
        data = dict()
        required=['product','component','version','short_desc','comment',
             'rep_platform','bug_severity','op_sys','bug_file_loc','priority']
        optional=['cc']
        for a in required + optional:
            i = getattr(opt,a)
            if i:
                data[a] = i
        for k in required:
            if k not in data:
                parser.error('Missing required argument: %s' % k)
        log.debug("bz.createbug(%s)", data)
        b = bz.createbug(**data)
        buglist = [b]
        
    elif action == 'modify':
        bugid_list = []
        for a in args:
            if ',' in a:
                for b in a.split(','):
                    bugid_list.append(b)
            else:
                bugid_list.append(a)
        # Surely there's a simpler way to do that..
        # bail out if no bugs were given
        if not bugid_list:
            parser.error('No bug IDs given (maybe you forgot an argument somewhere?)')
        # Iterate over a list of Bug objects
        # FIXME: this should totally use some multicall magic
        buglist = bz.getbugssimple(bugid_list)
        for id,bug in zip(bugid_list,buglist):
            if not bug: 
                log.error("  failed to load bug %s" % id)
                continue
            log.debug("modifying bug %s" % bug.bug_id)
            if opt.comment:
                log.debug("  add comment: %s" % opt.comment)
                bug.addcomment(opt.comment)
            if opt.close:
                log.debug("  close %s" % opt.close)
                bug.close(opt.close)
    else:
        print "Sorry - '%s' not implemented yet." % action

    # If we're doing new/query/modify, output our results
    if action in ('new','query'):
        if opt.outputformat:
            format_field_re = re.compile("%{[a-z0-9_]+}")
            def bug_field(matchobj):
                fieldname = matchobj.group()[2:-1]
                return str(getattr(b,fieldname))
            for b in buglist:
                print format_field_re.sub(bug_field,opt.outputformat)
        elif opt.output == 'ids':
            for b in buglist:
                print b.bug_id
        elif opt.output == 'full':
            fullbuglist = bz.getbugs([b.bug_id for b in buglist])
            for b in fullbuglist:
                print b
                if b.cc: print "CC: %s" % " ".join(b.cc)
                if b.blocked: print "Blocked: %s" % " ".join([str(i) for i in b.blocked])
                if b.dependson: print "Depends: %s" % " ".join([str(i) for i in b.dependson])
                for c in b.longdescs:
                    print "* %s - %s (%s):\n%s\n" % (c['time'],c['name'],c['email'] or c['safe_email'],c['body'])
        elif opt.output == 'normal':
            for b in buglist:
                print b
        else:
            parser.error("opt.output was set to something weird.")
