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

import com.atlassian.bitbucket.mesh.git.Reflog;
import com.atlassian.bitbucket.mesh.git.exception.CommandTimeoutException;
import com.atlassian.bitbucket.mesh.gprc.GrpcSupport;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.io.PathWithAttributes;
import com.atlassian.bitbucket.mesh.process.ProcessSummary;
import com.atlassian.bitbucket.mesh.repository.ManagedRepository;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.atlassian.bitbucket.mesh.repository.RepositoryVisitor;
import com.atlassian.bitbucket.mesh.util.SatiableConsumer;

import javax.annotation.Nonnull;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;

import static com.atlassian.bitbucket.mesh.git.GitConstants.PATH_LOGS;
import static java.util.Objects.requireNonNull;

public class ReflogStreamingVisitor implements RepositoryVisitor<Void> {

    private final SatiableConsumer<Reflog> callback;
    private final StreamReflogsParameters parameters;

    public ReflogStreamingVisitor(@Nonnull StreamReflogsParameters parameters,
                                  @Nonnull SatiableConsumer<Reflog> callback) {
        this.callback = requireNonNull(callback, "callback");
        this.parameters = requireNonNull(parameters, "parameters");
    }

    /**
     * Streams all the reflogs for the provided repository to the provided {@code callback}. This method combines the
     * reflogs located in {@code shared-objects} and the reflogs located in the regular repository and de-duplicates
     * them.
     * <p>
     * If a reflog exists in both places, the 'regular' reflog should always be preferred because the next vacuuming
     * run would replace the shared-objects reflog anyway. In some cases, a reflog will only exist in one place:
     * <ul>
     *     <li>in the repository itself - if the reflog has been newly created since the last vacuuming run.</li>
     *     <li>in {@code shared-objects} - if the reflog was pruned from the repository in a vacuuming run;
     *     this is done for pull request reflogs for closed pull requests.</li>
     * </ul>
     *
     * @param repository the managed repository
     * @return {@code null}
     */
    @Override
    public Void visit(@Nonnull ManagedRepository repository) {
        Instant deadline = GrpcSupport.getDeadline(Duration.ofSeconds(parameters.getExecutionTimeout()));

        Path reflogDir = repository.getPath().resolve(PATH_LOGS);
        Path sharedLogDir = repository.getSharedObjects().getReflogDir(repository);
        Iterator<PathWithAttributes> it = MoreFiles.iterateFileTreeSorted(reflogDir);
        Iterator<PathWithAttributes> sharedIt = MoreFiles.iterateFileTreeSorted(sharedLogDir);

        Reflog next = null;
        Reflog nextShared = null;
        while (next != null || it.hasNext() || nextShared != null || sharedIt.hasNext()) {
            checkTimeout(deadline);
            while (next == null && it.hasNext()) {
                next = toReflog(reflogDir, it.next());
            }
            while (nextShared == null && sharedIt.hasNext()) {
                nextShared = toReflog(sharedLogDir, sharedIt.next());
            }

            Reflog toEmit = null;
            if (next != null || nextShared != null) {
                if (next == null) {
                    //nextShared is a shared reflog that does not exist in the regular repository
                    toEmit = nextShared;
                    nextShared = null;
                } else if (nextShared == null) {
                    //next is a reflog that does not exist in the shared repository
                    toEmit = next;
                    next = null;
                } else {
                    //Both next and nextShared are non-null
                    int compare = next.getRefId().compareTo(nextShared.getRefId());
                    if (compare == 0) {
                        //If a reflog exists both in the shared-objects and the regular repository,
                        //the regular repository should win.
                        toEmit = next;
                        next = nextShared = null;
                    } else if (compare < 0) {
                        toEmit = next;
                        next = null;
                    } else {
                        toEmit = nextShared;
                        nextShared = null;
                    }
                }
            }
            if (toEmit != null && callback.accept(toEmit)) {
                //Callback is done
                break;
            }
        }
        return null;
    }

    @Override
    public Void defaultValue(Repository repository) {
        Instant deadline = GrpcSupport.getDeadline(Duration.ofSeconds(parameters.getExecutionTimeout()));

        Path reflogDir = repository.getPath().resolve(PATH_LOGS);
        Iterator<PathWithAttributes> it = MoreFiles.iterateFileTreeSorted(reflogDir);
        while (it.hasNext()) {
            checkTimeout(deadline);
            Reflog reflog = toReflog(reflogDir, it.next());
            if (reflog != null && callback.accept(reflog)) {
                //Callback is done
                break;
            }
        }
        return null;
    }

    private static void checkTimeout(Instant deadline) {
        if (Instant.now().isAfter(deadline)) {
            throw new CommandTimeoutException("Timed out while streaming reflogs",
                    new ProcessSummary.Builder("stream reflogs")
                            .timedOut(true)
                            .build());
        }
    }

    private static Reflog toReflog(Path reflogDir, PathWithAttributes value) {
        BasicFileAttributes attrs = value.getAttributes();
        if (attrs.isRegularFile()) {
            return new Reflog(reflogDir.relativize(value.getPath()).toString(),
                    attrs.size(), attrs.lastModifiedTime().toInstant());
        }
        return null;
    }
}
