import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import { bookingRepo } from "@/server/repositories/booking.repo";
import { paymentRepo } from "@/server/repositories/payment.repo";
import { isTripDepartureDayPast } from "@/lib/trip-dates";

const SERIAL_TX_ATTEMPTS = 6;

function isSerializationConflict(err: unknown): boolean {
  return (
    typeof err === "object" &&
    err !== null &&
    "code" in err &&
    (err as { code: string }).code === "P2034"
  );
}

function manualOrderId(bookingId: string): string {
  return `manual-${bookingId}`;
}

export const bookingService = {
  async getByParticipantId(participantId: string) {
    return bookingRepo.findByParticipantId(participantId);
  },

  async getByTripAndUser(tripId: string, userId: string) {
    return bookingRepo.findByTripAndUser(tripId, userId);
  },

  /**
   * Peserta tandai sudah transfer manual. Idempotent: kalau sudah ada Payment
   * MANUAL aktif, biarkan; kalau Booking sudah PAID, tolak.
   */
  async markPaidManual(bookingId: string, userId: string) {
    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        return await prisma.$transaction(
          async (tx) => {
            const booking = await tx.booking.findUnique({
              where: { id: bookingId },
              include: { trip: { select: { price: true, date: true } } },
            });
            if (!booking) {
              throw new Error("Booking tidak ditemukan");
            }
            if (booking.userId !== userId) {
              throw new Error("Booking ini bukan milikmu");
            }
            if (booking.amount <= 0) {
              throw new Error(
                "Booking ini tidak butuh pembayaran (gratis)"
              );
            }
            if (booking.status === "PAID") {
              throw new Error("Pembayaran sudah dikonfirmasi");
            }
            if (booking.status !== "AWAITING_PAY") {
              throw new Error(
                "Booking belum siap menerima pembayaran (tunggu approve organizer)"
              );
            }
            if (isTripDepartureDayPast(booking.trip.date)) {
              throw new Error(
                "Trip sudah lewat tanggal berangkat — pembayaran ditutup"
              );
            }

            const existing = await tx.payment.findFirst({
              where: {
                bookingId,
                provider: "MANUAL",
                status: { in: ["PENDING", "AWAITING"] },
              },
              orderBy: { createdAt: "desc" },
            });

            if (existing && existing.status === "AWAITING") {
              return existing;
            }

            const payment = existing
              ? await tx.payment.update({
                  where: { id: existing.id },
                  data: { status: "AWAITING" },
                })
              : await tx.payment.create({
                  data: {
                    bookingId,
                    provider: "MANUAL",
                    externalOrderId: manualOrderId(bookingId),
                    amount: booking.amount,
                    status: "AWAITING",
                  },
                });

            // Backward-compat: tetap update timestamp di TripParticipant
            // selama UI lama masih membaca kolom ini.
            await tx.tripParticipant.update({
              where: { id: booking.participantId },
              data: { markedPaidAt: new Date() },
            });

            return payment;
          },
          {
            isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
            maxWait: 5000,
            timeout: 15000,
          }
        );
      } catch (e) {
        lastErr = e;
        if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
          continue;
        }
        throw e;
      }
    }
    throw lastErr instanceof Error
      ? lastErr
      : new Error("Gagal menandai pembayaran. Coba lagi sebentar.");
  },

  /**
   * Organizer konfirmasi pembayaran manual masuk.
   * Idempotent: kalau sudah PAID, tolak (UI lama bisa muncul tombol dua kali).
   */
  async confirmPaidManual(bookingId: string, organizerId: string) {
    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        return await prisma.$transaction(
          async (tx) => {
            const booking = await tx.booking.findUnique({
              where: { id: bookingId },
              include: { trip: { select: { organizerId: true, price: true } } },
            });
            if (!booking) {
              throw new Error("Booking tidak ditemukan");
            }
            if (booking.trip.organizerId !== organizerId) {
              throw new Error(
                "Hanya organizer trip ini yang bisa mengonfirmasi pembayaran"
              );
            }
            if (booking.amount <= 0) {
              throw new Error(
                "Booking ini gratis — tidak ada pembayaran yang perlu dikonfirmasi"
              );
            }
            if (booking.status === "PAID") {
              throw new Error("Pembayaran sudah dikonfirmasi sebelumnya");
            }

            const awaitingPayment = await tx.payment.findFirst({
              where: {
                bookingId,
                provider: "MANUAL",
                status: "AWAITING",
              },
              orderBy: { createdAt: "desc" },
            });

            if (!awaitingPayment) {
              throw new Error(
                "Peserta belum menandai sudah membayar"
              );
            }

            const now = new Date();

            await tx.payment.update({
              where: { id: awaitingPayment.id },
              data: {
                status: "PAID",
                paidAt: now,
                method: "manual_transfer",
              },
            });

            await tx.booking.update({
              where: { id: bookingId },
              data: { status: "PAID" },
            });

            // Backward-compat: tetap update timestamp di TripParticipant.
            await tx.tripParticipant.update({
              where: { id: booking.participantId },
              data: { paymentConfirmedAt: now },
            });

            return { ok: true as const };
          },
          {
            isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
            maxWait: 5000,
            timeout: 15000,
          }
        );
      } catch (e) {
        lastErr = e;
        if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
          continue;
        }
        throw e;
      }
    }
    throw lastErr instanceof Error
      ? lastErr
      : new Error("Gagal mengonfirmasi pembayaran. Coba lagi sebentar.");
  },

  /**
   * Daftar booking yang masih menunggu konfirmasi organizer di trip tertentu.
   * Dipakai OrganizerPaymentQueue.
   */
  async getAwaitingManualForTrip(tripId: string) {
    return bookingRepo.findAwaitingManualConfirmation(tripId);
  },
};
