// Top-right toast stack — Alpine-driven reactive list.
//
// Reactive state lives in Alpine.store('toasts'). Auto-dismiss timers,
// action handlers, and the slide-out animation timeout stay as
// module-scoped plain JS — putting them in the reactive store would
// trigger render churn (every timer tick = re-render).
//
// Rules:
//   - info/warn auto-dismiss after 8s; errors persist until user acts.
//   - max 4 visible; the rest collapse into a "+N more" button at the
//     bottom of the stack. Clicking expands the overflow in place.
//   - hovering a toast pauses its auto-dismiss timer.
//   - clicking the body (not a button) toggles is-expanded so multi-line
//     details show in full.
//   - dismiss is keyed so repeat pushes (e.g. the 30s Mac health poll)
//     don't re-animate an already-visible toast (signature dedupe).
//   - server-source dismissals POST /api/notifications/dismiss and are
//     remembered until the server's signature changes.

const AUTO_DISMISS_MS = 8000;
const SLIDE_OUT_MS = 220;
const MAX_VISIBLE = 4;

const timers = new Map();
const actionHandlers = {};

function severityFor(level) {
    if (level === 'error') return 'error';
    if (level === 'warn' || level === 'warning') return 'warn';
    return 'info';
}

function signatureFor(t) {
    return `${t.level}|${t.title || ''}|${t.detail || ''}|${(t.actions || []).map(a => a.id).join(',')}`;
}

function startAutoDismiss(key) {
    clearAutoDismiss(key);
    timers.set(key, setTimeout(() => {
        timers.delete(key);
        window.Alpine?.store('toasts')?.dismiss(key);
    }, AUTO_DISMISS_MS));
}

function clearAutoDismiss(key) {
    if (timers.has(key)) {
        clearTimeout(timers.get(key));
        timers.delete(key);
    }
}

function storeDefinition() {
    return {
        items: [],
        byKey: {},
        serverDismissed: {},
        overflowExpanded: false,

        get visible() {
            return this.overflowExpanded
                ? this.items
                : this.items.slice(0, MAX_VISIBLE);
        },
        get overflowCount() {
            return Math.max(0, this.items.length - MAX_VISIBLE);
        },

        severityOf(level) { return severityFor(level); },

        upsert(t, source) {
            const signature = signatureFor(t);
            const existing = this.byKey[t.key];
            if (existing && existing.signature === signature) {
                return;
            }
            const persistent = severityFor(t.level) === 'error';
            const entry = {
                ...t,
                signature,
                persistent,
                source,
                expanded: false,
                dismissing: false,
            };
            if (existing) {
                clearAutoDismiss(t.key);
                const idx = this.items.findIndex(x => x.key === t.key);
                if (idx >= 0) this.items[idx] = entry;
                else this.items.push(entry);
            } else {
                this.items.push(entry);
            }
            this.byKey[t.key] = entry;
            if (!persistent) {
                startAutoDismiss(t.key);
            }
        },

        dismiss(key) {
            const t = this.byKey[key];
            if (!t) return;
            clearAutoDismiss(key);
            if (t.source === 'server') {
                fetch('/api/notifications/dismiss', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({key}),
                }).catch(() => {});
                this.serverDismissed[key] = t.signature;
            }
            t.dismissing = true;
            // Keep the entry in the store long enough for the slide-out
            // CSS animation to play, then drop it.
            setTimeout(() => {
                const idx = this.items.findIndex(x => x.key === key);
                if (idx >= 0) this.items.splice(idx, 1);
                delete this.byKey[key];
            }, SLIDE_OUT_MS);
        },

        syncServer(notifications) {
            const incomingKeys = new Set();
            for (const n of notifications) {
                if (!n.key) continue;
                incomingKeys.add(n.key);
                const sig = signatureFor(n);
                const dismissedAt = this.serverDismissed[n.key];
                if (dismissedAt === sig) continue;
                if (dismissedAt && dismissedAt !== sig) {
                    delete this.serverDismissed[n.key];
                }
                this.upsert({
                    key: n.key,
                    level: n.level || 'info',
                    title: n.title,
                    detail: n.detail,
                    actions: n.actions || [],
                    dismissible: n.dismissible !== false,
                }, 'server');
            }
            for (const t of [...this.items]) {
                if (t.source === 'server' && !incomingKeys.has(t.key)) {
                    this.dismiss(t.key);
                }
            }
        },

        toggleOverflow() {
            this.overflowExpanded = true;
        },

        toggleExpanded(key) {
            const t = this.byKey[key];
            if (t) t.expanded = !t.expanded;
        },

        pauseTimer(key) {
            clearAutoDismiss(key);
        },

        resumeTimer(key) {
            const t = this.byKey[key];
            if (!t || t.persistent) return;
            startAutoDismiss(key);
        },

        runAction(key, actionId) {
            const handler = actionHandlers[actionId];
            if (handler) handler({key, actionId});
        },
    };
}

function ensureStore() {
    if (!window.Alpine) return null;
    if (!window.Alpine.store('toasts')) {
        window.Alpine.store('toasts', storeDefinition());
    }
    return window.Alpine.store('toasts');
}

if (window.Alpine) {
    ensureStore();
} else {
    document.addEventListener('alpine:init', ensureStore);
}

// Cache-bypass: WKWebView caches HTML aggressively. If the cached page
// has the pre-Alpine toast-stack markup (or no markup at all), rebuild
// it as Alpine markup so subsequent state pushes can render correctly.
function ensureToastStackInDom() {
    let stack = document.getElementById('toast-stack');
    if (!stack) {
        stack = document.createElement('div');
        stack.id = 'toast-stack';
        stack.className = 'toast-stack';
        stack.setAttribute('aria-live', 'polite');
        document.body.appendChild(stack);
    }
    if (!stack.hasAttribute('x-data')) {
        stack.setAttribute('x-data', '');
    }
    if (!stack.querySelector('template[x-for]')) {
        stack.innerHTML = stackInnerHtml();
    }
    return stack;
}

function stackInnerHtml() {
    return `
        <template x-for="t in $store.toasts.visible" :key="t.key">
            <div class="toast"
                 :class="['level-' + $store.toasts.severityOf(t.level), { 'is-expanded': t.expanded, 'is-dismissing': t.dismissing }]"
                 x-on:click="$store.toasts.toggleExpanded(t.key)"
                 x-on:mouseenter="$store.toasts.pauseTimer(t.key)"
                 x-on:mouseleave="$store.toasts.resumeTimer(t.key)">
                <div class="toast-body">
                    <div class="toast-title" x-text="t.title || ''"></div>
                    <div class="toast-detail" x-show="!!t.detail" x-text="t.detail || ''"></div>
                    <div class="toast-actions" x-show="t.actions && t.actions.length">
                        <template x-for="a in t.actions" :key="a.id">
                            <span style="display: contents">
                                <a x-show="!!a.href" class="action-btn"
                                   :href="a.href || '#'"
                                   :target="a.target || ''"
                                   rel="noopener noreferrer"
                                   :data-action="a.id"
                                   x-text="a.label"
                                   x-on:click.stop="$store.toasts.runAction(t.key, a.id)"></a>
                                <button x-show="!a.href" class="action-btn" type="button"
                                        :data-action="a.id"
                                        x-text="a.label"
                                        x-on:click.stop="$store.toasts.runAction(t.key, a.id)"></button>
                            </span>
                        </template>
                    </div>
                </div>
                <button x-show="t.dismissible !== false" class="toast-dismiss"
                        title="Dismiss" aria-label="Dismiss"
                        x-on:click.stop="$store.toasts.dismiss(t.key)">&times;</button>
            </div>
        </template>
        <button x-show="$store.toasts.overflowCount > 0 && !$store.toasts.overflowExpanded"
                class="toast-overflow-more"
                type="button"
                x-on:click="$store.toasts.toggleOverflow()"
                x-text="'+' + $store.toasts.overflowCount + ' more'"></button>
    `;
}

ensureToastStackInDom();

// Public API — preserved for the 8 caller modules. Each delegates into
// the store, with a graceful no-op if Alpine never loaded.
export function pushToast(t) {
    if (!t.key) throw new Error('pushToast requires a key');
    ensureStore()?.upsert(t, 'client');
}

export function dismissToast(key) {
    ensureStore()?.dismiss(key);
}

export function syncServerToasts(notifications) {
    ensureStore()?.syncServer(notifications || []);
}

export function registerToastAction(id, handler) {
    actionHandlers[id] = handler;
}
