pragma Singleton

// The firewall, answered as "what can another machine reach?"
//
// Listing zones and services is what firewall-cmd already does. The question it
// does not answer needs both halves at once: a port is reachable only when
// something is listening on a network address AND the firewall permits it.
// Either alone tells you nothing, which is how a tidy set of rules coexists
// with an exposed database.
//
// Changes go through firewall-cmd, which is polkit-aware, so they prompt --
// through Panama's own prompt now.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property bool running: false
    property bool enabledAtBoot: false
    property bool available: false
    property string defaultZone: ""
    property var allZones: []
    property var activeZones: ({})
    property var zones: []
    property var exposed: []
    property var exposedDataStores: []
    property int sshSessions: 0
    property bool scanned: false
    property string lastError: ""

    readonly property bool busy: query.running || mutation.running

    readonly property var zone: root.zones.length > 0 ? root.zones[0] : null

    // The range Fedora Workstation opens by default, if this zone has it. Named
    // separately because it is the single rule that explains almost everything
    // on the exposed list.
    readonly property var openRanges: (root.zone?.ports ?? []).filter(
        spec => String(spec).indexOf("-") > 0)

    readonly property bool wideOpen: root.openRanges.length > 0

    function serviceCount(): int { return (root.zone?.services ?? []).length; }

    function allowedByRange(entry: var): bool {
        return String(entry?.allowedBy ?? "").indexOf("range") >= 0;
    }

    // What closing the open range would cut off, by name, so the consequence is
    // stated before it happens rather than discovered afterwards.
    function rangeDependents(): var {
        return root.exposed.filter(entry => root.allowedByRange(entry));
    }

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

    function absorb(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.running = parsed.running === true;
            root.enabledAtBoot = parsed.enabledAtBoot === true;
            root.available = parsed.available === true;
            root.defaultZone = String(parsed.defaultZone ?? "");
            root.allZones = Array.isArray(parsed.allZones) ? parsed.allZones : [];
            root.activeZones = parsed.activeZones ?? ({});
            root.zones = Array.isArray(parsed.zones) ? parsed.zones : [];
            root.exposed = Array.isArray(parsed.exposed) ? parsed.exposed : [];
            root.exposedDataStores = Array.isArray(parsed.exposedDataStores)
                ? parsed.exposedDataStores : [];
            root.sshSessions = Number(parsed.sshSessions ?? 0);
            root.lastError = String(parsed.error ?? "");
        } catch (error) {
            root.lastError = "Could not read the firewall's state.";
            console.warn("Firewall: could not parse helper output:", error);
        }
        root.scanned = true;
    }

    function run(arguments: var): void {
        if (mutation.running)
            return;
        root.lastError = "";
        mutation.command = [root.helperPath].concat(arguments);
        mutation.running = true;
    }

    function removeService(name: string): void { root.run(["remove-service", name]); }
    function addService(name: string): void { root.run(["add-service", name]); }
    function removePort(spec: string): void { root.run(["remove-port", spec]); }
    function addPort(spec: string): void { root.run(["add-port", spec]); }
    function setZone(interfaceName: string, zoneName: string): void {
        root.run(["set-zone", interfaceName, zoneName]);
    }
    function setDefaultZone(zoneName: string): void { root.run(["set-default-zone", zoneName]); }

    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: mutation
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
    }
}
