#!/usr/bin/env python
"""Change source forwarder for bitbucket.org POST service.

bitbucket_buildbot.py will determine the repository information from
the JSON HTTP POST it receives from bitbucket.org and build the
appropriate repository.

If your bitbucket repository is private, you must add a ssh key to the
bitbucket repository for the user who initiated bitbucket_buildbot.py

bitbucket_buildbot.py is based on github_buildbot.py
"""

import logging
from optparse import OptionParser
import sys
import tempfile
import traceback

from twisted.web import server, resource
from twisted.internet import reactor
from twisted.spread import pb
from twisted.cred import credentials

try:
    import json
except ImportError:
    import simplejson as json


class BitBucketBuildBot(resource.Resource):
    """
    BitBucketBuildBot creates the webserver that responds to the
    BitBucket POST Service Hook.
    """
    isLeaf = True
    bitbucket = None
    master = None
    port = None
    private = False

    def render_POST(self, request):
        """
        Reponds only to POST events and starts the build process

        :arguments:
            request
                the http request object
        """
        try:
            payload = json.loads(request.args['payload'][0])
            logging.debug("Payload: " + str(payload))
            self.process_change(payload)
        except Exception:
            logging.error("Encountered an exception:")
            for msg in traceback.format_exception(*sys.exc_info()):
                logging.error(msg.strip())

    def process_change(self, payload):
        """
        Consumes the JSON as a python object and actually starts the build.

        :arguments:
            payload
                Python Object that represents the JSON sent by Bitbucket POST
                Service Hook.
        """
        if self.private:
            repo_url = 'ssh://hg@%s%s' % (
                self.bitbucket,
                payload['repository']['absolute_url'],
                )
        else:
            repo_url = 'http://%s%s' % (
                self.bitbucket,
                payload['repository']['absolute_url'],
                )
        changes = []
        for commit in payload['commits']:
            files = [file_info['file'] for file_info in commit['files']]
            revlink = 'http://%s%s/changeset/%s/' % (
                self.bitbucket,
                payload['repository']['absolute_url'],
                commit['node'],
                )
            change = {
                'revision': commit['node'],
                'revlink': revlink,
                'comments': commit['message'],
                'who': commit['author'],
                'files': files,
                'links': [revlink],
                'properties': dict(repository=repo_url),
                }
            changes.append(change)
        # Submit the changes, if any
        if not changes:
            logging.warning("No changes found")
            return
        host, port = self.master.split(':')
        port = int(port)
        factory = pb.PBClientFactory()
        deferred = factory.login(credentials.UsernamePassword("change",
                                                              "changepw"))
        reactor.connectTCP(host, port, factory)
        deferred.addErrback(self.connectFailed)
        deferred.addCallback(self.connected, changes)

    def connectFailed(self, error):
        """
        If connection is failed.  Logs the error.
        """
        logging.error("Could not connect to master: %s"
                % error.getErrorMessage())
        return error

    def addChange(self, dummy, remote, changei, src='hg'):
        """
        Sends changes from the commit to the buildmaster.
        """
        logging.debug("addChange %s, %s" % (repr(remote), repr(changei)))
        try:
            change = changei.next()
        except StopIteration:
            remote.broker.transport.loseConnection()
            return None

        logging.info("New revision: %s" % change['revision'][:8])
        for key, value in change.iteritems():
            logging.debug("  %s: %s" % (key, value))

        change['src'] = src
        deferred = remote.callRemote('addChange', change)
        deferred.addCallback(self.addChange, remote, changei, src)
        return deferred

    def connected(self, remote, changes):
        """
        Reponds to the connected event.
        """
        return self.addChange(None, remote, changes.__iter__())


def main():
    """
    The main event loop that starts the server and configures it.
    """
    usage = "usage: %prog [options]"
    parser = OptionParser(usage)
    parser.add_option(
        "-p", "--port",
        help="Port the HTTP server listens to for the Bitbucket Service Hook"
        " [default: %default]", default=4000, type=int, dest="port")
    parser.add_option(
        "-m", "--buildmaster",
        help="Buildbot Master host and port. ie: localhost:9989 [default:"
        + " %default]", default="localhost:9989", dest="buildmaster")
    parser.add_option(
        "-l", "--log",
        help="The absolute path, including filename, to save the log to"
        " [default: %default]",
        default = tempfile.gettempdir() + "/bitbucket_buildbot.log",
        dest="log")
    parser.add_option(
        "-L", "--level",
        help="The logging level: debug, info, warn, error, fatal [default:"
        " %default]", default='warn', dest="level")
    parser.add_option(
        "-g", "--bitbucket",
        help="The bitbucket serve [default: %default]",
        default='bitbucket.org',
        dest="bitbucket")
    parser.add_option(
        '-P', '--private',
        help='Use SSH to connect, for private repositories.',
        dest='private',
        default=False,
        action='store_true',
        )
    (options, _) = parser.parse_args()
    # Set up logging.
    levels = {
        'debug': logging.DEBUG,
        'info': logging.INFO,
        'warn': logging.WARNING,
        'error': logging.ERROR,
        'fatal': logging.FATAL,
        }
    filename = options.log
    log_format = "%(asctime)s - %(levelname)s - %(message)s"
    logging.basicConfig(filename=filename, format=log_format,
                        level=levels[options.level])
    # Start listener.
    bitbucket_bot = BitBucketBuildBot()
    bitbucket_bot.bitbucket = options.bitbucket
    bitbucket_bot.master = options.buildmaster
    bitbucket_bot.private = options.private
    site = server.Site(bitbucket_bot)
    reactor.listenTCP(options.port, site)
    reactor.run()


if __name__ == '__main__':
    main()
