/**
 * EventBus — Live in-memory event transport for AirCoding V1.0.0 Alpha
 *
 * Implements live transport only (never recovery source).
 * Provides: publish, subscribe, match, drain methods.
 * Supports ephemeral coalescing for 7 event types per event-registry-v1.md §4.
 *
 * @module packages/runtime/src/events/EventBus
 */

// Global console declaration for Node.js runtime
declare const console: {
  error: (...args: unknown[]) => void
  warn: (...args: unknown[]) => void
  log: (...args: unknown[]) => void
}

import type { RuntimeEvent, EventFilter } from '@aircoding/contracts'
import { eventSchemaRegistry } from './EventSchemaRegistry.js'

// =============================================================================
// Types
// =============================================================================

export type EventHandler<T = unknown> = (event: RuntimeEvent<T>) => void | Promise<void>

export interface Subscription {
  filter: EventFilter
  handler: EventHandler<unknown>
  id: string
}

/**
 * Coalescing state for ephemeral events that support throttling.
 * Per event-registry §4: agent.heartbeat, task.progress, assistant.message.delta,
 * tool.progress, command.stdout.delta, command.stderr.delta, hud.frame.rendered
 */
interface CoalesceState {
  lastEvent: RuntimeEvent | null
  lastEmitTime: number
  pendingCount: number
}

// =============================================================================
// Constants
// =============================================================================

/** Ephemeral event types that support coalescing/throttling */
const COALESCEABLE_TYPES: Set<string> = new Set([
  'agent.heartbeat',
  'task.progress',
  'assistant.message.delta',
  'tool.progress',
  'command.stdout.delta',
  'command.stderr.delta',
  'hud.frame.rendered',
])

/** Default coalescing window in milliseconds */
const DEFAULT_COALESCE_WINDOW_MS = 100

/** Maximum events to queue before forcing emit */
const MAX_PENDING_COUNT = 10

// =============================================================================
// EventBus
// =============================================================================

/**
 * In-memory event bus for live event transport.
 *
 * Rules (contracts §7):
 * - Live transport only, never a recovery source of truth
 * - If a handler throws, catch error, log to developer log, do not propagate
 * - Subscription stays active after errors
 * - drain() flushes pending async handlers for clean shutdown
 */
export class EventBus {
  private subscriptions: Map<string, Subscription> = new Map()
  private subscriptionIdCounter: number = 0
  private coalesceStates: Map<string, CoalesceState> = new Map()
  private coalesceWindowMs: number = DEFAULT_COALESCE_WINDOW_MS
  private isDraining: boolean = false
  private pendingHandlers: Array<Promise<void>> = []

  constructor(options?: { coalesceWindowMs?: number }) {
    if (options?.coalesceWindowMs) {
      this.coalesceWindowMs = options.coalesceWindowMs
    }
  }

  /**
   * Publish an event to all matching subscribers.
   * For ephemeral events, applies coalescing logic.
   *
   * @param event - The event to publish
   */
  publish<T>(event: RuntimeEvent<T>): void {
    // Check if this is a coalesceable ephemeral event
    if (this.shouldCoalesce(event.type)) {
      this.publishCoalesced(event as RuntimeEvent<unknown>)
      return
    }

    // Direct publish for non-coalesced events
    this.doPublish(event)
  }

  /**
   * Subscribe to events matching the given filter.
   *
   * @param filter - EventFilter defining which events to receive
   * @param handler - Callback function to invoke when matching events occur
   * @returns Subscription object that can be used to unsubscribe
   */
  subscribe<T>(filter: EventFilter, handler: EventHandler<T>): Subscription {
    const id = `sub_${++this.subscriptionIdCounter}`
    // Cast handler to unknown handler type for storage
    const subscription: Subscription = { filter, handler: handler as EventHandler<unknown>, id }
    this.subscriptions.set(id, subscription)
    return subscription
  }

  /**
   * Unsubscribe from events.
   *
   * @param subscription - The subscription to remove
   */
  unsubscribe(subscription: Subscription): void {
    this.subscriptions.delete(subscription.id)
  }

  /**
   * Check if an event matches an EventFilter.
   *
   * @param filter - The filter to test against
   * @param event - The event to check
   * @returns true if the event matches the filter
   */
  match(filter: EventFilter, event: RuntimeEvent): boolean {
    // Check session_id
    if (filter.session_id && event.session_id !== filter.session_id) {
      return false
    }

    // Check types
    if (filter.types && filter.types.length > 0) {
      if (!filter.types.includes(event.type)) {
        return false
      }
    }

    // Check task_id
    if (filter.task_id && event.payload && typeof event.payload === 'object') {
      const payload = event.payload as Record<string, unknown>
      if (payload.task_id !== filter.task_id) {
        return false
      }
    }

    // Check agent_id
    if (filter.agent_id && event.payload && typeof event.payload === 'object') {
      const payload = event.payload as Record<string, unknown>
      if (payload.agent_id !== filter.agent_id) {
        return false
      }
    }

    // Check tool_run_id
    if (filter.tool_run_id && event.payload && typeof event.payload === 'object') {
      const payload = event.payload as Record<string, unknown>
      if (payload.tool_run_id !== filter.tool_run_id) {
        return false
      }
    }

    // Check command_run_id
    if (filter.command_run_id && event.payload && typeof event.payload === 'object') {
      const payload = event.payload as Record<string, unknown>
      if (payload.command_run_id !== filter.command_run_id) {
        return false
      }
    }

    // Check route_prefix
    if (filter.route_prefix && filter.route_prefix.length > 0) {
      const routePrefix = filter.route_prefix.join('/')
      if (!event.route.join('/').startsWith(routePrefix)) {
        return false
      }
    }

    // Check since (timestamp)
    if (filter.since) {
      if (event.timestamp < filter.since) {
        return false
      }
    }

    return true
  }

  /**
   * Drain all pending async handlers.
   * Waits for all in-flight handlers to complete before returning.
   *
   * @returns Promise that resolves when all handlers have completed
   */
  async drain(): Promise<void> {
    this.isDraining = true
    try {
      if (this.pendingHandlers.length > 0) {
        await Promise.all(this.pendingHandlers)
      }
    } finally {
      this.isDraining = false
    }
  }

  /**
   * Get the count of active subscriptions.
   */
  getSubscriptionCount(): number {
    return this.subscriptions.size
  }

  // ---------------------------------------------------------------------------
  // Private methods
  // ---------------------------------------------------------------------------

  /**
   * Check if an event type should be coalesced.
   * Only ephemeral events with coalescing support are coalesced.
   */
  private shouldCoalesce(eventType: string): boolean {
    if (!COALESCEABLE_TYPES.has(eventType)) {
      return false
    }

    // Verify it's an ephemeral event
    const persistence = eventSchemaRegistry.getPersistence(eventType, 1)
    return persistence === 'ephemeral'
  }

  /**
   * Publish with coalescing for high-frequency ephemeral events.
   * Throttles events within the coalesce window.
   */
  private publishCoalesced(event: RuntimeEvent<unknown>): void {
    const state = this.getOrCreateCoalesceState(event.type)

    const now = Date.now()
    const timeSinceLastEmit = now - state.lastEmitTime

    // Update pending count
    state.pendingCount++

    // Store the latest event
    state.lastEvent = event

    // Emit if: max pending reached, no previous event, or window elapsed
    if (state.pendingCount >= MAX_PENDING_COUNT || state.lastEmitTime === 0 || timeSinceLastEmit >= this.coalesceWindowMs) {
      this.flushCoalesced(event.type)
    }
  }

  /**
   * Get or create coalescing state for an event type.
   */
  private getOrCreateCoalesceState(eventType: string): CoalesceState {
    let state = this.coalesceStates.get(eventType)
    if (!state) {
      state = { lastEvent: null, lastEmitTime: 0, pendingCount: 0 }
      this.coalesceStates.set(eventType, state)
    }
    return state
  }

  /**
   * Flush coalesced events for a given type.
   */
  private flushCoalesced(eventType: string): void {
    const state = this.coalesceStates.get(eventType)
    if (!state || !state.lastEvent) {
      return
    }

    // Emit the last event (which contains the latest state)
    this.doPublish(state.lastEvent)

    // Reset coalesce state
    state.lastEmitTime = Date.now()
    state.pendingCount = 0
  }

  /**
   * Internal publish that delivers to all matching subscribers.
   */
  private doPublish<T>(event: RuntimeEvent<T>): void {
    for (const subscription of this.subscriptions.values()) {
      if (this.match(subscription.filter, event)) {
        this.invokeHandler(subscription.handler, event)
      }
    }
  }

  /**
   * Invoke a handler, catching and logging any errors.
   * Per contracts §7 rule 5: errors do not propagate and subscription stays active.
   */
  private invokeHandler<T>(handler: EventHandler<T>, event: RuntimeEvent<T>): void {
    try {
      const result = handler(event)
      if (result instanceof Promise) {
        if (!this.isDraining) {
          this.pendingHandlers.push(result.catch((err) => {
            // Log to developer log - in production this would go to a proper logger
            console.error('[EventBus] Handler error (non-fatal):', err)
          }))
        } else {
          // During drain, await the promise
          this.pendingHandlers.push(result.catch((err) => {
            console.error('[EventBus] Handler error during drain:', err)
          }))
        }
      }
    } catch (err) {
      // Per contracts §7 rule 5: errors do not propagate
      console.error('[EventBus] Handler threw sync error (non-fatal):', err)
    }
  }
}

/**
 * Default singleton instance for global use.
 */
export const eventBus = new EventBus()