package com.atlassian.stash.internal.scm.git.command;

import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.DeleteCommandParameters;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Deletes the specified repository on disk.
 * <p>
 * When forks are created, they have a single alternate which references their origin. When a repository is deleted,
 * if it still has forks, it is only marked for deletion on disk. Marked repositories are only cleaned up when their
 * entire hierarchy becomes empty. This could be improved, to allow dead sections of a deep fork tree to be cleaned
 * up independently, rather than waiting for the hierarchy to become empty, but the effort to do so is non-trivial.
 * For the moment, under the assumption fork trees will generally be broad rather than deep, this approach should be
 * sufficient.
 *
 * @since 2.4
 */
public class DeleteCommand extends SimpleGitCommand<Void> {

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

    private final GitScmConfig config;
    private final DeleteCommandParameters parameters;
    private final Repository repository;
    private final ScheduledExecutorService scheduledExecutorService;

    public DeleteCommand(@Nonnull GitScmConfig config, @Nonnull ScheduledExecutorService executorService,
                         @Nonnull Repository repository, @Nonnull DeleteCommandParameters parameters) {
        super(executorService);

        this.config = checkNotNull(config, "config");
        this.parameters = checkNotNull(parameters, "parameters");
        this.repository = checkNotNull(repository, "repository");
        this.scheduledExecutorService = executorService;
    }

    @Override
    public Void call() {
        final String hierarchyId = repository.getHierarchyId();

        File repositoryDir = config.getRepositoryDir(repository);
        if (parameters.hasForks()) {
            //If the repository has forks, because it cannot be known which of its objects they're borrowing, the
            //repository's directory cannot be removed. It can only be "marked" for deletion, so that when the
            //hierarchy itself is fully deleted the directory can be removed with all others in the hierarchy.
            File marker = new File(repositoryDir, "DELETED");
            try {
                FileUtils.writeStringToFile(marker, hierarchyId, "UTF-8");
            } catch (IOException e) {
                log.warn("Repository " + repository.getProject().getKey() + "/" + repository.getSlug() +
                        " (" + repository.getId() + ") could not be marked as deleted. It may not be " +
                        "automatically cleaned up with its hierarchy", e);
            }
        } else {
            List<File> deleteDirs = Lists.newArrayList(repositoryDir);
            if (parameters.isLastInHierarchy()) {
                //If this repository is the last one in the hierarchy, search for repositories in the hierarchy that
                //were only marked as deleted, previously, and clean them up too
                File[] hierarchyDirs = config.getRepositoriesDir().listFiles(new FileFilter() {

                    @Override
                    public boolean accept(File file) {
                        if (file.isDirectory()) {
                            File deleted = new File(file, "DELETED");
                            if (deleted.isFile()) {
                                try {
                                    if (hierarchyId.equals(FileUtils.readFileToString(deleted, "UTF-8"))) {
                                        //This is a repository in the deleted hierarchy and is also free to be cleaned up
                                        //now that the entire hierarchy is empty
                                        return true;
                                    }
                                } catch (IOException e) {
                                    log.warn("Cannot verify hierarchy for repository in [" + file.getAbsolutePath() +
                                            "]; it will not be removed", e);
                                }
                            }
                        }

                        //Ignore files, non-DELETED repositories and DELETED repositories which are from other
                        //hierarchies. They will not be included in this cleanup
                        return false;
                    }
                });

                if (!(hierarchyDirs == null || hierarchyDirs.length == 0)) {
                    deleteDirs.addAll(Arrays.asList(hierarchyDirs));
                }
            }

            new DeleteRunnable(scheduledExecutorService, deleteDirs).run();
        }

        return null;
    }

    private static class DeleteRunnable implements Runnable {

        private final ScheduledExecutorService scheduledExecutorService;
        private final List<File> repositoryDirs;
        private final int total;

        private int attempts;
        private int deleted;

        protected DeleteRunnable(ScheduledExecutorService scheduledExecutorService, List<File> repositoryDirs) {
            this.scheduledExecutorService = scheduledExecutorService;
            this.repositoryDirs = repositoryDirs;

            total = repositoryDirs.size();
        }

        @Override
        public void run() {
            ++attempts;

            for (Iterator<File> iterator = repositoryDirs.iterator(); iterator.hasNext();) {
                File repositoryDir = iterator.next();

                try {
                    FileUtils.deleteDirectory(repositoryDir);
                    iterator.remove(); //Remove the directory from the list; it's been deleted

                    log.info("Successfully deleted repository directory {}", repositoryDir.getAbsolutePath());
                    ++deleted;
                } catch (IOException e) {
                    //The directory could not be deleted; leave it in the list to potentially retry deletion
                }
            }

            if (!repositoryDirs.isEmpty()) {
                if (attempts < 5) {
                    if (log.isDebugEnabled()) {
                        log.debug("Pass {}/5: Deleted {} of {}. Will retry deletion for remaining",
                                attempts, deleted, total);
                    }

                    scheduledExecutorService.schedule(this, 1L, TimeUnit.MINUTES);
                } else {
                    StringBuilder builder = new StringBuilder("The following repository director")
                            .append(repositoryDirs.size() == 1 ? "y" : "ies")
                            .append(" could not be deleted after 5 attempts:");
                    for (File repositoryDir : repositoryDirs) {
                        builder.append("\n\t").append(repositoryDir.getAbsolutePath());
                    }
                    builder.append("\nNo further attempts will be made.");

                    log.warn(builder.toString());
                }
            }
        }
    }
}
