import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  OnGatewayInit,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Logger, OnModuleDestroy } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { MetricsService } from '../metrics/metrics.service';
import { RedisPubSubService } from '../redis/redis-pubsub.service';

interface SubscriptionPayload {
  metrics: string[];
  interval?: number;
}

interface ClientSubscription {
  metrics: string[];
  timer?: NodeJS.Timeout;
}

@WebSocketGateway({
  cors: {
    origin: '*',
    credentials: true,
  },
  namespace: '/analytics',
})
export class AnalyticsGateway
  implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, OnModuleDestroy {
  @WebSocketServer()
  server!: Server;

  private readonly logger = new Logger(AnalyticsGateway.name);
  private readonly subscriptions = new Map<string, ClientSubscription>();
  private redisUnsubscribe?: () => void;
  private fallbackTimer?: NodeJS.Timeout;

  constructor(
    private readonly metricsService: MetricsService,
    private readonly redisPubSub: RedisPubSubService,
  ) {}

  afterInit() {
    this.logger.log('WebSocket gateway initialized');

    // Subscribe to Redis pub/sub updates
    this.redisUnsubscribe = this.redisPubSub.onUpdate((message) => {
      this.handleRedisUpdate(message);
    });

    // Set up fallback polling (60s) in case Redis is down
    this.fallbackTimer = setInterval(async () => {
      if (!this.redisPubSub.isHealthy()) {
        this.logger.warn('Redis unhealthy, using fallback polling');
        await this.broadcastToAllClients();
      }
    }, 60_000);

    this.logger.log('Redis pub/sub listener registered');
  }

  onModuleDestroy() {
    if (this.redisUnsubscribe) {
      this.redisUnsubscribe();
    }
    if (this.fallbackTimer) {
      clearInterval(this.fallbackTimer);
    }
  }

  handleConnection(client: Socket) {
    this.logger.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`Client disconnected: ${client.id}`);
    this.clearSubscriptions(client.id);
  }

  @SubscribeMessage('subscribe')
  handleSubscribe(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: SubscriptionPayload,
  ) {
    const { metrics } = payload;

    this.logger.log(`Client ${client.id} subscribing to: ${metrics.join(', ')}`);

    // Clear existing subscription
    this.clearSubscriptions(client.id);

    // Store client subscription (no polling timer needed)
    this.subscriptions.set(client.id, { metrics });

    // Send initial data immediately
    this.metricsService.getRealtimeMetrics(metrics).then((data) => {
      client.emit('metrics', data);
    });

    return { subscribed: metrics, mode: 'push' };
  }

  @SubscribeMessage('unsubscribe')
  handleUnsubscribe(@ConnectedSocket() client: Socket) {
    this.clearSubscriptions(client.id);
    return { unsubscribed: true };
  }

  private clearSubscriptions(clientId: string) {
    const subscription = this.subscriptions.get(clientId);
    if (subscription) {
      if (subscription.timer) {
        clearInterval(subscription.timer);
      }
      this.subscriptions.delete(clientId);
    }
  }

  /**
   * Handle metrics update from Redis pub/sub.
   * Broadcast to all connected clients with matching subscriptions.
   */
  private async handleRedisUpdate(message: any) {
    this.logger.debug(`Received Redis update: ${JSON.stringify(message)}`);

    // Broadcast to all subscribed clients
    for (const [clientId, subscription] of this.subscriptions.entries()) {
      const socket = this.server.sockets.get(clientId);
      if (socket) {
        try {
          const data = await this.metricsService.getRealtimeMetrics(subscription.metrics);
          socket.emit('metrics', data);
        } catch (error) {
          this.logger.error(`Failed to send metrics to client ${clientId}: ${error}`);
        }
      }
    }
  }

  /**
   * Fallback: broadcast to all clients (used when Redis is down).
   */
  private async broadcastToAllClients() {
    for (const [clientId, subscription] of this.subscriptions.entries()) {
      const socket = this.server.sockets.get(clientId);
      if (socket) {
        try {
          const data = await this.metricsService.getRealtimeMetrics(subscription.metrics);
          socket.emit('metrics', data);
        } catch (error) {
          this.logger.error(`Failed to send metrics to client ${clientId}: ${error}`);
        }
      }
    }
  }

  /**
   * Legacy method for manual metric updates (kept for backwards compatibility).
   */
  broadcastMetricUpdate(metric: string, value: number) {
    this.server.emit('metric-update', { metric, value, timestamp: new Date() });
  }
}
