import { Subscriber } from 'zeromq';
import type { FastifyBaseLogger } from 'fastify';
import type { SessionRegistry, Session } from './session-registry.js';
import {
  deserializeUserEvent,
  parseEventPayload,
  getEventTypeName,
  type UserEvent,
} from './types.js';

/**
 * EventSubscriber handles informational events from user containers.
 *
 * Uses ZMQ SUB socket to connect to container XPUB sockets.
 * When a user session connects, we subscribe to their events.
 * When they disconnect, we unsubscribe.
 *
 * This is for fire-and-forget events that only matter if the user
 * is actively connected (e.g., chart updates, strategy logs).
 */
export class EventSubscriber {
  private socket: Subscriber;
  private sessions: SessionRegistry;
  private logger: FastifyBaseLogger;

  // Track which container endpoints we're connected to
  // Map: userId -> containerEndpoint
  private containerConnections = new Map<string, string>();

  // Track active subscriptions
  // Set of topic strings we're subscribed to
  private activeSubscriptions = new Set<string>();

  private running = false;
  private messageLoopPromise: Promise<void> | null = null;

  constructor(sessions: SessionRegistry, logger: FastifyBaseLogger) {
    this.socket = new Subscriber();
    this.sessions = sessions;
    this.logger = logger.child({ component: 'EventSubscriber' });
  }

  /**
   * Start the event subscriber.
   */
  async start(): Promise<void> {
    this.running = true;
    this.messageLoopPromise = this.messageLoop();
    this.logger.info('Event subscriber started');
  }

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

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

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

  /**
   * Called when a user session connects.
   * Connects to the user's container XPUB and subscribes to their events.
   */
  async onSessionConnect(session: Session): Promise<void> {
    const topic = `USER:${session.userId}`;
    const endpoint = session.containerEndpoint;

    // Connect to container if not already connected
    if (!this.containerConnections.has(session.userId)) {
      try {
        this.socket.connect(endpoint);
        this.containerConnections.set(session.userId, endpoint);
        this.logger.info(
          { userId: session.userId, endpoint },
          'Connected to container XPUB'
        );
      } catch (error) {
        this.logger.error(
          { error, userId: session.userId, endpoint },
          'Failed to connect to container XPUB'
        );
        return;
      }
    }

    // Subscribe to user's topic
    if (!this.activeSubscriptions.has(topic)) {
      this.socket.subscribe(topic);
      this.activeSubscriptions.add(topic);
      this.logger.info({ userId: session.userId, topic }, 'Subscribed to user events');
    }
  }

  /**
   * Called when a user session disconnects.
   * Unsubscribes from their events.
   */
  async onSessionDisconnect(session: Session): Promise<void> {
    const topic = `USER:${session.userId}`;

    // Unsubscribe from user's topic
    if (this.activeSubscriptions.has(topic)) {
      this.socket.unsubscribe(topic);
      this.activeSubscriptions.delete(topic);
      this.logger.info({ userId: session.userId, topic }, 'Unsubscribed from user events');
    }

    // Optionally disconnect from container after a delay
    // (in case user reconnects quickly)
    // For now, we keep the connection open
  }

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

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

        try {
          await this.processMessage(topicBuf, payloadBuf);
        } catch (error) {
          this.logger.error({ error }, 'Error processing informational event');
        }
      }
    } catch (error) {
      if (this.running) {
        this.logger.error({ error }, 'Message loop error');
      }
    }

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

  /**
   * Process a single message from the SUB socket.
   */
  private async processMessage(topicBuf: Buffer, payloadBuf: Buffer): Promise<void> {
    const topic = topicBuf.toString();
    const userId = topic.replace('USER:', '');

    // Deserialize the event
    const event = deserializeUserEvent(payloadBuf);

    this.logger.debug(
      {
        userId,
        eventId: event.eventId,
        eventType: getEventTypeName(event.eventType),
      },
      'Received informational event'
    );

    // Forward to active session
    const session = this.sessions.get(userId);
    if (!session) {
      this.logger.debug({ userId }, 'No active session for event, dropping');
      return;
    }

    // Format and send to WebSocket
    const wsMessage = this.formatWebSocketMessage(event);
    try {
      session.socket.send(wsMessage);
      this.logger.debug(
        { userId, eventId: event.eventId },
        'Forwarded event to WebSocket'
      );
    } catch (error) {
      this.logger.warn(
        { error, userId, eventId: event.eventId },
        'Failed to send event to WebSocket'
      );
    }
  }

  /**
   * 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,
    });
  }

  /**
   * Get current subscription count.
   */
  getSubscriptionCount(): number {
    return this.activeSubscriptions.size;
  }

  /**
   * Get current connection count.
   */
  getConnectionCount(): number {
    return this.containerConnections.size;
  }
}
