package org.limine.snapper.processes;

import org.limine.snapper.formats.json1.ImageDetail;
import org.limine.snapper.formats.json1.Manifest;
import org.limine.snapper.formats.json1.SnapperEntry;
import org.limine.snapper.formats.json1.SnapshotEntry;
import org.limine.snapper.formats.snapper.SnapperProperty;
import org.limine.snapper.objects.Config;
import org.limine.snapper.objects.ConsoleColor;
import org.limine.snapper.objects.Hash;
import org.limine.snapper.objects.Output;
import tools.jackson.databind.json.JsonMapper;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HistoryReader {

    public static Manifest load(String filePath) {
        if (Utility.isFilePresent(filePath)) {
            JsonMapper jsonMapper = new JsonMapper();
            try {
                return jsonMapper.readValue(new File(filePath), Manifest.class);
            } catch (Exception e) {
                String errMessage = "The manifest is broken or incompatible:";
                Utility.errorMessage(errMessage, e.getMessage());
                if (Utility.isFilePresent(filePath + Config.BACKUP_SUFFIX)) {
                    Utility.errorMessage("Then loading the old file from the backup:", filePath + Config.BACKUP_SUFFIX);
                    return load(filePath + Config.BACKUP_SUFFIX);
                } else {
                    System.exit(1);
                    return null;
                }
            }

        } else {
            Utility.ensurePathExists(filePath);
            // Create a new manifest with default values.
            Manifest manifest = new Manifest();
            manifest.setVersion(Config.MANIFEST_VERSION);
            manifest.setLastSnapshotID(0);
            manifest.setUuid(null);
            manifest.setLastLocalTime(Config.DEFAULT_TIMESTAMP);
            manifest.setLastUTCTime(Config.DEFAULT_TIMESTAMP);
            return manifest;
        }
    }

    public static LinkedHashSet<String> displaySnapshotList(Manifest manifest) {
        System.out.println();
        List<String[]> data = new ArrayList<>();
        LinkedHashSet<String> IDs = new LinkedHashSet<>();
        int columnWidth1 = "ID".length(), columnWidth2 = "Timestamp".length(), columnWidth3 = "Description".length();
        for (SnapshotEntry snapshot : manifest.getSnapshotEntries()) {
            SnapperEntry entry = snapshot.snapperEntry();
            String id = String.valueOf(entry.snapshotID());
            IDs.add(id);
            if (columnWidth1 < id.length()) {
                columnWidth1 = id.length();
            }
            if (columnWidth2 < entry.localTime().length()) {
                columnWidth2 = entry.localTime().length();
            }
            String description = entry.properties().get(SnapperProperty.DESCRIPTION.replace);
            if (description != null) {
                if (columnWidth3 < description.length()) {
                    columnWidth3 = description.length();
                }
            } else {
                description = "";
            }
            String[] line = new String[]{id, entry.localTime(), description};
            data.add(line);
        }
        // print a snapshots-table
        printTable(data, columnWidth1, columnWidth2, columnWidth3);
        return IDs;
    }

    public static void displayInfo(Manifest manifest, Config config) {
        System.out.println();
        // Display a detail:
        printFormattedInfo("Version", Config.VERSION, true, ConsoleColor.GREEN_BOLD);
        if (Utility.isCommandPresent("limine")) {
            Output output = Utility.getTextFromCommand("limine --version", false, "");
            if (output.isSuccess()) {
                String limineVersion = extractVersion(output.text().getFirst());
                if (limineVersion != null) {
                    if (isLimineMajorUnsupported(limineVersion)) {
                        printFormattedInfo("Limine version", limineVersion + " is not supported.", true, ConsoleColor.RED_BOLD);
                    }
                }
            }
        }
        printFormattedInfo("Manifest version", manifest.getVersion(), true, ConsoleColor.GREEN_BOLD);
        String uuid = manifest.getUuid();
        if (uuid != null) {
            printFormattedInfo("FS UUID", manifest.getUuid(), true, ConsoleColor.GREEN_BOLD);
        } else {
            printFormattedInfo("FS UUID", "No FS UUID", true, ConsoleColor.RED_BOLD);
        }
        if (!Config.DEFAULT_TIMESTAMP.equals(manifest.getLastLocalTime())) {
            printFormattedInfo("Last snapshot", "ID: " + manifest.getLastSnapshotID() + ", date: " + manifest.getLastLocalTime(), true, ConsoleColor.GREEN_BOLD);
        }
        String snapshotsInfo = "" + manifest.getSnapshotEntries().size();
        if (0 <= Config.MAX_SNAPSHOT_ENTRIES && Config.MAX_SNAPSHOT_ENTRIES <= 10000) {
            snapshotsInfo += " (max " + Config.MAX_SNAPSHOT_ENTRIES + ")";
        } else if (Config.MAX_SNAPSHOT_ENTRIES == Config.AUTO_SNAPSHOT_LIMIT) {
            snapshotsInfo += " (max: auto)";
        }
        if (manifest.getSnapshotEntries().size() < Config.MAX_SNAPSHOT_ENTRIES || Config.MAX_SNAPSHOT_ENTRIES == Config.AUTO_SNAPSHOT_LIMIT) {
            printFormattedInfo("Snapshot entries", snapshotsInfo, true, ConsoleColor.GREEN_BOLD);
        } else {
            printFormattedInfo("Snapshot entries", snapshotsInfo, true, ConsoleColor.YELLOW_BOLD);
        }
        double usagePercent = Utility.getUsagePercent(config.espPath());
        String usageInfo = usagePercent + "% of " + Utility.getTotalSpace(config.espPath()) + " (max " + Config.LIMIT_USAGE_PERCENT + "%)";
        if (usagePercent + 10.0 < Config.LIMIT_USAGE_PERCENT) {
            printFormattedInfo("Boot usage", usageInfo, true, ConsoleColor.GREEN_BOLD);
        } else if (usagePercent + 5.0 > Config.LIMIT_USAGE_PERCENT) {
            printFormattedInfo("Boot usage", usageInfo, true, ConsoleColor.RED_BOLD);
        } else {
            printFormattedInfo("Boot usage", usageInfo, true, ConsoleColor.YELLOW_BOLD);
        }
        verifyFiles(manifest, config);
        System.out.println();
    }

    public static void verifyFiles(Manifest manifest, Config config) {
        System.out.print(ConsoleColor.YELLOW + "Verifying files ..." + ConsoleColor.RESET);
        List<String> unusedFiles = new ArrayList<>();
        List<String> missingFiles = new ArrayList<>();
        List<String> nonHashableFiles = new ArrayList<>();
        List<String> filesToVerify = new ArrayList<>();

        LinkedHashSet<String> neededFiles = new LinkedHashSet<>();
        for (SnapshotEntry entry : manifest.getSnapshotEntries()) {
            for (ImageDetail image : entry.allImages()) {
                neededFiles.add(image.fileHashName());
            }
        }

        String dirPath = Utility.buildPath(config.espPath(), config.machineID(), Config.HISTORY_FOLDER);
        List<String> directory = Utility.collectFilesFromDirectory(dirPath);
        var allFiles = new ArrayList<String>();
        var allFilesSizeBytes = 0L;

        for (String fileName : directory) {
            if ((Config.MANIFEST_FILE + Config.BACKUP_SUFFIX).startsWith(fileName)) {
                continue;
            }

            // Check if the file is unnecessary
            if (!neededFiles.contains(fileName)) {
                unusedFiles.add(fileName);
            }

            // Count all files and sum sizes
            allFiles.add(fileName);
            allFilesSizeBytes += new File(dirPath, fileName).length();
        }

        for (String file : neededFiles) {
            if (!directory.contains(file)) {
                missingFiles.add(file);
            } else if (Hash.isFileHashable(file)) {
                filesToVerify.add(file);
            } else {
                nonHashableFiles.add(file);
            }
        }

        // Multithreaded corruption check
        List<String> corruptedFiles = filesToVerify
                .parallelStream()
                .filter(file -> Hash.hasCorruption(Utility.buildPath(dirPath, file)))
                .toList();

        //Check which snapshots are affected by corrupted files
        var snapshotsWithCorruptedFiles = new LinkedHashSet<String>();

        //Check which snapshots are affected by missing files
        var snapshotsWithMissingFiles = new LinkedHashSet<String>();

        //Check which snapshots are affected by non-hashable files
        var snapshotsWithNonHashableFiles = new LinkedHashSet<String>();

        for (var snapshot : manifest.getSnapshotEntries()) {
            String snapshotId = String.valueOf(snapshot.snapperEntry().snapshotID());
            for (var image : snapshot.allImages()) {
                String fileWithHash = image.fileHashName();
                if (corruptedFiles.contains(fileWithHash)) {
                    snapshotsWithCorruptedFiles.add(snapshotId);
                }
                if (missingFiles.contains(fileWithHash)) {
                    snapshotsWithMissingFiles.add(snapshotId);
                }
                if (nonHashableFiles.contains(fileWithHash)) {
                    snapshotsWithNonHashableFiles.add(snapshotId);
                }
            }
        }

        // erase the current terminal line
        System.out.print("\r\u001B[K");
        printFormattedInfo("Snapshot files", allFiles.size() + " (" + String.format("%.1f MiB", allFilesSizeBytes / (1024.0 * 1024.0)) + ")", true, ConsoleColor.GREEN_BOLD);

        if (unusedFiles.isEmpty()) {
            printFormattedInfo("Unused files", "0", true, ConsoleColor.GREEN_BOLD);
        } else {
            printFormattedInfo("Unused files", String.valueOf(unusedFiles.size()), true, ConsoleColor.YELLOW_BOLD);
            for (String file : unusedFiles) {
                printFormattedInfo("", file, false, ConsoleColor.YELLOW);
            }
        }

        if (missingFiles.isEmpty()) {
            printFormattedInfo("Missing files", "0", true, ConsoleColor.GREEN_BOLD);
        } else {
            printFormattedInfo("Missing files", String.valueOf(missingFiles.size()), true, ConsoleColor.RED_BOLD);
            for (String file : missingFiles) {
                printFormattedInfo("", file, false, ConsoleColor.RED);
            }
            printFormattedInfo("Affected snapshots", String.join(", ", snapshotsWithMissingFiles), true, ConsoleColor.RED_BOLD);
        }

        if (corruptedFiles.isEmpty()) {
            printFormattedInfo("Corrupted files", "0", true, ConsoleColor.GREEN_BOLD);
        } else {
            printFormattedInfo("Corrupted files", String.valueOf(corruptedFiles.size()), true, ConsoleColor.RED_BOLD);
            for (String file : corruptedFiles) {
                printFormattedInfo("", file, false, ConsoleColor.RED);
            }
            printFormattedInfo("Affected snapshots", String.join(", ", snapshotsWithCorruptedFiles), true, ConsoleColor.RED_BOLD);
        }

        if (!nonHashableFiles.isEmpty()) {
            printFormattedInfo("Non-hashable files", String.valueOf(nonHashableFiles.size()), true, ConsoleColor.YELLOW_BOLD);
            for (String file : nonHashableFiles) {
                printFormattedInfo("", file, false, ConsoleColor.RED);
            }
            printFormattedInfo("Affected snapshots", String.join(", ", snapshotsWithNonHashableFiles), true, ConsoleColor.YELLOW_BOLD);
        }
    }

    private static void printFormattedInfo(String label, String value, boolean showColon, ConsoleColor color) {
        int labelWidth = 18; // Adjust this to the desired width
        String formattedLabel = String.format("%-" + labelWidth + "s", label);
        if (showColon) {
            System.out.println(color + formattedLabel + ": " + ConsoleColor.RESET + value);
        } else {
            System.out.println(color + formattedLabel + "  " + ConsoleColor.RESET + value);
        }
    }

    /**
     * Method to extract a version number from a given string
     */
    private static String extractVersion(String input) {
        // Define a regular expression pattern for matching version numbers
        Pattern versionPattern = Pattern.compile("\\b(\\d+\\.\\d+\\.\\d+)\\b");
        Matcher matcher = versionPattern.matcher(input);

        // Check if a match is found and return the version number
        if (matcher.find()) {
            return matcher.group(1);
        } else {
            // Return a default value or handle the case when no version is found
            return null;
        }
    }

    private static boolean isLimineMajorUnsupported(String versionString) {
        // Regex to extract the major part of the version
        String regex = "^([0-9]+)\\.";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(versionString);

        if (matcher.find()) {
            try {
                // Extract a major version and convert to integer
                int majorVersion = Integer.parseInt(matcher.group(1));
                // Check if the major version is supported
                return Config.UNSUPPORTED_LIMINE_MAJOR_VERSIONS.contains(majorVersion);
            } catch (NumberFormatException e) {
                // Error parsing major version
                Utility.errorMessage("Invalid version format:", versionString);
            }
        } else {
            Utility.errorMessage("No valid version format found:", versionString);
        }
        return false;
    }


    /**
     * Prints a snapper table with a nice format for the output in the terminal.
     */
    public static void printTable(List<String[]> data, int columnWidth1, int columnWidth2, int columnWidth3) {

        boolean unicode = Utility.isUnicodeSupported();
        String colSep = unicode ? "│" : "|";
        String rowColSep = unicode ? "┼" : "|";
        String rowSep = unicode ? "─" : "-";

        // Here you can define the width of the columns
        int[] columnWidths = {columnWidth1, columnWidth2, columnWidth3};

        // Print header
        String header = String.format(" %1$-" + columnWidths[0] + "s " + colSep + " %2$-" + columnWidths[1] + "s " + colSep + " %3$-" + columnWidths[2] + "s ", "ID", "Date", "Description");
        String rowSeparator = rowSep.repeat(columnWidths[0] + 2) + rowColSep + rowSep.repeat(columnWidths[1] + 2) + rowColSep + rowSep.repeat(columnWidths[2] + 2);

        System.out.println(header);
        System.out.println(rowSeparator);

        // Print lines
        for (String[] row : data) {
            System.out.printf(" %1$-" + columnWidths[0] + "s " + colSep + " %2$-" + columnWidths[1] + "s " + colSep + " %3$-" + columnWidths[2] + "s %n", row[0], row[1], row[2]);
        }
    }

}
