import { resolveRabbitMqConfig } from '../../../libs/common/src';
import { NOTIFICATION_RMQ_DLQ, NOTIFICATION_RMQ_QUEUE, NOTIFICATION_RPC, NotificationRpcResponse } from '../../../libs/contracts/src';
import { FollowRequestApprovedEvent } from '../../../libs/events/src';
import { Injectable, OnApplicationBootstrap, OnModuleDestroy, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ClientProxy, ClientProxyFactory, RmqRecordBuilder, Transport } from '@nestjs/microservices';
import { randomUUID } from 'crypto';
import { firstValueFrom, retry, timeout, timer } from 'rxjs';
import { NotificationCategory, NotificationQueryDto } from './dto/notification-query.dto';
import { NotificationsGateway } from './notifications.gateway';
import { NotificationsService } from './notifications.service';

@Injectable()
export class NotificationServiceClient implements OnApplicationBootstrap, OnModuleDestroy {
  private client?: ClientProxy;
  private rabbitReady = false;
  private consecutiveFailures = 0;
  private circuitOpenUntil = 0;

  constructor(private readonly config: ConfigService, private readonly local: NotificationsService, private readonly gateway: NotificationsGateway) {
    if (this.isRemoteEnabled()) this.getClient();
  }

  isRemoteEnabled(): boolean {
    return String(this.config.get('NOTIFICATION_SERVICE_ENABLED') ?? 'false').toLowerCase() === 'true';
  }

  isReady(): boolean {
    return !this.isRemoteEnabled() || this.rabbitReady;
  }

  async onApplicationBootstrap(): Promise<void> {
    if (!this.isRemoteEnabled()) return;
    try { await this.getClient().connect(); }
    catch { this.rabbitReady = false; }
  }

  async getMine(recipientId: string, query: NotificationQueryDto, requestId?: string) {
    if (!this.isRemoteEnabled()) return this.local.getMine(recipientId, query);
    return this.call(NOTIFICATION_RPC.list, { recipientId, query }, requestId);
  }

  async getUnreadCount(recipientId: string, category?: NotificationCategory, requestId?: string) {
    if (!this.isRemoteEnabled()) return this.local.getUnreadCountByCategory(recipientId, category);
    return this.call(NOTIFICATION_RPC.unreadCount, { recipientId, category }, requestId);
  }

  async markRead(recipientId: string, notificationId: string, requestId?: string) {
    if (!this.isRemoteEnabled()) return this.local.markRead(recipientId, notificationId);
    return this.call(NOTIFICATION_RPC.markRead, { recipientId, notificationId }, requestId);
  }

  async markAllRead(recipientId: string, requestId?: string) {
    if (!this.isRemoteEnabled()) return this.local.markAllRead(recipientId);
    return this.call(NOTIFICATION_RPC.markAllRead, { recipientId }, requestId);
  }

  async publishFollowRequestApproved(event: FollowRequestApprovedEvent): Promise<void> {
    if (!this.isRemoteEnabled()) {
      await this.local.createFollowRequestApprovedNotification({ ...event.payload, eventId: event.eventId });
      return;
    }
    await this.call(event.eventType, event, event.payload.requestId, event.eventId);
  }

  async onModuleDestroy(): Promise<void> {
    if (this.client) await this.client.close();
  }

  private getClient(): ClientProxy {
    if (this.client) return this.client;
    const rabbit = resolveRabbitMqConfig(
      { get: (key) => {
        const value = this.config.get(key);
        return value === undefined || value === null ? undefined : String(value);
      } },
      'api-gateway',
      true,
    );
    this.client = ClientProxyFactory.create({
      transport: Transport.RMQ,
      options: {
        urls: [rabbit.url], socketOptions: rabbit.socketOptions,
        queue: NOTIFICATION_RMQ_QUEUE,
        queueOptions: { durable: true, arguments: { 'x-dead-letter-exchange': '', 'x-dead-letter-routing-key': NOTIFICATION_RMQ_DLQ } },
      },
    });
    this.client.status.subscribe((status) => {
      this.rabbitReady = status === 'connected' || status === 'unblocked';
    });
    return this.client;
  }

  private async call<T>(pattern: string, payload: unknown, requestId?: string, correlationId?: string): Promise<T> {
    requestId ??= randomUUID();
    correlationId ??= randomUUID();
    if (Date.now() < this.circuitOpenUntil) throw new ServiceUnavailableException('Notification service circuit is open');
    const record = new RmqRecordBuilder(payload).setOptions({ messageId: correlationId, persistent: true, headers: { correlationId, requestId } }).build();
    try {
      const response = await firstValueFrom(this.getClient().send<NotificationRpcResponse<T>>(pattern, record).pipe(
        timeout(Number(this.config.get<string>('NOTIFICATION_RPC_TIMEOUT_MS') ?? 3000)),
        retry({ count: Number(this.config.get<string>('NOTIFICATION_RPC_RETRIES') ?? 1), delay: (_error, count) => timer(100 * count) }),
      ));
      this.consecutiveFailures = 0;
      if (response.realtime) {
        const realtime = response.realtime;
        if (realtime.notification) this.gateway.emitCreated(realtime.recipientId, realtime.notification, realtime.unreadCount, realtime.unreadCounts);
        else this.gateway.emitUnreadCount(realtime.recipientId, realtime.unreadCount, realtime.unreadCounts);
      }
      return response.result;
    } catch (error) {
      this.consecutiveFailures += 1;
      const threshold = Number(this.config.get<string>('NOTIFICATION_CIRCUIT_FAILURE_THRESHOLD') ?? 3);
      if (this.consecutiveFailures >= threshold) {
        this.circuitOpenUntil = Date.now() + Number(this.config.get<string>('NOTIFICATION_CIRCUIT_RESET_MS') ?? 15_000);
        this.consecutiveFailures = 0;
      }
      throw new ServiceUnavailableException('Notification service is unavailable', { cause: error });
    }
  }
}
