// LibrePortal — live system telemetry client.
//
// Singleton EventSource manager. Subscribers register a callback that receives
// each fused live sample pushed by the backend /api/system/stream endpoint.
//
// Design intent:
//   - One EventSource per tab no matter how many widgets subscribe — joining a
//     stream is cheap, but on a busy page (dashboard + admin → system both
//     mounted) several connections would each pin their own /proc reader on
//     the backend. One shared connection means one backend ticker, full stop.
//   - The connection only opens while at least one subscriber wants it, and
//     closes the moment the last unsubscribes — an idle WebUI tab uses zero
//     bandwidth and no server resources.
//   - Page Visibility integration: a backgrounded tab closes the stream and
//     reopens it on return, so a phone left in the pocket doesn't keep a
//     connection warm.
//   - Auto-reconnect with capped exponential backoff (1 s → 30 s).
//   - Late subscribers get the last-known sample synchronously so the UI can
//     draw a frame before the next push lands.
window.LiveSystem = (() => {
    const ENDPOINT = '/api/system/stream';
    const MAX_BACKOFF_MS = 30000;

    let es = null;
    let last = null;
    let backoff = 0;
    let reopenTimer = null;
    const subs = new Set();

    function emit(payload) {
        for (const fn of subs) {
            try { fn(payload); } catch (_) { /* a misbehaving sub mustn't poison others */ }
        }
    }

    function open() {
        if (es || subs.size === 0 || document.hidden) return;
        try {
            es = new EventSource(ENDPOINT);
        } catch (_) {
            scheduleReopen();
            return;
        }
        es.onopen = () => { backoff = 0; };
        es.onmessage = (ev) => {
            // Ignore the keepalive heartbeat (sent as a comment, doesn't fire
            // onmessage, but a stray empty data: line might).
            if (!ev.data) return;
            let payload;
            try { payload = JSON.parse(ev.data); } catch (_) { return; }
            last = payload;
            emit(payload);
        };
        es.onerror = () => {
            // EventSource auto-reconnects on its own, but only while the connection
            // hasn't been closed. If the server returned 401/5xx the browser may
            // close it; force a manual cycle with backoff so we don't hammer.
            close();
            scheduleReopen();
        };
    }

    function close() {
        if (es) {
            try { es.close(); } catch (_) {}
            es = null;
        }
    }

    function scheduleReopen() {
        if (reopenTimer || subs.size === 0) return;
        backoff = backoff ? Math.min(MAX_BACKOFF_MS, backoff * 2) : 1000;
        reopenTimer = setTimeout(() => { reopenTimer = null; open(); }, backoff);
    }

    // Visibility: hide → close, show → reopen if anyone still cares.
    document.addEventListener('visibilitychange', () => {
        if (document.hidden) {
            close();
            if (reopenTimer) { clearTimeout(reopenTimer); reopenTimer = null; }
        } else {
            open();
        }
    });

    return {
        // Register a callback to receive each live sample. Returns an
        // unsubscribe function. If a sample is already in hand it fires
        // synchronously so callers can render immediately.
        subscribe(fn) {
            if (typeof fn !== 'function') return () => {};
            subs.add(fn);
            if (last) {
                try { fn(last); } catch (_) {}
            }
            open();
            return () => {
                subs.delete(fn);
                if (subs.size === 0) {
                    close();
                    if (reopenTimer) { clearTimeout(reopenTimer); reopenTimer = null; }
                }
            };
        },
        // Fetch the last sample synchronously (or null if the stream hasn't
        // produced one yet). Useful for snapshot-style reads without a sub.
        get last() { return last; },
        // Force-close the stream and cancel any pending reopen. Used by the
        // page lifecycle (pagehide) so the browser can put the page into
        // BFCache; the visibilitychange handler would normally do this too,
        // but firing explicitly leaves nothing to race.
        pause() {
            close();
            if (reopenTimer) { clearTimeout(reopenTimer); reopenTimer = null; }
        },
        // Reopen if anything is still subscribed and the document is visible.
        // Called from pageshow(persisted=true) when the page is restored from
        // BFCache.
        resume() { open(); },
        // Test hook: count of active subscribers.
        get subCount() { return subs.size; }
    };
})();
