package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.git.hook.GitHookService;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.fasterxml.jackson.core.JsonGenerator;

import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;

import static com.atlassian.bitbucket.mesh.util.ResourceUtils.unpackTar;
import static java.util.Objects.requireNonNull;

abstract class BitbucketServerShard extends AbstractShard {

    private final String id;
    private final Path workTreesDir;

    BitbucketServerShard(@Nonnull String id, @Nonnull Path rootDir, @Nonnull Path workTreesDir) {
        super(rootDir);

        this.id = requireNonNull(id, "id");
        this.workTreesDir = requireNonNull(workTreesDir, "workTreesDir");
    }

    @Nonnull
    StorageStatus attachStorage(@Nonnull DefaultBitbucketServerRepository repository) {
        repository.attachTo(this);

        try {
            BasicFileAttributes attributes = Files.readAttributes(repository.getPath(), BasicFileAttributes.class);
            if (attributes.isDirectory()) {
                //If the directory exists, check whether the repository has been marked as deleted and
                //return the status accordingly
                return Files.exists(repository.getDeletedMarker()) ? StorageStatus.DELETED : StorageStatus.ATTACHED;
            }

            //If there's something other than a directory at the target path, the repository is occluded
            return StorageStatus.OCCLUDED;
        } catch (FileNotFoundException | NoSuchFileException e) {
            return StorageStatus.NONEXISTENT;
        } catch (IOException e) {
            //If we can't check the path, return UNKNOWN status to allow the caller to decide whether to
            //proceed or preemptively fail
            log.warn("{}: Failed to check repository path: {}", repository.getId(), repository.getPath());
            return StorageStatus.UNKNOWN;
        }
    }

    @Nonnull
    String getId() {
        return id;
    }

    @Nonnull
    abstract String getLayoutTar();

    @Nonnull
    abstract Path getLfsDir(@Nonnull String hierarchyId);

    @Nonnull
    abstract Path getRepositoriesDir(@Nonnull String hierarchyId);

    @Nonnull
    @Override
    ShardState getState() {
        return ShardState.AVAILABLE;
    }

    @Nonnull
    Path getWorkTreesDir() {
        return workTreesDir;
    }

    void start(@Nonnull GitHookService hookService) {
        Instant localTime = Instant.now();
        Instant start;
        try {
            start = MoreFiles.getFileSystemTime(rootDir);
            if (Math.abs(start.toEpochMilli() - localTime.toEpochMilli()) > 10_000L) {
                log.warn("The file server clock for {} diverges from the local clock by more than 10s ({} vs {})!",
                        rootDir, start, localTime);
            }
        } catch (IOException e) {
            log.warn("Could not determine file system time for {}. Using local clock instead", rootDir, e);
            start = localTime;
        }
        try {
            //Unpack the layout tar, which writes standard files to the shard. These standard
            //files may be updated when the system is upgraded, so this happens every startup
            unpackTar(getLayoutTar(), rootDir);
            //Once the layout has been applied, install hooks
            hookService.installHooks(getHooksDir());
        } catch (IOException e) {
            log.warn("{}: Failed to initialize shard at {}", id, rootDir, e);
        }

        pruneStaleConfig(start, getHooksDir());
    }

    @Override
    void writeExtraSupportInfo(JsonGenerator json) throws IOException {
        json.writeStringField("id", getId());
    }
}
