package com.atlassian.stash.internal.repository.ref.restriction;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.event.branch.BranchCreationRequestedEvent;
import com.atlassian.bitbucket.event.branch.BranchDeletionRequestedEvent;
import com.atlassian.bitbucket.event.repository.RefChangeRequestedEvent;
import com.atlassian.bitbucket.event.tag.TagCreationRequestedEvent;
import com.atlassian.bitbucket.event.tag.TagDeletionRequestedEvent;
import com.atlassian.bitbucket.hook.HookResponse;
import com.atlassian.bitbucket.hook.PreReceiveHook;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.pull.PullRequestRef;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.repository.ref.restriction.*;
import com.atlassian.bitbucket.scm.pull.MergeRequest;
import com.atlassian.bitbucket.scm.pull.MergeRequestCheck;
import com.atlassian.bitbucket.user.EscalatedSecurityContext;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.event.api.EventListener;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.TreeMultimap;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;

/**
 * Implements a {@link PreReceiveHook pre-receive hook} and {@link MergeRequestCheck merge check} that
 * enforces the configured ref restrictions for the repository in question.
 *
 * @since 3.10
 */
public class RestrictionEnforcer implements PreReceiveHook, MergeRequestCheck {

    private static final Comparator<RefChange> REF_CHANGE_COMPARATOR = (refChange1, refChange2) ->
            refChange1.getRef().getId().compareTo(refChange2.getRef().getId());

    private final AuthenticationContext authenticationContext;
    private final I18nService i18nService;
    private final RefRestrictionService restrictionService;
    private final EscalatedSecurityContext withRepoAdmin;

    public RestrictionEnforcer(AuthenticationContext authenticationContext,
                               I18nService i18nService,
                               RefRestrictionService restrictionService,
                               SecurityService securityService) {
        this.authenticationContext = authenticationContext;
        this.i18nService = i18nService;
        this.restrictionService = restrictionService;
        this.withRepoAdmin = securityService.withPermission(Permission.REPO_ADMIN, "For branch permission");
    }

    @Override
    public boolean onReceive(@Nonnull Repository repository, @Nonnull Collection<RefChange> refChanges,
                             @Nonnull HookResponse hookResponse) {

        // 1. match the paths
        RestrictionMatchRequest matchRequest = new RestrictionMatchRequest.Builder(repository, Collections.emptyList())
                .user(authenticationContext.getCurrentUser())
                .refChanges(refChanges)
                .build();
        Multimap<RefChange, RefRestriction> toBeChecked = withRepoAdmin
                .call(() -> restrictionService.match(matchRequest));

        if (toBeChecked.isEmpty()) {
            return true;
        }

        Multimap<RefChange, String> errors = TreeMultimap.create(REF_CHANGE_COMPARATOR, Ordering.natural());

        // 2. generate errors for any matching restrictions
        for (Map.Entry<RefChange, RefRestriction> entry : toBeChecked.entries()) {
            RefRestriction restriction = entry.getValue();
            RefChange refChange = entry.getKey();
            String refId = refChange.getRef().getId();

            switch (restriction.getType()) {
                case NO_DELETES:
                    errors.put(refChange, i18nService.getMessage("bitbucket.branch.permission.reject.no.delete",
                            refId));
                    break;
                case FAST_FORWARD_ONLY:
                    errors.put(refChange, i18nService.getMessage("bitbucket.branch.permission.reject.fast.forward.only",
                            refId));
                    break;
                case PULL_REQUEST_ONLY:
                    errors.put(refChange, i18nService.getMessage("bitbucket.branch.permission.reject.pull.request.only",
                            refId));
                    break;
                case READ_ONLY:
                    errors.put(refChange, i18nService.getMessage("bitbucket.branch.permission.reject.read.only",
                            refId));
                    break;
            }
        }

        // 3. output any errors
        if (!errors.isEmpty()) {
            hookResponse.out().append(BOUNCER_ASCII_ART);
            hookResponse.out().append("\n").append(SEPARATOR_LINE);
            for (String error : errors.values()) {
                hookResponse.out().append(error).append("\n");
            }
            hookResponse.out().println(i18nService
                    .getMessage("bitbucket.branch.permission.check.settings.administrator"));
            hookResponse.out().append(SEPARATOR_LINE).append("\n");
        }

        return errors.isEmpty();
    }

    @Override
    public void check(@Nonnull MergeRequest request) {
        PullRequestRef toRef = request.getPullRequest().getToRef();

        if (!canMergePullRequest(request.getPullRequest().getFromRef(), toRef)) {
            request.veto(
                    i18nService.getMessage("bitbucket.branch.permission.branch.permission.merge.check.summary"),
                    i18nService.getMessage("bitbucket.branch.permission.branch.permission.merge.check",
                            toRef.getDisplayId()));
        }
    }

    @EventListener
    public void onBranchCreationRequested(BranchCreationRequestedEvent event) {
        cancelIfNoCreatePermission(event.getBranch(), event);
    }

    @EventListener
    public void onBranchDeletionRequested(BranchDeletionRequestedEvent event) {
        cancelIfNoDeletePermission(event.getBranch(), event);
    }

    @EventListener
    public void onTagCreationRequested(TagCreationRequestedEvent event) {
        cancelIfNoCreatePermission(event.getTag(), event);
    }

    @EventListener
    public void onTagDeletionRequested(TagDeletionRequestedEvent event) {
        cancelIfNoDeletePermission(event.getTag(), event);
    }

    private void cancelIfNoCreatePermission(MinimalRef ref, RefChangeRequestedEvent event) {
        RefAccessRequest accessRequest = new RefAccessRequest.Builder(event.getRepository(), RefAccessType.CREATE)
                .ref(ref)
                .build();
        if (!restrictionService.hasPermission(accessRequest)) {
            event.cancel(i18nService.createKeyedMessage("bitbucket.branch.permission.cancel.read.only",
                    ref.getDisplayId()));
        }
    }

    private void cancelIfNoDeletePermission(MinimalRef ref, RefChangeRequestedEvent event) {
        RefAccessRequest accessRequest = new RefAccessRequest.Builder(event.getRepository(), RefAccessType.DELETE)
                .ref(ref)
                .build();
        if (!restrictionService.hasPermission(accessRequest)) {
            event.cancel(i18nService.createKeyedMessage("bitbucket.branch.permission.reject.no.delete",
                    ref.getDisplayId()));
        }
    }

    private boolean canMergePullRequest(PullRequestRef fromRef, PullRequestRef toRef) {
        RefChange refChange = new SimpleRefChange.Builder().from(fromRef).to(toRef).type(RefChangeType.UPDATE).build();
        RestrictionMatchRequest request = new RestrictionMatchRequest.Builder(toRef.getRepository(), ImmutableList.of(
                RefRestrictionType.READ_ONLY))
                .refChange(refChange)
                .user(authenticationContext.getCurrentUser())
                .build();
        return withRepoAdmin.call(() -> restrictionService.match(request)).isEmpty();
    }

    private final String SEPARATOR_LINE = "----------------------------------------------------\n";
    private final String BOUNCER_ASCII_ART =
                    "                            *%%%%%.                            \n" +
                    "                        %%%         %%%                        \n" +
                    "                     ,%#               %%                      \n" +
                    "                    %%                   %%                    \n" +
                    "                   %#                     %%                   \n" +
                    "                  %%                       %                   \n" +
                    "                  %(                       %%                  \n" +
                    "                  %%%%%%%%%%%%%%%%%%%%%%%%%%%                  \n" +
                    "                %#%*%#///////%# %%///////%%%%%%                \n" +
                    "               ,% %*%%******%#   %%******%(%%,%                \n" +
                    "                 %%/ %%/**%%/%%%%%%%(**#%( %%#                 \n" +
                    "                  %%          %%%          %(                  \n" +
                    "                   %                      .%                   \n" +
                    "                   *%        %%%%%       .%                    \n" +
                    "                     %#                 %%                     \n" +
                    "                      .%%            .%%                       \n" +
                    "                      .%%.%%,     %%%.%%/                      \n" +
                    "                %%%%%%##%.  #%%%%%.  .%((%%%%%%                \n" +
                    "            %%#(((((((((%%,         #%%(((((((((#%%.           \n" +
                    "      %%%((((((((((((((((((%%%, .%%%((((((((((((((((((#%%*     \n" +
                    "    %%(((((((((((((((((((((((((%(((((((((((((((((((((((((#%.   \n" +
                    "  ,%(((((((((((((((((((((((((((((((((((((((((((((((((((((((%#  \n" +
                    "  %#((((((((((((((((((((((((((((((((((((((((((((((((((((((((%  \n" +
                    "  %%%%%%%%%%%%%(((((((((((((((((((((((((((((((((%%%%%%%%%%%%%  \n" +
                    " %%            %####((((((###%%%%%%%%#(((((((((%            ,% \n" +
                    ",%             %%%%%%#.               %%%((((((%*            %%\n" +
                    "#%                                       %%%#                %%\n" +
                    ".%                             .%%%%%%%%%                    %#\n" +
                    " %                         #%%%                              % \n" +
                    " %                     %%%%                                  %*\n" +
                    "/%************/#%%%%%%######%%*                        ..,*/(%%\n" +
                    "              %%######(((((((##################%%              \n" +
                    "              %%######(((((((((((((((((((((((((%%              \n" +
                    "//////////////%%%%%%%%#########################%%/////////  ///";

}
