import { cache } from "react";
import { Prisma } from "@/app/generated/prisma/client";
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
import { refundService } from "@/server/services/refund.service";
import { payoutService } from "@/server/services/payout.service";
import { payoutRepo } from "@/server/repositories/payout.repo";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
import { isFreeTrip } from "@/lib/trip-pricing";
import type { ItineraryItemInput } from "@/lib/itinerary";
import { runSerializable } from "@/lib/serializable-tx";

interface CreateTripInput {
  category: ActivityCategory;
  title: string;
  description?: string;
  destination: string;
  location: string;
  meetingPoint?: string;
  whatsIncluded?: string;
  whatsExcluded?: string;
  date: Date;
  endDate?: Date;
  maxParticipants: number;
  price: number;
  vibe?: Vibe;
  organizerId: string;
  imageUrls?: string[];
  itineraryItems?: ItineraryItemInput[];
}

export const tripService = {
  async getOpenTrips(filters?: TripFilters) {
    return tripRepo.findOpen(filters);
  },

  async getAllTrips() {
    return tripRepo.findAll();
  },

  /**
   * Ambil trip by id. Dibungkus `React.cache()` — `generateMetadata`, body
   * halaman, dan `opengraph-image` memanggil ini untuk id yang sama dalam satu
   * request, jadi query Prisma yang berat ini cukup jalan sekali per request.
   */
  getTripById: cache(async (id: string) => {
    const trip = await tripRepo.findById(id);
    if (!trip) {
      throw new Error("Trip tidak ditemukan");
    }
    return trip;
  }),

  async createTrip(input: CreateTripInput) {
    if (isTripDepartureDayPast(input.date)) {
      throw new Error("Tanggal berangkat tidak boleh di masa lalu");
    }

    if (input.endDate && input.endDate.getTime() < input.date.getTime()) {
      throw new Error("Tanggal pulang tidak boleh sebelum tanggal berangkat");
    }

    const since = utcStartOfDay(new Date());
    const images = input.imageUrls?.length
      ? {
          create: input.imageUrls.map((url, i) => ({ url, order: i })),
        }
      : undefined;

    const itineraryItems = input.itineraryItems?.length
      ? {
          create: input.itineraryItems.map((item, i) => ({
            day: item.day,
            startTime: item.startTime,
            endTime: item.endTime ?? null,
            activity: item.activity.trim(),
            order: i,
          })),
        }
      : undefined;

    const tripData = {
      category: input.category,
      title: input.title,
      description: input.description,
      destination: input.destination,
      location: input.location,
      meetingPoint: input.meetingPoint,
      whatsIncluded: input.whatsIncluded,
      whatsExcluded: input.whatsExcluded,
      date: input.date,
      endDate: input.endDate,
      maxParticipants: input.maxParticipants,
      price: input.price,
      vibe: input.vibe,
      organizer: { connect: { id: input.organizerId } },
      images,
      itineraryItems,
    } satisfies Prisma.TripCreateInput;

    return runSerializable(async (tx) => {
      const todayCount = await tx.trip.count({
        where: {
          organizerId: input.organizerId,
          createdAt: { gte: since },
        },
      });
      if (todayCount >= LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY) {
        throw new Error(
          `Batas harian: maksimal ${LIMITS.MAX_TRIPS_PER_ORGANIZER_PER_DAY} trip per hari. Coba lagi besok.`
        );
      }
      return tx.trip.create({ data: tripData });
    }, "Gagal membuat trip. Coba lagi sebentar.");
  },

  async joinTrip(tripId: string, userId: string) {
    return runSerializable(async (tx) => {
      const trip = await tx.trip.findUnique({
        where: { id: tripId },
        select: {
          id: true,
          status: true,
          date: true,
          organizerId: true,
          maxParticipants: true,
          price: true,
        },
      });

      if (!trip) {
        throw new Error("Trip tidak ditemukan");
      }
      if (trip.status !== "OPEN") {
        throw new Error("Trip tidak tersedia untuk pendaftaran");
      }
      if (isTripDepartureDayPast(trip.date)) {
        throw new Error(
          "Trip sudah melewati tanggal berangkat, tidak bisa mendaftar"
        );
      }
      if (trip.organizerId === userId) {
        throw new Error("Organizer tidak bisa join trip sendiri");
      }

      const existing = await tx.tripParticipant.findUnique({
        where: { tripId_userId: { tripId, userId } },
      });
      if (existing && existing.status !== "CANCELLED") {
        throw new Error("Kamu sudah terdaftar di trip ini");
      }

      const participantCount = await tx.tripParticipant.count({
        where: { tripId, status: { not: "CANCELLED" } },
      });
      if (participantCount >= trip.maxParticipants) {
        throw new Error("Trip sudah penuh");
      }

      const participant =
        existing?.status === "CANCELLED"
          ? await tx.tripParticipant.update({
              where: { tripId_userId: { tripId, userId } },
              data: {
                status: "PENDING",
                markedPaidAt: null,
                paymentConfirmedAt: null,
              },
            })
          : await tx.tripParticipant.create({
              data: { tripId, userId, status: "PENDING" },
            });

      // Booking 1-1 ke participant. Upsert untuk handle re-join setelah CANCELLED.
      await tx.booking.upsert({
        where: { participantId: participant.id },
        create: {
          tripId,
          userId,
          participantId: participant.id,
          amount: trip.price,
          status: "PENDING",
        },
        update: {
          status: "PENDING",
          amount: trip.price,
        },
      });

      const newCount = await tx.tripParticipant.count({
        where: { tripId, status: { not: "CANCELLED" } },
      });
      if (newCount >= trip.maxParticipants) {
        await tx.trip.update({
          where: { id: tripId },
          data: { status: "FULL" },
        });
      }

      return participant;
    }, "Pendaftaran sedang ramai. Coba lagi sebentar.");
  },

  /**
   * Peserta batal ikut trip (untuk booking yang BELUM lunas). Seluruh read +
   * write dibungkus satu transaksi Serializable supaya aman dari race:
   * - Status booking dicek di dalam tx, dan `booking.updateMany` difilter
   *   `PENDING/AWAITING_PAY` — kalau webhook pembayaran menandai booking PAID
   *   bersamaan, booking lunas TIDAK ikut di-cancel (cegah uang menggantung
   *   tanpa Refund record).
   * - Re-open trip FULL → OPEN dilakukan di tx yang sama dan kondisional
   *   (`status: "FULL"`) supaya tidak menimpa trip yang sudah CLOSED.
   */
  async cancelJoin(tripId: string, userId: string) {
    return runSerializable(async (tx) => {
      const trip = await tx.trip.findUnique({
        where: { id: tripId },
        select: { id: true, status: true, date: true, maxParticipants: true },
      });
      if (!trip) {
        throw new Error("Trip tidak ditemukan");
      }
      if (isTripDepartureDayPast(trip.date)) {
        throw new Error(
          "Tanggal berangkat trip sudah lewat — pendaftaran tidak bisa dibatalkan. Jika trip sudah selesai, gunakan bagian ulasan."
        );
      }

      const existing = await tx.tripParticipant.findUnique({
        where: { tripId_userId: { tripId, userId } },
      });
      if (!existing || existing.status === "CANCELLED") {
        throw new Error("Kamu tidak terdaftar di trip ini");
      }

      // Safety: kalau booking sudah PAID/PARTIALLY_REFUNDED, paksa lewat refund
      // flow supaya tidak ada uang menggantung tanpa Refund record. Dicek di
      // dalam tx supaya konsisten dengan webhook pembayaran.
      const booking = await tx.booking.findUnique({
        where: { participantId: existing.id },
        select: { status: true },
      });
      if (
        booking &&
        (booking.status === "PAID" || booking.status === "PARTIALLY_REFUNDED")
      ) {
        throw new Error(
          "Booking kamu sudah lunas — pakai tombol 'Cancel & Request Refund' supaya uang bisa dikembalikan."
        );
      }

      const cancelled = await tx.tripParticipant.update({
        where: { tripId_userId: { tripId, userId } },
        data: {
          status: "CANCELLED",
          markedPaidAt: null,
          paymentConfirmedAt: null,
        },
      });

      // Hanya cancel booking yang belum lunas — filter status menutup race
      // dengan webhook pembayaran yang bisa menandai PAID secara bersamaan.
      await tx.booking.updateMany({
        where: {
          participantId: existing.id,
          status: { in: ["PENDING", "AWAITING_PAY"] },
        },
        data: { status: "CANCELLED" },
      });

      // Slot kembali kosong → re-open trip. Kondisional `status: "FULL"`
      // supaya tidak menghidupkan trip yang sudah CLOSED/COMPLETED.
      if (trip.status === "FULL") {
        const activeCount = await tx.tripParticipant.count({
          where: { tripId, status: { not: "CANCELLED" } },
        });
        if (activeCount < trip.maxParticipants) {
          await tx.trip.updateMany({
            where: { id: tripId, status: "FULL" },
            data: { status: "OPEN" },
          });
        }
      }

      return cancelled;
    });
  },

  async confirmParticipant(
    tripId: string,
    participantId: string,
    organizerId: string
  ) {
    return runSerializable(async (tx) => {
      const trip = await tx.trip.findUnique({
        where: { id: tripId },
        select: { id: true, organizerId: true, price: true },
      });
      if (!trip) {
        throw new Error("Trip tidak ditemukan");
      }
      if (trip.organizerId !== organizerId) {
        throw new Error(
          "Hanya organizer trip ini yang bisa mengonfirmasi peserta"
        );
      }

      const participant = await tx.tripParticipant.findUnique({
        where: { id: participantId },
        select: { id: true, tripId: true, status: true },
      });
      if (!participant || participant.tripId !== tripId) {
        throw new Error("Peserta tidak ditemukan");
      }
      if (participant.status !== "PENDING") {
        throw new Error("Peserta ini tidak dalam status menunggu persetujuan");
      }

      // Trip gratis: Booking langsung PAID. Trip berbayar: AWAITING_PAY.
      const nextBookingStatus = isFreeTrip(trip) ? "PAID" : "AWAITING_PAY";

      // Update kondisional — kalau peserta sudah berubah status (mis. user
      // batal ikut bersamaan), count 0 → tolak, jangan resurrect peserta.
      const confirmed = await tx.tripParticipant.updateMany({
        where: { id: participantId, status: "PENDING" },
        data: { status: "CONFIRMED" },
      });
      if (confirmed.count === 0) {
        throw new Error("Peserta ini tidak dalam status menunggu persetujuan");
      }

      // Filter `not CANCELLED` supaya booking yang sudah dibatalkan tidak
      // ikut "dihidupkan" kembali oleh konfirmasi yang balapan.
      await tx.booking.updateMany({
        where: { participantId, status: { not: "CANCELLED" } },
        data: { status: nextBookingStatus },
      });

      return tx.tripParticipant.findUniqueOrThrow({
        where: { id: participantId },
      });
    });
  },

  async rejectParticipant(
    tripId: string,
    participantId: string,
    organizerId: string
  ) {
    return runSerializable(async (tx) => {
      const trip = await tx.trip.findUnique({
        where: { id: tripId },
        select: {
          id: true,
          status: true,
          organizerId: true,
          maxParticipants: true,
        },
      });
      if (!trip) {
        throw new Error("Trip tidak ditemukan");
      }
      if (trip.organizerId !== organizerId) {
        throw new Error(
          "Hanya organizer trip ini yang bisa menolak permintaan bergabung"
        );
      }

      const participant = await tx.tripParticipant.findUnique({
        where: { id: participantId },
        select: { id: true, tripId: true, status: true },
      });
      if (!participant || participant.tripId !== tripId) {
        throw new Error("Peserta tidak ditemukan");
      }
      if (participant.status !== "PENDING") {
        throw new Error(
          "Hanya permintaan yang masih menunggu yang bisa ditolak"
        );
      }

      // Update kondisional pada status PENDING — aman dari race dengan
      // confirm/cancel yang berjalan bersamaan.
      const rejected = await tx.tripParticipant.updateMany({
        where: { id: participantId, status: "PENDING" },
        data: {
          status: "CANCELLED",
          markedPaidAt: null,
          paymentConfirmedAt: null,
        },
      });
      if (rejected.count === 0) {
        throw new Error(
          "Hanya permintaan yang masih menunggu yang bisa ditolak"
        );
      }

      // Peserta PENDING belum bisa punya booking lunas, tapi filter status
      // tetap dipasang supaya tidak pernah menimpa booking PAID.
      await tx.booking.updateMany({
        where: {
          participantId,
          status: { in: ["PENDING", "AWAITING_PAY"] },
        },
        data: { status: "CANCELLED" },
      });

      // Slot kembali kosong → re-open trip, kondisional `status: "FULL"`.
      if (trip.status === "FULL") {
        const activeCount = await tx.tripParticipant.count({
          where: { tripId, status: { not: "CANCELLED" } },
        });
        if (activeCount < trip.maxParticipants) {
          await tx.trip.updateMany({
            where: { id: tripId, status: "FULL" },
            data: { status: "OPEN" },
          });
        }
      }

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

  /**
   * Auto-complete trip yang sudah lewat. Dipakai cron harian.
   *
   * Cutoff = start of today UTC. Trip dengan endDate < cutoff (atau, kalau
   * endDate null, date < cutoff) di-set status COMPLETED — selama statusnya
   * masih OPEN/FULL. CLOSED trip tidak disentuh (organizer eksplisit batalkan).
   *
   * Idempotent: dua kali run di hari sama, run kedua nge-match 0 row.
   */
  async autoCompletePastTrips() {
    const cutoff = utcStartOfDay(new Date());
    return tripRepo.bulkCompletePastTrips(cutoff);
  },

  /**
   * Batalkan trip (Trip.status = CLOSED). Bisa dipicu organizer sendiri ATAU
   * admin (intervensi). Atomic dalam satu serializable transaction:
   * - Set Trip.status = CLOSED (+ `cancelledByAdminId` & `cancelledReason`
   *   kalau actor = ADMIN).
   * - Untuk setiap peserta aktif:
   *     - Booking PAID → buat Refund ORGANIZER_CANCELLED (auto-approved, full
   *       amount). Booking tetap PAID sampai admin mark SUCCEEDED — jejak
   *       finansial harus terjaga.
   *     - Booking PENDING/AWAITING_PAY → set CANCELLED langsung (uang belum
   *       masuk, tidak ada refund).
   *     - Booking PARTIALLY_REFUNDED / dengan refund aktif → di-skip (admin
   *       handle manual supaya tidak double-refund).
   * - Semua TripParticipant aktif → CANCELLED.
   *
   * Idempotent: trip yang sudah CLOSED/COMPLETED akan ditolak supaya tidak
   * dobel-buat refund.
   */
  async closeTrip(
    tripId: string,
    actor:
      | { type: "ORGANIZER"; userId: string }
      | { type: "ADMIN"; adminId: string; reason: string }
  ) {
    return runSerializable(async (tx) => {
      const trip = await tx.trip.findUnique({
        where: { id: tripId },
        select: {
          id: true,
          status: true,
          organizerId: true,
          date: true,
          title: true,
          organizer: { select: { id: true, email: true, name: true } },
        },
      });
      if (!trip) {
        throw new Error("Trip tidak ditemukan");
      }
      if (actor.type === "ORGANIZER" && trip.organizerId !== actor.userId) {
        throw new Error("Hanya organizer trip ini yang bisa membatalkan trip");
      }
      if (trip.status === "CLOSED") {
        throw new Error("Trip sudah dibatalkan");
      }
      if (trip.status === "COMPLETED") {
        throw new Error(
          "Trip sudah selesai (COMPLETED) — tidak bisa dibatalkan"
        );
      }
      if (isTripDepartureDayPast(trip.date)) {
        throw new Error(
          "Tanggal berangkat sudah lewat — gunakan flow pelaporan biasa ke admin"
        );
      }

      const bookings = await tx.booking.findMany({
        where: { tripId },
        include: {
          payments: {
            where: { status: "PAID" },
            orderBy: { paidAt: "desc" },
            take: 1,
          },
          refunds: {
            where: {
              status: { in: ["PENDING", "APPROVED", "PROCESSING"] },
            },
            select: { id: true },
          },
        },
      });

      const refundsCreated: string[] = [];
      const cancelledBookings: string[] = [];
      const skippedBookings: string[] = [];
      // userId → nominal refund yang dibuat untuk dia (untuk email pembatalan).
      const refundByUser = new Map<string, number>();

      for (const b of bookings) {
        if (b.status === "CANCELLED" || b.status === "EXPIRED") {
          continue;
        }
        if (b.status === "REFUNDED") {
          continue;
        }
        if (b.refunds.length > 0) {
          // Sudah ada refund aktif (mis. user request cancel). Admin
          // handle manual supaya tidak konflik dengan refund existing.
          skippedBookings.push(b.id);
          continue;
        }

        if (b.status === "PAID" || b.status === "PARTIALLY_REFUNDED") {
          const paid = b.payments[0];
          if (!paid) {
            // Payment tidak konsisten dgn booking status — skip + flag.
            skippedBookings.push(b.id);
            continue;
          }
          // Untuk PARTIALLY_REFUNDED, hitung sisa refundable.
          const alreadyRefunded = await tx.refund.aggregate({
            where: { bookingId: b.id, status: "SUCCEEDED" },
            _sum: { amount: true },
          });
          const remaining = paid.amount - (alreadyRefunded._sum.amount ?? 0);
          if (remaining <= 0) {
            continue;
          }
          const refund = await refundService.createSystemRefundForClosedTrip(
            tx,
            {
              bookingId: b.id,
              paymentId: paid.id,
              amount: remaining,
            }
          );
          refundsCreated.push(refund.id);
          refundByUser.set(b.userId, remaining);

          // Trip dibatalkan → cancel payout HELD/RELEASED organizer untuk
          // booking ini. Payout PAID di-flag clawback otomatis.
          const payout = await payoutRepo.findByBookingId(b.id, tx);
          if (payout) {
            await payoutService.cancel(tx, {
              payoutId: payout.id,
              reason: "Trip dibatalkan organizer.",
            });
          }
        } else {
          // PENDING / AWAITING_PAY → uang belum masuk → langsung CANCELLED.
          await tx.booking.update({
            where: { id: b.id },
            data: { status: "CANCELLED" },
          });
          cancelledBookings.push(b.id);
        }
      }

      // Snapshot peserta aktif SEBELUM di-cancel — untuk notifikasi email.
      const activeParticipants = await tx.tripParticipant.findMany({
        where: { tripId, status: { not: "CANCELLED" } },
        select: { user: { select: { id: true, email: true, name: true } } },
      });

      // Semua participant aktif → CANCELLED (apapun status booking-nya).
      await tx.tripParticipant.updateMany({
        where: { tripId, status: { not: "CANCELLED" } },
        data: {
          status: "CANCELLED",
          markedPaidAt: null,
          paymentConfirmedAt: null,
        },
      });

      await tx.trip.update({
        where: { id: tripId },
        data: {
          status: "CLOSED",
          ...(actor.type === "ADMIN" && {
            cancelledByAdminId: actor.adminId,
            cancelledReason: actor.reason,
          }),
        },
      });

      return {
        ok: true as const,
        refundsCreated,
        cancelledBookings,
        skippedBookings,
        // Data penerima notifikasi — email dikirim oleh action setelah tx commit.
        notify: {
          tripTitle: trip.title,
          organizer: trip.organizer,
          participants: activeParticipants.map((p) => ({
            userId: p.user.id,
            email: p.user.email,
            name: p.user.name,
            refundAmount: refundByUser.get(p.user.id) ?? 0,
          })),
        },
      };
    }, "Gagal membatalkan trip. Coba lagi sebentar.");
  },
};
