import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { randomUUID } from 'crypto';
import { ClientSession, Model } from 'mongoose';
import { AppLoggerService } from '../../infrastructure/logging/app-logger.service';
import { AppQueueService } from '../../infrastructure/queue/app-queue.service';
import { NotificationsService } from '../notifications/notifications.service';
import { OutboxEvent, OutboxEventDocument } from './schemas/outbox-event.schema';

@Injectable()
export class OutboxService implements OnModuleInit {
  private static readonly PROCESS_EVENT_JOB = 'process_outbox_event';

  constructor(
    @InjectModel(OutboxEvent.name) private readonly outboxEventModel: Model<OutboxEventDocument>,
    private readonly notificationsService: NotificationsService,
    private readonly queueService: AppQueueService,
    private readonly logger: AppLoggerService,
  ) {}

  onModuleInit(): void {
    this.queueService.registerProcessor(OutboxService.PROCESS_EVENT_JOB, async (payload) => {
      await this.processEvent(String(payload.eventId ?? ''));
    });
  }

  async enqueueFollowNotification(actorId: string, recipientId: string, referenceId?: string): Promise<void> {
    const event = await this.outboxEventModel.create({
      eventId: randomUUID(),
      eventType: 'follow_notification',
      payload: { actorId, recipientId, referenceId: referenceId ?? '' },
      status: 'pending',
    });
    await this.enqueuePersistedEvent(event.eventId);
  }

  async enqueueFollowRequestApprovedNotification(
    actorId: string,
    recipientId: string,
    requestId: string,
  ): Promise<void> {
    const eventId = await this.createFollowRequestApprovedEvent(actorId, recipientId, requestId);
    await this.enqueuePersistedEvent(eventId);
  }

  async createFollowRequestApprovedEvent(
    actorId: string,
    recipientId: string,
    requestId: string,
    session?: ClientSession,
  ): Promise<string> {
    const eventId = `follow.request.approved:${String(requestId)}`;
    await this.outboxEventModel.updateOne(
      { eventId },
      { $setOnInsert: {
        eventId,
        eventType: 'follow_request_approved_notification',
        payload: { actorId: String(actorId), recipientId: String(recipientId), requestId: String(requestId) },
        correlationId: eventId,
        requestId: String(requestId),
        status: 'pending',
        attempts: 0,
        nextRetryAt: new Date(),
      } },
      { upsert: true, session },
    ).exec();
    return eventId;
  }

  async enqueuePersistedEvent(eventId: string): Promise<void> {
    await this.queueService.enqueue(OutboxService.PROCESS_EVENT_JOB, { eventId });
  }

  async listByStatuses(statuses: OutboxEvent['status'][], limit = 100): Promise<OutboxEventDocument[]> {
    return this.outboxEventModel.find({ status: { $in: statuses } }).sort({ createdAt: 1 }).limit(limit).exec();
  }

  async retryEvent(eventId: string): Promise<boolean> {
    const result = await this.outboxEventModel.updateOne(
      { eventId, status: { $in: ['failed', 'dead'] } },
      { $set: { status: 'pending', nextRetryAt: new Date(), lastError: '', lockedAt: null, lockedBy: null }, $unset: { deadAt: 1 } },
    ).exec();
    return result.modifiedCount === 1;
  }

  async processEvent(eventId: string): Promise<void> {
    const event = await this.outboxEventModel.findOne({
      $or: [{ eventId }, ...(eventId.match(/^[a-f\d]{24}$/i) ? [{ _id: eventId }] : [])],
    }).exec();
    if (!event || event.status === 'processed') return;
    // A legacy BullMQ job may still exist during cutover. The dispatcher is the sole
    // owner of approval events whenever remote notification routing is enabled.
    if (event.eventType === 'follow_request_approved_notification'
      && process.env.NOTIFICATION_SERVICE_ENABLED === 'true') return;

    try {
      if (event.eventType === 'follow_notification') {
        await this.notificationsService.createFollowNotification(
          String(event.payload.actorId ?? ''),
          String(event.payload.recipientId ?? ''),
          String(event.payload.referenceId ?? ''),
        );
      } else if (event.eventType === 'follow_request_approved_notification') {
        await this.notificationsService.createFollowRequestApprovedNotification({
          actorId: String(event.payload.actorId ?? ''),
          recipientId: String(event.payload.recipientId ?? ''),
          requestId: String(event.payload.requestId ?? ''),
          eventId: event.eventId || event.id,
        });
      }
      event.status = 'processed';
      event.processedAt = new Date();
      event.lastError = '';
    } catch (error) {
      event.status = 'failed';
      event.lastError = error instanceof Error ? error.message : 'unknown outbox error';
      this.logger.warn({ eventId: event.eventId || event.id, eventType: event.eventType, error: event.lastError }, OutboxService.name);
      throw error;
    } finally {
      event.attempts += 1;
      await event.save();
    }
  }
}
