pragma Singleton

// Snapshots of Panama's durable settings stores.
//
// DesktopPreferences and HomePreferences use separate files. The helper owns
// the transactional filesystem boundary; this service owns settling the live
// desktop after those files have changed underneath it.
//
// HomePreferences intentionally keeps its FileView in Quickshell's private
// state directory while snapshots use Panama's canonical state directory. This
// service bridges them through HomePreferences' public mutation API, then soft
// reloads once external consumers have settled.

import Quickshell
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-settings-backup"

    property var snapshots: []
    property string lastError: ""
    property string lastAction: ""

    // Narrow service boundaries keep restore sequencing explicit and make it
    // possible to verify the real handler in an isolated shell without ever
    // calling the daily-driver compositor or wallpaper services.
    property var readHomeState: function() {
        return {
            initialized: HomePreferences.initialized,
            favorites: HomePreferences.favorites
        };
    }
    property var resetHome: function() { HomePreferences.resetHomeDefaults(); }
    property var initializeHome: function(ids) { HomePreferences.initialize(ids); }
    property var aliasHome: function(id, alias) { HomePreferences.setAlias(id, alias); }
    property var reloadDesktop: function() { DesktopPreferences.reload(); }
    property var readDisplays: function() { return DesktopPreferences.get("displays"); }
    property var protectDisplays: function(value) { return DesktopPreferences.set("displays", value); }
    property var displayBusy: function() { return Displays.busy || Displays.awaitingConfirmation; }
    property var setDisplayBlocked: function(blocked) { Displays.externalChangeBlocked = blocked; }
    property var applyCompositor: function() { SystemSettings.applyPersistedDisplayPolicy(); }
    property var reloadKeybinds: function() { Keybinds.applyReload(); }
    property var keybindsReloading: function() { return Keybinds.reloading; }
    property var systemBusy: function() { return SystemSettings.busy; }
    property var currentWallpaper: function() {
        return String(DesktopPreferences.get("wallpaperPath") ?? "");
    }
    property var applyWallpaper: function(path) { Wallpaper.set(path); }
    property var reloadShell: function() { Quickshell.reload(false); }
    property var protectedDisplays: ({})

    readonly property bool busy: listQuery.running || actionRun.running
        || applyRestoredState.running || settleReload.running

    Process {
        id: listQuery
        command: [root.helperPath, "list"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const parsed = JSON.parse(this.text);
                    root.snapshots = Array.isArray(parsed) ? parsed : [];
                    if (root.lastError === "Could not read the list of snapshots.")
                        root.lastError = "";
                } catch (error) {
                    root.lastError = "Could not read the list of snapshots.";
                }
            }
        }
    }

    Process {
        id: actionRun
        property bool restoring: false
        property string outputText: ""
        stdout: StdioCollector {
            onStreamFinished: actionRun.outputText = this.text
        }
        onStarted: actionRun.outputText = ""
        onExited: (exitCode, exitStatus) => {
            if (exitCode !== 0) {
                root.lastError = actionRun.restoring
                    ? "That snapshot could not be restored."
                    : "The settings could not be backed up.";
                if (actionRun.restoring) {
                    root.setDisplayBlocked(false);
                    root.protectedDisplays = ({});
                }
                return;
            }
            root.lastAction = actionRun.restoring ? "restored" : "saved";
            if (actionRun.restoring) {
                const homeReloaded = root.handleRestoreOutput(actionRun.outputText);
                root.lastError = homeReloaded
                    ? ""
                    : "Desktop settings were restored, but Home favourites could not be reloaded.";
                if (!homeReloaded) {
                    root.setDisplayBlocked(false);
                    root.protectedDisplays = ({});
                }
            } else
                root.lastError = "";
            root.refresh();
        }
    }

    Timer {
        id: applyRestoredState
        interval: 80
        repeat: false
        onTriggered: {
            // DesktopPreferences.reload() invalidates reactive shell bindings.
            // These services also own state outside QML and need an explicit
            // replay: compositor options, Lua-generated binds, and hyprpaper.
            root.applyCompositor();
            root.reloadKeybinds();
            root.applyWallpaper(root.currentWallpaper());

            settleReload.attempts = 0;
            settleReload.restart();
        }
    }

    Timer {
        id: settleReload
        property int attempts: 0
        interval: 100
        repeat: true
        onTriggered: {
            attempts++;
            // Let the current instances finish their external writes before a
            // soft reload replaces them. The cap keeps a failed external tool
            // from leaving restored Home state stale indefinitely.
            if ((!root.keybindsReloading() && !root.systemBusy()) || attempts >= 30) {
                stop();
                root.setDisplayBlocked(false);
                root.protectedDisplays = ({});
                root.reloadShell();
            }
        }
    }

    Component.onCompleted: root.refresh()

    function refresh(): void {
        if (!listQuery.running)
            listQuery.running = true;
    }

    function save(): void {
        if (actionRun.running)
            return;
        actionRun.restoring = false;
        actionRun.exec([root.helperPath, "save", root.serialiseHomeState()]);
    }

    function serialiseHomeState(): string {
        const current = root.readHomeState();
        const favorites = [];
        for (const favorite of current.favorites ?? []) {
            favorites.push({
                id: String(favorite.id ?? ""),
                alias: String(favorite.alias ?? "")
            });
        }
        return JSON.stringify({
            initialized: current.initialized === true,
            favorites: favorites
        });
    }

    function handleRestoreOutput(text: string): bool {
        if (!root.reloadHomeState(text))
            return false;
        root.reloadDesktop();
        if (!root.protectDisplays(root.protectedDisplays))
            return false;
        applyRestoredState.restart();
        return true;
    }

    // Restore output carries the canonical Home state. Reconstructing through
    // these methods keeps validation and persistence inside HomePreferences;
    // this service never mutates its aliases or private FileView directly.
    function reloadHomeState(text: string): bool {
        try {
            const result = JSON.parse(text);
            const restored = result?.home;
            if (!restored || restored.preserve === true)
                return true;

            if (restored.present !== true)
                return restored.present === false
                    ? root.resetHomeState()
                    : false;

            const data = restored.data;
            if (!data || typeof data.initialized !== "boolean" || !Array.isArray(data.favorites))
                return false;
            const ids = [];
            const aliases = [];
            const seen = {};
            for (const favorite of data.favorites) {
                const id = favorite?.id;
                const alias = favorite?.alias;
                if (typeof id !== "string" || !/^light\.[a-z0-9_]+$/.test(id)
                        || typeof alias !== "string" || seen[id])
                    return false;
                seen[id] = true;
                ids.push(id);
                aliases.push(alias);
            }
            if (!data.initialized && ids.length > 0)
                return false;

            root.resetHome();
            if (!data.initialized)
                return true;
            root.initializeHome(ids);
            for (let index = 0; index < ids.length; index++)
                root.aliasHome(ids[index], aliases[index]);
            return true;
        } catch (error) {
            return false;
        }
    }

    function resetHomeState(): bool {
        root.resetHome();
        return true;
    }

    // The name is matched against the snapshot list rather than trusted, so no
    // caller-supplied path reaches the helper even though it validates as well.
    function restore(name: string): bool {
        if (actionRun.running)
            return false;
        if (root.displayBusy()) {
            root.lastError = "Finish the current display change before restoring settings.";
            return false;
        }
        if (!root.snapshots.some(snapshot => snapshot.name === name)) {
            root.lastError = "That snapshot is not in the list.";
            return false;
        }
        const currentDisplays = root.readDisplays();
        root.protectedDisplays = JSON.parse(JSON.stringify(
            currentDisplays && typeof currentDisplays === "object" ? currentDisplays : {}));
        root.setDisplayBlocked(true);
        actionRun.restoring = true;
        actionRun.exec([root.helperPath, "restore", name]);
        return true;
    }
}
