<?php

namespace FluentBooking\App\Services;

use FluentBooking\App\Models\Booking;
use FluentBooking\App\Models\Calendar;
use FluentBooking\App\Models\CalendarSlot;
use FluentBooking\Framework\Support\Arr;
use FluentBooking\Framework\Support\DateTime;

class TimeSlotService
{
    protected $calendarSlot;

    protected $calendar;

    protected $hostId = null;
    
    public function __construct(Calendar $calendar, CalendarSlot $calendarSlot)
    {
        $this->hostId = null;
        $this->calendar = $calendar;
        $this->calendarSlot = $calendarSlot;
    }

    public function getDates($fromDate = false, $toDate = false, $duration = null, $isDoingBooking = false, $timeZone = 'UTC')
    {
        $duration = $this->calendarSlot->getDuration($duration);

        $fromDate = $fromDate ?: gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $toDate = $toDate ?: gmdate('Y-m-t 23:59:59', strtotime($fromDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        $ranges = $this->getCurrentDateRange($fromDate, $toDate);

        $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking);

        $ranges = $this->maybeBookingFrequencyLimitRanges($ranges);
        $ranges = $this->maybeBookingDurationLimitRanges($ranges, $duration);

        $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds();

        $maxBookingTime = $this->getMaxBookingTimestamp($fromDate, $toDate, $timeZone);

        $timezoneInfo = $this->getTimezoneInfo();

        $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo);

        $rangedSlots = $this->maybeBookingPerDayLimitSlots($rangedSlots, $bookedSlots, $duration);

        return $rangedSlots;
    }

    protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null)
    {
        $period = $duration * 60;

        $hostId = $hostId ?: $this->hostId;

        $bufferTime = $this->calendarSlot->getTotalBufferTime() * 60;

        $daySlots = $this->getWeekDaySlots($duration, $hostId);

        $dateOverrides = $this->calendarSlot->getDateOverrides($hostId);

        list($scheduleTimezone, $dstTime) = $timezoneInfo;

        $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        $lastDate = end($ranges);

        foreach ($ranges as $date) {
            $day = strtolower(gmdate('D', strtotime($date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

            $availableSlots = $daySlots[$day] ?? [];

            $availableSlots = $this->maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration);

            if (!$availableSlots) {
                continue;
            }

            $isToday = $date === $todayDate;

            $isLastDay = $date === $lastDate;

            $validSlots = [];

            $validDstDateSlots = [];

            foreach ($availableSlots as $start) {
                $end = gmdate('H:i', strtotime($start) + $period); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $endDate = $start < $end ? $date : gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

                $slot = [
                    'start' => $date . ' ' . $start . ':00',
                    'end'   => $endDate . ' ' . $end . ':00'
                ];

                $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone);

                $slotDate = gmdate('Y-m-d', strtotime($slot['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

                $currentBookedSlots = $bookedSlots[$slotDate] ?? [];

                if ($isToday && strtotime($slot['start']) < $cutOutTime) {
                    continue;
                }

                if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) {
                    continue;
                }

                $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId);

                if ($isSlotAvailable) {
                    if ($slotDate != $date) {
                        $validDstDateSlots[] = $slot;
                    } else {
                        $validSlots[] = $slot;
                    }
                }
            }

            if ($validSlots) {
                $currentSlots = $rangedSlots[$date] ?? [];
                $rangedSlots[$date] = $this->mergeAndSortSlots($currentSlots, $validSlots);
            }

            if ($validDstDateSlots) {
                $slotDate = gmdate('Y-m-d', strtotime($validDstDateSlots[0]['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $currentSlots = $rangedSlots[$slotDate] ?? [];
                $rangedSlots[$slotDate] = $this->mergeAndSortSlots($currentSlots, $validDstDateSlots);
            }
        }

        return $rangedSlots;
    }

    protected function isSlotAvailable(&$slot, $currentBookedSlots, $bufferTime, $hostId)
    {
        if (!$currentBookedSlots) {
            return true;
        }

        $startTimeStamp = strtotime($slot['start']);
        $endTimeStamp = strtotime($slot['end']);

        foreach ($currentBookedSlots as $bookedSlot) {
            $bookedStart = strtotime($bookedSlot['start']);
            $bookedEnd = strtotime($bookedSlot['end']);

            if (Arr::get($bookedSlot, 'source')) {
                $bookedStart = $bookedStart - $bufferTime;
                $bookedEnd = $bookedEnd + $bufferTime;
            }

            if (
                ($startTimeStamp >= $bookedStart && $startTimeStamp < $bookedEnd) ||
                ($endTimeStamp > $bookedStart && $endTimeStamp <= $bookedEnd) ||
                ($startTimeStamp <= $bookedStart && $endTimeStamp > $bookedStart) ||
                ($startTimeStamp < $bookedEnd && $endTimeStamp >= $bookedEnd)
            ) {
                if (!Arr::get($bookedSlot, 'remaining')) {
                    return false;
                }
                $slot['remaining'] = $bookedSlot['remaining'];
            }
        }

        return true;
    }

    public function isSpotAvailable($fromTime, $toTime, $duration = null, $hostId = null)
    {
        $this->hostId = $hostId;

        $fromTimeStamp = strtotime($fromTime);
        $toTimeStamp = strtotime($toTime);

        $duration = $this->calendarSlot->getDuration($duration);

        list($scheduleTimezone, $dstTime) = $this->getTimezoneInfo();

        $fromStartTime = $this->maybeDayLightSavingTime($fromTime, $dstTime, $scheduleTimezone);
        $toEndTime = $this->maybeDayLightSavingTime($toTime, $dstTime, $scheduleTimezone);

        $fromTime = gmdate('Y-m-d 00:00:00', strtotime($fromStartTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $toTime = gmdate('Y-m-d 23:59:59', strtotime($toEndTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        $slots = $this->getDates($fromTime, $toTime, $duration, true);

        $fromDate = gmdate('Y-m-d', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $toDate = gmdate('Y-m-d', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        $availableSlots = $slots[$fromDate] ?? [];

        if ($fromDate != $toDate) {
            $availableSlots = array_merge($availableSlots, $slots[$toDate] ?? []);
        }

        return $this->isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp);
    }

    protected function isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp)
    {
        $left = 0;
        $right = count($availableSlots) - 1;

        while ($left <= $right) {
            $mid = $left + (($right - $left) >> 1);
            $slot = $availableSlots[$mid];

            $slotStartTime = strtotime($slot['start']);
            $slotEndTime = strtotime($slot['end']);

            if ($fromTimeStamp == $slotStartTime && $toTimeStamp == $slotEndTime) {
                return $slot;
            } elseif ($fromTimeStamp > $slotStartTime) {
                $left = $mid + 1;
            } else {
                $right = $mid - 1;
            }
        }

        return false;
    }

    protected function getCurrentDateRange($startDate = false, $endDate = false)
    {
        if (!$startDate) {
            $startDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        }

        if (!$endDate) {
            $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        }

        $currentDate = strtotime($startDate);
        $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59
        $oneDay = 24 * 60 * 60;

        $dateArray = [];

        while ($currentDate <= $endDate) {
            $dateArray[] = gmdate('Y-m-d', $currentDate); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
            $currentDate += $oneDay;
        }

        return $dateArray;
    }

    protected function bookSlot($eventId, $start, $end, $remaining = 0, $source = null)
    {
        return [
            'event_id'  => $eventId,
            'start'     => $start,
            'end'       => $end,
            'remaining' => $remaining,
            'source'    => $source
        ];
    }

    protected function getBookedSlots($dateRange, $toTimeZone = 'UTC', $isDoingBooking = false)
    {
        if ($toTimeZone != 'UTC') {
            $dateRange[0] = DateTimeHelper::convertToUtc($dateRange[0], $toTimeZone);
            $dateRange[1] = DateTimeHelper::convertToUtc($dateRange[1], $toTimeZone);
        }

        $hostIds = $this->calendarSlot->getHostIds($this->hostId);
        $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];

        $bookings = Booking::with(['calendar_event'])
            ->whereHas('hosts', function ($query) use ($hostIds) {
                $query->whereIn('user_id', $hostIds);
            })
            ->where(function ($query) use ($dateRange) {
                $query->whereBetween('start_time', $dateRange)
                      ->orWhereBetween('end_time', $dateRange);
            })
            ->orderBy('start_time', 'ASC')
            ->whereIn('status', $status)
            ->get()
            ->groupBy('group_id');

        $maxBooking = $this->calendarSlot->getMaxBookingPerSlot();

        $isGroupBooking = $maxBooking > 1;

        $books = $this->processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking);

        $books = apply_filters('fluent_booking/local_booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);

        $remoteBookings = apply_filters('fluent_booking/remote_booked_events', [], $this->calendarSlot, $toTimeZone, $dateRange, $this->hostId, $isDoingBooking);

        $books = $this->processRemoteBookings($books, $remoteBookings);

        return apply_filters('fluent_booking/booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);
    }

    protected function processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking)
    {
        $books = [];
        foreach ($bookings as $booking) {
            $booked = $booking->count();
            $booking = $booking[0];

            $remaining = 0;
            if ($this->calendarSlot->id == $booking->event_id) {
                if ($booking->status == 'reserved') {
                    continue;
                }
                $remaining = max(0, $maxBooking - $booked);
            }

            if ($toTimeZone != 'UTC') {
                $booking->start_time = DateTimeHelper::convertToTimeZone($booking->start_time, 'UTC', $toTimeZone);
                $booking->end_time = DateTimeHelper::convertToTimeZone($booking->end_time, 'UTC', $toTimeZone);
            }

            $date = gmdate('Y-m-d', strtotime($booking->start_time)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

            $books[$date] = $books[$date] ?? [];

            $bufferTime = $booking->calendar_event->getTotalBufferTime();
            if ($bufferTime) {
                $beforeBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->start_time . " -$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $afterBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->end_time . " +$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                if ($remaining) {
                    if ($beforeBufferTime < $booking->start_time) {
                        $books[$date][] = $this->bookSlot(null, $beforeBufferTime, $booking->start_time);
                    }
                    if ($afterBufferTime > $booking->end_time) {
                        $books[$date][] = $this->bookSlot(null, $booking->end_time, $afterBufferTime);
                    }
                } else {
                    $booking->start_time = $beforeBufferTime;
                    $booking->end_time = $afterBufferTime;
                }
            }

            $rangedItems = $this->createDateRangeArrayFromSlotConfig([
                'event_id'  => $booking->event_id,
                'start'     => $booking->start_time,
                'end'       => $booking->end_time,
                'remaining' => $remaining
            ]);

            $eventIdAdded = false;
            foreach ($rangedItems as $date => $slot) {
                if ($isGroupBooking && $remaining && $this->calendarSlot->id == $booking->event_id) {
                    if ($eventIdAdded) {
                        $slot['event_id'] = null;
                    }
                    $eventIdAdded = true;
                }
                $books[$date] = $books[$date] ?? [];
                $books[$date][] = $slot;
            }
        }

        return $books;
    }

    protected function processRemoteBookings($books, $remoteBookings)
    {
        if (!$remoteBookings) {
            return $books;
        }

        foreach ($remoteBookings as $slot) {
            $rangedItems = $this->createDateRangeArrayFromSlotConfig([
                'start'   => $slot['start'],
                'end'     => $slot['end'],
                'source'  => $slot['source']
            ]);

            foreach ($rangedItems as $rangedDate => $rangedSlot) {
                $books[$rangedDate] = $books[$rangedDate] ?? [];
            
                if (!$this->isLocalBooking($books[$rangedDate], $rangedSlot)) {
                    $books[$rangedDate][] = $rangedSlot;
                }
            }
        }
        return $books;
    }

    protected function getWeekDaySlots($duration, $hostId = null)
    {
        $period = $duration * 60;

        $hostId = $hostId ?: $this->hostId;

        $interval = $this->calendarSlot->getSlotInterval($duration) * 60;

        $weeklySlots = $this->calendarSlot->getWeeklySlots($hostId);

        $items = $this->getEnabledSlots($weeklySlots);

        // create range of each day slots from $items array above with $period minutes interval
        $formattedSlots = [];
        $days = array_keys($items);
        foreach ($items as $day => &$slots) {
            $daySlots = [];
            foreach ($slots as $slot) {
                $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
                $start = strtotime($slot['start']);
                $end = strtotime($slot['end']);

                while ($start + $period <= $end) {
                    $daySlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $start += $interval;
                }

                if ($slot['end'] == '24:00' && $start < $end) {
                    $daySlots = $this->handleNextDaySlot($daySlots, $items, $start, $end, $interval, $period, $day, $days);
                }
            }
            if ($daySlots) {
                $formattedSlots[$day] = $daySlots;
            }
        }

        return $formattedSlots;
    }

    private function getEnabledSlots($weeklySlots)
    {
        $items = [];

        foreach ($weeklySlots as $weekDay => $weeklySlot) {
            if ($weeklySlot['enabled'] || !empty($weeklySlot['slots'])) {
                $items[$weekDay] = $weeklySlot['slots'];
            }
        }

        return $items;
    }

    protected function handleNextDaySlot($daySlots, &$items, $start, $end, $interval, $period, $day, $days)
    {
        $nextDayIndex = (array_search($day, $days) + 1) % count($items);

        if (isset($days[$nextDayIndex])) {
            $nextDay = $items[$days[$nextDayIndex]];

            if ($nextDay && $nextDay[0]['start'] == '00:00') {
                $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
                $reserveTime = $end - $start;

                while ($period - $reserveTime <= $nextDayEndTime) {
                    $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $daySlots[] = $startTime;

                    if ($nextDayStart < $startTime) {
                        $items[$days[$nextDayIndex]][0]['start'] = $nextDayStart;
                        break;
                    }

                    $start += $interval;
                    $reserveTime = $end - $start;
                }
            }
        }

        return $daySlots;
    }

    protected function maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration)
    {
        if (!$dateOverrides) {
            return $availableSlots;
        }

        list($overrideSlots, $overrideDays) = $dateOverrides;

        if ($overrideDays && isset($overrideDays[$date])) {
            $availableSlots = $this->removeOverrideSlots($availableSlots, $overrideDays[$date]);
        }
        
        if ($overrideSlots && isset($overrideSlots[$date])) {
            $flatOverrideSlots = $this->convertSlotSetsToFlat($overrideSlots, $date, $duration);
            $availableSlots = array_merge($availableSlots, $flatOverrideSlots);
            $availableSlots = $this->sortDaySlots($availableSlots);
        }

        return $availableSlots;
    }

    protected function convertSlotSetsToFlat(&$overrideSlots, $date, $duration = null)
    {
        $period = $this->calendarSlot->getDuration($duration) * 60;

        $interval = $this->calendarSlot->getSlotInterval($duration) * 60;

        $formattedSlots = [];

        $slotSets = $overrideSlots[$date];

        foreach ($slotSets as $slot) {
            $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
            $start = strtotime($slot['start']);
            $end = strtotime($slot['end']);

            while ($start + $period <= $end) {
                $formattedSlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                $start += $interval;
            }

            if ($slot['end'] == '24:00' && $start < $end) {
                $formattedSlots = $this->handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, $overrideSlots, $date);
            }
        }

        return $formattedSlots;
    }

    protected function handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, &$overrideSlots, $date)
    {
        $nextDayIndex = gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        if (isset($overrideSlots[$nextDayIndex])) {
            $nextDay = $overrideSlots[$nextDayIndex];

            if ($nextDay && $nextDay[0]['start'] == '00:00') {
                $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
                $reserveTime = $end - $start;

                while ($period - $reserveTime <= $nextDayEndTime) {
                    $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                    $formattedSlots[] = $startTime;

                    if ($startTime > $nextDayStart) {
                        $overrideSlots[$nextDayIndex][0]['start'] = $nextDayStart;
                        break;
                    }

                    $start += $interval;
                    $reserveTime = $end - $start;
                }
            }
        }

        return $formattedSlots;
    }

    protected function removeOverrideSlots($availableSlots, $overrideDay)
    {
        if (!$availableSlots || !$overrideDay) {
            return $availableSlots;
        }

        $startTime = strtotime($overrideDay['start']);
        $endTime   = strtotime($overrideDay['end']);

        $filteredSlots = array_filter($availableSlots, function ($slot) use ($startTime, $endTime) {
            return strtotime($slot) < $startTime || strtotime($slot) >= $endTime;
        });

        return $filteredSlots;
    }

    public function getAvailableSpots($startDate, $timeZone = 'UTC', $duration = null, $hostId = null)
    {
        $this->hostId = $hostId;

        $event    = $this->calendarSlot;
        $duration = $event->getDuration($duration);

        $adjustedDate = $this->adjustStartDate($startDate, $timeZone);

        $isDisplaySpots = $event->is_display_spots;
        $isMultiGuest   = $event->isMultiGuestEvent();
        $isMultiBooking = $event->isAdditionalGuestEnabled();
        $endDate        = $event->getMaxBookableDateTime($adjustedDate, $timeZone);
        $startDate      = $event->getMinBookableDateTime($startDate, $timeZone);

        $maxBooking = false;
        if ($isMultiGuest && ($isDisplaySpots || $isMultiBooking)) {
            $maxBooking = $event->getMaxBookingPerSlot();
        }

        if (strtotime($startDate) > strtotime($endDate)) {
            return new \WP_Error('invalid_date_range', __('Invalid date range', 'fluent-booking'));
        }

        $slots = $this->getDates($startDate, $endDate, $duration, false, $timeZone);

        return $this->convertSpots($slots, $startDate, 'UTC', $timeZone, $maxBooking);
    }

    protected function adjustStartDate($startDate, $timeZone)
    {
        $requestedDate = $startDate;

        $startDate = DateTimeHelper::convertToUtc($startDate, $timeZone);
        $currentDateTime = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

        if (strtotime($startDate) < strtotime($currentDateTime)) {
            $startDate = $currentDateTime;
        }

        // Extract month and year from the timezone converted start date and requested date
        list($startDateMonth, $startDateYear) = $this->extractMonthAndYear($startDate);
        list($requestedDateMonth, $requestedDateYear) = $this->extractMonthAndYear($requestedDate);

        if ($startDateYear < $requestedDateYear || $startDateMonth < $requestedDateMonth) {
            $startDate = gmdate('Y-m-01 00:00:00', strtotime($requestedDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        }

        return $startDate;
    }

    protected function convertSpots($slots, $startDate, $fromTimeZone = 'UTC', $toTimeZone = 'UTC', $maxBooking = false)
    {
        $minBookableTimestamp = strtotime($startDate);

        $convertedSpots = [];
        foreach ($slots as $spots) {
            foreach ($spots as $spot) {
                $start = $spot['start'];
                $end = $spot['end'];

                if (strtotime($start) < $minBookableTimestamp) {
                    continue;
                }

                if ($fromTimeZone != $toTimeZone) {
                    $start = DateTimeHelper::convertToTimeZone($spot['start'], 'UTC', $toTimeZone);
                    $end = DateTimeHelper::convertToTimeZone($spot['end'], 'UTC', $toTimeZone);
                }

                $spotStartDate = gmdate('Y-m-d', strtotime($start)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date

                $convertedSpots[$spotStartDate] = $convertedSpots[$spotStartDate] ?? [];

                $remainingSlots = $maxBooking ? Arr::get($spot, 'remaining', $maxBooking) : false;

                $convertedSpots[$spotStartDate][$start] = [
                    'start'     => $start,
                    'end'       => $end,
                    'remaining' => $remainingSlots,
                ];
            }
        }

        $convertedSpots = array_map(function ($spots) {
            return array_values(ksort($spots) ? $spots : $spots);
        }, $convertedSpots);

        return $convertedSpots;
    }

    private function extractMonthAndYear($date)
    {
        $month = gmdate('m', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        $year = gmdate('Y', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        return [$month, $year];
    }

    protected function createDateRangeArrayFromSlotConfig($slotConfig = [])
    {
        if (empty($slotConfig['start']) || empty($slotConfig['end'])) {
            return [];
        }

        $startTime = $slotConfig['start'];
        $endTime = $slotConfig['end'];
        if (gmdate('Ymd', strtotime($startTime)) == gmdate('Ymd', strtotime($endTime))) {
            return [
                gmdate('Y-m-d', strtotime($startTime)) => $this->bookSlot(Arr::get($slotConfig, 'event_id'), $startTime, $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'))
            ];
        }

        $start = new \DateTime($startTime);
        $end = new \DateTime($endTime);

        // Set the end time to the end of the day if it's set to the beginning of a day
        if ($end->format('H:i:s') === '00:00:00') {
            $end->modify('-1 second'); // This will set the time to 23:59:59 of the previous day
        }

        $interval = new \DateInterval('P1D');
        $dateRange = new \DatePeriod($start, $interval, $end);

        $rangeArray = [];
        foreach ($dateRange as $date) {
            $dateKey = $date->format('Y-m-d');

            if ($date->format('Y-m-d') === $start->format('Y-m-d')) {
                $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $startTime, $date->format('Y-m-d 23:59:59'), Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
            } elseif ($date->format('Y-m-d') === $end->format('Y-m-d')) {
                $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $date->format('Y-m-d 00:00:00'), $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
            } else {
                $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $date->format('Y-m-d 00:00:00'), $date->format('Y-m-d 23:59:59'), Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
            }
        }

        // Add the last day if it was not included in the loop
        if ($end->format('Y-m-d') !== $start->format('Y-m-d')) {
            $lastDayKey = $end->format('Y-m-d');
            $rangeArray[$lastDayKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $end->format('Y-m-d 00:00:00'), $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
        }

        return $rangeArray;
    }

    private function maybeBookingFrequencyLimitRanges($ranges)
    {
        if (!$ranges) {
            return $ranges;
        }

        $isBookingFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');
        if (!$isBookingFrequencyEnabled) {
            return $ranges;
        }

        $keyedFrequenceyLimits = [];
        $frequenceyLimits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);
        foreach ($frequenceyLimits as $limit) {
            if (!empty($limit['value'])) {
                $keyedFrequenceyLimits[$limit['unit']] = $limit['value'];
            }
        }

        // Per Month Booking Frequency Limit Hanlder
        if (!empty($keyedFrequenceyLimits['per_month'])) {
            $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges)));
            $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges)));

            $monthlyLimit = (int)$keyedFrequenceyLimits['per_month'];

            $monthlyCount = $this->getBookingsTotal($startDate, $endDate);

            if ($monthlyCount >= $monthlyLimit) {
                return [];
            }
        }

        // Per Week Booking Frequency Limit Hanlder
        if (!empty($keyedFrequenceyLimits['per_week'])) {

            if (!$ranges) {
                return [];
            }

            $weeklyLimit = (int)$keyedFrequenceyLimits['per_week'];
            $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges));
            foreach ($filledWeeks as $filledWeek) {

                $weeklyCount = $this->getBookingsTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59');

                if ($weeklyCount >= $weeklyLimit) {
                    $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) {
                        return !in_array($rangeDate, $filledWeek);
                    });

                    if (!$ranges) {
                        return [];
                    }
                }
            }
        }
        return $ranges;
    }

    private function maybeBookingDurationLimitRanges($ranges, $duration)
    {
        if (!$ranges) {
            return $ranges;
        }

        if (!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled')) {
            return $ranges;
        }

        $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);

        $keyedLimits = [];
        foreach ($limits as $limit) {
            if (!empty($limit['value'])) {
                $keyedLimits[$limit['unit']] = (int)$limit['value'];
            }
        }

        // Per Month Booking Frequency Limit Hanlder
        if (!empty($keyedLimits['per_month'])) {
            $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges)));
            $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges)));

            $monthlyDuration = $this->getBookingDurationTotal($startDate, $endDate);

            if ($monthlyDuration + $duration > $keyedLimits['per_month']) {
                $ranges = [];
            }
        }

        // Per Week Booking Frequency Limit Hanlder
        if (!empty($keyedLimits['per_week'])) {
            $weeklyLimit = (int)$keyedLimits['per_week'];
            $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges));
            foreach ($filledWeeks as $filledWeek) {
                $weeklyDuration = $this->getBookingDurationTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59');

                if ($weeklyDuration + $duration > $weeklyLimit) {
                    $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) {
                        return !in_array($rangeDate, $filledWeek);
                    });

                    if (!$ranges) {
                        return [];
                    }
                }
            }
        }
        return $ranges;
    }

    protected function maybeBookingPerDayLimitSlots($rangesSlots, $bookedSlots, $duration)
    {
        $isDurationEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled');
        
        $isFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');

        if (!$isDurationEnabled && !$isFrequencyEnabled) {
            return $rangesSlots;
        }

        $hostTimeZone = $this->calendarSlot->getScheduleTimezone($this->hostId);

        $convertedRangesSlots = $this->convertSlotsByTimezone($rangesSlots, 'UTC', $hostTimeZone);

        $convertedBookedSlots = $this->convertSlotsByTimezone($bookedSlots, 'UTC', $hostTimeZone);

        $convertedRangesSlots = $this->maybeBookingDurationDayLimit($convertedRangesSlots, $convertedBookedSlots, $duration, $isDurationEnabled);

        $convertedRangesSlots = $this->maybeBookingFrequencyDayLimit($convertedRangesSlots, $convertedBookedSlots, $isFrequencyEnabled);

        $rangesSlots = $this->convertSlotsByTimezone($convertedRangesSlots, $hostTimeZone, 'UTC');

        return $rangesSlots;
    }

    protected function maybeBookingDurationDayLimit($rangesSlots, $bookedSlots, $duration, $isEnabled)
    {
        if (!$isEnabled) {
            return $rangesSlots;
        }

        $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);

        $perDayLimit = null;
        foreach ($limits as $limit) {
            if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
                $perDayLimit = (int)Arr::get($limit, 'value');
                break;
            }
        }

        if (!$perDayLimit) {
            return $rangesSlots;
        }

        $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();

        foreach ($rangesSlots as $rangeDate => &$slots) {
            if (!isset($bookedSlots[$rangeDate])) {
                continue;
            }

            $dayDuration = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
                if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
                    $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
                }
                return $carry;
            }, 0);

            if ($dayDuration + $duration > $perDayLimit) {
                if ($isMultiSlot) {
                    $slots = array_values(array_filter($slots, function ($slot) {
                        return !empty(Arr::get($slot, 'remaining'));
                    }));
                }
                if (!$isMultiSlot || !count($slots)) {
                    unset($rangesSlots[$rangeDate]);
                }
            }
        }

        return $rangesSlots;
    }

    protected function maybeBookingFrequencyDayLimit($rangesSlots, $bookedSlots, $isEnabled)
    {
        if (!$isEnabled) {
            return $rangesSlots;
        }

        $limits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);

        $perDayLimit = null;
        foreach ($limits as $limit) {
            if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
                $perDayLimit = (int)Arr::get($limit, 'value');
                break;
            }
        }

        if (!$perDayLimit) {
            return $rangesSlots;
        }

        $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();

        foreach ($rangesSlots as $rangeDate => &$slots) {
            if (!isset($bookedSlots[$rangeDate])) {
                continue;
            }

            $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
                return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
            });

            if (count($dayBooked) >= $perDayLimit) {
                if ($isMultiSlot) {
                    $slots = array_values(array_filter($slots, function ($slot) {
                        return !empty(Arr::get($slot, 'remaining'));
                    }));
                }
                if (!$isMultiSlot || !count($slots)) {
                    unset($rangesSlots[$rangeDate]);
                }
            }
        }

        return $rangesSlots;
    }

    protected function convertSlotsByTimezone($slots, $fromTimeZone, $toTimeZone)
    {
        if ($fromTimeZone == $toTimeZone) {
            return $slots;
        }

        $convertedSlots = [];

        foreach ($slots as $spots) {
            foreach ($spots as $spot) {
                $spot['start'] = DateTimeHelper::convertToTimeZone($spot['start'], $fromTimeZone, $toTimeZone);
                $spot['end'] = DateTimeHelper::convertToTimeZone($spot['end'], $fromTimeZone, $toTimeZone);

                $spotDate = gmdate('Y-m-d', strtotime($spot['start']));

                $convertedSlots[$spotDate] = $convertedSlots[$spotDate] ?? [];

                $convertedSlots[$spotDate][] = $spot;
            }
        }

        return $convertedSlots;
    }

    public function getFilledWeeks($from, $to, $weekStart = '')
    {
        $weekStart = $weekStart ? $weekStart : Arr::get(Helper::getGlobalSettings(), 'administration.start_day', 'sun');

        $startDate = new DateTime($from);
        $endDate = new DateTime($to);

        if (strtolower($startDate->format('D')) != $weekStart) {
            $startDate->modify('last ' . $weekStart);
        }

        $weeks = [];

        while ($startDate <= $endDate) {
            // get all days in this week
            $week = [];
            for ($i = 0; $i < 7; $i++) {
                $week[] = $startDate->format('Y-m-d');
                $startDate->modify('+1 day');
            }
            $weeks[] = $week;
        }

        return $weeks;
    }

    protected function getBookingsTotal($start, $end)
    {
        if ($this->calendarSlot->event_type == 'group') {
            return Booking::query()
                ->where('event_id', $this->calendarSlot->id)
                ->whereBetween('start_time', [$start, $end])
                ->whereIn('status', ['scheduled', 'completed'])
                ->groupBy('group_id')
                ->count();
        }

        return Booking::query()
            ->where('event_id', $this->calendarSlot->id)
            ->whereBetween('start_time', [$start, $end])
            ->whereIn('status', ['scheduled', 'completed'])
            ->count();
    }

    protected function getBookingDurationTotal($start, $end)
    {
        if ($this->calendarSlot->event_type == 'group') {
            return Booking::query()
                ->select(['group_id', 'slot_minutes'])
                ->where('event_id', $this->calendarSlot->id)
                ->whereBetween('start_time', [$start, $end])
                ->whereIn('status', ['scheduled', 'completed'])
                ->groupBy('group_id')
                ->get()
                ->sum('slot_minutes');
        }

        return Booking::query()
            ->where('event_id', $this->calendarSlot->id)
            ->whereBetween('start_time', [$start, $end])
            ->whereIn('status', ['scheduled', 'completed'])
            ->sum('slot_minutes');
    }

    protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone)
    {
        $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($toDate, $timeZone, 'Y-m-d H:i:s');

        return strtotime($maxBookingTime);
    }

    protected function getTimezoneInfo($hostId = null)
    {
        $hostId = $hostId ?: $this->hostId;

        $scheduleTimezone = $this->calendarSlot->getScheduleTimezone($hostId);

        $dstTime = DateTimeHelper::getDaylightSavingTime($scheduleTimezone);

        return [$scheduleTimezone, $dstTime];
    }

    protected function maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone, $adjustSign = '-')
    {
        if (!$dstTime) {
            return $slot;
        }

        $slot['start'] = $this->maybeDayLightSavingTime($slot['start'], $dstTime, $scheduleTimezone, $adjustSign);
        $slot['end'] = $this->maybeDayLightSavingTime($slot['end'], $dstTime, $scheduleTimezone, $adjustSign);

        return $slot;
    }

    protected function maybeDayLightSavingTime($time, $dstTime, $timezone, $adjustSign = '+')
    {
        $scheduleTime = DateTimeHelper::convertToTimeZone($time, 'UTC', $timezone);
        if (DateTimeHelper::isDaylightSavingActive($scheduleTime, $timezone)) {
            $time = gmdate('Y-m-d H:i:s', strtotime($time . " $adjustSign $dstTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
        }

        return $time;
    }

    protected function isLocalBooking($bookings, $slot)
    {
        if (empty($bookings)) {
            return false;
        }

        foreach ($bookings as $book) {
            if ($book['start'] == $slot['start'] && $book['end'] == $slot['end']) {
                return true;
            }
        }
        
        return false;
    }

    protected function mergeAndSortSlots($currentSlots, $validSlots)
    {
        if (!$currentSlots) {
            return $validSlots;
        }

        $mergedSlots = array_merge($currentSlots, $validSlots);

        usort($mergedSlots, function ($a, $b) {
            return strtotime($a['start']) - strtotime($b['start']);
        });

        return $mergedSlots;
    }

    protected function sortDaySlots($daySlots)
    {
        usort($daySlots, function ($a, $b) {
            return strtotime($a) - strtotime($b);
        });

        return $daySlots;
    }
}
