pragma Singleton

// VPN and WireGuard connections, for the quick-settings toggle and the bar.
//
// Everything goes through scripts/panama-vpn, the one sanctioned nmcli
// exception (see the note there and in Connectivity.qml): Quickshell's
// Networking module has no VPN surface yet. Nothing is stored in settings.json
// -- NetworkManager owns which profiles exist and which are active, and a
// stored copy would just be restored over whatever the daemon knows.
//
// The design requirement, learned the hard way: activating a profile whose
// server is unreachable must never leave the machine stranded. The helper
// bounds activation and rolls a failed attempt back down; this service only
// reports what actually happened.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    // [{name, uuid, kind, active, timestamp}]
    property var connections: []
    property bool scanned: false
    property bool busy: false
    property string lastError: ""

    readonly property bool available: root.connections.length > 0
    readonly property var activeConnections: root.connections.filter(connection => connection.active)
    readonly property bool anyActive: root.activeConnections.length > 0

    // What the toggle's sublabel shows: the one active name, or how many.
    readonly property string activeSummary: {
        if (root.activeConnections.length === 0)
            return "";
        if (root.activeConnections.length === 1)
            return root.activeConnections[0].name;
        return root.activeConnections.length + " active";
    }

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

    function setActive(uuid: string, on: bool): void {
        if (root.busy)
            return;
        root.busy = true;
        root.lastError = "";
        apply.command = [root.helperPath, on ? "up" : "down", uuid];
        apply.running = true;
    }

    // The main toggle: anything active goes down; nothing active brings up the
    // profile most recently used, which is GNOME's behavior and almost always
    // what was meant on a machine with more than one profile.
    function toggle(): void {
        if (root.anyActive) {
            root.setActive(root.activeConnections[0].uuid, false);
            return;
        }
        const preferred = root.connections.reduce((best, candidate) =>
            best === null || candidate.timestamp > best.timestamp ? candidate : best, null);
        if (preferred !== null)
            root.setActive(preferred.uuid, true);
    }

    Process {
        id: query
        command: [root.helperPath, "list"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const parsed = JSON.parse(this.text);
                    root.connections = Array.isArray(parsed.connections) ? parsed.connections : [];
                    if (String(parsed.error ?? "") !== "")
                        root.lastError = String(parsed.error);
                } catch (error) {
                    root.connections = [];
                    root.lastError = "Could not read the VPN helper's output.";
                    console.warn("Vpn: could not parse helper output:", error);
                }
                root.scanned = true;
            }
        }
    }

    Process {
        id: apply
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
        // Re-read rather than assuming: activation can fail, and the helper
        // may have rolled a failed attempt back down.
        onExited: {
            root.busy = false;
            root.refresh();
        }
    }

    // NetworkManager state can change under us -- nmcli in a terminal, a
    // connection dropping, another device editing profiles. `nmcli monitor`
    // emits a line per event; the refresh is coalesced because one action
    // often produces several lines in a burst.
    Process {
        id: monitor
        command: ["nmcli", "monitor"]
        running: true
        stdout: SplitParser {
            onRead: refreshDebounce.restart()
        }
        // If NetworkManager restarts, the monitor exits; come back gently
        // rather than spinning against a daemon that is still down.
        onExited: monitorRestart.restart()
    }

    Timer {
        id: refreshDebounce
        interval: 400
        onTriggered: root.refresh()
    }

    Timer {
        id: monitorRestart
        interval: 3000
        onTriggered: monitor.running = true
    }

    Component.onCompleted: root.refresh()
}
