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 {
  constructor(private readonly unitOfWork: UnitOfWork) {}

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

  async save(event: {
    aggregateType: string;
    aggregateId: string;
    eventType: string;
    payload: any;
    topic?: string;
    key?: string;
  }): Promise<void> {
    const topic = event.topic ?? `contribution.${event.eventType.toLowerCase()}`;
    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 ?? `contribution.${e.eventType.toLowerCase()}`,
        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' },
      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;

    await this.client.outboxEvent.update({
      where: { id },
      data: {
        status: shouldRetry ? 'PENDING' : 'FAILED',
        retryCount,
        lastError: error,
        nextRetryAt: shouldRetry
          ? new Date(Date.now() + Math.pow(2, retryCount) * 1000) // exponential backoff
          : 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,
    };
  }
}
