import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import {
  IOutboxEventRepository,
  OutboxEvent,
  OutboxEventData,
  OutboxEventStatus,
} from '@/domain/repositories/outbox-event.repository.interface';

@Injectable()
export class OutboxEventRepositoryImpl implements IOutboxEventRepository {
  private readonly logger = new Logger(OutboxEventRepositoryImpl.name);

  constructor(private readonly prisma: PrismaService) {}

  async create(data: OutboxEventData): Promise<OutboxEvent> {
    const record = await this.prisma.outboxEvent.create({
      data: {
        eventType: data.eventType,
        aggregateId: data.aggregateId,
        aggregateType: data.aggregateType,
        payload: data.payload as Prisma.InputJsonValue,
        status: OutboxEventStatus.PENDING,
        retryCount: 0,
        maxRetries: 10,
      },
    });
    return this.mapToOutboxEvent(record);
  }

  async createMany(data: OutboxEventData[]): Promise<void> {
    await this.prisma.outboxEvent.createMany({
      data: data.map((d) => ({
        eventType: d.eventType,
        aggregateId: d.aggregateId,
        aggregateType: d.aggregateType,
        payload: d.payload as Prisma.InputJsonValue,
        status: OutboxEventStatus.PENDING,
        retryCount: 0,
        maxRetries: 10,
      })),
    });
  }

  async findById(id: bigint): Promise<OutboxEvent | null> {
    const record = await this.prisma.outboxEvent.findUnique({
      where: { id },
    });
    return record ? this.mapToOutboxEvent(record) : null;
  }

  async findPendingEvents(limit: number = 100): Promise<OutboxEvent[]> {
    const now = new Date();
    const records = await this.prisma.outboxEvent.findMany({
      where: {
        status: OutboxEventStatus.PENDING,
        OR: [
          { nextRetryAt: null },
          { nextRetryAt: { lte: now } },
        ],
      },
      orderBy: { createdAt: 'asc' },
      take: limit,
    });
    return records.map((r) => this.mapToOutboxEvent(r));
  }

  async findUnackedEvents(timeoutSeconds: number, limit: number = 100): Promise<OutboxEvent[]> {
    const cutoff = new Date(Date.now() - timeoutSeconds * 1000);
    const records = await this.prisma.outboxEvent.findMany({
      where: {
        status: OutboxEventStatus.SENT,
        sentAt: { lte: cutoff },
      },
      orderBy: { sentAt: 'asc' },
      take: limit,
    });
    return records.map((r) => this.mapToOutboxEvent(r));
  }

  async markAsSent(id: bigint): Promise<void> {
    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxEventStatus.SENT,
        sentAt: new Date(),
      },
    });
  }

  async markAsAcked(id: bigint): Promise<void> {
    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxEventStatus.ACKED,
        ackedAt: new Date(),
      },
    });
  }

  async markAsAckedByAggregateId(
    aggregateType: string,
    aggregateId: string,
    eventType: string,
  ): Promise<void> {
    const result = await this.prisma.outboxEvent.updateMany({
      where: {
        aggregateType,
        aggregateId,
        eventType,
        status: OutboxEventStatus.SENT,
      },
      data: {
        status: OutboxEventStatus.ACKED,
        ackedAt: new Date(),
      },
    });
    this.logger.debug(
      `Marked ${result.count} events as ACKED for ${aggregateType}:${aggregateId}:${eventType}`,
    );
  }

  async recordFailure(id: bigint, error: string): Promise<void> {
    const event = await this.prisma.outboxEvent.findUnique({
      where: { id },
    });

    if (!event) return;

    const newRetryCount = event.retryCount + 1;
    // 指数退避: 2^retryCount 秒，最大 5 分钟
    const backoffSeconds = Math.min(Math.pow(2, newRetryCount), 300);
    const nextRetryAt = new Date(Date.now() + backoffSeconds * 1000);

    if (newRetryCount >= event.maxRetries) {
      await this.markAsFailed(id, error);
    } else {
      await this.prisma.outboxEvent.update({
        where: { id },
        data: {
          status: OutboxEventStatus.PENDING,
          retryCount: newRetryCount,
          lastError: error,
          nextRetryAt,
        },
      });
    }
  }

  async markAsFailed(id: bigint, error: string): Promise<void> {
    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxEventStatus.FAILED,
        lastError: error,
      },
    });
    this.logger.warn(`Event ${id} marked as FAILED: ${error}`);
  }

  async resetToPending(id: bigint): Promise<void> {
    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxEventStatus.PENDING,
        retryCount: 0,
        lastError: null,
        nextRetryAt: null,
        sentAt: null,
        ackedAt: null,
      },
    });
  }

  async cleanupAckedEvents(olderThanDays: number): Promise<number> {
    const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
    const result = await this.prisma.outboxEvent.deleteMany({
      where: {
        status: OutboxEventStatus.ACKED,
        ackedAt: { lte: cutoff },
      },
    });
    this.logger.log(`Cleaned up ${result.count} old ACKED events`);
    return result.count;
  }

  private mapToOutboxEvent(record: {
    id: bigint;
    eventType: string;
    aggregateId: string;
    aggregateType: string;
    payload: unknown;
    status: string;
    retryCount: number;
    maxRetries: number;
    lastError: string | null;
    nextRetryAt: Date | null;
    createdAt: Date;
    sentAt: Date | null;
    ackedAt: Date | null;
  }): OutboxEvent {
    return {
      id: record.id,
      eventType: record.eventType,
      aggregateId: record.aggregateId,
      aggregateType: record.aggregateType,
      payload: record.payload as Record<string, unknown>,
      status: record.status as OutboxEventStatus,
      retryCount: record.retryCount,
      maxRetries: record.maxRetries,
      lastError: record.lastError,
      nextRetryAt: record.nextRetryAt,
      createdAt: record.createdAt,
      sentAt: record.sentAt,
      ackedAt: record.ackedAt,
    };
  }
}
