package com.atlassian.bitbucket.mesh;

import com.atlassian.bitbucket.mesh.app.*;
import com.atlassian.bitbucket.mesh.auth.AuthenticationManager;
import com.atlassian.bitbucket.mesh.client.GrpcMeshClient;
import com.atlassian.bitbucket.mesh.client.MeshClient;
import com.atlassian.bitbucket.mesh.concurrent.CallerBlocksPolicy;
import com.atlassian.bitbucket.mesh.config.*;
import com.atlassian.bitbucket.mesh.coordination.DefaultWriteTokenRegistry;
import com.atlassian.bitbucket.mesh.coordination.LamportClock;
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.credentials.SimpleCredentialsHelper;
import com.atlassian.bitbucket.mesh.event.EventPublisher;
import com.atlassian.bitbucket.mesh.event.SpringEventPublisher;
import com.atlassian.bitbucket.mesh.execution.ExecutionManager;
import com.atlassian.bitbucket.mesh.execution.GrpcExecutionManager;
import com.atlassian.bitbucket.mesh.git.*;
import com.atlassian.bitbucket.mesh.git.auth.DefaultGitAuthenticationHelper;
import com.atlassian.bitbucket.mesh.git.auth.GitAuthenticationHelper;
import com.atlassian.bitbucket.mesh.git.batch.BatchManager;
import com.atlassian.bitbucket.mesh.git.batch.DefaultBatchManager;
import com.atlassian.bitbucket.mesh.git.binary.DefaultGitBinaryHelper;
import com.atlassian.bitbucket.mesh.git.binary.GitBinary;
import com.atlassian.bitbucket.mesh.git.binary.GitBinaryHelper;
import com.atlassian.bitbucket.mesh.git.builder.*;
import com.atlassian.bitbucket.mesh.git.builder.merge.GitMergeFastForward;
import com.atlassian.bitbucket.mesh.git.cache.DefaultPackCacheService;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheExpiryStrategy;
import com.atlassian.bitbucket.mesh.git.cache.PackCacheService;
import com.atlassian.bitbucket.mesh.git.commit.CommitManager;
import com.atlassian.bitbucket.mesh.git.commit.DefaultCommitManager;
import com.atlassian.bitbucket.mesh.git.content.ContentManager;
import com.atlassian.bitbucket.mesh.git.content.DefaultContentManager;
import com.atlassian.bitbucket.mesh.git.fetch.DefaultFetchManager;
import com.atlassian.bitbucket.mesh.git.fetch.FetchManager;
import com.atlassian.bitbucket.mesh.git.file.DefaultEditFileManager;
import com.atlassian.bitbucket.mesh.git.file.EditFileManager;
import com.atlassian.bitbucket.mesh.git.gc.DefaultGarbageCollectionManager;
import com.atlassian.bitbucket.mesh.git.gc.GarbageCollectionManager;
import com.atlassian.bitbucket.mesh.git.hook.DefaultGitHookService;
import com.atlassian.bitbucket.mesh.git.hook.GitHookHandler;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.git.hook.GitHookThreadPoolExecutor;
import com.atlassian.bitbucket.mesh.git.hook.handler.CachingPackObjectsHookHandler;
import com.atlassian.bitbucket.mesh.git.hook.script.*;
import com.atlassian.bitbucket.mesh.git.merge.*;
import com.atlassian.bitbucket.mesh.git.porcelain.GitPorcelain;
import com.atlassian.bitbucket.mesh.git.porcelain.ShinyGitPorcelain;
import com.atlassian.bitbucket.mesh.git.pull.DarkReferenceTransactionListener;
import com.atlassian.bitbucket.mesh.git.pull.DefaultPullRequestManager;
import com.atlassian.bitbucket.mesh.git.pull.PullRequestManager;
import com.atlassian.bitbucket.mesh.git.push.DefaultPushManager;
import com.atlassian.bitbucket.mesh.git.push.PushManager;
import com.atlassian.bitbucket.mesh.git.rebase.DefaultRebaseManager;
import com.atlassian.bitbucket.mesh.git.rebase.RebaseManager;
import com.atlassian.bitbucket.mesh.git.ref.DefaultRefManager;
import com.atlassian.bitbucket.mesh.git.ref.RefManager;
import com.atlassian.bitbucket.mesh.git.revert.DefaultRevertManager;
import com.atlassian.bitbucket.mesh.git.revert.RevertManager;
import com.atlassian.bitbucket.mesh.git.submodule.DefaultSubmoduleManager;
import com.atlassian.bitbucket.mesh.git.submodule.SubmoduleManager;
import com.atlassian.bitbucket.mesh.git.transcode.*;
import com.atlassian.bitbucket.mesh.layout.DefaultHomeSubdirectoryResolver;
import com.atlassian.bitbucket.mesh.layout.HomeSubdirectoryResolver;
import com.atlassian.bitbucket.mesh.layout.SimpleStorageLayout;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.atlassian.bitbucket.mesh.management.*;
import com.atlassian.bitbucket.mesh.metrics.*;
import com.atlassian.bitbucket.mesh.migration.DefaultMigrationStateManager;
import com.atlassian.bitbucket.mesh.migration.MigrationStateManager;
import com.atlassian.bitbucket.mesh.migration.SidecarMigrationTransactionListener;
import com.atlassian.bitbucket.mesh.network.*;
import com.atlassian.bitbucket.mesh.platform.DefaultOperatingSystemService;
import com.atlassian.bitbucket.mesh.platform.OperatingSystemService;
import com.atlassian.bitbucket.mesh.process.DefaultProcessConfigurer;
import com.atlassian.bitbucket.mesh.process.ProcessConfigurer;
import com.atlassian.bitbucket.mesh.process.ProcessCreator;
import com.atlassian.bitbucket.mesh.process.nu.NuProcessCreator;
import com.atlassian.bitbucket.mesh.repair.*;
import com.atlassian.bitbucket.mesh.repository.*;
import com.atlassian.bitbucket.mesh.request.DefaultRequestManager;
import com.atlassian.bitbucket.mesh.request.RequestManager;
import com.atlassian.bitbucket.mesh.spring.ProfilingAspect;
import com.atlassian.bitbucket.mesh.throttle.ResourceThrottleStrategyProvider;
import com.atlassian.bitbucket.mesh.throttle.SemaphoreThrottleService;
import com.atlassian.bitbucket.mesh.throttle.ThrottleService;
import com.atlassian.bitbucket.mesh.transaction.*;
import com.atlassian.bitbucket.mesh.util.Version;
import com.atlassian.util.contentcache.BackgroundThreadStreamPumper;
import com.atlassian.util.contentcache.ContentCacheManager;
import com.atlassian.util.contentcache.FileContentCacheManager;
import com.atlassian.util.contentcache.LruEvictionStrategy;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.jmx.JmxReporter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import io.grpc.ClientInterceptor;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.jmx.JmxConfig;
import io.micrometer.jmx.JmxMeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.transaction.PlatformTransactionManager;

import javax.annotation.Nonnull;
import javax.management.MalformedObjectNameException;
import java.nio.file.Path;
import java.time.Clock;
import java.util.*;
import java.util.concurrent.*;

import static com.atlassian.bitbucket.mesh.MeshConstants.*;
import static com.atlassian.bitbucket.mesh.util.ConcurrencyUtils.createDaemonThreadFactory;
import static com.atlassian.bitbucket.mesh.util.ConcurrencyUtils.createScheduledExecutor;
import static java.util.Collections.emptyList;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * Spring wiring for the core module.
 */
@Configuration
public class MeshCoreWiring {

    private static final int ORDER_RCV_PACK_TX = 10;

    //These constants are ordered by their _values_, not their names, so they're in the order their
    //associated listeners are run in.
    private static final int ORDER_TX_LISTENER_READ_ONLY = 1;
    private static final int ORDER_TX_LISTENER_MESH_MIGRATION = 2;
    private static final int ORDER_TX_LISTENER_COORDINATOR = 3;
    private static final int ORDER_TX_LISTENER_DARK_REFS = 3;

    public MeshCoreWiring(Environment environment) {
        Set<String> activeProfiles = Sets.newHashSet(environment.getActiveProfiles());
        activeProfiles.retainAll(ImmutableSet.of(PROFILE_CLOUD, PROFILE_SIDECAR, PROFILE_STANDALONE));

        if (activeProfiles.size() > 1) {
            throw new InvalidConfigurationException(
                    "Conflicting profiles were set: " + String.join(", ", activeProfiles),
                    "Update the run configuration and restart the application");
        }
    }

    @Bean
    public ApplicationConfig applicationConfig(Environment environment, GitBinaryHelper gitBinaryHelper,
                                               StorageLayout storageLayout) {
        return new DefaultApplicationConfig(environment, gitBinaryHelper, storageLayout);
    }

    @Bean
    @ConditionalOnMissingBean //to let the tests override this
    public ApplicationDirs applicationDirs(Environment environment) {
        ApplicationDirResolver resolver = new ApplicationDirResolver(environment);
        Path homeDir = resolver.resolveHomeDir();
        Path sharedHomeDir = null;
        List<Path> dataDirs;
        if (environment.acceptsProfiles(PROFILES_SIDECAR)) {
            sharedHomeDir = resolver.resolveDir("shared home", "bitbucket.shared-home",
                    homeDir.resolve("shared").toString());
            //Legacy data stores should be accessed via BitbucketProperties instead, and only in sidecar
            //mode, so they are not resolved here
            dataDirs = emptyList();
        } else if (environment.acceptsProfiles(PROFILES_CLOUD)) {
            //In cloud mode, no data directories need to be provided.
            dataDirs = emptyList();
        } else {
            dataDirs = resolver.resolveDataDirs();
        }

        return new ApplicationDirs(homeDir, sharedHomeDir, dataDirs);
    }

    @Bean
    public ApplicationSupportInfoProvider applicationSupportPropertiesProvider(BuildInfo buildInfo,
                                                                               ApplicationConfig config,
                                                                               FileSanitizer fileSanitizer,
                                                                               NuProcessCreator processCreator) {
        return new ApplicationSupportInfoProvider(buildInfo, config, fileSanitizer, processCreator);
    }

    @Bean
    public GitAuthenticationHelper authenticationHelper(ApplicationConfig config, StorageLayout storageLayout) {
        return new DefaultGitAuthenticationHelper(config.getProcess(), storageLayout);
    }

    @Bean
    public BatchManager batchManager(GitProcessBuilderFactory builderFactory, StorageLayout storageLayout) {
        return new DefaultBatchManager(builderFactory, storageLayout);
    }

    @Bean
    public Clock clock() {
        return Clock.systemDefaultZone();
    }

    @Bean
    public CommitManager commitManager(GitProcessBuilderFactory builderFactory) {
        return new DefaultCommitManager(builderFactory);
    }

    @Bean
    public ContentManager contentManager(GitProcessBuilderFactory builderFactory, SubmoduleManager submoduleManager) {
        return new DefaultContentManager(builderFactory, submoduleManager);
    }

    @Bean
    public CredentialsHelper credentialsHelper(ExecutionManager executionManager) {
        return new SimpleCredentialsHelper(executionManager);
    }

    @Bean
    public DataStoreManager dataStoreManager(Optional<BitbucketServerDataStore> bitbucketServerDataStore,
                                             Optional<ManagedDataStore> managedDataStore,
                                             Optional<PathDataStore> pathDataStore) {
        //Depending on configuration and profile, some DataStore types may not be available. The only
        //invariant is that at least one must be set, or an exception will be thrown
        return new DefaultDataStoreManager(bitbucketServerDataStore.orElse(null),
                managedDataStore.orElse(null), pathDataStore.orElse(null));
    }

    @Bean
    public EditFileManager editFileManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                           EventPublisher eventPublisher, GitPorcelain porcelain) {
        return new DefaultEditFileManager(agent, builderFactory, eventPublisher, porcelain);
    }

    @Bean
    public EventPublisher eventPublisher(ApplicationEventPublisher applicationEventPublisher,
                                         TransactionManager transactionManager) {
        return new SpringEventPublisher(applicationEventPublisher, transactionManager);
    }

    @Bean
    @Primary
    public ExecutorService executorService(ApplicationConfig config) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, config.getExecutorMaxThreads(), 1, TimeUnit.MINUTES,
                new SynchronousQueue<>(), createDaemonThreadFactory(EXECUTOR_COMMON));
        executor.setRejectedExecutionHandler(new CallerBlocksPolicy(config.getExecutorTimeout()));
        return executor;
    }

    @Bean
    public FetchManager fetchManager(GitAgent agent, GitAuthenticationHelper authenticationHelper,
                                     ApplicationConfig config, GitProcessBuilderFactory builderFactory,
                                     EventPublisher eventPublisher) {
        return new DefaultFetchManager(agent, authenticationHelper, builderFactory,
                config.getProcess(), eventPublisher);
    }

    @Bean
    public FileSanitizer fileSanitizer(StorageLayout storageLayout) {
        return new FileSanitizer(storageLayout.getTempDir());
    }

    @Bean(destroyMethod = "shutdown")
    public DefaultGarbageCollectionManager garbageCollectionManager(GitProcessBuilderFactory builderFactory,
                                                                    ApplicationConfig config,
                                                                    GitRepositoryLayout repositoryLayout) {
        GcConfig gcConfig = config.getGit().getGc();
        ScheduledExecutorService gcExecutor = createScheduledExecutor(gcConfig.getThreads(),
                createDaemonThreadFactory(EXECUTOR_GIT_GC));

        return new DefaultGarbageCollectionManager(gcConfig, builderFactory, gcExecutor, repositoryLayout);
    }

    @Bean
    public GitAgent gitAgent() {
        return new RawGitAgent();
    }

    @Bean
    public GitBinary gitBinary(ApplicationConfig config) {
        return config.getGit().getBinary();
    }

    @Bean
    public GitBinaryHelper gitBinaryHelper(@Value("${git.version.unsupported-from:}") String unsupportedFrom) {
        DefaultGitBinaryHelper helper = new DefaultGitBinaryHelper();
        if (isNotBlank(unsupportedFrom)) {
            helper.setUnsupportedFromVersion(new Version(unsupportedFrom));
        }
        return helper;
    }

    @Bean
    public GitPorcelain gitPorcelain(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                     StorageLayout storageLayout) {
        return new ShinyGitPorcelain(agent, builderFactory, storageLayout);
    }

    @Bean
    public GitProcessBuilderFactory gitProcessBuilderFactory(GitBinary binary,
                                                             GitProcessBuilderConfigurer[] configurers,
                                                             ApplicationConfig config,
                                                             ProcessCreator processCreator,
                                                             GitQuarantineRegistry quarantineRegistry,
                                                             RepositoryManager repositoryManager,
                                                             StorageLayout storageLayout) {
        return new DefaultGitProcessBuilderFactory(binary, configurers, config.getProcess(),
                processCreator, quarantineRegistry, repositoryManager, storageLayout);
    }

    @Bean
    public GitRepositoryLayout gitRepositoryLayout() {
        return new DefaultGitRepositoryLayout();
    }

    @Bean
    public HomeSubdirectoryResolver homeSubdirectoryResolver(Environment environment) {
        return new DefaultHomeSubdirectoryResolver(environment);
    }

    @Bean
    @Profile(CONDITION_NOT_SIDECAR) //Garbage collection should only be scheduled when the app is not running as a sidecar
    public HookScriptGarbageCollectionScheduler hookScriptGarbageCollectionScheduler(HookScriptManager hookScriptManager) {
        return new HookScriptGarbageCollectionScheduler(hookScriptManager);
    }

    @Bean
    @ConditionalOnMissingBean //to allow tests to override the implementation
    public HookScriptManager hookScriptManager(GitProcessBuilderFactory builderFactory,
                                               HookScriptCommandLineFactory commandLineFactory,
                                               ApplicationConfig config, Environment environment,
                                               ProcessCreator processCreator,
                                               GitQuarantineRegistry quarantineRegistry,
                                               StorageLayout storageLayout) {
        boolean sidecarEnabled = environment.acceptsProfiles(PROFILES_SIDECAR);

        return new DefaultHookScriptManager(builderFactory, commandLineFactory, config.getGit().getHookScripts(),
                processCreator, quarantineRegistry, storageLayout, sidecarEnabled);
    }

    @Bean
    public LfsManager lfsManager(ApplicationConfig config) {
        return new DefaultLfsManager(config.getLfs());
    }

    @Bean
    public LogicalClock logicalClock() {
        return new LamportClock();
    }

    @Bean
    public MergeManager mergeManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                     EventPublisher eventPublisher, GitBinary gitBinary,
                                     MergeStrategyFactory mergeStrategyFactory, GitPorcelain porcelain) {
        return new DefaultMergeManager(agent, builderFactory, eventPublisher, gitBinary, mergeStrategyFactory, porcelain);
    }

    @Bean
    public MergeStrategyFactory mergeStrategyFactory(GitAgent agent, GitProcessBuilderFactory builderFactory) {
        //All merge strategies are thread-safe, so it's possible to instantiate them once and reuse the same instance
        //for all merge operations.
        //Each GitMergeStrategy is _required_ to have an entry here or MapMergeStrategyFactory's constructor will
        //throw an UnsupportedMergeStrategyException.
        return new MapMergeStrategyFactory(ImmutableMap.<GitMergeStrategy, MergeStrategy>builder()
                .put(GitMergeStrategy.FF, new DefaultMergeStrategy(builderFactory, GitMergeFastForward.ON))
                .put(GitMergeStrategy.FF_ONLY, new DefaultMergeStrategy(builderFactory, GitMergeFastForward.ONLY))
                .put(GitMergeStrategy.NO_FF, new DefaultMergeStrategy(builderFactory, GitMergeFastForward.OFF))
                .put(GitMergeStrategy.REBASE_FF_ONLY,
                        new RebaseMergeStrategy(agent, builderFactory, GitMergeFastForward.ONLY))
                .put(GitMergeStrategy.REBASE_NO_FF,
                        new RebaseMergeStrategy(agent, builderFactory, GitMergeFastForward.OFF))
                .put(GitMergeStrategy.SQUASH, new SquashMergeStrategy(builderFactory, GitMergeFastForward.DEFAULT))
                .put(GitMergeStrategy.SQUASH_FF_ONLY,
                        new SquashMergeStrategy(builderFactory, GitMergeFastForward.ONLY))
                .build());
    }

    @Bean
    public GrpcMeshClient meshClient(GitProcessBuilderFactory builderFactory, ChannelRegistry channelRegistry,
                                     ApplicationConfig config, EventPublisher eventPublisher,
                                     ExecutorService executorService, GarbageCollectionManager garbageCollectionManager,
                                     LogicalClock logicalClock, GitPorcelain porcelain,
                                     InteractiveRepairHelper repairHelper, ReplicaStateRegistry replicaStateRegistry,
                                     RepositoryHashService repositoryHashService, RepositoryManager repositoryManager,
                                     TranscodeService transcodeService) {
        return new GrpcMeshClient(builderFactory, channelRegistry, config, eventPublisher, executorService,
                garbageCollectionManager, logicalClock, porcelain, repairHelper, replicaStateRegistry,
                repositoryHashService, repositoryManager, transcodeService);
    }

    @Bean
    public ConnectivitySupportInfoProvider meshConnectivityDiagnosticsProvider(MeshClient meshClient,
                                                                               MeshManager meshManager) {
        return new ConnectivitySupportInfoProvider(meshClient, meshManager);
    }

    @Bean
    public MeshManager meshManager(ApplicationConfig config, EventPublisher eventPublisher) {
        return new DefaultMeshManager(eventPublisher, config.getNode());
    }

    @Bean
    public NodeManager nodeManager(Optional<AuthenticationManager> authenticationManager, BuildInfo buildInfo,
                                   GitProcessBuilderFactory builderFactory, ApplicationConfig config,
                                   ConfigurableEnvironment environment, EventPublisher eventPublisher,
                                   FileSanitizer fileSanitizer, MeshManager meshManager,
                                   ReplicaStateRegistry replicaStateRegistry, StorageLayout storageLayout,
                                   SupportInfoRegistry supportInfoRegistry, ThrottleService throttleService) {
        boolean blockForTopology = environment.acceptsProfiles(PROFILES_STANDALONE);

        return new DefaultNodeManager(authenticationManager.orElse(null), buildInfo, builderFactory, config,
                environment, eventPublisher, fileSanitizer, meshManager, new PropertiesPropertySourceLoader(),
                replicaStateRegistry, storageLayout, supportInfoRegistry, throttleService, blockForTopology);
    }

    @Bean(destroyMethod = "shutdown")
    public NuProcessCreator processCreator(ProcessConfigurer[] configurers, ExecutionManager executionManager,
                                           ThrottleService throttleService) {
        return new NuProcessCreator(configurers, executionManager, throttleService);
    }

    @Bean
    public ProfilingAspect profilingAspect() {
        return new ProfilingAspect();
    }

    @Bean
    public PullRequestManager pullRequestManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                                 ApplicationConfig config, RepositoryHashService hashService,
                                                 GitPorcelain porcelain, TransactionManager transactionManager) {
        return new DefaultPullRequestManager(agent, builderFactory, config.getGit(), hashService, porcelain,
                transactionManager);
    }

    @Bean
    public PushManager pushManager(GitAgent agent, GitAuthenticationHelper authenticationHelper,
                                   ApplicationConfig config, GitProcessBuilderFactory builderFactory) {
        return new DefaultPushManager(agent, authenticationHelper, config.getProcess(), builderFactory);
    }

    @Bean
    public RebaseManager rebaseManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                       EventPublisher eventPublisher, GitPorcelain porcelain) {
        return new DefaultRebaseManager(agent, builderFactory, eventPublisher, porcelain);
    }

    @Bean
    public RefHasher refHasher(GitProcessBuilderFactory builderFactory) {
        return new DefaultRefHasher(builderFactory);
    }

    @Bean
    public RefManager refManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                 EventPublisher eventPublisher, @Lazy MeshClient meshClient,
                                 ReplicaStateRegistry replicaStateRegistry,
                                 TransactionManager transactionManager,
                                 UpdateRefsContextHolder updateRefsContextHolder) {
        return new DefaultRefManager(agent, builderFactory, eventPublisher, meshClient, replicaStateRegistry,
                transactionManager, updateRefsContextHolder);
    }

    @Bean
    public RepairGate repairGate(ApplicationConfig config) {
        return new RepairGate(config.getRepair().getThrottleLimit());
    }

    @Bean
    public InteractiveRepairHelper repairHelper(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                                ApplicationConfig config, EventPublisher eventPublisher,
                                                ExecutionManager executionManager, ExecutorService executorService,
                                                GarbageCollectionManager garbageCollectionManager, RepairGate gate,
                                                GitPorcelain porcelain, RefManager refManager,
                                                Optional<RepairMetrics> repairMetrics,
                                                RepositoryAttributeManager repositoryAttributeManager,
                                                RepositoryHashService repositoryHashService,
                                                RepositoryManager repositoryManager,
                                                TransactionManager transactionManager,
                                                TranscodeService transcodeService,
                                                WriteTokenRegistry writeTokenRegistry) {
        return new DefaultInteractiveRepairHelper(agent, builderFactory, config.getRepair(), eventPublisher,
                executionManager, executorService, garbageCollectionManager, gate, porcelain, refManager,
                repairMetrics.orElse(null), repositoryAttributeManager, repositoryHashService, repositoryManager,
                transactionManager, transcodeService, writeTokenRegistry);
    }

    @Bean(destroyMethod = "destroy")
    public DefaultRepairManager repairManager(ApplicationConfig config, ExecutionManager executionManager,
                                              MeshClient meshClient, MeshManager meshManager,
                                              ReplicaStateRegistry replicaStateRegistry, TaskScheduler taskScheduler) {
        int threads = config.getRepair().getThrottleLimit();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(threads, threads, 2, TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(), createDaemonThreadFactory(EXECUTOR_REPAIR));
        threadPoolExecutor.allowCoreThreadTimeOut(true);

        return new DefaultRepairManager(config.getRepair(), executionManager, threadPoolExecutor,
                meshClient, meshManager, replicaStateRegistry, taskScheduler);
    }

    @Bean
    public ReplicaStateRegistry replicaStateRegistry(EventPublisher eventPublisher, LogicalClock logicalClock,
                                                     SidebandManager sidebandManager) {
        return new DefaultReplicaStateRegistry(eventPublisher, logicalClock, sidebandManager);
    }

    @Bean
    public RpcRepositoryAccessTracker repositoryAccessTracker(ExecutionManager executionManager,
                                                              RequestManager requestManager) {
        return new DefaultRpcRepositoryAccessTracker(executionManager, requestManager);
    }

    @Bean
    public RepositoryAttributeManager repositoryAttributeManager(RepositoryAttributeDao repositoryAttributeDao) {
        return new DefaultRepositoryAttributeManager(repositoryAttributeDao);
    }

    @Bean
    public RepositoryHashService repositoryHashService(GitAgent agent, ApplicationConfig config,
                                                       RefHasher refHasher,
                                                       RepositoryAttributeManager repositoryAttributeManager,
                                                       TransactionManager transactionManager,
                                                       TranscodeService transcodeService) {
        return new DefaultRepositoryHashService(agent, refHasher, config.getReplication(),
                repositoryAttributeManager, transactionManager, transcodeService);
    }

    @Bean
    public RepositoryManager repositoryManager(GitAgent agent, RepositoryAttributeManager attributeManager,
                                               @Lazy GitProcessBuilderFactory builderFactory,
                                               DataStoreManager dataStoreManager, EventPublisher eventPublisher,
                                               ThreadPoolTaskScheduler taskScheduler,
                                               ReplicaStateRegistry replicaStateRegistry,
                                               RepositoryAccessListener repositoryAccessListener,
                                               @Lazy RepositoryHashService repositoryHashService,
                                               GitRepositoryLayout repositoryLayout,
                                               TransactionManager transactionManager, WorkTreeManager workTreeManager) {
        return new DefaultRepositoryManager(agent, attributeManager, builderFactory, dataStoreManager,
                eventPublisher, taskScheduler.getScheduledExecutor(), replicaStateRegistry, repositoryAccessListener,
                repositoryHashService, repositoryLayout, transactionManager, workTreeManager);
    }

    @Bean
    public RequestManager requestManager(ExecutionManager executionManager) {
        return new DefaultRequestManager(executionManager);
    }

    @Bean
    public RevertManager revertManager(GitAgent agent, GitProcessBuilderFactory builderFactory,
                                       EventPublisher eventPublisher, GitPorcelain porcelain) {
        return new DefaultRevertManager(agent, builderFactory, eventPublisher, porcelain);
    }

    @Bean
    public SidebandManager sidebandManager(ApplicationConfig config, RequestManager requestManager) {
        return new DefaultSidebandManager(requestManager, config.getNode().getSidebandQueueLimit());
    }

    @Bean
    public SubmoduleManager submoduleManager(GitProcessBuilderFactory builderFactory) {
        return new DefaultSubmoduleManager(builderFactory);
    }

    @Bean
    public SupportInfoRegistry supportInfoRegistry() {
        return new SupportInfoRegistry();
    }

    @Bean
    public TransactionManager transactionManager(ApplicationConfig config, @Lazy EventPublisher eventPublisher,
                                                 ExecutionManager executionManager, LogicalClock logicalClock,
                                                 @Lazy MeshClient meshClient, @Lazy MeshManager meshManager,
                                                 @Lazy RepositoryAttributeManager repositoryAttributeManager,
                                                 @Lazy RepositoryHashService repositoryHashService,
                                                 @Lazy RepositoryManager repositoryManager,
                                                 SidebandManager sidebandManager,
                                                 @Lazy WriteTokenRegistry writeTokenRegistry) {
        return new DefaultTransactionManager(config, eventPublisher, executionManager, logicalClock, meshClient,
                meshManager, repositoryAttributeManager, repositoryHashService, repositoryManager, sidebandManager,
                writeTokenRegistry);
    }

    @Bean
    @Order(ORDER_RCV_PACK_TX)
    public TransactionGitReceivePackListener transactionGitReceivePackListener(GitAgent agent,
                                                                               ApplicationConfig config,
                                                                               ExecutionManager executionManager,
                                                                               TransactionManager transactionManager) {
        return new TransactionGitReceivePackListener(agent, executionManager, config.getGit().getHosting(),
                transactionManager);
    }

    @Bean
    public TranscodeService transcodeService(GitProcessBuilderFactory builderFactory,
                                             EventPublisher eventPublisher,
                                             TransactionManager transactionManager) {
        return new DefaultTranscodeService(builderFactory, eventPublisher, transactionManager);
    }

    @Bean
    public UploadManager uploadManager(GitAgent agent, EventPublisher eventPublisher,
                                       RepositoryHashService repositoryHashService) {
        return new DefaultUploadManager(agent, eventPublisher, repositoryHashService);
    }

    @Bean
    public WriteTokenRegistry writeTokenRegistry(ApplicationConfig applicationConfig, LogicalClock logicalClock,
                                                 MeshManager meshManager, ThreadPoolTaskScheduler taskScheduler) {
        return new DefaultWriteTokenRegistry(logicalClock, meshManager, taskScheduler,
                applicationConfig.getReplication().getYieldTokenRetryInterval());
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    AtlassianProfilingIntegration atlassianProfilingIntegration() {
        return new AtlassianProfilingIntegration();
    }

    @Bean(destroyMethod = "shutdown")
    GrpcChannelRegistry channelRegistry(ApplicationConfig config, ClientInterceptor[] interceptors) {
        return new GrpcChannelRegistry(config.getGrpc(), interceptors);
    }

    @Bean
    public WorkTreeManager workTreeManager(GitAgent agent, @Lazy GitProcessBuilderFactory builderFactory,
                                           DataStoreManager dataStoreManager, @Lazy GitPorcelain porcelain,
                                           @Lazy RepositoryManager repositoryManager, TaskScheduler taskScheduler,
                                           PlatformTransactionManager transactionManager,
                                           WorkTreeStateDao workTreeStateDao) {
        return new DefaultWorkTreeManager(agent, builderFactory, dataStoreManager, porcelain, repositoryManager,
                taskScheduler, transactionManager, workTreeStateDao);
    }

    @Bean
    @Order(1)
    ConfigurableGitProcessBuilderConfigurer configurableGitProcessBuilderConfigurer(ApplicationConfig config) {
        return new ConfigurableGitProcessBuilderConfigurer(config.getGit().getProcess());
    }

    @Bean
    @Order(ORDER_TX_LISTENER_DARK_REFS)
    DarkReferenceTransactionListener darkReferenceTransactionListener(GitAgent agent) {
        return new DarkReferenceTransactionListener(agent);
    }

    @Bean
    @Order(2)
    DeadlineGitProcessBuilderConfigurer deadlineGitProcessBuilderConfigurer() {
        return new DeadlineGitProcessBuilderConfigurer();
    }

    @Bean
    @Order(0)
    DefaultGitProcessBuilderConfigurer defaultGitProcessBuilderConfigurer() {
        return new DefaultGitProcessBuilderConfigurer();
    }

    @Bean
    @Order(0)
    DefaultProcessConfigurer defaultGitProcessConfigurer(ApplicationConfig config, StorageLayout storageLayout) {
        return new DefaultProcessConfigurer(config, storageLayout);
    }

    @Bean
    ExecutionManager executionManager() {
        return new GrpcExecutionManager();
    }

    @Bean(destroyMethod = "shutdown", initMethod = "startup")
    @ConditionalOnMissingBean //to allow tests to override the implementation
    DefaultGitHookService gitHookService(ApplicationConfig config, ExecutionManager executionManager,
                                         GitHookThreadPoolExecutor executor, List<GitHookHandler> handlers,
                                         RepositoryManager repositoryManager, StorageLayout storageLayout) {
        return new DefaultGitHookService(config.getGit().getHooks(), executionManager, executor, handlers,
                repositoryManager, storageLayout.getTempDir());
    }

    @Bean
    GitHookThreadPoolExecutor gitHookThreadPoolExecutor(ApplicationConfig config) {
        return new GitHookThreadPoolExecutor(config.getGit().getHooks(),
                createDaemonThreadFactory(EXECUTOR_GIT_HOOK));
    }

    @Bean
    GitQuarantineRegistry gitQuarantineRegistry() {
        return new GitQuarantineRegistry();
    }

    @Bean
    @Order(LifecyclePhases.HOME_INIT)
    HomeInitializer homeInitializer(ApplicationDirs applicationDirs) {
        return new HomeInitializer(applicationDirs.getHomeDir());
    }

    @Bean(initMethod = "lock", destroyMethod = "unlock")
    @Order(LifecyclePhases.HOME_LOCK)
    HomeLockAcquirer homeLockAcquirer(ApplicationDirs applicationDirs) {
        return new HomeLockAcquirer(applicationDirs.getHomeDir());
    }

    @Bean
    @Order(2)
    HookGitProcessBuilderConfigurer hookGitProcessBuilderConfigurer(@Lazy GitHookService hookService) {
        return new HookGitProcessBuilderConfigurer(hookService);
    }

    @Bean
    HookScriptCommandLineFactory hookScriptCommandLineFactory() {
        return new PosixHookScriptCommandLineFactory();
    }

    @Bean
    @ConditionalOnProperty("management.metrics.export.jmx.enabled")
    JmxMeterRegistry jmxMeterRegistry(JmxConfig jmxConfig, io.micrometer.core.instrument.Clock clock,
                                      Environment environment) {
        MetricRegistry registry = new MetricRegistry();
        JmxRegistryNameHandler nameHandler = new JmxRegistryNameHandler(metricCommonTags(environment));
        return new JmxMeterRegistry(jmxConfig, clock, nameHandler, registry, JmxReporter.forRegistry(registry)
                .inDomain(jmxConfig.domain())
                .createsObjectNamesWith(nameHandler)
                .build());
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    MeterRegistryCustomizer<MeterRegistry> metricsCommonTagsCustomizer(Environment environment) {
        Map<String, String> commonTags = metricCommonTags(environment);
        return registry -> {
            MeterRegistry.Config config = registry.config();
            commonTags.forEach(config::commonTags);
        };
    }

    @Bean
    OperatingSystemService operatingSystemService() throws MalformedObjectNameException {
        return new DefaultOperatingSystemService();
    }

    @Bean(destroyMethod = "shutdown")
    FileContentCacheManager packCacheManager(ApplicationConfig config) {
        PackCacheConfig cacheConfig = config.getPackCache();
        return new FileContentCacheManager.Builder(cacheConfig.getCacheDir())
                .evictionFreeSpaceBytes(cacheConfig.getEvictionTriggerFreeSpace())
                .evictionStrategy(new LruEvictionStrategy())
                .expiryCheckInterval(cacheConfig.getExpiryCheckInterval().getSeconds(), TimeUnit.SECONDS)
                .expiryStrategy(new PackCacheExpiryStrategy(cacheConfig))
                .hysteresis(cacheConfig.getEvictionHysteresis())
                .minFreeSpaceBytes(cacheConfig.getMinimumFreeSpace())
                .streamPumper(new BackgroundThreadStreamPumper("pack-cache-streamer"))
                .build();
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    PackCacheMetrics packCacheMetrics(PackCacheService packCacheService) {
        return new PackCacheMetrics(packCacheService);
    }

    @Bean(destroyMethod = "destroy", initMethod = "init")
    DefaultPackCacheService packCacheService(ContentCacheManager packCacheManager, ApplicationConfig config) {
        return new DefaultPackCacheService(packCacheManager, config.getPackCache());
    }

    @Bean
    CachingPackObjectsHookHandler packObjectsHookHandler(GitProcessBuilderFactory builderFactory,
                                                         ApplicationConfig config, PackCacheService packCacheService,
                                                         ThrottleService throttleService) {
        return new CachingPackObjectsHookHandler(builderFactory, config.getGit().getHooks(),
                packCacheService, throttleService);
    }

    @Bean
    @Order(ORDER_TX_LISTENER_READ_ONLY)
    ReferenceTransactionListener readOnlyReferenceTransactionListener(InteractiveRepairHelper repairHelper) {
        return new ReadOnlyReferenceTransactionListener(repairHelper);
    }

    @Bean
    @Order(ORDER_TX_LISTENER_COORDINATOR)
    ReferenceTransactionListener referenceTransactionCoordinator(ApplicationConfig config,
                                                                 ExecutionManager executionManager,
                                                                 RepositoryHashService repositoryHashService,
                                                                 TransactionManager transactionManager) {
        return new CoordinatingReferenceTransactionListener(config.getReplication(), executionManager,
                repositoryHashService, transactionManager);
    }

    @Bean
    GitReferenceTransactionHookHandler referenceTransactionHookHandler(ApplicationConfig config,
                                                                       GitProcessBuilderFactory builderFactory,
                                                                       ExecutionManager executionManager,
                                                                       List<ReferenceTransactionListener> listeners,
                                                                       UpdateRefsContextHolder updateRefsContextHolder) {
        return new GitReferenceTransactionHookHandler(config.getGit().getCompatibility(), builderFactory,
                executionManager, listeners, updateRefsContextHolder);
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    RepairMetrics repairMetrics(ReplicaStateRegistry replicaStateRegistry) {
        return new DefaultRepairMetrics(replicaStateRegistry);
    }

    @Bean
    @ConditionalOnMissingBean //to allow tests to override the implementation
    StorageLayout storageLayout(ApplicationDirs applicationDirs, HomeSubdirectoryResolver homeSubdirectoryResolver) {
        return new SimpleStorageLayout(applicationDirs, homeSubdirectoryResolver);
    }

    @Bean
    ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(2);
        threadPoolTaskScheduler.setThreadNamePrefix("task-scheduler");
        return threadPoolTaskScheduler;
    }

    @Bean
    @Conditional(MetricsEnabledCondition.class)
    ThrottlingMetrics throttlingMetrics(ThrottleService throttleService) {
        return new ThrottlingMetrics(throttleService);
    }

    @Bean(initMethod = "initialize")
    SemaphoreThrottleService throttleService(Clock clock, ExecutionManager executionManager,
                                             TaskScheduler taskScheduler,
                                             ResourceThrottleStrategyProvider throttleStrategyProvider) {
        return new SemaphoreThrottleService(clock, executionManager, taskScheduler, throttleStrategyProvider);
    }

    @Bean(initMethod = "initialize")
    ResourceThrottleStrategyProvider throttleStrategyProvider(Clock clock, Environment environment,
                                                              OperatingSystemService operatingSystemService) {
        return new ResourceThrottleStrategyProvider(clock, environment, operatingSystemService);
    }

    @Bean
    TransactionGitProcessConfigurer transactionGitProcessConfigurer(TransactionManager transactionManager) {
        return new TransactionGitProcessConfigurer(transactionManager);
    }

    @Bean
    TranscodeGitProcessBuilderConfigurer transcodeProcessBuilderConfigurer(TranscodeServer transcodeServer) {
        return new TranscodeGitProcessBuilderConfigurer(transcodeServer);
    }

    @Bean(destroyMethod = "stop", initMethod = "start")
    DefaultTranscodeServer transcodeServer(ApplicationConfig config, ExecutorService executorService,
                                           StorageLayout storageLayout) {
        TranscodeConfig transcode = config.getTranscode();
        return new DefaultTranscodeServer(executorService, new DefaultTranscoder(storageLayout),
                transcode.getAddress(), transcode.getPort());
    }

    @Bean
    UpdateRefsContextHolder updateRefsContextHolder(ExecutionManager executionManager) {
        return new DefaultUpdateRefsContextHolder(executionManager);
    }

    @Nonnull
    private static Map<String, String> metricCommonTags(Environment environment) {
        return Binder.get(environment)
                .bind("metrics.tags", Bindable.mapOf(String.class, String.class))
                .orElseGet(Collections::emptyMap);
    }

    @Configuration
    @EnableConfigurationProperties
    @PropertySource("classpath:build.properties")
    public static class BuildInfoWiring {

        @Bean
        @ConfigurationProperties(prefix = "build")
        public BuildProperties buildProperties() {
            return new BuildProperties();
        }

        @Bean
        public BuildInfo buildInfo() {
            return new BuildInfo(buildProperties());
        }

        /**
         * Logs application build information at startup and shutdown
         *
         * @return {@link BuildInfoLogger}
         */
        @Bean(initMethod = "start", destroyMethod = "stop")
        @Order(LifecyclePhases.BOOTSTRAP)
        public BuildInfoLogger buildInfoLogger() {
            return new BuildInfoLogger(buildInfo());
        }
    }

    @Configuration
    @Profile(PROFILE_CLOUD)
    public static class CloudWiring {

        @Bean //Always enabled in the "cloud" profile, at least for now
        public PathDataStore pathDataStore(@Lazy GitHookService hookService, StorageLayout storageLayout) {
            //Passing GitHookService and StorageLayout here means PathRepositories will run the system's
            //standard hooks, rather than whatever hooks are in the repository's own "hooks" directory
            return new PathDataStore(null, hookService, storageLayout);
        }
    }

    @Configuration
    @Profile(PROFILE_SIDECAR)
    public static class SidecarWiring {

        @Bean
        @ConfigurationProperties(prefix = "bitbucket")
        public BitbucketProperties bitbucketProperties() {
            return new BitbucketProperties();
        }

        @Bean
        public BitbucketServerDataStore bitbucketServerDataStore(BitbucketProperties bitbucketProperties,
                                                                 @Lazy GitHookService hookService,
                                                                 StorageLayout storageLayout) {
            return new BitbucketServerDataStore(bitbucketProperties, hookService, storageLayout);
        }

        @Bean
        public MigrationStateManager migrationStateManager(BitbucketProperties bitbucketProperties) {
            return new DefaultMigrationStateManager(bitbucketProperties.getSharedHome().resolve("data/hierarchies"));
        }

        @Bean
        @ConditionalOnProperty(prefix = "mesh.data-stores", name = "allow-paths", havingValue = "true")
        public PathDataStore pathDataStore() {
            return new PathDataStore(null);
        }

        @Bean
        @Order(ORDER_TX_LISTENER_MESH_MIGRATION)
        public SidecarMigrationTransactionListener sidecarMigrationTransactionListener(
                MigrationStateManager stateManager) {
            return new SidecarMigrationTransactionListener(stateManager);
        }
    }

    @Configuration
    @Profile(PROFILE_STANDALONE)
    public static class StandaloneWiring {

        @Bean(initMethod = "validate")
        @DependsOn("homeLockAcquirer")
        public HomeValidator homeValidator(ApplicationDirs applicationDirs, BuildInfo buildInfo) {
            Version version = new Version(buildInfo.getBuildVersion());
            return new HomeValidator(applicationDirs.getHomeDir(), version);
        }

        @Bean
        public ManagedDataStore managedDataStore(ApplicationDirs applicationDirs, BuildInfo buildInfo,
                                                 @Lazy GitHookService hookService) {
            return new ManagedDataStore(new Version(buildInfo.getBuildVersion()), hookService,
                    applicationDirs.getDataDirs());
        }

        @Bean(destroyMethod = "shutdown")
        public ManagedDataStoreCleanupManager managedDataStoreCleanupManager(ApplicationConfig config,
                                                                             ManagedDataStore managedDataStore,
                                                                             MeshManager meshManager) {
            StorageConfig storageConfig = config.getStorageConfig();
            ScheduledExecutorService cleanupExecutor =
                    createScheduledExecutor(storageConfig.getCleanupThreads(), createDaemonThreadFactory(EXECUTOR_DATASTORE_CLEANUP));

            return new ManagedDataStoreCleanupManager(storageConfig.getPartitionCleanupDelay(), cleanupExecutor,
                    managedDataStore, meshManager);
        }

        @Bean
        @ConditionalOnProperty(prefix = "mesh.data-stores", name = "allow-paths", havingValue = "true")
        public PathDataStore pathDataStore() {
            return new PathDataStore(null);
        }

        @Bean
        public StorageManager storageManager(ApplicationConfig config, StorageLayout storageLayout) {
            return new DefaultStorageManager(config.getStorageConfig(), storageLayout);
        }
    }
}
