/**
 * Global events WebSocket — stays connected while logged in,
 * dispatches DOM custom events that feature modules can listen to.
 *
 * Events dispatched:
 *   server:state_change          — target processing start/stop/crash
 *   server:automation_state_changed — automation activated/deactivated
 *   server:entity_changed        — entity CRUD (create/update/delete)
 *   server:device_health_changed — device online/offline status change
 */

import { apiKey, authRequired } from './state.ts';
import { showRestartingOverlay } from './api.ts';
import { logError } from './log.ts';
import { openAuthedWs } from './ws-auth.ts';

/**
 * Allowed ``type`` values on inbound server-event messages. Anything outside
 * this list is rejected before dispatch so a malformed message can't
 * synthesise an arbitrary ``server:*`` CustomEvent. New event types must be
 * added here intentionally — the server is the schema's source of truth.
 *
 * Audit (matches Python sources for ``fire_event`` / ``_fire_event`` /
 * ``self._emit`` / ``fire_entity_event`` call sites — see the parity
 * regression test in ``server/tests/test_events_ws_parity.py``):
 *   server_restarting           — server_ref.py / update_service.py
 *   state_change                — wled_target_processor.py / auto_restart.py
 *   automation_state_changed    — automation_engine.py
 *   entity_changed              — dependencies.fire_entity_event
 *   device_health_changed       — device_health.py
 *   update_available            — update_service.py (consumed by features/update.ts)
 *   update_download_progress    — update_service.py (consumed by features/update.ts)
 *   device_discovered           — discovery_watcher.py (consumed by features/notifications-watcher.ts)
 *   device_lost                 — discovery_watcher.py (consumed by features/notifications-watcher.ts)
 *
 * Missing any of these silently breaks the corresponding UI flow — keep
 * this list in sync when adding new event types on the server side.
 */
const _ALLOWED_SERVER_EVENT_TYPES: ReadonlySet<string> = new Set([
    'server_restarting',
    'state_change',
    'automation_state_changed',
    'entity_changed',
    'device_health_changed',
    'update_available',
    'update_download_progress',
    'device_discovered',
    'device_lost',
]);

interface ServerEventEnvelope {
    type: string;
    [key: string]: unknown;
}

function _isServerEventEnvelope(value: unknown): value is ServerEventEnvelope {
    if (!value || typeof value !== 'object') return false;
    const t = (value as { type?: unknown }).type;
    if (typeof t !== 'string' || !_ALLOWED_SERVER_EVENT_TYPES.has(t)) return false;
    // Event-name character set: identifiers only. CustomEvent names can be
    // anything but pinning them keeps the listener namespace predictable.
    if (!/^[a-zA-Z0-9_]+$/.test(t)) return false;
    return true;
}

/** True when the server has signalled it is restarting (not crashed). */
export let serverRestarting = false;

export function clearRestartingFlag() {
    serverRestarting = false;
}

let _ws: WebSocket | null = null;
let _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let _reconnectDelay = 1000;  // start at 1s, exponential backoff to 30s
const _RECONNECT_MIN = 1000;
const _RECONNECT_MAX = 30000;

export function startEventsWS() {
    stopEventsWS();
    if (authRequired && !apiKey) return;

    const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    const url = `${wsProto}//${location.host}/api/v1/events/ws`;

    openAuthedWs(url).then((ws) => {
        _ws = ws;
        _reconnectDelay = _RECONNECT_MIN;

        ws.onmessage = (event) => {
            try {
                const data: unknown = JSON.parse(event.data);
                // Validate the envelope before we dispatch — without this,
                // a malformed/hostile server message becomes an arbitrary
                // ``server:*`` CustomEvent on document, which feature
                // listeners then trust.
                if (!_isServerEventEnvelope(data)) {
                    logError('events-ws.message', `Discarded malformed server message`);
                    return;
                }
                if (data.type === 'server_restarting') {
                    serverRestarting = true;
                    showRestartingOverlay();
                }
                document.dispatchEvent(new CustomEvent(`server:${data.type}`, { detail: data }));
            } catch (err) { logError('events-ws.message', err); }
        };
        ws.onclose = () => {
            _ws = null;
            _reconnectTimer = setTimeout(startEventsWS, _reconnectDelay);
            _reconnectDelay = Math.min(_reconnectDelay * 2, _RECONNECT_MAX);
        };
        ws.onerror = (err) => {
            logError('events-ws.onerror', err);
        };
    }).catch(() => {
        _ws = null;
        _reconnectTimer = setTimeout(startEventsWS, _reconnectDelay);
        _reconnectDelay = Math.min(_reconnectDelay * 2, _RECONNECT_MAX);
    });
}

export function stopEventsWS() {
    if (_reconnectTimer) {
        clearTimeout(_reconnectTimer);
        _reconnectTimer = null;
    }
    if (_ws) {
        _ws.onclose = null;
        _ws.close();
        _ws = null;
    }
    _reconnectDelay = _RECONNECT_MIN;
}
