import { DatabaseError } from "@/lib/error";
import { logError } from "@/lib/logger";
import type { Selectable } from "kysely";
import type { BookingItineraries, BookingUnits } from "../datastore/db";
import type { RepositoryContext } from "../datastore/repository";
import { BaseRepository } from "../datastore/repository";

export const BookingRepository = (ctx: RepositoryContext) => {
  const { datastore } = new BaseRepository(ctx);

  const CreateEntry = async (entry: {
    member_id: string;
    entity_type:
      | "RESORT"
      | "CURATED_EXPERIENCE"
      | "RCI_EXCHANGE"
      | "RCI_RENTAL";
    entity_code: string;
    entity_name: string;
    additional_data?: Record<string, string | boolean | number>;
    itinerary?: Omit<
      Selectable<BookingItineraries>,
      "id" | "booking_destination_id" | "created_at" | "updated_at"
    >;
    units?: (Omit<
      Selectable<BookingUnits>,
      "id" | "booking_id" | "booking_ext_ref_id" | "created_at" | "updated_at"
    > & {
      external_source?: {
        inventory_id?: string;
        ref_id: string;
        source_name: "RCI" | "GUESTLINE";
        additional_data?: Record<string, string | boolean | number>;
      };
      guests: {
        member_number: string;
        type: string;
        additional_data?: Record<string, string | boolean | number>;
      }[];
      charges: {
        type: string;
        name: string;
        value: number;
        currency?: string;
        note?: string;
        additional_data?: Record<string, string | boolean | number>;
      }[];
    })[];
    note?: string;
  }) => {
    const trx = await datastore.startTransaction().execute();

    try {
      let entity_id: string | undefined;
      if (
        entry.entity_type === "RESORT" ||
        entry.entity_type === "RCI_EXCHANGE" ||
        entry.entity_type === "RCI_RENTAL"
      ) {
        const booking_destination = await trx
          .insertInto("booking_destinations")
          .values({
            code: entry.entity_code,
            name: entry.entity_name,
          })
          .onConflict((oc) =>
            oc.column("code").doUpdateSet({ name: entry.entity_name }),
          )
          .returning(["id"])
          .executeTakeFirst();
        entity_id = booking_destination?.id;
      }
      if (!entity_id) {
        return null;
      }

      const newBooking = await trx
        .insertInto("bookings")
        .values({
          member_id: entry.member_id,
          entity_id,
          entity_type: entry.entity_type as string,
          note: entry.note,
          additional_data: entry.additional_data,
        })
        .returningAll()
        .executeTakeFirstOrThrow();

      if (entry.itinerary) {
        await trx
          .insertInto("booking_itineraries")
          .values({
            name: entry.itinerary.name,
            available_from: entry.itinerary.available_from,
            available_to: entry.itinerary.available_to,
            booking_destination_id: entity_id,
            additional_data: entry.itinerary.additional_data,
          })
          .executeTakeFirstOrThrow();
      }

      if (entry.units) {
        for (const unit of entry.units) {
          let external_booking_source_id: string | undefined;
          if (unit.external_source) {
            const external_booking_source_record = await trx
              .insertInto("booking_external_source_refs")
              .values({
                ref_id: unit.external_source.ref_id,
                source_name: unit.external_source.source_name,
                additional_data: unit.external_source.additional_data,
              })
              .returning(["id"])
              .executeTakeFirstOrThrow();
            external_booking_source_id = external_booking_source_record.id;
          }
          const booking_unit = await trx
            .insertInto("booking_units")
            .values({
              check_in_date: unit.check_in_date,
              check_out_date: unit.check_out_date,
              booking_id: newBooking.id,
              unit_code: unit.unit_code,
              unit_name: unit.unit_name,
              status: unit.status,
              adults: unit.adults,
              children: unit.children,
              infants: unit.infants,
              viewpoint_booking_number: unit.viewpoint_booking_number,
              hold_start: unit.hold_start,
              hold_expiration: unit.hold_expiration,
              hold_source: unit.hold_source,
              note: unit.note,
              additional_data: unit.additional_data,
              booking_ext_ref_id: external_booking_source_id,
            })
            .returning(["id"])
            .executeTakeFirstOrThrow();

          if (
            (entry.entity_type === "RCI_EXCHANGE" ||
              entry.entity_type === "RCI_RENTAL") &&
              unit.external_source?.inventory_id
          ) {
            const inventory = await trx
              .selectFrom("rci_resort_unit_inventory")
              .select(["rci_resort_unit_inventory.count"])
              .where(
                "rci_resort_unit_inventory.id",
                "=",
                unit.external_source?.inventory_id!,
              )
              .executeTakeFirstOrThrow();

            await trx
              .updateTable("rci_resort_unit_inventory as inv")
              .where("inv.id", "=", unit.external_source?.inventory_id)
              .set({
                count: Math.max(0, inventory.count - 1),
              })
              .executeTakeFirstOrThrow();
          }

          for (const guest of unit.guests) {
            await trx
              .insertInto("booking_unit_guests")
              .values({
                booking_unit_id: booking_unit.id,
                member_number: guest.member_number,
                type: guest.type,
                additional_data: guest.additional_data,
              })
              .executeTakeFirstOrThrow();
          }
          for (const charge of unit.charges) {
            await trx
              .insertInto("booking_unit_charges")
              .values({
                booking_unit_id: booking_unit.id,
                type: charge.type,
                name: charge.name,
                value: charge.value,
                currency: charge.currency,
                note: charge.note,
                additional_data: charge.additional_data,
              })
              .execute();
          }
        }
      }
      await trx.commit().execute();
      return newBooking;
    } catch (err) {
      await trx.rollback().execute();
      logError(err);
      return null;
    }
  };

  const FindPaginatedBookings = async (entry: {
    entity_type: ("RESORT" | "RCI_EXCHANGE" | "RCI_RENTAL" | "EVENT")[];
    page: number;
    limit: number;
    searchTerm?: string;
  }) => {
    try {
      const { entity_type, limit, page, searchTerm } = entry;
      const offset = (page - 1) * limit;
      const baseQuery = await datastore
        .selectFrom("booking_units")
        .innerJoin("bookings", "bookings.id", "booking_units.booking_id")
        .select([
          "booking_units.id",
          "booking_units.status",
          "booking_units.adults",
          "booking_units.children",
          "booking_units.complete_source",
          "booking_units.hold_end",
          "booking_units.hold_expiration",
          "booking_units.hold_start",
          "booking_units.infants",
          "booking_units.unit_code",
          "booking_units.unit_name",
          "booking_units.completed_at",
          "booking_units.viewpoint_booking_number",
          "booking_units.check_in_date",
          "booking_units.check_out_date",
          "bookings.id as booking_id",
          "bookings.entity_type",
          "bookings.entity_id",
          "bookings.member_id",
          "bookings._is_deleted",
        ])
        .where((eb) => {
          const clause = [];
          clause.push(eb("bookings.entity_type", "in", entity_type));
          if (searchTerm) {
            const search = `%${searchTerm}%`;
            clause.push(
              eb.or([
                eb("booking_units.booking_id", "ilike", search),
                eb("booking_units.unit_name", "ilike", search),
              ]),
            );
          }
          return eb.and(clause);
        });

      const bookingUnits = await baseQuery
        .limit(limit)
        .offset(offset)
        .orderBy("bookings.created_at", "desc")
        .execute();

      const totalResult = await baseQuery
        .clearSelect()
        .select((eb) => eb.fn.countAll().as("total"))
        .executeTakeFirst();

      const total = Number(totalResult?.total ?? 0);
      const totalPages = Math.ceil(total / limit);

      const member_bookings = [];
      for (const unit of bookingUnits) {
        const destination = await datastore
          .selectFrom("booking_destinations")
          .select([
            "booking_destinations.id",
            "booking_destinations.additional_data",
            "booking_destinations.code",
            "booking_destinations.name",
          ])
          .where("booking_destinations.id", "=", unit.entity_id)
          .executeTakeFirstOrThrow();

        member_bookings.push({ ...unit, destination });
      }
      return {
        bookings: member_bookings,
        pagination: {
          page,
          limit,
          total,
          totalPages,
          hasNextPage: page < totalPages,
          hasPrevPage: page > 1,
        },
      };
    } catch (err) {
      logError(err);
      throw new DatabaseError({
        error: err,
        message: "Failed to fetch bookings",
      });
    }
  };
  return {
    CreateEntry,
    FindPaginatedBookings,
  };
};

export type TBookingRepository = ReturnType<typeof BookingRepository>;
