package com.atlassian.stash.internal.scm.git.command;

import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.MinimalCommit;
import com.atlassian.bitbucket.commit.graph.CommitGraphNode;
import com.atlassian.bitbucket.commit.graph.TraversalCallback;
import com.atlassian.bitbucket.commit.graph.TraversalStatus;
import com.atlassian.bitbucket.commit.graph.TraversalSummary;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.pull.BulkPullRequestRescopeAnalyzer;
import com.atlassian.bitbucket.pull.PullRequestRescopeAnalysisResult;
import com.atlassian.bitbucket.pull.RescopeDetails;
import com.atlassian.bitbucket.pull.SimpleRescopeDetails;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.merge.GitNoCommonAncestorException;
import com.atlassian.bitbucket.scm.git.command.revlist.GitRevListOrder;
import com.atlassian.bitbucket.scm.git.command.revlist.GitRevListWalk;
import com.atlassian.bitbucket.scm.pull.BulkRescopeContext;
import com.atlassian.bitbucket.scm.pull.PullRequestRescope;
import com.atlassian.bitbucket.util.ShaUtils;
import com.atlassian.stash.internal.scm.git.command.merge.MergeBaseExitHandler;
import com.atlassian.stash.internal.scm.git.command.merge.MergeBaseOutputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.CallbackRevListOutputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.RevListInputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.TraversalRevListOutputHandler;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.atlassian.bitbucket.pull.PullRequestRescopeOutcome.UPDATE;

/**
 * @since 4.5
 */
public class BulkRescopeCommand extends SimpleGitCommand<Void> {

    private static final Logger log = LoggerFactory.getLogger(BulkRescopeCommand.class);

    private final Set<Repository> alternates;
    private final GitCommandBuilderFactory commandBuilderFactory;
    private final BulkRescopeContext context;
    private final I18nService i18nService;
    private final Repository repository;

    public BulkRescopeCommand(@Nonnull ExecutorService executorService,
                              @Nonnull Repository repository, @Nonnull GitCommandBuilderFactory builderFactory,
                              @Nonnull I18nService i18nService, @Nonnull BulkRescopeContext context) {
        super(executorService);

        this.commandBuilderFactory = builderFactory;
        this.context = context;
        this.i18nService = i18nService;
        this.repository = repository;

        alternates = new HashSet<>();
    }

    @Override
    public Void call() {
        // Create the rev-list callback that will analyze all PullRequestRescope instances available on the context.
        BulkPullRequestRescopeAnalyzer.Builder analyzerBuilder = new BulkPullRequestRescopeAnalyzer.Builder(
                context.getMaxCommitIds());
        Set<String> hashes = new HashSet<>();
        for (PullRequestRescope rescope : context) {
            analyzerBuilder.rescope(rescope);

            alternates.add(rescope.getFromRepository());
            alternates.add(rescope.getToRepository());
            hashes.add(rescope.getOldFromHash());
            hashes.add(rescope.getOldToHash());
            hashes.add(rescope.getNewFromHash());
            hashes.add(rescope.getNewToHash());
        }
        alternates.remove(repository);
        hashes.remove(null); // in case one of the newToHashes was null

        if (hashes.isEmpty()) {
            // nothing to do
            return null;
        }

        RescopeAnalysingCallback callback = new RescopeAnalysingCallback(analyzerBuilder.build());
        // determine the merge base (if possible)
        String mergeBase = getMergeBase(hashes, alternates);
        Set<String> excludes = mergeBase == null ? Collections.emptySet() : Collections.singleton(mergeBase);

        // include boundary commits to ensure the merge-base is included. The callback may need the merge-base
        // while doing analysis
        TraversalRevListOutputHandler outputHandler = new TraversalRevListOutputHandler(callback);
        configureAndCall(commandBuilderFactory.builder(repository)
                .revList()
                .ignoreMissing(true)
                .alternates(alternates)
                .boundary(true)
                .parents(true)
                .order(GitRevListOrder.DATE)
                .inputHandler(RevListInputHandler.forRefs(hashes, excludes))
                .build(outputHandler));
        return null;
    }

    private Map<String, Commit> getCommits(Set<String> commitIds) {
        if (commitIds.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<String, Commit> result = new HashMap<>();
        CallbackRevListOutputHandler outputHandler = new CallbackRevListOutputHandler(repository, commit -> {
            result.put(commit.getId(), commit);
            return true;
        });

        try {
            configureAndCall(commandBuilderFactory.builder(repository)
                    .revList()
                    .ignoreMissing(true)
                    .alternates(alternates)
                    .walk(GitRevListWalk.NO_WALK)
                    .inputHandler(RevListInputHandler.forRefs(commitIds, null))
                    .format(outputHandler.getFormat())
                    .build(outputHandler));
        } catch (ServiceException e) {
            log.warn("{}: Could not retrieve commit details", repository);
        }

        // also map any short commit IDs in the result
        for (String commitId : commitIds) {
            if (!result.containsKey(commitId)) {
                // look for a partial match
                for (String longId : result.keySet()) {
                    if (ShaUtils.hashesMatch(commitId, longId)) {
                        result.put(commitId, result.get(longId));
                        break;
                    }
                }
            }
        }

        return result;
    }

    private String getMergeBase(Set<String> commitIds, Set<Repository> alternates) {
        try {
            Set<String> mergeBases = configureAndCall(commandBuilderFactory.builder(repository).mergeBase()
                    .octopus()
                    .alternates(alternates)
                    .commits(commitIds)
                    .exitHandler(new MergeBaseExitHandler(i18nService, repository, commitIds) {
                        @Override
                        protected boolean isError(String command, int exitCode, String stdErr, Throwable thrown) {
                            // exit code 1 and no error message means that there is no common ancestor for the provided
                            // commits. Do not treat that as an error and just return null
                            return !(exitCode == 1 && StringUtils.isBlank(stdErr) && thrown == null) &&
                                    super.isError(command, exitCode, stdErr, thrown);
                        }
                    })
                    .build(new MergeBaseOutputHandler()));
            return mergeBases == null || mergeBases.isEmpty() ? null : mergeBases.iterator().next();
        } catch (GitNoCommonAncestorException e) {
            log.info("{} could not find the common ancestor for refs {}", repository, commitIds);
            return null;
        } catch (ServiceException e) {
            log.warn("{} could not determine merge base for rescopes", repository, e);
            return null;
        }
    }

    private class RescopeAnalysingCallback extends TraversalCallback {
        private final BulkPullRequestRescopeAnalyzer analyzer;

        private RescopeAnalysingCallback(BulkPullRequestRescopeAnalyzer analyzer) {
            this.analyzer = analyzer;
        }

        @Override
        public TraversalStatus onNode(@Nonnull CommitGraphNode node) {
            String[] parentIds = node.getParents().stream().map(MinimalCommit::getId).toArray(String[]::new);
            //Boundary commits will be prefixed with '-', however when the same hashes are listed as parent commits
            //there's no prefix. The '-' prefix must be stripped in order for the hashes to match
            boolean workLeft = analyzer.onCommit(StringUtils.removeStart(node.getCommit().getId(), "-"), parentIds);
            return workLeft ? TraversalStatus.CONTINUE : TraversalStatus.FINISH;
        }

        public void onEnd(@Nonnull TraversalSummary summary) {
            analyzer.close();
            SortedSet<String> commitIds = new TreeSet<>();
            analyzer.getResults().stream()
                    .filter(result -> result.getOutcome() == UPDATE)
                    .forEach(result -> {
                        commitIds.addAll(result.getAddedCommits());
                        commitIds.addAll(result.getRemovedCommits());
                    });

            Map<String, Commit> commits = getCommits(commitIds);
            for (PullRequestRescopeAnalysisResult result : analyzer.getResults()) {
                switch (result.getOutcome()) {
                    case DECLINE:
                        context.decline(result.getRescope());
                        break;
                    case MERGE:
                        context.merge(result.getRescope(), result.getMergeHash().orElse(null));
                        break;
                    case UPDATE:
                        context.update(result.getRescope(),
                                toDetails(result.getAddedCommits(), result.getAddedCommitsCount(), commits),
                                toDetails(result.getRemovedCommits(), result.getRemovedCommitsCount(), commits));
                        break;
                }
            }
        }

        private RescopeDetails toDetails(List<String> commitIds, int total, Map<String, Commit> commits) {
            if (total == 0) {
                return SimpleRescopeDetails.EMPTY;
            }
            SimpleRescopeDetails.Builder builder = new SimpleRescopeDetails.Builder().total(total);
            commitIds.stream().map(commits::get).forEach(builder::commit);
            return builder.build();
        }
    }
}
