package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.app.BitbucketProperties;
import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.layout.StorageLayout;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static java.util.Objects.requireNonNull;

public class BitbucketServerDataStore implements DataStore {

    private static final Logger log = LoggerFactory.getLogger(BitbucketServerDataStore.class);

    private final GitHookService hookService;
    private final ConcurrentMap<String, BitbucketServerShard> shardsById;
    private final SharedHomeBitbucketServerShard sharedHome;
    private final Path workTreesDir;

    public BitbucketServerDataStore(BitbucketProperties properties, GitHookService hookService,
                                    StorageLayout storageLayout) {
        this.hookService = hookService;

        workTreesDir = MoreFiles.mkdir(storageLayout.getTempDir(), "work-trees");
        sharedHome = new SharedHomeBitbucketServerShard(properties.getSharedHome(), workTreesDir);

        shardsById = new ConcurrentHashMap<>();
        shardsById.put(SharedHomeBitbucketServerShard.ID, sharedHome);
        properties.getDataStores().forEach((shardId, rootDir) ->
                shardsById.computeIfAbsent(shardId, id -> createShard(id, rootDir)));
    }

    @Nonnull
    @Override
    public DataStoreInfo getInfo() {
        ImmutableList.Builder<ShardInfo> builder = ImmutableList.builder();
        shardsById.forEach((id, shard) -> builder.add(new ShardInfo.Builder(shard.rootDir.toString(), getUsableSpace(shard))
                .build()));
        return new DataStoreInfo(DataStoreType.BITBUCKET, builder.build());
    }

    @Nonnull
    @Override
    public DataStoreType getType() {
        return DataStoreType.BITBUCKET;
    }

    @Override
    @Nonnull
    public Set<Path> getWorkTreesDirs() {
        return Collections.singleton(workTreesDir);
    }

    @Override
    public void start() {
        shardsById.values().forEach(shard -> shard.start(hookService));
    }

    @Override
    public String toString() {
        return "BitbucketServerDataStore{sharedHome=" + sharedHome.getRootDir() +
                ", " + (shardsById.size() - 1) + " additional shards}";
    }

    @Override
    public void writeSupportInfo(@Nonnull JsonGenerator json) throws IOException {
        json.writeStartObject();
        try {
            json.writeStringField("type", "bitbucket-server");
            json.writeArrayFieldStart("shards");
            try {
                for (BitbucketServerShard shard : shardsById.values()) {
                    shard.writeSupportInfo(json);
                }
            } finally {
                json.writeEndArray();
            }
        } finally {
            json.writeEndObject();
        }
    }

    /**
     * Adds a new {@link AdditionalBitbucketServerShard} to the data store, if the specified
     * {@link AddShardParameters#getId ID} has not already been used.
     *
     * @param parameters parameters describing the shard to add
     * @return {@code true} if the shard was attached; otherwise, {@code false} if the ID is already in use
     */
    boolean addShard(@Nonnull AddShardParameters parameters) {
        String id = requireNonNull(parameters, "parameters").getId();

        //To avoid racily adding two shards with the same ID, the shard is always created and then
        //the result of putIfAbsent is used to decide whether the shard was added. If it was, then
        //it's started; otherwise, it's left unused to go out of scope
        BitbucketServerShard created = createShard(id, parameters.getPath());
        if (shardsById.putIfAbsent(id, created) == null) {
            created.start(hookService);

            return true;
        }

        return false;
    }

    @Nonnull
    StorageStatus attachStorage(@Nonnull DefaultBitbucketServerRepository repository) {
        requireNonNull(repository, "repository");

        BitbucketServerShard shard = shardsById.get(repository.getShardId());
        if (shard == null) {
            return StorageStatus.NO_STORE;
        }

        return shard.attachStorage(repository);
    }

    private AdditionalBitbucketServerShard createShard(String id, Path rootDir) {
        return new AdditionalBitbucketServerShard(id, rootDir, sharedHome.getTemplateDir(), workTreesDir);
    }

    private long getUsableSpace(BitbucketServerShard shard) {
        try {
            return shard.getUsableSpace();
        } catch (IOException e) {
            log.warn("Error while finding information about shard {}", shard, e);
            return -1L;
        }
    }
}
