import { Prisma } from "@/app/generated/prisma/client";
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
import { prisma } from "@/lib/prisma";
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
import { participantRepo } from "@/server/repositories/participant.repo";
import { bookingRepo } from "@/server/repositories/booking.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";

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

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();
  },

  async getTripById(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;

    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        return await prisma.$transaction(
          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 });
          },
          {
            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 membuat trip. Coba lagi sebentar.");
  },

  async joinTrip(tripId: string, userId: string) {
    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        return await prisma.$transaction(
          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;
          },
          {
            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("Pendaftaran sedang ramai. Coba lagi sebentar.");
  },

  async cancelJoin(tripId: string, userId: string) {
    const trip = await tripRepo.findById(tripId);
    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 participantRepo.findByTripAndUser(tripId, userId);
    if (!existing || existing.status === "CANCELLED") {
      throw new Error("Kamu tidak terdaftar di trip ini");
    }

    // Safety: kalau booking sudah PAID, paksa lewat refund flow supaya tidak
    // ada uang menggantung tanpa Refund record.
    const existingBooking = await bookingRepo.findByTripAndUser(tripId, userId);
    if (
      existingBooking &&
      (existingBooking.status === "PAID" ||
        existingBooking.status === "PARTIALLY_REFUNDED")
    ) {
      throw new Error(
        "Booking kamu sudah lunas — pakai tombol 'Cancel & Request Refund' supaya uang bisa dikembalikan."
      );
    }

    const result = await prisma.$transaction(async (tx) => {
      const cancelled = await tx.tripParticipant.update({
        where: { tripId_userId: { tripId, userId } },
        data: {
          status: "CANCELLED",
          markedPaidAt: null,
          paymentConfirmedAt: null,
        },
      });
      await tx.booking.updateMany({
        where: { participantId: existing.id },
        data: { status: "CANCELLED" },
      });
      return cancelled;
    });

    if (trip.status === "FULL") {
      const count = await participantRepo.countByTrip(tripId);
      if (count < trip.maxParticipants) {
        await tripRepo.updateStatus(tripId, "OPEN");
      }
    }

    return result;
  },

  async confirmParticipant(
    tripId: string,
    participantId: string,
    organizerId: string
  ) {
    const trip = await tripRepo.findById(tripId);
    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 participantRepo.findById(participantId);
    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 (tinggal bayar).
    const nextBookingStatus = isFreeTrip(trip) ? "PAID" : "AWAITING_PAY";

    return prisma.$transaction(async (tx) => {
      await tx.booking.updateMany({
        where: { participantId },
        data: { status: nextBookingStatus },
      });
      return tx.tripParticipant.update({
        where: { id: participantId },
        data: { status: "CONFIRMED" },
      });
    });
  },

  async rejectParticipant(
    tripId: string,
    participantId: string,
    organizerId: string
  ) {
    const trip = await tripRepo.findById(tripId);
    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 participantRepo.findById(participantId);
    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");
    }

    await prisma.$transaction(async (tx) => {
      await tx.tripParticipant.update({
        where: { id: participantId },
        data: {
          status: "CANCELLED",
          markedPaidAt: null,
          paymentConfirmedAt: null,
        },
      });
      await tx.booking.updateMany({
        where: { participantId },
        data: { status: "CANCELLED" },
      });
    });

    if (trip.status === "FULL") {
      const count = await participantRepo.countByTrip(tripId);
      if (count < trip.maxParticipants) {
        await tripRepo.updateStatus(tripId, "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 }
  ) {
    let lastErr: unknown;
    for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
      try {
        return await prisma.$transaction(
          async (tx) => {
            const trip = await tx.trip.findUnique({
              where: { id: tripId },
              select: { id: true, status: true, organizerId: true, date: 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[] = [];

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

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

            // 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,
            };
          },
          {
            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 membatalkan trip. Coba lagi sebentar.");
  },
};
