import { Injectable } from '@nestjs/common';
import { Prisma, type Payment as PrismaPayment, type PaymentStatus } from '@prisma/client';
import { PrismaService } from '@modules/shared';
import { PaymentEntity, type PaymentProps } from '../../domain/entities/payment.entity';
import { type IPaymentRepository } from '../../domain/repositories/payment.repository';
import { Money } from '../../domain/value-objects/money.vo';

@Injectable()
export class PrismaPaymentRepository implements IPaymentRepository {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): Promise<PaymentEntity | null> {
    const payment = await this.prisma.payment.findUnique({ where: { id } });
    return payment ? this.toDomain(payment) : null;
  }

  async findByProviderTxId(providerTxId: string): Promise<PaymentEntity | null> {
    const payment = await this.prisma.payment.findFirst({
      where: { providerTxId },
    });
    return payment ? this.toDomain(payment) : null;
  }

  async findByIdempotencyKey(key: string): Promise<PaymentEntity | null> {
    const payment = await this.prisma.payment.findFirst({
      where: { idempotencyKey: key },
    });
    return payment ? this.toDomain(payment) : null;
  }

  async findByUserId(
    userId: string,
    options?: { status?: PaymentStatus; limit?: number; offset?: number },
  ): Promise<{ items: PaymentEntity[]; total: number }> {
    const where = {
      userId,
      ...(options?.status ? { status: options.status } : {}),
    };

    const [payments, total] = await Promise.all([
      this.prisma.payment.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        take: options?.limit ?? 20,
        skip: options?.offset ?? 0,
      }),
      this.prisma.payment.count({ where }),
    ]);

    return {
      items: payments.map((p) => this.toDomain(p)),
      total,
    };
  }

  async save(entity: PaymentEntity): Promise<void> {
    await this.prisma.payment.create({
      data: {
        id: entity.id,
        userId: entity.userId,
        transactionId: entity.transactionId,
        provider: entity.provider,
        type: entity.type,
        amountVND: entity.amount.value,
        status: entity.status,
        providerTxId: entity.providerTxId,
        callbackData: entity.callbackData as Prisma.InputJsonValue,
        idempotencyKey: entity.idempotencyKey,
      },
    });
  }

  async update(entity: PaymentEntity): Promise<void> {
    await this.prisma.payment.update({
      where: { id: entity.id },
      data: {
        status: entity.status,
        providerTxId: entity.providerTxId,
        callbackData: entity.callbackData as Prisma.InputJsonValue,
      },
    });
  }

  async updateIfStatus(
    id: string,
    expectedStatuses: PaymentStatus[],
    data: { status: PaymentStatus; providerTxId?: string; callbackData?: unknown },
  ): Promise<PaymentEntity | null> {
    try {
      const updated = await this.prisma.payment.update({
        where: {
          id,
          status: { in: expectedStatuses },
        },
        data: {
          status: data.status,
          ...(data.providerTxId !== undefined ? { providerTxId: data.providerTxId } : {}),
          ...(data.callbackData !== undefined ? { callbackData: data.callbackData as Prisma.InputJsonValue } : {}),
        },
      });
      return this.toDomain(updated);
    } catch (error) {
      // P2025: Record not found (status didn't match or ID doesn't exist)
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
        return null;
      }
      throw error;
    }
  }

  private toDomain(raw: PrismaPayment): PaymentEntity {
    const amount = Money.create(raw.amountVND).unwrap();

    const props: PaymentProps = {
      userId: raw.userId,
      transactionId: raw.transactionId,
      provider: raw.provider,
      type: raw.type,
      amount,
      status: raw.status,
      providerTxId: raw.providerTxId,
      callbackData: raw.callbackData,
      idempotencyKey: raw.idempotencyKey ?? null,
    };

    return new PaymentEntity(raw.id, props, raw.createdAt, raw.updatedAt);
  }
}
