import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@prisma/client';

export enum OutboxStatus {
  PENDING = 'PENDING',
  SENT = 'SENT',
  CONFIRMED = 'CONFIRMED',
  FAILED = 'FAILED',
}

export interface OutboxEventData {
  eventType: string;
  topic: string;
  key: string;
  payload: Record<string, unknown>;
  aggregateId: string;
  aggregateType: string;
}

export interface OutboxEvent extends OutboxEventData {
  id: bigint;
  status: OutboxStatus;
  retryCount: number;
  maxRetries: number;
  lastError: string | null;
  createdAt: Date;
  publishedAt: Date | null;
  nextRetryAt: Date | null;
}

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

  constructor(private readonly prisma: PrismaService) {}

  async saveInTransaction(
    tx: Prisma.TransactionClient,
    events: OutboxEventData[],
  ): Promise<void> {
    if (events.length === 0) return;

    await tx.outboxEvent.createMany({
      data: events.map((event) => ({
        eventType: event.eventType,
        topic: event.topic,
        key: event.key,
        payload: event.payload as Prisma.JsonObject,
        aggregateId: event.aggregateId,
        aggregateType: event.aggregateType,
        status: OutboxStatus.PENDING,
      })),
    });
  }

  async saveEvents(events: OutboxEventData[]): Promise<void> {
    if (events.length === 0) return;

    await this.prisma.outboxEvent.createMany({
      data: events.map((event) => ({
        eventType: event.eventType,
        topic: event.topic,
        key: event.key,
        payload: event.payload as Prisma.JsonObject,
        aggregateId: event.aggregateId,
        aggregateType: event.aggregateType,
        status: OutboxStatus.PENDING,
      })),
    });
  }

  async findPendingEvents(limit: number = 100): Promise<OutboxEvent[]> {
    const events = await this.prisma.outboxEvent.findMany({
      where: { status: OutboxStatus.PENDING },
      orderBy: { createdAt: 'asc' },
      take: limit,
    });
    return events.map((e) => this.mapToOutboxEvent(e));
  }

  async findEventsForRetry(limit: number = 50): Promise<OutboxEvent[]> {
    const now = new Date();
    const events = await this.prisma.outboxEvent.findMany({
      where: {
        status: OutboxStatus.FAILED,
        retryCount: { lt: 5 },
        nextRetryAt: { lte: now },
      },
      orderBy: { nextRetryAt: 'asc' },
      take: limit,
    });
    return events.map((e) => this.mapToOutboxEvent(e));
  }

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

  async markAsConfirmed(eventId: string, eventType?: string): Promise<boolean> {
    const whereClause: Prisma.OutboxEventWhereInput = {
      aggregateId: eventId,
      status: OutboxStatus.SENT,
    };
    if (eventType) {
      whereClause.eventType = eventType;
    }

    const result = await this.prisma.outboxEvent.updateMany({
      where: whereClause,
      data: { status: OutboxStatus.CONFIRMED },
    });

    if (result.count > 0) {
      this.logger.log(`[OUTBOX] Event ${eventId} (${eventType || 'all types'}) confirmed`);
      return true;
    }
    return false;
  }

  async markAsConfirmedById(id: bigint): Promise<void> {
    await this.prisma.outboxEvent.update({
      where: { id },
      data: { status: OutboxStatus.CONFIRMED },
    });
  }

  async findSentEventsTimedOut(timeoutMinutes: number = 5, limit: number = 50): Promise<OutboxEvent[]> {
    const cutoffTime = new Date();
    cutoffTime.setMinutes(cutoffTime.getMinutes() - timeoutMinutes);

    const events = await this.prisma.outboxEvent.findMany({
      where: {
        status: OutboxStatus.SENT,
        publishedAt: { lt: cutoffTime },
        retryCount: { lt: 5 },
      },
      orderBy: { publishedAt: 'asc' },
      take: limit,
    });
    return events.map((e) => this.mapToOutboxEvent(e));
  }

  async resetSentToPending(id: bigint): Promise<void> {
    const event = await this.prisma.outboxEvent.findUnique({ where: { id } });
    if (!event) return;

    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxStatus.PENDING,
        retryCount: event.retryCount + 1,
        lastError: 'Consumer confirmation timeout',
      },
    });
  }

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

    const newRetryCount = event.retryCount + 1;
    const delayMinutes = Math.pow(2, newRetryCount - 1);
    const nextRetryAt = new Date();
    nextRetryAt.setMinutes(nextRetryAt.getMinutes() + delayMinutes);

    await this.prisma.outboxEvent.update({
      where: { id },
      data: {
        status: OutboxStatus.FAILED,
        retryCount: newRetryCount,
        lastError: error,
        nextRetryAt: newRetryCount >= event.maxRetries ? null : nextRetryAt,
      },
    });
  }

  async cleanupOldEvents(retentionDays: number = 7): Promise<number> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - retentionDays);

    const result = await this.prisma.outboxEvent.deleteMany({
      where: {
        status: OutboxStatus.CONFIRMED,
        publishedAt: { lt: cutoffDate },
      },
    });
    return result.count;
  }

  async getStats(): Promise<{
    pending: number;
    sent: number;
    confirmed: number;
    failed: number;
  }> {
    const [pending, sent, confirmed, failed] = await Promise.all([
      this.prisma.outboxEvent.count({ where: { status: OutboxStatus.PENDING } }),
      this.prisma.outboxEvent.count({ where: { status: OutboxStatus.SENT } }),
      this.prisma.outboxEvent.count({ where: { status: OutboxStatus.CONFIRMED } }),
      this.prisma.outboxEvent.count({ where: { status: OutboxStatus.FAILED } }),
    ]);
    return { pending, sent, confirmed, failed };
  }

  private mapToOutboxEvent(record: any): OutboxEvent {
    return {
      id: record.id,
      eventType: record.eventType,
      topic: record.topic,
      key: record.key,
      payload: record.payload as Record<string, unknown>,
      aggregateId: record.aggregateId,
      aggregateType: record.aggregateType,
      status: record.status as OutboxStatus,
      retryCount: record.retryCount,
      maxRetries: record.maxRetries,
      lastError: record.lastError,
      createdAt: record.createdAt,
      publishedAt: record.publishedAt,
      nextRetryAt: record.nextRetryAt,
    };
  }
}
