package com.atlassian.bitbucket.mesh.git.pull;

import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.revlist.*;
import com.atlassian.bitbucket.mesh.git.commit.*;
import com.atlassian.bitbucket.mesh.git.exception.AbstractGitException;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.util.ShaUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

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

public class BatchRescopeOperation {

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

    private final Set<Repository> alternates;
    private final GitProcessBuilderFactory builderFactory;
    private final PullRequestRescopeCallback callback;
    private final int maxMessageLength;
    private final BatchRescopePullRequestsParameters parameters;

    private Repository repository;

    public BatchRescopeOperation(@Nonnull GitProcessBuilderFactory builderFactory,
                                 @Nonnull PullRequestRescopeCallback callback,
                                 @Nonnull BatchRescopePullRequestsParameters parameters, int maxMessageLength) {
        this.builderFactory = builderFactory;
        this.callback = callback;
        this.maxMessageLength = maxMessageLength;
        this.parameters = parameters;

        alternates = new HashSet<>();
    }

    public void call() {
        if (parameters.getRescopes().isEmpty()) {
            return;
        }

        // Create the rev-list callback that will analyze all PullRequestRescope instances available on the context.
        BatchPullRequestRescopeAnalyzer.Builder analyzerBuilder = new BatchPullRequestRescopeAnalyzer.Builder(
                parameters.getMaxCommitsPerUpdate());
        Set<String> hashes = new HashSet<>();
        for (PullRequestRescope rescope : parameters.getRescopes()) {
            analyzerBuilder.rescope(rescope);

            alternates.add(rescope.getFromRepository());
            alternates.add(rescope.getToRepository());
            hashes.add(rescope.getOldFromCommit());
            hashes.add(rescope.getOldToCommit());
            hashes.add(rescope.getNewFromCommit());
            hashes.add(rescope.getNewToCommit());
        }
        repository = parameters.getRescopes().get(0).getToRepository();
        alternates.remove(repository);
        hashes.remove(null); // in case one of the newToHashes was  null

        RescopeAnalyzingCallback analysingCallback = new RescopeAnalyzingCallback(analyzerBuilder.build());
        try {
            if (hashes.isEmpty()) {
                // nothing to do
                return;
            }

            // include boundary commits to ensure the merge-base is included. The callback may need the merge-base
            // while doing analysis
            TraversalRevListStdioHandler stdioHandler = new TraversalRevListStdioHandler(hashes.stream(), analysingCallback);
            parameters.configureAndRun(builderFactory.builder(repository).revList()
                    .alternates(alternates)
                    .completionHandler(new RevCoreCompletionHandler(hashes, true))
                    .ignoreMissing(true)
                    .order(GitRevListOrder.DATE)
                    .parents(true)
                    .stdin(true)
                    .build(stdioHandler));
        } finally {
            analysingCallback.onEnd();
        }
    }

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

        Map<String, Commit> result = new HashMap<>();
        CommitCallback callback = (commit, renameChange) -> {
            result.put(commit.getId(), commit);
            return true;
        };
        CallbackRevListStdioHandler stdioHandler = new CallbackRevListStdioHandler(
                callback, maxMessageLength != 0, maxMessageLength, Collections.emptyList(), commitIds.stream());

        try {
            parameters.configureAndRun(builderFactory.builder(repository).revList()
                    .alternates(alternates)
                    .completionHandler(new RevCoreCompletionHandler(commitIds, true))
                    .format(stdioHandler.getFormat())
                    .ignoreMissing(true)
                    .stdin(true)
                    .walk(GitRevListWalk.NO_WALK)
                    .build(stdioHandler));
        } catch (AbstractGitException e) {
            log.warn("{}: Could not retrieve commit details", repository, e);
        }

        // 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 class RescopeAnalyzingCallback implements TraversalCallback {

        private final BatchPullRequestRescopeAnalyzer analyzer;

        private int commitCount;

        private RescopeAnalyzingCallback(BatchPullRequestRescopeAnalyzer analyzer) {
            this.analyzer = analyzer;
        }

        public void onEnd() {
            log.debug("[{}] Rescope rev-list traversed {} commits", repository, commitCount);
            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 = loadCommits(commitIds);
            for (PullRequestRescopeAnalysisResult result : analyzer.getResults()) {
                switch (result.getOutcome()) {
                    case DECLINE:
                        callback.onDeclined(result.getRescope());
                        break;
                    case MERGE:
                        callback.onMerged(result.getRescope(), result.getMergeCommit().orElse(null));
                        break;
                    case UPDATE:
                        callback.onUpdated(result.getRescope(),
                                getCommits(result.getAddedCommits(), commits), result.getAddedCommitsCount(),
                                getCommits(result.getRemovedCommits(), commits), result.getRemovedCommitsCount());
                        break;
                    default:
                        callback.onUnknown(result.getRescope());
                        break;
                }
            }
        }

        @Nonnull
        @Override
        public TraversalStatus onNode(@Nonnull CommitGraphNode node) {
            //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.getId(), "-"), node.getParentIds());
            ++commitCount;
            return workLeft ? TraversalStatus.CONTINUE : TraversalStatus.FINISH;
        }

        private List<Commit> getCommits(List<String> commitIds, Map<String, Commit> commits) {
            return commitIds.stream().map(commits::get).filter(Objects::nonNull).collect(Collectors.toList());
        }
    }
}
