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

import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.pull.AbstractPullRequestActivityVisitor;
import com.atlassian.bitbucket.pull.PullRequestRescopeActivity;
import com.atlassian.bitbucket.pull.RescopeDetails;
import com.atlassian.bitbucket.pull.SimpleRescopeDetails;
import com.atlassian.bitbucket.scm.CommitsCommandParameters;
import com.atlassian.bitbucket.scm.ScmService;
import com.atlassian.bitbucket.util.Timer;
import com.atlassian.bitbucket.util.TimerUtils;
import com.atlassian.stash.internal.commit.InternalCommitEnricher;
import com.atlassian.stash.internal.pull.*;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

/**
 * Populates {@link RescopeDetails details} for {@link PullRequestRescopeActivity rescope
 * activities}, determining which commits have been added to or removed from the pull request, if any.
 * <p>
 * It's possible (likely, in fact) that a rescope has no added or removed any commits. In such cases,
 * details are still added to the activity but indicate a {@link RescopeDetails#getTotal() total} of
 * {@code 0} {@link RescopeDetails#getCommits() commits} were added. The commit list in such cases is
 * always empty, never {@code null}.
 *
 * @since 1.3
 */
@Component
public class RescopePullRequestActivityEnricher implements PullRequestActivityEnricher {

    private final InternalCommitEnricher commitEnricher;
    private final RescopeProcessor rescopeProcessor;
    private final ScmService scmService;

    @Autowired
    public RescopePullRequestActivityEnricher(InternalCommitEnricher commitEnricher, RescopeProcessor rescopeProcessor,
                                              ScmService scmService) {
        this.commitEnricher = commitEnricher;
        this.rescopeProcessor = rescopeProcessor;
        this.scmService = scmService;
    }

    @Override
    public void enrich(@Nonnull InternalPullRequest pullRequest, @Nonnull Iterable<InternalPullRequestActivity> activities) {
        String timerName = "Enrich: Rescopes from " + pullRequest.getGlobalId() + " (" + Iterables.size(activities) + ")";
        try (Timer ignored = TimerUtils.start(timerName)) {
            new ActivityEnricher(pullRequest).enrich(activities);
        }
    }

    /**
     * Populates {@link InternalPullRequestRescopeActivity#getAdded() added} and
     * {@link InternalPullRequestRescopeActivity#getRemoved() removed} commits
     * for {@link InternalPullRequestRescopeActivity rescope activities}.
     * <ul>
     *     <li>If IDs for the added and removed commits for a given activity have been
     *     {@link InternalPullRequestRescopeActivity#isPrecalculated() precalculated},
     *     the full {@link Commit} is loaded from the SCM and used to populate the added
     *     and removed {@link RescopeDetails details} for the activity</li>
     *     <li>Otherwise, the {@link RescopeProcessor} is used to both calculate the
     *     added and removed commits and apply them to the activity</li>
     * </ul>
     *
     * @since 4.6
     */
    private class ActivityEnricher extends AbstractPullRequestActivityVisitor {

        private final Set<String> commitIds;
        private final List<InternalPullRequestRescopeActivity> pendingRescopes;
        private final List<InternalPullRequestRescopeActivity> precalculatedRescopes;
        private final InternalPullRequest pullRequest;

        /**
         * Constructs a new {@code ActivityEnricher} for examining and enriching
         * {@link InternalPullRequestRescopeActivity rescope activities}.
         */
        private ActivityEnricher(InternalPullRequest pullRequest) {
            this.pullRequest = pullRequest;

            commitIds = new HashSet<>();
            pendingRescopes = new ArrayList<>();
            precalculatedRescopes = new ArrayList<>();
        }

        public void enrich(Iterable<InternalPullRequestActivity> activities) {
            activities.forEach(activity -> activity.accept(this));

            rescopeProcessor.processAll(pullRequest, pendingRescopes);
            applyPrecalculatedCommits();
        }

        /**
         * Examines the provided {@link PullRequestRescopeActivity rescope activity}, .
         *
         * @param activity the comment activity
         */
        @Override
        public void visit(@Nonnull PullRequestRescopeActivity activity) {
            InternalPullRequestRescopeActivity rescopeActivity = (InternalPullRequestRescopeActivity) activity;

            if (rescopeActivity.isPrecalculated()) {
                rescopeActivity.getCommits().stream()
                        .map(InternalPullRequestRescopeCommit::getCommitId)
                        .forEach(commitIds::add);
                precalculatedRescopes.add(rescopeActivity);
            } else {
                pendingRescopes.add(rescopeActivity);
            }
        }

        /**
         * {@link #loadCommits() Loads commits} and applies {@link RescopeDetails} for added and removed commits
         * on {@link #precalculatedRescopes precalculated rescopes}.
         */
        private void applyPrecalculatedCommits() {
            if (precalculatedRescopes.isEmpty()) {
                return;
            }

            //Map out the full commit details by their hash to simplify constructing details later
            Map<String, Commit> commitsById = Maps.uniqueIndex(loadCommits(), Commit::getId);

            for (InternalPullRequestRescopeActivity activity : precalculatedRescopes) {
                SimpleRescopeDetails.Builder added = new SimpleRescopeDetails.Builder()
                        .total(activity.getTotalAdded());
                SimpleRescopeDetails.Builder removed = new SimpleRescopeDetails.Builder()
                        .total(activity.getTotalRemoved());
                for (InternalPullRequestRescopeCommit rescopeCommit : activity.getCommits()) {
                    Commit commit = commitsById.get(rescopeCommit.getCommitId());
                    if (commit == null) {
                        //This could happen if (for some reason) the commit was garbage collected after it was calculated
                        //to be part of the rescope and stored in the database. Just omit it from display; don't fail
                        continue;
                    }

                    if (rescopeCommit.getAction() == PullRequestRescopeCommitAction.ADDED) {
                        added.commit(commit);
                    } else {
                        removed.commit(commit);
                    }
                }

                activity.setAdded(added.build());
                activity.setRemoved(removed.build());
            }
        }

        /**
         * Retrieves the specified commits from the {@link ScmService SCM} and enriches them. Enriching the
         * commits maps authors to users, which allows the commit list to link to their profiles.
         *
         * @return enriched commits for the provided IDs
         */
        @SuppressWarnings("deprecation")
        private Iterable<Commit> loadCommits() {
            if (commitIds.isEmpty()) {
                return Collections.emptyList();
            }

            List<Commit> commits = new ArrayList<>(commitIds.size());
            scmService.getCommandFactory(pullRequest.getScopeRepository()).commits(
                    new CommitsCommandParameters.Builder()
                            .ignoreMissing(true)
                            .include(commitIds)
                            .secondaryRepository(pullRequest.getFromRef().getRepository())
                            .traverse(false) //Don't traverse ancestry; just return the requested commits
                            .build(),
                    commit -> {
                            commits.add(commit);
                            return true;
                    }).call();

            //Enrich the commits, applying various additional pieces of (hopefully) useful data
            return commitEnricher.enrichAll(pullRequest.getScopeRepository(), commits, null);
        }
    }
}