package com.atlassian.bitbucket.mesh.grpc;

import com.atlassian.bitbucket.mesh.MeshCoreWiring;
import com.atlassian.bitbucket.mesh.auth.*;
import com.atlassian.bitbucket.mesh.capability.GrpcCapabilityService;
import com.atlassian.bitbucket.mesh.config.ApplicationConfig;
import com.atlassian.bitbucket.mesh.coordination.LogicalClock;
import com.atlassian.bitbucket.mesh.coordination.WriteTokenRegistry;
import com.atlassian.bitbucket.mesh.credentials.CredentialsHelper;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.execution.DefaultGrpcAsyncExecutor;
import com.atlassian.bitbucket.mesh.execution.ExecutionManager;
import com.atlassian.bitbucket.mesh.execution.GrpcAsyncExecutor;
import com.atlassian.bitbucket.mesh.git.*;
import com.atlassian.bitbucket.mesh.git.batch.BatchManager;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.commit.CommitManager;
import com.atlassian.bitbucket.mesh.git.content.ContentManager;
import com.atlassian.bitbucket.mesh.git.fetch.FetchManager;
import com.atlassian.bitbucket.mesh.git.file.EditFileManager;
import com.atlassian.bitbucket.mesh.git.gc.GarbageCollectionManager;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hook.script.HookScriptManager;
import com.atlassian.bitbucket.mesh.git.hosting.GitReceivePackListener;
import com.atlassian.bitbucket.mesh.git.merge.MergeManager;
import com.atlassian.bitbucket.mesh.git.porcelain.GitPorcelain;
import com.atlassian.bitbucket.mesh.git.pull.PullRequestManager;
import com.atlassian.bitbucket.mesh.git.push.PushManager;
import com.atlassian.bitbucket.mesh.git.rebase.RebaseManager;
import com.atlassian.bitbucket.mesh.git.ref.RefManager;
import com.atlassian.bitbucket.mesh.git.revert.RevertManager;
import com.atlassian.bitbucket.mesh.git.transcode.TranscodeService;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.bitbucket.mesh.management.GrpcManagementService;
import com.atlassian.bitbucket.mesh.management.NodeManager;
import com.atlassian.bitbucket.mesh.management.SidebandManager;
import com.atlassian.bitbucket.mesh.metrics.GrpcMetrics;
import com.atlassian.bitbucket.mesh.metrics.MetricsEnabledCondition;
import com.atlassian.bitbucket.mesh.network.MeshManager;
import com.atlassian.bitbucket.mesh.repair.InteractiveRepairHelper;
import com.atlassian.bitbucket.mesh.repair.RepairManager;
import com.atlassian.bitbucket.mesh.repository.*;
import com.atlassian.bitbucket.mesh.request.RequestManager;
import com.atlassian.bitbucket.mesh.request.RequestServerInterceptor;
import com.atlassian.bitbucket.mesh.spring.AuthorisationAspect;
import com.atlassian.bitbucket.mesh.throttle.ThrottleService;
import com.atlassian.bitbucket.mesh.transaction.DefaultRpcTransactionExecutor;
import com.atlassian.bitbucket.mesh.transaction.RpcTransactionExecutor;
import com.atlassian.bitbucket.mesh.transaction.TransactionManager;
import com.atlassian.bitbucket.mesh.transaction.UpdateRefsContextHolder;
import com.atlassian.bitbucket.mesh.util.ConcurrencyUtils;
import io.grpc.BindableService;
import io.grpc.ServerInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.TaskScheduler;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;

import static com.atlassian.bitbucket.mesh.MeshConstants.EXECUTOR_RPC_ASYNC;

@ConditionalOnProperty(name = "grpc.export", havingValue = "true", matchIfMissing = true)
@Configuration
@Import(MeshCoreWiring.class)
public class MeshGrpcWiring {

    //These constants are grouped by type (client and server) and ordered by their _values_, not their names, so
    //they're in the order their associated interceptors will be run in.
    //Note: Regardless of type (client or server), interception has 2 phases:
    //- *Interceptor.interceptCall and *Call.start are called in the order of these constants
    //- For interceptors that wrap the *Call.Listener, those callbacks run in the _opposite order_
    //  of these constants
    //When making modifications to this order, ensure you've considered both phases.

    //ClientInterceptors
    private static final int ORDER_CI_AUTHENTICATION = 3; //Include authentication token in headers
    private static final int ORDER_CI_METADATA = 2;       //Include other metadata (node, request) in headers
    private static final int ORDER_CI_ERROR_HANDLER = 1;  //Handle common errors (has ClientCall.Listener)

    //ServerInterceptors
    private static final int ORDER_SI_LOGGING = 8;        //Log any UNKNOWN errors
    private static final int ORDER_SI_EXEC_CONTEXT = 8;   //Prepare the ExecutionContext before anything
    private static final int ORDER_SI_AUTHENTICATION = 7; //Authenticate before processing the request
    private static final int ORDER_SI_ZERO_COPY = 6;      //Override the request marshallers
    private static final int ORDER_SI_REMOTE_REQUEST = 5; //Bind RequestContext to ExecutionContext
    private static final int ORDER_SI_CONFIGURED = 4;     //Block RPCs until the node is configured (Optional)
    private static final int ORDER_SI_WRONG_TARGET = 3;   //Block RPCs intended for another node (stale DNS) (Optional)
    private static final int ORDER_SI_METRICS = 2;        //Track metrics
    private static final int ORDER_SI_THROTTLING = 1;     //Apply throttling (so delays are included in metrics)

    @Bean
    public RpcArgumentDecoder argumentDecoder(RepositoryManager repositoryManager) {
        return new DefaultRpcArgumentDecoder(repositoryManager);
    }

    @Bean
    public AuthorisationAspect authorisationAspect(AuthorisationManager authorisationManager) {
        return new AuthorisationAspect(authorisationManager);
    }

    @Bean
    public AuthorisationManager authorisationManager(Optional<AuthenticationManager> authenticationManager) {
        return new DefaultAuthorisationManager(authenticationManager.orElse(null));
    }

    @Bean
    public GrpcCapabilityService capabilityService() {
        return new GrpcCapabilityService();
    }

    @Bean
    @Order(ORDER_CI_ERROR_HANDLER)
    public ErrorHandlingClientInterceptor errorHandlingClientInterceptor() {
        return new ErrorHandlingClientInterceptor();
    }

    @Bean
    @Order(ORDER_SI_EXEC_CONTEXT)
    public ExecutionContextServerInterceptor executionContextServerInterceptor(ExecutionManager executionManager) {
        return new ExecutionContextServerInterceptor(executionManager);
    }

    @Bean
    public GrpcBatchService gitBatchService(BatchManager batchManager, GrpcAsyncExecutor executor,
                                            RepositoryManager repositoryManager) {
        return new GrpcBatchService(batchManager, executor, repositoryManager);
    }

    @Bean
    public GrpcContentService gitContentService(ContentManager contentManager, GrpcAsyncExecutor executor,
                                                RepositoryManager repositoryManager) {
        return new GrpcContentService(contentManager, executor, repositoryManager);
    }

    @Bean
    public GrpcCommitService gitCommitService(GitAgent agent, CommitManager commitManager,
                                              EditFileManager editFileManager, GrpcAsyncExecutor executor,
                                              MergeManager mergeManager, RebaseManager rebaseManager,
                                              RefManager refManager, RepositoryManager repositoryManager,
                                              RevertManager revertManager, StorageLayout storageLayout) {
        return new GrpcCommitService(agent, commitManager, editFileManager, executor, mergeManager,
                rebaseManager, refManager, repositoryManager, revertManager, storageLayout);
    }

    @Bean
    public GrpcGitDataService gitDataService(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                             EventPublisher eventPublisher, ExecutionManager executionManager,
                                             RepositoryManager repositoryManager, UploadManager uploadManager) {
        return new GrpcGitDataService(agent, builderFactory, eventPublisher, executionManager,
                repositoryManager, uploadManager);
    }

    @Bean
    public GrpcHookScriptService gitHookScriptService(HookScriptManager hookScriptManager,
                                                      RepositoryManager repositoryManager) {
        return new GrpcHookScriptService(hookScriptManager, repositoryManager);
    }

    @Bean
    public GrpcHostingService gitHostingService(GitProcessBuilderFactory builderFactory,
                                                ApplicationConfig config,
                                                EventPublisher eventPublisher, GitHookService hookService,
                                                PackCacheService packCacheService, GrpcPushTracer pushTracer,
                                                GitQuarantineRegistry quarantineRegistry,
                                                List<GitReceivePackListener> receivePackListeners,
                                                RepositoryManager repositoryManager,
                                                RequestManager requestManager,
                                                SidebandManager sidebandManager,
                                                UpdateRefsContextHolder updateRefsHelper) {
        return new GrpcHostingService(builderFactory, config, eventPublisher, hookService, packCacheService,
                pushTracer, quarantineRegistry, receivePackListeners, repositoryManager, requestManager,
                sidebandManager, updateRefsHelper);
    }

    @Bean
    public GrpcPackCacheService gitPackCacheService(PackCacheService packCacheService,
                                                    RepositoryManager repositoryManager) {
        return new GrpcPackCacheService(packCacheService, repositoryManager);
    }

    @Bean
    public GrpcPullRequestService gitPullRequestService(GitProcessBuilderFactory builderFactory,
                                                        CommitManager commitManager, ApplicationConfig config,
                                                        GrpcAsyncExecutor executor, MergeManager mergeManager,
                                                        PullRequestManager pullRequestManager,
                                                        RepositoryManager repositoryManager) {
        return new GrpcPullRequestService(builderFactory, commitManager, config.getGrpc(), executor, mergeManager,
                pullRequestManager, repositoryManager);
    }

    @Bean
    public GrpcPlumbingService gitPlumbingService(RpcArgumentDecoder argumentDecoder,
                                                  GitProcessBuilderFactory builderFactory, ApplicationConfig config,
                                                  EventPublisher eventPublisher, ExecutionManager executionManager,
                                                  RepositoryManager repositoryManager) {
        return new GrpcPlumbingService(argumentDecoder, builderFactory, eventPublisher, executionManager,
                config.getProcess(), repositoryManager);
    }

    @Bean
    public GrpcPorcelainService gitPorcelainService(GitProcessBuilderFactory builderFactory, ApplicationConfig config) {
        return new GrpcPorcelainService(builderFactory, config.getProcess());
    }

    @Bean
    public GrpcRefService gitRefService(GrpcAsyncExecutor executor, RefManager refManager,
                                        RepositoryManager repositoryManager) {
        return new GrpcRefService(executor, refManager, repositoryManager);
    }

    @Bean
    public GrpcRepositoryService gitRepositoryService(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                                      ApplicationConfig config, CredentialsHelper credentialsHelper,
                                                      EventPublisher eventPublisher, GrpcAsyncExecutor executor,
                                                      ExecutorService executorService, FetchManager fetchManager,
                                                      GarbageCollectionManager garbageCollectionManager,
                                                      LogicalClock logicalClock, GitPorcelain porcelain,
                                                      PullRequestManager pullRequestManager, PushManager pushManager,
                                                      InteractiveRepairHelper repairHelper,
                                                      ReplicaStateRegistry replicaStateRegistry,
                                                      RepositoryHashService repositoryHashService,
                                                      RepositoryManager repositoryManager,
                                                      TransactionManager transactionManager,
                                                      TranscodeService transcodeService) {
        return new GrpcRepositoryService(agent, builderFactory, config, credentialsHelper, eventPublisher, executor,
                executorService, fetchManager, garbageCollectionManager, logicalClock, porcelain, pullRequestManager,
                pushManager, repairHelper, replicaStateRegistry, repositoryHashService, repositoryManager,
                transactionManager, transcodeService);
    }

    @Bean
    public GrpcWorkTreeService gitWorkTreeService(RepositoryManager repositoryManager,
                                                  GitQuarantineRegistry quarantineRegistry,
                                                  WorkTreeManager workTreeManager) {
            return new GrpcWorkTreeService(repositoryManager, quarantineRegistry, workTreeManager);
    }

    @Bean(destroyMethod = "shutdown")
    public GrpcAsyncExecutor grpcAsyncExecutor(ExecutionManager executionManager) {
        return new DefaultGrpcAsyncExecutor(executionManager, 2, Integer.MAX_VALUE, Duration.ofMinutes(1L),
                ConcurrencyUtils.createDaemonThreadFactory(EXECUTOR_RPC_ASYNC));
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    public GrpcMetrics grpcMetrics(GrpcServer server) {
        return new GrpcMetrics(server);
    }

    @Bean
    public GrpcPushTracer grpcPushTracer(ExecutionManager executionManager,
                                         @Value("${git.trace.push.buffer-size:768}") int pushTraceBufferSize) {
        return new LoggingGrpcPushTracer(executionManager, pushTraceBufferSize);
    }

    @Bean
    public GrpcServer grpcServer(ApplicationConfig config, EventPublisher eventPublisher,
                                 ServerInterceptor[] interceptors, BindableService[] services) {
        return new GrpcServer(config.getGrpc(), eventPublisher, interceptors, services);
    }

    @Bean
    public GrpcServiceAdvice grpcServiceAdvice() {
        return new GrpcServiceAdvice();
    }

    @Bean
    public GrpcHealthService healthService() {
        return new GrpcHealthService();
    }

    @Bean
    public GrpcLfsService lfsService(LfsManager lfsManager, RepositoryManager repositoryManager) {
        return new GrpcLfsService(lfsManager, repositoryManager);
    }

    @Bean
    @Order(ORDER_SI_LOGGING)
    public LoggingServerInterceptor loggingServerInterceptor() {
        return new LoggingServerInterceptor();
    }

    @Bean
    public GrpcManagementService managementService(GrpcAsyncExecutor asyncExecutor,
                                                   RepositoryAttributeManager attributeManager,
                                                   DataStoreManager dataStoreManager,
                                                   MeshManager meshManager, NodeManager nodeManager,
                                                   RepairManager repairManager,
                                                   ReplicaStateRegistry replicaStateRegistry,
                                                   RpcRepositoryAccessTracker repositoryAccessTracker,
                                                   RepositoryManager repositoryManager,
                                                   SidebandManager sidebandManager) {
        return new GrpcManagementService(asyncExecutor, attributeManager, dataStoreManager, meshManager, nodeManager,
                repairManager, replicaStateRegistry, repositoryAccessTracker, repositoryManager, sidebandManager);
    }

    @Bean
    @Order(ORDER_CI_METADATA)
    public MetadataClientInterceptor metadataClientInterceptor() {
        return new MetadataClientInterceptor();
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    @Order(ORDER_SI_METRICS)
    public MetricsServerInterceptor metricsServerInterceptor() {
        return new MetricsServerInterceptor();
    }

    @Bean
    @Order(ORDER_SI_REMOTE_REQUEST)
    public RequestServerInterceptor requestServerInterceptor(RequestManager requestManager) {
        return new RequestServerInterceptor(requestManager);
    }

    @Bean
    @ConditionalOnProperty(prefix = "node", name = "config-required", havingValue = "true", matchIfMissing = true)
    @Order(ORDER_SI_CONFIGURED)
    public RequireConfiguredServerInterceptor requireConfiguredServerInterceptor(ApplicationConfig config,
                                                                                 NodeManager nodeManager) {
        return new RequireConfiguredServerInterceptor(nodeManager, config.getNode().getConfigTimeout());
    }

    @Bean
    public RpcTransactionExecutor rpcTransactionExecutor(GitHookService hookService,
                                                         PullRequestManager pullRequestManager, RefManager refManager,
                                                         RepositoryManager repositoryManager,
                                                         TranscodeService transcodeService) {
        return new DefaultRpcTransactionExecutor(hookService, pullRequestManager, refManager,
                repositoryManager, transcodeService);
    }

    @Bean
    @Order(ORDER_SI_THROTTLING)
    public ThrottlingServerInterceptor throttlingServerInterceptor(ThrottleService throttleService) {
        return new ThrottlingServerInterceptor(throttleService);
    }

    @Bean(destroyMethod = "shutdown")
    public GrpcTransactionService transactionService(GitProcessBuilderFactory builderFactory,
                                                     EventPublisher eventPublisher, ExecutionManager executionManager,
                                                     LogicalClock logicalClock,
                                                     MeshManager meshManager, ReplicaStateRegistry replicaStateRegistry,
                                                     RepositoryAttributeManager repositoryAttributeManager,
                                                     RepositoryHashService repositoryHashService,
                                                     RepositoryManager repositoryManager,
                                                     RpcTransactionExecutor transactionExecutor,
                                                     TransactionManager transactionManager,
                                                     WriteTokenRegistry writeTokenRegistry) {
        return new GrpcTransactionService(builderFactory, eventPublisher, executionManager,
                logicalClock, meshManager, replicaStateRegistry, repositoryAttributeManager, repositoryHashService,
                repositoryManager, transactionExecutor, transactionManager, writeTokenRegistry);
    }

    @Bean
    @Order(ORDER_SI_WRONG_TARGET)
    public WrongTargetServerInterceptor wrongTargetServerInterceptor(ApplicationConfig config) {
        return new WrongTargetServerInterceptor(config.getNode());
    }

    @Bean
    @Order(ORDER_SI_ZERO_COPY)
    public ZeroCopyServerInterceptor zeroCopyServerInterceptor(ApplicationConfig config) {
        return new ZeroCopyServerInterceptor(config.getGrpc());
    }

    @Configuration
    @ConditionalOnProperty(name = "authentication.enabled", havingValue = "true", matchIfMissing = true)
    public static class AuthenticationWiring {

        @Bean
        @Order(ORDER_CI_AUTHENTICATION)
        public AuthenticationClientInterceptor authenticationClientInterceptor(Authenticator authenticator) {
            return new AuthenticationClientInterceptor(authenticator);
        }

        @Bean(initMethod = "prepareKeys")
        public DefaultAuthenticationManager authenticationManager(ExecutionManager executionManager,
                                                                  PublicKeyRegistry keyRegistry,
                                                                  StorageLayout storageLayout,
                                                                  TaskScheduler taskScheduler) {
            return new DefaultAuthenticationManager(storageLayout.getConfigDir(), executionManager,
                    keyRegistry, taskScheduler);
        }

        @Bean
        @Order(ORDER_SI_AUTHENTICATION)
        public AuthenticationServerInterceptor authenticationServerInterceptor(
                AuthenticationManager authenticationManager, Authenticator authenticator) {
            return new AuthenticationServerInterceptor(authenticationManager, authenticator);
        }

        @Bean
        public Authenticator authenticator(AuthenticationManager authenticationManager, ApplicationConfig config,
                                           PublicKeyRegistry keyRegistry, MeshManager meshManager,
                                           SidebandManager sidebandManager) {
            return new JwtAuthenticator(authenticationManager, config, keyRegistry, meshManager, sidebandManager);
        }

        @Bean
        public DefaultPublicKeyRegistry publicKeyRegistry() {
            return new DefaultPublicKeyRegistry();
        }
    }
}
