/*
 * Copyright (c) 2018.
 * This code is released under The 3-Clause BSD License.
 * https://github.com/techpavan
 */

package com.github.techpavan.maven;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.internal.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.versioning.ComparableVersion;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;

import static org.apache.commons.io.FilenameUtils.concat;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.SystemUtils.USER_HOME;

@Slf4j
public class CleanM2 {
    private static Map<DeleteReason, Set<File>> DELETE_MAP = new LinkedHashMap<>();
    private static Map<String, Set<FileInfo>> PROCESS_MAP = new HashMap<>();
    private static Map<SkipReason, Set<String>> SKIP_MAP = new HashMap<>();
    private static List<File> DELETE_FAILURE_LIST = new ArrayList<>();
    private static ArgData argData = new ArgData();
    private static List<String> RESTRICTED_FILES = Lists.newArrayList("repository.xml", "_maven.repositories", "_remote.repositories", "m2e-lastUpdated.properties", "resolver-status.properties");
    private static List<String> RESTRICTED_PATTERN = Lists.newArrayList("maven-metadata-", ".jar.lastUpdated", ".pom.lastUpdated");
    private static String LS = System.lineSeparator();

    static {
        DELETE_MAP.put(DeleteReason.FORCED_SNAPSHOT, new HashSet<>());
        DELETE_MAP.put(DeleteReason.FORCED_SOURCE, new HashSet<>());
        DELETE_MAP.put(DeleteReason.FORCED_JAVADOC, new HashSet<>());
        DELETE_MAP.put(DeleteReason.ACCESS_DATE, new HashSet<>());
        DELETE_MAP.put(DeleteReason.DOWNLOAD_DATE, new HashSet<>());
        DELETE_MAP.put(DeleteReason.NON_LATEST, new HashSet<>());
        DELETE_MAP.put(DeleteReason.FORCED_ARTIFACT, new HashSet<>());
        DELETE_MAP.put(DeleteReason.FORCED_GROUP, new HashSet<>());

        SKIP_MAP.put(SkipReason.RESERVED, new HashSet<>());
        SKIP_MAP.put(SkipReason.IGNORED_ARTIFACT, new HashSet<>());
        SKIP_MAP.put(SkipReason.IGNORED_GROUP, new HashSet<>());
        SKIP_MAP.put(SkipReason.RETAIN_OLD, new HashSet<>());
        SKIP_MAP.put(SkipReason.LATEST, new HashSet<>());
    }

    public static void main(String[] args) {
        JCommander jCommander = parseInputParams(args);
        File repoDir = evaluateM2Path(jCommander);
        String[] filter = argData.isDeleteSource() || argData.isDeleteJavadoc() ? null : new String[]{"pom"};
        FileUtils.listFiles(repoDir, filter, true).forEach(file -> parseAndEvaluate(file));
        processVersion();
        log.info("*********** Files to be deleted ***********");
        DELETE_MAP.forEach((k, v) -> log.info(LS + LS + k + " : " + LS + StringUtils.join(v.stream().sorted().iterator(), LS)));
        log.info(LS + LS + "*********** Files skipped ***********");
        SKIP_MAP.forEach((k, v) -> log.info(LS + LS + k + " : " + LS + StringUtils.join(v.stream().sorted().iterator(), LS)));
        if (!argData.isDryRun()) {
            log.info(LS + LS + "*********** Beginning to Delete Files ***********");
            deleteMarked();
            log.info(LS + LS + "*********** Deletion Completed ***********");
            log.info(LS + LS + "*********** Files having error in deletion ***********");
            log.info(StringUtils.join(DELETE_FAILURE_LIST.stream().sorted().iterator(), LS));
        } else {
            log.info(LS + LS + "*********** No files were deleted as program was run in DRY-RUN mode  ***********");
        }
    }

    private static File evaluateM2Path(JCommander jCommander) {
        String m2Path = defaultString(argData.getM2Path(), concat(USER_HOME, ".m2"));
        File m2Dir = new File(m2Path);
        File repoDir = new File(m2Path, "repository");
        if (!m2Dir.exists() || !repoDir.exists()) {
            log.error("Valid Maven repository could not be found. Please provide a valid input.");
            jCommander.usage();
            System.exit(1);
        }
        return repoDir;
    }

    private static JCommander parseInputParams(String[] args) {
        JCommander jCommander = JCommander.newBuilder().addObject(argData).build();
        try {
            jCommander.parse(args);
        } catch (ParameterException e) {
            jCommander.usage();
            System.exit(1);
        }
        return jCommander;
    }

    private static void deleteMarked() {
        DELETE_MAP.values().forEach(fileSet -> {
            fileSet.forEach(file -> {
                if (!FileUtils.deleteQuietly(file)) {
                    DELETE_FAILURE_LIST.add(file);
                }
            });
        });
    }

    private static void processVersion() {
        PROCESS_MAP.forEach((gaId, versionSet) -> {
            String latest = getLatestVersion(versionSet);
            versionSet.forEach(fileInfo -> {
                if (!latest.equals(fileInfo.getVersion())) {
                    DELETE_MAP.get(DeleteReason.NON_LATEST).add(fileInfo.getFile());
                } else {
                    SKIP_MAP.get(SkipReason.LATEST).add(fileInfo.getFile().getAbsolutePath());
                }
            });
        });
    }

    private static String getLatestVersion(Set<FileInfo> versionSet) {
        List<String> versionList = versionSet.stream().map(f -> f.getVersion()).collect(Collectors.toList());
        String latest = versionList.stream().map(v -> new ComparableVersion(v)).sorted().reduce((a, b) -> b).get().toString();
        return latest;
    }

    private static void parseAndEvaluate(File file) {
        try {
            if (isRestrictedFile(file.getName())) {
                SKIP_MAP.get(SkipReason.RESERVED).add(file.getAbsolutePath());
                return;
            }
            FileInfo fileInfo = createFileInfo(file);

            if (checkIgnoredFile(fileInfo)) {
                return;
            }
            DeleteReason deleteReason = checkForcedDeleteReason(fileInfo);
            if (deleteReason != null) {
                if (DeleteReason.FORCED_ARTIFACT == deleteReason) {
                    DELETE_MAP.get(deleteReason).add(fileInfo.getParentFileInfo().getParentFileInfo().getFile());
                } else if (DeleteReason.FORCED_GROUP == deleteReason) {
                    DELETE_MAP.get(deleteReason).add(fileInfo.getParentFileInfo().getParentFileInfo().getParentFileInfo().getFile());
                } else {
                    DELETE_MAP.get(deleteReason).add(file);
                }
                return;
            }

            BasicFileAttributes attributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
            // For below cases, add entire folder instead of just individual files
            if (attributes.lastAccessTime().toMillis() < argData.getAccessedBefore()) {
                DELETE_MAP.get(DeleteReason.ACCESS_DATE).add(file.getParentFile());
                return;
            } else if (attributes.lastModifiedTime().toMillis() < argData.getDownloadedBefore()) {
                DELETE_MAP.get(DeleteReason.DOWNLOAD_DATE).add(file.getParentFile());
                return;
            }
            if (argData.isRetainOld()) {
                SKIP_MAP.get(SkipReason.RETAIN_OLD).add(file.getParentFile().getAbsolutePath());
                return;
            }
            // When none of the above are matched, add it for latest / oldest processing
            addToProcessMap(fileInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static boolean isRestrictedFile(String name) {
        if (RESTRICTED_FILES.contains(name)) {
            return true;
        }
        return RESTRICTED_PATTERN.stream().filter(pattern -> name.contains(pattern)).count() > 0;
    }

    private static void addToProcessMap(FileInfo fileInfo) {
        String gaId = fileInfo.getGroupId() + ":" + fileInfo.getArtifactId();
        if (PROCESS_MAP.get(gaId) == null) {
            PROCESS_MAP.put(gaId, new HashSet<>());
        }
        PROCESS_MAP.get(gaId).add(fileInfo.getParentFileInfo());
    }

    private static DeleteReason checkForcedDeleteReason(FileInfo fileInfo) {
        if (argData.isDeleteJavadoc() && fileInfo.getFile().getName().contains(fileInfo.getVersion() + "-javadoc.jar")) {
            return DeleteReason.FORCED_JAVADOC;
        } else if (argData.isDeleteSource() && fileInfo.getFile().getName().contains(fileInfo.getVersion() + "-sources.jar")) {
            return DeleteReason.FORCED_SOURCE;
        } else if (argData.isDeleteAllSnapshots() && fileInfo.getVersion().endsWith("-SNAPSHOT")) {
            return DeleteReason.FORCED_SNAPSHOT;
        } else if (argData.getForceArtifacts() != null && argData.getForceArtifacts().contains(fileInfo.getGroupId() + ":" + fileInfo.getArtifactId())) {
            return DeleteReason.FORCED_ARTIFACT;
        } else if (argData.getForceGroups() != null && argData.getForceGroups().contains(fileInfo.getGroupId())) {
            return DeleteReason.FORCED_GROUP;
        }
        return null;
    }

    private static FileInfo createFileInfo(File file) {
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFile(file);
        fileInfo.setArtifactId(findArtifactId(file));
        fileInfo.setGroupId(findGroupId(file, fileInfo.getArtifactId()));
        fileInfo.setVersion(findVersion(file));
        return fileInfo;
    }

    private static boolean checkIgnoredFile(FileInfo fileInfo) {
        if (argData.getIgnoreArtifacts() != null && argData.getIgnoreArtifacts().contains(fileInfo.getGroupId() + ":" + fileInfo.getArtifactId())) {
            SKIP_MAP.get(SkipReason.IGNORED_ARTIFACT).add(fileInfo.getParentFileInfo().getParentFileInfo().getFile().getAbsolutePath());
            return true;
        } else if (argData.getIgnoreGroups() != null && argData.getIgnoreGroups().contains(fileInfo.getGroupId())) {
            SKIP_MAP.get(SkipReason.IGNORED_GROUP).add(fileInfo.getParentFileInfo().getParentFileInfo().getParentFileInfo().getFile().getAbsolutePath());
            return true;
        }
        return false;
    }

    private static String findGroupId(File file, String artifactId) {
        List<String> split = Arrays.asList(StringUtils.split(file.getAbsolutePath(), File.separatorChar));
        return StringUtils.join(split.stream().skip(split.indexOf("repository") + 1).limit(split.lastIndexOf(artifactId) - split.indexOf("repository") - 1).collect(Collectors.toList()), ".");
    }

    private static String findArtifactId(File file) {
        return file.getParentFile().getParentFile().getName();
    }

    private static String findVersion(File file) {
        return file.getParentFile().getName();
    }

    private enum DeleteReason {
        ACCESS_DATE,
        DOWNLOAD_DATE,
        FORCED_SNAPSHOT,
        FORCED_SOURCE,
        FORCED_JAVADOC,
        FORCED_GROUP,
        FORCED_ARTIFACT,
        NON_LATEST
    }

    private enum SkipReason {
        IGNORED_ARTIFACT, IGNORED_GROUP, LATEST, RESERVED, RETAIN_OLD
    }

}