pragma Singleton

// Curated shell feedback. This is deliberately not a notification store: one
// event is visible, at most four wait behind it, and expired entries vanish.

import Quickshell
import Quickshell.Hyprland
import QtQuick

Singleton {
    id: root

    readonly property int ambientPriority: 10
    readonly property int importantPriority: 50
    readonly property int criticalPriority: 90

    property var activeEvent: null
    property var pendingEvents: []
    property int sequence: 0

    readonly property bool active: root.activeEvent !== null
    readonly property int queueLength: root.pendingEvents.length

    signal eventPublished(var event)
    signal eventDismissed(string key)

    Timer {
        id: expiryTimer
        repeat: false
        onTriggered: root.dismiss()
    }

    function publish(candidate: var): bool {
        const event = root.normalize(candidate);
        if (!event)
            return false;

        // DND is allowed to quiet device ambience, but it must never hide a
        // privacy transition or the result of something the user initiated.
        if (Notifs.doNotDisturb && event.priority < root.importantPriority)
            return false;

        if (root.activeEvent && root.activeEvent.key === event.key) {
            root.activeEvent = event;
            root.armExpiry();
            root.eventPublished(event);
            return true;
        }

        const withoutEquivalent = root.pendingEvents.filter(item => item.key !== event.key);
        if (!root.activeEvent || event.priority > root.activeEvent.priority) {
            if (root.activeEvent)
                withoutEquivalent.push(root.activeEvent);
            root.pendingEvents = root.trimQueue(withoutEquivalent);
            root.activeEvent = event;
            root.armExpiry();
        } else {
            withoutEquivalent.push(event);
            root.pendingEvents = root.trimQueue(withoutEquivalent);
        }

        root.eventPublished(event);
        return true;
    }

    function dismiss(): void {
        if (!root.activeEvent)
            return;
        const dismissedKey = root.activeEvent.key;
        expiryTimer.stop();
        root.activeEvent = null;
        root.eventDismissed(dismissedKey);
        root.advance();
    }

    function invoke(): void {
        if (!root.activeEvent)
            return;

        const action = root.activeEvent.actionId;
        const data = root.activeEvent.actionData;
        root.dismiss();

        if (action === "focus-workspace") {
            FocusSession.activateWorkspace();
        } else if (action === "open-workspace") {
            const workspaceId = Number(data);
            if (Number.isInteger(workspaceId) && workspaceId > 0)
                Hyprland.dispatch(`hl.dsp.focus({ workspace = ${workspaceId} })`);
        } else if (action === "open-activity") {
            ShellState.open("activity");
        } else if (action === "open-path" && data) {
            Quickshell.execDetached(["xdg-open", data]);
        } else if (action === "open-settings" && data) {
            // The page name is checked by ShellState's own allow-list, so an
            // event naming a page that does not exist lands on Home rather
            // than nowhere.
            ShellState.openSettings(data);
        }
    }

    function reset(): void {
        expiryTimer.stop();
        root.activeEvent = null;
        root.pendingEvents = [];
    }

    function normalize(candidate: var): var {
        if (!candidate || !candidate.key || !candidate.title)
            return null;

        root.sequence++;
        const priority = Number(candidate.priority ?? root.ambientPriority);
        const duration = Number(candidate.durationMs ?? 3200);
        return {
            key: String(candidate.key),
            icon: String(candidate.icon ?? "dialog-information-symbolic"),
            glyph: String(candidate.glyph ?? ""),
            title: String(candidate.title),
            detail: String(candidate.detail ?? ""),
            tone: String(candidate.tone ?? "accent"),
            priority: Number.isFinite(priority) ? priority : root.ambientPriority,
            durationMs: Math.max(1200, Number.isFinite(duration) ? duration : 3200),
            actionId: String(candidate.actionId ?? ""),
            actionData: String(candidate.actionData ?? ""),
            monitorName: String(candidate.monitorName ?? ""),
            sequence: root.sequence
        };
    }

    function trimQueue(events: var): var {
        const sorted = events.slice().sort((a, b) => {
            if (a.priority !== b.priority)
                return b.priority - a.priority;
            return a.sequence - b.sequence;
        });
        return sorted.slice(0, 4);
    }

    function advance(): void {
        if (root.pendingEvents.length === 0)
            return;
        const next = root.pendingEvents[0];
        root.pendingEvents = root.pendingEvents.slice(1);
        root.activeEvent = next;
        root.armExpiry();
    }

    function armExpiry(): void {
        expiryTimer.stop();
        if (!root.activeEvent)
            return;
        expiryTimer.interval = root.activeEvent.durationMs;
        expiryTimer.start();
    }
}
