import { Injectable } from '@nestjs/common';
import { UnitOfWork, TransactionClient } from '../unit-of-work/unit-of-work';

export interface OutboxEvent {
  id: bigint;
  aggregateType: string;
  aggregateId: string;
  eventType: string;
  topic: string;
  key: string;
  payload: any;
  status: string;
  retryCount: number;
  maxRetries: number;
  lastError: string | null;
  createdAt: Date;
  publishedAt: Date | null;
  nextRetryAt: Date | null;
}

/**
 * Outbox Pattern 仓库
 * 用于保证事件发布的可靠性
 */
@Injectable()
export class OutboxRepository {
  /**
   * 退避时间配置（毫秒）
   * 第1次: 30s, 第2次: 1min, 第3次: 2min, 第4次: 5min,
   * 第5次: 10min, 第6次: 30min, 第7次: 1h, 第8次: 2h, 第9次: 4h, 第10次: 4h
   */
  private readonly BACKOFF_INTERVALS = [
    30_000, // 30 seconds
    60_000, // 1 minute
    120_000, // 2 minutes
    300_000, // 5 minutes
    600_000, // 10 minutes
    1_800_000, // 30 minutes
    3_600_000, // 1 hour
    7_200_000, // 2 hours
    14_400_000, // 4 hours (max)
    14_400_000, // 4 hours (max)
  ];

  constructor(private readonly unitOfWork: UnitOfWork) {}

  private get client(): TransactionClient {
    return this.unitOfWork.getClient();
  }

  /**
   * 根据事件类型构建默认的 Kafka topic 名称
   * 例如: CONTRIBUTION_DISTRIBUTED -> contribution.distributed
   */
  private buildDefaultTopic(eventType: string): string {
    return 'contribution.' + eventType.toLowerCase().replace(/_/g, '.');
  }

  async save(event: {
    aggregateType: string;
    aggregateId: string;
    eventType: string;
    payload: any;
    topic?: string;
    key?: string;
  }): Promise<void> {
    const topic = event.topic ?? this.buildDefaultTopic(event.eventType);
    const key = event.key ?? event.aggregateId;

    await this.client.outboxEvent.create({
      data: {
        aggregateType: event.aggregateType,
        aggregateId: event.aggregateId,
        eventType: event.eventType,
        topic,
        key,
        payload: event.payload,
        status: 'PENDING',
      },
    });
  }

  async saveMany(events: {
    aggregateType: string;
    aggregateId: string;
    eventType: string;
    payload: any;
    topic?: string;
    key?: string;
  }[]): Promise<void> {
    if (events.length === 0) return;

    await this.client.outboxEvent.createMany({
      data: events.map((e) => ({
        aggregateType: e.aggregateType,
        aggregateId: e.aggregateId,
        eventType: e.eventType,
        topic: e.topic ?? this.buildDefaultTopic(e.eventType),
        key: e.key ?? e.aggregateId,
        payload: e.payload,
        status: 'PENDING',
      })),
    });
  }

  async findUnprocessed(limit: number): Promise<OutboxEvent[]> {
    const records = await this.client.outboxEvent.findMany({
      where: {
        status: 'PENDING',
        OR: [{ nextRetryAt: null }, { nextRetryAt: { lte: new Date() } }],
      },
      orderBy: { createdAt: 'asc' },
      take: limit,
    });

    return records.map((r) => this.toDomain(r));
  }

  async markAsProcessed(ids: bigint[]): Promise<void> {
    await this.client.outboxEvent.updateMany({
      where: { id: { in: ids } },
      data: { status: 'PUBLISHED', publishedAt: new Date() },
    });
  }

  async markAsFailed(id: bigint, error: string): Promise<void> {
    const event = await this.client.outboxEvent.findUnique({ where: { id } });
    if (!event) return;

    const retryCount = event.retryCount + 1;
    const shouldRetry = retryCount < event.maxRetries;

    // 使用预定义的退避时间表
    const backoffIndex = Math.min(
      retryCount - 1,
      this.BACKOFF_INTERVALS.length - 1,
    );
    const delayMs = this.BACKOFF_INTERVALS[backoffIndex];

    await this.client.outboxEvent.update({
      where: { id },
      data: {
        status: shouldRetry ? 'PENDING' : 'FAILED',
        retryCount,
        lastError: error,
        nextRetryAt: shouldRetry ? new Date(Date.now() + delayMs) : null,
      },
    });
  }

  async deleteProcessed(beforeDate: Date): Promise<number> {
    const result = await this.client.outboxEvent.deleteMany({
      where: {
        status: 'PUBLISHED',
        publishedAt: { lt: beforeDate },
      },
    });

    return result.count;
  }

  private toDomain(record: any): OutboxEvent {
    return {
      id: record.id,
      aggregateType: record.aggregateType,
      aggregateId: record.aggregateId,
      eventType: record.eventType,
      topic: record.topic,
      key: record.key,
      payload: record.payload,
      status: record.status,
      retryCount: record.retryCount,
      maxRetries: record.maxRetries,
      lastError: record.lastError,
      createdAt: record.createdAt,
      publishedAt: record.publishedAt,
      nextRetryAt: record.nextRetryAt,
    };
  }
}
