package org.limine.snapper.processes;

import org.limine.snapper.formats.json1.*;
import org.limine.snapper.formats.limine8.LimineKey;
import org.limine.snapper.formats.snapper.SnapperProperty;
import org.limine.snapper.objects.Config;
import org.limine.snapper.objects.EntryOptions;
import org.limine.snapper.objects.TreeNode;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;

public class LimineWriter {

    private final Config config;
    private final LimineReader limine;

    public LimineWriter(Config config, LimineReader limine) {
        this.config = config;
        this.limine = limine;
    }

    /**
     * Back up limine.conf
     */
    public void backup() {
        String limineConfigFilePath = Utility.buildPath(config.espPath(), Config.LIMINE_CONFIG_FILE);
        if (Utility.isFilePresent(limineConfigFilePath)) {
            // Auto time-based backup. Check if an older backup exists.
            String backupPath = limineConfigFilePath + Config.BACKUP_SUFFIX;
            boolean isBackupNeeded = true;
            if (Utility.isFilePresent(backupPath)) {
                isBackupNeeded = Utility.isFileOlderThan(backupPath, Config.CONFIG_BACKUP_THRESHOLD);
            }
            if (isBackupNeeded) {
                Utility.copyFile(limineConfigFilePath, backupPath, true, false, false);
            }
        }
    }

    public void setSnapshots(Manifest manifest) {
        if (!limine.isSnapshotsNodeFound()) {
            // The OS node does not have a "snapshots" node, so a default "//Snapshots" node is created.
            List<TreeNode> kernelNodes = limine.getTargetOsNode().nodes;
            TreeNode snapshotsNode = new TreeNode(Config.SPACES + LimineKey.SNAPSHOTS_INSIDE_OS_NODE, 1, limine.getTargetOsNode().name);
            kernelNodes.add(kernelNodes.size(), snapshotsNode);
        }

        TreeNode snapshotsNode = limine.getTargetOsNode().findNodeByName(LimineKey.SNAPSHOTS_INSIDE_OS_NODE.toString(), 1);
        if (snapshotsNode != null) {
            // Save all snapshots (from the manifest) in the Snapshots-node inside your OS node.
            this.overwriteSnapshotsNode(snapshotsNode, manifest.getSnapshotEntries(), true);
            // If there is no snapshot, then delete the snapshots-menu in the OS node.
            if (snapshotsNode.nodes.isEmpty()) {
                limine.getTargetOsNode().nodes.remove(snapshotsNode);
            }
            return;
        }

        snapshotsNode = limine.getRootNode().findNodeByName(LimineKey.SNAPSHOTS_OUTSIDE_OS_NODE.toString(), 1);
        if (snapshotsNode != null) {
            // Save all snapshots (from the manifest) in the Snapshots-node outside your OS node.
            this.overwriteSnapshotsNode(snapshotsNode, manifest.getSnapshotEntries(), false);
            return;
        }
        // Check if "//Snapshots" or "/Snapshots" exists in limine.conf
        String errMessage = "'" + LimineKey.SNAPSHOTS_INSIDE_OS_NODE + "' or '" + LimineKey.SNAPSHOTS_OUTSIDE_OS_NODE + "' is not found in " + Utility.buildPath(config.espPath(), Config.LIMINE_CONFIG_FILE);
        Utility.exitWithError(errMessage);
    }

    public void setTargetEntry(SnapshotEntry restoredOS) {
        // Save the restored entry (from the Limine snapshots) in the OS node.
        TreeNode snapshotsNode = limine.getTargetOsNode().findNodeByName(LimineKey.SNAPSHOTS_INSIDE_OS_NODE.toString(), 1);
        List<TreeNode> kernelNodes = createKernelNodes(restoredOS.kernelEntries(), LimineKey.SUB_ENTRY_KEY.toString(), 2, Config.TARGET_OS_NAME);
        limine.getTargetOsNode().nodes.clear();
        limine.getTargetOsNode().nodes.addAll(kernelNodes);

        if (snapshotsNode != null) {
            limine.getTargetOsNode().nodes.add(snapshotsNode);
        }
        limine.getTargetOsNode().enableLineBreak = true;
    }

    private List<TreeNode> createKernelNodes(List<KernelEntry> kernels, String entryKey, int depth, String parentNodeName) {
        List<TreeNode> nodes = new ArrayList<>();
        for (KernelEntry k : kernels) {
            String entryName = k.leadingSpaces() + entryKey + k.kernelVersion();
            TreeNode node = new TreeNode(entryName, depth, parentNodeName);
            node.configLines.addAll(k.allInConfig());
            if (k.subKernels() != null && !k.subKernels().isEmpty()) {
                String newEntryKey = entryKey + LimineKey.ENTRY_KEY;
                List<TreeNode> subNodes = createKernelNodes(k.subKernels(), newEntryKey, depth + 1, k.kernelVersion());
                node.nodes.addAll(subNodes);
            }
            nodes.add(node);
        }
        return nodes;
    }

    /**
     * Save the tree node in limine.conf
     */
    public void save() {
        List<String> lines = new ArrayList<>();
        this.writeNodeToLines(limine.getRootNode(), lines, 0);
        String limineConfigPath = Utility.buildPath(config.espPath(), Config.LIMINE_CONFIG_FILE);
        // Check if "/<esp>/limine.conf" exists.
        if (!Utility.isFilePresent(limineConfigPath)) {
            String errMessage = "File path: " + limineConfigPath + " does not exit!";
            Utility.exitWithError(errMessage);
            return;
        }
        Path limineTmpConfigPath = Paths.get(limineConfigPath + ".tmp");
        try {
            //Run commands before save
            if (Config.COMMANDS_BEFORE_SAVE != null && !Config.COMMANDS_BEFORE_SAVE.isEmpty()) {
                Utility.runCommand(Config.COMMANDS_BEFORE_SAVE, !Config.QUIET, false);
            }
            Files.write(limineTmpConfigPath, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
            Utility.moveFile(limineTmpConfigPath.toString(), limineConfigPath, true, false, false);
            Utility.runCommand("sync -f " + limineConfigPath, false, false);
            Utility.infoMessage("Updated:", limineConfigPath);
            //Run command after save
            if (Config.COMMANDS_AFTER_SAVE != null && !Config.COMMANDS_AFTER_SAVE.isEmpty()) {
                Utility.runCommand(Config.COMMANDS_AFTER_SAVE, !Config.QUIET, false);
            }
        } catch (IOException e) {
            String errMessage = "Error writing file: " + e.getMessage();
            Utility.exitWithError(errMessage);
        }
    }

    /**
     * Overwrite the updated snapshot entries in the snapshots-node.
     */
    private void overwriteSnapshotsNode(TreeNode snapshotsNode, List<SnapshotEntry> snapshotEntries, boolean isSnapshotsInsideOsNode) {
        if (snapshotsNode == null) {
            return;
        }
        int maxWidth = 0;
        if (!snapshotEntries.isEmpty()) {
            Integer lastSnapshotID = snapshotEntries.getLast().snapperEntry().snapshotID();
            maxWidth = String.valueOf(lastSnapshotID).length();
        }
        String snapshotsComment;
        if (0 <= Config.MAX_SNAPSHOT_ENTRIES && Config.MAX_SNAPSHOT_ENTRIES <= 10000) {
            snapshotsComment = snapshotEntries.size() + " / " + Config.MAX_SNAPSHOT_ENTRIES + " snapshots";
        } else {
            snapshotsComment = snapshotEntries.size() + " snapshots";
        }

        if (isSnapshotsInsideOsNode) {
            // Snapshots-node "//Snapshots" is inside your OS node
            snapshotsNode.configLines.clear();
            snapshotsNode.nodes.clear();
            snapshotsNode.addConfigLine(Config.SPACES + "### Auto-generated by limine-snapper-sync");
            snapshotsNode.addConfigLine(Config.SPACES + LimineKey.COMMENT + " " + snapshotsComment);
            snapshotsNode.enableLineBreak = true;

            for (SnapshotEntry snapshotEntry : snapshotEntries) {
                String description = snapshotEntry.snapperEntry().properties().get(SnapperProperty.DESCRIPTION.replace);
                if (description == null) {
                    description = "";
                }
                String snapshotName = this.snapshotNameFormat(maxWidth, snapshotEntry.snapperEntry().snapshotID(), snapshotEntry.snapperEntry().localTime(), description);
                TreeNode snapshotNode = new TreeNode(snapshotName, 3, Config.TARGET_OS_NAME);
                snapshotNode.addConfigLine(Config.SPACES + LimineKey.COMMENT + " " + description);
                for (KernelEntry kernel : snapshotEntry.kernelEntries()) {
                    TreeNode kernelNode = buildBootNode(kernel, LimineKey.SUB_X3_ENTRY_KEY.toString(), 4, "Snapshots");
                    if (!kernelNode.configLines.isEmpty() || !kernelNode.nodes.isEmpty()) {
                        snapshotNode.addNode(kernelNode);
                    }
                }
                if (!snapshotNode.nodes.isEmpty()) {
                    // Insert the snapshots-node at the beginning of the list.
                    snapshotsNode.nodes.addFirst(snapshotNode);
                }
            }

        } else {
            // Snapshots-node "/Snapshots" is outside your OS node.
            // Search your OS as a sub snapshots-node inside the snapshots-node.
            // If not available, create a new sub-snapshots-node of your OS inside the snapshots-node.
            TreeNode osNode = snapshotsNode.findOsNode(config.machineID(), Config.TARGET_OS_NAME, 1);
            if (osNode == null) {
                // Create a new node of your OS snapshots
                String osName = Config.SPACES + LimineKey.SUB_ENTRY_KEY + Config.TARGET_OS_NAME;
                osNode = new TreeNode(osName, 2, LimineKey.SNAPSHOTS_OUTSIDE_OS_NODE.toString());
                snapshotsNode.addNode(osNode);
            } else {
                osNode.configLines.clear();
                osNode.nodes.clear();
            }
            // Update the OS name - rename the old OS name to the new OS name
            osNode.name = Config.SPACES + LimineKey.SUB_ENTRY_KEY + Config.TARGET_OS_NAME;
            osNode.cleanName = Config.TARGET_OS_NAME;
            osNode.addConfigLine(Config.SPACES + "### Auto-generated by limine-snapper-sync");
            osNode.addConfigLine(Config.SPACES + LimineKey.COMMENT + " " + snapshotsComment);
            osNode.addConfigLine(Config.SPACES + LimineKey.COMMENT + " " + EntryOptions.Name.MACHINE_ID + config.machineID());
            osNode.enableLineBreak = true;

            for (SnapshotEntry snapshotEntry : snapshotEntries) {
                String description = snapshotEntry.snapperEntry().properties().get(SnapperProperty.DESCRIPTION.replace);
                if (description == null) {
                    description = "";
                }
                String snapshotName = this.snapshotNameFormat(maxWidth, snapshotEntry.snapperEntry().snapshotID(), snapshotEntry.snapperEntry().localTime(), description);
                TreeNode snapshotNode = new TreeNode(snapshotName, 3, Config.TARGET_OS_NAME);
                snapshotNode.addConfigLine(Config.SPACES + LimineKey.COMMENT + " " + description);
                for (KernelEntry kernel : snapshotEntry.kernelEntries()) {
                    TreeNode kernelNode = buildBootNode(kernel, LimineKey.SUB_X3_ENTRY_KEY.toString(), 4, "Snapshots");
                    if (!kernelNode.configLines.isEmpty() || !kernelNode.nodes.isEmpty()) {
                        snapshotNode.addNode(kernelNode);
                    }
                }
                if (!snapshotNode.nodes.isEmpty()) {
                    // Insert the snapshots-node at the beginning of the list.
                    osNode.nodes.addFirst(snapshotNode);
                }
            }
        }
    }

    private TreeNode buildBootNode(KernelEntry kernel, String entryKey, int depth, String parentName) {
        String kernelName = Config.SPACES + entryKey + kernel.kernelVersion();
        TreeNode kernelNode = new TreeNode(kernelName, 3, parentName);

        if (kernel.allInSnapshotConfig().isEmpty()) {
            for (OptionDetail option : kernel.optionDetails()) {
                kernelNode.configLines.add(Config.SPACES + option.getSnapshotLine());
            }
            for (ImageDetail image : kernel.imageDetails()) {
                kernelNode.configLines.add(Config.SPACES + image.getSnapshotLine());
            }
            for (CmdlineDetail cmdline : kernel.cmdlineDetails()) {
                kernelNode.configLines.add(Config.SPACES + cmdline.getSnapshotLine());
            }
        } else {
            for (String snapConfigLine : kernel.allInSnapshotConfig()) {
                kernelNode.configLines.add(Config.SPACES + snapConfigLine.trim());
            }
        }

        if (kernel.subKernels() != null && !kernel.subKernels().isEmpty()) {
            for (KernelEntry subKernel : kernel.subKernels()) {
                TreeNode subKernelNode = buildBootNode(subKernel, entryKey + LimineKey.ENTRY_KEY, depth + 1, kernel.kernelVersion());
                if (!subKernelNode.configLines.isEmpty() || !subKernelNode.nodes.isEmpty()) {
                    kernelNode.addNode(subKernelNode);
                }
            }
        }
        return kernelNode;
    }

    private String snapshotNameFormat(int maxWidth, Integer snapshotID, String timestamp, String description) {
        // Format snapshotID to be left-aligned with dynamic width.
        // Note: Do not use "right-aligned" as Limine removes right-aligned spaces by default.
        String formattedSnapshotID = String.format("%-" + maxWidth + "d", snapshotID);
        return switch (Config.SNAPSHOT_FORMAT_CHOICE) {
            case "1" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + formattedSnapshotID + "│" + timestamp;
            case "2" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + formattedSnapshotID + " │ " + timestamp;
            case "3" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + timestamp + "│" + snapshotID;
            case "4" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + timestamp + " │ " + snapshotID;
            case "5" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + timestamp;
            case "6" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + snapshotID;
            case "7" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + snapshotID + "│" + description;
            case "8" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + timestamp + "│" + description;
            // Note: "│" helps avoid escaping characters when matching Limine keys e.g, + or /
            case "9" -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + "│" + description;
            default -> Config.SPACES + LimineKey.SUB_X2_ENTRY_KEY + "ID=" + formattedSnapshotID + " " + timestamp;
        };
    }

    private void writeNodeToLines(TreeNode node, List<String> lines, int depth) {
        if (depth > 0) {
            lines.add(node.name);
        }
        lines.addAll(node.configLines);
        for (TreeNode childNode : node.nodes) {
            writeNodeToLines(childNode, lines, depth + 1);
        }
        if (node.enableLineBreak) {
            lines.add("");
        }
    }
}


