import QtQuick
import Quickshell.Services.Notifications

QtObject {
    id: root

    property ListModel notifications: ListModel {}
    property var groupedNotifications: []
    property var groupExpansion: ({})
    property int versionCounter: 0

    function nextVersion() {
        versionCounter += 1;
        return versionCounter;
    }

    function indexOfId(notificationId) {
        const wantedId = String(notificationId);
        for (let i = 0; i < notifications.count; i++) {
            const row = notifications.get(i);
            if (String(row.rowNotificationId) === wantedId)
                return i;
        }
        return -1;
    }

    function indexOfIdAndObject(notificationId, notificationObject) {
        const wantedId = String(notificationId);
        for (let i = 0; i < notifications.count; i++) {
            const row = notifications.get(i);
            if (String(row.rowNotificationId) === wantedId && row.rowObject === notificationObject)
                return i;
        }
        return -1;
    }

    function removeRowsById(notificationId, replacementObject) {
        const wantedId = String(notificationId);
        for (let i = notifications.count - 1; i >= 0; i--) {
            const row = notifications.get(i);
            if (String(row.rowNotificationId) !== wantedId)
                continue;

            if (row.rowObject && row.rowObject !== replacementObject)
                row.rowObject.dismiss();

            notifications.remove(i);
        }
    }

    function normalizedString(value) {
        if (value === undefined || value === null)
            return "";
        return String(value);
    }

    function hintString(notificationObject, name) {
        if (!notificationObject || !notificationObject.hints)
            return "";
        const value = notificationObject.hints[name];
        return value === undefined || value === null ? "" : String(value);
    }

    function isNotifyToolName(value) {
        const normalized = String(value || "").trim().toLowerCase();
        if (normalized.length === 0)
            return false;
        return normalized === "notify-send" || normalized === "dunstify" || normalized === "notify-send.desktop" || normalized === "dunstify.desktop";
    }

    function isNotifyToolNotification(notificationObject) {
        if (!notificationObject)
            return false;
        return isNotifyToolName(notificationObject.appName) || isNotifyToolName(hintString(notificationObject, "desktop-entry"));
    }

    function removeOldNotifyToolRows(replacementObject) {
        if (!isNotifyToolNotification(replacementObject))
            return;

        for (let i = notifications.count - 1; i >= 0; i--) {
            const row = notifications.get(i);
            const rowObject = row.rowObject;
            if (!rowObject || rowObject === replacementObject)
                continue;
            if (!isNotifyToolNotification(rowObject))
                continue;

            rowObject.dismiss();
            notifications.remove(i);
        }
    }

    function replacementFallbackKeyFor(notificationObject) {
        if (!notificationObject)
            return "";

        const idValue = Number(notificationObject.id);
        if (Number.isFinite(idValue) && idValue > 0)
            return "id:" + String(Math.trunc(idValue));

        return "";
    }

    function removeRowsByFallbackReplacementKey(replacementObject) {
        const fallbackKey = replacementFallbackKeyFor(replacementObject);
        if (fallbackKey.length === 0)
            return;

        for (let i = notifications.count - 1; i >= 0; i--) {
            const row = notifications.get(i);
            if (row.rowObject === replacementObject)
                continue;
            if (String(row.rowReplacementFallbackKey || "") !== fallbackKey)
                continue;

            if (row.rowObject)
                row.rowObject.dismiss();
            notifications.remove(i);
        }
    }

    function groupKeyFor(notificationObject) {
        const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase();
        if (desktopEntry.length > 0)
            return "desktop:" + desktopEntry;

        const appName = normalizedString(notificationObject ? notificationObject.appName : "").trim().toLowerCase();
        if (appName.length > 0)
            return "app:" + appName;

        const appIcon = normalizedString(notificationObject ? notificationObject.appIcon : "").trim().toLowerCase();
        if (appIcon.length > 0)
            return "icon:" + appIcon;

        const image = normalizedString(notificationObject ? notificationObject.image : "").trim().toLowerCase();
        if (image.length > 0)
            return "image:" + image;

        return "id:" + String(notificationObject ? notificationObject.id : Date.now());
    }

    function sortRowsNewestFirst(left, right) {
        return (right.createdAt || 0) - (left.createdAt || 0);
    }

    function groupSortNewestFirst(left, right) {
        return (right.newestAt || 0) - (left.newestAt || 0);
    }

    function rebuildGroups() {
        const grouped = ({});

        for (let i = 0; i < notifications.count; i++) {
            const row = notifications.get(i);
            const key = row.rowGroupKey;
            if (!grouped[key]) {
                grouped[key] = {
                    groupKey: key,
                    appName: row.rowAppName,
                    appIcon: row.rowAppIcon,
                    desktopEntry: row.rowDesktopEntry,
                    newestAt: row.rowCreatedAt,
                    notifications: []
                };
            }

            grouped[key].notifications.push({
                notificationId: row.rowNotificationId,
                notifObject: row.rowObject,
                notifVersion: row.rowVersion,
                createdAt: row.rowCreatedAt,
                appName: row.rowAppName,
                appIcon: row.rowAppIcon,
                image: row.rowImage
            });

            if (row.rowCreatedAt > grouped[key].newestAt)
                grouped[key].newestAt = row.rowCreatedAt;
        }

        const groups = [];
        for (const groupKey in grouped) {
            if (!grouped.hasOwnProperty(groupKey))
                continue;

            const group = grouped[groupKey];
            group.notifications.sort(sortRowsNewestFirst);
            groups.push(group);
        }

        groups.sort(groupSortNewestFirst);
        groupedNotifications = groups;
    }

    function groupIsExpanded(groupKey) {
        return Boolean(groupExpansion[groupKey]);
    }

    function setGroupExpanded(groupKey, expanded) {
        const next = Object.assign({}, groupExpansion);
        next[groupKey] = Boolean(expanded);
        groupExpansion = next;
    }

    function toggleGroupExpanded(groupKey) {
        setGroupExpanded(groupKey, !groupIsExpanded(groupKey));
    }

    function rowDataFor(notificationObject, createdAtMsOverride) {
        return {
            rowNotificationId: notificationObject.id,
            rowObject: notificationObject,
            rowVersion: nextVersion(),
            rowCreatedAt: createdAtMsOverride !== undefined ? createdAtMsOverride : Date.now(),
            rowGroupKey: groupKeyFor(notificationObject),
            rowAppName: normalizedString(notificationObject.appName),
            rowAppIcon: normalizedString(notificationObject.appIcon),
            rowImage: normalizedString(notificationObject.image),
            rowDesktopEntry: hintString(notificationObject, "desktop-entry"),
            rowReplacementFallbackKey: replacementFallbackKeyFor(notificationObject)
        };
    }

    function bumpNotificationToTop(notificationObject) {
        const rowIndex = indexOfIdAndObject(notificationObject.id, notificationObject);
        if (rowIndex < 0)
            return;

        if (rowIndex === 0) {
            notifications.set(0, rowDataFor(notificationObject));
            rebuildGroups();
            return;
        }

        removeRowsById(notificationObject.id, notificationObject);
        notifications.insert(0, rowDataFor(notificationObject));
        rebuildGroups();
    }

    function addNotification(notificationObject) {
        if (notificationObject.lastGeneration) {
            notificationObject.dismiss();
            return;
        }

        notificationObject.tracked = true;
        const groupKey = groupKeyFor(notificationObject);

        const idCopy = notificationObject.id;
        const objectCopy = notificationObject;
        // Guard against reconnecting signals when the same QObject is seen again.
        // Replacement is strict freedesktop id semantics:
        // `id=$(notify-send -p ...)`, then `notify-send -r "$id" ...`.
        if (!notificationObject.__dqModelWired) {
            notificationObject.__dqModelWired = true;

            notificationObject.closed.connect(function () {
                let rowIndex = indexOfIdAndObject(idCopy, objectCopy);
                if (rowIndex < 0) {
                    const idOnlyIndex = indexOfId(idCopy);
                    if (idOnlyIndex >= 0 && notifications.get(idOnlyIndex).rowObject === objectCopy)
                        rowIndex = idOnlyIndex;
                }

                if (rowIndex >= 0) {
                    notifications.remove(rowIndex);
                    setGroupExpanded(groupKey, false);
                    rebuildGroups();
                }
            });

            const onUpdated = function () {
                root.bumpNotificationToTop(objectCopy);
            };
            notificationObject.expireTimeoutChanged.connect(onUpdated);
            notificationObject.appNameChanged.connect(onUpdated);
            notificationObject.appIconChanged.connect(onUpdated);
            notificationObject.summaryChanged.connect(onUpdated);
            notificationObject.bodyChanged.connect(onUpdated);
            notificationObject.actionsChanged.connect(onUpdated);
            notificationObject.desktopEntryChanged.connect(onUpdated);
            notificationObject.imageChanged.connect(onUpdated);
            notificationObject.hintsChanged.connect(onUpdated);
        }

        const existingIndex = indexOfId(notificationObject.id);
        if (existingIndex >= 0 && notifications.get(existingIndex).rowObject === notificationObject) {
            const currentRow = notifications.get(existingIndex);
            notifications.set(existingIndex, rowDataFor(notificationObject, currentRow.rowCreatedAt));
            rebuildGroups();
            return;
        }

        removeRowsById(notificationObject.id, notificationObject);
        // Quickshell currently doesn't expose client replaces_id in QML, so keep
        // notify-send/dunstify entries collapsed to newest to match -r user intent.
        removeOldNotifyToolRows(notificationObject);
        // Fallback replacement key uses id only.
        removeRowsByFallbackReplacementKey(notificationObject);

        notifications.insert(0, rowDataFor(notificationObject));
        // setGroupExpanded(groupKey, true);
        rebuildGroups();
    }

    function closeById(notificationId, reason) {
        const rowIndex = indexOfId(notificationId);
        if (rowIndex < 0)
            return;

        const row = notifications.get(rowIndex);
        const notifObject = row.rowObject;

        if (notifObject) {
            if (reason === "timeout") {
                notifObject.expire();
            } else if (reason === "click" || reason === "dismiss" || reason === "drag") {
                notifObject.dismiss();
            }
        }

        const remainingIndex = indexOfId(notificationId);
        if (remainingIndex >= 0) {
            const remainingRow = notifications.get(remainingIndex);
            // Avoid removing a newly inserted replacement row with the same id.
            if (!notifObject || remainingRow.rowObject === notifObject) {
                notifications.remove(remainingIndex);
                // setGroupExpanded(groupKey, true);
                rebuildGroups();
            }
        }
    }

    property NotificationServer server: NotificationServer {
        keepOnReload: false
        bodySupported: true
        bodyMarkupSupported: false
        bodyHyperlinksSupported: false
        actionsSupported: true

        // NotificationServer is Quickshell's DBus implementation for
        // org.freedesktop.Notifications, so this is the notification source.
        onNotification: function (notificationObject) {
            root.addNotification(notificationObject);
        }
    }
}
