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

import com.atlassian.bitbucket.mesh.git.GitConstants;
import com.atlassian.bitbucket.mesh.io.MoreFiles;
import com.atlassian.bitbucket.mesh.io.SyncFileTreeCallback;
import com.atlassian.bitbucket.mesh.repository.ManagedRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Set;

import static com.atlassian.bitbucket.mesh.git.GitConstants.PATH_STASH_REFS;
import static java.lang.Long.parseLong;

public class VacuumRefLogsCallback implements SyncFileTreeCallback {

    private static final Path PATH_DARK_REFS = Paths.get(GitConstants.PATH_DARK_REFS);
    private static final int PULL_REQUEST_ID_INDEX = 2;
    private static final Logger log = LoggerFactory.getLogger(VacuumRefLogsCallback.class);

    private final boolean convertStashRefs;
    private final Path destinationPath;
    private final Set<Long> openPullRequestIds;
    private final ManagedRepository repository;
    private final Path sourcePath;
    private int syncedCount;
    private int mergedCount;

    public VacuumRefLogsCallback(ManagedRepository repository, Set<Long> openPullRequestIds, Path sourcePath,
                                 Path destinationPath,
                                 boolean convertStashRefs) {
        this.openPullRequestIds = openPullRequestIds;
        this.repository = repository;
        this.sourcePath = sourcePath;
        this.destinationPath = destinationPath;
        this.convertStashRefs = convertStashRefs;
    }

    public int getMergedCount() {
        return mergedCount;
    }

    public int getSyncedCount() {
        return syncedCount;
    }

    @Override
    public void onError(@Nonnull Path source, @Nonnull BasicFileAttributes sourceAttributes,
                        @Nonnull Path destination, @Nonnull IOException exception) {
        log.info("[{}] Could not vacuum {}", repository, source, exception);
    }

    @Override
    public void onConflict(@Nonnull Path source, @Nonnull BasicFileAttributes sourceAttributes,
                           @Nonnull Path destination) {
        try {
            new ReflogMerger(repository, source, destination).merge();
            mergedCount++;
            maybeDeleteSource(source);
            if (log.isTraceEnabled()) {
                log.trace("[{}] Merged reflog {}", repository, repository.getSharedObjects().getPath().relativize(destination));
            }
        } catch (NoSuchFileException | FileNotFoundException e) {
            log.info("[{}] Unable to merge reflog file {} with {}",
                    repository, repository.getSharedObjects().getPath().relativize(destination), repository.getPath().relativize(source),
                    log.isDebugEnabled() ? e : null);
        } catch (IOException e) {
            log.info("[{}] Unable to merge reflog file {} with {}",
                    repository, repository.getSharedObjects().getPath().relativize(destination), repository.getPath().relativize(source), e);
        }
    }

    @Override
    public void onMissingInSource(@Nonnull Path source, @Nonnull Path destination) {
        try {
            Files.delete(destination);
            if (log.isDebugEnabled()) {
                log.debug("[{}] Pruned reflog {}", repository, repository.getPath().relativize(destination));
            }
        } catch (NoSuchFileException e) {
            //If it's not there, no problem
        } catch (IOException e) {
            log.info("[{}] Failed to prune reflog {}: {}", repository, destination, e.getMessage());
        }
    }

    @Override
    public void onSynced(@Nonnull Path source, @Nonnull BasicFileAttributes sourceAttributes,
                         @Nonnull Path destination) {
        syncedCount++;
        maybeDeleteSource(source);
        if (convertStashRefs) {
            //Convert stash-refs to dark-refs if present
            Path relativePath = destinationPath.relativize(destination);
            if (relativePath.startsWith(PATH_STASH_REFS)) {
                Path darkRefsDestination = destinationPath.resolve(PATH_DARK_REFS)
                        .resolve(relativePath.subpath(1, relativePath.getNameCount()));
                try {
                    MoreFiles.mkdir(darkRefsDestination.getParent());
                    Files.move(destination, darkRefsDestination);
                } catch (FileAlreadyExistsException e) {
                    log.debug("[{}] Could not convert stash-ref ({}) to dark-ref: dark-ref already exists.",
                            repository, repository.getSharedObjects().getPath().relativize(destination));
                } catch (IOException e) {
                    log.info("[{}] Could not convert stash-ref ({}) to dark-ref",
                            repository, repository.getSharedObjects().getPath().relativize(destination), e);
                }
            }
        }
    }

    private Long getPullRequestIdFromPath(Path refLog) {
        Path path = sourcePath.relativize(refLog);
        if (PULL_REQUEST_ID_INDEX < path.getNameCount()) {
            try {
                return parseLong(path.getName(PULL_REQUEST_ID_INDEX).toString());
            } catch (NumberFormatException e) {
                return null;
            }
        }
        return null;
    }

    private void maybeDeleteSource(Path source) {
        if (openPullRequestIds != null) {
            Long pullRequestId = getPullRequestIdFromPath(source);
            if (pullRequestId != null && !openPullRequestIds.contains(pullRequestId)) {
                try {
                    Files.delete(source);
                    if (log.isTraceEnabled()) {
                        log.trace("[{}] Pruned fully vacuumed ref log {} because the pull request has been closed",
                                repository, repository.getPath().relativize(source));
                    }
                } catch (IOException e) {
                    log.info("[{}] Could not prune fully vacuumed ref log {}",
                            repository, repository.getPath().relativize(source), e);
                }
            }
        }
    }
}

