pragma Singleton

// Transition-only device monitoring. Resting state is intentionally silent.

import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Services.Pipewire
import QtQuick
import qs.config

Singleton {
    id: root

    property bool outputInitialized: false
    property bool bluetoothInitialized: false
    property bool kdeInitialized: false
    property bool powerInitialized: false
    property bool previousAcOnline: true
    property string previousOutput: ""
    property string previousBluetooth: ""
    property bool previousKdeReachable: false
    property string previousKdeName: ""

    PwObjectTracker { objects: [Pipewire.defaultAudioSink] }

    readonly property string outputName: {
        const sink = Pipewire.defaultAudioSink;
        return sink?.description || sink?.nickname || sink?.name || "";
    }

    readonly property string bluetoothNames: {
        const devices = Bluetooth.devices?.values ?? [];
        return devices.filter(device => device.connected)
            .map(device => device.name || device.address)
            .sort()
            .join("\u001f");
    }

    readonly property string kdeName: KdeConnect.preferredPhone?.name ?? ""

    onOutputNameChanged: {
        if (!root.outputInitialized) {
            root.previousOutput = root.outputName;
            return;
        }
        if (root.outputInitialized && root.outputName && root.outputName !== root.previousOutput) {
            StatusEvents.publish({
                key: "device-output",
                glyph: "\u{F07E7}",
                title: root.outputName,
                detail: "Audio output selected",
                priority: StatusEvents.ambientPriority
            });
        }
        root.previousOutput = root.outputName;
    }

    onBluetoothNamesChanged: {
        if (!root.bluetoothInitialized) {
            root.previousBluetooth = root.bluetoothNames;
            return;
        }
        if (root.bluetoothInitialized && root.bluetoothNames !== root.previousBluetooth) {
            const current = root.bluetoothNames ? root.bluetoothNames.split("\u001f") : [];
            const previous = root.previousBluetooth ? root.previousBluetooth.split("\u001f") : [];
            const connected = current.find(name => !previous.includes(name));
            const disconnected = previous.find(name => !current.includes(name));
            StatusEvents.publish({
                key: "device-bluetooth",
                glyph: "\u{F00B1}",
                title: connected || disconnected || "Bluetooth device",
                detail: connected ? "Connected" : "Disconnected",
                tone: connected ? "accent" : "warn",
                priority: StatusEvents.ambientPriority
            });
        }
        root.previousBluetooth = root.bluetoothNames;
    }

    Timer {
        id: discoverySettle
        interval: 1800
        onTriggered: {
            root.previousOutput = root.outputName;
            root.previousBluetooth = root.bluetoothNames;
            root.previousKdeReachable = KdeConnect.phoneReachable;
            root.previousKdeName = root.kdeName;
            root.previousAcOnline = Battery.acOnline;
            root.outputInitialized = true;
            root.bluetoothInitialized = true;
            root.kdeInitialized = true;
            root.powerInitialized = true;
        }
    }

    // The charger. Announced the way every other device transition here is:
    // once, on the change, and never as a description of the resting state.
    // Ambient priority, so Do Not Disturb quiets it -- a charger is exactly
    // the kind of thing DND is for.
    //
    // Absent entirely on a desktop, because Battery.acChanged only fires where
    // there is a battery to be on.
    Connections {
        target: Battery
        function onAcChanged(online: bool): void {
            if (!root.powerInitialized) {
                root.previousAcOnline = online;
                return;
            }
            if (online === root.previousAcOnline)
                return;
            root.previousAcOnline = online;
            StatusEvents.publish({
                key: "device-power",
                glyph: online ? "\u{F06A5}" : "\u{F0084}",
                title: online ? "Charging" : "On battery",
                detail: Battery.available ? Math.round(Battery.percent) + "%" : "",
                priority: StatusEvents.ambientPriority
            });
        }
    }

    // The "Warn at" threshold from the Power page. A quiet mention, not an
    // interruption: Do Not Disturb may still silence it, and the bar glyph
    // turns with it. For a long time this threshold changed only the glyph's
    // color, which nobody watching their work ever saw -- the first actual
    // message arrived at the critical level.
    Connections {
        target: Battery
        function onLowChanged(): void {
            if (!root.powerInitialized || !Battery.low || Battery.critical)
                return;
            StatusEvents.publish({
                key: "battery-low",
                glyph: "\u{F007A}",
                title: "Battery low",
                detail: Math.round(Battery.percent) + "% remaining",
                tone: "warning",
                priority: StatusEvents.importantPriority,
                actionId: "open-settings",
                actionData: "power"
            });
        }
    }

    // Running out is not ambient. Published at a priority Do Not Disturb does
    // not silence, because the one notification you must not miss is the one
    // saying the machine is about to stop.
    //
    // And then, unless told otherwise, the machine is put to sleep: a suspend
    // at the urgent threshold preserves the session for days, a hard cut at
    // 0% preserves nothing. The grace timer is what makes the warning above
    // readable and the action cancelable -- plugging in during it stands
    // everything down.
    Timer {
        id: criticalSuspend
        interval: 15000
        onTriggered: {
            if (Battery.critical && !Battery.acOnline
                    && DesktopPreferences.get("batteryCriticalAction") === "suspend")
                Quickshell.execDetached(["systemctl", "suspend"]);
        }
    }

    Connections {
        target: Battery
        function onAcOnlineChanged(): void {
            if (Battery.acOnline)
                criticalSuspend.stop();
        }
    }

    Connections {
        target: Battery
        function onCriticalChanged(): void {
            if (!root.powerInitialized || !Battery.critical)
                return;
            if (!Battery.acOnline
                    && DesktopPreferences.get("batteryCriticalAction") === "suspend")
                criticalSuspend.restart();
            StatusEvents.publish({
                key: "battery-critical",
                glyph: "\u{F0083}",
                title: "Battery critically low",
                detail: Math.round(Battery.percent) + "% remaining",
                tone: "danger",
                priority: StatusEvents.criticalPriority,
                actionId: "open-settings",
                actionData: "power"
            });
        }
    }

    Connections {
        target: KdeConnect

        function onPhoneReachableChanged(): void {
            if (!root.kdeInitialized)
                return;
            const reachable = KdeConnect.phoneReachable;
            if (reachable === root.previousKdeReachable)
                return;
            StatusEvents.publish({
                key: "device-kdeconnect",
                glyph: "\u{F03F2}",
                title: root.kdeName || root.previousKdeName || "Phone",
                detail: reachable ? "KDE Connect available" : "KDE Connect disconnected",
                tone: reachable ? "accent" : "warn",
                priority: StatusEvents.ambientPriority
            });
            root.previousKdeReachable = reachable;
            root.previousKdeName = root.kdeName || root.previousKdeName;
        }
    }

    Component.onCompleted: {
        root.previousOutput = root.outputName;
        root.previousBluetooth = root.bluetoothNames;
        root.previousKdeReachable = KdeConnect.phoneReachable;
        root.previousKdeName = root.kdeName;
        discoverySettle.start();
    }
}
