package com.atlassian.stash.internal.integrity;

import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.NoSuchCommitException;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.pull.DuplicatePullRequestException;
import com.atlassian.bitbucket.pull.PullRequestDeclineRequest;
import com.atlassian.bitbucket.pull.PullRequestRef;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.RefService;
import com.atlassian.bitbucket.repository.ResolveRefRequest;
import com.atlassian.bitbucket.repository.StandardRefType;
import com.atlassian.bitbucket.scm.CommitsCommandParameters;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.user.SecurityService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.stash.internal.pull.InternalPullRequest;
import com.atlassian.stash.internal.pull.InternalPullRequestService;
import com.atlassian.stash.internal.user.InternalApplicationUser;

import java.util.function.Supplier;

class PullRequestIntegrityHelper {

    private final IntegrityCheckReporter integrityCheckReporter;
    private final InternalPullRequestService pullRequestService;
    private final RefService refService;
    private final ScmService scmService;
    private final SecurityService securityService;
    private final Supplier<InternalApplicationUser> serviceUser;

    PullRequestIntegrityHelper(IntegrityCheckReporter integrityCheckReporter,
                               InternalPullRequestService pullRequestService,
                               RefService refService, ScmService scmService,
                               SecurityService securityService, Supplier<InternalApplicationUser> serviceUser) {
        this.integrityCheckReporter = integrityCheckReporter;
        this.pullRequestService = pullRequestService;
        this.refService = refService;
        this.scmService = scmService;
        this.securityService = securityService;
        this.serviceUser = serviceUser;
    }

    /**
     * Checks if the merge commit for the merge activity exists in the repository. If it does not, reopen or decline the
     * pull request.
     *
     * @param pullRequest the pull request that may be reopened or declined
     * @param commitId    the ID for the merge commit to check
     * @return {@code true} if a reopen or decline to restore integrity was attempted on the merge activity's pull
     * request,
     * {@code false} otherwise
     */
    boolean checkIntegrity(InternalPullRequest pullRequest, String commitId) {
        integrityCheckReporter.debug("{}: Integrity checking Pull Request #{}", pullRequest.getScopeRepository(),
                pullRequest.getId());
        if (pullRequest.getState() != PullRequestState.MERGED) {
            integrityCheckReporter.debug("{}: Pull request #{} is not marked as merged. Not attempting repair.",
                    pullRequest.getScopeRepository(), pullRequest.getId());
            return false;
        }

        return maybeReopenOrDecline(pullRequest, commitId);
    }

    private void decline(InternalPullRequest pullRequest, String reason) {
        securityService.impersonating(serviceUser.get(), "Declining pull request")
                .withPermission(Permission.REPO_READ)
                .call(() -> {
                    try {
                        integrityCheckReporter.reportInconsistency("{}: Pull request #{} could not be reopened, " +
                                "declining instead. (Reason: {})",
                                pullRequest.getScopeRepository(), pullRequest.getId(), reason);
                        pullRequestService.forceDecline(
                                new PullRequestDeclineRequest.Builder(pullRequest, pullRequest.getVersion())
                                        .build());
                        integrityCheckReporter.info("{}: Pull request #{} declined successfully",
                                pullRequest.getScopeRepository(), pullRequest.getId());
                    } catch (RuntimeException e) {
                        integrityCheckReporter.warn("{}: Pull request #{} {}: Pull request #{} could not be declined",
                                pullRequest.getScopeRepository(), pullRequest.getId(), e);
                    }
                    return null;
                });
    }

    private boolean maybeReopenOrDecline(InternalPullRequest pullRequest, String commitId) {
        PullRequestRef toRef = pullRequest.getToRef();
        CommitsCommandParameters parameters = new CommitsCommandParameters.Builder()
                .exclude(toRef.getId())
                .include(commitId)
                .ignoreMissing(false) // If either the merge-commit or target ref is missing, we want to know about it.
                .build();

        try {
            Page<Commit> commitPage = scmService.getCommandFactory(toRef.getRepository()).commits(parameters,
                    PageUtils.newRequest(0, 1)).call();
            if (commitPage == null) {
                return false;
            }

            return commitPage.stream()
                    .findFirst()
                    .map(commit -> tryReopen(pullRequest)) // Commit is in the repository, but not on the
                    // target branch
                    .orElse(false); // Commit is on the target ref, pull request _is_ merged
        } catch (NoSuchCommitException e) {
            if (commitId.equals(e.getCommitId())) {
                return tryReopen(pullRequest); // Merge-commit is not in the repository. Attempt to restore the
                // pull request
            } else if (toRef.getId().equals(e.getCommitId())) {
                // This is a valid state. Scenario:
                // - (FS Snapshot)
                // - Create PR branch-1 -> branch-2
                // - Create PR branch-2 -> master
                // - Merge branch-1 -> branch-2 -> master and delete branches on merge
                // - (DB Snapshot)
                integrityCheckReporter.info("{}: Pull request #{}'s target branch does not exist. This is a valid " +
                                "scenario. No attempt will be made to modify pull request state",
                        pullRequest.getScopeRepository(), pullRequest.getId());
                return false;
            } else {
                decline(pullRequest, "unexpected missing commit");
            }
        }

        return false;
    }

    //This method first checks that a pullRequest's fromRef and toRef exist if they don't it declines the pullRequest
    //If they do an attempt will be made to reopen the pull request
    private boolean tryReopen(InternalPullRequest pullRequest) {
        integrityCheckReporter.reportInconsistency("{}: Pull request #{} is marked merged but the merge commit could " +
                        "not be found on the target ref. Trying to restore integrity by reopening",
                pullRequest.getScopeRepository(), pullRequest.getId());

        Ref fromRef = refService.resolveRef(new ResolveRefRequest.Builder(pullRequest.getFromRef().getRepository())
                .refId(pullRequest.getFromRef().getId())
                .type(null)
                .build());

        Ref toRef = refService.resolveRef(new ResolveRefRequest.Builder(pullRequest.getToRef().getRepository())
                .refId(pullRequest.getToRef().getId())
                .type(StandardRefType.BRANCH)
                .build());

        if (fromRef == null) {
            decline(pullRequest, "fromRef could not be resolved");
            return true;
        } else if (toRef == null) {
            integrityCheckReporter.info("{}: Could not find toRef for pull request #{} no further action will be " +
                    "taken", pullRequest.getScopeRepository(), pullRequest.getId());
            return false;
        }

        securityService.impersonating(serviceUser.get(), "Reopening unmerged pull request")
                .withPermission(Permission.REPO_READ)
                .call(() -> {
                    try {
                        pullRequestService.forceReopen(pullRequest.getScopeRepository().getId(),
                                pullRequest.getId());
                        integrityCheckReporter.info("{}: Pull request #{} has been successfully reopened",
                                pullRequest.getScopeRepository(), pullRequest.getId());
                    } catch (DuplicatePullRequestException exception) {
                        decline(pullRequest, "an open pull request with the same to and from refs already exists");
                    } catch (RuntimeException e) {
                        integrityCheckReporter.warn("{}: Pull request #{} could not be reopened",
                                pullRequest.getScopeRepository(), pullRequest.getId(), e);
                    }
                    return null;
                });
        return true;
    }
}
