import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import {
  MIDTRANS,
  createSnapTransaction,
  mapMidtransStatus,
  verifyMidtransSignature,
  type MidtransWebhookBody,
} from "@/lib/midtrans";
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 midtransOrderId(bookingId: string, attempt: number): string {
  return `midtrans-${bookingId}-${attempt}`;
}

export interface StartMidtransResult {
  paymentId: string;
  snapToken: string;
  snapJsUrl: string;
  clientKey: string;
  expiresAt: Date;
}

export type WebhookOutcome =
  | { ok: true; status: "updated" | "skipped" | "ignored" | "booking_conflict" }
  | { ok: false; reason: "signature_mismatch" | "amount_mismatch" };

export const paymentService = {
  /**
   * Mulai pembayaran Midtrans untuk Booking. Idempotent — kalau ada Payment
   * MIDTRANS aktif (status PENDING/AWAITING), reuse token yang sudah ada
   * selama belum expired.
   */
  async startMidtransPayment(bookingId: string, userId: string): Promise<StartMidtransResult> {
    const clientKey = process.env.NEXT_PUBLIC_MIDTRANS_CLIENT_KEY;
    if (!clientKey) {
      throw new Error("NEXT_PUBLIC_MIDTRANS_CLIENT_KEY belum di-set");
    }

    const booking = await prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        user: { select: { id: true, name: true, email: true } },
        trip: { select: { title: true, date: true } },
        payments: {
          where: { provider: "MIDTRANS" },
          orderBy: { createdAt: "desc" },
        },
      },
    });

    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 gratis tidak butuh pembayaran online");
    }
    if (booking.status === "PAID") {
      throw new Error("Booking sudah lunas");
    }
    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"
      );
    }

    // Reuse Payment AWAITING/PENDING yang belum kadaluarsa, kalau ada.
    const now = new Date();
    const reusable = booking.payments.find(
      (p) =>
        (p.status === "PENDING" || p.status === "AWAITING") &&
        p.snapToken &&
        (!p.expiresAt || p.expiresAt.getTime() > now.getTime())
    );

    const snapJsUrl = MIDTRANS.snapJsUrl();

    if (reusable && reusable.snapToken) {
      return {
        paymentId: reusable.id,
        snapToken: reusable.snapToken,
        snapJsUrl,
        clientKey,
        expiresAt: reusable.expiresAt ?? new Date(now.getTime() + 24 * 3600 * 1000),
      };
    }

    // Tutup attempt lama yang sudah expired (housekeeping ringan)
    const expiredIds = booking.payments
      .filter(
        (p) =>
          (p.status === "PENDING" || p.status === "AWAITING") &&
          p.expiresAt &&
          p.expiresAt.getTime() <= now.getTime()
      )
      .map((p) => p.id);
    if (expiredIds.length > 0) {
      await prisma.payment.updateMany({
        where: { id: { in: expiredIds } },
        data: { status: "EXPIRED" },
      });
    }

    const attemptNumber = booking.payments.length + 1;
    const orderId = midtransOrderId(booking.id, attemptNumber);
    const expirySeconds = 24 * 3600; // 24 jam, default Midtrans
    const expiresAt = new Date(now.getTime() + expirySeconds * 1000);

    // Create Payment row dulu (PENDING) supaya kalau call gagal, kita tetap punya audit trail.
    const payment = await prisma.payment.create({
      data: {
        bookingId: booking.id,
        provider: "MIDTRANS",
        externalOrderId: orderId,
        amount: booking.amount,
        status: "PENDING",
        expiresAt,
      },
    });

    let snapResult;
    try {
      snapResult = await createSnapTransaction({
        orderId,
        grossAmount: booking.amount,
        customer: {
          name: booking.user.name,
          email: booking.user.email,
        },
        itemName: booking.trip.title,
        expirySeconds,
      });
    } catch (err) {
      // Roll back Payment ke FAILED supaya orderId tidak nyangkut PENDING selamanya.
      await prisma.payment.update({
        where: { id: payment.id },
        data: {
          status: "FAILED",
          failedAt: new Date(),
          rejectionReason: err instanceof Error ? err.message : "Snap API error",
        },
      });
      throw err;
    }

    const updated = await prisma.payment.update({
      where: { id: payment.id },
      data: {
        snapToken: snapResult.token,
        status: "AWAITING",
      },
    });

    return {
      paymentId: updated.id,
      snapToken: snapResult.token,
      snapJsUrl,
      clientKey,
      expiresAt,
    };
  },

  /**
   * Handle webhook callback dari Midtrans. Idempotent — boleh dipanggil berulang.
   * Selalu return result yang webhook handler bisa terjemahkan ke HTTP status.
   */
  async handleMidtransWebhook(
    payload: MidtransWebhookBody
  ): Promise<WebhookOutcome> {
    const { order_id: orderId, status_code: statusCode, gross_amount: grossAmount, signature_key: signatureKey } = payload;

    const signatureValid = verifyMidtransSignature(
      orderId,
      statusCode,
      grossAmount,
      signatureKey
    );
    if (!signatureValid) {
      return { ok: false, reason: "signature_mismatch" };
    }

    const payment = await prisma.payment.findUnique({
      where: { externalOrderId: orderId },
      include: { booking: true },
    });

    if (!payment) {
      // Order tidak dikenal — return ok supaya Midtrans tidak retry forever.
      return { ok: true, status: "ignored" };
    }

    // Cek amount cocok. gross_amount dari Midtrans format "100000.00".
    const amountFromGateway = Math.round(Number(grossAmount));
    if (
      Number.isNaN(amountFromGateway) ||
      amountFromGateway !== payment.amount
    ) {
      // Tetap simpan callback mentah untuk audit, tapi jangan ubah status.
      await prisma.payment.update({
        where: { id: payment.id },
        data: {
          rawCallback: payload as unknown as Prisma.InputJsonValue,
          rejectionReason: `Amount mismatch: gateway=${grossAmount}, expected=${payment.amount}`,
        },
      });
      return { ok: false, reason: "amount_mismatch" };
    }

    const finalStatuses = new Set([
      "PAID",
      "FAILED",
      "EXPIRED",
      "CANCELLED",
      "REFUNDED",
    ]);
    if (finalStatuses.has(payment.status)) {
      // Idempotent: skip update tapi tetap log callback baru.
      await prisma.payment.update({
        where: { id: payment.id },
        data: {
          rawCallback: payload as unknown as Prisma.InputJsonValue,
        },
      });
      return { ok: true, status: "skipped" };
    }

    const newStatus = mapMidtransStatus(
      payload.transaction_status,
      payload.fraud_status ?? null
    );

    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        await prisma.$transaction(
          async (tx) => {
            const now = new Date();

            // Re-baca booking di dalam transaksi — bisa berubah (mis. user cancel) sejak
            // outer fetch. Lock-free read tetap aman karena isolasi serializable.
            const currentBooking = await tx.booking.findUnique({
              where: { id: payment.bookingId },
              select: { status: true, participantId: true },
            });

            const bookingInConflictState =
              currentBooking?.status === "CANCELLED" ||
              currentBooking?.status === "REFUNDED" ||
              currentBooking?.status === "EXPIRED";

            // Selalu simpan callback Payment (truth dari gateway). Kalau booking
            // di state konflik dan webhook mau set PAID, payment tetap PAID
            // (uang masuk) tapi tidak propagate ke booking — flag untuk manual review/refund.
            const conflictNote =
              bookingInConflictState && newStatus === "PAID"
                ? `Webhook PAID arrived but Booking is ${currentBooking.status}. Manual review required (potential refund).`
                : null;

            await tx.payment.update({
              where: { id: payment.id },
              data: {
                status: newStatus,
                externalTxId: payload.transaction_id ?? null,
                method: payload.payment_type ?? null,
                rawCallback: payload as unknown as Prisma.InputJsonValue,
                paidAt: newStatus === "PAID" ? now : null,
                failedAt:
                  newStatus === "FAILED" || newStatus === "EXPIRED" ? now : null,
                rejectionReason: conflictNote,
              },
            });

            if (newStatus === "PAID" && !bookingInConflictState) {
              await tx.booking.update({
                where: { id: payment.bookingId },
                data: { status: "PAID" },
              });
              // Backward-compat: sync timestamp di TripParticipant.
              await tx.tripParticipant.update({
                where: { id: payment.booking.participantId },
                data: { paymentConfirmedAt: now, markedPaidAt: now },
              });
            }
          },
          {
            isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
            maxWait: 5000,
            timeout: 15000,
          }
        );

        // Recheck post-tx untuk reporting outcome.
        const finalBooking = await prisma.booking.findUnique({
          where: { id: payment.bookingId },
          select: { status: true },
        });
        const isConflict =
          newStatus === "PAID" &&
          finalBooking?.status !== "PAID";
        return {
          ok: true,
          status: isConflict ? "booking_conflict" : "updated",
        };
      } catch (e) {
        lastErr = e;
        if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
          continue;
        }
        throw e;
      }
    }
    throw lastErr instanceof Error
      ? lastErr
      : new Error("Webhook gagal diproses karena konflik transaksi");
  },
};
