"""Library for reading Jitterbug records and submitting them to SF

A Jitterbug database has one or more categories of bugs, like
incoming, open, resolved, etc.  Each category is represented by a
directory.  Each bug is stored in one or more files in that
directory; each bug has a unique id. The files for bug NNN are

    * NNN: A mail message containing the bug report
    * NNN.notes: Any notes entered via the Jitterbug Web interface
    * NNN.audit: A log of changes to the bug record; one per line
    * NNN.followup.I: one or more followup mail messages; first one is
        numbered 1
    * NNN.reply.I: one or more replies entered throught the Web form;
        first one is numbered 1

This program loads each bug report for a category into the SF Bug
Tracker.
"""

import cgi
import os
import re
import rfc822
import urllib
import urlparse

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

import sflib

VERBOSE = 0
SF_SUBMIT_URL = "http://sourceforge.net/bugs/index.php"

class BugLabels:
    PROJECT_GROUP_ID = None
    ASSIGNMENT = "100"
    CATEGORY = "100"
    GROUP = "100"
    PRIORITY = "5"

def set_label(label, val):
    setattr(BugLabels, label, val)

class Message:
    def __init__(self, path):
        self.path = path
        f = open(path)
        self.msg = rfc822.Message(f)
        self.body = self.msg.fp.read()
        f.close()

    def __getattr__(self, attr):
        return getattr(self.msg, attr)

    def dump(self):
        """Return a string with a minimal copy of the message"""
        headers = []
        for field in "From", "Subject", "Date":
            val = self.msg.getheader(field)
            if val:
                headers.append("%s: %s" % (field, val))
        return "\n".join(headers) + "\n\n" + self.body

class Notes:
    def __init__(self, buf):
        self.buf = buf

    def dump(self):
        return self.buf

class Bug:
    def __init__(self, dir, bug_id):
        self.id = bug_id
        self.dir = dir
        self.root = os.path.join(dir, bug_id)
        self._load()

    def _load(self):
        self._load_root()
        self._load_audit()
        self._load_followups()
        self._load_replys()
        self._load_notes()

    def _load_root(self):
        f = open(self.root)
        self.msg = rfc822.Message(f)
        # The body of a Jitterbug mail message has four more rfc822
        # headers.  Parse these as another message object, then get
        # the real body out of the second Message object. 
        g = StringIO(self.msg.fp.read())
        self.msg_headers = rfc822.Message(g)
        self.msg_body = self.msg_headers.fp.read()
        g.close()
        f.close()

    def _load_audit(self):
        audit_path = self.root + ".audit"
        if not os.path.exists(audit_path):
            self.audit = None
        else:
            f = open(audit_path)
            self.audit = f.read()
            f.close()

    def _load_notes(self):
        notes_path = self.root + ".notes"
        if not os.path.exists(notes_path):
            self.notes = None
        else:
            f = open(notes_path)
            self.notes = f.read()
            f.close()

    def _load_numbered(self, name):
        rx = re.compile("%s.%s.(\d+)" % (self.id, name))
        elts = {}
        for file in os.listdir(self.dir):
            mo = rx.match(file)
            if not mo:
                continue
            msg = Message(os.path.join(self.dir, file))
            elts[int(mo.group(1))] = msg
        if elts:
            l = elts.items()
            l.sort()
            l = map(lambda x:x[1], l)
        else:
            l = []
        return l

    def _load_followups(self):
        self.followups = self._load_numbered('followup')

    def _load_replys(self):
        self.replys = self._load_numbered('reply')

    def dump(self, io):
        template = """Jitterbug-Id: %(jid)s
Submitted-By: %(sender)s
Date: %(date)s
Version: %(version)s
OS: %(os)s

%(body)s

====================================================================
Audit trail:
%(audit)s
"""

        jid = self.id
        sender = self.msg.getheader('from')
        date = self.msg.getheader('date')
        version = self.msg_headers.getheader('version')
        os = self.msg_headers.getheader('os')
        body = self.msg_body
        audit = self.audit

        io.write(template % vars())

    def submit(self, url):
        buf = self.submit_initial(url)

        if not (self.followups or self.replys or self.notes):
            if VERBOSE:
                print "Done"
            return
        # find the SF bug id and post comments for each reply or
        # followup
        p = self._load_bug_summary(url, buf)
        self.submit_followups(url)

    def submit_initial(self, url):
        if VERBOSE:
            print "Submitting bug PR#%s" % self.id
        data = self.encode_initial_bug_report()
        f = urllib.urlopen(url, data)
        resp = f.read()
        f.close()
        return resp

    def submit_followups(self, url):
        bug_id = self.find_bug_id(url)
        if bug_id is None:
            print "Error entering bug PR#%s" % self.id
            return
        i = 0
        for msg in self.replys + self.followups:
            i = i + 1
            data = self.encode_followup_comment(bug_id, msg)
            if VERBOSE:
                print "Submitting followup/reply", i
            urllib.urlopen(url, data)
        if self.notes:
            if VERBOSE:
                print "Submitting notes"
            data = self.encode_followup_comment(bug_id,
                                                Notes(self.notes))
            urllib.urlopen(url, data)
            

    def find_bug_id(self, url):
        try:
            return self._bsp.get_sf_bug_id(self.id)
        except KeyError:
            return None

    def _load_bug_summary(self, url, buf):
        """Load a bug summary start with the HTML response in buf"""
        self._bsp = BugSummaryParser()
        self._bsp.parse(buf)
        
    def encode_initial_bug_report(self):
        # the form vars definitions are defined by the form used to
        # submit bugs on SF
        form_vars = {'func': 'postaddbug',
                     'group_id': BugLabels.PROJECT_GROUP_ID,
                     'category': BugLabels.CATEGORY,
                     'bug_group_id': BugLabels.GROUP,
                     'priority': BugLabels.PRIORITY,
                     'assigned_to': BugLabels.ASSIGNMENT,
                     }
        form_vars['summary'] = "%s (PR#%s)" % \
                               (self.msg.getheader('subject'),
                                self.id)
        collector = StringIO()
        self.dump(collector)
        collector.seek(0, 0)
        form_vars['details'] = collector.read().strip()

        return urllib.urlencode(form_vars)

    def encode_followup_comment(self, bug_id, msg):
        form_vars = {'func': 'postaddcomment',
                     'group_id': BugLabels.PROJECT_GROUP_ID,
                     'bug_id': bug_id,
                     }
        form_vars['details'] = msg.dump()
        return urllib.urlencode(form_vars)

class BugSummaryParser:
    """Parse the bug summary page from sourceforge

    Specific intent of this class is to extract the SF bug id
    associated with a newly entered jitterbug record.  We identify the
    Jitterbug record by the (PR#NNN) string in the details line.  The

    Requires that each bug is on its own line in the HTML table.  If
    SF changes its output, all bets are off.
    """

    def __init__(self):
        self.bugs = {}
        self.parser = sflib.SummaryParser(SF_SUBMIT_URL,
                                          ('detailbug',))

    def get_sf_bug_id(self, pr_id):
        return self.bugs[pr_id]

    rx_pr = re.compile('\(PR#(\d+)\)')

    def parse(self, buf):
        self.parser.parse(buf)
        self._load_hrefs()

    def _load_hrefs(self):
        """Load hrefs from the parser object inecto self.bugs"""
        for href, query_dict, line in self.parser.get_hrefs():
            mo = self.rx_pr.search(line)
            if not mo:
                continue
            pr_id = mo.group(1)
            bug_id = query_dict['bug_id']
            self.bugs[pr_id] = bug_id

