package com.atlassian.bitbucket.mesh.repository;

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
import static java.util.Collections.emptySet;

class WorkTreeCleanupTask implements Runnable {

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

    private final DataStoreManager dataStoreManager;
    private final TransactionTemplate transactionTemplate;
    private final WorkTreeStateDao workTreeStateDao;

    WorkTreeCleanupTask(DataStoreManager dataStoreManager, TransactionTemplate transactionTemplate,
                        WorkTreeStateDao workTreeStateDao) {
        this.dataStoreManager = dataStoreManager;
        this.transactionTemplate = transactionTemplate;
        this.workTreeStateDao = workTreeStateDao;
    }

    @Override
    public void run() {
        Set<Long> activeWorkTrees = new HashSet<>();
        Instant now = Instant.now();
        //Used so that any directories that are created but not (yet) in the db are still left on disk.
        Instant unknownWorkTreeExpiry = now.minusSeconds(60);
        List<Long> allDbIds = transactionTemplate.execute(tx -> workTreeStateDao.getAllIds());

        for (Path path : dataStoreManager.getWorkTreesDirs()) {
            if (!MoreFiles.dirExists(path)) {
                log.debug("Directory {} does not exist, skipping", path);
                return;
            }
            try {
                Files.walkFileTree(path, emptySet(), 1, new SimpleFileVisitor<Path>() {

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                        if (!attrs.isDirectory()) {
                            return SKIP_SUBTREE;
                        }
                        String fileName = file.getFileName().toString();
                        if (!fileName.startsWith("wt-")) {
                            return SKIP_SUBTREE;
                        }
                        long wtId;
                        try {
                            wtId = Long.parseLong(fileName.substring(3));
                        } catch (NumberFormatException e) {
                            //not a work tree dir, ignore the directory and continue.
                            return SKIP_SUBTREE;
                        }
                        transactionTemplate.execute(tx -> {
                            WorkTreeState state = workTreeStateDao.findById(wtId);

                            if (shouldBeRemoved(state, attrs, now, unknownWorkTreeExpiry)) {
                                try {
                                    log.debug("Removing expired/invalid work tree '{}'", file);
                                    MoreFiles.deleteRecursively(file);
                                    workTreeStateDao.deleteById(wtId);
                                    allDbIds.remove(wtId);
                                } catch (FileNotFoundException | NoSuchFileException e) {
                                    //ignored, this can happen when a work tree is closed concurrently
                                } catch (IOException e) {
                                    log.info("Could not remove invalid work tree '{}', will try again later", wtId, e);
                                }
                            } else {
                                activeWorkTrees.add(wtId);
                            }
                            return null;
                        });
                        return SKIP_SUBTREE;
                    }

                    @Override
                    public FileVisitResult visitFileFailed(Path file, IOException exc) {
                        log.info("Could not visit '{}' will try again later", file, exc);
                        return FileVisitResult.CONTINUE;
                    }
                });
            } catch (FileNotFoundException | NoSuchFileException e) {
                //should not happen, but if it does happen we can safely ignore it and continue
            } catch (IOException e) {
                log.warn("Work tree cleanup failed, will try again later", e);
                return;
            }
        }

        transactionTemplate.execute(tx -> {
            allDbIds.removeAll(activeWorkTrees);
            workTreeStateDao.deleteByIds(allDbIds);
            return null;
        });
    }

    private static boolean shouldBeRemoved(@Nullable WorkTreeState state, BasicFileAttributes attributes,
                                           Instant now, Instant createTimeBuffer) {
        if (state == null && attributes.creationTime().toInstant().isBefore(createTimeBuffer)) {
            return true;
        }
        return state != null && state.getExpiry().isBefore(now);
    }
}
