package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.annotation.Profiled;
import com.atlassian.bitbucket.mesh.config.LfsConfig;
import com.atlassian.bitbucket.mesh.git.exception.LfsStorageException;
import com.atlassian.bitbucket.mesh.git.exception.ObjectNotFoundException;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

/**
 * Given a Mesh {@code DataStore} this class manages file level operations specific to Git LFS.
 */
@Profiled
public class DefaultLfsManager implements LfsManager {

    private static final Logger log = LoggerFactory.getLogger(DefaultLfsManager.class);
    private static final Pattern SHA256_PATTERN = Pattern.compile("[a-f0-9]{64}");

    private final LfsConfig config;

    public DefaultLfsManager(LfsConfig config) {
        this.config = config;
    }

    @Override
    public boolean canStore(@Nonnull Repository repository, long size) {
        Path lfsDir = requireNonNull(repository, "repository").getLfsDir();

        try {
            //If no objects have been uploaded for this repository in the past its LFS directory may not
            //actually exist on disk, so this uses the parent directory instead
            long usableSpace = Files.getFileStore(lfsDir.getParent()).getUsableSpace();

            return usableSpace - config.getMinimumFreeSpace() > size;
        } catch (IOException e) {
            log.warn("{}: Error checking usable/free space", repository.getSlug(), e);
            return false;
        }
    }

    @Override
    public boolean objectExists(@Nonnull Repository repository, @Nonnull String oid) {
        requireNonNull(repository, "repository");
        requireOid(oid);

        return MoreFiles.canOpen(getObjectPath(repository, oid));
    }

    @Nonnull
    @Override
    public LfsInputStreamSupplier readObject(@Nonnull Repository repository, @Nonnull String oid) {
        Path objectPath = getObjectPathOrThrow(repository, oid);
        return new LfsInputStreamSupplier(objectPath);
    }

    @Nonnull
    @Override
    public LfsInputStreamSupplier readObject(@Nonnull Repository repository, @Nonnull String oid,
                                             long start, long end) {
        Path objectPath = getObjectPathOrThrow(repository, oid);
        return new LfsInputStreamSupplier(objectPath, start, end);
    }

    @Nonnull
    @Override
    public LfsObjectWriter writeObject(@Nonnull Repository repository, @Nonnull String oid) {
        try {
            Path objectFile = getObjectPath(repository, oid);   // <hierarchy>/<sha256[0:1]>/<sha256[2:63]>
            MoreFiles.mkdir(objectFile.getParent());            // <hierarchy>/<sha256[0:1]>/
            return new DefaultLfsObjectWriter(oid, objectFile);
        } catch (IOException e) {
            log.error("{}: Error writing object {}", repository, oid, e);
            throw new LfsStorageException(e);
        }
    }

    private static Path getObjectPath(@Nonnull Repository repository, @Nonnull String oid) {
        return repository.getLfsDir().resolve(oid.substring(0, 2)).resolve(oid.substring(2));
    }

    private static Path getObjectPathOrThrow(@Nonnull Repository repository, @Nonnull String oid) {
        Path objectPath = getObjectPath(repository, oid);
        if (MoreFiles.canOpen(objectPath)) {
            return objectPath;
        }
        throw new ObjectNotFoundException(oid);
    }

    /**
     * The OID may be validated at the service layer but here where we build paths we also validate as a
     * backstop against path traversal attacks since it is user provided input
     */
    private static void requireOid(String value) {
        if (value == null || !SHA256_PATTERN.matcher(value).matches()) {
            throw new ObjectNotFoundException(value != null ? value : "null");
        }
    }
}