#!/usr/bin/env python
"""
A script for reporting the results of an invirtibuild.  Supports any
combination of the following reporting mechanisms.

Note that all strings are interpolated with a dictionary containing
the following keys:

build_id, commit, failed_stage, inserted_at, package,
pocket, principal, result, short_commit, traceback, version

== zephyr ==

To configure zephyr, add something like the following to your invirt config:

build:
 hooks:
  post_build:
   zephyr: &post_build_zephyr
    class: myclass [required]
    instance: myinstance [optional]
    zsig: myzsig [optional]
  failed_build:
   zephyr: *post_build_zephyr
  ...

== mail ==

To configure email notifications, add something like the following to your invirt config:

build:
 hooks:
  post_build:
   mail: &post_build_mail
    to: myemail@example.com [required]
    from: myemail@example.com [required]
    subject: My Subject [optional]
  failed_build:
   mail: *post_build_mail
  ...

The script chooses which configuration option to use based off the
name it is called with.  This name also determines which command-line
arguments the script takes, as well as how they are formatted.  When
called as:

post-build: uses post_build option
failed-build: uses failed_build option
post-submit: uses post_submit option
failed-submit: uses failed_submit option
post-add-repo: uses post_add_repo option
"""

import optparse
import os
import re
import sys
import textwrap

from email.mime import text

from invirt import common, database, builder
from invirt.config import structs as config

def build_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x,
                         escape=lambda x: x):
    """Format a message reporting the results of a build"""
    values = dict(values)
    if not verbose and values['traceback'] is not None:
        split = values['traceback'].split('\n')
        # Here, have a hackish heuristic
        truncated = '(empty)'
        for i in xrange(2, len(split)):
            truncated = textwrap.fill('\n'.join(split[-i:]))
            if len(truncated) >= 10:
                break
        values['traceback'] = truncated

    for key in ['package', 'version', 'pocket', 'principal', 'inserted_at', 'short_commit']:
        values[key] = escape(values[key])

    if succeeded:
        values['result'] = success(values['result'])
        msg = """Build of %(package)s %(version)s in %(pocket)s %(result)s.
Job submitted by %(principal)s at %(inserted_at)s.
Branch %(pocket)s has been advanced to %(short_commit)s.""" % values
    else:
        values['result'] = failure(values['result'])
        msg = """Build of %(package)s version %(version)s in %(pocket)s %(result)s while %(failed_stage)s.
Job submitted by %(principal)s at %(inserted_at)s.
Error: %(traceback)s""" % values
    return msg

def submit_completion_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x,
                          escape=lambda x: x):
    values = dict(values)
    if succeeded:
        values['result'] = success(values['result'])
        values['_extra'] = ''
    else:
        values['result'] = failure(values['result'])
        values['_extra'] = '\nError: %s' % escape(sys.stdin.read())
    for key in ['commit', 'pocket', 'principal']:
        values[key] = escape(values[key])
    msg = """Submission of %(commit)s (%(package)s) to be built in %(pocket)s %(result)s.
Build submitted by %(principal)s.%(_extra)s""" % values
    return msg

def repo_creation_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x,
                      escape=lambda x: x):
    values = dict(values)
    assert succeeded
    for key in ['category', 'name', 'principal']:
        values[key] = escape(values[key])
    msg = '%(principal)s just created a new repository, %(category)s/%(name)s.git' % values
    return msg

def prebuild_msg(succeeded, values, verbose=True, success=lambda x: x, failure=lambda x: x,
                 escape=lambda x: x):
    for key in ['build_id', 'pocket', 'package', 'commit', 'principal', 'version', 'inserted_at']:
        values[key] = escape(values[key])
    msg = """Build started: %(package)s %(version)s in %(pocket)s.
Base commit %(commit)s .
Job submitted by %(principal)s at %(inserted_at)s.""" % values
    return msg

# Names of hooks
POST_BUILD = 'post-build'
FAILED_BUILD = 'failed-build'
POST_SUBMIT = 'post-submit'
FAILED_SUBMIT = 'failed-submit'
POST_ADD_REPO = 'post-add-repo'
PRE_BUILD = 'pre-build'

# Types of communication

ZEPHYR = 'zephyr'
MAIL = 'mail'

message_generators = {
    ZEPHYR : { POST_BUILD : build_completion_msg,
               FAILED_BUILD : build_completion_msg,
               POST_SUBMIT : submit_completion_msg,
               FAILED_SUBMIT : submit_completion_msg,
               POST_ADD_REPO : repo_creation_msg,
               PRE_BUILD : prebuild_msg },
    MAIL   : { POST_BUILD : build_completion_msg,
               FAILED_BUILD : build_completion_msg,
               POST_SUBMIT : submit_completion_msg,
               FAILED_SUBMIT : submit_completion_msg,
               POST_ADD_REPO : repo_creation_msg,
               PRE_BUILD : prebuild_msg }
    }

def zephyr_escape(m):
    m = str(m)
    m = re.sub('@', '@@', m)
    m = re.sub('}', '@(})', m)
    return m

def zephyr_success(m):
    return '}@{@color(green)%s}@{' % zephyr_escape(m)

def zephyr_failure(m):
    return '}@{@color(red)%s}@{' % zephyr_escape(m)

def main():
    parser = optparse.OptionParser('Usage: %prog [options] [arguments]')
    opts, args = parser.parse_args()
    prog = os.path.basename(sys.argv[0])

    try:
        if prog == POST_BUILD:
            hook_config = config.build.hooks.post_build
        elif prog == FAILED_BUILD:
            hook_config = config.build.hooks.failed_build
        elif prog == POST_SUBMIT:
            hook_config = config.build.hooks.post_submit
        elif prog == FAILED_SUBMIT:
            hook_config = config.build.hooks.failed_submit
        elif prog == POST_ADD_REPO:
            hook_config = config.build.hooks.post_add_repo
        elif prog == PRE_BUILD:
            hook_config = config.build.hooks.pre_build
        else:
            parser.error('hook script invoked with unrecognized name %s' % prog)
            return 2
    except common.InvirtConfigError:
        print >>sys.stderr, 'No hook configuration found for %s.' % prog
        return 1

    if prog in [POST_BUILD, FAILED_BUILD, PRE_BUILD]:
        if len(args) != 1:
            parser.set_usage('Usage: %prog [options] build_id')
            parser.print_help()
            return 1
        database.connect()
        build = database.Build.query().get(args[0])
        short_commit = builder.canonicalize_commit(build.package, build.commit, shorten=True)
        values = { 'build_id' : build.build_id,
                   'commit' : build.commit,
                   'failed_stage' : build.failed_stage,
                   'inserted_at' : build.inserted_at,
                   'package' : build.package,
                   'pocket' : build.pocket,
                   'principal' : build.principal,
                   'short_commit' : short_commit,
                   'traceback' : build.traceback,
                   'version' : build.version,
                   'default_instance' : 'b%(build_id)s',
                   'default_subject' : 'Build %(build_id)d %(result)s: %(package)s %(version)s in %(pocket)s'}
        if prog == PRE_BUILD:
            succeeded = True
        elif build.succeeded:
            assert prog == POST_BUILD
            values['result'] = 'succeeded'
            succeeded = True
        else:
            assert prog == FAILED_BUILD
            values['result'] = 'failed'
            succeeded = False
    elif prog in [POST_SUBMIT, FAILED_SUBMIT]:
        if len(args) != 4:
            parser.set_usage('Usage: %prog [options] pocket package commit principal')
            parser.print_help()
            return 2
        values = { 'pocket' : args[0],
                   'package' : args[1],
                   'commit' : args[2],
                   'principal' : args[3],
                   'default_instance' : 'submission',
                   'default_subject' : 'Submission %(result)s: %(package)s %(version)s in %(pocket)s'}
        if prog == POST_SUBMIT:
            values['result'] = 'succeeded'
            succeeded = True
        else:
            values['result'] = 'failed'
            succeeded = False
    elif prog in [POST_ADD_REPO]:
        if len(args) != 3:
            parser.set_usage('Usage: %prog [options] category name principal')
            parser.print_help()
            return 3
        values = { 'category' : args[0],
                   'name' : args[1],
                   'principal' : args[2],
                   'default_instance' : 'new-repo',
                   'default_subject' : 'New repository %(category)s/%(name)s'}
        succeeded = True
    else:
        raise AssertionError('Impossible state')

    try:
        zephyr_config = hook_config.zephyr
        klass = zephyr_config['class'] % values
    except common.InvirtConfigError:
        print >>sys.stderr, 'No zephyr configuration specified for %s.' % prog
    else:
        make_msg = message_generators[ZEPHYR][prog]
        msg = '@{%s}' % make_msg(succeeded, values, verbose=False,
                                 success=zephyr_success, failure=zephyr_failure,
                                 escape=zephyr_escape)
        instance = zephyr_config.get('instance', values['default_instance']) % values
        zsig = zephyr_config.get('zsig', 'XVM Buildbot') % values
        common.captureOutput(['zwrite', '-c', klass, '-i', instance, '-s', zsig,
                              '-O', 'auto', '-d', '-m', msg],
                             stdout=None, stderr=None)

    try:
        mail_config = hook_config.mail
        to = mail_config.to % values
        sender = mail_config['from'] % values
    except common.InvirtConfigError:
        print >>sys.stderr, 'No email configuration specified for %s.' % prog
    else:
        make_msg = message_generators[MAIL][prog]
        msg = make_msg(succeeded, values)
        email = text.MIMEText(msg)
        email['To'] = to % values
        email['From'] = sender % values
        email['Subject'] = mail_config.get('subject', values['default_subject']) % values
        common.captureOutput(['sendmail', '-t'], email.as_string(),
                             stdout=None, stderr=None)
        
if __name__ == '__main__':
    sys.exit(main())