pragma Singleton

// ─────────────────────────────────────────────────────────────────────────────
// Dictation: the two pieces that have to be present before holding the key does
// anything, and the download of the one that is not a package.
//
// Neither piece is installed by ./install, deliberately. The model is half a
// gigabyte that no repository carries, and the speech server is a container
// image compiled from upstream -- both are slow, both want the network, and an
// installer that quietly does either is the surprise the interview exists to
// prevent. So this reports what is missing and offers the part it can do.
//
// Status is read from the helper rather than inferred. Whether an image exists
// and whether a server is answering are facts about the machine, and a page
// that guessed at them would be confidently wrong on exactly the machine where
// somebody is trying to work out why dictation is silent.
// ─────────────────────────────────────────────────────────────────────────────

import Quickshell
import Quickshell.Io
import QtQuick
import qs.config

Singleton {
    id: root

    readonly property string helper: Quickshell.shellDir + "/scripts/panama-dictate"

    property bool modelInstalled: false
    property bool imageBuilt: false
    property bool serverReady: false
    property bool typingAvailable: false
    property int modelBytes: 0

    property bool downloading: false
    property int downloadedBytes: 0
    property int downloadTotalBytes: 0
    property string lastError: ""

    readonly property bool ready: root.modelInstalled && root.imageBuilt

    readonly property real downloadFraction: root.downloadTotalBytes > 0
        ? Math.min(1, root.downloadedBytes / root.downloadTotalBytes)
        : 0

    // What is missing, in the order it has to be fixed. The image first: the
    // model is useless without something to run it, and telling somebody to
    // download half a gigabyte before mentioning they also need to build an
    // image would be two trips.
    readonly property string missing: {
        if (!root.imageBuilt)
            return "image";
        if (!root.modelInstalled)
            return "model";
        return "";
    }

    Process {
        id: statusRun
        command: [root.helper, "status"]
        stdout: StdioCollector {
            onStreamFinished: {
                try {
                    const state = JSON.parse(this.text);
                    root.modelInstalled = state.modelInstalled === true;
                    root.imageBuilt = state.imageBuilt === true;
                    root.serverReady = state.serverReady === true;
                    root.typingAvailable = state.typingAvailable === true;
                    root.modelBytes = state.modelBytes ?? 0;
                } catch (error) {
                    root.lastError = "Could not read dictation status.";
                }
            }
        }
    }

    Process {
        id: downloadRun
        command: [root.helper, "download"]
        stdout: SplitParser {
            // One JSON object per line, roughly once a second, so the progress
            // bar moves without the helper flooding a pipe nobody drains.
            onRead: line => {
                try {
                    const update = JSON.parse(line);
                    if (update.ok === false) {
                        root.lastError = "The model could not be downloaded.";
                        return;
                    }
                    if (update.bytes !== undefined)
                        root.downloadedBytes = update.bytes;
                    if (update.total !== undefined)
                        root.downloadTotalBytes = update.total;
                } catch (error) {
                    // A line that is not JSON is not worth failing a download
                    // over; the exit status is what decides.
                }
            }
        }
        onExited: (exitCode, exitStatus) => {
            root.downloading = false;
            if (exitCode !== 0 && root.lastError === "")
                root.lastError = "The model could not be downloaded.";
            root.refresh();
        }
    }

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

    function download(): void {
        if (downloadRun.running)
            return;
        root.lastError = "";
        root.downloadedBytes = 0;
        root.downloadTotalBytes = 0;
        root.downloading = true;
        downloadRun.running = true;
    }

    Component.onCompleted: root.refresh()
}
