import { InjectQueue } from "@nestjs/bullmq";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import {
  Provider,
  WebhookEventIn,
  WebhookEventOut,
  WebhookEventOutStatus,
} from "@prisma/client";
import { Queue } from "bullmq";
import { PrismaService } from "../prisma/prisma.service";
import { WEBHOOK_DELIVERY_QUEUE } from "../webhook-events/webhook-events.constants";

interface ListOutOpts {
  merchantId: string;
  limit?: number;
  cursor?: string;
  status?: WebhookEventOutStatus;
  endpointId?: string;
}

interface ListInOpts {
  merchantId: string;
  limit?: number;
  cursor?: string;
  provider?: Provider;
  processed?: boolean;
}

@Injectable()
export class AdminEventsService {
  constructor(
    private readonly prisma: PrismaService,
    @InjectQueue(WEBHOOK_DELIVERY_QUEUE) private readonly queue: Queue,
  ) {}

  async listOutbound(opts: ListOutOpts) {
    const take = Math.min(Math.max(opts.limit ?? 20, 1), 100);
    const rows = await this.prisma.webhookEventOut.findMany({
      where: {
        endpoint: { merchantId: opts.merchantId },
        ...(opts.status && { status: opts.status }),
        ...(opts.endpointId && { endpointId: opts.endpointId }),
      },
      take: take + 1,
      ...(opts.cursor ? { cursor: { id: opts.cursor }, skip: 1 } : {}),
      orderBy: { createdAt: "desc" },
    });
    const hasMore = rows.length > take;
    const data = hasMore ? rows.slice(0, take) : rows;
    return {
      data: data.map((r) => this.toOutDto(r)),
      nextCursor: hasMore ? data[data.length - 1].id : null,
    };
  }

  async listInbound(opts: ListInOpts) {
    const take = Math.min(Math.max(opts.limit ?? 20, 1), 100);
    const rows = await this.prisma.webhookEventIn.findMany({
      where: {
        ...(opts.provider && { provider: opts.provider }),
        ...(opts.processed !== undefined && { processed: opts.processed }),
        attempt: { intent: { merchantId: opts.merchantId } },
      },
      take: take + 1,
      ...(opts.cursor ? { cursor: { id: opts.cursor }, skip: 1 } : {}),
      orderBy: { receivedAt: "desc" },
    });
    const hasMore = rows.length > take;
    const data = hasMore ? rows.slice(0, take) : rows;
    return {
      data: data.map((r) => this.toInDto(r)),
      nextCursor: hasMore ? data[data.length - 1].id : null,
    };
  }

  async retryOutbound(merchantId: string, eventId: string) {
    const event = await this.prisma.webhookEventOut.findFirst({
      where: { id: eventId, endpoint: { merchantId } },
    });
    if (!event) throw new NotFoundException("Event not found");
    if (event.status === WebhookEventOutStatus.delivered) {
      throw new BadRequestException("Event already delivered");
    }

    await this.prisma.webhookEventOut.update({
      where: { id: event.id },
      data: { status: WebhookEventOutStatus.pending, nextAttemptAt: new Date() },
    });
    await this.queue.add(
      "deliver",
      { eventOutId: event.id },
      { jobId: `${event.id}:manual:${Date.now()}`, removeOnComplete: true, removeOnFail: 1000 },
    );
    return { queued: true, id: event.id };
  }

  private toOutDto(row: WebhookEventOut) {
    return {
      id: row.id,
      endpointId: row.endpointId,
      intentId: row.intentId,
      eventType: row.eventType,
      status: row.status,
      attempts: row.attempts,
      lastAttemptAt: row.lastAttemptAt,
      nextAttemptAt: row.nextAttemptAt,
      lastError: row.lastError,
      deliveredAt: row.deliveredAt,
      payload: row.payloadJson as Record<string, unknown>,
      createdAt: row.createdAt,
    };
  }

  private toInDto(row: WebhookEventIn) {
    return {
      id: row.id,
      provider: row.provider,
      attemptId: row.attemptId,
      signature: row.signature,
      processed: row.processed,
      processedAt: row.processedAt,
      error: row.error,
      payload: row.payloadJson as Record<string, unknown>,
      receivedAt: row.receivedAt,
    };
  }
}
