#! /usr/bin/env python

# The configuration file should look like:
"""
[ircbot]
nick=NICKNAME
pass=PASSWORD
server=irc.freenode.net
port=6667
channel_config=/path/to/yaml/config

[gerrit]
user=gerrit2
key=/path/to/id_rsa
host=review.example.com
port=29418
"""

# The yaml channel config should look like:
"""
openstack-dev:
    events:
      - patchset-created
      - change-merged
    projects:
      - openstack/nova
      - openstack/swift
    branches:
      - master
"""

import ircbot
import time
import subprocess
import threading
import select
import json
import sys
import os
import ConfigParser
import daemon, daemon.pidlockfile
import traceback
import yaml

class GerritBot(ircbot.SingleServerIRCBot):
    def __init__(self, channels, nickname, password, server, port=6667):
        ircbot.SingleServerIRCBot.__init__(self, 
                                           [(server, port)], 
                                           nickname, nickname)
        self.channel_list = channels
        self.nickname = nickname
        self.password = password

    def on_nicknameinuse(self, c, e):
        c.nick(c.get_nickname() + "_")
        c.privmsg("nickserv", "identify %s " % self.password)
        c.privmsg("nickserv", "ghost %s %s" % (self.nickname, self.password))
        c.privmsg("nickserv", "release %s %s" % (self.nickname, self.password))
        time.sleep(1)
        c.nick(self.nickname)

    def on_welcome(self, c, e):
        c.privmsg("nickserv", "identify %s "% self.password)
        for channel in self.channel_list:
            c.join(channel)

    def send(self, channel, msg):
        self.connection.privmsg(channel, msg)
        time.sleep(0.5)

class Gerrit(threading.Thread):
    def __init__(self, ircbot, channel_config,
                 username, keyfile, server, port=29418):
        threading.Thread.__init__(self)
        self.ircbot = ircbot
        self.channel_config = channel_config
        self.username = username
        self.keyfile = keyfile
        self.server = server
        self.port = port
        self.proc = None
        self.poll = select.poll()

    def _open(self):
        self.proc = subprocess.Popen(['/usr/bin/ssh', '-p', str(self.port),
                                      '-i', self.keyfile,
                                      '-l', self.username, self.server,
                                      'gerrit', 'stream-events'],
                                     bufsize=1, 
                                     stdin=None,
                                     stdout=subprocess.PIPE,
                                     stderr=None,
                                     )
        self.poll.register(self.proc.stdout)

    def _close(self):
        try:
            self.poll.unregister(self.proc.stdout)
        except:
            pass
        try:
            self.proc.kill()
        except:
            pass
        self.proc = None

    def patchset_created(self, channel, data):
        msg = '%s proposed a change to %s: %s  %s' % (
            data['patchSet']['uploader']['name'],
            data['change']['project'],
            data['change']['subject'],
            data['change']['url'])
        self.ircbot.send(channel, msg)

    def comment_added(self, channel, data):
        msg = 'A comment has been added to a proposed change to %s: %s  %s' % (
            data['change']['project'],
            data['change']['subject'],
            data['change']['url'])
        self.ircbot.send(channel, msg)
        
        for approval in data.get('approvals', []):
            if (approval['type'] == 'VRIF' and approval['value'] == '-2' and
                                channel in self.channel_config.events.get(
                                                    'x-vrif-minus-2', set())):
                msg = 'Verification of a change to %s failed: %s  %s' % (
                    data['change']['project'],
                    data['change']['subject'],
                    data['change']['url'])
                self.ircbot.send(channel, msg)

            if (approval['type'] == 'VRIF' and approval['value'] == '2' and
                                channel in self.channel_config.events.get(
                                                    'x-vrif-plus-2', set())):
                msg = 'Verification of a change to %s succeeded: %s  %s' % (
                    data['change']['project'],
                    data['change']['subject'],
                    data['change']['url'])
                self.ircbot.send(channel, msg)

            if (approval['type'] == 'CRVW' and approval['value'] == '-2' and
                                channel in self.channel_config.events.get(
                                                    'x-crvw-minus-2', set())):
                msg = 'A change to %s has been rejected: %s  %s' % (
                    data['change']['project'],
                    data['change']['subject'],
                    data['change']['url'])
                self.ircbot.send(channel, msg)

            if (approval['type'] == 'CRVW' and approval['value'] == '2' and
                                channel in self.channel_config.events.get(
                                                    'x-crvw-plus-2', set())):
                msg = 'A change to %s has been approved: %s  %s' % (
                    data['change']['project'],
                    data['change']['subject'],
                    data['change']['url'])
                self.ircbot.send(channel, msg)

    def change_merged(self, channel, data):
        msg = 'A change was merged to %s: %s  %s' % (
            data['change']['project'],
            data['change']['subject'],
            data['change']['url'])
        self.ircbot.send(channel, msg)

    def _read(self):
        l = self.proc.stdout.readline()
        data = json.loads(l)
        channel_set = (self.channel_config.projects.get(
                            data['change']['project'], set()) &
                       self.channel_config.events.get(
                            data['type'], set()) &
                       self.channel_config.branches.get(
                            data['change']['branch'], set()))
        for channel in channel_set:
            if data['type'] == 'comment-added':
                self.comment_added(channel, data)
            elif data['type'] == 'patchset-created':
                self.patchset_created(channel, data)
            elif data['type'] == 'change-merged':
                self.change_merged(channel, data)

    def _listen(self):
        while True:
            ret = self.poll.poll()
            for (fd, event) in ret:
                if fd == self.proc.stdout.fileno():
                    if event == select.POLLIN:
                        self._read()
                    else:
                        raise Exception("event on ssh connection")
        
    def _run(self):
        try:
            if not self.proc:
                self._open()
            self._listen()
        except:
            traceback.print_exc()
            self._close()
            time.sleep(5)

    def run(self):
        time.sleep(5)
        while True:
            self._run()
            
class ChannelConfig(object):
    def __init__(self, data):
        self.data = data
        keys = data.keys()
        for key in keys:
            if key[0] != '#':
                data['#'+key] = data.pop(key)
        self.channels = data.keys()
        self.projects = {}
        self.events = {}
        self.branches = {}
        for channel, val in self.data.iteritems():
            for event in val['events']:
                event_set = self.events.get(event, set())
                event_set.add(channel)
                self.events[event] = event_set
            for project in val['projects']:
                project_set = self.projects.get(project, set())
                project_set.add(channel)
                self.projects[project] = project_set
            for branch in val['branches']:
                branch_set = self.branches.get(branch, set())
                branch_set.add(channel)
                self.branches[branch] = branch_set

def _main():
    config=ConfigParser.ConfigParser()
    config.read(sys.argv[1])

    fp = config.get('ircbot', 'channel_config')
    if fp:
        fp = os.path.expanduser(fp)
        if not os.path.exists(fp):
            raise Exception("Unable to read layout config file at %s" % fp)
    else:
        raise Exception("Channel Config must be specified in config file.")

    channel_config = ChannelConfig(yaml.load(open(fp)))

    bot = GerritBot(channel_config.channels,
                    config.get('ircbot', 'nick'),
                    config.get('ircbot', 'pass'),
                    config.get('ircbot', 'server'),
                    config.getint('ircbot', 'port'))
    g = Gerrit(bot, 
               channel_config,
               config.get('gerrit', 'user'),
               config.get('gerrit', 'key'),
               config.get('gerrit', 'host'),
               config.getint('gerrit', 'port'))
    g.start()
    bot.start()

def main():
    if len(sys.argv) != 2:
        print "Usage: %s CONFIGFILE" % sys.argv[0]
        sys.exit(1)

    pid = daemon.pidlockfile.TimeoutPIDLockFile(
            "/var/run/gerritbot/gerritbot.pid", 10)
    with daemon.DaemonContext(pidfile=pid):
        _main()


if __name__ == "__main__":
    main()