pragma Singleton

// The login keyring's lock state.
//
// The keyring is unlocked at sign-in by pam_gnome_keyring, exactly as it is
// under GNOME. What a bare Hyprland session lacks is anywhere to see when that
// has stopped being true.
//
// It stops being true rarely but expensively: gnome-keyring-daemon can crash,
// D-Bus activates a replacement, and the replacement never received the login
// password -- so the keyring is locked in the middle of a session that unlocked
// it correctly. Nothing announces this. What the user sees instead is a mail
// account that will not authenticate, a git push that cannot find its key, or
// an integration reporting "not configured", none of which mention keyrings.
//
// Checked on demand and after an unlock, not polled: the state changes only
// when a daemon dies or a password is entered.

import Quickshell
import Quickshell.Io
import QtQuick

Singleton {
    id: root

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

    property bool available: false
    property bool locked: false
    property bool scanned: false
    property bool unlocking: false
    property string lastError: ""

    // "pam" when the daemon that holds the keyring is the one PAM started at
    // login, "dbus" when it is a D-Bus-activated replacement -- which is the
    // signature of the crash case, and worth showing, because a dbus daemon
    // that is currently unlocked was unlocked by hand and will not survive.
    property string daemon: ""

    readonly property bool replacementDaemon: root.daemon === "dbus"

    // What is stored, never what is stored IN it. No property on this service
    // ever holds a password: the value is read by the helper, handed straight
    // to the clipboard, and forgotten. It does not cross into QML at all.
    property var collections: []
    property bool listed: false
    property bool listing: false
    property bool working: false

    // Set for a moment after a successful copy, so the page can say what
    // happened without the page having to know how long a clipboard lasts.
    property string copiedPath: ""

    readonly property int storedCount: {
        let total = 0;
        for (const collection of root.collections)
            total += (collection.items ?? []).length;
        return total;
    }

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

    // Raises the standard password dialog. The password never passes through
    // Panama -- the Secret Service prompts, the same way it does under GNOME.
    function unlock(): void {
        if (root.unlocking)
            return;
        root.unlocking = true;
        unlockProcess.running = true;
    }

    // The stored-secret list. Separate from status() because it is the only
    // read that needs the keyring UNLOCKED, and because a settings page should
    // not enumerate someone's passwords just because it was opened.
    function list(): void {
        if (root.listing)
            return;
        root.listing = true;
        items.command = [root.helperPath, "items"];
        items.running = true;
    }

    // Puts one stored secret on the clipboard. The value never reaches this
    // process; the helper reads it and writes it to wl-copy's stdin, and clears
    // it again shortly afterwards if it is still there.
    function copy(path: string): void {
        if (root.working)
            return;
        root.working = true;
        root.copiedPath = path;
        items.command = [root.helperPath, "copy", path];
        items.running = true;
    }

    // Irreversible. The page confirms before calling this.
    function forget(path: string): void {
        if (root.working)
            return;
        root.working = true;
        root.copiedPath = "";
        items.command = [root.helperPath, "forget", path];
        items.running = true;
    }

    function absorbItems(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.collections = Array.isArray(parsed.collections) ? parsed.collections : [];
            root.lastError = String(parsed.error ?? "");
            if (root.lastError !== "")
                root.copiedPath = "";
        } catch (error) {
            root.collections = [];
            root.lastError = "Could not read the stored secrets.";
            console.warn("Keyring: could not parse item output:", error);
        }
        root.listed = true;
    }

    function absorb(text: string): void {
        try {
            const parsed = JSON.parse(text);
            root.available = parsed.available === true;
            root.locked = parsed.locked === true;
            root.daemon = String(parsed.daemon ?? "");
            root.lastError = String(parsed.error ?? "");
        } catch (error) {
            root.available = false;
            root.lastError = "Could not read the keyring helper's output.";
            console.warn("Keyring: could not parse helper output:", error);
        }
        root.scanned = true;
    }

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

    // One process for all three item operations: each of them answers with the
    // same list, so a page never has to ask again to find out what changed.
    Process {
        id: items
        stdout: StdioCollector { onStreamFinished: root.absorbItems(this.text) }
        stderr: StdioCollector {
            onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
        }
        onExited: {
            root.listing = false;
            root.working = false;
        }
    }

    // The clipboard notice is transient, and says so by disappearing.
    Timer {
        running: root.copiedPath !== ""
        interval: 12000
        onTriggered: root.copiedPath = ""
    }

    Process {
        id: unlockProcess
        command: [root.helperPath, "unlock"]
        stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
        onExited: root.unlocking = false
    }
}
