import { Router } from 'zeromq';
import type { FastifyBaseLogger } from 'fastify';
import type { SessionRegistry } from './session-registry.js';
import type { DeliveryService } from './delivery-service.js';
import {
  deserializeUserEvent,
  serializeEventAck,
  parseEventPayload,
  getEventTypeName,
  getChannelTypeName,
  AckStatus,
  ChannelType,
  Priority,
  type UserEvent,
  type EventAck,
} from './types.js';

export interface EventRouterConfig {
  sessions: SessionRegistry;
  delivery: DeliveryService;
  logger: FastifyBaseLogger;
  bindEndpoint?: string;
  dedupTtlMs?: number;
}

/**
 * EventRouter handles critical events from user containers.
 *
 * Uses ZMQ ROUTER socket that all containers connect to via DEALER.
 * Provides guaranteed delivery with acknowledgments.
 *
 * Flow:
 * 1. Container sends UserEvent via DEALER
 * 2. Router receives with identity frame
 * 3. Router delivers event through channel preferences
 * 4. Router sends EventAck back to container
 *
 * Features:
 * - Deduplication (5 minute window by default)
 * - Channel preference ordering (try first channel, then second, etc.)
 * - Acknowledgment with delivery status
 */
export class EventRouter {
  private socket: Router;
  private sessions: SessionRegistry;
  private delivery: DeliveryService;
  private logger: FastifyBaseLogger;
  private bindEndpoint: string;
  private dedupTtlMs: number;

  // Deduplication: track recently processed event IDs
  // Map: eventId -> timestamp when processed
  private processedEvents = new Map<string, number>();

  private running = false;
  private messageLoopPromise: Promise<void> | null = null;
  private cleanupInterval: NodeJS.Timeout | null = null;

  constructor(config: EventRouterConfig) {
    this.socket = new Router();
    this.sessions = config.sessions;
    this.delivery = config.delivery;
    this.logger = config.logger.child({ component: 'EventRouter' });
    this.bindEndpoint = config.bindEndpoint || 'tcp://*:5571';
    this.dedupTtlMs = config.dedupTtlMs || 5 * 60 * 1000; // 5 minutes
  }

  /**
   * Start the event router.
   */
  async start(): Promise<void> {
    await this.socket.bind(this.bindEndpoint);
    this.logger.info({ endpoint: this.bindEndpoint }, 'Event router bound');

    this.running = true;
    this.messageLoopPromise = this.messageLoop();
    this.startCleanupTimer();

    this.logger.info('Event router started');
  }

  /**
   * Stop the event router.
   */
  async stop(): Promise<void> {
    this.running = false;

    if (this.cleanupInterval) {
      clearInterval(this.cleanupInterval);
      this.cleanupInterval = null;
    }

    this.socket.close();

    if (this.messageLoopPromise) {
      try {
        await this.messageLoopPromise;
      } catch {
        // Ignore errors during shutdown
      }
    }

    this.logger.info('Event router stopped');
  }

  /**
   * Main message processing loop.
   */
  private async messageLoop(): Promise<void> {
    this.logger.debug('Starting message loop');

    try {
      for await (const [identity, payload] of this.socket) {
        if (!this.running) break;

        try {
          await this.processMessage(identity, payload);
        } catch (error) {
          this.logger.error({ error }, 'Error processing critical event');
          // Don't ack on error - let container retry
        }
      }
    } catch (error) {
      if (this.running) {
        this.logger.error({ error }, 'Message loop error');
      }
    }

    this.logger.debug('Message loop ended');
  }

  /**
   * Process a single message from the ROUTER socket.
   */
  private async processMessage(identity: Buffer, payload: Buffer): Promise<void> {
    // Deserialize the event
    const event = deserializeUserEvent(payload);

    this.logger.info(
      {
        userId: event.userId,
        eventId: event.eventId,
        eventType: getEventTypeName(event.eventType),
        priority: Priority[event.delivery.priority],
      },
      'Received critical event'
    );

    // Deduplication check
    if (this.processedEvents.has(event.eventId)) {
      this.logger.debug({ eventId: event.eventId }, 'Duplicate event, sending cached ack');
      await this.sendAck(identity, {
        eventId: event.eventId,
        status: AckStatus.DELIVERED,
        errorMessage: '',
      });
      return;
    }

    // Deliver through channel preferences
    const result = await this.deliverEvent(event);

    // Mark as processed (for deduplication)
    this.processedEvents.set(event.eventId, Date.now());

    // Send ack back to container
    await this.sendAck(identity, result);

    this.logger.info(
      {
        eventId: event.eventId,
        status: AckStatus[result.status],
        deliveredVia: result.deliveredVia ? getChannelTypeName(result.deliveredVia) : undefined,
      },
      'Event processed'
    );
  }

  /**
   * Deliver an event through channel preferences.
   * Tries each channel in order until one succeeds.
   */
  private async deliverEvent(event: UserEvent): Promise<EventAck> {
    for (const pref of event.delivery.channels) {
      // Skip if channel requires active session but none exists
      if (pref.onlyIfActive && !this.sessions.has(event.userId)) {
        this.logger.debug(
          {
            eventId: event.eventId,
            channel: getChannelTypeName(pref.channel),
          },
          'Skipping channel (requires active session)'
        );
        continue;
      }

      try {
        const delivered = await this.deliverToChannel(event, pref.channel);
        if (delivered) {
          return {
            eventId: event.eventId,
            status: AckStatus.DELIVERED,
            errorMessage: '',
            deliveredVia: pref.channel,
          };
        }
      } catch (error) {
        this.logger.warn(
          {
            error,
            eventId: event.eventId,
            channel: getChannelTypeName(pref.channel),
          },
          'Channel delivery failed, trying next'
        );
        // Continue to next channel preference
      }
    }

    // All channels failed
    this.logger.error({ eventId: event.eventId }, 'All delivery channels failed');
    return {
      eventId: event.eventId,
      status: AckStatus.ACK_ERROR,
      errorMessage: 'All delivery channels failed',
    };
  }

  /**
   * Deliver event to a specific channel.
   * Returns true if delivery succeeded.
   */
  private async deliverToChannel(event: UserEvent, channel: ChannelType): Promise<boolean> {
    switch (channel) {
      case ChannelType.ACTIVE_SESSION: {
        const session = this.sessions.get(event.userId);
        if (!session) return false;

        const message = this.formatWebSocketMessage(event);
        session.socket.send(message);
        return true;
      }

      case ChannelType.WEB: {
        // WEB is same as ACTIVE_SESSION for WebSocket connections
        const session = this.sessions.get(event.userId);
        if (!session || session.channelType !== 'websocket') return false;

        const message = this.formatWebSocketMessage(event);
        session.socket.send(message);
        return true;
      }

      case ChannelType.TELEGRAM:
        await this.delivery.sendTelegram(event.userId, event);
        return true;

      case ChannelType.EMAIL:
        await this.delivery.sendEmail(event.userId, event);
        return true;

      case ChannelType.PUSH:
        await this.delivery.sendPush(event.userId, event);
        return true;

      case ChannelType.DISCORD:
        await this.delivery.sendDiscord(event.userId, event);
        return true;

      case ChannelType.SLACK:
        await this.delivery.sendSlack(event.userId, event);
        return true;

      default:
        this.logger.warn({ channel }, 'Unknown channel type');
        return false;
    }
  }

  /**
   * Send an EventAck back to a container.
   */
  private async sendAck(identity: Buffer, ack: EventAck): Promise<void> {
    const payload = serializeEventAck(ack);
    await this.socket.send([identity, payload]);
  }

  /**
   * Format a UserEvent as a WebSocket message.
   */
  private formatWebSocketMessage(event: UserEvent): string {
    const payload = parseEventPayload(event);

    return JSON.stringify({
      type: 'event',
      eventType: getEventTypeName(event.eventType),
      eventId: event.eventId,
      timestamp: event.timestamp,
      payload: payload,
      priority: Priority[event.delivery.priority],
    });
  }

  /**
   * Start the deduplication cleanup timer.
   */
  private startCleanupTimer(): void {
    this.cleanupInterval = setInterval(() => {
      this.cleanupProcessedEvents();
    }, 60000); // Cleanup every minute
  }

  /**
   * Remove expired entries from the processed events map.
   */
  private cleanupProcessedEvents(): void {
    const now = Date.now();
    let cleaned = 0;

    for (const [eventId, timestamp] of this.processedEvents) {
      if (now - timestamp > this.dedupTtlMs) {
        this.processedEvents.delete(eventId);
        cleaned++;
      }
    }

    if (cleaned > 0) {
      this.logger.debug({ cleaned }, 'Cleaned up processed events');
    }
  }

  /**
   * Get count of tracked processed events (for monitoring).
   */
  getProcessedEventCount(): number {
    return this.processedEvents.size;
  }
}
