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 { bookingService } from "@/server/services/booking.service";
import { LIMITS } from "@/lib/limits";
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
import { isFreeTrip } from "@/lib/trip-pricing";

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;
  itinerary?: string;
  whatsIncluded?: string;
  whatsExcluded?: string;
  date: Date;
  endDate?: Date;
  maxParticipants: number;
  price: number;
  vibe?: Vibe;
  organizerId: string;
  imageUrls?: string[];
}

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 tripData = {
      category: input.category,
      title: input.title,
      description: input.description,
      destination: input.destination,
      location: input.location,
      meetingPoint: input.meetingPoint,
      itinerary: input.itinerary,
      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,
    } 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");
    }

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

  async markParticipantPayment(tripId: string, userId: string) {
    const trip = await tripRepo.findById(tripId);
    if (!trip) {
      throw new Error("Trip tidak ditemukan");
    }

    if (isFreeTrip(trip)) {
      throw new Error(
        "Trip ini gratis — tidak ada pembayaran yang perlu ditandai"
      );
    }

    if (isTripDepartureDayPast(trip.date)) {
      throw new Error("Trip sudah lewat — pembayaran tidak perlu ditandai");
    }

    const booking = await bookingRepo.findByTripAndUser(tripId, userId);
    if (!booking || booking.status === "CANCELLED") {
      throw new Error("Kamu tidak terdaftar di trip ini");
    }

    return bookingService.markPaidManual(booking.id, userId);
  },

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

  async confirmParticipantPayment(
    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 yang bisa mengonfirmasi pembayaran");
    }
    if (isFreeTrip(trip)) {
      throw new Error("Trip ini gratis — tidak ada pembayaran yang perlu dikonfirmasi");
    }

    const booking = await bookingRepo.findByParticipantId(participantId);
    if (!booking || booking.tripId !== tripId) {
      throw new Error("Booking tidak ditemukan");
    }

    return bookingService.confirmPaidManual(booking.id, organizerId);
  },
};
