package com.atlassian.bitbucket.mesh.repository;

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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Set;

import static java.util.Objects.requireNonNull;

/**
 * A {@link DataStore} implementation that accepts {@link Repository#getId() repository IDs} of the form
 * {@code path:<path-on-disk>}, as long as the path starts with the configured root directory - if one was provided.
 */
public class PathDataStore implements DataStore {

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

    private final GitHookService hookService;
    private final Path hooksDir;
    private final Path root;

    /**
     * Constructs a new {@code PathDataStore}, optionally restricting paths to a specified root directory.
     * <p>
     * If no {@code root} is specified, any path is accepted. Otherwise, {@link #attachStorage} will only
     * attach for repositories whose path is somewhere under the specified {@code root} and will return
     * {@link StorageStatus#NO_STORE NO_STORE} for paths outside it.
     * <p>
     * When the store is constructed this way, {@link PathRepository} instances will use whatever hooks are
     * in their own {@code hooks} directories. The system's standard hooks will not be invoked.
     *
     * @param root the (optional) store root to use to restrict repository paths
     */
    public PathDataStore(@Nullable Path root) {
        this.root = root;

        hooksDir = null;
        hookService = null;
    }

    /**
     * Constructs a new {@code PathDataStore}, optionally restricting paths to a specified root directory.
     * <p>
     * If no {@code root} is specified, any path is accepted. Otherwise, {@link #attachStorage} will only
     * attach for repositories whose path is somewhere under the specified {@code root} and will return
     * {@link StorageStatus#NO_STORE NO_STORE} for paths outside it.
     * <p>
     * When the store is constructed this way, {@link PathRepository} instances will use the system's standard
     * hooks instead of whatever hooks are in their own {@code hooks} directories. This means, for example,
     * pushes via {@code HostingService} will trigger {@code pre-receive} and {@code post-receive} fragments,
     * and clones will be able to leverage the pack cache.
     *
     * @param root          the (optional) store root to use to restrict repository paths
     * @param hookService   the hook service, used to install shared hooks
     * @param storageLayout the storage layout, used to find the {@code bin} directory
     */
    public PathDataStore(@Nullable Path root, @Nonnull GitHookService hookService,
                         @Nonnull StorageLayout storageLayout) {
        this.hookService = requireNonNull(hookService, "hookService");
        this.root = root;

        hooksDir = MoreFiles.mkdir(requireNonNull(storageLayout, "storageLayout").getBinDir(), "git-hooks");
    }

    @Nonnull
    @Override
    public DataStoreInfo getInfo() {
        return new DataStoreInfo(DataStoreType.PATH,
                Collections.singletonList(new ShardInfo.Builder(String.valueOf(root), getUsableSpace()).build()));
    }

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

    @Override
    @Nonnull
    public Set<Path> getWorkTreesDirs() {
        //PathRepositories do not support work trees, so return empty set
        return Collections.emptySet();
    }

    @Override
    public void start() {
        if (hooksDir == null || hookService == null) {
            return;
        }

        try {
            hookService.installHooks(hooksDir);
        } catch (IOException e) {
            log.warn("Failed to install hooks in {}", hooksDir, e);
        }
    }

    @Override
    public String toString() {
        return "PathDataStore{root=" + root + '}';
    }

    @Override
    public void writeSupportInfo(@Nonnull JsonGenerator json) throws IOException {
        json.writeStartObject();
        try {
            json.writeStringField("type", "path");
        } finally {
            json.writeEndObject();
        }
    }

    @Nonnull
    StorageStatus attachStorage(@Nonnull DefaultPathRepository repository) {
        if (isWithinStore(requireNonNull(repository, "repository").getPath())) {
            if (hooksDir != null) {
                //If shared hooks have been set up for this DataStore, override the default hooksDir for
                //the PathRepository to use the standard hooks instead
                repository.setHooksDir(hooksDir);
            }

            return StorageStatus.ATTACHED;
        }

        return StorageStatus.NO_STORE;
    }

    private long getUsableSpace() {
        try {
            if (root != null) {
                return Files.getFileStore(root).getUsableSpace();
            }
        } catch (IOException e) {
            log.warn("Error while finding usable space for path '{}'", root, e);
            return -1L;
        }
        return -2L;
    }

    private boolean isWithinStore(@Nonnull Path path) {
        return (root == null || path.startsWith(root)) && Files.isDirectory(path);
    }
}
