/**
 * RedisEventBus -- Redis pub/sub transport implementing the same public surface
 * as the file-based EventBus.
 *
 * Motivation: the file-based EventBus suffers 3-60 second cross-pod latency on
 * shared NFS/EFS because Linux NFS stat caching delays file-change visibility.
 * Healthcare HA deployments require sub-second event propagation. Redis pub/sub
 * delivers sub-millisecond cross-pod latency.
 *
 * Usage:
 *   REDIS_EVENT_BUS_ENABLED=true   -- activates this transport (default: false)
 *   REDIS_URL=redis://...          -- full connection string (required when enabled)
 *
 * When REDIS_EVENT_BUS_ENABLED is false (the default), this module is never
 * instantiated and the file-based EventBus remains the only transport. The two
 * implementations are additive, not exclusive.
 *
 * Architecture notes:
 *   - One Redis channel per EventType (e.g. "aiag:events:handoff:created").
 *     This scales well: subscribers only receive the event types they care about,
 *     avoiding per-subscriber envelope filtering overhead.
 *   - A wildcard '*' subscriber additionally subscribes to the catch-all channel
 *     "aiag:events:*" via Redis PSUBSCRIBE.
 *   - Disconnect: auto-reconnect with exponential backoff (100ms .. 30s).
 *     During disconnect a local warning is logged; events are NOT spooled to file
 *     (enhancement deferred per Wave B scope).
 *   - The file-based EventBus is NOT replaced. Run both in parallel or choose
 *     one per deployment via env var.
 *
 * @connexum/ai-governance  Wave-B
 */
// ---------------------------------------------------------------------------
// Environment config
// ---------------------------------------------------------------------------
export function isRedisEventBusEnabled() {
    return (process.env['REDIS_EVENT_BUS_ENABLED'] ?? 'false').toLowerCase() === 'true';
}
// ---------------------------------------------------------------------------
// Channel helpers
// ---------------------------------------------------------------------------
const CHANNEL_PREFIX = 'aiag:events:';
const WILDCARD_CHANNEL = `${CHANNEL_PREFIX}*`;
function channelForType(type) {
    return `${CHANNEL_PREFIX}${type}`;
}
function typeFromChannel(channel) {
    if (!channel.startsWith(CHANNEL_PREFIX))
        return null;
    return channel.slice(CHANNEL_PREFIX.length);
}
// ---------------------------------------------------------------------------
// RedisEventBus
// ---------------------------------------------------------------------------
export class RedisEventBus {
    agentName;
    redisUrl;
    maxReconnectDelayMs;
    /** Type-specific handlers (mirrors EventBus.handlers) */
    handlers = new Map();
    /** Publisher client -- used for PUBLISH */
    pubClient = null;
    /** Subscriber client -- used for SUBSCRIBE / PSUBSCRIBE */
    subClient = null;
    started = false;
    stopped = false;
    /** The set of EventTypes that have registered handlers (for selective subscription) */
    subscribedTypes = new Set();
    wildcardSubscribed = false;
    constructor(config) {
        this.agentName = config.agentName;
        this.redisUrl = config.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379';
        this.maxReconnectDelayMs = config.maxReconnectDelayMs ?? 30_000;
    }
    // ---------------------------------------------------------------------------
    // Public API (mirrors EventBus)
    // ---------------------------------------------------------------------------
    /**
     * Subscribe to an event type (or '*' for all events).
     * Handlers registered before start() are effective immediately after start().
     * Handlers registered after start() cause a live SUBSCRIBE on the sub client.
     */
    on(type, handler) {
        const existing = this.handlers.get(type) ?? [];
        existing.push(handler);
        this.handlers.set(type, existing);
        // If already started, subscribe to the new channel live
        if (this.started && !this.stopped && this.subClient) {
            this.subscribeToType(type);
        }
    }
    /**
     * Emit an event -- publishes to the appropriate Redis channel.
     * Also dispatches locally to any local handlers registered on this instance
     * (same pod semantics).
     */
    emit(type, data, target) {
        const event = {
            type,
            source: this.agentName,
            target,
            data,
            timestamp: new Date().toISOString(),
        };
        // Publish to Redis (fire-and-forget -- failures are logged, not thrown)
        if (this.pubClient) {
            const channel = channelForType(type);
            const payload = JSON.stringify(event);
            this.pubClient.publish(channel, payload).catch((err) => {
                console.warn(`[redis-event-bus] PUBLISH failed on channel ${channel}: ${err.message}`);
            });
        }
        else {
            console.warn('[redis-event-bus] emit() called before start() or after stop() -- event not published to Redis');
        }
        // Dispatch locally (same-pod consumers receive it immediately)
        this.dispatch(event);
    }
    /**
     * Start the Redis pub/sub transport.
     * Creates publisher + subscriber clients, subscribes to all registered types.
     */
    async start() {
        if (this.started)
            return;
        this.started = true;
        this.stopped = false;
        const { Redis } = await import('ioredis');
        // Publisher client
        this.pubClient = this.makeClient(Redis, 'pub');
        // Subscriber client
        this.subClient = this.makeClient(Redis, 'sub');
        // Register message handler on the subscriber client
        this.subClient.on('message', (channel, payload) => {
            this.handleIncoming(channel, payload);
        });
        // Pattern message handler for wildcard subscriptions
        this.subClient.on('pmessage', (_pattern, channel, payload) => {
            this.handleIncoming(channel, payload);
        });
        // Subscribe to all currently registered types
        for (const type of this.handlers.keys()) {
            this.subscribeToType(type);
        }
    }
    /**
     * Stop the Redis pub/sub transport.
     * Disconnects both pub and sub clients.
     */
    async stop() {
        this.stopped = true;
        if (this.pubClient) {
            await this.pubClient.quit().catch(() => { });
            this.pubClient = null;
        }
        if (this.subClient) {
            await this.subClient.quit().catch(() => { });
            this.subClient = null;
        }
    }
    // ---------------------------------------------------------------------------
    // Internal helpers
    // ---------------------------------------------------------------------------
    /**
     * Create an ioredis client with auto-reconnect backoff.
     * `role` is 'pub' or 'sub' -- used only for log labelling.
     */
    makeClient(Redis, role) {
        const client = new Redis(this.redisUrl, {
            lazyConnect: false,
            retryStrategy: (times) => {
                if (this.stopped)
                    return null; // signal ioredis to stop retrying
                // Exponential backoff: 100ms, 200ms, 400ms, ... capped at maxReconnectDelayMs
                const delay = Math.min(100 * Math.pow(2, times - 1), this.maxReconnectDelayMs);
                return delay;
            },
            enableReadyCheck: true,
            maxRetriesPerRequest: null,
        });
        client.on('error', (err) => {
            console.warn(`[redis-event-bus:${role}] Redis error: ${err.message}. Auto-reconnect in progress.`);
        });
        client.on('close', () => {
            if (!this.stopped) {
                console.warn(`[redis-event-bus:${role}] Redis connection closed. Attempting reconnect...`);
            }
        });
        client.on('reconnecting', () => {
            console.warn(`[redis-event-bus:${role}] Reconnecting to Redis...`);
        });
        client.on('ready', () => {
            // Re-subscribe after reconnect -- ioredis does this automatically for SUBSCRIBE,
            // but we log it for observability.
            console.log(`[redis-event-bus:${role}] Redis connection ready.`);
        });
        return client;
    }
    /**
     * Subscribe the sub client to the channel for a given EventType (or wildcard).
     * Idempotent: no-ops if already subscribed.
     */
    subscribeToType(type) {
        if (!this.subClient)
            return;
        if (type === '*') {
            if (this.wildcardSubscribed)
                return;
            this.wildcardSubscribed = true;
            this.subClient.psubscribe(WILDCARD_CHANNEL).catch((err) => {
                console.warn(`[redis-event-bus:sub] PSUBSCRIBE failed: ${err.message}`);
            });
        }
        else {
            if (this.subscribedTypes.has(type))
                return;
            this.subscribedTypes.add(type);
            const channel = channelForType(type);
            this.subClient.subscribe(channel).catch((err) => {
                console.warn(`[redis-event-bus:sub] SUBSCRIBE failed for ${channel}: ${err.message}`);
            });
        }
    }
    /**
     * Handle an incoming Redis message from either SUBSCRIBE or PSUBSCRIBE.
     * Skips events sourced from this agent (already dispatched locally in emit()).
     */
    handleIncoming(channel, payload) {
        let event;
        try {
            event = JSON.parse(payload);
        }
        catch {
            // Malformed message -- skip
            return;
        }
        // Don't re-dispatch events we emitted (already dispatched locally)
        if (event.source === this.agentName)
            return;
        // Filter by target if set
        if (event.target && event.target !== this.agentName)
            return;
        // Validate that the channel matches the event type (defense-in-depth)
        const expectedType = typeFromChannel(channel);
        if (expectedType !== null && expectedType !== event.type)
            return;
        this.dispatch(event);
    }
    /**
     * Dispatch an event to all registered handlers for its type and wildcard handlers.
     */
    dispatch(event) {
        const typeHandlers = this.handlers.get(event.type) ?? [];
        for (const handler of typeHandlers) {
            try {
                handler(event);
            }
            catch {
                // Swallow handler errors to prevent cascading failures
            }
        }
        const wildcardHandlers = this.handlers.get('*') ?? [];
        for (const handler of wildcardHandlers) {
            try {
                handler(event);
            }
            catch {
                // Swallow
            }
        }
    }
}
//# sourceMappingURL=redis-event-bus.js.map