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

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.GitAgent;
import com.atlassian.bitbucket.mesh.git.GitTimers;
import com.atlassian.bitbucket.mesh.git.GitUtils;
import com.atlassian.bitbucket.mesh.git.builder.GitProcess;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
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.*;
import com.atlassian.bitbucket.mesh.git.fetch.ObjectRefFetchStrategy;
import com.atlassian.bitbucket.mesh.git.fetch.RefFetchParameters;
import com.atlassian.bitbucket.mesh.git.fetch.TimedRefFetchStrategy;
import com.atlassian.bitbucket.mesh.git.porcelain.GitPorcelain;
import com.atlassian.bitbucket.mesh.git.porcelain.WorkTreeParameters;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.process.handler.LoggingStdioHandler;
import com.atlassian.bitbucket.mesh.repository.RefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.util.Person;
import com.atlassian.util.profiling.Ticker;
import io.grpc.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.atlassian.bitbucket.mesh.git.GitConstants.PATH_GIT;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcInvalidRebase.Reason.COMMITTER_EMAIL_BLANK;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcInvalidRebase.Reason.COMMITTER_NAME_BLANK;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcInvalidRefName.Reason.INVALID;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isBlank;

@Profiled
public class DefaultRebaseManager implements RebaseManager {

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

    private final GitAgent agent;
    private final GitProcessBuilderFactory builderFactory;
    private final EventPublisher eventPublisher;
    private final GitPorcelain porcelain;

    public DefaultRebaseManager(@Nonnull GitAgent agent, @Nonnull GitProcessBuilderFactory builderFactory,
                                @Nonnull EventPublisher eventPublisher, @Nonnull GitPorcelain porcelain) {
        this.agent = requireNonNull(agent, "agent");
        this.builderFactory = requireNonNull(builderFactory, "builderFactory");
        this.eventPublisher = requireNonNull(eventPublisher, "eventPublisher");
        this.porcelain = requireNonNull(porcelain, "porcelain");
    }

    @Nonnull
    @Override
    public String rebase(@Nonnull RebaseParameters parameters) {
        requireNonNull(parameters, "parameters");

        String commitId = doRebase(parameters, false);
        if (commitId == null) {
            throw new RebaseException("The rebase could not be completed automatically", Status.ABORTED);
        }
        return commitId;
    }

    @Override
    public void tryRebase(@Nonnull RebaseParameters parameters) {
        requireNonNull(parameters, "parameters");

        doRebase(parameters, true);
    }

    @Nonnull
    @Override
    public Branch updateRef(@Nonnull RebaseParameters parameters, @Nonnull String commitId) {
        requireNonNull(parameters, "parameters");
        requireNonNull(commitId, "commitId");

        Repository repository = parameters.getRepository();
        String branchId = parameters.getBranch().getId();

        //If the rebase wasn't canceled, update the target branch. This may fail if the target branch was updated
        //simultaneously by other processing, but that's no different than if we had fetched to it directly
        log.debug("{}: Updating target ref {} to {}", repository, branchId, commitId);
        TimedRefFetchStrategy.time(new ObjectRefFetchStrategy(agent, builderFactory))
                .updateRef(new RefFetchParameters.Builder(parameters).build(), commitId);
        eventPublisher.publish(new RefsUpdatedEvent(parameters.getRepository()));

        Branch rebased = agent.resolveBranch(repository, branchId, true);
        if (rebased == null) {
            log.debug("{}: Branch {} could not be resolved as absolute; trying as relative", repository, branchId);
            rebased = agent.resolveBranch(repository, branchId);
        }
        if (rebased == null) {
            log.error("{}: Branch {} could not be resolved", repository, branchId);
            throw new RebaseException("Failed to resolve branch");
        }
        return rebased;
    }

    private static void validate(RebaseParameters parameters) {
        Person committer = parameters.getCommitter();
        if (isBlank(committer.getName())) {
            throw new InvalidRebaseException(COMMITTER_NAME_BLANK);
        }
        if (isBlank(committer.getEmail())) {
            throw new InvalidRebaseException(COMMITTER_EMAIL_BLANK);
        }
        String upstream = parameters.getUpstream();
        if (!GitUtils.isHash(upstream)) {
            throw new InvalidRefNameException(INVALID, upstream);
        }
    }

    private String doFetch(RebaseParameters parameters, Path workTree) {
        //If we make it here, the rebase ran without conflicts and the target branch in the work tree has been
        //updated to reference the rebased commit. The last step, if we're doing this for real, is to fetch
        //those commits into the repository.
        log.debug("{}: Fetching successful rebase for {} from {}", parameters.getRepository(),
                parameters.getBranch(), workTree);
        return TimedRefFetchStrategy.time(new ObjectRefFetchStrategy(agent, builderFactory))
                .fetch(new RefFetchParameters.Builder(parameters).build(), workTree);
    }

    private String doRebase(RebaseParameters parameters, boolean dryRun) {
        validate(parameters);

        Path workTree = null;
        try (Ticker ignored = GitTimers.REBASE.start(parameters)) {
            WorkTreeParameters.Builder builder = new WorkTreeParameters.Builder(parameters);
            workTree = porcelain.workTree(builder.build());

            return doWithWorkTree(workTree, parameters, dryRun);
        } finally {
            if (workTree != null) {
                MoreFiles.deleteQuietly(workTree);
            }
        }
    }

    private String doWithWorkTree(Path workTree, RebaseParameters parameters, boolean dryRun) {
        try {
            performRebase(parameters, workTree);

            if (dryRun) {
                log.debug("{}: Successfully rebased {}. The dry run rebase will be discarded",
                        parameters.getRepository(), parameters);
                return null;
            }
            return doFetch(parameters, workTree);
        } catch (RuntimeException e) {
            if (e instanceof RebaseConflictedException) {
                ((RebaseConflictedException) e).setFromCommitId(parameters.getBranch().getLatestCommit());
                throw e;
            }

            if (e instanceof RebaseException) {
                throw e;
            }

            throw new RebaseException("The rebase could not be completed automatically", e);
        }
    }

    private void performRebase(RebaseParameters parameters, Path workTree) {
        String upstream = parameters.getUpstream();

        //Perform the rebase in the temporary work tree
        GitProcess<Void> process = builderFactory.builder().workDir(workTree).rebase()
                //The branch _does_ exist in the work tree, but it's set to the upstream commit, to prevent rebase
                //from immediately doing a checkout, so rebase using the branch's tip commit explicitly
                .branch(parameters.getBranch().getLatestCommit())
                .committer(parameters.getCommitter())
                .completionHandler(new RebaseCompletionHandler())
                .quiet(true) //Suppresses _all_ stdout; implies --no-stat
                .upstream(upstream)
                .verify(GitRebaseVerify.OFF)
                .workDir(workTree)
                .build(new LoggingStdioHandler());
        log.debug("{}: Rebasing {} in {}", parameters.getRepository(), parameters, workTree);
        parameters.configureAndRun(process);

        if (!parameters.isAllowEmpty()) {
            String rebased = agent.revParse(workTree.resolve(PATH_GIT), GitAgent.HEAD);
            if (upstream.equals(rebased)) {
                throw new EmptyRebaseException();
            }
        }
    }
}
