import { Prisma } from "@/app/generated/prisma/client";
import { prisma } from "@/lib/prisma";
import { payoutRepo } from "@/server/repositories/payout.repo";
import { emailService } from "@/lib/email/send";

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"
  );
}

async function runSerializable<T>(
  fn: (tx: Prisma.TransactionClient) => Promise<T>
): Promise<T> {
  let lastErr: unknown;
  for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
    try {
      return await prisma.$transaction(fn, {
        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 memproses payout. Coba lagi sebentar.");
}

/** Buffer hari setelah trip selesai sebelum payout boleh ditransfer. */
export const PAYOUT_HOLD_BUFFER_DAYS = 3;

/** Hitung heldUntil dari trip date. Pakai endDate kalau ada, kalau tidak pakai date. */
function computeHeldUntil(tripDate: Date, tripEndDate: Date | null): Date {
  const baseDate = tripEndDate ?? tripDate;
  const result = new Date(baseDate);
  result.setUTCDate(result.getUTCDate() + PAYOUT_HOLD_BUFFER_DAYS);
  return result;
}

export const payoutService = {
  /**
   * Dipanggil saat Booking → PAID (webhook Midtrans atau organizer confirm manual).
   * Idempotent: kalau Payout untuk booking ini sudah ada, no-op (return existing).
   *
   * Snapshot bank info dari OrganizerVerification (kalau ada) supaya audit-friendly
   * walau organizer ganti bank nanti.
   */
  async createForPaidBooking(
    tx: Prisma.TransactionClient,
    input: { bookingId: string }
  ) {
    const booking = await tx.booking.findUnique({
      where: { id: input.bookingId },
      select: {
        id: true,
        amount: true,
        status: true,
        userId: true,
        trip: {
          select: {
            id: true,
            organizerId: true,
            date: true,
            endDate: true,
          },
        },
      },
    });
    if (!booking) {
      throw new Error("Booking tidak ditemukan saat membuat payout");
    }
    if (booking.amount <= 0) {
      // Trip gratis — tidak ada uang yang perlu di-payout.
      return null;
    }
    const existing = await payoutRepo.findByBookingId(booking.id, tx);
    if (existing) {
      return existing;
    }

    const bankInfo = await tx.organizerVerification.findUnique({
      where: { userId: booking.trip.organizerId },
      select: {
        status: true,
        bankName: true,
        bankAccountNumber: true,
        bankAccountName: true,
      },
    });

    const heldUntil = computeHeldUntil(booking.trip.date, booking.trip.endDate);

    return payoutRepo.create(
      {
        bookingId: booking.id,
        tripId: booking.trip.id,
        organizerId: booking.trip.organizerId,
        amount: booking.amount,
        heldUntil,
        bankName:
          bankInfo?.status === "APPROVED" ? bankInfo.bankName : null,
        bankAccountNumber:
          bankInfo?.status === "APPROVED" ? bankInfo.bankAccountNumber : null,
        bankAccountName:
          bankInfo?.status === "APPROVED" ? bankInfo.bankAccountName : null,
      },
      tx
    );
  },

  /**
   * Cron-callable: cari semua HELD payout yang sudah lewat heldUntil & trip-nya
   * COMPLETED, lalu flip ke RELEASED. Idempotent.
   */
  async releaseEligible() {
    const now = new Date();
    const eligible = await payoutRepo.findEligibleForRelease(now);
    if (eligible.length === 0) {
      return { releasedIds: [] as string[] };
    }
    const ids = eligible.map((p) => p.id);
    await prisma.payout.updateMany({
      where: { id: { in: ids }, status: "HELD" },
      data: { status: "RELEASED", releasedAt: now },
    });

    // E3.6 — kabari organizer payout-nya sudah lepas hold & masuk antrian
    // transfer. Pin ke `releasedAt: now` supaya hanya yang baru di-release.
    const released = await prisma.payout.findMany({
      where: { id: { in: ids }, status: "RELEASED", releasedAt: now },
      select: {
        id: true,
        amount: true,
        organizer: { select: { email: true, name: true } },
        trip: { select: { title: true } },
      },
    });
    for (const p of released) {
      void emailService.send({
        to: p.organizer.email,
        idempotencyKey: `payout_released-${p.id}`,
        template: {
          template: "payout_released",
          data: {
            organizerName: p.organizer.name,
            tripTitle: p.trip.title,
            amount: p.amount,
          },
        },
      });
    }

    return { releasedIds: ids };
  },

  /** RELEASED → PAID. Catatan/referensi transfer wajib (audit trail). */
  async markPaid(input: { payoutId: string; adminId: string; adminNote: string }) {
    if (!input.adminNote.trim()) {
      throw new Error("Catatan/referensi transfer wajib diisi");
    }
    return runSerializable(async (tx) => {
      const payout = await tx.payout.findUnique({
        where: { id: input.payoutId },
      });
      if (!payout) {
        throw new Error("Payout tidak ditemukan");
      }
      if (payout.status !== "RELEASED") {
        throw new Error(
          "Hanya payout RELEASED yang bisa ditandai PAID. Tunggu trip selesai + buffer."
        );
      }
      return payoutRepo.update(
        input.payoutId,
        {
          status: "PAID",
          paidAt: new Date(),
          processedById: input.adminId,
          adminNote: input.adminNote.trim(),
        },
        tx
      );
    });
  },

  /**
   * Cancel payout — biasanya dipanggil internal saat refund SUCCEEDED penuh
   * atau trip dibatalkan. Tidak boleh untuk payout yang sudah PAID (uang sudah
   * keluar ke organizer; admin perlu clawback manual).
   */
  async cancel(
    tx: Prisma.TransactionClient,
    input: { payoutId: string; reason: string; adminId?: string | null }
  ) {
    const payout = await tx.payout.findUnique({
      where: { id: input.payoutId },
    });
    if (!payout) return null;
    if (payout.status === "CANCELLED") return payout;
    if (payout.status === "PAID") {
      // Uang sudah ditransfer — tidak bisa undo otomatis. Catat note saja.
      return payoutRepo.update(
        input.payoutId,
        {
          adminNote:
            (payout.adminNote ? `${payout.adminNote}\n---\n` : "") +
            `[!] ${input.reason} setelah PAID — perlu clawback manual.`,
        },
        tx
      );
    }
    return payoutRepo.update(
      input.payoutId,
      {
        status: "CANCELLED",
        cancelledAt: new Date(),
        processedById: input.adminId ?? null,
        adminNote: input.reason,
      },
      tx
    );
  },

  /**
   * Refund SUCCEEDED — kurangi nominal payout sesuai nominal refund. Kalau
   * jatuh ke 0 atau lebih, cancel payout. Dipanggil dari refund.service.
   */
  async applyRefundDelta(
    tx: Prisma.TransactionClient,
    input: { bookingId: string; refundAmount: number }
  ) {
    const payout = await payoutRepo.findByBookingId(input.bookingId, tx);
    if (!payout) return null;
    if (payout.status === "CANCELLED") return payout;
    if (payout.status === "PAID") {
      // Uang sudah ditransfer ke organizer — flag untuk clawback manual.
      return payoutRepo.update(
        payout.id,
        {
          adminNote:
            (payout.adminNote ? `${payout.adminNote}\n---\n` : "") +
            `[!] Refund Rp${input.refundAmount.toLocaleString("id-ID")} terjadi setelah payout PAID. Perlu clawback manual dari organizer.`,
        },
        tx
      );
    }

    const nextAmount = payout.amount - input.refundAmount;
    if (nextAmount <= 0) {
      return payoutRepo.update(
        payout.id,
        {
          status: "CANCELLED",
          cancelledAt: new Date(),
          adminNote:
            (payout.adminNote ? `${payout.adminNote}\n---\n` : "") +
            "Dibatalkan otomatis karena refund penuh.",
        },
        tx
      );
    }
    return payoutRepo.update(payout.id, { amount: nextAmount }, tx);
  },
};
