package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.config.GrpcConfig;
import com.atlassian.bitbucket.mesh.config.HooksConfig;
import com.atlassian.bitbucket.mesh.config.HostingConfig;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderSupport;
import com.atlassian.bitbucket.mesh.git.builder.receivepack.GitReceivePackBuilder;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.exception.InvalidArgumentException;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackListener;
import com.atlassian.bitbucket.mesh.management.SidebandManager;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryManager;
import com.atlassian.bitbucket.mesh.request.RequestManager;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.atlassian.bitbucket.mesh.transaction.UpdateRefsContextHolder;
import io.grpc.stub.StreamObserver;

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

import static com.atlassian.bitbucket.mesh.git.BidirectionalFragmentRequestObserver.requireCase;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcHostingOperationType.*;

public class GrpcHostingService extends HostingServiceGrpc.HostingServiceImplBase {

    private final GitProcessBuilderFactory builderFactory;
    private final EventPublisher eventPublisher;
    private final GrpcConfig grpcConfig;
    private final GitHookService hookService;
    private final HooksConfig hooksConfig;
    private final HostingConfig hostingConfig;
    private final PackCacheService packCacheService;
    private final GrpcPushTracer pushTracer;
    private final GitQuarantineRegistry quarantineRegistry;
    private final List<GitReceivePackListener> receivePackListeners;
    private final RepositoryManager repositoryManager;
    private final RequestManager requestManager;
    private final SidebandManager sidebandManager;
    private final UpdateRefsContextHolder updateRefsContextHolder;

    public GrpcHostingService(GitProcessBuilderFactory builderFactory, ApplicationConfig config,
                              EventPublisher eventPublisher, GitHookService hookService,
                              PackCacheService packCacheService, GrpcPushTracer pushTracer,
                              GitQuarantineRegistry quarantineRegistry,
                              List<GitReceivePackListener> receivePackListeners, RepositoryManager repositoryManager,
                              RequestManager requestManager, SidebandManager sidebandManager,
                              UpdateRefsContextHolder updateRefsContextHolder) {
        this.builderFactory = builderFactory;
        this.eventPublisher = eventPublisher;
        this.hookService = hookService;
        this.packCacheService = packCacheService;
        this.pushTracer = pushTracer;
        this.quarantineRegistry = quarantineRegistry;
        this.receivePackListeners = receivePackListeners;
        this.repositoryManager = repositoryManager;
        this.requestManager = requestManager;
        this.sidebandManager = sidebandManager;
        this.updateRefsContextHolder = updateRefsContextHolder;

        grpcConfig = config.getGrpc();
        hooksConfig = config.getGit().getHooks();
        hostingConfig = config.getGit().getHosting();
    }

    @Nonnull
    @Override
    public StreamObserver<RpcHttpBackendFragment> httpBackend(
            StreamObserver<RpcHostingWriteStreamFragment> responseObserver) {
        return new BidirectionalFragmentRequestObserver<>(responseObserver,
                new HttpBackendFragmentHandler(builderFactory, eventPublisher, hookService,
                        hooksConfig.getCallbackTimeout(), hostingConfig, grpcConfig.getMaxFragmentSize(),
                        responseObserver, packCacheService, pushTracer, quarantineRegistry, receivePackListeners,
                        repositoryManager, requestManager, sidebandManager, updateRefsContextHolder));
    }

    @Nonnull
    @Override
    public StreamObserver<RpcReceivePackFragment> receivePack(
            StreamObserver<RpcHostingWriteStreamFragment> responseObserver) {
        return new BidirectionalFragmentRequestObserver<>(responseObserver,
                new AbstractHostingWriteFragmentHandler<RpcReceivePackFragment>(HOSTING_OP_PUSH, eventPublisher,
                        hookService, hooksConfig.getCallbackTimeout(), grpcConfig.getMaxFragmentSize(),
                        responseObserver, pushTracer, quarantineRegistry, receivePackListeners, requestManager,
                        sidebandManager, updateRefsContextHolder) {

                    @Nonnull
                    @Override
                    public GitProcessBuilderSupport<?> createBuilder(@Nonnull RpcReceivePackFragment fragment) {
                        requireCase(fragment.getRequestOneofCase(), RpcReceivePackFragment.RequestOneofCase.REQUEST);

                        RpcReceivePackRequest request = fragment.getRequest();
                        Repository repository = repositoryManager.getById(request.getRepository());

                        //The repository's directory will be determined as part of creating the builder,
                        //and passed to receive-pack; we don't need to set it manually here
                        GitReceivePackBuilder builder = builderFactory.builder(request).receivePack()
                                .advertiseRefs(request.getAdvertiseRefs())
                                .statelessRpc(request.getStatelessRpc());

                        invokeReceivePackListeners(repository);
                        return registerHookRequest(repository)
                                .configure(builder);
                    }

                    @Override
                    public RpcStreamChunk onFragment(@Nonnull RpcReceivePackFragment fragment) {
                        switch (fragment.getRequestOneofCase()) {
                            case STDIN:
                                return onStdin(fragment.getStdin());
                            case POST_RECEIVE:
                                onPostReceive(fragment.getPostReceive());
                                break;
                            case PRE_RECEIVE:
                                onPreReceive(fragment.getPreReceive());
                                break;
                            default:
                                throw new InvalidArgumentException("Received [" + fragment.getRequestOneofCase() +
                                        "] where [STDIN, POST_RECEIVE or PRE_RECEIVE] was expected");
                        }
                        return null;
                    }

                    @Nonnull
                    @Override
                    protected RpcHostingOperationType getOperationType() {
                        return isUpdate() ? HOSTING_OP_PUSH : HOSTING_OP_REF_ADVERTISEMENT;
                    }
                }
        );
    }

    @Nonnull
    @Override
    public StreamObserver<RpcUploadArchiveFragment> uploadArchive(
            StreamObserver<RpcHostingReadStreamFragment> responseObserver) {
        return new BidirectionalFragmentRequestObserver<>(responseObserver,
                new AbstractHostingReadFragmentHandler<RpcUploadArchiveFragment>(HOSTING_OP_ARCHIVE) {

                    @Nonnull
                    @Override
                    public GitProcessBuilderSupport<?> createBuilder(@Nonnull RpcUploadArchiveFragment fragment) {
                        requireCase(fragment.getRequestOneofCase(),
                                RpcUploadArchiveFragment.RequestOneofCase.REQUEST);

                        //The repository's directory will be determined as part of creating the builder,
                        //and passed to upload-archive; we don't need to set it manually here
                        return builderFactory.builder(fragment.getRequest()).uploadArchive();
                    }

                    @Nonnull
                    @Override
                    public RpcStreamChunk onFragment(@Nonnull RpcUploadArchiveFragment fragment) {
                        requireCase(fragment.getRequestOneofCase(),
                                RpcUploadArchiveFragment.RequestOneofCase.STDIN);

                        return fragment.getStdin();
                    }
                });
    }

    @Nonnull
    @Override
    public StreamObserver<RpcUploadPackFragment> uploadPack(
            StreamObserver<RpcHostingReadStreamFragment> responseObserver) {
        return new BidirectionalFragmentRequestObserver<>(responseObserver,
                new UploadPackFragmentHandler(builderFactory, hookService, hostingConfig,
                        packCacheService, repositoryManager));
    }
}
