package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.AbstractStatusException;
import com.atlassian.bitbucket.mesh.git.builder.GitQuarantineEnvironment;
import com.atlassian.bitbucket.mesh.git.ref.RefUpdate;
import com.atlassian.bitbucket.mesh.git.worktree.PublishWorkTreeParameters;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.WorkTree;
import com.atlassian.bitbucket.mesh.repository.WorkTreeManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.util.Person;
import com.google.common.collect.ImmutableMap;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;

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

import static com.atlassian.bitbucket.mesh.git.GitConstants.*;
import static java.util.Objects.requireNonNull;

public class PublishWorkTreeRequestObserver implements StreamObserver<RpcPublishWorkTreeFragment> {

    private final StreamObserver<RpcPublishWorkTreeResponseFragment> responseObserver;
    private final WorkTreeManager workTreeManager;

    private volatile boolean completed;
    private PublishWorkTreeParameters parameters;
    private RefUpdate refUpdate;
    private GitQuarantineRegistry quarantineRegistry;
    private WorkTree workTree;

    public PublishWorkTreeRequestObserver(@Nonnull StreamObserver<RpcPublishWorkTreeResponseFragment> responseObserver,
                                          @Nonnull GitQuarantineRegistry quarantineRegistry,
                                          @Nonnull WorkTreeManager workTreeManager) {
        this.workTreeManager = requireNonNull(workTreeManager, "workTreeManager");
        this.quarantineRegistry = requireNonNull(quarantineRegistry, "quarantineRegistry");
        this.responseObserver = requireNonNull(responseObserver, "responseObserver");
    }

    @Override
    public void onCompleted() {
        maybeComplete();
    }

    @Override
    public void onError(Throwable throwable) {
        //If we get here from our own onError callback in onNext below, this won't do anything. But if
        //we get here because the client canceled the request, or had some other failure, then we need
        //to complete the server-side observer to prevent the RPC from lingering until its deadline
        maybeComplete();
    }

    @Override
    public void onNext(RpcPublishWorkTreeFragment fragment) {
        try {
            switch (fragment.getRequestOneofCase()) {
                case REQUEST:
                    handleRequest(fragment.getRequest());
                    break;
                case UPDATE_REF:
                    handleUpdateRef();
                    break;
                default:
                    throw Status.UNIMPLEMENTED
                            .withDescription("[" + fragment.getRequestOneofCase() + "] is not supported")
                            .asRuntimeException();
            }
        } catch (RuntimeException e) {
            if (e instanceof AbstractStatusException) {
                e = ((AbstractStatusException) e).toStatusException();
            }
            completed = true;
            responseObserver.onError(e);
        }
    }

    private PublishWorkTreeParameters buildParameters(RpcPublishWorkTreeRequest request) {
        PublishWorkTreeParameters.Builder builder = new PublishWorkTreeParameters.Builder()
                .branch(request.getBranch().toStringUtf8())
                .timeouts(request.getTimeouts());
        if (request.hasAuthor()) {
            builder.author(new Person(request.getAuthor()));
        }
        if (request.hasExpectedHash()) {
            builder.expectedHash(request.getExpectedHash());
        }
        return builder.build();
    }

    private void handleRequest(RpcPublishWorkTreeRequest request) {
        if (refUpdate != null) {
            throw Status.INVALID_ARGUMENT
                    .withDescription("Only a single PublishWorkTreeRequest may be sent")
                    .asRuntimeException();
        }
        this.parameters = buildParameters(request);
        this.workTree = workTreeManager.getById(request.getWorkTree());
        refUpdate = workTreeManager.publish(workTree, parameters);

        String quarantineId = registerQuarantineEnvironment(workTree.getRepository());
        responseObserver.onNext(RpcPublishWorkTreeResponseFragment.newBuilder().setRunHooks(
                        RpcRunPublishWorkTreeHooks.newBuilder()
                                .setRefUpdate(refUpdate.toRefUpdate())
                                .setRepositoryId(quarantineId))
                .build());
    }

    private void handleUpdateRef() {
        if (refUpdate == null) {
            throw Status.INVALID_ARGUMENT
                    .withDescription("A PublishWorkTreeRequest must be sent before updating ref")
                    .asRuntimeException();
        }

        workTreeManager.updateRef(workTree, parameters, refUpdate);
        responseObserver.onNext(RpcPublishWorkTreeResponseFragment.newBuilder()
                .setResponse(RpcPublishWorkTreeResponse.newBuilder()
                        .setRefUpdate(refUpdate.toRefUpdate())).build());
    }

    private void maybeComplete() {
        if (!completed) {
            responseObserver.onCompleted();
            completed = true;
        }
    }

    @Nonnull
    private String registerQuarantineEnvironment(Repository repository) {
        Path workTreeGitDir = workTree.getPath().resolve(".git");
        Map<String, String> environment = ImmutableMap.of(
                ENV_ALTERNATE_OBJECT_DIRS, workTree.getRepository().getPath().toString(),
                ENV_QUARANTINE_PATH, workTreeGitDir.resolve("objects").toString(),
                ENV_OBJECT_DIR, workTreeGitDir.resolve("objects").toString());
        return GitQuarantineEnvironment.fromEnvironment(repository, environment)
                .map(quarantineRegistry::register)
                .orElse(repository.getId());
    }
}
