/**
 * Runtime Events — barrel re-exports and RuntimeEventBus.
 *
 * Import from this module to access the typed event system:
 * ```ts
 * import { RuntimeEventBus, createEventEnvelope } from '../runtime/events/index.js';
 * ```
 */
import { logger } from '../../utils/logger.js';
import { createEventEnvelope } from './envelope.js';
import { summarizeError } from '../../utils/error-display.js';
import { listenerErrorsTotal } from '../metrics.js';
export { createEventEnvelope } from './envelope.js';
export { RUNTIME_EVENT_DOMAINS, isRuntimeEventDomain } from '../../../events/domain-map.js';
export { registeredEventTypes, validateKnownEvent } from '../../../events/contracts.js';
export { AUTOMATION_SCHEDULE_KINDS, AUTOMATION_RUN_OUTCOMES } from '../../../events/automation.js';
export { ROUTE_SURFACE_KINDS, ROUTE_TARGET_KINDS } from '../../../events/routes.js';
export { CONTROL_PLANE_CLIENT_KINDS, CONTROL_PLANE_TRANSPORT_KINDS, CONTROL_PLANE_PRINCIPAL_KINDS } from '../../../events/control-plane.js';
export { DELIVERY_KINDS } from '../../../events/deliveries.js';
export { WATCHER_SOURCE_KINDS } from '../../../events/watchers.js';
export { SURFACE_KINDS } from '../../../events/surfaces.js';
/**
 * Maximum listeners per channel before a potential memory leak warning is emitted.
 *
 * 100 is a generous threshold for a single event type or domain; normal usage
 * rarely exceeds single-digit listeners. Exceeding this strongly suggests a
 * subscriber is being registered without a corresponding unsubscribe.
 */
export const MAX_LISTENERS = 100;
/** Extract a plain string error message from an unknown thrown value. */
function extractErrorMessage(err) {
    if (err instanceof Error)
        return err.message;
    if (typeof err === 'string')
        return err;
    return summarizeError(err);
}
/**
 * RuntimeEventBus — typed event bus for domain-structured runtime events.
 *
 * Supports two subscription modes:
 * - `on(eventType, callback)` — subscribe to a specific event type
 * - `onDomain(domain, callback)` — subscribe to all events in a domain
 *
 * All events are wrapped in a RuntimeEventEnvelope providing traceId,
 * sessionId, timestamps, and source context.
 *
 * This is the authoritative event transport for runtime domain signaling.
 */
export class RuntimeEventBus {
    /** Per-event-type listener sets. Keyed by the exact event type string. */
    _listeners = new Map();
    /** Per-domain listener sets. Keyed by RuntimeEventDomain. */
    _domainListeners = new Map();
    /** Effective listener cap for this instance. */
    _maxListeners;
    /** Track per-listener error counts for misbehaving-listener dedup. */
    _listenerErrorCounts = new WeakMap();
    /** Number of errors a listener must throw before OPS_LISTENER_MISBEHAVING is emitted. */
    static _MISBEHAVE_DEDUP_THRESHOLD = 1;
    constructor(opts) {
        this._maxListeners = opts?.maxListeners ?? MAX_LISTENERS;
    }
    /**
     * Subscribe to a specific event type.
     *
     * @param eventType - The exact event type string to listen for.
     * @param callback - Called with the full envelope on each emission.
     * @returns An unsubscribe function.
     */
    on(eventType, callback) {
        if (!this._listeners.has(eventType)) {
            this._listeners.set(eventType, new Set());
        }
        const set = this._listeners.get(eventType);
        set.add(callback);
        if (set.size > this._maxListeners) {
            if (process.env['NODE_ENV'] === 'development') {
                // Remove the just-added listener before throwing to maintain state consistency.
                set.delete(callback);
                if (set.size === 0)
                    this._listeners.delete(eventType);
                throw new RangeError(`[RuntimeEventBus] listener cap exceeded — maxListeners=${this._maxListeners} eventType=${String(eventType)}`);
            }
            logger.warn('[RuntimeEventBus] possible listener leak detected', {
                eventType,
                count: set.size,
                max: this._maxListeners,
            });
        }
        return () => this._off(eventType, callback);
    }
    /**
     * Subscribe to all events in a named domain.
     *
     * @param domain - Domain name (e.g. 'turn', 'tools', 'session').
     * @param callback - Called with the full envelope for each domain event.
     * @returns An unsubscribe function.
     */
    onDomain(domain, callback) {
        if (!this._domainListeners.has(domain)) {
            this._domainListeners.set(domain, new Set());
        }
        const set = this._domainListeners.get(domain);
        set.add(callback);
        if (set.size > this._maxListeners) {
            if (process.env['NODE_ENV'] === 'development') {
                // Remove the just-added listener before throwing to maintain state consistency.
                set.delete(callback);
                if (set.size === 0)
                    this._domainListeners.delete(domain);
                throw new RangeError(`[RuntimeEventBus] domain listener cap exceeded — maxListeners=${this._maxListeners} domain=${String(domain)}`);
            }
            logger.warn('[RuntimeEventBus] possible domain listener leak detected', {
                domain,
                count: set.size,
                max: this._maxListeners,
            });
        }
        return () => this._offDomain(domain, callback);
    }
    emit(domain, envelope) {
        // Snapshot both sets before iterating so subscribe/unsubscribe calls
        // triggered by a handler do not mutate the live Set mid-iteration.
        const typeSet = this._listeners.get(envelope.type);
        const typeHandlers = typeSet ? Array.from(typeSet) : [];
        const domainSet = this._domainListeners.get(domain);
        const domainHandlers = domainSet ? Array.from(domainSet) : [];
        // Dispatch each subscriber in its own microtask so a slow or
        // throwing subscriber cannot block the emitter. Errors are caught and
        // logged per-subscriber; a single bad handler never cascades to others.
        // Dispatch to per-type listeners
        for (const handler of typeHandlers) {
            const h = handler;
            const ev = envelope;
            const evType = envelope.type;
            queueMicrotask(() => {
                try {
                    h(ev);
                }
                catch (err) {
                    this._recordListenerError(h, evType, err);
                }
            });
        }
        // Dispatch to per-domain listeners
        for (const handler of domainHandlers) {
            const h = handler;
            const ev = envelope;
            const evType = envelope.type;
            const d = domain;
            queueMicrotask(() => {
                try {
                    h(ev);
                }
                catch (err) {
                    this._recordListenerError(h, evType, err, String(d));
                }
            });
        }
    }
    /**
     * Record a listener error: increment metrics, update error count, emit
     * OPS_LISTENER_MISBEHAVING once the dedup threshold is reached, and log.
     *
     * @param listener - The misbehaving listener function.
     * @param eventType - The event type that triggered the listener.
     * @param err - The thrown value caught from the listener.
     * @param domain - Optional domain name, present when the listener was a domain subscriber.
     */
    _recordListenerError(listener, eventType, err, domain) {
        const errMsg = extractErrorMessage(err);
        listenerErrorsTotal.add(1, { event_type: eventType });
        const prev = this._listenerErrorCounts.get(listener) ?? 0;
        const next = prev + 1;
        this._listenerErrorCounts.set(listener, next);
        if (next === RuntimeEventBus._MISBEHAVE_DEDUP_THRESHOLD) {
            this._emitListenerMisbehaving(listener, String(eventType), errMsg, next);
        }
        if (domain !== undefined) {
            logger.error('[RuntimeEventBus] domain listener error', {
                domain,
                eventType,
                error: summarizeError(err),
            });
        }
        else {
            logger.error('[RuntimeEventBus] listener error', {
                eventType,
                error: summarizeError(err),
            });
        }
    }
    /**
     * Directly dispatch an OPS_LISTENER_MISBEHAVING envelope to any registered
     * OPS_LISTENER_MISBEHAVING and 'ops' domain listeners.
     *
     * Bypasses emit() to avoid potential recursion: a listener watching for
     * misbehaving events itself misbehaving would otherwise cause infinite loops.
     */
    _emitListenerMisbehaving(listener, eventType, errorMessage, errorCount) {
        const payload = {
            type: 'OPS_LISTENER_MISBEHAVING',
            listenerId: listener.name || '(anonymous)',
            eventType,
            errorMessage,
            errorCount,
        };
        const envelope = createEventEnvelope('OPS_LISTENER_MISBEHAVING', payload, { sessionId: 'runtime-bus', source: 'runtime-bus' });
        // Dispatch directly — no queueMicrotask, no recursion path through emit().
        const typeListeners = this._listeners.get('OPS_LISTENER_MISBEHAVING');
        if (typeListeners) {
            for (const h of Array.from(typeListeners)) {
                try {
                    h(envelope);
                }
                catch (error) {
                    logger.warn('OPS_LISTENER_MISBEHAVING listener failed', { error: summarizeError(error) });
                }
            }
        }
        const domainListeners = this._domainListeners.get('ops');
        if (domainListeners) {
            for (const h of Array.from(domainListeners)) {
                try {
                    h(envelope);
                }
                catch (error) {
                    logger.warn('OPS_LISTENER_MISBEHAVING domain listener failed', { error: summarizeError(error) });
                }
            }
        }
    }
    _off(eventType, callback) {
        const set = this._listeners.get(eventType);
        set?.delete(callback);
        if (set?.size === 0)
            this._listeners.delete(eventType);
    }
    _offDomain(domain, callback) {
        const set = this._domainListeners.get(domain);
        set?.delete(callback);
        if (set?.size === 0)
            this._domainListeners.delete(domain);
    }
}
