import { withLimitedRetry } from '@app/common';
import {
  MessageContext,
  NOTIFICATION_RPC,
  NotificationListRequest,
  NotificationMarkAllReadRequest,
  NotificationMarkReadRequest,
  NotificationRpcResponse,
  NotificationUnreadCountRequest,
} from '@app/contracts';
import {
  FOLLOW_REQUEST_APPROVED_EVENT,
  FollowRequestApprovedEvent,
  isFollowRequestApprovedEvent,
} from '@app/events';
import { Controller } from '@nestjs/common';
import { Ctx, MessagePattern, Payload, RmqContext, RpcException } from '@nestjs/microservices';
import { NotificationCategory, NotificationQueryDto } from '../../../src/modules/notifications/dto/notification-query.dto';
import { NotificationsService } from '../../../src/modules/notifications/notifications.service';

@Controller()
export class NotificationConsumer {
  constructor(private readonly notifications: NotificationsService) {}

  @MessagePattern(NOTIFICATION_RPC.list)
  async list(@Payload() input: NotificationListRequest, @Ctx() context: RmqContext) {
    return this.rpc(context, () => this.notifications.getMine(
      input.recipientId,
      input.query as unknown as NotificationQueryDto,
    ));
  }

  @MessagePattern(NOTIFICATION_RPC.unreadCount)
  async unreadCount(@Payload() input: NotificationUnreadCountRequest, @Ctx() context: RmqContext) {
    return this.rpc(context, () => this.notifications.getUnreadCountByCategory(
      input.recipientId,
      input.category as NotificationCategory | undefined,
    ));
  }

  @MessagePattern(NOTIFICATION_RPC.markRead)
  async markRead(@Payload() input: NotificationMarkReadRequest, @Ctx() context: RmqContext) {
    return this.rpc(context, async () => {
      const result = await this.notifications.markRead(input.recipientId, input.notificationId);
      const unreadCounts = await this.notifications.getUnreadCounts(input.recipientId);
      return { result, realtime: {
        recipientId: input.recipientId,
        unreadCount: unreadCounts.total,
        unreadCounts,
      } };
    }, true);
  }

  @MessagePattern(NOTIFICATION_RPC.markAllRead)
  async markAllRead(@Payload() input: NotificationMarkAllReadRequest, @Ctx() context: RmqContext) {
    return this.rpc(context, async () => {
      const result = await this.notifications.markAllRead(input.recipientId);
      const unreadCounts = await this.notifications.getUnreadCounts(input.recipientId);
      return { result, realtime: {
        recipientId: input.recipientId,
        unreadCount: unreadCounts.total,
        unreadCounts,
      } };
    }, true);
  }

  @MessagePattern(FOLLOW_REQUEST_APPROVED_EVENT)
  async followRequestApproved(
    @Payload() event: FollowRequestApprovedEvent,
    @Ctx() context: RmqContext,
  ) {
    if (!isFollowRequestApprovedEvent(event)) {
      return this.reject(context, new Error('Invalid follow.request.approved contract'));
    }

    const metadata = this.metadata(context);
    try {
      const notification = await withLimitedRetry(
        () => this.notifications.createFollowRequestApprovedNotification({
          ...event.payload,
          eventId: event.eventId,
        }),
        3,
        25,
      );
      const unreadCounts = await this.notifications.getUnreadCounts(event.payload.recipientId);
      this.ack(context);
      this.log('info', 'follow.request.approved consumed', metadata, { eventId: event.eventId });
      return {
        result: notification,
        realtime: {
          recipientId: event.payload.recipientId,
          notification,
          unreadCount: unreadCounts.total,
          unreadCounts,
        },
      };
    } catch (error) {
      return this.reject(context, error, metadata, event.eventId);
    }
  }

  private async rpc<T>(
    context: RmqContext,
    operation: () => Promise<T | NotificationRpcResponse<T>>,
    alreadyWrapped = false,
  ): Promise<NotificationRpcResponse<T>> {
    try {
      const value = await operation();
      this.ack(context);
      return (alreadyWrapped ? value : { result: value }) as NotificationRpcResponse<T>;
    } catch (error) {
      this.ack(context);
      throw new RpcException(error instanceof Error ? error.message : 'Notification operation failed');
    }
  }

  private ack(context: RmqContext): void {
    context.getChannelRef().ack(context.getMessage());
  }

  private reject(
    context: RmqContext,
    error: unknown,
    metadata = this.metadata(context),
    eventId?: string,
  ): never {
    context.getChannelRef().nack(context.getMessage(), false, false);
    const message = error instanceof Error ? error.message : 'Notification event failed';
    this.log('error', message, metadata, { eventId });
    throw new RpcException(message);
  }

  private metadata(context: RmqContext): MessageContext {
    const message = context.getMessage() as { properties?: { headers?: Record<string, unknown>; correlationId?: string } };
    const headers = message.properties?.headers ?? {};
    return {
      correlationId: String(headers.correlationId ?? message.properties?.correlationId ?? ''),
      requestId: String(headers.requestId ?? ''),
    };
  }

  private log(
    level: 'info' | 'error',
    message: string,
    context: MessageContext,
    extra: Record<string, unknown>,
  ): void {
    const entry = JSON.stringify({
      timestamp: new Date().toISOString(), level, service: 'notification-service', message, ...context, ...extra,
    });
    if (level === 'error') console.error(entry);
    else console.log(entry);
  }
}
