#!/usr/bin/env python
#
# This file is part of REPA: Release Engineering Process Assistant.
#
# Copyright (C) 2013 Intel Corporation
#
# REPA is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# version 2 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

"""
REPA: Release Engineering Process Assistant.

Copyright (C) Intel Corporation 2013
Licence: GPL version 2
Author: Ed Bartosh <eduard.bartosh@intel.com>

Group module.
Group submissions.
"""

import sys
import time
import json
import re

from collections import defaultdict
from StringIO import StringIO
from multiprocessing.pool import ThreadPool
from functools import partial

from repa.main import sub_main
from repa.obs import OBS
from repa.common import RepaException, get_project_by_name, OBS_PROJECT_PREFIX


def check_target_prj(submissions):
    """Check if target projects are the same for all submissions"""
    result = defaultdict(list)
    for submission, data in submissions.iteritems():
        result[data['meta']['obs_target_prj']].append(submission)
    if len(result) > 1:
        message = '\n'.join('%s: %s' % (project, ' '.join(subms)) \
                                for project, subms in result.iteritems())
        raise RepaException('Target projects differ:\n%s\n' % message)


def check_build_results(bresults):
    """Check if build targets are published."""
    for subm, _, results in bresults:
        for target, res in results.iteritems():
            if res['state'] != 'published' or res['code'] != 'published':
                if res['packages']:
                    raise RepaException("%s: target %s is not published yet" %
                                        (subm, target))
            # check if status of package builds is not worse than in
            # target project: for pkg, status in res['packages'] ...


def check_binary_pkgs(obs, submissions, noaggregate=''):
    """
    Check if submissions have common binary packages.
    Check if binary packages exist.
    Returns: list of non-conflicting submissions
    """
    binaries = defaultdict(dict)
    result = set(submissions.keys())
    for submission, data in sorted(submissions.iteritems()):
        pkgs = list(obs.get_binary_packages(data['project']))
        # check if submission has binary packages
        for repo, bins in pkgs:
            # check if submissions have common packages
            for subm, info in binaries.iteritems():
                if repo in info:
                    common = set(info[repo]).intersection(bins)
                    if common and noaggregate:
                        common = set(pkg for pkg in common \
                                      if not re.match(noaggregate, pkg))
                    if common:
                        print '%s and %s have %d common packages,' \
                              ' skipping %s' % (subm, submission,
                                                len(common), submission)
                        if submission in result:
                            result.remove(submission)
                        break
            else:
                binaries[submission][repo] = bins
    return result


def create_group_project(obs, submissions, meta, comment):
    """Create group project."""
    target_prj = str(meta['obs_target_prj'])
    timestamp = time.strftime('%Y%m%d.%H%M%S', time.gmtime())
    name = "submitgroup/%s/%s" % ('/'.join(meta['git_tag'].split('/')[1:-1]),
                                  timestamp)
    gmeta = {'name': name, 'obs_target_prj': target_prj,
             'submissions': submissions, 'comment': comment}
    project = '%s%s:%s-group' % (OBS_PROJECT_PREFIX, str(target_prj),
                                 name.replace('/', ':'))

    saved = sys.stdout
    sys.stdout = StringIO()
    try:
        obs.create_project(project, target_prj, description=json.dumps(gmeta))
    finally:
        sys.stdout = saved
    return name, str(project)


def aggregate(obs, bresults, gproject, processes):
    """Aggregate packages into group project."""
    def notify(out, submission, result):
        """Callback, called by apply_async."""
        pkg = result[1]
        out.write('aggregated: %s/%s\n' % (submission, pkg))
    aggregated = set()
    obs.set_global_flag('publish', 'disable', gproject)
    if processes > 1:
        pool = ThreadPool(processes=processes)
    for subm, prj, results in bresults:
        for res in results.itervalues():
            for pkg, state in res['packages']:
                if state == 'succeeded' and pkg not in aggregated:
                    if processes > 1:
                        callback = partial(notify, sys.stdout, subm)
                        pool.apply_async(obs.aggregate_package,
                                         [prj, pkg, gproject, pkg],
                                         callback=callback)
                    else:
                        obs.aggregate_package(prj, pkg, gproject, pkg)
                        print 'aggregated %s/%s' % (subm, pkg)
                    aggregated.add(pkg)

    if processes > 1:
        pool.close()
        pool.join()

    obs.set_global_flag('publish', 'enable', gproject)

    return aggregated


def group_submissions(obs, submissions, target, comment,
                      processes=0, noaggregate=''):
    """Group multiple submissions into one group."""
    # find correspondent prerelease projects
    info = {}
    for submission in submissions:
        project, meta, build_results = get_project_by_name(obs, submission,
                                                           target)
        info[submission] = {'project': project, 'meta': meta,
                            'build_results': build_results}

    # Validate submissions
    check_target_prj(info)

    bresults = [(subm, data['project'], data['build_results']) \
                     for subm, data in info.iteritems()]
    check_build_results(bresults)

    # filter out conflicting submissions
    filtered = check_binary_pkgs(obs, info, noaggregate)
    bresults = [item for item in bresults if item[0] in filtered]
    info = dict(item for item in info.iteritems() if item[0] in filtered)

    # create group project
    name, gproject = create_group_project(obs, info.keys(),
                                          info.itervalues().next()['meta'],
                                          comment)
    print 'Created submit group %s\n' % name

    aggregated = aggregate(obs, bresults, gproject, processes)

    print '\n%d submissions (%d packages) have been merged into %s' % \
          (len(info), len(aggregated), name)

class Group(object):
    """Subcommand: Manage group  submissions."""

    name = 'group'
    description = 'Group submissions'
    help = description

    @staticmethod
    def add_arguments(parser, config):
        """Adds arguments to the parser. Called from [sub_]main."""
        parser.add_argument('submission', nargs='+',
                            help='space separated list of submissions')
        parser.add_argument('--processes', type=int,
                            help='amount of parallel processes to use',
                            default=config.get('processes'))
        parser.add_argument('-c', '--comment', help='comment', default='')
        parser.add_argument('--noaggregate',
                            default=config.get('noaggregate', ''),
                            help='do not aggregate packages matching regexp')

    @staticmethod
    def run(argv):
        """Command line entry point. Called from [sub_]main."""
        obs = OBS(argv.apiurl, argv.apiuser, argv.apipasswd)
        return group_submissions(obs, argv.submission, argv.project,
                                 argv.comment, argv.processes,
                                 argv.noaggregate)


if __name__ == '__main__':
    sys.exit(sub_main(sys.argv[1:], Group()))
