<?php
declare(strict_types=1);
/**
 * Notification bell for the PPphp console navbar.
 *
 * Self-contained (markup + styles + script). Drop it into any signed-in console
 * navbar; it reads $baseUrl from the surrounding page. Ambient by design — if
 * the endpoint fails it simply shows nothing rather than breaking the page.
 */
$ppBellBase = $baseUrl ?? '';
?>
<style>
    .pp-bell { position: relative; }
    .pp-bell-btn { position: relative; display: inline-flex; align-items: center; justify-content: center;
        width: 36px; height: 36px; border: 0; border-radius: 9px; cursor: pointer;
        background: rgba(255, 255, 255, .08); color: #fff; font-size: 16px; transition: background .15s; }
    .pp-bell-btn:hover { background: rgba(255, 255, 255, .18); }
    .pp-bell-dot { position: absolute; top: 4px; right: 4px; min-width: 16px; height: 16px; padding: 0 4px;
        border-radius: 999px; background: #ef4444; color: #fff; font-size: 10px; font-weight: 700;
        display: none; align-items: center; justify-content: center; line-height: 1; }
    .pp-bell-dot.on { display: inline-flex; }

    .pp-bell-menu { position: absolute; right: 0; top: calc(100% + 10px); width: min(380px, calc(100vw - 24px));
        background: var(--pp-surface, #fff); color: var(--pp-text, #1e293b);
        border: 1px solid var(--pp-border, #e2e8f0); border-radius: 14px; z-index: 1400;
        box-shadow: 0 24px 60px rgba(2, 6, 23, .28); overflow: hidden; display: none; }
    .pp-bell-menu.open { display: block; }
    .pp-bell-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
        padding: 12px 14px; border-bottom: 1px solid var(--pp-border, #eef1f6); background: var(--pp-surface-soft, #f8fafc); }
    .pp-bell-head strong { font-size: 13px; color: var(--pp-text); }
    .pp-bell-clear { border: 0; background: transparent; color: var(--pp-accent, #4f46e5); cursor: pointer;
        font-size: 11.5px; font-weight: 700; padding: 2px 4px; }
    .pp-bell-clear:hover { text-decoration: underline; }
    .pp-bell-list { max-height: 340px; overflow-y: auto; }
    .pp-bell-item { display: flex; gap: 11px; padding: 12px 14px; text-decoration: none; color: inherit;
        border-bottom: 1px solid var(--pp-border, #f1f5f9); cursor: pointer; }
    .pp-bell-item:last-child { border-bottom: 0; }
    .pp-bell-item:hover { background: var(--pp-surface-soft, #f8fafc); }
    .pp-bell-item.unread { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 7%, var(--pp-surface, #fff)); }
    .pp-bell-ic { flex: 0 0 32px; width: 32px; height: 32px; border-radius: 9px; display: flex; align-items: center;
        justify-content: center; font-size: 14px; color: var(--pp-accent, #4f46e5);
        background: color-mix(in srgb, var(--pp-accent, #4f46e5) 13%, var(--pp-surface, #fff)); }
    .pp-bell-body { min-width: 0; flex: 1; }
    /* Each line is a <span> inside an <a>, so it needs an explicit block. */
    .pp-bell-title { display: block; font-size: 13px; font-weight: 700; color: var(--pp-text); }
    .pp-bell-text { display: block; font-size: 12px; line-height: 1.5; color: var(--pp-text-muted, #64748b);
        margin-top: 1px; overflow-wrap: anywhere; }
    .pp-bell-time { display: block; font-size: 10.5px; color: var(--pp-text-muted, #94a3b8); margin-top: 3px; }
    .pp-bell-empty { padding: 34px 18px; text-align: center; color: var(--pp-text-muted, #64748b); font-size: 12.5px; }
    .pp-bell-empty i { font-size: 26px; display: block; margin-bottom: 8px; opacity: .5; }

    /* A pinned platform/update notice — reads as a small banner atop the list. */
    .pp-bell-item.pinned { background: linear-gradient(120deg,
            color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, var(--pp-surface, #fff)),
            color-mix(in srgb, var(--pp-accent, #4f46e5) 4%, var(--pp-surface, #fff)));
        border-bottom: 1px solid color-mix(in srgb, var(--pp-accent, #4f46e5) 22%, transparent); }
    .pp-bell-item.pinned .pp-bell-ic { color: #fff;
        background: linear-gradient(135deg, var(--pp-accent, #4f46e5), color-mix(in srgb, var(--pp-accent, #4f46e5) 60%, #000)); }
    .pp-bell-pill { display: inline-block; margin-left: 7px; vertical-align: middle; padding: 1px 7px;
        border-radius: 999px; font-size: 9.5px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
        color: #fff; background: var(--pp-accent, #4f46e5); }
    /* Semantic left accent by severity (Owner-pushed notices use warning/error). */
    .pp-bell-item.t-warning { box-shadow: inset 3px 0 0 #f59e0b; }
    .pp-bell-item.t-error   { box-shadow: inset 3px 0 0 #ef4444; }
    .pp-bell-item.t-success { box-shadow: inset 3px 0 0 #10b981; }
    .pp-bell-item.t-error .pp-bell-ic   { color: #ef4444; background: color-mix(in srgb, #ef4444 13%, var(--pp-surface, #fff)); }
    .pp-bell-item.t-warning .pp-bell-ic { color: #b45309; background: color-mix(in srgb, #f59e0b 15%, var(--pp-surface, #fff)); }

    /* ── Read vs unread: read notifications recede (dim), unread stay sharp ── */
    .pp-bell-item { position: relative; --nt: var(--pp-accent, #4f46e5);
        transition: opacity .15s ease, background-color .15s ease; }
    .pp-bell-item.t-success { --nt: #10b981; }
    .pp-bell-item.t-warning { --nt: #f59e0b; }
    .pp-bell-item.t-error   { --nt: #ef4444; }
    .pp-bell-item:not(.unread) { opacity: .55; }                 /* READ = dim */
    .pp-bell-item:not(.unread) .pp-bell-ic { filter: grayscale(.55); }
    .pp-bell-item:not(.unread):hover { opacity: 1; }             /* hover restores a read row */
    .pp-bell-item.unread { opacity: 1; background: color-mix(in srgb, var(--nt) 10%, var(--pp-surface, #fff)); }
    .pp-bell-item.unread .pp-bell-title { font-weight: 800; }
    .pp-bell-item.unread::after {                                /* crisp unread status dot */
        content: ""; position: absolute; top: 15px; right: 13px; width: 8px; height: 8px; border-radius: 50%;
        background: var(--nt); box-shadow: 0 0 0 3px color-mix(in srgb, var(--nt) 22%, transparent); }

    /* ── Live bell: the badge pulses while unread exist; bell swings + badge pops on a NEW arrival ── */
    .pp-bell-btn i { display: inline-block; transform-origin: 50% 12%; transition: color .2s ease; }
    .pp-bell.has-unread .pp-bell-btn i { color: #fecaca; }
    .pp-bell.has-unread .pp-bell-dot { animation: ppb-pulse 1.9s ease-out infinite; }
    .pp-bell-dot.pop { animation: ppb-pop .34s cubic-bezier(.3, 1.4, .5, 1); }
    .pp-bell.ring .pp-bell-btn i { animation: ppb-swing 1s ease; }
    @keyframes ppb-pulse { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, #ef4444 65%, transparent); }
        70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
    @keyframes ppb-pop { 0% { transform: scale(.4); } 60% { transform: scale(1.18); } 100% { transform: scale(1); } }
    @keyframes ppb-swing { 0%, 100% { transform: rotate(0); } 10% { transform: rotate(16deg); } 22% { transform: rotate(-13deg); }
        34% { transform: rotate(10deg); } 46% { transform: rotate(-7deg); } 58% { transform: rotate(4deg); } 70% { transform: rotate(-2deg); } }
    @media (prefers-reduced-motion: reduce) {
        .pp-bell.has-unread .pp-bell-dot, .pp-bell-dot.pop, .pp-bell.ring .pp-bell-btn i { animation: none; }
    }

    /* ── Mobile: the bell isn't at the far-right, so `right:0` anchored to the bell
       pushes the 380px panel off the left edge. Detach it and span the viewport
       just under the fixed navbar instead (native-app sheet). The navbar sits at
       the viewport top, so a fixed `top` lands just below it whether the panel
       resolves against the viewport or the navbar's own backdrop-filter box. ── */
    @media (max-width: 767.98px) {
        .pp-bell-menu {
            position: fixed;
            top: calc(var(--topbar-height, 56px) + env(safe-area-inset-top, 0px) + 6px);
            left: 8px; right: 8px; width: auto; max-width: none;
            border-radius: 16px;
        }
        .pp-bell-list { max-height: min(360px, calc(100dvh - 160px)); }
    }

    /* ── Live toast: a new notification slides in bottom-right while you work ── */
    .pp-bell-toasts { position: fixed; right: 16px; bottom: 16px; z-index: 2000;
        display: flex; flex-direction: column; gap: 10px; max-width: min(360px, calc(100vw - 32px)); }
    .pp-bell-toast { display: flex; gap: 11px; align-items: flex-start; text-decoration: none;
        padding: 12px 12px 12px 13px; border-radius: 13px; color: var(--pp-text, #1e293b);
        background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e2e8f0);
        border-left: 3px solid var(--pp-accent, #4f46e5); box-shadow: 0 18px 44px rgba(2, 6, 23, .26);
        position: relative; opacity: 0; transform: translateY(12px) scale(.98);
        transition: opacity .28s ease, transform .28s cubic-bezier(.3, 1.2, .5, 1); }
    .pp-bell-toast.in { opacity: 1; transform: none; }
    .pp-bell-toast.t-success { border-left-color: #10b981; }
    .pp-bell-toast.t-warning { border-left-color: #f59e0b; }
    .pp-bell-toast.t-error   { border-left-color: #ef4444; }
    .pp-bell-toast-ic { flex: 0 0 30px; width: 30px; height: 30px; border-radius: 8px; display: flex;
        align-items: center; justify-content: center; font-size: 14px; color: var(--pp-accent, #4f46e5);
        background: color-mix(in srgb, var(--pp-accent, #4f46e5) 14%, var(--pp-surface, #fff)); }
    .pp-bell-toast.t-success .pp-bell-toast-ic { color: #10b981; background: color-mix(in srgb, #10b981 14%, var(--pp-surface, #fff)); }
    .pp-bell-toast.t-warning .pp-bell-toast-ic { color: #b45309; background: color-mix(in srgb, #f59e0b 16%, var(--pp-surface, #fff)); }
    .pp-bell-toast.t-error   .pp-bell-toast-ic { color: #ef4444; background: color-mix(in srgb, #ef4444 14%, var(--pp-surface, #fff)); }
    .pp-bell-toast-body { min-width: 0; flex: 1; padding-right: 14px; }
    .pp-bell-toast-title { display: block; font-size: 13px; font-weight: 800; color: var(--pp-text); }
    .pp-bell-toast-text { font-size: 12px; line-height: 1.45; color: var(--pp-text-muted, #64748b); margin-top: 1px;
        overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
    .pp-bell-toast-x { position: absolute; top: 6px; right: 8px; border: 0; background: transparent;
        color: var(--pp-text-muted, #94a3b8); font-size: 17px; line-height: 1; cursor: pointer; padding: 2px 4px; }
    .pp-bell-toast-x:hover { color: var(--pp-text); }
    @media (prefers-reduced-motion: reduce) { .pp-bell-toast { transition: opacity .2s linear; transform: none; } }
    @media (max-width: 767.98px) { .pp-bell-toasts { left: 8px; right: 8px; bottom: 8px; max-width: none; } }
</style>

<div class="pp-bell" id="ppBell" data-base="<?= htmlspecialchars((string) $ppBellBase) ?>">
    <button type="button" class="pp-bell-btn" id="ppBellBtn" aria-label="Notifications" aria-expanded="false" title="Notifications">
        <i class="bi bi-bell"></i>
        <span class="pp-bell-dot" id="ppBellDot">0</span>
    </button>
    <div class="pp-bell-menu" id="ppBellMenu" role="menu" aria-label="Notifications">
        <div class="pp-bell-head">
            <strong>Notifications</strong>
            <button type="button" class="pp-bell-clear" id="ppBellClear">Mark all read</button>
        </div>
        <div class="pp-bell-list" id="ppBellList">
            <div class="pp-bell-empty"><i class="bi bi-hourglass-split"></i> Loading…</div>
        </div>
    </div>
</div>

<script>
(function () {
    var root = document.getElementById('ppBell');
    if (!root) return;
    var base = root.dataset.base || '';
    var btn = document.getElementById('ppBellBtn');
    var menu = document.getElementById('ppBellMenu');
    var list = document.getElementById('ppBellList');
    var dot = document.getElementById('ppBellDot');
    var loaded = false, prevUnread = 0, firstLoad = true;
    var lastTopId = 0, firstToast = true, toastWrap = null;

    function esc(s) {
        return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
            return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c];
        });
    }

    function ago(ts) {
        var s = Math.floor(Date.now() / 1000) - ts;
        if (s < 60) return 'Just now';
        if (s < 3600) return Math.floor(s / 60) + ' min ago';
        if (s < 86400) return Math.floor(s / 3600) + ' hr ago';
        if (s < 172800) return 'Yesterday';
        if (s < 2592000) return Math.floor(s / 86400) + ' days ago';
        return new Date(ts * 1000).toLocaleDateString();
    }

    function badge(n) {
        dot.textContent = n > 99 ? '99+' : n;
        dot.classList.toggle('on', n > 0);
        root.classList.toggle('has-unread', n > 0);
        // A NEW notification arrived (count rose after the first load) → pop the
        // badge and swing the bell once so it catches the eye.
        if (n > prevUnread && !firstLoad) {
            dot.classList.remove('pop'); void dot.offsetWidth; dot.classList.add('pop');
            root.classList.remove('ring'); void root.offsetWidth; root.classList.add('ring');
            setTimeout(function () { root.classList.remove('ring'); }, 1100);
        }
        prevUnread = n; firstLoad = false;
    }

    // An "update available" notice (or any Owner-pushed platform notice that links
    // to /platform/updates) is rendered as a pinned, accent banner row at the very
    // top of the list, so it can't be lost among ordinary notifications.
    function isPinned(n) { return /\/platform\/updates$/.test(String(n.url || '')); }

    function render(items) {
        if (!items.length) {
            list.innerHTML = '<div class="pp-bell-empty"><i class="bi bi-check2-circle"></i>' +
                'You\'re all caught up.</div>';
            return;
        }
        // Pinned notices float to the top; everything else keeps its order.
        items = items.slice().sort(function (a, b) { return (isPinned(b) ? 1 : 0) - (isPinned(a) ? 1 : 0); });
        list.innerHTML = items.map(function (n) {
            var pinned = isPinned(n);
            var type = ['info', 'success', 'warning', 'error'].indexOf(n.type) >= 0 ? n.type : 'info';
            return '<a class="pp-bell-item t-' + type + (pinned ? ' pinned' : '') + ' ' + (n.unread ? 'unread' : '') + '"' +
                ' data-id="' + n.id + '"' + (n.url ? ' href="' + esc(base + n.url) + '"' : ' href="#"') + '>' +
                '<span class="pp-bell-ic"><i class="bi bi-' + esc(n.icon || 'bell') + '"></i></span>' +
                '<span class="pp-bell-body">' +
                (n.title ? '<span class="pp-bell-title">' + esc(n.title) +
                    (pinned ? '<span class="pp-bell-pill">Update</span>' : '') + '</span>' : '') +
                '<span class="pp-bell-text">' + esc(n.message) + '</span>' +
                '<span class="pp-bell-time">' + esc(ago(n.ts)) + '</span>' +
                '</span></a>';
        }).join('');
    }

    // A brand-new notification (a higher top id than we've seen) slides in as a
    // toast while the tab is visible — so you notice it without opening the bell.
    function maybeToast(items) {
        var top = items && items.length ? items[0] : null;
        var topId = top ? (top.id | 0) : 0;
        if (!firstToast && top && top.unread && topId > lastTopId && !document.hidden) {
            showToast(top);
        }
        if (topId > lastTopId) lastTopId = topId;
        firstToast = false;
    }

    function showToast(n) {
        if (!toastWrap) {
            toastWrap = document.createElement('div');
            toastWrap.className = 'pp-bell-toasts';
            document.body.appendChild(toastWrap);
        }
        var type = ['info', 'success', 'warning', 'error'].indexOf(n.type) >= 0 ? n.type : 'info';
        var el = document.createElement('a');
        el.className = 'pp-bell-toast t-' + type;
        el.setAttribute('href', n.url ? esc(base + n.url) : '#');
        el.innerHTML =
            '<span class="pp-bell-toast-ic"><i class="bi bi-' + esc(n.icon || 'bell') + '"></i></span>' +
            '<span class="pp-bell-toast-body">' +
              (n.title ? '<span class="pp-bell-toast-title">' + esc(n.title) + '</span>' : '') +
              '<span class="pp-bell-toast-text">' + esc(n.message) + '</span>' +
            '</span>' +
            '<button type="button" class="pp-bell-toast-x" aria-label="Dismiss">×</button>';
        function dismiss() { el.classList.remove('in'); setTimeout(function () { el.remove(); }, 320); }
        var timer = setTimeout(dismiss, 6500);
        el.addEventListener('click', function (e) {
            if (e.target.closest('.pp-bell-toast-x')) { e.preventDefault(); clearTimeout(timer); dismiss(); return; }
            if (n.id) mark(n.id); // follows the href after marking (default navigation)
        });
        toastWrap.appendChild(el);
        requestAnimationFrame(function () { el.classList.add('in'); });
    }

    // Apply a fresh payload — from a poll OR the live SSE stream — to the bell.
    function apply(j) {
        if (!j || j.success === false) return;
        maybeToast(j.items || []);
        badge(j.unread || 0);
        render(j.items || []);
        loaded = true;
    }

    function load() {
        fetch(base + '/notifications', { credentials: 'same-origin', headers: { 'Accept': 'application/json' } })
            .then(function (r) { return r.json(); })
            .then(apply)
            .catch(function () { /* ambient — stay quiet */ });
    }

    function mark(id) {
        return fetch(base + '/notifications/read', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            credentials: 'same-origin', body: JSON.stringify({ id: id || 0 }),
        }).then(function (r) { return r.json(); }).catch(function () { return null; });
    }

    btn.addEventListener('click', function (e) {
        e.stopPropagation();
        var open = menu.classList.toggle('open');
        btn.setAttribute('aria-expanded', open ? 'true' : 'false');
        if (open && !loaded) load();
    });

    document.getElementById('ppBellClear').addEventListener('click', function (e) {
        e.stopPropagation();
        mark(0).then(function () { badge(0); load(); });
    });

    // Clicking an item marks just that one read, then follows its link.
    list.addEventListener('click', function (e) {
        var item = e.target.closest('.pp-bell-item');
        if (!item) return;
        var id = parseInt(item.dataset.id, 10);
        var href = item.getAttribute('href');
        if (item.classList.contains('unread')) {
            e.preventDefault();
            mark(id).then(function () {
                if (href && href !== '#') window.location.href = href;
                else { item.classList.remove('unread'); load(); }
            });
        } else if (!href || href === '#') {
            e.preventDefault();
        }
    });

    document.addEventListener('click', function () { menu.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); });
    menu.addEventListener('click', function (e) { e.stopPropagation(); });
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') { menu.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); }
    });

    // ── Liveness: prefer a live SSE stream; fall back to gentle polling ──
    // The stream (/notifications/stream) pushes updates within ~2.5s using one
    // held connection. If a proxy buffers it or the host can't stream, we detect
    // the repeated failure and switch to a 30s poll so the bell still works.
    var es = null, esFails = 0, sseDead = false, pollTimer = null;

    function startPoll() {
        if (pollTimer) return;
        pollTimer = setInterval(function () { if (!document.hidden) load(); }, 30000);
    }
    function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }

    function openSSE() {
        if (sseDead || es || document.hidden) return;
        if (!('EventSource' in window)) { sseDead = true; startPoll(); return; }
        try { es = new EventSource(base + '/notifications/stream'); }
        catch (e) { sseDead = true; startPoll(); return; }
        es.onmessage = function (ev) {
            esFails = 0; stopPoll();           // stream is healthy — polling not needed
            try { apply(JSON.parse(ev.data)); } catch (e) {}
        };
        es.onerror = function () {
            if (++esFails >= 3) {               // give up on SSE, poll instead
                sseDead = true;
                try { es.close(); } catch (e) {}
                es = null;
                startPoll();
            }
        };
    }
    function closeSSE() { if (es) { try { es.close(); } catch (e) {} es = null; } }

    load();     // instant badge on page load
    openSSE();  // then go live

    window.addEventListener('focus', function () { if (sseDead) load(); else openSSE(); });
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            closeSSE(); // a backgrounded tab shouldn't hold a streaming worker open
        } else {
            load();                 // catch up instantly
            if (!sseDead) openSSE(); // and resume the live stream
        }
    });
    window.addEventListener('pagehide', closeSSE);
}());
</script>
