package com.atlassian.bitbucket.mesh.git.cache;

import com.atlassian.bitbucket.mesh.config.PackCacheConfig;
import com.atlassian.bitbucket.mesh.event.EventListener;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.MaybeRefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.RefsUpdatedEvent;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryDeletedEvent;
import com.atlassian.util.contentcache.CacheStatistics;
import com.atlassian.util.contentcache.ContentCache;
import com.atlassian.util.contentcache.ContentCacheManager;
import com.atlassian.util.contentcache.ContentCacheStatistics;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;

/**
 * Implementation of {@link PackCacheService} that delegates down to the cache.
 */
public class DefaultPackCacheService implements PackCacheService {

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

    private final ContentCacheManager cacheManager;
    private final PackCacheConfig config;

    public DefaultPackCacheService(ContentCacheManager cacheManager, PackCacheConfig config) {
        this.cacheManager = cacheManager;
        this.config = config;
    }

    @Override
    public void clear() {
        internalClear();
    }

    @Override
    public void clear(@Nonnull Repository repository) {
        String region = getCacheRegion(repository);
        internalClear(region, null);
    }

    @Override
    public void clear(@Nonnull Repository repository, @Nonnull String cacheKey) {
        String region = getCacheRegion(repository);
        internalClear(region, requireNonNull(cacheKey, "cacheKey"));
    }

    @Override
    public Date createExpiryDate() {
        long ttl = config.getTimeToLive();
        return ttl > 0 ? new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttl)) : null;
    }

    public void destroy() {
        deleteCacheDir();
    }

    @Nonnull
    @Override
    public ContentCache getCache(@Nonnull Repository repository) {
        return cacheManager.getCache(getCacheRegion(repository));
    }

    @Override
    public int getMaxCount() {
        return config.getMaxCount();
    }

    @Nonnull
    @Override
    public CacheStatistics getStatistics() {
        return cacheManager.getStatistics();
    }

    @Override
    public ContentCacheStatistics getStatistics(@Nonnull Repository repository) {
        return getCache(repository).getStatistics();
    }

    public void init() {
        deleteCacheDir();
    }

    @Override
    public boolean isEnabledForHttp() {
        return config.isEnabledForHttp();
    }

    @Override
    public boolean isEnabledForSsh() {
        return config.isEnabledForSsh();
    }

    @EventListener
    public void onRefsUpdated(RefsUpdatedEvent event) {
        if (!(event instanceof MaybeRefsUpdatedEvent)) {
            //Only clear the repository cache when we're certain that one of more refs have been updated.
            clear(event.getRepository());
        }
    }

    @EventListener
    public void onRepositoryDeleted(RepositoryDeletedEvent event) {
        clear(event.getRepository());
    }

    @VisibleForTesting
    static String getCacheRegion(@Nonnull Repository repository) {
        return requireNonNull(repository, "repository").getSlug();
    }

    private void deleteCacheDir() {
        Path cacheDir = config.getCacheDir();
        try {
            MoreFiles.deleteRecursively(cacheDir);
        } catch (IOException e) {
            log.warn("Could not clear {} directory: {}", cacheDir.getFileName(), e.getMessage(),
                    log.isDebugEnabled() ? e : null);
        }
    }

    private void internalClear() {
        cacheManager.clear();
    }

    private void internalClear(String region, String key) {
        ContentCache cache = cacheManager.getCache(region);
        if (key == null) {
            cache.clear();
        } else if (key.endsWith("*")) {
            String prefix = key.substring(0, key.length() - 1);
            Set<String> toPrune = cache.getEntries().keySet().stream()
                .filter(k -> k.startsWith(prefix))
                .collect(Collectors.toSet());
            toPrune.forEach(cache::remove);
        } else {
            cache.remove(key);
        }
    }
}
