/**
 * Platform EventBus — in-process pub/sub for decoupled service integration.
 *
 * Design:
 *  - Uses Node.js EventEmitter (single process, zero serialization overhead)
 *  - Typed events via PlatformEventMap (compile-time safety)
 *  - Wildcard subscriptions: subscribe('shift.*') catches all shift events
 *  - Error isolation: each listener wraps its handler in try-catch
 *  - Stats tracking: per-event and per-listener counters for observability
 *
 * Usage:
 *   // Publish (from any service)
 *   eventBus.publish('shift.signup.created', { shiftId, userName, ... });
 *
 *   // Subscribe (from listeners registered at startup)
 *   eventBus.subscribe('shift.signup.created', async (payload) => { ... });
 *   eventBus.subscribe('shift.*', async (payload) => { ... }); // wildcard
 */

import { EventEmitter } from 'events';
import { logger } from '../utils/logger';
import type { PlatformEventMap, PlatformEventName, EventPayload } from '../types/events';

type EventHandler<E extends PlatformEventName> = (payload: EventPayload<E>) => void | Promise<void>;

interface ListenerRegistration {
  name: string;
  pattern: string;
  handler: (event: string, payload: unknown) => void | Promise<void>;
}

interface EventStats {
  published: number;
  lastPublishedAt: Date | null;
}

class EventBus {
  private emitter = new EventEmitter();
  private listeners: ListenerRegistration[] = [];
  private eventStats = new Map<string, EventStats>();
  private listenerStats = new Map<string, { handled: number; errors: number }>();

  constructor() {
    // Allow many listeners (we'll have multiple per event)
    this.emitter.setMaxListeners(100);
  }

  /**
   * Publish a typed event. All matching subscribers are called asynchronously.
   * This is fire-and-forget — errors in listeners do NOT propagate to the publisher.
   */
  publish<E extends PlatformEventName>(event: E, payload: EventPayload<E>): void {
    // Update stats
    const stats = this.eventStats.get(event) ?? { published: 0, lastPublishedAt: null };
    stats.published++;
    stats.lastPublishedAt = new Date();
    this.eventStats.set(event, stats);

    // Emit to exact subscribers
    this.emitter.emit(event, payload);

    // Emit to wildcard subscribers
    for (const reg of this.listeners) {
      if (reg.pattern.endsWith('.*')) {
        const prefix = reg.pattern.slice(0, -2);
        if (event.startsWith(prefix + '.') && event !== reg.pattern) {
          this.safeCall(reg.name, () => reg.handler(event, payload));
        }
      }
    }

    logger.debug(`EventBus: ${event}`, { event });
  }

  /**
   * Subscribe to a specific event with a named listener.
   * The name is used for stats tracking and debugging.
   */
  subscribe<E extends PlatformEventName>(
    event: E,
    name: string,
    handler: EventHandler<E>,
  ): void {
    const wrappedHandler = (payload: EventPayload<E>) => {
      this.safeCall(name, () => handler(payload));
    };

    this.emitter.on(event, wrappedHandler);
    this.listeners.push({
      name,
      pattern: event,
      handler: (_event: string, payload: unknown) => handler(payload as EventPayload<E>),
    });
    this.listenerStats.set(name, { handled: 0, errors: 0 });
  }

  /**
   * Subscribe to all events matching a wildcard pattern (e.g., 'shift.*').
   * Handler receives both the event name and payload.
   */
  subscribePattern(
    pattern: string,
    name: string,
    handler: (event: string, payload: unknown) => void | Promise<void>,
  ): void {
    this.listeners.push({ name, pattern, handler });
    this.listenerStats.set(name, { handled: 0, errors: 0 });
  }

  /**
   * Call a handler with error isolation and stats tracking.
   */
  private safeCall(listenerName: string, fn: () => void | Promise<void>): void {
    const stats = this.listenerStats.get(listenerName);

    try {
      const result = fn();
      if (result instanceof Promise) {
        result
          .then(() => {
            if (stats) stats.handled++;
          })
          .catch((err) => {
            if (stats) {
              stats.handled++;
              stats.errors++;
            }
            logger.debug(`EventBus listener "${listenerName}" error:`, err);
          });
      } else {
        if (stats) stats.handled++;
      }
    } catch (err) {
      if (stats) {
        stats.handled++;
        stats.errors++;
      }
      logger.debug(`EventBus listener "${listenerName}" sync error:`, err);
    }
  }

  /**
   * Get stats for observability dashboard.
   */
  getStats(): {
    totalEventsPublished: number;
    eventCounts: Record<string, { published: number; lastPublishedAt: string | null }>;
    listenerCounts: Record<string, { handled: number; errors: number }>;
    registeredListeners: { name: string; pattern: string }[];
  } {
    let total = 0;
    const eventCounts: Record<string, { published: number; lastPublishedAt: string | null }> = {};
    for (const [name, stats] of this.eventStats) {
      total += stats.published;
      eventCounts[name] = {
        published: stats.published,
        lastPublishedAt: stats.lastPublishedAt?.toISOString() ?? null,
      };
    }

    const listenerCounts: Record<string, { handled: number; errors: number }> = {};
    for (const [name, stats] of this.listenerStats) {
      listenerCounts[name] = { ...stats };
    }

    return {
      totalEventsPublished: total,
      eventCounts,
      listenerCounts,
      registeredListeners: this.listeners.map(l => ({ name: l.name, pattern: l.pattern })),
    };
  }

  /**
   * Remove all listeners (for testing or shutdown).
   */
  removeAllListeners(): void {
    this.emitter.removeAllListeners();
    this.listeners = [];
  }
}

export const eventBus = new EventBus();
