pragma Singleton

// Network and Bluetooth state for the settings page.
//
// The hard parts -- scanning, joining, pairing -- already work in the quick
// settings panel through Quickshell.Networking and Quickshell.Bluetooth, which
// speak to NetworkManager and BlueZ over DBus. Nothing here shells out to nmcli
// or bluetoothctl, and nothing should: the founding requirement for this
// desktop was never having to drop to a terminal to join a network.
//
// This exists so the page does not have to reach into those modules for the
// same derived values the panel already computes, and so scanning is driven by
// whether the page is actually on screen. Scanning while nobody is looking is
// radio time and battery spent on a list that is not being read.

import Quickshell
import Quickshell.Networking
import Quickshell.Bluetooth
import QtQuick

Singleton {
    id: root

    // Set by the page while it is visible; drives both scanners.
    property bool active: false

    readonly property var wifiDevice: {
        for (const device of Networking.devices.values) {
            if (device.type === DeviceType.Wifi)
                return device;
        }
        return null;
    }

    readonly property var wiredDevice: {
        let fallback = null;
        for (const device of Networking.devices.values) {
            if (device.type !== DeviceType.Wired)
                continue;
            if (device.connected)
                return device;
            if (!fallback)
                fallback = device;
        }
        return fallback;
    }

    // Whether the wired connection is on, as a person means it: carrying
    // traffic, and set to come back by itself.
    readonly property bool wiredOn: root.wiredDevice !== null
        && root.wiredDevice.connected

    // Deliberately NOT gated on hasLink. That property reads false while the
    // device is merely disconnected -- NetworkManager still reports the carrier
    // as on -- so using it to decide whether the switch works built a trap
    // door: turning Ethernet off made the switch disable itself, claim "no
    // cable", and leave no way to turn it back on. A wired device that exists
    // can always be asked to come up; if there is genuinely no cable, the
    // attempt fails and says so, which is the honest failure.
    readonly property bool wiredAvailable: root.wiredDevice !== null

    // Turning wired networking off means both halves. Disconnecting alone lasts
    // about a second: NetworkManager sees a managed device with autoconnect set
    // and immediately brings it back, so the switch would flip itself on again
    // and read as broken.
    function setWired(enabled: bool): void {
        const device = root.wiredDevice;
        if (!device)
            return;
        device.autoconnect = enabled;
        if (enabled) {
            // With autoconnect restored NetworkManager will usually bring it up
            // on its own; asking directly makes it immediate rather than
            // whenever the daemon next looks.
            if (device.network)
                device.network.connect();
        } else {
            device.disconnect();
        }
    }

    readonly property var adapter: Bluetooth.defaultAdapter

    readonly property bool wifiEnabled: Networking.wifiEnabled
    readonly property bool wifiAvailable: Networking.wifiHardwareEnabled

    // Current network, then saved, then by signal -- the order GNOME uses,
    // which is the order you actually look for things in.
    readonly property var networks: {
        if (!root.wifiDevice || !root.wifiDevice.networks)
            return [];
        const list = root.wifiDevice.networks.values.slice();
        list.sort((a, b) => {
            if (a.connected !== b.connected)
                return a.connected ? -1 : 1;
            if (a.known !== b.known)
                return a.known ? -1 : 1;
            return b.signalStrength - a.signalStrength;
        });
        return list;
    }

    readonly property var savedNetworks: root.networks.filter(network => network.known)

    readonly property var bluetoothDevices: {
        if (!Bluetooth.devices)
            return [];
        const list = Bluetooth.devices.values.slice();
        list.sort((a, b) => {
            if (a.connected !== b.connected)
                return a.connected ? -1 : 1;
            if (a.paired !== b.paired)
                return a.paired ? -1 : 1;
            return String(a.name || "").localeCompare(String(b.name || ""));
        });
        return list;
    }

    readonly property var activeNetwork: root.networks.find(network => network.connected) ?? null

    function isSecured(network: var): bool {
        return network.security !== WifiSecurityType.Open
            && network.security !== WifiSecurityType.Owe
            && network.security !== WifiSecurityType.Unknown;
    }

    function securityLabel(network: var): string {
        if (!root.isSecured(network))
            return "Open";
        switch (network.security) {
        case WifiSecurityType.Wep: return "WEP";
        case WifiSecurityType.Wpa: return "WPA";
        case WifiSecurityType.Wpa2: return "WPA2";
        case WifiSecurityType.Wpa3: return "WPA3";
        case WifiSecurityType.Enterprise: return "Enterprise";
        }
        return "Secured";
    }

    // Four bars is what people read signal as, so bucket rather than showing a
    // percentage that changes every scan and means nothing to anyone.
    //
    // signalStrength is 0.0-1.0, NOT a percentage. Treating it as 0-100 puts
    // every network including the connected one in the bottom bucket, which is
    // exactly as useless as showing nothing. Thresholds match the icon buckets
    // in modules/quicksettings/WifiList.qml so the two never disagree.
    function signalLabel(strength: real): string {
        if (strength >= 0.8) return "Excellent";
        if (strength >= 0.55) return "Good";
        if (strength >= 0.3) return "Fair";
        if (strength > 0.05) return "Weak";
        return "No signal";
    }

    function connectionFailureText(reason: var): string {
        switch (reason) {
        case ConnectionFailReason.WifiAuthTimeout:
        case ConnectionFailReason.Authentication:
            return "Wrong password";
        case ConnectionFailReason.WifiNetworkLost:
            return "Network out of range";
        }
        return "Could not connect";
    }

    // Scanning follows visibility. NetworkManager keeps scanning as long as it
    // is asked to, and Bluetooth discovery is worse -- it holds the radio.
    function syncScanners(): void {
        if (root.wifiDevice)
            root.wifiDevice.scannerEnabled = root.active && root.wifiEnabled;

        if (root.adapter && root.adapter.enabled) {
            const shouldDiscover = root.active;
            if (root.adapter.discovering !== shouldDiscover)
                root.adapter.discovering = shouldDiscover;
        }
    }

    onActiveChanged: root.syncScanners()
    onWifiDeviceChanged: root.syncScanners()
    onWifiEnabledChanged: root.syncScanners()
    onAdapterChanged: root.syncScanners()

    // adapter.enabled has no property on root to bind onXChanged to, so it
    // needs its own Connections -- the Bluetooth equivalent of onWifiEnabledChanged.
    Connections {
        target: root.adapter
        function onEnabledChanged(): void { root.syncScanners(); }
    }
}
