import { randomBytes } from "crypto";
import { Prisma } from "@/app/generated/prisma/client";
import { refundRepo } from "@/server/repositories/refund.repo";
import { calculateRefundAmount, daysUntilDeparture } from "@/lib/refund-policy";
import { isTripDepartureDayPast } from "@/lib/trip-dates";
import { payoutService } from "@/server/services/payout.service";
import { runSerializable } from "@/lib/serializable-tx";

function newIdempotencyKey(): string {
  return `refund_${randomBytes(16).toString("hex")}`;
}

type RequestRefundInput = {
  bookingId: string;
  reason:
    | "USER_CANCELLATION"
    | "ORGANIZER_CANCELLED"
    | "TRIP_ISSUE"
    | "ADMIN_ADJUSTMENT"
    | "DISPUTE_RESOLVED"
    | "OTHER";
  reportedBy: "PARTICIPANT" | "ORGANIZER";
  reportNote: string;
  /** Nominal refund (IDR). Kalau tidak diisi → service akan pakai sisa
   *  refundable amount (payment.amount - sudah-di-refund). */
  amount?: number;
  /** Admin yang memasukkan laporan ke sistem. */
  initiatedByAdminId: string;
};

export const refundService = {
  /**
   * Admin mencatat laporan refund dari peserta atau organizer ke sistem.
   * Status awal: PENDING. Belum mengubah Booking/Payment status.
   *
   * Idempotency: kalau booking masih punya refund PENDING/APPROVED/PROCESSING,
   * tolak — admin harus selesaikan yang lama dulu (reject atau succeeded).
   */
  async requestRefund(input: RequestRefundInput) {
    return runSerializable(async (tx) => {
      const booking = await tx.booking.findUnique({
        where: { id: input.bookingId },
        include: {
          payments: {
            where: { status: "PAID" },
            orderBy: { paidAt: "desc" },
            take: 1,
          },
        },
      });
      if (!booking) {
        throw new Error("Booking tidak ditemukan");
      }
      if (booking.amount <= 0) {
        throw new Error("Booking gratis — tidak ada nominal untuk di-refund");
      }
      if (booking.status === "CANCELLED" || booking.status === "EXPIRED") {
        throw new Error(
          "Booking sudah dibatalkan/expired — tidak ada pembayaran untuk di-refund"
        );
      }
      if (booking.status === "REFUNDED") {
        throw new Error("Booking sudah refund penuh");
      }

      const paidPayment = booking.payments[0];
      if (!paidPayment) {
        throw new Error(
          "Tidak ada Payment dengan status PAID di booking ini — tidak bisa di-refund"
        );
      }

      const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
      if (hasActive) {
        throw new Error(
          "Booking ini masih punya refund yang sedang diproses. Selesaikan dulu sebelum membuat yang baru."
        );
      }

      const alreadyRefunded = await refundRepo.sumSucceededAmount(input.bookingId, tx);
      const remaining = paidPayment.amount - alreadyRefunded;
      if (remaining <= 0) {
        throw new Error("Seluruh nominal sudah di-refund");
      }

      const amount = input.amount ?? remaining;
      if (!Number.isInteger(amount) || amount <= 0) {
        throw new Error("Nominal refund harus bilangan bulat positif");
      }
      if (amount > remaining) {
        throw new Error(
          `Nominal refund (Rp ${amount.toLocaleString("id-ID")}) melebihi sisa yang bisa di-refund (Rp ${remaining.toLocaleString("id-ID")})`
        );
      }

      return refundRepo.create(
        {
          bookingId: input.bookingId,
          paymentId: paidPayment.id,
          amount,
          reason: input.reason,
          reportedBy: input.reportedBy,
          reportNote: input.reportNote,
          initiatedBy: "ADMIN",
          idempotencyKey: newIdempotencyKey(),
        },
        tx
      );
    });
  },

  /** PENDING → APPROVED. Boleh menambah catatan admin (opsional). */
  async approveRefund(input: { refundId: string; adminId: string; adminNote?: string }) {
    return runSerializable(async (tx) => {
      const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
      if (!refund) {
        throw new Error("Refund tidak ditemukan");
      }
      if (refund.status !== "PENDING") {
        throw new Error("Hanya refund berstatus PENDING yang bisa disetujui");
      }
      return refundRepo.update(
        input.refundId,
        {
          status: "APPROVED",
          reviewedById: input.adminId,
          reviewedAt: new Date(),
          adminNote: input.adminNote ?? refund.adminNote,
        },
        tx
      );
    });
  },

  /** PENDING → REJECTED. Alasan wajib supaya audit trail jelas. */
  async rejectRefund(input: { refundId: string; adminId: string; adminNote: string }) {
    if (!input.adminNote.trim()) {
      throw new Error("Alasan tolak wajib diisi");
    }
    return runSerializable(async (tx) => {
      const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
      if (!refund) {
        throw new Error("Refund tidak ditemukan");
      }
      if (refund.status !== "PENDING") {
        throw new Error("Hanya refund berstatus PENDING yang bisa ditolak");
      }
      return refundRepo.update(
        input.refundId,
        {
          status: "REJECTED",
          reviewedById: input.adminId,
          reviewedAt: new Date(),
          adminNote: input.adminNote.trim(),
        },
        tx
      );
    });
  },

  /**
   * APPROVED → SUCCEEDED untuk manual transfer (admin sudah transfer manual
   * ke rekening peserta). adminNote diharapkan berisi referensi transfer.
   *
   * Side effects:
   * - Update Payment.status → REFUNDED hanya saat full refund.
   * - Update Booking.status → REFUNDED (full) atau PARTIALLY_REFUNDED (partial).
   * - Untuk USER_CANCELLATION: bebaskan slot — set TripParticipant → CANCELLED
   *   dan re-open Trip (FULL → OPEN) kalau peserta aktif < maxParticipants.
   *   Untuk ORGANIZER_CANCELLED slot tidak perlu dibebaskan (trip sudah CLOSED).
   */
  async markSucceededManual(input: {
    refundId: string;
    adminId: string;
    adminNote: string;
  }) {
    if (!input.adminNote.trim()) {
      throw new Error("Catatan/referensi transfer wajib diisi");
    }
    return runSerializable(async (tx) => {
      const refund = await tx.refund.findUnique({
        where: { id: input.refundId },
      });
      if (!refund) {
        throw new Error("Refund tidak ditemukan");
      }
      if (refund.status !== "APPROVED") {
        throw new Error(
          "Hanya refund APPROVED yang bisa ditandai SUCCEEDED. Setujui dulu."
        );
      }

      const now = new Date();

      await refundRepo.update(
        input.refundId,
        {
          status: "SUCCEEDED",
          succeededAt: now,
          reviewedById: input.adminId,
          reviewedAt: now,
          adminNote: input.adminNote.trim(),
        },
        tx
      );

      // Escrow: kurangi (atau cancel) payout organizer sesuai nominal refund.
      await payoutService.applyRefundDelta(tx, {
        bookingId: refund.bookingId,
        refundAmount: refund.amount,
      });

      const totalRefunded = await refundRepo.sumSucceededAmount(
        refund.bookingId,
        tx
      );

      if (refund.paymentId) {
        const payment = await tx.payment.findUnique({
          where: { id: refund.paymentId },
        });
        if (payment && totalRefunded >= payment.amount) {
          await tx.payment.update({
            where: { id: payment.id },
            data: { status: "REFUNDED" },
          });
        }
      }

      const booking = await tx.booking.findUnique({
        where: { id: refund.bookingId },
        include: {
          trip: { select: { id: true, status: true, maxParticipants: true } },
          payments: {
            where: { status: { in: ["PAID", "REFUNDED"] } },
            orderBy: { paidAt: "desc" },
            take: 1,
          },
        },
      });
      if (!booking) {
        throw new Error("Booking tidak ditemukan saat menutup refund");
      }
      const paid = booking.payments[0];
      if (paid) {
        const nextStatus =
          totalRefunded >= paid.amount ? "REFUNDED" : "PARTIALLY_REFUNDED";
        if (booking.status !== nextStatus) {
          await tx.booking.update({
            where: { id: booking.id },
            data: { status: nextStatus },
          });
        }
      }

      // Slot release untuk user cancellation. Organizer cancel di-handle
      // closeTrip (participant + trip sudah di-CANCELLED/CLOSED di sana).
      if (refund.reason === "USER_CANCELLATION") {
        await tx.tripParticipant.updateMany({
          where: {
            id: booking.participantId,
            status: { not: "CANCELLED" },
          },
          data: {
            status: "CANCELLED",
            markedPaidAt: null,
            paymentConfirmedAt: null,
          },
        });
        if (booking.trip.status === "FULL") {
          const remaining = await tx.tripParticipant.count({
            where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
          });
          if (remaining < booking.trip.maxParticipants) {
            await tx.trip.update({
              where: { id: booking.tripId },
              data: { status: "OPEN" },
            });
          }
        }
      }

      return { ok: true as const };
    });
  },

  /**
   * Peserta cancel booking sendiri. Hitung refund pakai policy default
   * (lib/refund-policy.ts) — hardcoded MVP, akan jadi data-driven di R-5.
   *
   * Behaviour:
   * - Kalau hasil hitung = 0 (di luar window): cancel participant + booking
   *   langsung, tanpa Refund row (uang tidak balik).
   * - Kalau hasil hitung > 0: buat Refund PENDING (initiatedBy=USER,
   *   reportedBy=PARTICIPANT, reason=USER_CANCELLATION). Participant + booking
   *   TETAP CONFIRMED/PAID sampai admin mark SUCCEEDED — slot baru bebas saat
   *   refund tuntas. Cegah double-request via hasActiveRefund.
   */
  async requestUserCancellation(input: {
    bookingId: string;
    userId: string;
  }) {
    return runSerializable(async (tx) => {
      const booking = await tx.booking.findUnique({
        where: { id: input.bookingId },
        include: {
          trip: {
            select: {
              id: true,
              date: true,
              status: true,
              maxParticipants: true,
            },
          },
          payments: {
            where: { status: "PAID" },
            orderBy: { paidAt: "desc" },
            take: 1,
          },
        },
      });
      if (!booking) {
        throw new Error("Booking tidak ditemukan");
      }
      if (booking.userId !== input.userId) {
        throw new Error("Booking ini bukan milikmu");
      }
      if (
        booking.trip.status === "CLOSED" ||
        booking.trip.status === "COMPLETED"
      ) {
        throw new Error(
          "Trip sudah dibatalkan/selesai — pembatalan mandiri ditutup. Hubungi admin untuk proses refund."
        );
      }
      if (booking.status !== "PAID") {
        throw new Error(
          "Hanya booking PAID yang bisa cancel dengan refund. Booking yang belum lunas bisa cancel dari tombol 'Batal Ikut'."
        );
      }
      if (isTripDepartureDayPast(booking.trip.date)) {
        throw new Error(
          "Trip sudah lewat tanggal berangkat — pembatalan ditutup"
        );
      }
      const paid = booking.payments[0];
      if (!paid) {
        throw new Error(
          "Tidak ada Payment dengan status PAID di booking ini"
        );
      }

      const days = daysUntilDeparture(booking.trip.date);
      const refundAmount = calculateRefundAmount(paid.amount, days);

      if (refundAmount === 0) {
        await tx.tripParticipant.update({
          where: { id: booking.participantId },
          data: {
            status: "CANCELLED",
            markedPaidAt: null,
            paymentConfirmedAt: null,
          },
        });
        await tx.booking.update({
          where: { id: booking.id },
          data: { status: "CANCELLED" },
        });
        if (booking.trip.status === "FULL") {
          const remaining = await tx.tripParticipant.count({
            where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
          });
          if (remaining < booking.trip.maxParticipants) {
            await tx.trip.update({
              where: { id: booking.tripId },
              data: { status: "OPEN" },
            });
          }
        }
        return {
          kind: "CANCELLED_NO_REFUND" as const,
          days,
          refundAmount: 0,
        };
      }

      const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
      if (hasActive) {
        throw new Error(
          "Booking ini sudah punya refund yang sedang diproses"
        );
      }

      const percentage = Math.floor((refundAmount * 100) / paid.amount);
      const refund = await refundRepo.create(
        {
          bookingId: booking.id,
          paymentId: paid.id,
          amount: refundAmount,
          reason: "USER_CANCELLATION",
          reportedBy: "PARTICIPANT",
          reportNote: `Self-service cancel oleh peserta — H-${days} dari tanggal berangkat (refund ${percentage}%).`,
          initiatedBy: "USER",
          idempotencyKey: newIdempotencyKey(),
        },
        tx
      );

      return {
        kind: "REFUND_PENDING" as const,
        refundId: refund.id,
        days,
        refundAmount,
      };
    });
  },

  /**
   * Dipanggil dari tripService.closeTrip (organizer cancel trip) dengan tx
   * yang sama. Buat Refund auto-approved untuk satu booking PAID. Tidak
   * mengecek hasActiveRefund (caller harus filter dulu) supaya batch closeTrip
   * idempotent dengan retry-safe.
   *
   * Refund langsung APPROVED — policy jelas (organizer cancel = 100% refund),
   * tapi eksekusi (SUCCEEDED) tetap manual oleh admin.
   */
  async createSystemRefundForClosedTrip(
    tx: Prisma.TransactionClient,
    input: {
      bookingId: string;
      paymentId: string;
      amount: number;
    }
  ) {
    const now = new Date();
    return tx.refund.create({
      data: {
        bookingId: input.bookingId,
        paymentId: input.paymentId,
        amount: input.amount,
        reason: "ORGANIZER_CANCELLED",
        reportedBy: "ORGANIZER",
        reportNote: "Organizer membatalkan trip — auto-create oleh SYSTEM.",
        initiatedBy: "SYSTEM",
        idempotencyKey: newIdempotencyKey(),
        status: "APPROVED",
        reviewedAt: now,
        adminNote: "Auto-approved (SYSTEM): organizer cancel = full refund.",
      },
    });
  },

  /**
   * APPROVED/PROCESSING → FAILED. Catatan wajib (alasan gagal).
   * Tidak mengubah Booking/Payment — uang belum keluar.
   */
  async markFailed(input: { refundId: string; adminId: string; adminNote: string }) {
    if (!input.adminNote.trim()) {
      throw new Error("Alasan gagal wajib diisi");
    }
    return runSerializable(async (tx) => {
      const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
      if (!refund) {
        throw new Error("Refund tidak ditemukan");
      }
      if (refund.status !== "APPROVED" && refund.status !== "PROCESSING") {
        throw new Error(
          "Hanya refund APPROVED atau PROCESSING yang bisa ditandai FAILED"
        );
      }
      return refundRepo.update(
        input.refundId,
        {
          status: "FAILED",
          failedAt: new Date(),
          reviewedById: input.adminId,
          reviewedAt: new Date(),
          adminNote: input.adminNote.trim(),
        },
        tx
      );
    });
  },
};
