import { w as withReporting, l as logger, B as BookingStatus, p as prisma } from "./orgDomains-eTg-cjth.js";
import { g as getBusyCalendarTimes } from "./CalendarManager-CJCRPKCp.js";
import dayjs from "dayjs";
import { s as stringToDayjs } from "./index-ruwhZ-bu.js";
import { i as intervalLimitKeyToUnit } from "./isBookingLimits-4PD5exXf.js";
import { e as getPiiFreeBooking } from "./piiFreeData-dxrPDUBm.js";
import "./perfObserver-y9pC1HYV.js";
import { performance } from "perf_hooks";
function getAdjustedTimezone(date, timeZone, travelSchedules) {
  let adjustedTimezone = timeZone;
  for (const travelSchedule of travelSchedules) {
    if (!date.isBefore(travelSchedule.startDate) && (!travelSchedule.endDate || !date.isAfter(travelSchedule.endDate))) {
      adjustedTimezone = travelSchedule.timeZone;
      break;
    }
  }
  return adjustedTimezone;
}
function processWorkingHours(results, {
  item,
  timeZone,
  dateFrom,
  dateTo,
  travelSchedules
}) {
  const utcDateTo = dateTo.utc();
  let endTimeToKeyMap;
  for (let date = dateFrom.startOf("day"); utcDateTo.isAfter(date); date = date.add(1, "day")) {
    const fromOffset = dateFrom.startOf("day").utcOffset();
    const adjustedTimezone = getAdjustedTimezone(date, timeZone, travelSchedules);
    const offset = date.tz(adjustedTimezone).utcOffset();
    const dateInTz = date.add(fromOffset - offset, "minutes").tz(adjustedTimezone);
    if (!item.days.includes(dateInTz.day())) {
      continue;
    }
    let start = dateInTz.add(item.startTime.getUTCHours(), "hours").add(item.startTime.getUTCMinutes(), "minutes");
    let end = dateInTz.add(item.endTime.getUTCHours(), "hours").add(item.endTime.getUTCMinutes(), "minutes");
    const offsetBeginningOfDay = dayjs(start.format("YYYY-MM-DD hh:mm")).tz(adjustedTimezone).utcOffset();
    const offsetDiff = start.utcOffset() - offsetBeginningOfDay;
    start = start.add(offsetDiff, "minute");
    end = end.add(offsetDiff, "minute");
    const startResult = dayjs.max(start, dateFrom);
    let endResult = dayjs.min(end, dateTo.tz(adjustedTimezone));
    if (endResult.hour() === 23 && endResult.minute() === 59) {
      endResult = endResult.add(1, "minute");
    }
    if (endResult.isBefore(startResult)) {
      continue;
    }
    const endTimeKey = endResult.valueOf();
    if (!endTimeToKeyMap) {
      endTimeToKeyMap = /* @__PURE__ */ new Map();
      for (const [key, range] of Object.entries(results)) {
        const endTime = range.end.valueOf();
        if (!endTimeToKeyMap.has(endTime)) {
          endTimeToKeyMap.set(endTime, []);
        }
        endTimeToKeyMap.get(endTime).push(Number(key));
      }
    }
    const keysWithSameEndTime = endTimeToKeyMap.get(endTimeKey) || [];
    let foundOverlapping = false;
    for (const key of keysWithSameEndTime) {
      const existingRange = results[key];
      if (startResult.valueOf() <= existingRange.end.valueOf() && endResult.valueOf() >= existingRange.start.valueOf()) {
        results[key] = {
          start: dayjs.min(existingRange.start, startResult),
          end: endResult
        };
        foundOverlapping = true;
        break;
      }
    }
    if (foundOverlapping) {
      continue;
    }
    if (results[startResult.valueOf()]) {
      const oldKey = startResult.valueOf();
      const newKey2 = endResult.valueOf();
      results[newKey2] = {
        start: results[oldKey].start,
        end: dayjs.max(results[oldKey].end, endResult)
      };
      if (endTimeToKeyMap) {
        const oldEndTime = results[oldKey].end.valueOf();
        const oldKeys = endTimeToKeyMap.get(oldEndTime) || [];
        const filteredKeys = oldKeys.filter((k) => k !== oldKey);
        if (filteredKeys.length === 0) {
          endTimeToKeyMap.delete(oldEndTime);
        } else {
          endTimeToKeyMap.set(oldEndTime, filteredKeys);
        }
        if (!endTimeToKeyMap.has(endTimeKey)) {
          endTimeToKeyMap.set(endTimeKey, []);
        }
        endTimeToKeyMap.get(endTimeKey).push(newKey2);
      }
      delete results[oldKey];
      continue;
    }
    const newKey = endResult.valueOf();
    results[newKey] = {
      start: startResult,
      end: endResult
    };
    if (endTimeToKeyMap) {
      if (!endTimeToKeyMap.has(endTimeKey)) {
        endTimeToKeyMap.set(endTimeKey, []);
      }
      endTimeToKeyMap.get(endTimeKey).push(newKey);
    }
  }
  return results;
}
function processDateOverride({
  item,
  itemDateAsUtc,
  timeZone,
  travelSchedules
}) {
  const overrideDate = dayjs(item.date);
  const adjustedTimezone = getAdjustedTimezone(overrideDate, timeZone, travelSchedules);
  const itemDateStartOfDay = itemDateAsUtc.startOf("day");
  const startDate = itemDateStartOfDay.add(item.startTime.getUTCHours(), "hours").add(item.startTime.getUTCMinutes(), "minutes").second(0).tz(adjustedTimezone, true);
  let endDate = itemDateStartOfDay;
  const endTimeHours = item.endTime.getUTCHours();
  const endTimeMinutes = item.endTime.getUTCMinutes();
  if (endTimeHours === 23 && endTimeMinutes === 59) {
    endDate = endDate.add(1, "day").tz(timeZone, true);
  } else {
    endDate = itemDateStartOfDay.add(endTimeHours, "hours").add(endTimeMinutes, "minutes").second(0).tz(adjustedTimezone, true);
  }
  return {
    start: startDate,
    end: endDate
  };
}
function processOOO(outOfOffice, timeZone) {
  const OOOdate = outOfOffice.tz(timeZone, true);
  return {
    start: OOOdate,
    end: OOOdate
  };
}
function buildDateRanges({
  availability,
  timeZone,
  dateFrom,
  dateTo,
  travelSchedules,
  outOfOffice
}) {
  const dateFromOrganizerTZ = dateFrom.tz(timeZone);
  const groupedWorkingHours = groupByDate(
    Object.values(
      availability.reduce((processed, item) => {
        if (!("days" in item)) {
          return processed;
        }
        processed = processWorkingHours(processed, {
          item,
          timeZone,
          dateFrom: dateFromOrganizerTZ,
          dateTo,
          travelSchedules
        });
        return processed;
      }, {})
    )
  );
  const groupedOOO = groupByDate(
    outOfOffice ? Object.keys(outOfOffice).map((outOfOffice2) => processOOO(dayjs.utc(outOfOffice2), timeZone)) : []
  );
  const groupedDateOverrides = groupByDate(
    Object.values(
      availability.reduce((processed, item) => {
        if (!("date" in item && !!item.date)) {
          return processed;
        }
        const itemDateAsUtc = dayjs.utc(item.date);
        if (itemDateAsUtc.isBetween(
          dateFrom.subtract(1, "day").startOf("day"),
          dateTo.add(1, "day").endOf("day"),
          null,
          "[]"
        )) {
          const newProcessedDateOverride = processDateOverride({
            item,
            itemDateAsUtc,
            timeZone,
            travelSchedules
          });
          if (processed[newProcessedDateOverride.start.valueOf()]) {
            processed[newProcessedDateOverride.start.valueOf()].end = dayjs.max(
              processed[newProcessedDateOverride.start.valueOf()].end,
              newProcessedDateOverride.end
            );
            return processed;
          }
          processed[newProcessedDateOverride.end.valueOf()] = newProcessedDateOverride;
        }
        return processed;
      }, {})
    )
  );
  const dateRanges = Object.values({
    ...groupedWorkingHours,
    ...groupedDateOverrides
  }).map(
    // remove 0-length overrides that were kept to cancel out working dates until now.
    (ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
  );
  const oooExcludedDateRanges = Object.values({
    ...groupedWorkingHours,
    ...groupedDateOverrides,
    ...groupedOOO
  }).map(
    // remove 0-length overrides && OOO dates that were kept to cancel out working dates until now.
    (ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
  );
  return { dateRanges: dateRanges.flat(), oooExcludedDateRanges: oooExcludedDateRanges.flat() };
}
function groupByDate(ranges) {
  const results = ranges.reduce(
    (previousValue, currentValue) => {
      const dateString = dayjs(currentValue.start).format("YYYY-MM-DD");
      previousValue[dateString] = typeof previousValue[dateString] === "undefined" ? [currentValue] : [...previousValue[dateString], currentValue];
      return previousValue;
    },
    {}
  );
  return results;
}
function intersect(ranges) {
  if (!ranges.length) {
    return [];
  }
  const sortedRanges = ranges.map(
    (userRanges) => userRanges.map((r) => ({
      ...r,
      startValue: r.start.valueOf(),
      endValue: r.end.valueOf()
    })).sort((a, b) => a.startValue - b.startValue)
  );
  let commonAvailability = sortedRanges[0];
  for (let i = 1; i < sortedRanges.length; i++) {
    if (commonAvailability.length === 0) {
      return [];
    }
    const userRanges = sortedRanges[i];
    const intersectedRanges = [];
    let commonIndex = 0;
    let userIndex = 0;
    while (commonIndex < commonAvailability.length && userIndex < userRanges.length) {
      const commonRange = commonAvailability[commonIndex];
      const userRange = userRanges[userIndex];
      const intersectStartValue = Math.max(commonRange.startValue, userRange.startValue);
      const intersectEndValue = Math.min(commonRange.endValue, userRange.endValue);
      if (intersectStartValue < intersectEndValue) {
        const intersectStart = commonRange.startValue > userRange.startValue ? commonRange.start : userRange.start;
        const intersectEnd = commonRange.endValue < userRange.endValue ? commonRange.end : userRange.end;
        intersectedRanges.push({
          start: intersectStart,
          end: intersectEnd,
          startValue: intersectStartValue,
          endValue: intersectEndValue
        });
      }
      if (commonRange.endValue <= userRange.endValue) {
        commonIndex++;
      } else {
        userIndex++;
      }
    }
    commonAvailability = intersectedRanges;
  }
  return commonAvailability.map(({ start, end }) => ({ start, end }));
}
function subtract(sourceRanges, excludedRanges) {
  const result = [];
  const sortedExcludedRanges = [...excludedRanges].sort((a, b) => a.start.valueOf() - b.start.valueOf());
  for (const { start: sourceStart, end: sourceEnd, ...passThrough } of sourceRanges) {
    let currentStart = sourceStart;
    for (const excludedRange of sortedExcludedRanges) {
      if (excludedRange.start.valueOf() >= sourceEnd.valueOf())
        break;
      if (excludedRange.end.valueOf() <= currentStart.valueOf())
        continue;
      if (excludedRange.start.valueOf() > currentStart.valueOf()) {
        result.push({ start: currentStart, end: excludedRange.start, ...passThrough });
      }
      if (excludedRange.end.valueOf() > currentStart.valueOf()) {
        currentStart = excludedRange.end;
      }
    }
    if (sourceEnd.valueOf() > currentStart.valueOf()) {
      result.push({ start: currentStart, end: sourceEnd, ...passThrough });
    }
  }
  return result;
}
function mergeOverlappingRanges(ranges) {
  if (ranges.length === 0)
    return [];
  const sortedRanges = ranges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
  const mergedRanges = [sortedRanges[0]];
  for (let i = 1; i < sortedRanges.length; i++) {
    const lastMergedRange = mergedRanges[mergedRanges.length - 1];
    const currentRange = sortedRanges[i];
    if (currentRange.start.getTime() <= lastMergedRange.end.getTime()) {
      lastMergedRange.end = new Date(Math.max(lastMergedRange.end.getTime(), currentRange.end.getTime()));
    } else {
      mergedRanges.push(currentRange);
    }
  }
  return mergedRanges;
}
const getDefinedBufferTimes = () => {
  return [5, 10, 15, 20, 30, 45, 60, 90, 120];
};
class BusyTimesService {
  constructor(dependencies) {
    this.dependencies = dependencies;
    this.getBusyTimes = withReporting(this._getBusyTimes.bind(this), "getBusyTimes");
  }
  async _getBusyTimes(params) {
    const {
      credentials,
      userId,
      userEmail,
      username,
      eventTypeId,
      startTime,
      endTime,
      beforeEventBuffer,
      afterEventBuffer,
      selectedCalendars,
      seatedEvent,
      rescheduleUid,
      duration,
      bypassBusyCalendarTimes = false,
      shouldServeCache
    } = params;
    logger.silly(
      `Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
        userId,
        eventTypeId,
        status: BookingStatus.ACCEPTED
      })}`
    );
    performance.mark("prismaBookingGetStart");
    const startTimeDate = rescheduleUid && duration ? dayjs(startTime).subtract(duration, "minute").toDate() : new Date(startTime);
    const endTimeDate = rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
    const definedBufferTimes = getDefinedBufferTimes();
    const maxBuffer = definedBufferTimes[definedBufferTimes.length - 1];
    const startTimeAdjustedWithMaxBuffer = dayjs(startTimeDate).subtract(maxBuffer, "minute").toDate();
    const endTimeAdjustedWithMaxBuffer = dayjs(endTimeDate).add(maxBuffer, "minute").toDate();
    let bookings = params.currentBookings;
    if (!bookings) {
      const bookingRepo = this.dependencies.bookingRepo;
      bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
        userIdAndEmailMap: /* @__PURE__ */ new Map([[userId, userEmail]]),
        eventTypeId,
        startDate: startTimeAdjustedWithMaxBuffer,
        endDate: endTimeAdjustedWithMaxBuffer,
        seatedEvent
      });
    }
    const bookingSeatCountMap = {};
    const busyTimes = bookings.reduce((aggregate, booking) => {
      const { id, startTime: startTime2, endTime: endTime2, eventType, title, ...rest } = booking;
      const minutesToBlockBeforeEvent = (eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0);
      const minutesToBlockAfterEvent = (eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0);
      if (rest._count?.seatsReferences) {
        const bookedAt = `${dayjs(startTime2).utc().format()}<>${dayjs(endTime2).utc().format()}`;
        bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0;
        bookingSeatCountMap[bookedAt]++;
        if (
          // there are still seats available.
          bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && // and this is the seated event, other event types should be blocked.
          eventTypeId === eventType?.id
        ) {
          if (minutesToBlockBeforeEvent) {
            aggregate.push({
              start: dayjs(startTime2).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
              end: dayjs(startTime2).toDate()
              // The event starts after the buffer
            });
          }
          if (minutesToBlockAfterEvent) {
            aggregate.push({
              start: dayjs(endTime2).toDate(),
              // The event ends before the buffer
              end: dayjs(endTime2).add(minutesToBlockAfterEvent, "minute").toDate()
            });
          }
          return aggregate;
        }
        delete bookingSeatCountMap[bookedAt];
      }
      if (rest.uid === rescheduleUid) {
        return aggregate;
      }
      aggregate.push({
        start: dayjs(startTime2).subtract(minutesToBlockBeforeEvent, "minute").toDate(),
        end: dayjs(endTime2).add(minutesToBlockAfterEvent, "minute").toDate(),
        title,
        source: `eventType-${eventType?.id}-booking-${id}`
      });
      return aggregate;
    }, []);
    logger.debug(
      `Busy Time from Cal Bookings ${JSON.stringify({
        busyTimes,
        bookings: bookings?.map((booking) => getPiiFreeBooking(booking)),
        numCredentials: credentials?.length
      })}`
    );
    performance.mark("prismaBookingGetEnd");
    performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
    if (credentials?.length > 0 && !bypassBusyCalendarTimes) {
      const startConnectedCalendarsGet = performance.now();
      const calendarBusyTimesQuery = await getBusyCalendarTimes(
        credentials,
        startTime,
        endTime,
        selectedCalendars,
        shouldServeCache
      );
      if (!calendarBusyTimesQuery.success) {
        throw new Error(
          `Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
            (calendar) => calendar.id
          )}`
        );
      }
      const calendarBusyTimes = calendarBusyTimesQuery.data;
      const endConnectedCalendarsGet = performance.now();
      logger.debug(
        `Connected Calendars get took ${endConnectedCalendarsGet - startConnectedCalendarsGet} ms for user ${username}`,
        JSON.stringify({
          eventTypeId,
          startTimeDate,
          endTimeDate,
          calendarBusyTimes
        })
      );
      const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
        const [start, end] = key.split("<>");
        return {
          start: dayjs(start),
          end: dayjs(end)
        };
      });
      if (rescheduleUid) {
        const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
        if (originalRescheduleBooking) {
          openSeatsDateRanges.push({
            start: dayjs(originalRescheduleBooking.startTime),
            end: dayjs(originalRescheduleBooking.endTime)
          });
        }
      }
      const result = subtract(
        calendarBusyTimes.map((value) => ({
          ...value,
          end: dayjs(value.end),
          start: dayjs(value.start)
        })),
        openSeatsDateRanges
      );
      busyTimes.push(
        ...result.map((busyTime) => ({
          ...busyTime,
          start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
          end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate()
        }))
      );
    } else {
      logger.warn(`No credentials found for user ${userId}`, {
        selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id)
      });
    }
    logger.debug(
      "getBusyTimes:",
      JSON.stringify({
        allBusyTimes: busyTimes
      })
    );
    return busyTimes;
  }
  getStartEndDateforLimitCheck(startDate, endDate, bookingLimits, durationLimits) {
    const startTimeAsDayJs = stringToDayjs(startDate);
    const endTimeAsDayJs = stringToDayjs(endDate);
    let limitDateFrom = stringToDayjs(startDate);
    let limitDateTo = stringToDayjs(endDate);
    for (const key of ["PER_MONTH", "PER_WEEK", "PER_DAY"]) {
      if (bookingLimits?.[key] || durationLimits?.[key]) {
        const unit = intervalLimitKeyToUnit(key);
        limitDateFrom = dayjs.min(limitDateFrom, startTimeAsDayJs.startOf(unit));
        limitDateTo = dayjs.max(limitDateTo, endTimeAsDayJs.endOf(unit));
      }
    }
    return { limitDateFrom, limitDateTo };
  }
  async getBusyTimesForLimitChecks(params) {
    const { userIds, eventTypeId, startDate, endDate, rescheduleUid, bookingLimits, durationLimits } = params;
    performance.mark("getBusyTimesForLimitChecksStart");
    let busyTimes = [];
    if (!bookingLimits && !durationLimits) {
      return busyTimes;
    }
    const { limitDateFrom, limitDateTo } = this.getStartEndDateforLimitCheck(
      startDate,
      endDate,
      bookingLimits,
      durationLimits
    );
    logger.silly(
      `Fetch limit checks bookings in range ${limitDateFrom} to ${limitDateTo} for input ${JSON.stringify({
        eventTypeId,
        status: BookingStatus.ACCEPTED
      })}`
    );
    const where = {
      userId: {
        in: userIds
      },
      eventTypeId,
      status: BookingStatus.ACCEPTED,
      // FIXME: bookings that overlap on one side will never be counted
      startTime: {
        gte: limitDateFrom.toDate()
      },
      endTime: {
        lte: limitDateTo.toDate()
      }
    };
    if (rescheduleUid) {
      where.NOT = {
        uid: rescheduleUid
      };
    }
    const bookings = await prisma.booking.findMany({
      where,
      select: {
        id: true,
        startTime: true,
        endTime: true,
        eventType: {
          select: {
            id: true
          }
        },
        title: true,
        userId: true
      }
    });
    busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
      start: dayjs(startTime).toDate(),
      end: dayjs(endTime).toDate(),
      title,
      source: `eventType-${eventType?.id}-booking-${id}`,
      userId
    }));
    logger.silly(`Fetch limit checks bookings for eventId: ${eventTypeId} ${JSON.stringify(busyTimes)}`);
    performance.mark("getBusyTimesForLimitChecksEnd");
    performance.measure(
      `prisma booking get for limits took $1'`,
      "getBusyTimesForLimitChecksStart",
      "getBusyTimesForLimitChecksEnd"
    );
    return busyTimes;
  }
}
class LimitManager {
  constructor() {
    this.busyMap = /* @__PURE__ */ new Map();
  }
  /**
   * Creates a busy map key
   */
  static createKey(start, unit, timeZone) {
    const tzStart = timeZone ? start.tz(timeZone) : start;
    return `${unit}-${tzStart.startOf(unit).toISOString()}`;
  }
  /**
   * Checks if already marked busy by ancestors or siblings
   */
  isAlreadyBusy(start, unit, timeZone) {
    if (this.busyMap.has(LimitManager.createKey(start, "year", timeZone)))
      return true;
    if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month", timeZone))) {
      return true;
    } else if (unit === "week" && // weeks can be part of two months
    (this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) && this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month", timeZone)) || this.busyMap.has(LimitManager.createKey(start, "week", timeZone)))) {
      return true;
    } else if (unit === "day" && (this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) || this.busyMap.has(LimitManager.createKey(start, "week", timeZone)) || this.busyMap.has(LimitManager.createKey(start, "day", timeZone)))) {
      return true;
    } else {
      return false;
    }
  }
  /**
   * Adds a new busy time
   */
  addBusyTime(start, unit, timeZone) {
    const tzStart = timeZone ? start.tz(timeZone) : start;
    this.busyMap.set(`${unit}-${tzStart.toISOString()}`, {
      start: tzStart.toISOString(),
      end: tzStart.endOf(unit).toISOString()
    });
  }
  /**
   * Merges busy times from another LimitManager
   */
  mergeBusyTimes(otherManager) {
    otherManager.busyMap.forEach((busyTime, key) => {
      if (!this.busyMap.has(key)) {
        this.busyMap.set(key, busyTime);
      }
    });
  }
  /**
   * Returns all busy times
   */
  getBusyTimes() {
    return Array.from(this.busyMap.values());
  }
}
function extractDateParameters(booking, periodStart, periodEnd, timeZone) {
  const bookingStart = dayjs(booking.start).tz(timeZone);
  const bookingDay = bookingStart.format("YYYY-MM-DD");
  const periodStartDay = periodStart.format("YYYY-MM-DD");
  const periodEndDay = periodEnd.format("YYYY-MM-DD");
  return {
    bookingStart,
    bookingDay,
    periodStartDay,
    periodEndDay
  };
}
function isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone) {
  const { bookingDay, periodStartDay, periodEndDay } = extractDateParameters(
    booking,
    periodStart,
    periodEnd,
    timeZone
  );
  return !(bookingDay < periodStartDay || bookingDay > periodEndDay);
}
export {
  BusyTimesService as B,
  LimitManager as L,
  isBookingWithinPeriod as a,
  buildDateRanges as b,
  intersect as i,
  mergeOverlappingRanges as m,
  subtract as s
};
