// ==========================================
// Switchboard Core — SDK: Event Bus
// ==========================================
// Labeled event bus with WebSocket bridge.
// Port of events.js as ES module with factory pattern.
//
// Factory: createEvents(getWsAuth)
// ==========================================

/**
 * Create the event bus.
 *
 * @param {() => Promise<string|null>} getWsAuth — returns WS auth query param
 * @returns {object} events
 */
export function createEvents(getWsAuth) {

    const _handlers    = new Map();     // label → Set<{fn, once}>
    let   _ws          = null;
    let   _wsUrl       = null;
    let   _wsReconnectMs    = 1000;
    const _wsMaxReconnectMs = 30000;
    let   _wsReconnectTimer = null;
    let   _wsConnected      = false;
    let   _wsRetries        = 0;
    const _wsMaxRetries     = 5;
    const _wsQueue          = [];       // buffered while disconnected
    let   _heartbeatInterval = null;

    // Labels that should NOT cross the wire
    const _localOnly = new Set([
        'ui.modal.open', 'ui.modal.close',
        'ui.sidebar.toggle', 'ui.sidebar.collapse',
        'ui.toast',
        'auth.login', 'auth.logout', 'auth.refresh',
        'auth.boot', 'auth.permissions.changed',
        'sdk.ready', 'theme.changed',
    ]);

    // Labels the FE should never receive from BE
    const _serverOnly = new Set([
        'plugin.hook.pre_completion',
        'plugin.hook.post_completion',
        'internal.',
        'db.',
    ]);

    // ── Pattern matching ────────────────────────

    function _match(label, pattern) {
        if (pattern === '*') return true;
        if (pattern === label) return true;
        if (!pattern.endsWith('*')) return false;
        const prefix = pattern.slice(0, -1);
        return label.startsWith(prefix);
    }

    function _isLocalOnly(label) {
        if (_localOnly.has(label)) return true;
        for (const prefix of _localOnly) {
            if (prefix.endsWith('.') && label.startsWith(prefix)) return true;
        }
        return false;
    }

    // ── Internal dispatch ───────────────────────

    function _dispatch(label, payload, meta) {
        for (const [pattern, entries] of _handlers) {
            if (_match(label, pattern)) {
                for (const entry of entries) {
                    try {
                        entry.fn(payload, meta);
                    } catch (e) {
                        console.error(`[sw.events] Handler error for ${pattern}:`, e);
                    }
                    if (entry.once) entries.delete(entry);
                }
            }
        }
    }

    // ── WebSocket ───────────────────────────────

    async function _doConnect() {
        if (!_wsUrl) return;

        let url = _wsUrl;
        if (url.startsWith('/')) {
            const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
            url = `${proto}//${location.host}${url}`;
        }

        const authParam = await getWsAuth();
        if (!authParam) {
            console.warn('[sw.events] No auth available — skipping WebSocket');
            return;
        }
        url += (url.includes('?') ? '&' : '?') + authParam;

        try {
            _ws = new WebSocket(url);
        } catch (e) {
            console.warn('[sw.events] WebSocket creation failed:', e);
            _scheduleReconnect();
            return;
        }

        _ws.onopen = () => {
            console.log('[sw.events] WebSocket connected');
            _wsConnected = true;
            _wsReconnectMs = 1000;
            _wsRetries = 0;

            // Flush queued events
            while (_wsQueue.length > 0) {
                _ws.send(JSON.stringify(_wsQueue.shift()));
            }

            _dispatch('ws.connected', {}, { event: 'ws.connected', ts: Date.now(), local: true });
        };

        _ws.onmessage = (evt) => {
            try {
                const msg = JSON.parse(evt.data);
                if (msg.event === 'pong') return;

                for (const prefix of _serverOnly) {
                    if (msg.event.startsWith(prefix)) return;
                }

                const meta = {
                    event: msg.event,
                    room: msg.room || null,
                    ts: msg.ts || Date.now(),
                    local: false,
                };
                _dispatch(msg.event, msg.payload, meta);
            } catch (_) {
                console.warn('[sw.events] Bad message:', evt.data);
            }
        };

        _ws.onclose = (evt) => {
            _wsConnected = false;
            _ws = null;
            if (evt.code !== 1000) {
                console.log(`[sw.events] WebSocket closed (code=${evt.code}, reason=${evt.reason || 'none'})`);
            }
            _dispatch('ws.disconnected', { code: evt.code }, { event: 'ws.disconnected', ts: Date.now(), local: true });
            _scheduleReconnect();
        };

        _ws.onerror = () => {};

        _startHeartbeat();
    }

    function _scheduleReconnect() {
        if (!_wsUrl) return;
        if (_wsReconnectTimer) return;

        _wsRetries++;
        if (_wsRetries > _wsMaxRetries) {
            console.warn(`[sw.events] WebSocket failed after ${_wsMaxRetries} attempts`);
            _dispatch('ws.failed', { retries: _wsRetries }, { event: 'ws.failed', ts: Date.now(), local: true });
            return;
        }

        console.log(`[sw.events] Reconnecting in ${_wsReconnectMs}ms (attempt ${_wsRetries}/${_wsMaxRetries})`);
        _wsReconnectTimer = setTimeout(() => {
            _wsReconnectTimer = null;
            _doConnect();
        }, _wsReconnectMs);

        _wsReconnectMs = Math.min(_wsReconnectMs * 2, _wsMaxReconnectMs);
    }

    function _send(msg) {
        if (_wsConnected && _ws?.readyState === WebSocket.OPEN) {
            _ws.send(JSON.stringify(msg));
        } else {
            if (_wsQueue.length < 100) _wsQueue.push(msg);
        }
    }

    function _startHeartbeat() {
        if (_heartbeatInterval) clearInterval(_heartbeatInterval);
        _heartbeatInterval = setInterval(() => {
            if (_wsConnected) {
                _send({ event: 'ping', payload: {}, ts: Date.now() });
            }
        }, 30000);
    }

    // ── Public API ──────────────────────────────

    const events = {
        on(label, fn) {
            if (!_handlers.has(label)) _handlers.set(label, new Set());
            const entry = { fn, once: false };
            _handlers.get(label).add(entry);
            return () => _handlers.get(label)?.delete(entry);
        },

        once(label, fn) {
            if (!_handlers.has(label)) _handlers.set(label, new Set());
            const entry = { fn, once: true };
            _handlers.get(label).add(entry);
            return () => _handlers.get(label)?.delete(entry);
        },

        off(label, fn) {
            if (!fn) {
                _handlers.delete(label);
            } else {
                const set = _handlers.get(label);
                if (set) {
                    for (const entry of set) {
                        if (entry.fn === fn) { set.delete(entry); break; }
                    }
                }
            }
        },

        emit(label, payload, opts) {
            const _opts = opts || {};
            const meta = {
                event: label,
                room: _opts.room || null,
                ts: Date.now(),
                local: true,
            };
            _dispatch(label, payload, meta);

            if (!_opts.localOnly && !_isLocalOnly(label)) {
                _send({ event: label, room: meta.room, payload, ts: meta.ts });
            }
        },

        connect(url) {
            _wsUrl = url;
            _wsReconnectMs = 1000;
            _wsRetries = 0;
            _doConnect();
        },

        disconnect() {
            if (_wsReconnectTimer) clearTimeout(_wsReconnectTimer);
            _wsReconnectTimer = null;
            _wsRetries = 0;
            if (_ws) {
                _ws.onclose = null;
                _ws.close();
                _ws = null;
            }
            _wsConnected = false;
            if (_heartbeatInterval) clearInterval(_heartbeatInterval);
            _heartbeatInterval = null;
            _dispatch('ws.disconnected', {}, { event: 'ws.disconnected', ts: Date.now(), local: true });
        },

        get connected() { return _wsConnected; },

        clear() { _handlers.clear(); },

        debug() {
            const subs = {};
            for (const [label, entries] of _handlers) {
                subs[label] = entries.size;
            }
            return subs;
        },
    };

    return events;
}
