package com.atlassian.bitbucket.internal.scm.git.lfs.mirror;

import com.atlassian.bitbucket.internal.scm.git.lfs.embedded.LfsFileSystemHelper;
import com.atlassian.bitbucket.internal.scm.git.lfs.settings.GitLfsSettingsService;
import com.atlassian.util.contentcache.*;
import com.atlassian.util.contentcache.internal.AbstractContentCache;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;

/**
 * A specialization of {@link AbstractContentCache} that provides a durable read through cache for Git LFS.
 * <p>
 * This cache is durable in the sense that its on disk entries are only ever deleted in the case of a checksum failure.
 * Eviction is still supported to prevent excessive memory pressure. When an entry is evicted it is only
 * removed from the in memory representation and not the disk.
 * <p>
 * In this context read through means that "unknown" entries will be requested from the upstream and served to the
 * client in a transparent manner.
 * <p>
 * On disk cache entries are loaded into memory in a lazy manner when the entry is accessed.
 * The following steps are performed to determine where an entry should be loaded from.
 * <ol>
 * <li>Check the "entries" map for the cache key if an entry is found proceed as normal.</li>
 * <li>If an entry does not exist check the disk for a file corresponding to the cache key.</li>
 * <li>If a file exists then load it into the "entries" map using an {@link ExistingLfsCacheEntry}</li>
 * <li>If a file does not exist attempt to stream it from the upstream server.</li>
 * </ol>
 */
public class LfsContentCache extends AbstractContentCache {

    private final CacheExpiryStrategy cacheExpiryStrategy;
    private final ContentCacheManager contentCacheManager;
    private final EvictionStrategy evictionStrategy;
    private final LfsFileSystemHelper lfsFileSystemHelper;
    private final GitLfsSettingsService settingsService;

    public LfsContentCache(@Nonnull LfsFileSystemHelper lfsFileSystemHelper,
                           @Nonnull CacheExpiryStrategy cacheExpiryStrategy,
                           @Nonnull ContentCacheManager contentCacheManager, @Nonnull EvictionStrategy evictionStrategy,
                           @Nonnull GitLfsSettingsService settingsService, @Nonnull StreamPumper streamPumper) {
        super("lfs", cacheExpiryStrategy, streamPumper);
        this.lfsFileSystemHelper = lfsFileSystemHelper;
        this.cacheExpiryStrategy = cacheExpiryStrategy;
        this.contentCacheManager = contentCacheManager;
        this.evictionStrategy = evictionStrategy;
        this.settingsService = settingsService;
    }

    @Nonnull
    @Override
    public CacheAccess access(@Nonnull String key, @Nonnull OutputStream outputStream,
                              @Nonnull ContentProvider contentProvider) throws IOException {
        maybePopulateFromDisk(LfsCacheKey.lfsCacheKey(key));
        return super.access(key, outputStream, contentProvider);
    }

    @Nonnull
    @Override
    public CacheStreamAccess accessStream(@Nonnull String key, @Nonnull ContentProvider contentProvider)
            throws IOException {
        maybePopulateFromDisk(LfsCacheKey.lfsCacheKey(key));
        return super.accessStream(key, contentProvider);
    }

    @Nullable
    @Override
    protected CacheEntry newCacheEntry(@Nonnull String cacheKey, @Nonnull Date expiry) throws IOException {
        if (lfsFileSystemHelper.getFreeSpace() <= lfsFileSystemHelper.getMinimumFreeSpace()) {
            log.warn("Require {} bytes of free space to cache LFS file but there is only {} bytes available, " +
                            "bypassing mirror local storage and streaming directly from upstream",
                    lfsFileSystemHelper.getMinimumFreeSpace(), lfsFileSystemHelper.getFreeSpace());
            return null;
        }
        if (entries.size() > settingsService.getMirrorMaxInMemoryEntries()) {
            evictionStrategy.evict(settingsService.getMirrorEvictionEntries(), contentCacheManager);
        }
        return new LfsCacheEntry(cacheExpiryStrategy, LfsCacheKey.lfsCacheKey(cacheKey), lfsFileSystemHelper);
    }

    private boolean existsOnDisk(LfsCacheKey key) {
        return lfsFileSystemHelper.objectExists(key.hierarchyId(), key.oid());
    }

    private void maybePopulateFromDisk(LfsCacheKey key) {
        if (!entries.containsKey(key.cacheKey())) {
            //Check to see if we have it on disk.
            if (existsOnDisk(key)) {
                entries.putIfAbsent(key.cacheKey(), new ExistingLfsCacheEntry(key, cacheExpiryStrategy,
                        lfsFileSystemHelper.getObjectPath(key.hierarchyId(), key.oid()).toFile()));
            }
        }
    }
}
