package org.limine.snapper.processes;

import org.limine.snapper.objects.Config;
import org.limine.snapper.objects.Hash;
import org.limine.snapper.objects.Output;
import org.limine.snapper.objects.RestoreMethod;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.limine.snapper.processes.Utility.getLinuxDistroName;

public class ConfigReader {

    private final Map<String, String> configs = new HashMap<>();

    public ConfigReader() {
        loadConfigFile("/etc/limine-snapper-sync.conf");
        loadConfigFile(Config.DEFAULT_CONFIG_FILE_PATH);

        Output c = Utility.getTextFromCommand("tput colors", false, "");
        if (c.isSuccess() && !c.text().isEmpty()) {
            try {
                Config.ENABLE_COLOR = Integer.parseInt(c.text().getFirst().trim()) >= 8;
            } catch (NumberFormatException ignored) {
                Config.ENABLE_COLOR = false;
            }
        }

        if (Config.ENABLE_TMP_CONFIG && Utility.isFilePresent(Config.TMP_CONFIG_FILE_PATH)) {
            if (Utility.isRootOwnedFile(Config.TMP_CONFIG_FILE_PATH)) {
                loadConfigFile(Config.TMP_CONFIG_FILE_PATH);
            } else {
                Utility.warnMessage("The temporary config file " + Config.TMP_CONFIG_FILE_PATH + " is ignored because it is not owned by root.");
            }
        }
    }

    private List<String> getKernelParams(String input) {
        List<String> params = new ArrayList<>();
        if (input == null || input.isEmpty()) {
            return params;
        }
        String[] parts = input.split("\\s+");
        for (String part : parts) {
            if (!part.isEmpty()) {
                params.add(part);
            }
        }
        return params;
    }

    private void loadConfigFile(String filePath) {
        Path path = Paths.get(filePath);
        if (!Files.exists(path)) return;

        try (BufferedReader reader = Files.newBufferedReader(path)) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();

                // Skip empty lines and comments
                if (line.isEmpty() || line.startsWith("#")) continue;

                int eqIndex = line.indexOf('=');
                if (eqIndex == -1) continue;

                String key = line.substring(0, eqIndex).trim();
                String value = Utility.removeSurrounding(line.substring(eqIndex + 1).trim(), "\""); // remove surrounding quotes;

                switch (key) {
                    case "SNAPSHOT_KERNEL_PARAMETERS+":
                        Config.KERNEL_PARAMETERS_TO_ADD.addAll(getKernelParams(value));
                        break;
                    case "SNAPSHOT_KERNEL_PARAMETERS-":
                        Config.KERNEL_PARAMETERS_TO_REMOVE.addAll(getKernelParams(value));
                        break;
                    case "SNAPSHOT_KERNEL_PARAMETERS":
                        Config.KERNEL_PARAMETERS_TO_REPLACE.addAll(getKernelParams(value));
                    default:
                        configs.put(key, value);
                        break;
                }
            }
        } catch (IOException e) {
            Utility.exitWithError("Failed to load config file:", filePath + " due to " + e.getMessage());
        }
    }

    public Config readConfig() {
        String targetOs = configs.get("TARGET_OS_NAME");
        if (targetOs == null || targetOs.isBlank()) {
            Config.TARGET_OS_NAME = getLinuxDistroName();
        } else {
            Config.TARGET_OS_NAME = Utility.cleanName(targetOs);
        }

        String maxSnapshotEntries = configs.get("MAX_SNAPSHOT_ENTRIES");
        if (maxSnapshotEntries != null) {
            if ("auto".equalsIgnoreCase(maxSnapshotEntries)) {
                Config.MAX_SNAPSHOT_ENTRIES = Config.AUTO_SNAPSHOT_LIMIT;
            } else {
                try {
                    int number = Integer.parseInt(maxSnapshotEntries);
                    if (number >= 0) {
                        Config.MAX_SNAPSHOT_ENTRIES = number;
                    } else {
                        Config.MAX_SNAPSHOT_ENTRIES = Config.AUTO_SNAPSHOT_LIMIT;
                    }
                } catch (NumberFormatException ignored) {
                    Utility.errorMessage("Invalid value: MAX_SNAPSHOT_ENTRIES must be 'auto' or numeric and no more than 9 digits.");
                }
            }
        }

        String espPath = configs.get("ESP_PATH");
        if (espPath == null) {
            if (Utility.isSystemEfi() && Utility.isCommandPresent("bootctl")) {
                Output output = Utility.getTextFromCommand("bootctl --print-esp-path", false, null);
                if (output.isSuccess()) {
                    espPath = output.text().getFirst();
                }
            }
        }
        if (espPath != null) {
            espPath = Utility.buildPath(espPath.trim());
            // Check if espPath is valid in your system, e.g., Does $ESP_PATH/limine.conf exist?
            String limineConfigPath = Utility.buildPath(espPath, Config.LIMINE_CONFIG_FILE);
            if (!Utility.isFilePresent(limineConfigPath)) {
                String errMessage = "The file: " + limineConfigPath + " is not found.";
                Utility.exitWithError(errMessage);
                return null;
            }
        } else {
            String errMessage = "Please set ESP_PATH in " + Config.DEFAULT_CONFIG_FILE_PATH;
            Utility.exitWithError(errMessage);
            return null;
        }

        String rootSubvolumePath = configs.get("ROOT_SUBVOLUME_PATH");
        // Please do not implement automatic detection of subvolume as this will not work when booting into a snapshot or overlayFs.
        if (rootSubvolumePath != null) {
            if (!rootSubvolumePath.isBlank()) {
                rootSubvolumePath = Utility.buildPath(rootSubvolumePath);
            } else {
                rootSubvolumePath = null;
            }
        }
        if (rootSubvolumePath == null) {
            String errMessage = "No ROOT_SUBVOLUME_PATH. Please set ROOT_SUBVOLUME_PATH in " + Config.DEFAULT_CONFIG_FILE_PATH;
            Utility.exitWithError(errMessage);
            return null;
        }

        String rootSnapshotsPath = configs.get("ROOT_SNAPSHOTS_PATH");
        if (rootSnapshotsPath != null) {
            if (!rootSnapshotsPath.isBlank()) {
                rootSnapshotsPath = Utility.buildPath(rootSnapshotsPath);
            } else {
                rootSnapshotsPath = null;
            }
        }
        if (rootSnapshotsPath == null) {
            String errMessage = "No ROOT_SNAPSHOTS_PATH. Please set ROOT_SNAPSHOTS_PATH in " + Config.DEFAULT_CONFIG_FILE_PATH;
            Utility.exitWithError(errMessage);
            return null;
        }

        if (!Utility.isCommandPresent("snapper")) {
            String errMessage = "Snapper is not installed.";
            Utility.exitWithError(errMessage);
            return null;
        }

        String snapshotWritable = configs.get("SNAPSHOT_WRITABLE");
        if (snapshotWritable != null) {
            Config.SNAPSHOT_WRITABLE = "yes".equalsIgnoreCase(snapshotWritable);
        }

        String setSnapshotAsDefault = configs.get("SET_SNAPSHOT_AS_DEFAULT");
        if (setSnapshotAsDefault != null) {
            Config.SET_SNAPSHOT_AS_DEFAULT = "yes".equalsIgnoreCase(setSnapshotAsDefault);
        }

        String excludeSnapshotTypes = configs.get("EXCLUDE_SNAPSHOT_TYPES");
        if (excludeSnapshotTypes != null) {
            for (String type : excludeSnapshotTypes.split(",")) {
                String trimmed = type.trim();
                if (!trimmed.isEmpty()) {
                    Config.EXCLUDE_SNAPSHOT_TYPES.add(trimmed);
                }
            }
        }

        String restoreMethod = configs.get("RESTORE_METHOD");
        if (restoreMethod != null) {
            switch (restoreMethod) {
                case "snapper", "opensuse":
                    Config.RESTORE_METHOD = RestoreMethod.SNAPPER;
                    break;
                case "rsync":
                    if (Utility.isCommandPresent("rsync")) {
                        Config.RESTORE_METHOD = RestoreMethod.RSYNC;
                    } else {
                        String errMessage = "'RESTORE_METHOD=rsync' can not be used, as command 'rsync' is not installed.";
                        Utility.errorMessage(errMessage);
                        Config.RESTORE_METHOD = RestoreMethod.REPLACE;
                    }
                    break;
                case "replace":
                    Config.RESTORE_METHOD = RestoreMethod.REPLACE;
                    break;
                default:
                    String errMessage = "Invalid option: RESTORE_METHOD=" + restoreMethod + ". Available options: replace, rsync, or snapper.";
                    Utility.errorMessage(errMessage);
                    Config.RESTORE_METHOD = RestoreMethod.REPLACE;
                    break;
            }
        } else {
            String enableRsync = configs.get("ENABLE_RSYNC_ASK");
            if ("yes".equalsIgnoreCase(enableRsync) && Utility.isCommandPresent("rsync")) {
                Config.RESTORE_METHOD = RestoreMethod.RSYNC;
                Utility.warnMessage("Config: 'ENABLE_RSYNC_ASK=yes' is deprecated. Please set 'RESTORE_METHOD=rsync' instead");
            } else {
                Config.RESTORE_METHOD = RestoreMethod.REPLACE;
            }
        }

        Config.USE_OPENSUSE_LAYOUT = Config.RESTORE_METHOD == RestoreMethod.SNAPPER;

        String spaceNumber = configs.get("SPACE_NUMBER");
        if (spaceNumber != null) {
            try {
                int number = Integer.parseInt(spaceNumber);
                StringBuilder spaces = new StringBuilder();
                while (number > 0) {
                    spaces.append(" ");
                    number--;
                }
                Config.SPACES = spaces.toString();
            } catch (NumberFormatException ignored) {
                String errMessage = "Invalid value: SPACE_NUMBER must be numeric.";
                Utility.errorMessage(errMessage);
            }
        }

        String limitUsagePercent = configs.get("LIMIT_USAGE_PERCENT");
        if (limitUsagePercent != null) {
            try {
                double percent = Double.parseDouble(limitUsagePercent);
                if (0.0 < percent && percent < 100.0) {
                    Config.LIMIT_USAGE_PERCENT = percent;
                }
            } catch (NumberFormatException e) {
                String errMessage = "Invalid value: LIMIT_USAGE_PERCENT should be from 1 to 99!";
                Utility.errorMessage(errMessage);
            }
        }

        String fsUuid = configs.get("FS_UUID");
        String uuid = configs.get("UUID");
        if (Utility.isUuidValid(fsUuid)) {
            Config.FS_UUID = fsUuid;
        } else if (Utility.isUuidValid(uuid)) {
            Config.FS_UUID = uuid;
        }

        String snapshotFormatChoice = configs.get("SNAPSHOT_FORMAT_CHOICE");
        if (snapshotFormatChoice != null) {
            Config.SNAPSHOT_FORMAT_CHOICE = snapshotFormatChoice;
        }

        String hashName = configs.get("HASH_FUNCTION");
        if (hashName != null) {
            Hash hashFunction = Hash.getHashFunction(hashName);
            if (hashFunction != null && Utility.isCommandPresent(hashFunction.command)) {
                Config.HASH_FUNCTION = hashFunction;
            } else {
                String errMessage = "The hash function '" + hashName + "' was not found or is not supported.";
                Utility.errorMessage(errMessage);
            }
        }

        // The default hash function must be available if no other hash function is selected or found.
        if (Config.HASH_FUNCTION == Hash.SHA256 && !Utility.isCommandPresent(Hash.SHA256.command)) {
            Utility.exitWithError("The default hash utility '" + Hash.SHA256.command + "' is not installed.");
            return null;
        }

        String configBackupThreshold = configs.get("CONFIG_BACKUP_THRESHOLD");
        if (configBackupThreshold != null) {
            try {
                Config.CONFIG_BACKUP_THRESHOLD = Integer.parseInt(configBackupThreshold);
            } catch (NumberFormatException e) {
                Utility.errorMessage("'CONFIG_BACKUP_THRESHOLD=" + configBackupThreshold + "' is invalid, it should be in hours format.");
            }
        }

        String commands = configs.get("COMMANDS_BEFORE_SAVE");
        if (commands != null) {
            Config.COMMANDS_BEFORE_SAVE = commands;
        }

        commands = configs.get("COMMANDS_AFTER_SAVE");
        if (commands != null) {
            Config.COMMANDS_AFTER_SAVE = commands;
        }

        return new Config(Utility.readMachineID(), espPath, rootSubvolumePath, rootSnapshotsPath);
    }

    public void loadQuietMode() {
        Config.QUIET = "yes".equalsIgnoreCase(configs.get("QUIET_MODE"));
    }

    public void readSnapperConfig() {
        String snapperConfigName = configs.get("SNAPPER_CONFIG_NAME");
        if (snapperConfigName != null) {
            if (!snapperConfigName.isBlank()) {
                Config.SNAPPER_CONFIG_NAME = snapperConfigName;
            } else {
                Config.SNAPPER_CONFIG_NAME = null;
            }
        }
        boolean disableDbus = Utility.areYouInChroot();
        if (Config.SNAPPER_CONFIG_NAME == null) {
            Config.SNAPPER_CONFIG_NAME = Utility.snapperConfigName(disableDbus);
        }
        if (Config.SNAPPER_CONFIG_NAME == null && Utility.areYouInBtrfs()) {
            String warnMessage = "No Snapper config found for '/'. Creating a Snapper config named 'root' and single root snapshot.";
            Utility.warnMessage(warnMessage);
            // Automatically create a Snapper configuration and root snapshot if not present.
            Config.SNAPPER_CONFIG_NAME = initSnapperConfig();
        }
        if (Config.SNAPPER_CONFIG_NAME != null) {
            String dbusOption = disableDbus ? " --no-dbus" : "";
            Config.SNAPPER_LIST_CMDLINE = Config.SNAPPER_COMMAND
                    + dbusOption
                    + " -c " + Config.SNAPPER_CONFIG_NAME
                    + " list --disable-used-space --columns number,date,type,description";
        } else {
            String errMessage;
            if (Utility.areYouInBtrfs()) {
                errMessage = "No Snapper config found for '/'. Please configure Snapper and set SNAPPER_CONFIG_NAME in " + Config.DEFAULT_CONFIG_FILE_PATH + ".";
            } else {
                errMessage = "The '/' partition is not using Btrfs.";
            }
            Utility.exitWithError(errMessage);
        }
    }

    /**
     * Automatically create a Snapper configuration and root snapshot.
     */
    private String initSnapperConfig() {
        List<String> commands = List.of(
                "snapper --no-dbus -c root create-config /",
                "snapper --no-dbus -c root create --description 'limine-snapper-sync' --cleanup-algorithm number --read-only"
        );
        if (Utility.runCommands(commands, true, true, true)) {
            return "root";
        } else {
            return null;
        }
    }

    /**
     * Ensures that the mount paths are valid before taking a snapshot prior to restore.
     * In a chroot environment, the restore process cannot directly verify mount paths.
     * Therefore, it relies on reading these correct configurations in the snapshot.
     */
    public void checkMountPaths(Config config) {
        String rootMount = Utility.getSubvolFromProcMounts("/");
        String snapMount = Utility.getSubvolFromProcMounts("/.snapshots");

        // If your system uses an OpenSUSE-style layout
        if (Config.USE_OPENSUSE_LAYOUT) {
            boolean isLayoutOpenSuse = true;
            String errMessages = "\"RESTORE_METHOD=snapper\" cannot be used: snapper rollback is incompatible with your Btrfs layout and requires OpenSUSE-style layout.";
            errMessages += "\nOpenSUSE-style layout requires removing hardcoded 'subvol=' and 'subvolid=' kernel parameters for normal boot";
            // The system must be running on either a writable or read-only Snapper snapshot.
            if (rootMount == null || !rootMount.endsWith("/snapshot")) {
                errMessages += "\nOpenSUSE-style layout requires system running on a default writable snapshot, not on a root subvolume.";
                isLayoutOpenSuse = false;
            }

            // The subvolume '/@/.snapshots' must be mounted at '/.snapshots'
            if (snapMount == null || !snapMount.endsWith("/.snapshots")) {
                errMessages += "\nOpenSUSE-style layout requires '/.snapshots' subvolume permanently mounted via /etc/fstab.";
                isLayoutOpenSuse = false;
            }

            if (!isLayoutOpenSuse) {
                Utility.exitWithError(errMessages);
            }
        } else {
            if (rootMount != null && !rootMount.equals(config.rootSubvolumePath())) {
                String errMessages = "ROOT_SUBVOLUME_PATH=\"" + config.rootSubvolumePath() + "\" doesn't match the expected path \"" + rootMount + "\" from /proc/mounts";
                errMessages += "\nPlease set 'ROOT_SUBVOLUME_PATH=" + rootMount + "' in " + Config.DEFAULT_CONFIG_FILE_PATH;
                // Ignore the error when switching from the restore method "opensuse" to "replace" or "rsync"
                if (!rootMount.endsWith("/snapshot")) {
                    Utility.exitWithError(errMessages);
                }
            }

            if (snapMount != null && !snapMount.equals(config.rootSnapshotsPath())) {
                //snapshots in a custom snapper layout
                String errMessages = "ROOT_SNAPSHOTS_PATH=\"" + config.rootSnapshotsPath() + "\" doesn't match the expected path \"" + snapMount + "\" from /proc/mounts";
                errMessages += "\nPlease set 'ROOT_SNAPSHOTS_PATH=" + snapMount + "' in " + Config.DEFAULT_CONFIG_FILE_PATH;
                Utility.exitWithError(errMessages);
            } else if (snapMount == null) {
                String rootSubvolPath = config.rootSubvolumePath();
                if ("/".equals(rootSubvolPath)) {
                    rootSubvolPath = "";
                }
                if (!(rootSubvolPath + "/.snapshots").equals(config.rootSnapshotsPath())) {
                    // Nested snapshots in default snapper layout
                    String errMessages = "ROOT_SNAPSHOTS_PATH=\"" + config.rootSnapshotsPath() + "\" doesn't match the expected path \"" + rootSubvolPath + "/.snapshots\"";
                    errMessages += "\nPlease set 'ROOT_SNAPSHOTS_PATH=" + rootSubvolPath + "/.snapshots' in " + Config.DEFAULT_CONFIG_FILE_PATH;
                    Utility.exitWithError(errMessages);
                }
            }
        }
    }
}
