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.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.repository.ManagedRepository;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.SharedObjectsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.time.Duration;
import java.time.Instant;
import java.util.stream.Stream;

/**
 * Performs garbage collection in a {@code shared-objects} repository, which is the same as a {@link StandardGc}
 * preceded by a {@link VacuumOperation} for each repository in the same hierarchy that has changes that have not been
 * synchronized yet.
 */
class SharedObjectsGc implements GcOperation {

    /**
     * The name of the marker file to track the last time the repository was vacuumed. This file is created in the
     * {@link GitRepositoryLayout#getApplicationInfoDir app-info} directory.
     */
    static final String VACUUM_TIMESTAMP = "vacuum.ts";

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

    private final GitProcessBuilderFactory builderFactory;
    private final GcConfig config;
    private final SharedObjectsRepository repository;
    private final GitRepositoryLayout repositoryLayout;
    private final GcOperationScheduler scheduler;

    SharedObjectsGc(GitProcessBuilderFactory builderFactory, GcConfig config,
                    SharedObjectsRepository repository, GitRepositoryLayout repositoryLayout,
                    GcOperationScheduler scheduler) {
        this.builderFactory = builderFactory;
        this.config = config;
        this.repository = repository;
        this.repositoryLayout = repositoryLayout;
        this.scheduler = scheduler;
    }

    @Override
    public void run(@Nonnull GcContext context) {
        if (!shouldRun()) {
            return;
        }

        Instant vacuumStart = Instant.now();
        try (Stream<Path> dirtyMarkers = Files.list(getDirtyMarkerDir(repository))) {
            dirtyMarkers.forEach(path -> {
                String localId = path.getFileName().toString();
                ManagedRepository repo = repository.getMember(localId);
                if (repo == null) {
                    //Repository no longer exists
                    new PruneDeletedRepositoryRefsOperation(localId, repository, repositoryLayout, path).run(context);
                } else {
                    new VacuumOperation(builderFactory, config, repo, repositoryLayout, path).run(context);
                }
            });

            //Mark the hierarchy as vacuumed
            try {
                MoreFiles.touch(getVacuumMarkerFile(repository), vacuumStart);
            } catch (IOException e) {
                log.warn("[{}] Could not update vacuuming marker file. Vacuuming will run more frequently.", repository);
            }
            if (log.isDebugEnabled()) {
                log.debug("[{}] Vacuuming completed in {}ms",
                        repository, Duration.between(vacuumStart, Instant.now()).toMillis());
            }
            context.addAction(GcActionType.VACUUM_HIERARCHY, vacuumStart.toEpochMilli());
        } catch (FileNotFoundException | NoSuchFileException e) {
            //The directory with dirty markers does not exists, which means that there's nothing to do
        } catch (IOException e) {
            log.warn("[{}] Vacuuming failed", repository, e);
            context.addAction(GcActionType.VACUUM_HIERARCHY, vacuumStart.toEpochMilli(), null, true);
            return;
        }

        if (context.isSuccess(GcActionType.VACUUM_HIERARCHY)) {
            //Only run standard garbage collection operations in the shared-objects repository if vacuuming ran
            //and succeeded; otherwise there are no relevant changes to the repository.
            //Note that we _schedule_ StandardGc to run as an independent operation here instead of running it inline.
            //This ensures that the shouldRun check and retry-on-failure behaviors works as expected. If we ran it
            //inline here and it failed, on retry, SharedObjectsGc.shouldRun would return false and standard gc would
            //not be retried.
            scheduler.maybeSchedule(repository, new StandardGc(builderFactory, config, repository, repositoryLayout));
        } else {
            log.debug("[{}] Not scheduling regular garbage collection because vacuuming did not complete successfully",
                    repository);
        }
    }

    @Override
    public boolean shouldRun() {
        return MoreFiles.getDurationSinceLastModified(getVacuumMarkerFile(repository))
                       .compareTo(config.getVacuumInterval()) >= 0;
    }

    private Path getDirtyMarkerDir(Repository repository) {
        return MoreFiles.mkdir(repositoryLayout.getApplicationInfoDir(repository), "dirty");
    }

    @Nonnull
    private Path getVacuumMarkerFile(Repository repository) {
        return repositoryLayout.getApplicationInfoDir(repository).resolve(VACUUM_TIMESTAMP);
    }
}
