#!/usr/bin/env
# -*- coding: UTF-8 -*-
# vim: sw=4 ts=4 expandtab ai
#
# Copyright (c) 2013 Intel, Inc.
# License: GPLv2
# Author: Alexander Kanevskiy <alexander.kanevskiy@intel.com>
#
# This program 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.


"""
Add reviewers
"""

import netrc
import urlparse
import types
import re
import ConfigParser
from gerritrest import GerritREST

def to_bool(arg):
    """Convert string to bool"""
    if isinstance(arg, types.StringTypes):
        return bool(arg.lower() in ("true", "yes", "1"))
    else:
        return bool(arg)


def gerrit_client(config):
    """
    Returns tuple: GerritREST instance / error message
    """
    if not config.has_option("gerrit", "url"):
        return (None, 'Gerrit URL is not defined in config!')

    gerrit_url = config.get("gerrit", "url")
    if config.has_option("gerrit", "username") \
                and config.has_option("gerrit", "password"):
        user = config.get("gerrit", "username")
        password = config.get("gerrit", "password")
    else:
        # Fallback to ~/.netrc
        try:
            user, _, password = \
                    netrc.netrc().hosts[urlparse.urlparse(gerrit_url).netloc]
        except KeyError:
            return (None, "Unable to find credentials in ~/.netrc")

    return (GerritREST(gerrit_url, user, password), None)


def patchset_created(hook = None, config = None, logger = None, params = None,
                        extra_params = None):
    """Main entry on add_reviewers plugin"""

    section_name = 'add_reviewers'
    error_message = ''
    if not params.change:
        error_message = 'Change not defined'
    if not params.project:
        error_message = 'Project not defined'
    if not params.branch:
        error_message = 'Branch not defined'
    if error_message:
        logger.error(error_message)
        return 1

    rules, rop = get_rules(config, section_name)

    if not rop['enabled']:
        logger.warning("No rules enabled")
        return 0

    gerrit, error_message = gerrit_client(config)
    if error_message:
        logger.error(error_message)
        return 1

    # get change information from gerrit
    change_query = "change:%s+project:%s+branch:%s" % \
                (params.change, params.project, params.branch)

    logger.debug("Getting information about change: %s", change_query)
    change_info = gerrit.get_changes(query=change_query, all_revisions=True,
        all_files=rop['files'], detailed_accounts=rop['owners'],
        detailed_labels=True)
    if not change_info:
        logger.error("Change not found!")
        return 2
    elif len(change_info) > 1:
        logger.warning("More than one change like that found! Using first one.")
    change_info = change_info[0]

    if rop['project_groups']:
        logger.debug("Getting groups info for project %s", params.project)
        project_groups = gerrit.get_groups(project=params.project)
    else:
        project_groups = {}

    reviewers = []
    reviewer_groups = []
    for rule in rules:
        if rules[rule]['enabled'] and \
            match_branch(rules[rule], params.branch) and \
            match_project(rules[rule], params.project) and \
            match_change_author(rule, rules[rule], change_info, logger) and \
            match_change_file(rule, rules[rule], change_info, params.commit,
                logger):

            reviewers, reviewer_groups = return_reviewers(rules[rule])
            reviewer_groups.extend(
                match_project_group(rule, rules[rule], project_groups, logger))

    try:
        dry_run = config.getboolean("add_reviewers", "dry_run")
    except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
        dry_run = False

    if reviewers:
        logger.debug("reviewers: %d %s", len(reviewers), reviewers)
        add_reviewers(reviewers, gerrit, change_info, dry_run, logger)
    if reviewer_groups:
        logger.debug("reviewer groups: %d %s", len(reviewer_groups), reviewer_groups)
        add_reviewers(reviewer_groups, gerrit, change_info, dry_run, logger)


def match_project(rule, project):
    """Checks if project rule is present and project matches it."""
    if project and rule.get('project_regexp', None):
        project_match = re.compile(rule['project_regexp'])
        if not project_match.search(project):
            return False
    return True

def match_branch(rule, branch):
    """Checks if branch rule is present and branch matches it."""
    if branch and rule.get('branch_regexp', None):
        branch_match = re.compile(rule['branch_regexp'])
        if not branch_match.search(branch):
            return False
    return True

def match_change_file(name, rule, change, commit, logger):
    """Matches files changed in this change"""

    result = True
    if rule.get('file_regexp', None):
        file_match = re.compile(rule['file_regexp'])
        if commit and commit in change['revisions']:
            files = change['revisions'][commit].get('files', {})
        else:
            files = change['revisions']\
                    [change['current_revision']].get('files',{})
        result = False
        for change_file in files:
            if file_match.search(change_file):
                result = True
                logger.debug("[%s] Matched file: %s", name, change_file)
                break
    return result

def match_change_author(name, rule, change, logger):
    """Matches change author"""
    result = True
    if rule.get('author_regexp', None):
        owner_match = re.compile(rule['author_regexp'], re.IGNORECASE)
        owner_info = []
        owner_email = change['owner'].get('email', None)
        if owner_email:
            owner_info.append(owner_email)
        owner_name = change['owner'].get('name', None)
        if owner_name:
            owner_info.append(owner_name)
        logger.debug("[%s] Author info %s", name, owner_info)
        result = False
        for info in owner_info:
            if owner_match.search(info):
                result = True
                logger.debug("[%s] Matched owner: %s", rule, info)
                break
    return result

def match_project_group(name, rule, project_groups, logger):
    """Matches group associated with project"""
    result = []
    if rule.get('project_groups_regexp', None):
        group_match = re.compile(rule['project_groups_regexp'])
        for group in project_groups:
            if group_match.search(group):
                logger.debug("[%s] Matched group %s", name, group)
                result.append(group)
    return result

def return_reviewers(rule):
    """Returns reviewers and reviewer groups from rule"""
    reviewers = set()
    reviewer_groups = set()
    for reviewer in re.split(r'\s*[,;]\s*', rule.get('reviewer',"")):
        if reviewer.strip():
            reviewers.add(reviewer.strip())
    for group in re.split(r'\s*[,;]\s*', rule.get('reviewer_group',"")):
        if group.strip():
            reviewer_groups.add(group.strip())
    return list(reviewers), list(reviewer_groups)


def get_rules(config, section_name):
    """
    Parses config options and returns rules + options
    """
    section_name_len = len(section_name)
    rules = {}
    options = {
        'project_groups': False,
        'files': False,
        'owners': False,
        'enabled': False
    }

    for section in config.sections():
        if len(section) < section_name_len+1 or \
                        section[:section_name_len] != section_name:
            continue
        rule = section[section_name_len+1:]
        rules[rule] = dict(config.items(section))
        rules[rule]['enabled'] = to_bool(rules[rule].get('enabled', None))
        if rules[rule]['enabled']:
            options['enabled'] = True
            if rules[rule].get('project_groups_regexp', None):
                options['project_groups'] = True
            if rules[rule].get('file_regexp', None):
                options['files'] = True
            if rules[rule].get('author_regexp', None):
                options['owners'] = True
    return rules, options


def add_reviewers(reviewers, gerrit, change, dry_run, logger):
    """Adding reviewers for change if dry_run is False"""
    for reviewer in reviewers:
        if not reviewer:
            continue
        logger.debug("Adding reviewer %s (dry run: %s)", reviewer, dry_run)
        if not dry_run:
            added = gerrit.add_change_reviewer(change['id'],
                                                reviewer, confirmed=True)
            if not added:
                logger.error("Unknown error adding %s to change %s",
                                                    reviewer, change['id'])
                continue
            if 'error' in added:
                logger.error("Error adding %s to change %s: %s",
                                reviewer, change['change_id'], added['error'])
            elif 'reviewers' in added and added['reviewers']:
                for person in added['reviewers']:
                    logger.info("Added reviewer %s to change %s",
                        person.get('name',
                            person.get('email',
                                person.get('_account_id', None))),
                        change['change_id'])

