import type { Source } from "../../../operations/from";
import type { Exchange, ExchangeHeaders } from "../../../exchange";
import type { CraftContext } from "../../../context";
import type { EventName, EventPayload } from "../../../types";
import type { EventFilter } from "./types";

/**
 * Event source adapter that produces exchanges from framework events.
 * Subscribes to context events and emits them as exchanges.
 *
 * ⚠️ **Circular Route Warning:**
 * Be careful when subscribing to `:operation:` or `:exchange:` events.
 * If your handler emits new operation events (e.g., by calling adapters),
 * you can create infinite loops.
 *
 * **Unsafe Pattern (Infinite Loop):**
 * ```typescript
 * // ❌ DON'T: This creates an infinite loop
 * craft()
 *   .from(event('route:*:step:**'))
 *   .to(http(...))  // Emits route:X:step:started -> loop!
 * ```
 *
 * **Safe Patterns:**
 * ```typescript
 * // ✅ SAFE: Use non-step destinations
 * craft()
 *   .from(event('route:*:step:**'))
 *   .to(log())  // log() doesn't emit step events
 *
 * // ✅ SAFE: Filter to avoid self-reference
 * craft()
 *   .from(event('route:*:step:completed'))
 *   .filter((ex) => ex.body.details.routeId !== 'monitoring-route')
 *   .to(metrics())
 *
 * // ✅ SAFE: Subscribe to non-step events only
 * craft()
 *   .from(event('route:*:started'))
 *   .to(http(...))  // Route lifecycle events don't cause loops
 * ```
 *
 * Use step events for observability routes that don't emit more step events.
 */
export class EventSourceAdapter implements Source<EventPayload<EventName>> {
  readonly adapterId = "routecraft.adapter.event";

  constructor(private filter: EventFilter) {}

  async subscribe(
    context: CraftContext,
    handler: (
      message: EventPayload<EventName>,
      headers?: ExchangeHeaders,
    ) => Promise<Exchange>,
    abortController: AbortController,
    onReady?: () => void,
  ): Promise<void> {
    const filters = Array.isArray(this.filter) ? this.filter : [this.filter];
    const unsubscribers: Array<() => void> = [];

    // Get the route ID from the first handler call (when onReady is called, we're part of a route)
    // This allows us to filter out events generated by this route to prevent infinite loops
    let initialized = false;

    // Determine which events to subscribe to
    const eventNames = this.resolveEventNames(filters);

    context.logger.debug(
      { adapter: "event", filter: this.filter, eventNames },
      "Subscribing to events",
    );

    // Subscribe to each resolved event
    for (const eventName of eventNames) {
      const unsubscribe = context.on(
        eventName as EventName,
        async (payload) => {
          if (!abortController.signal.aborted) {
            // Initialize route ID on first event if needed
            if (!initialized) {
              initialized = true;
            }

            // Note: All event patterns (including operation/exchange patterns) are now
            // passed directly to context.on() for matching. Be careful to avoid circular
            // routes where operation events trigger handlers that emit more operation events.

            try {
              await handler(payload);
            } catch (err) {
              const metaMessage =
                typeof err === "object" &&
                err !== null &&
                "meta" in err &&
                typeof (err as { meta?: { message?: unknown } }).meta
                  ?.message === "string"
                  ? (err as { meta: { message: string } }).meta.message
                  : undefined;
              const errorMessage =
                typeof err === "object" &&
                err !== null &&
                "message" in err &&
                typeof (err as { message?: unknown }).message === "string"
                  ? (err as { message: string }).message
                  : undefined;

              context.logger.warn(
                { adapter: "event", event: eventName, err },
                metaMessage ?? errorMessage ?? "Event handler failed",
              );
            }
          }
        },
      );
      unsubscribers.push(unsubscribe);
    }

    // Cleanup on abort
    abortController.signal.addEventListener("abort", () => {
      context.logger.debug({ adapter: "event" }, "Unsubscribing from events");
      for (const unsubscribe of unsubscribers) {
        unsubscribe();
      }
    });

    onReady?.();

    // Keep the subscription alive until aborted
    return new Promise<void>((resolve) => {
      // Check if already aborted
      if (abortController.signal.aborted) {
        resolve();
        return;
      }

      // Add listener with { once: true } to prevent leaks
      abortController.signal.addEventListener("abort", () => resolve(), {
        once: true,
      });
    });
  }

  /**
   * Resolve event names from filters.
   *
   * Since context.on() supports wildcard matching (including ** globstar),
   * we simply pass patterns through without expansion. This allows
   * hierarchical patterns like "route:*:operation:**" to work correctly.
   */
  private resolveEventNames(filters: string[]): string[] {
    // Deduplicate filters
    const resolved = new Set<string>();

    for (const filter of filters) {
      resolved.add(filter);
    }

    return Array.from(resolved);
  }
}
