package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.app.LifecyclePhases;
import com.atlassian.bitbucket.mesh.management.SupportInfoProvider;
import com.atlassian.bitbucket.mesh.spring.AbstractSmartLifecycle;
import com.fasterxml.jackson.core.JsonGenerator;
import org.springframework.context.SmartLifecycle;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;

import static com.atlassian.bitbucket.mesh.util.MoreCollectors.toImmutableList;
import static com.atlassian.bitbucket.mesh.util.MoreCollectors.toImmutableSet;
import static java.util.Objects.requireNonNull;

/**
 * A default implementation of {@link DataStoreManager} which delegates to specialized {@link DataStore}s to
 * handle storage.
 * <p>
 * When a {@code DefaultDataStoreManager} is constructed, it is initialized with one ore more of the following
 * possible {@link DataStore} types:
 * <ul>
 *     <li>{@link BitbucketServerDataStore}</li>
 *     <li>{@link ManagedDataStore}</li>
 *     <li>{@link PathDataStore}</li>
 * </ul>
 * This implementation integrates with the {@link SmartLifecycle application lifecycle} to automatically start
 * and stop the configured {@link DataStore}s at the appropriate time.
 */
public class DefaultDataStoreManager
        extends AbstractSmartLifecycle
        implements DataStoreManager, SupportInfoProvider {

    private final RepositoryVisitor<StorageStatus> attacher;
    private final BitbucketServerDataStore bitbucketServerDataStore;
    private final ManagedDataStore managedDataStore;
    private final List<DataStore> stores;

    /**
     * Constructs a new {@code DefaultDataStoreManager} which can delegate to the provided {@link DataStore}s. Any
     * of the {@link DataStore} types can be omitted, but at least one must be provided.
     *
     * @param bitbucketServerDataStore provides access to repositories in the shared home, or legacy data stores
     * @param managedDataStore         provides access to repositories in Mesh's native storage format
     * @param pathDataStore            provides access to repositories specified using arbitrary paths
     * @throws IllegalStateException if all of the {@link DataStore}s are {@code null}
     */
    public DefaultDataStoreManager(@Nullable BitbucketServerDataStore bitbucketServerDataStore,
                                   @Nullable ManagedDataStore managedDataStore,
                                   @Nullable PathDataStore pathDataStore) {
        if (bitbucketServerDataStore == null && managedDataStore == null && pathDataStore == null) {
            throw new IllegalStateException("There are no DataStores available");
        }

        //Fields do not exist for the other types because they're only used by the attacher visitor
        this.bitbucketServerDataStore = bitbucketServerDataStore;
        this.managedDataStore = managedDataStore;

        attacher = new ConcreteRepositoryVisitor<StorageStatus>() {

            @Override
            public StorageStatus defaultValue(Repository repository) {
                return StorageStatus.NO_STORE;
            }

            @Override
            StorageStatus visit(@Nonnull DefaultBitbucketServerRepository repository) {
                return bitbucketServerDataStore == null ?
                        StorageStatus.NO_STORE :
                        bitbucketServerDataStore.attachStorage(repository);
            }

            @Override
            StorageStatus visit(@Nonnull DefaultManagedRepository repository) {
                return managedDataStore == null ? StorageStatus.NO_STORE : managedDataStore.attachStorage(repository);
            }

            @Override
            StorageStatus visit(@Nonnull DefaultPathRepository repository) {
                return pathDataStore == null ? StorageStatus.NO_STORE : pathDataStore.attachStorage(repository);
            }
        };
        stores = Stream.of(bitbucketServerDataStore, managedDataStore, pathDataStore)
                .filter(Objects::nonNull)
                .collect(toImmutableList());
    }

    /**
     * Adds a new shard to the {@link BitbucketServerDataStore} if one is available. Otherwise, the new shard is not
     * added. Notably, this does <i>not</i> support adding shards to the {@link ManagedDataStore}.
     *
     * @param parameters parameters describing the shard to add
     * @return {@code true} if a {@link BitbucketServerDataStore} is available and the shard was added successfully;
     *         otherwise, {@code false}
     */
    @Override
    @Profiled
    public boolean addShard(@Nonnull AddShardParameters parameters) {
        return bitbucketServerDataStore != null && bitbucketServerDataStore.addShard(parameters);
    }

    /**
     * Uses a {@link RepositoryVisitor} to determine the correct {@link DataStore} for the provided {@link Repository}
     * and attach storage.
     *
     * @param repository the repository to attach storage to
     * @return {@link StorageStatus#NO_STORE} if no appropriate {@link DataStore} is available; otherwise, the result
     *         of attaching the storage is returned
     */
    @Nonnull
    @Override
    public StorageStatus attachStorage(@Nonnull Repository repository) {
        return requireNonNull(repository, "repository").accept(attacher);
    }

    @Nonnull
    @Override
    public List<DataStoreInfo> getInfo() {
        return stores.stream().map(DataStore::getInfo).collect(toImmutableList());
    }

    @Override
    public int getPhase() {
        return LifecyclePhases.DATA_STORES;
    }

    @Override
    @Nonnull
    public Set<Path> getWorkTreesDirs() {
        return stores.stream().flatMap(store -> store.getWorkTreesDirs().stream()).collect(toImmutableSet());
    }

    @Override
    public void start() {
        stores.forEach(DataStore::start);

        super.start();
    }

    @Override
    public void stop() {
        super.stop();

        stores.forEach(DataStore::stop);
    }

    @Override
    public void streamRepositories(@Nonnull String partition, @Nonnull String hierarchyId,
                                   @Nonnull Consumer<Repository> callback) {
        requireNonNull(partition, "partition");
        requireNonNull(hierarchyId, "hierarchyId");
        requireNonNull(callback, "callback");

        if (managedDataStore != null) {
            managedDataStore.streamRepositories(partition, hierarchyId, callback);
        }
    }

    @Override
    public void writeSupportInfo(@Nonnull JsonGenerator json) throws IOException {
        json.writeArrayFieldStart("data-stores");
        try {
            for (DataStore store : stores) {
                store.writeSupportInfo(json);
            }
        } finally {
            json.writeEndArray();
        }
    }
}
