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

import com.atlassian.bitbucket.mesh.config.GcConfig;
import com.atlassian.bitbucket.mesh.git.GitRepositoryLayout;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.google.common.io.CharStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;

import static com.atlassian.bitbucket.mesh.git.gc.DefaultGarbageCollectionManager.GC_LOG;
import static com.atlassian.bitbucket.mesh.io.MoreFiles.getDurationSinceLastModified;

class LockingGcOperation implements GcOperation {

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

    private final GcConfig config;
    private final GcOperation delegate;
    private final String processIdentifier;
    private final Repository repository;
    private final GitRepositoryLayout repositoryLayout;

    LockingGcOperation(Repository repository, GcConfig config, GcOperation delegate, String processIdentifier,
                       GitRepositoryLayout repositoryLayout) {
        this.config = config;
        this.delegate = delegate;
        this.processIdentifier = processIdentifier;
        this.repository = repository;
        this.repositoryLayout = repositoryLayout;
    }

    @Override
    public void run(@Nonnull GcContext context) {
        // Trigger creation of app-info directory. This relies on a side effect of the getter method.
        repositoryLayout.getApplicationInfoDir(repository);

        deleteLockIfStale(repository);

        try {
            repositoryLayout.editFile(repository, GC_LOG, (reader, writer) -> {
                Path pidFile = repositoryLayout.getGcPidFile(repository);
                try {
                    MoreFiles.write(pidFile, processIdentifier, StandardCharsets.UTF_8);
                } catch (IOException e) {
                    log.debug("[{}] Could not write PID information", repository, e);
                }

                try {
                    // The checks outside the lock are approximations. As a result, it's possible that no actions
                    // will be performed while the lock is held. If that happens, don't write to the log
                    delegate.run(context);
                    List<GcAction> actions = context.getActions();
                    if (!actions.isEmpty()) {
                        writer.write(Instant.now().getEpochSecond() + ": " + processIdentifier +
                                     " " + actions + '\n');
                    }
                    if (reader != null) {
                        // Preserve recent entries from the log
                        CharStreams.readLines(reader, new GcLogProcessor(writer, config.getLogRetention()));
                    }
                } finally {
                    MoreFiles.deleteQuietly(pidFile);
                }
            });
        } catch (IOException | OverlappingFileLockException e) {
            log.debug("[{}] Garbage collection is already running ({})", repository, getPid(repository));
        }
    }

    @Override
    public boolean shouldRun() {
        return delegate.shouldRun();
    }

    private void deleteLockIfStale(Repository repository) {
        Path lockFile = repository.getPath().resolve(GC_LOG + ".lock");
        if (Files.exists(lockFile) &&
            getDurationSinceLastModified(lockFile).compareTo(config.getRepackTimeout().multipliedBy(2L)) > 0) {
            try {
                Files.delete(lockFile);
                log.info("[{}] Deleted stale garbage collection lock file", repository);
            } catch (FileNotFoundException | NoSuchFileException ignored) {
                // Phantom lock file, perhaps due NFS lookup caching. Either way, if the lock is gone, move on
            } catch (IOException e) {
                log.warn("[{}] Failed to delete stale garbage collection lock file", repository);
            }
        }
    }

    private String getPid(Repository repository) {
        Path pidFile = repositoryLayout.getGcPidFile(repository);
        try {
            return MoreFiles.toString(pidFile, StandardCharsets.UTF_8);
        } catch (IOException e) {
            return "<unknown>";
        }
    }
}
