package com.atlassian.stash.internal.pull.cleanup;

import com.atlassian.bitbucket.ForbiddenException;
import com.atlassian.bitbucket.IllegalEntityStateException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.pull.*;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.repository.RefChangeType;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.SimpleRefChange;
import com.atlassian.bitbucket.repository.ref.restriction.RefAccessRequest;
import com.atlassian.bitbucket.repository.ref.restriction.RefAccessType;
import com.atlassian.bitbucket.repository.ref.restriction.RefRestrictionService;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.GitScm;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.event.api.EventPublisher;
import com.google.common.base.Strings;

import java.util.Objects;

import static com.atlassian.bitbucket.util.Chainable.chain;

public class BranchDeletionServiceImpl implements PullRequestBranchDeletionService {

    private static final String EMPTY_HASH = Strings.repeat("0", 40);

    private final GitAgent agent;
    private final GitCommandBuilderFactory commandFactory;
    private final EventPublisher eventPublisher;
    private final I18nService i18nService;
    private final PermissionService permissionService;
    private final PullRequestService pullRequestService;
    private final RefRestrictionService refRestrictionService;

    public BranchDeletionServiceImpl(GitAgent agent,
                                     GitCommandBuilderFactory commandFactory,
                                     EventPublisher eventPublisher,
                                     I18nService i18nService, PermissionService permissionService,
                                     PullRequestService pullRequestService,
                                     RefRestrictionService refRestrictionService) {
        this.agent = agent;
        this.refRestrictionService = refRestrictionService;
        this.commandFactory = commandFactory;
        this.eventPublisher = eventPublisher;
        this.i18nService = i18nService;
        this.permissionService = permissionService;
        this.pullRequestService = pullRequestService;
    }

    @Override
    public void deleteSourceBranch(PullRequest pullRequest, boolean dryRun) {
        PullRequestRef fromRef = pullRequest.getFromRef();

        validateGitRepo(fromRef.getRepository());

        Branch sourceBranch = findBranch(fromRef.getRepository(), fromRef.getId());
        // If the branch is already deleted, don't do anything so we're idempotent
        if (sourceBranch != null) {
            // TODO: replace with BranchService.deleteBranch()
            // when it is decided to make the service a public component
            // (refer to the state of the working tree in 0faf00c as a guide)
            validateCanDeleteSourceBranch(pullRequest, sourceBranch);
            if (!dryRun) {
                actuallyDeleteSourceBranch(pullRequest);
            }
        }
    }

    private void actuallyDeleteSourceBranch(PullRequest pullRequest) {
        // ensure the pull request is not open
        if (pullRequest.getState() == PullRequestState.OPEN) {
            throw new IllegalEntityStateException(i18nService.createKeyedMessage("bitbucket.branch.deletion.on.open.pull.request"));
        }

        Repository sourceRepository = pullRequest.getFromRef().getRepository();
        PullRequestRef fromRef = pullRequest.getFromRef();

        // We're relying on the PR source reference to be fixed to the commit at merge-time
        commandFactory.builder(sourceRepository).updateRef().delete(fromRef.getId())
                .oldValue(fromRef.getLatestCommit()).build().call();

        eventPublisher.publish(new PullRequestSourceBranchDeletedEvent(this, sourceRepository,
                new SimpleRefChange.Builder()
                        .ref(fromRef)
                        .fromHash(fromRef.getLatestCommit())
                        .toHash(EMPTY_HASH)
                        .type(RefChangeType.DELETE)
                        .build()));
    }

    private Branch findBranch(Repository repository, String branchId) {
        return agent.resolveBranch(repository, branchId, true);
    }

    private void validateCanDeleteSourceBranch(PullRequest pullRequest, Branch sourceBranch) {
        PullRequestRef fromRef = pullRequest.getFromRef();
        Repository sourceRepository = fromRef.getRepository();

        if (sourceBranch.getIsDefault()) {
            throw new ArgumentValidationException(i18nService.createKeyedMessage("bitbucket.branch.deletion.default.branch", sourceBranch.getDisplayId()));
        }

        validateWritePermissions(sourceRepository);
        validateBranchPermissions(sourceRepository, sourceBranch);
        validateForOpenPullRequest(pullRequest, PullRequestDirection.INCOMING);
        validateForOpenPullRequest(pullRequest, PullRequestDirection.OUTGOING);
        validateForUnmergedChanges(pullRequest, sourceBranch);
    }

    private void validateForOpenPullRequest(PullRequest pullRequest, PullRequestDirection direction) {
        PullRequestRef ref = pullRequest.getFromRef();
        // get two results instead of one, as one may be _this_ pull request (if it is not already merged).
        Page<PullRequest> page = pullRequestService.search(new PullRequestSearchRequest.Builder()
                .repositoryAndBranch(direction, ref.getRepository().getId(), ref.getId())
                .state(PullRequestState.OPEN)
                .build(), new PageRequestImpl(0, 2));

        if (!chain(page.getValues()).transform(PullRequest.TO_ID).filter(id -> !Objects.equals(id, pullRequest.getId())).isEmpty()) {
            if (direction == PullRequestDirection.INCOMING) {
                throw new IllegalEntityStateException(i18nService.createKeyedMessage("bitbucket.branch.deletion.incoming.pull-request"));
            } else {
                throw new IllegalEntityStateException(i18nService.createKeyedMessage("bitbucket.branch.deletion.outgoing.pull-request"));
            }
        }
    }

    private void validateBranchPermissions(Repository sourceRepository, Branch sourceBranch) {
        RefAccessRequest accessRequest = new RefAccessRequest.Builder(sourceRepository, RefAccessType.DELETE)
                .ref(sourceBranch)
                .build();
        if (!refRestrictionService.hasPermission(accessRequest)) {
            throw new ForbiddenException(i18nService.createKeyedMessage("bitbucket.branch.deletion.branch.restrictions"));
        }
    }

    /**
     * Check whether the source branch has not changed since we merged. This is really the 90% case, as the branch
     * may have been involved in another PR later on which was also merged. We could check against HEAD, but that
     * is also incorrect, as you may want to delete PRs involved in release branches (etc).
     */
    private void validateForUnmergedChanges(PullRequest pullRequest, Branch branch) {
        if (branch != null && !pullRequest.getFromRef().getLatestCommit().equals(branch.getLatestCommit())) {
            throw new IllegalEntityStateException(i18nService.createKeyedMessage("bitbucket.branch.deletion.unmerged.changes"));
        }
    }

    private void validateGitRepo(Repository sourceRepository) {
        // check if the source repository is on a Git repository
        if (!GitScm.ID.equals(sourceRepository.getScmId())) {
            throw new IllegalEntityStateException(i18nService.createKeyedMessage("bitbucket.branch.deletion.nongit", sourceRepository.getScmId()));
        }
    }

    private void validateWritePermissions(Repository sourceRepository) {
        if (!permissionService.hasRepositoryPermission(sourceRepository, Permission.REPO_WRITE)) {
            throw new ForbiddenException(i18nService.createKeyedMessage("bitbucket.branch.deletion.no.write"));
        }
    }
}
