import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import {
  PaymentAttemptStatus,
  PaymentIntentStatus,
  PaymentRefund,
  PaymentRefundStatus,
  Prisma,
} from "@prisma/client";
import { newId } from "../common/ids/id.util";
import { PrismaService } from "../prisma/prisma.service";
import { ProviderRouterService } from "../providers/provider-router.service";
import { WebhookEventService } from "../webhook-events/webhook-event.service";
import { WEBHOOK_EVENT_TYPES } from "../webhook-events/webhook-events.constants";
import type { CreateRefundDto } from "./dto/create-refund.dto";

const ZERO_DECIMAL_CURRENCIES = new Set(["PYG", "CLP", "JPY", "KRW", "VND"]);

@Injectable()
export class RefundService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly router: ProviderRouterService,
    private readonly webhookEvents: WebhookEventService,
  ) {}

  async create(merchantId: string, dto: CreateRefundDto) {
    const intent = await this.prisma.paymentIntent.findFirst({
      where: { id: dto.intentId, merchantId },
      include: {
        attempts: {
          where: { status: PaymentAttemptStatus.approved },
          orderBy: { approvedAt: "desc" },
          take: 1,
        },
      },
    });
    if (!intent) throw new NotFoundException("Payment intent not found");
    const attempt = intent.attempts[0];
    if (!attempt || !attempt.providerPaymentId) {
      throw new BadRequestException("Intent has no approved attempt to refund");
    }

    const existingRefunds = await this.prisma.paymentRefund.findMany({
      where: {
        attemptId: attempt.id,
        status: { in: [PaymentRefundStatus.approved, PaymentRefundStatus.pending] },
      },
    });
    const alreadyRefunded = existingRefunds.reduce(
      (acc, r) => acc.plus(r.amount),
      new Prisma.Decimal(0),
    );
    const captured = attempt.capturedAmount ?? intent.amount;
    const remaining = new Prisma.Decimal(captured).minus(alreadyRefunded);

    if (remaining.lte(0)) {
      throw new BadRequestException("Intent is already fully refunded");
    }

    const requested = dto.amount
      ? this.toDecimal(dto.amount, intent.currency)
      : remaining;
    if (requested.gt(remaining)) {
      throw new BadRequestException(
        `Requested refund (${requested.toString()}) exceeds remaining (${remaining.toString()})`,
      );
    }

    const route = await this.router.route({
      merchantId,
      country: intent.country,
      currency: intent.currency,
      requestedProvider: attempt.provider,
    });

    const result = await route.adapter.refund({
      providerPaymentId: attempt.providerPaymentId,
      amount: requested.toString(),
      reason: dto.reason ?? null,
    });

    const refund = await this.prisma.paymentRefund.create({
      data: {
        id: newId("paymentRefund"),
        attemptId: attempt.id,
        amount: requested,
        reason: dto.reason ?? null,
        status: this.mapRefundStatus(result.status),
        providerRefundId: result.providerRefundId,
        rawRequestJson: result.rawRequest as Prisma.InputJsonValue,
        rawResponseJson: result.rawResponse as Prisma.InputJsonValue,
      },
    });

    if (refund.status === PaymentRefundStatus.approved) {
      const totalRefunded = alreadyRefunded.plus(requested);
      const fullyRefunded = totalRefunded.gte(captured);
      const nextStatus = fullyRefunded
        ? PaymentIntentStatus.refunded
        : PaymentIntentStatus.partially_refunded;
      const updatedIntent = await this.prisma.paymentIntent.update({
        where: { id: intent.id },
        data: { status: nextStatus },
      });
      await this.webhookEvents.emit({
        merchantId,
        eventType: fullyRefunded
          ? WEBHOOK_EVENT_TYPES.paymentIntentRefunded
          : WEBHOOK_EVENT_TYPES.paymentIntentPartiallyRefunded,
        data: {
          payment_intent_id: updatedIntent.id,
          merchant_id: updatedIntent.merchantId,
          external_reference: updatedIntent.externalReference,
          amount: updatedIntent.amount.toString(),
          refunded_amount: requested.toString(),
          total_refunded: totalRefunded.toString(),
          currency: updatedIntent.currency,
          status: updatedIntent.status,
        },
        paymentIntentId: updatedIntent.id,
      });
    }

    return this.toDto(refund);
  }

  async findOne(merchantId: string, id: string) {
    const refund = await this.prisma.paymentRefund.findFirst({
      where: { id, attempt: { intent: { merchantId } } },
    });
    if (!refund) throw new NotFoundException("Refund not found");
    return this.toDto(refund);
  }

  async list(
    merchantId: string,
    opts: { limit?: number; cursor?: string; intentId?: string } = {},
  ) {
    const take = Math.min(Math.max(opts.limit ?? 20, 1), 100);
    const rows = await this.prisma.paymentRefund.findMany({
      where: {
        attempt: {
          intent: { merchantId, ...(opts.intentId && { id: opts.intentId }) },
        },
      },
      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.toDto(r)),
      nextCursor: hasMore ? data[data.length - 1].id : null,
    };
  }

  private toDecimal(amount: number, currency: string): Prisma.Decimal {
    if (ZERO_DECIMAL_CURRENCIES.has(currency)) return new Prisma.Decimal(amount);
    return new Prisma.Decimal(amount).dividedBy(100);
  }

  private mapRefundStatus(s: "pending" | "approved" | "rejected" | "error"): PaymentRefundStatus {
    switch (s) {
      case "approved":
        return PaymentRefundStatus.approved;
      case "rejected":
        return PaymentRefundStatus.rejected;
      case "error":
        return PaymentRefundStatus.error;
      default:
        return PaymentRefundStatus.pending;
    }
  }

  private toDto(row: PaymentRefund) {
    return {
      id: row.id,
      attemptId: row.attemptId,
      amount: row.amount.toString(),
      reason: row.reason,
      status: row.status,
      providerRefundId: row.providerRefundId,
      createdAt: row.createdAt,
    };
  }
}
