pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import QtQuick

Singleton {
    id: root

    property bool panelOpen:   false
    property int  unreadCount: 0

    property ListModel notifications: ListModel {}
    property var       _refs:         ({}) // maps nid → live Notification object
    property var       _pids:         ({}) // maps nid → sender process PID (from D-Bus monitor)

    property ListModel toasts: ListModel {}

    // Runs every 100ms while toasts are active, decrementing progress (1.0 to 0.0 over 5s)
    // and removing any toast that has expired
    Timer {
        id:       toastTick
        interval: 100
        repeat:   true
        running:  root.toasts.count > 0

        onTriggered: {
            let i = 0
            while (i < root.toasts.count) {
                const p = root.toasts.get(i).progress - 0.02
                if (p <= 0) {
                    root.toasts.remove(i, 1)
                } else {
                    root.toasts.setProperty(i, "progress", p)
                    i++
                }
            }
        }
    }

    // Executes a niri focus-window command
    Process {
        id: _focusProc
    }

    // Monitors D-Bus for Notify calls and outputs "nid:sender_pid" so we can
    // walk /proc and focus the exact window that sent each notification.
    Process {
        id:      _pidMonitor
        command: ["bash", String(Qt.resolvedUrl("notif-pid-monitor.sh")).replace("file://", "")]
        running: true

        stdout: SplitParser {
            onRead: line => {
                const parts = line.split(":")
                if (parts.length !== 2) return
                const nid = parseInt(parts[0])
                const pid = parts[1].trim()
                if (!isNaN(nid) && pid) {
                    const p = root._pids
                    p[nid] = pid
                    root._pids = p
                }
            }
        }

        onExited: (exitCode, exitStatus) => _pidRestartTimer.start()
    }

    // Restarts the D-Bus monitor if dbus-monitor exits unexpectedly (e.g. session bus restart).
    Timer {
        id:       _pidRestartTimer
        interval: 1000
        repeat:   false
        onTriggered: { _pidMonitor.running = false; _pidMonitor.running = true }
    }

    NotificationServer {
        keepOnReload: true

        onNotification: notif => {
            const ts  = Date.now()
            const day = root._dayLabel(ts)

            root.notifications.insert(0, {
                nid:          notif.id,
                appName:      notif.appName      || "Unknown",
                summary:      notif.summary      || "",
                body:         notif.body         || "",
                appIcon:      notif.appIcon      || "",
                image:        notif.image        || "",
                desktopEntry: notif.desktopEntry || "",
                urgency:      notif.urgency      || 1,
                timestamp:    ts,
                day:          day
            })

            const r = root._refs
            r[notif.id] = notif
            root._refs = r

            if (!root.panelOpen) root.unreadCount++

            root.toasts.insert(0, {
                tid:          notif.id,
                appName:      notif.appName      || "Unknown",
                summary:      notif.summary      || "",
                body:         notif.body         || "",
                appIcon:      notif.appIcon      || "",
                image:        notif.image        || "",
                desktopEntry: notif.desktopEntry || "",
                urgency:      notif.urgency      || 1,
                progress:     1.0
            })
        }
    }

    function dismiss(index) {
        if (index < 0 || index >= notifications.count) return
        const entry = notifications.get(index)
        const ref   = _refs[entry.nid]
        if (ref) {
            try { ref.close() } catch (_) {}
            const r = _refs
            delete r[entry.nid]
            _refs = r
        }
        const p = _pids
        delete p[entry.nid]
        _pids = p
        notifications.remove(index, 1)
    }

    function dismissAll() {
        for (let i = 0; i < notifications.count; i++) {
            const ref = _refs[notifications.get(i).nid]
            if (ref) { try { ref.close() } catch (_) {} }
        }
        notifications.clear()
        _refs = {}
        _pids = {}
        unreadCount = 0
    }

    // Invokes the D-Bus action (tells the app which chat/content to open) and
    // always also focuses the window via niri IPC.
    // invoke() alone cannot steal focus on Wayland without an XDG activation token;
    // niri IPC bypasses that restriction because the compositor grants focus directly.
    // entry = desktopEntry || appName, passed from the model so it works even after
    // a hot-reload clears _refs.
    function invokeDefault(nid, entry) {
        // Send ActionInvoked to the app if we still have a live reference.
        const ref = _refs[nid]
        if (ref) {
            const actions = ref.actions ? Array.from(ref.actions) : []
            if (actions.length > 0) {
                let found = false
                for (let i = 0; i < actions.length; i++) {
                    if (actions[i].identifier === "default") {
                        try { actions[i].invoke() } catch (_) {}
                        found = true
                        break
                    }
                }
                if (!found) {
                    try { actions[0].invoke() } catch (_) {}
                }
            }
        }

        // Bring the window to front via niri IPC.
        const senderPid = _pids[nid]
        if (senderPid) {
            // Walk /proc from the sender PID upward until we find a PID that owns
            // a niri window, then focus that window. This correctly handles multiple
            // instances of the same app (e.g. several kitty terminals).
            _focusProc.command = [
                "bash", "-c",
                'P=' + senderPid + '; ' +
                'while [ "$P" -gt 1 ]; do ' +
                '  WIN=$(niri msg --json windows | jq -r --arg p "$P" \'.[] | select(.pid == ($p | tonumber)) | .id // empty\'); ' +
                '  if [ -n "$WIN" ]; then niri msg action focus-window --id "$WIN"; exit 0; fi; ' +
                '  P=$(awk \'/^PPid:/{print $2}\' /proc/"$P"/status 2>/dev/null); ' +
                '  [ -z "$P" ] && exit 1; ' +
                'done'
            ]
        } else if (entry) {
            // Fallback when no PID was captured: match by app_id, pick most recently focused.
            _focusProc.command = [
                "bash", "-c",
                'WID=$(niri msg --json windows | jq -r --arg id "' + entry + '" ' +
                '\'[.[] | select(.app_id == $id)] | sort_by(.focus_timestamp.secs, .focus_timestamp.nanos) | last | .id // empty\'); ' +
                '[ -n "$WID" ] && niri msg action focus-window --id "$WID"'
            ]
        } else {
            return
        }
        _focusProc.running = false
        _focusProc.running = true
    }

    function dismissToast(tid) {
        for (let i = 0; i < toasts.count; i++) {
            if (toasts.get(i).tid === tid) {
                toasts.remove(i, 1)
                return
            }
        }
    }

    function togglePanel() {
        panelOpen = !panelOpen
        if (panelOpen) unreadCount = 0
    }

    function formatTime(ts) {
        return Qt.formatDateTime(new Date(ts), "hh:mm")
    }

    function _dayLabel(ts) {
        const now       = new Date()
        const date      = new Date(ts)
        const today     = new Date(now.getFullYear(),  now.getMonth(),  now.getDate())
        const yesterday = new Date(today.getTime() - 86400000)
        const day       = new Date(date.getFullYear(), date.getMonth(), date.getDate())

        if (day.getTime() === today.getTime())     return "Today"
        if (day.getTime() === yesterday.getTime()) return "Yesterday"
        return Qt.formatDateTime(date, "dd/MM/yyyy")
    }
}
