pragma Singleton

// Software updates, from the three sources this machine actually uses.
//
// They are counted and applied separately because they fail separately: a
// flatpak mirror being down says nothing about whether a kernel security fix is
// waiting. Blending them into one number would hide exactly the case that
// matters.
//
// Checking costs about nine seconds. So the page opens on the last result and
// says when that was, the way every mature updater does, and refreshes in the
// background rather than making someone watch a spinner to learn there is
// nothing to do.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property var dnf: ({})
    property var flatpak: ({})
    property var firmware: ({})
    property var kernel: ({})
    property var automatic: ({})
    property int checkedAt: 0
    property bool scanned: false
    property string lastError: ""

    // Set after a successful apply, so the page can say what was done and
    // whether a restore point was taken.
    property var lastApplied: null

    // Guards read the Process objects directly; a derived binding is stale
    // inside the handler that changes it. See DefaultApps.qml.
    readonly property bool checking: checkProcess.running
    readonly property bool applying: applyProcess.running
    readonly property bool busy: root.checking || root.applying

    readonly property int total: Number(root.dnf?.count ?? 0)
        + Number(root.flatpak?.count ?? 0)
        + Number(root.firmware?.count ?? 0)

    readonly property int securityCount: Number(root.dnf?.securityCount ?? 0)
    readonly property bool rebootNeeded: root.kernel?.rebootNeeded === true
    readonly property bool everChecked: root.checkedAt > 0

    // What has actually been installed, newest first, from both sources at
    // once. Loaded on demand rather than with the snapshot: it asks dnf and
    // flatpak for their whole transaction log, which is not worth doing every
    // time the page opens.
    property var history: []
    property bool historyLoaded: false
    readonly property bool loadingHistory: historyProcess.running

    function loadHistory(): void {
        if (historyProcess.running)
            return;
        historyProcess.command = [root.helperPath, "history"];
        historyProcess.running = true;
    }

    function absorbHistory(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.history = Array.isArray(parsed.entries) ? parsed.entries : [];
        } catch (error) {
            console.warn("Updates: could not parse history:", error);
            root.history = [];
        }
        root.historyLoaded = true;
    }

    // "3 packages" reads better than a raw count next to a command line.
    function describeHistory(entry: var): string {
        const count = Number(entry.count ?? 0);
        if (entry.source === "flatpak")
            return "Flatpak";
        return count === 1 ? "1 package" : count + " packages";
    }

    // A count nobody has verified is not a count. Saying "up to date" on the
    // strength of a check that never ran is the one wrong answer that looks
    // reassuring.
    function summary(): string {
        if (!root.everChecked)
            return "Not checked yet";
        if (root.total === 0)
            return "Up to date";
        return root.total + " update" + (root.total === 1 ? "" : "s") + " available";
    }

    // The same shape as lastCheckedText, for an arbitrary moment. Kept beside it
    // so the two never drift into describing time differently on one page.
    function agoText(epochSeconds: int): string {
        if (!(epochSeconds > 0))
            return "at an unknown time";
        const seconds = Math.max(0, Math.floor(Date.now() / 1000) - epochSeconds);
        if (seconds < 90)
            return "just now";
        if (seconds < 3600)
            return Math.floor(seconds / 60) + " minutes ago";
        if (seconds < 172800)
            return Math.floor(seconds / 3600) + " hours ago";
        return Math.floor(seconds / 86400) + " days ago";
    }

    function lastCheckedText(): string {
        if (!root.everChecked)
            return "Never checked";
        const seconds = Math.max(0, Math.floor(Date.now() / 1000) - root.checkedAt);
        if (seconds < 90)
            return "Checked just now";
        if (seconds < 3600)
            return "Checked " + Math.floor(seconds / 60) + " minutes ago";
        if (seconds < 172800)
            return "Checked " + Math.floor(seconds / 3600) + " hours ago";
        return "Checked " + Math.floor(seconds / 86400) + " days ago";
    }

    function sourceLabel(source: string): string {
        switch (source) {
        case "dnf": return "System packages";
        case "flatpak": return "Applications";
        case "firmware": return "Firmware";
        default: return source;
        }
    }

    function refresh(): void {
        if (query.running)
            return;
        query.command = [root.helperPath, "snapshot"];
        query.running = true;
    }

    // The slow one, on request.
    function check(): void {
        if (checkProcess.running)
            return;
        root.lastError = "";
        checkProcess.command = [root.helperPath, "check"];
        checkProcess.running = true;
    }

    function apply(source: string): void {
        if (applyProcess.running)
            return;
        root.lastError = "";
        root.lastApplied = null;
        applyProcess.command = [root.helperPath, "apply", source];
        applyProcess.running = true;
    }

    function setAutomaticDnf(enabled: bool): void {
        if (applyProcess.running)
            return;
        root.lastError = "";
        applyProcess.command = [root.helperPath, "set-auto-dnf", enabled ? "true" : "false"];
        applyProcess.running = true;
    }

    function setAutomaticFlatpak(enabled: bool): void {
        if (applyProcess.running)
            return;
        root.lastError = "";
        applyProcess.command = [root.helperPath, "set-auto-flatpak", enabled ? "true" : "false"];
        applyProcess.running = true;
    }

    function absorb(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.dnf = parsed.dnf ?? ({});
            root.flatpak = parsed.flatpak ?? ({});
            root.firmware = parsed.firmware ?? ({});
            root.kernel = parsed.kernel ?? ({});
            root.automatic = parsed.automatic ?? ({});
            root.checkedAt = Number(parsed.checkedAt ?? 0);
            root.lastError = String(parsed.error ?? "");
            if (parsed.applied)
                root.lastApplied = parsed.applied;
        } catch (error) {
            root.lastError = "Could not read the update helper's answer.";
            console.warn("Updates: could not parse helper output:", error);
        }
        root.scanned = true;
    }

    Component.onCompleted: root.refresh()

    Process {
        id: query
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: checkProcess
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: applyProcess
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }

    Process {
        id: historyProcess
        stdout: StdioCollector { onStreamFinished: root.absorbHistory(this.text) }
    }
}
