package com.atlassian.bitbucket.internal.defaultreviewers.mergecheck;

import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.internal.defaultreviewers.PullRequestConditionService;
import com.atlassian.bitbucket.internal.defaultreviewers.model.PullRequestCondition;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestParticipant;
import com.atlassian.bitbucket.pull.PullRequestRef;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.pull.MergeRequest;
import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.bitbucket.util.PagedIterable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.*;

import static com.atlassian.bitbucket.util.MoreStreams.streamIterable;
import static com.google.common.collect.Sets.intersection;
import static java.lang.Math.min;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.empty;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.*;

/**
 * Checks whether, for the given MergeRequest, whether there are any DefaultReviewerAssociations that have
 * unsatisfied requirements. If so, it vetoes the merge.
 */
@Component
public class RequiredReviewersMergeCheck implements MergeRequestCheck {

    private static final int MAX_PAGE_SIZE = 100;
    @Autowired
    private I18nService i18nService;
    @Autowired
    private PullRequestConditionService pullRequestConditionService;

    @Override
    public void check(@Nonnull MergeRequest mergeRequest) {
        requireNonNull(mergeRequest, "mergeRequest");

        PullRequest pullRequest = mergeRequest.getPullRequest();
        PullRequestRef fromRef = pullRequest.getFromRef();
        PullRequestRef toRef = pullRequest.getToRef();
        Repository repository = toRef.getRepository();

        PagedIterable<PullRequestCondition> pullRequestConditions = new PagedIterable<>(request ->
                PageUtils.asPageOf(PullRequestCondition.class,
                        pullRequestConditionService.getPullRequestConditions(repository, request)),
                MAX_PAGE_SIZE);

        Set<ApplicationUser> approvers = pullRequest.getReviewers()
                .stream()
                .filter(PullRequestParticipant::isApproved)
                .map(PullRequestParticipant::getUser)
                .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));

        Map<Set<ApplicationUser>, Integer> alreadyVetoed = new HashMap<>();

        streamIterable(pullRequestConditions)
                // Get all conditions that have requirements
                .filter(pullRequestCondition -> pullRequestCondition.getRequiredApprovals() > 0)
                // Get all conditions that match this PR
                .filter(pullRequestCondition -> pullRequestCondition.getFromMatcher().matches(fromRef))
                .filter(pullRequestCondition -> pullRequestCondition.getToMatcher().matches(toRef))
                // Remove duplicates
                .distinct()
                // Sort the rules by the number of required reviewers so that duplicates and groups with the same
                // users but less required can be filtered out
                .sorted((a, b) -> b.getRequiredApprovals() - a.getRequiredApprovals())
                // Get the veto reasons for each condition
                .map(pullRequestCondition -> vetoReason(pullRequest, pullRequestCondition, approvers, alreadyVetoed))
                // Filter out conditions that have been satisfied
                .filter(Optional::isPresent)
                .map(Optional::get)
                // Add veto reasons to merge request
                .forEach(reason -> mergeRequest.veto(
                        i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.summary"),
                        reason));
    }

    private Optional<String> joinNames(List<String> names, String finalJoiner) {
        if (names == null || names.isEmpty()) {
            return empty();
        }
        if (names.size() == 1) {
            return ofNullable(names.get(0));
        }
        return joinNames(names.subList(0, names.size() - 1))
                // and the last name with a "finalJoiner" (i.e. "and" or "or")
                .map(reasonStart -> format("%s %s %s",
                        reasonStart,
                        finalJoiner,
                        names.get(names.size() - 1)));
    }

    private Optional<String> joinNames(List<String> names) {
        if (names == null || names.isEmpty()) {
            return empty();
        }
        if (names.size() == 1) {
            return Optional.of(names.get(0));
        }
        return names
                .stream()
                // Join the first names with a "joiner" (comma)
                .reduce((a, b) -> format("%s%s %s",
                        a,
                        i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joiner"),
                        b));
    }

    private Optional<String> vetoReason(PullRequest pullRequest,
                                        PullRequestCondition pullRequestCondition,
                                        Set<ApplicationUser> approvers,
                                        Map<Set<ApplicationUser>, Integer> alreadyVetoed) {
        Set<ApplicationUser> requiredApprovers = pullRequestCondition
                .getReviewers()
                .stream()
                .filter(user -> user != pullRequest.getAuthor().getUser())
                .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));

        // The number of approvers on the condition may have become smaller than the number required if the PR author
        // was a required reviewer for this condition. In this case, we reduce the required approvals.
        int requiredApprovals = min(pullRequestCondition.getRequiredApprovals(), requiredApprovers.size());

        // If we have an equivalent rule (or one with the same reviewers but more required) then don't add this veto
        if (alreadyVetoed.containsKey(requiredApprovers) && alreadyVetoed.get(requiredApprovers) >= requiredApprovals) {
            return empty();
        }

        // Count how many of the required approvers have already approved the PR
        int alreadyApproved = intersection(requiredApprovers, approvers).size();

        // Veto the merge if there are people that still need to approve the PR
        if (requiredApprovals > alreadyApproved) {
            alreadyVetoed.put(requiredApprovers, requiredApprovals);
            List<String> approverNames = requiredApprovers
                    .stream()
                    .map(ApplicationUser::getDisplayName)
                    .collect(toList());
            if (approverNames.size() == 1 || approverNames.size() == requiredApprovals) {
                // "User1 must review and approve this pull request before it can be merged."
                // "User1, User2, and User3 must review and approve this pull request before it can be merged."
                return joinNames(approverNames,
                        i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerAnd"))
                        .map(namesComponent ->
                                i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.description",
                                        namesComponent));
            } else if (requiredApprovals == 1) {
                // "User1, User2 or User3 must review and approve this pull request before it can be merged."
                return joinNames(approverNames,
                        i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.joinerOr"))
                        .map(namesComponent ->
                                i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.description",
                                        namesComponent));
            } else {
                // "At least n of the following users must review and approve this pull request before it can be merged: User1, User2, User3."
                return joinNames(approverNames)
                        .map(namesComponent ->
                                i18nService.getMessage("bitbucket.defaultreviewers.requiredreviewers.mergecheck.vetoed.general.description",
                                        requiredApprovals, namesComponent));
            }
        }
        return empty();
    }
}
