"""Git repository hook utilities."""

from collections import defaultdict
from copy import deepcopy

from rbtools.hooks.common import get_review_request_id
from rbtools.utils.process import run_process


def get_branch_name(ref_name):
    """Returns the branch name corresponding to the specified ref name."""
    branch_ref_prefix = 'refs/heads/'

    if ref_name.startswith(branch_ref_prefix):
        return ref_name[len(branch_ref_prefix):]


def get_commit_hashes(old_rev, new_rev):
    """Returns a list of abbreviated commit hashes from old_rev to new_rev."""
    git_command = ['git', 'rev-list', '--abbrev-commit', '--reverse', '%s..%s'
                   % (old_rev, new_rev)]

    return (
        run_process(git_command)
        .stdout
        .read()
        .split('\n')
    )


def get_unique_commit_hashes(ref_name, new_rev):
    """Returns a list of abbreviated commit hashes unique to ref_name."""
    git_command = ['git', 'rev-list', new_rev, '--abbrev-commit', '--reverse',
                   '--not']
    git_command.extend(get_excluded_branches(ref_name))

    return (
        run_process(git_command)
        .stdout
        .read()
        .strip()
        .split('\n')
    )


def get_excluded_branches(ref_name):
    """Returns a list of all branches, excluding the specified branch."""
    git_command = ['git', 'for-each-ref', 'refs/heads/', '--format=%(refname)']
    all_branches = (
        run_process(git_command)
        .stdout
        .read()
        .strip()
        .split('\n')
    )

    return [branch.strip() for branch in all_branches if branch != ref_name]


def get_branches_containing_commit(commit_hash):
    """Returns a list of all branches containing the specified commit."""
    git_command = ['git', 'branch', '--contains', commit_hash]
    branches = (
        run_process(git_command)
        .stdout
        .read()
        .replace('*', '')
        .split('\n')
    )

    return [branch.strip() for branch in branches]


def get_commit_message(commit):
    """Returns the specified commit's commit message."""
    git_command = ['git', 'show', '-s', '--pretty=format:%B', commit]

    return (
        run_process(git_command)
        .stdout
        .read()
        .strip()
    )


def get_review_id_to_commits_map(lines, regex):
    """Returns a dictionary, mapping a review request ID to a list of commits.

    The commits must be in the form: oldrev newrev refname (separated by
    newlines), as given by a Git pre-receive or post-receive hook.

    If a commit's commit message does not contain a review request ID, we
    append the commit to the key 0.
    """
    review_id_to_commits_map = defaultdict(list)

    # Store a list of new branches (which have an all-zero old_rev value)
    # created in this push to handle them specially.
    new_branches = []
    null_sha1 = '0' * 40

    for line in lines:
        old_rev, new_rev, ref_name = line.split()
        branch_name = get_branch_name(ref_name)

        if not branch_name or new_rev == null_sha1:
            continue

        if old_rev == null_sha1:
            new_branches.append(branch_name)
            commit_hashes = get_unique_commit_hashes(ref_name, new_rev)
        else:
            commit_hashes = get_commit_hashes(old_rev, new_rev)

        for commit_hash in commit_hashes:
            if commit_hash:
                commit_message = get_commit_message(commit_hash)
                review_request_id = get_review_request_id(regex,
                                                          commit_message)

                commit = '%s (%s)' % (branch_name, commit_hash)
                review_id_to_commits_map[review_request_id].append(commit)

    # If there are new branches, check every commit in the dictionary
    # (corresponding to only old branches) to see if the new branches also
    # contain that commit.
    if new_branches:
        review_id_to_commits_map_copy = deepcopy(review_id_to_commits_map)

        for review_id, commit_list in review_id_to_commits_map_copy.items():
            for commit in commit_list:
                commit_branch = commit[:commit.find('(') - 1]

                if commit_branch in new_branches:
                    continue

                commit_hash = commit[commit.find('(') + 1:-1]
                commit_branches = get_branches_containing_commit(commit_hash)

                for branch in set(new_branches).intersection(commit_branches):
                    new_commit = '%s (%s)' % (branch, commit_hash)
                    review_id_to_commits_map[review_id].append(new_commit)

    return review_id_to_commits_map
