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

import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.merge.GitMergeBuilder;
import com.atlassian.bitbucket.mesh.git.builder.merge.GitMergeFastForward;
import com.atlassian.bitbucket.mesh.git.builder.merge.MergeStdioHandler;
import com.atlassian.bitbucket.mesh.git.builder.rebase.GitRebaseVerify;
import com.atlassian.bitbucket.mesh.git.builder.rebase.RebaseCompletionHandler;
import com.atlassian.bitbucket.mesh.git.exception.EmptyRebaseMergeException;
import com.atlassian.bitbucket.mesh.git.porcelain.WorkTreeParameters;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.process.handler.LoggingStdioHandler;

import javax.annotation.Nonnull;
import java.nio.file.Path;

import static com.atlassian.bitbucket.mesh.git.GitConstants.ENV_WORK_TREE;
import static com.atlassian.bitbucket.mesh.git.GitConstants.PATH_GIT;

/**
 * A merge strategy which uses {@code git rebase} to rewrite commits from the source branch onto the target.
 * <p>
 * Since {@code git rebase} is intended to update the <i>source</i> branch, rather than the target, both "branch"
 * and "upstream" are provided as commit IDs instead of ref names. When the command completes the work tree is in
 * a detached state, with the {@code HEAD} set to the tip of the newly-rebased commits (or to a (redundant) merge
 * for {@link GitMergeStrategy#REBASE_NO_FF REBASE_NO_FF}).
 */
public class RebaseMergeStrategy implements MergeStrategy {

    private final GitAgent agent;
    private final GitProcessBuilderFactory builderFactory;
    private final GitMergeFastForward fastForward;

    public RebaseMergeStrategy(GitAgent agent, GitProcessBuilderFactory builderFactory, GitMergeFastForward fastForward) {
        this.agent = agent;
        this.builderFactory = builderFactory;
        this.fastForward = fastForward;
    }

    /**
     * Specifies that a {@link WorkTreeParameters.Builder#pristine(boolean) pristine} work tree is required.
     *
     * @param builder the builder to customize
     * @return the provided builder, with the {@code pristine} flag enabled
     */
    @Nonnull
    @Override
    public WorkTreeParameters.Builder customizeWorkTree(@Nonnull WorkTreeParameters.Builder builder) {
        return builder.pristine(true);
    }

    @Override
    public void merge(@Nonnull MergeParameters parameters, @Nonnull Path workTree) {
        String upstream = parameters.getTo().getLatestCommit();

        parameters.configureAndRun(builderFactory.builder().rebase()
                .author(parameters.getAuthor()) //Set for completeness, but will not be used by git rebase
                //The from branch does not exist in the work tree, so rebase using its tip commit explicitly
                .branch(parameters.getFrom().getLatestCommit())
                .committer(parameters.getCommitter())
                .completionHandler(new RebaseCompletionHandler())
                .quiet(true) //Suppresses _all_ stdout; implies --no-stat
                //Rebase from commit at the tip of the target branch
                .upstream(upstream)
                .verify(GitRebaseVerify.OFF)
                .workDir(workTree)
                .build(new LoggingStdioHandler()));

        //git rebase silently drops empty patches. If all of the patches to be applied end up empty, HEAD is
        //left pointing at the same commit it started at
        String rebased = agent.revParse(workTree.resolve(PATH_GIT), GitAgent.HEAD);
        if (upstream.equals(rebased)) {
            throw new EmptyRebaseMergeException();
        }

        //For rebase-no-ff mode, after the rebase completes we need to reset back to the original upstream
        //commit so that we can then do a git merge --no-ff to create the (redundant) merge commit
        if (fastForward == GitMergeFastForward.OFF) {
            //Reset back to the toRef's tip commit to ensure we don't "flip" the parents. Note that this leaves
            //unstaged changes in the work tree, but git merge doesn't care; this is no different than the work
            //tree used by all of the non-rebase merge strategies
            parameters.configureAndRun(builderFactory.builder().reset()
                    .commit(upstream)
                    .quiet(true)
                    .workDir(workTree)
                    .build());

            //Reusing the rebase work tree can result in unexpected merge failures due to unstaged changes left
            //behind after resetting away from the rebased commits. Rather than trying to "clean" the existing
            //work tree, whether by running "git reset --hard", manually deleting files or some other approach,
            //we create a secondary "work tree" inside the .git directory and perform the merge there. This will
            //still write the merge's _objects_ in the existing work tree's objects directory, so the fetch will
            //work as normal, but there won't be any existing files to interfere with the merge
            Path mergeDir = MoreFiles.resolve(workTree, PATH_GIT, "bbs");
            MoreFiles.mkdir(mergeDir);

            //Perform a --no-ff merge to apply the rebased commits to the target branch. Since the rebased commits
            //are fast-forward to the target branch it should not be possible for this merge to have conflicts
            MergeStdioHandler mergeHandler = new MergeStdioHandler();
            GitMergeBuilder mergeBuilder = builderFactory.builder().merge()
                    .normal()
                    .author(parameters.getAuthor())
                    .committer(parameters.getCommitter())
                    .commit(rebased)
                    .ff(GitMergeFastForward.OFF)
                    .log(parameters.getLog())
                    .message(parameters.getMessage())
                    .withEnvironment(ENV_WORK_TREE, mergeDir.toAbsolutePath().toString())
                    .workDir(workTree);
            parameters.configureAndRun(mergeHandler.build(mergeBuilder));
        }
    }
}