import { Booking, Property } from "@/db/models";
import { isBookingsMysql, isPropertiesMysql } from "@/db/activeDatabase";
import {
  getBlockedDatesMysql,
  propertyIdsBlockedOnDatesMysql,
} from "@/db/mysql/properties";
import {
  busyPropertyIdsFromBookingsMysql,
  clearCustomerPendingQuotesMysql,
  expireStaleQuotesMysql,
  findBlockingBookingMysql,
  listBookingDateRangesMysql,
} from "@/db/mysql/bookings";
import { isFinancialDualWriteEnabled } from "@/db/dualWriteFinancial";

/** Mark abandoned quotes as EXPIRED so they stop blocking calendar. */
export async function expireStaleQuotes(propertyId?: string) {
  const filter: Record<string, unknown> = {
    status: "PENDING_PAYMENT",
    deletedAt: null,
    exchangeRateExpiresAt: { $lt: new Date() },
  };
  if (propertyId) filter.propertyId = propertyId;
  // When BOOKINGS_DATABASE=mysql and dual-write is off, mongoose is not connected —
  // calling Booking.updateMany buffers ~10s then 500s property list/detail.
  if (!isBookingsMysql() || isFinancialDualWriteEnabled()) {
    await Booking.updateMany(filter, { $set: { status: "EXPIRED" } });
  }
  if (isBookingsMysql() || isFinancialDualWriteEnabled()) {
    await expireStaleQuotesMysql(propertyId);
  }
}

/**
 * Active holds that block dates:
 * - CONFIRMED / WAITING_OWNER always
 * - PENDING_PAYMENT only while the quote lock is still valid
 */
export function blockingBookingFilter(checkIn: Date, checkOut: Date, propertyId?: string) {
  const now = new Date();
  const filter: Record<string, unknown> = {
    deletedAt: null,
    checkIn: { $lt: checkOut },
    checkOut: { $gt: checkIn },
    $or: [
      { status: { $in: ["WAITING_OWNER", "CONFIRMED"] } },
      {
        status: "PENDING_PAYMENT",
        exchangeRateExpiresAt: { $gte: now },
      },
    ],
  };
  if (propertyId) filter.propertyId = propertyId;
  return filter;
}

function toIsoDay(d: Date) {
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
  const day = String(d.getUTCDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

/** Nights in [checkIn, checkOut) as YYYY-MM-DD */
export function nightsInRange(checkIn: Date, checkOut: Date) {
  const days: string[] = [];
  const cursor = new Date(
    Date.UTC(checkIn.getUTCFullYear(), checkIn.getUTCMonth(), checkIn.getUTCDate()),
  );
  const end = new Date(
    Date.UTC(checkOut.getUTCFullYear(), checkOut.getUTCMonth(), checkOut.getUTCDate()),
  );
  while (cursor < end) {
    days.push(toIsoDay(cursor));
    cursor.setUTCDate(cursor.getUTCDate() + 1);
  }
  return days;
}

export function propertyBlocksRange(
  blockedDates: string[] | undefined,
  checkIn: Date,
  checkOut: Date,
) {
  if (!blockedDates?.length) return false;
  const set = new Set(blockedDates);
  return nightsInRange(checkIn, checkOut).some((d) => set.has(d));
}

async function loadBlockedDates(propertyId: string): Promise<string[]> {
  if (isPropertiesMysql()) {
    return getBlockedDatesMysql(propertyId);
  }
  const property = await Property.findById(propertyId).select("blockedDates").lean();
  return (property?.blockedDates as string[] | undefined) || [];
}

export async function findBlockingBooking(params: {
  propertyId: string;
  checkIn: Date;
  checkOut: Date;
  excludeCustomerId?: string;
}) {
  await expireStaleQuotes(params.propertyId);
  const blockedDates = await loadBlockedDates(params.propertyId);
  if (propertyBlocksRange(blockedDates, params.checkIn, params.checkOut)) {
    return { _id: "blocked-by-owner", reason: "OWNER_BLOCKED" } as { _id: string };
  }
  if (isBookingsMysql()) {
    const row = await findBlockingBookingMysql(params);
    return row
      ? { ...row, _id: row.id, id: row.id, reason: "BOOKED" }
      : null;
  }
  const filter = blockingBookingFilter(
    params.checkIn,
    params.checkOut,
    params.propertyId,
  ) as Record<string, unknown>;
  if (params.excludeCustomerId) {
    filter.customerId = { $ne: params.excludeCustomerId };
  }
  return Booking.findOne(filter).lean();
}

export async function busyPropertyIds(checkIn: Date, checkOut: Date) {
  await expireStaleQuotes();
  const fromBookings = isBookingsMysql()
    ? await busyPropertyIdsFromBookingsMysql(checkIn, checkOut)
    : await Booking.find(blockingBookingFilter(checkIn, checkOut)).distinct("propertyId");
  const nights = nightsInRange(checkIn, checkOut);
  let fromOwnerBlocks: string[] = [];
  if (nights.length) {
    if (isPropertiesMysql()) {
      fromOwnerBlocks = await propertyIdsBlockedOnDatesMysql(nights);
    } else {
      fromOwnerBlocks = await Property.find({
        deletedAt: null,
        blockedDates: { $in: nights },
      }).distinct("_id");
    }
  }
  return [...new Set([...fromBookings, ...fromOwnerBlocks])];
}

/** Drop the customer's own abandoned/active quotes for this property so they can re-book. */
export async function clearCustomerPendingQuotes(customerId: string, propertyId: string) {
  if (!isBookingsMysql() || isFinancialDualWriteEnabled()) {
    await Booking.updateMany(
      {
        customerId,
        propertyId,
        status: "PENDING_PAYMENT",
        deletedAt: null,
      },
      { $set: { status: "EXPIRED" } },
    );
  }
  if (isBookingsMysql() || isFinancialDualWriteEnabled()) {
    await clearCustomerPendingQuotesMysql(customerId, propertyId);
  }
}

/**
 * Calendar nights that travelers cannot book: owner blocks + nights covered by
 * active holds (CONFIRMED / WAITING_OWNER / unexpired PENDING_PAYMENT).
 */
export async function unavailableDatesForProperty(
  propertyId: string,
  opts?: { from?: Date; monthsAhead?: number },
) {
  await expireStaleQuotes(propertyId);
  const from = opts?.from
    ? new Date(Date.UTC(opts.from.getUTCFullYear(), opts.from.getUTCMonth(), opts.from.getUTCDate()))
    : new Date(Date.UTC(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate()));
  const monthsAhead = opts?.monthsAhead ?? 14;
  const to = new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + monthsAhead, from.getUTCDate()));

  const blockedDates = await loadBlockedDates(propertyId);
  const set = new Set<string>();
  for (const d of blockedDates) {
    if (d >= toIsoDay(from) && d < toIsoDay(to)) set.add(d);
  }

  const bookings = isBookingsMysql()
    ? await listBookingDateRangesMysql(propertyId, from, to)
    : await Booking.find({
        ...blockingBookingFilter(from, to, propertyId),
      })
        .select("checkIn checkOut")
        .lean();

  for (const b of bookings) {
    for (const night of nightsInRange(b.checkIn, b.checkOut)) {
      if (night >= toIsoDay(from) && night < toIsoDay(to)) set.add(night);
    }
  }

  return [...set].sort();
}
