pragma Singleton

// Shared KDE Connect state. The Python helper owns all CLI and D-Bus parsing;
// this singleton exposes only normalized devices and user-initiated actions.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property bool fixtureMode: false
    property bool available: false
    property var devices: []
    property string lastError: ""

    property bool transferActive: false
    property string transferFileName: ""
    property string transferDeviceName: ""
    property var recentExchange: null

    property string actionKind: ""
    property string actionPath: ""

    // actionProc's `exited` and its stdout `streamFinished` are not guaranteed
    // to fire in a particular order (same hazard HomeAssistantConfig.qml's
    // settle-both pattern guards against). These track which of the two have
    // been observed for the action currently in flight so finishAction() is
    // only ever called once both have arrived, with the real stdout JSON as
    // the authoritative result.
    property bool actionExited: false
    property bool actionStdoutDone: false
    property string actionStdoutText: ""

    readonly property var preferredPhone: {
        const phones = root.devices.filter(device => device.type === "phone" && device.paired);
        return phones.find(device => device.reachable) ?? phones[0] ?? null;
    }
    readonly property bool phoneReachable: root.preferredPhone?.reachable === true
    readonly property int pairedCount: root.devices.filter(device => device.paired).length
    readonly property var phoneActions: root.preferredPhone?.actions ?? []

    function supports(action: string): bool {
        return root.phoneActions.indexOf(action) >= 0;
    }

    function refresh(): void {
        if (root.fixtureMode || statusProc.running)
            return;
        statusProc.running = true;
    }

    function consumeStatus(text: string): void {
        if (root.fixtureMode)
            return;
        try {
            const result = JSON.parse(text);
            root.available = result.available === true;
            root.devices = Array.isArray(result.devices) ? result.devices : [];
            root.lastError = String(result.error ?? "");
        } catch (error) {
            root.available = false;
            root.devices = [];
            root.lastError = "invalid-response";
        }
    }

    function sendFile(path: string): void {
        if (!root.phoneReachable || !root.supports("share") || path === "" || actionProc.running)
            return;
        root.actionKind = "share";
        root.actionPath = path;
        root.transferActive = true;
        root.transferFileName = path.split("/").pop();
        root.transferDeviceName = root.preferredPhone.name;
        root.actionExited = false;
        root.actionStdoutDone = false;
        actionProc.command = [root.helperPath, "send-file", root.preferredPhone.id, path];
        actionProc.running = true;
    }

    function sendClipboard(): void {
        root.runSimpleAction("clipboard", "send-clipboard");
    }

    function ring(): void {
        root.runSimpleAction("ring", "ring");
    }

    function runSimpleAction(kind: string, command: string): void {
        if (!root.phoneReachable || !root.supports(kind) || actionProc.running)
            return;
        root.actionKind = kind;
        root.actionPath = "";
        root.actionExited = false;
        root.actionStdoutDone = false;
        actionProc.command = [root.helperPath, command, root.preferredPhone.id];
        actionProc.running = true;
    }

    function cancelTransfer(): void {
        if (actionProc.running && root.actionKind === "share")
            actionProc.signal(2);
        root.actionKind = "";
        root.actionPath = "";
        root.transferActive = false;
        root.transferFileName = "";
        root.transferDeviceName = "";
        root.actionExited = false;
        root.actionStdoutDone = false;
    }

    // Called from both actionProc.onExited and its stdout streamFinished.
    // Only finalizes once both signals have arrived for the in-flight action,
    // since their firing order is not guaranteed -- see the actionExited /
    // actionStdoutDone comment above.
    function settleAction(): void {
        if (root.actionKind === "")
            return;
        if (!root.actionExited || !root.actionStdoutDone)
            return;
        root.actionExited = false;
        root.actionStdoutDone = false;
        root.finishAction(root.actionStdoutText);
    }

    function finishAction(text: string): void {
        if (root.actionKind === "")
            return;

        const kind = root.actionKind;
        const localPath = root.actionPath;
        const fileName = root.transferFileName;
        const deviceName = root.transferDeviceName || root.preferredPhone?.name || "Phone";
        let result = null;
        try {
            result = JSON.parse(text);
        } catch (error) {
            result = { ok: false, error: "invalid-response" };
        }

        root.actionKind = "";
        root.actionPath = "";
        if (kind === "share") {
            root.transferActive = false;
            root.transferFileName = "";
            root.transferDeviceName = "";
        }

        if (result.ok === true && kind === "share") {
            root.recentExchange = {
                direction: "sent",
                fileName: String(result.fileName || fileName),
                deviceName,
                timestamp: Date.now()
            };
            StatusEvents.publish({
                key: "phone-transfer",
                glyph: "\u{F03F2}",
                title: "Sent to " + deviceName,
                detail: String(result.fileName || fileName),
                tone: "ok",
                priority: StatusEvents.importantPriority,
                actionId: localPath !== "" ? "open-path" : "",
                actionData: localPath
            });
        } else if (result.ok !== true) {
            root.lastError = String(result.error || "action-failed");
            StatusEvents.publish({
                key: "phone-action",
                glyph: "\u{F03F2}",
                title: kind === "share" ? "File was not sent" : "Phone action failed",
                detail: "Check that the phone is nearby and try again",
                tone: "warn",
                priority: StatusEvents.importantPriority
            });
        }
        refreshDelay.restart();
    }

    function applyFixture(name: string): void {
        if (["reachable", "offline", "transfer"].indexOf(name) < 0)
            return;
        root.fixtureMode = true;
        root.available = true;
        root.lastError = "";
        root.recentExchange = null;
        root.devices = [{
            id: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
            name: "Fixture iPhone",
            type: "phone",
            paired: true,
            reachable: name !== "offline",
            actions: ["clipboard", "ping", "ring", "share"]
        }];
        root.transferActive = name === "transfer";
        root.transferFileName = name === "transfer" ? "Fixture document.pdf" : "";
        root.transferDeviceName = name === "transfer" ? "Fixture iPhone" : "";
        root.actionKind = name === "transfer" ? "share" : "";
        root.actionPath = "";
    }

    function clearFixture(): void {
        root.fixtureMode = false;
        root.available = false;
        root.devices = [];
        root.lastError = "";
        root.transferActive = false;
        root.transferFileName = "";
        root.transferDeviceName = "";
        root.actionKind = "";
        root.actionPath = "";
        root.recentExchange = null;
        root.refresh();
    }

    Process {
        id: statusProc
        command: [root.helperPath, "status"]
        stdout: StdioCollector {
            onStreamFinished: root.consumeStatus(this.text)
        }
    }

    Process {
        id: actionProc
        stdout: StdioCollector {
            onStreamFinished: {
                root.actionStdoutDone = true;
                root.actionStdoutText = this.text;
                root.settleAction();
            }
        }
        onExited: (code, status) => {
            root.actionExited = true;
            root.settleAction();
        }
    }

    Timer {
        interval: 30000
        repeat: true
        running: !root.fixtureMode
        triggeredOnStart: true
        onTriggered: root.refresh()
    }

    Timer {
        id: refreshDelay
        interval: 500
        onTriggered: root.refresh()
    }
}
