<?php
// phpcs:ignoreFile WordPress.WP.I18n.TextDomainMismatch

use Salon\Util\Date;
use Salon\Util\Time;


class SLN_Helper_Availability
{
    const MAX_DAYS = 365;

    private $settings;
    private $date;
    /** @var  SLN_Helper_Availability_AbstractDayBookings */
    private $dayBookings;
    /** @var  SLN_Helper_HoursBefore */
    private $hoursBefore;
    private $attendantsEnabled;
    private $items;
    private $itemsWithoutServiceOffset;
    private $holidayItemsWithWeekDayRules;
    private $holidayItems;
    private $offset;
    protected $initialDate;
    private $excludeHiddenFromFrontend = false;

    /**
     * Request-scoped memoization of the leaf per-minute validation primitives
     * (validateAttendantOnTime / validateServiceOnTime).
     *
     * Rationale: the alternate-order date step (SLN_Action_Ajax_CheckDateAlt)
     * validates every candidate slot of a day, and every slot re-validates the
     * whole [slot, slot+duration] minute window. Consecutive slots overlap
     * heavily (e.g. 09:00 checks 09:00→09:45, 09:05 checks 09:05→09:50, …), so
     * without caching each minute of the day is re-validated ~duration/interval
     * times per attendant/service — the dominant cost of the date step.
     *
     * These primitives are PURE functions of the immutable per-request day model
     * (existing bookings, opening hours, holiday/booking rules) for a given
     * (attendant|service, H:i, service, flags). Caching them yields identical
     * results while collapsing the redundant overlap to one computation per
     * distinct minute. The cache is invalidated in setDate() whenever a new
     * DayBookings model is built (day change or different booking context), so a
     * stale day can never leak a wrong result.
     *
     * @var array
     */
    private $slotValidationCache = array();

    /**
     * Lightweight per-request performance counters for the availability engine.
     * Populated only when self::$perfEnabled is true (set by a debug-gated caller,
     * e.g. SLN_Action_Ajax_CheckDate) so there is zero overhead in normal runs.
     * Surfaced to admins in the checkDate AJAX debug payload to diagnose whether
     * the slowness comes from cache misses (full window re-processed each request)
     * or from getTimes() itself — without relying on the (CDN-cached) log file.
     *
     * @var bool
     */
    public static $perfEnabled = false;
    /** @var array */
    public static $perf = array();

    public static function perfReset()
    {
        self::$perf = array(
            'cachedDays_calls'   => 0,
            'cachedDays_ms'      => 0.0,
            'window_days'        => 0,
            'hasDay_hit'         => 0,
            'hasDay_miss'        => 0,
            'processDate_calls'  => 0,
            'processDate_ms'     => 0.0,
            'getTimes_calls'     => 0,
            'getTimes_ms'        => 0.0,
            // Diagnostic: bounded list of dates getTimes() is invoked with, to
            // locate the hidden per-day loop that recomputes availability on a
            // warm cache. Capped to avoid bloating the debug AJAX payload.
            'getTimes_dates'     => array(),
            // Diagnostic: number of days in the persisted salon_cache option at
            // request start (-1 = option missing). Confirms whether the day
            // cache actually persists/reloads across requests, or is empty every
            // time (forcing getFullDays()->refreshAll() full-window recompute).
            'cache_option_count' => null,
            // Diagnostic: how many times the whole booking window was refreshed
            // (refreshAll/refresh) during this request. Expected 0 on a warm,
            // persisted cache.
            'refresh_calls'      => 0,
            'refresh_days'       => 0,
            // --- Phase timers for the alternate-order date step -----------------
            // (SLN_Action_Ajax_CheckDateAlt::getIntervalsArray). These split the
            // ~4s "assistant -> date" cost seen on heavy live sites into its two
            // dominant loops so we can tell whether the bottleneck is the per-day
            // availability scan or the suggested-day time-list validation.
            //
            // dateloop_ms: wall time of the whole `foreach($dates)` scan that
            //   decides which days are shown as bookable (checkDayServicesAndAttendants
            //   + dateHasBookableSlot per day).
            // timelist_ms: wall time of the final `foreach($times)` loop that
            //   validates every slot of the suggested day for the time picker.
            'dateloop_ms'        => 0.0,
            'timelist_ms'        => 0.0,
            // How many days the date loop scanned, and how many candidate slots the
            // per-day bookable-slot probe evaluated in total. Together with
            // dateloop_ms these give the real per-day / per-slot unit cost.
            'dateHasBookableSlot_calls' => 0,
            'candidateSlots_scanned'    => 0,
            // checkDateTimeServicesAndAttendants() invocations (the strict per-slot
            // validation) across the whole request — the multiplier that makes the
            // date step expensive.
            'checkSlot_calls'    => 0,
            // DayBookings model (re)builds. Each build loads the day's existing
            // bookings from the DB, so on a cold per-request state this scales with
            // the number of distinct days touched (~window size) and is the prime
            // suspect for the residual cost the per-minute memoization can't remove.
            'dayBookings_builds'   => 0,
            'dayBookings_build_ms' => 0.0,
            // Effectiveness of the request-scoped per-minute validation memoization
            // (validateAttendantOnTime / validateServiceOnTime).
            'validation_hits'    => 0,
            'validation_misses'  => 0,
        );
    }

    public static function perfGet()
    {
        $p = self::$perf;
        foreach (array('cachedDays_ms', 'processDate_ms', 'getTimes_ms', 'dateloop_ms', 'timelist_ms', 'dayBookings_build_ms') as $k) {
            if (isset($p[$k])) {
                $p[$k] = round($p[$k], 1);
            }
        }
        return $p;
    }

    public function __construct(SLN_Plugin $plugin)
    {
        $this->settings = $plugin->getSettings();
        $this->initialDate = $plugin->getBookingBuilder()->getEmptyValue();
        $this->attendantsEnabled = $this->settings->isAttendantsEnabled();
    }

    /**
     * When enabled, attendants flagged "Hide on front-end" are excluded from
     * attendant availability/auto-assignment. Front-end booking flows opt in;
     * back-end and API leave this off so hidden attendants stay fully usable.
     *
     * @param bool $value
     * @return $this
     */
    public function setExcludeHiddenFromFrontend($value)
    {
        $this->excludeHiddenFromFrontend = (bool) $value;

        return $this;
    }

    public function getHoursBeforeHelper()
    {
        if (!isset($this->hoursBefore)) {
            $this->hoursBefore = new SLN_Helper_HoursBefore($this->settings);
        }

        return $this->hoursBefore;
    }

    public function getHoursBeforeString()
    {
        return $this->getHoursBeforeHelper()->getHoursBeforeString();
    }

    public function getCachedDays() {
        $perf = self::$perfEnabled;
        $perfStart = $perf ? microtime(true) : 0;
        $bc = SLN_Plugin::getInstance()->getBookingCache();
        $interval = $this->getHoursBeforeHelper();
        $from = Date::create($interval->getFromDate());
        $count = $interval->getCountDays();
        $ret = array();
        $avItems = $this->getItems();
        $hItems  = $this->getHolidaysItemsWithWeekDayRules($avItems->getWeekDayRules());
        $dayLog = SLN_Helper_Availability_AdminRuleLog::getInstance();
        if ($perf) {
            self::$perf['cachedDays_calls']++;
            self::$perf['window_days'] += $count;
        }
        
        // PERFORMANCE OPTIMIZATION: Batch process all missing dates at once
        // Instead of: processDate() → save() → processDate() → save() (89 times)
        // We do: processDate() → processDate() → ... → save() (once)
        $dates_to_process = array();
        $temp_from = clone $from;
        $temp_count = $count;
        
        while ($temp_count > 0) {
            $temp_count--;
            if(!$bc->hasDay($temp_from)) {
                if ($perf) { self::$perf['hasDay_miss']++; }
                $dates_to_process[] = clone $temp_from;
            } elseif ($perf) {
                self::$perf['hasDay_hit']++;
            }
            $temp_from = $temp_from->getNextDate();
        }
        
        // Process all missing dates, then save ONCE
        if (!empty($dates_to_process)) {
            $procStart = $perf ? microtime(true) : 0;
            foreach ($dates_to_process as $date) {
                $bc->processDate($date);
            }
            if ($perf) {
                self::$perf['processDate_calls'] += count($dates_to_process);
                self::$perf['processDate_ms'] += (microtime(true) - $procStart) * 1000;
            }
            $bc->save(); // Single save instead of N saves
        }
        
        // Now build the result array
        while ($count > 0) {
            $count--;
            $day = $bc->getDay($from);
            
            if ($dayLog->isEnabled()) {
                $dayLog->addDateLog( $from->toString(), $hItems->isValidDate($from), __( 'The day is holiday', 'salon-booking-system' ) );
                $dayLog->addDateLog( $from->toString(), !(isset($day) && $day['status'] == 'booking_rules'), __( 'This is non-working day', 'salon-booking-system' ) );
                $dayLog->addDateLog( $from->toString(), !(isset($day) && $day['status'] == 'full'), __( 'The day is full', 'salon-booking-system' ));
            }
            if((!isset($day) && $avItems->isValidDate($from) && $hItems->isValidDate($from) && $this->isValidDate($from)) ||
            (isset($day) && $day['status'] == 'free')) {
                $ret[] = $from;
            }
            $from = $from->getNextDate();
        }
        
        if ($perf) {
            self::$perf['cachedDays_ms'] += (microtime(true) - $perfStart) * 1000;
        }
        return $ret;
    }

    public function getDays()
    {
        $interval = $this->getHoursBeforeHelper();
        $from = Date::create($interval->getFromDate());
        $count = $interval->getCountDays();
        $ret = array();
        $avItems = $this->getItems();
        $hItems  = $this->getHolidaysItemsWithWeekDayRules($avItems->getWeekDayRules());
        while ($count > 0) {
            $count--;
            if ($avItems->isValidDate($from) && $hItems->isValidDate($from) && $this->isValidDate($from)) {
                $ret[] = $from->toString();
            }
            $from = $from->getNextDate();
        }

        return $ret;
    }

    public function getCachedTimes(Date $date, Time $duration = null)
    {
        $bc = SLN_Plugin::getInstance()->getBookingCache();
        if ($bc->hasFullDay($date)) {
            return array();
        }
        // PERFORMANCE: When a day is already resolved in the persisted day cache
        // as non-bookable, getTimes() would recompute an empty result anyway
        // (that is exactly why processDate() flagged it as full/non-working/holiday).
        // Returning early keeps the result identical while skipping the expensive
        // availability engine. This is what makes the forward day-scan in
        // SLN_Helper_Intervals::setDatetime() cheap on a warm cache: without it,
        // every scanned non-bookable day re-ran the full computation on each request
        // (hasFullDay() only short-circuits the 'full' status, not 'booking_rules'
        // or 'holiday_rules').
        if ($bc->hasDay($date)) {
            $day = $bc->getDay($date);
            $status = isset($day['status']) ? $day['status'] : '';
            if (in_array($status, array('full', 'booking_rules', 'holiday_rules'), true)) {
                return array();
            }
            // PERFORMANCE: reuse the day's cached free_slots (the raw getTimes()
            // output persisted by processDate()/processBooking()/refresh()) instead
            // of recomputing the expensive per-slot availability engine on every
            // request. getTimes() output depends only on opening hours, holidays and
            // existing bookings — never on the selected service/attendant/duration
            // (those filters are applied by callers AFTER this method) — so the
            // cached slots are an exact substitute, and the cache is already
            // invalidated whenever a booking or rule changes.
            //
            // This is the fix for the multi-second date step in the alternate step
            // order (SLN_Action_Ajax_CheckDateAlt): its per-day bookability scan
            // (dateHasBookableSlot) called getCachedTimes() -> getTimes() once for
            // EVERY day in the booking window on each checkDate request (~tens of ms
            // per day × the whole window), even on a warm cache.
            //
            // Restricted to days strictly AFTER today so the "hours before" lower
            // bound — which trims already-passed times from *today* as the clock
            // advances within the request — is never in play; today (and the boundary
            // day) still recompute for exactness.
            if ($status === 'free' && !empty($day['free_slots']) && is_array($day['free_slots'])) {
                // Reuse cached free_slots only for days strictly AFTER BOTH today and
                // the "hours before" minimum-advance day. The original guard used today
                // alone, assuming the hours-before lower bound only ever trims times
                // from *today*. That assumption breaks when hours_before_from pushes the
                // earliest bookable moment onto a FUTURE day (e.g. "+1 day" => tomorrow
                // 20:45): that boundary day's cached slots were computed with an earlier
                // $from and still contain times that are now too near, so the date step
                // offered (and defaulted to) slots that immediately failed the
                // "date is too near" check. Recomputing via getTimes() for the boundary
                // day re-applies the $d >= $from filter, dropping the too-near slots (and
                // a day left with no valid slot correctly disappears from the calendar).
                $hbFromDay = $this->getHoursBeforeHelper()->getFromDate()->format('Y-m-d');
                $safeAfter = max(SLN_TimeFunc::date('Y-m-d'), $hbFromDay);
                if ($date->toString() > $safeAfter) {
                    $ret = array();
                    foreach ($day['free_slots'] as $hi) {
                        $ret[$hi] = new SLN_DateTime($date->toString() . ' ' . $hi);
                    }

                    return $ret;
                }
            }
        }
        $ret = $this->getTimes($date);
        if(empty($ret)){
            $bc->processDate($date);
            $bc->save();
        }

        return $ret;
    }

    /**
     * Filter start times so the whole service duration fits the time grid, while still allowing
     * nested bookings that span only "missing" steps that fall inside other bookings' breaks.
     *
     * Without the nested-break coverage check, a start time could be re-added only because its
     * first minute was a break slot while later minutes crossed another customer's work segment.
     *
     * @param array<string,\DateTimeInterface|mixed> $times Keys are "H:i", values are datetime objects (as from getTimes).
     * @param Time                                   $duration Service duration (working time, excluding own break).
     * @return array<string,mixed>
     */
    public function filterTimesArrayByDurationWithBreakAllowance(array $times, Time $duration)
    {
        if (empty($times) || !$duration) {
            return $times;
        }

        $filtered = Time::filterTimesArrayByDuration($times, $duration);

        // End-time overflow guard: Time::filterTimesArrayByDuration checks that each
        // booking-interval grid step from start up to (not including) start+duration
        // exists in $times. When interval >= service duration the loop runs only once
        // and only the start slot is checked — the service end time is never validated.
        // Example: 15-min interval, 60-min service, closing 20:00 → 19:45 is shown
        // because 20:00 is in $times, but 19:45+60min=20:45 exceeds closing.
        // Fix: use the authoritative isValidDatetimeDuration() (same check used at
        // booking confirmation) to verify the full [start, start+duration] window.
        $durationMinutes = \SLN_Func::getMinutesFromDuration($duration->toDateTime());
        if ($durationMinutes > 0) {
            $dtDuration = new \DateTime('@' . ($durationMinutes * 60));
            $avItems    = $this->getItems();
            $hItems     = $this->getHolidaysItems();
            $originalFiltered = $filtered;
            foreach ($filtered as $label => $slotDt) {
                if (!($slotDt instanceof \DateTimeInterface)) {
                    continue;
                }
                $dt = clone $slotDt;
                if (!$avItems->isValidDatetimeDuration($dt, $dtDuration)
                    || !$hItems->isValidDatetimeDuration($dt, $dtDuration)
                ) {
                    unset($filtered[$label]);
                }
            }
            \SLN_Helper_AvailabilityDebugger::logFilteredTimes($originalFiltered, $filtered, 'End-time overflow guard');
        }

        $missing  = array_diff_key($times, $filtered);
        if (empty($missing)) {
            return $filtered;
        }

        $dayBookings = $this->getDayBookings();
        if (!$dayBookings) {
            return $filtered;
        }

        $dateYmd = null;
        foreach ($times as $slotObj) {
            if ($slotObj instanceof \DateTimeInterface) {
                $dateYmd = $slotObj->format('Y-m-d');
                break;
            }
        }
        if (!$dateYmd) {
            return $filtered;
        }

        $breakSlots = array();
        foreach ($missing as $label => $slot) {
            $slotDt = isset($times[ $label ]) ? $times[ $label ] : null;
            if ($slotDt instanceof \DateTimeInterface) {
                $tb = $dayBookings->getTime($slotDt->format('H'), $slotDt->format('i'));
            } else {
                $tb = $dayBookings->getTime((int) substr((string) $label, 0, 2), (int) substr((string) $label, 3, 2));
            }
            if (!$dayBookings->isBreakSlot($tb)) {
                continue;
            }
            $startT = Time::create($label);
            if (!$this->durationStepsCoveredByTimesOrNestedBreaks($times, $startT, $duration, $dateYmd)) {
                continue;
            }
            $filtered[ $label ] = $slot;
            $breakSlots[ $label ] = $slot;
        }

        if (empty($filtered)) {
            if (!empty($breakSlots)) {
                ksort($breakSlots);

                return $breakSlots;
            }

            return $filtered;
        }

        ksort($filtered);

        return $filtered;
    }

    /**
     * Each step from start (inclusive) to start+duration (exclusive) must exist in $times or be a nested-break slot.
     */
    private function durationStepsCoveredByTimesOrNestedBreaks(array $times, Time $startTime, Time $duration, $dateYmd)
    {
        $dayBookings = $this->getDayBookings();
        $end         = Time::increment($startTime, $duration);
        $time        = Time::create($startTime);
        while ($time->isLt($end)) {
            $key = (string) $time;
            if (!isset($times[ $key ])) {
                $dt = new SLN_DateTime($dateYmd . ' ' . $key);
                $tb = $dayBookings->getTime($dt->format('H'), $dt->format('i'));
                if (!$dayBookings->isBreakSlot($tb)) {
                    return false;
                }
            }
            $time = Time::increment($time);
        }

        return true;
    }
    
    public function getTimes(Date $date)
    {
        $perf = self::$perfEnabled;
        $perfStart = $perf ? microtime(true) : 0;
        if ($perf) {
            self::$perf['getTimes_calls']++;
            if (count(self::$perf['getTimes_dates']) < 250) {
                self::$perf['getTimes_dates'][] = $date->toString();
            }
        }
        $ret = array();
        $avItems = $this->getItems();
        $hItems = $this->getHolidaysItems();
        $hb = $this->getHoursBeforeHelper();
        $from = $hb->getFromDate();
        $to = $hb->getToDate();
        
        // Debug: Log from/to times for November 12
        if ($date->toString() === '2025-11-12') {
            SLN_Plugin::addLog(sprintf('[getTimes DEBUG] November 12 - From: %s | To: %s', $from->format('Y-m-d H:i:s'), $to->format('Y-m-d H:i:s')));
        }
        
        $timeLog = SLN_Helper_Availability_AdminRuleLog::getInstance();
        foreach (SLN_Func::getMinutesIntervals() as $time) {
        	$d = new SLN_DateTime($date->toString().' '.$time);

            if ($timeLog->isEnabled()) {
                foreach( $avItems->getDateSubset( Salon\Util\Date::create($d) ) as $data){
                    $debugMessage = __( 'Working hours', 'salon-booking-system' ). ': ';
                    if(isset($data->getData()['from']) && is_array($data->getData()['from']) && is_array($data->getData()['to'])){
                        foreach( array_map( null, $data->getData()['from'], $data->getData()['to'] ) as $t ){
                            $debugMessage .= ' '. __( 'from', 'salon-booking-system' ). ' '. $t[0];
                            $debugMessage .= ' '. __( 'to', 'salon-booking-system' ). ' '. $t[1]. ',';
                        }
                    }
                    $debugMessage[-1] = '.';
                    $timeLog->addLog( $time,  $avItems->isValidDatetime( $d ), $debugMessage );
                }
                $debugMessage = __( 'Parallels hours for booking', 'salon-booking-system' );
                $debugMessage .= ' '. $this->settings->get('parallels_hour'). '.';
                try{
                    $debugMessage .= ' '. __( 'Number of reserved booking', 'salon-booking-system' );
                    $keyRegex = '/'. $debugMessage. ' (\d*)./';
                    $debugMessage .= ' '. $this->getBookingsHourCount($d->format('H'), $d->format('i') ). '.';
                }catch( Throwable $ex){
                    $debugMessage .= __( 'Time out of the booking time range.', 'salon-booking-system' );
                }
                $timeLog->replaceKeyRegex( $time, $keyRegex, $debugMessage, $this->isValidTime( $d ) );
                $debugMessage = __( 'Available booking time range: from', 'salon-booking-system' ). ' '. $from->format('d-M H:i');
                $debugMessage .= ' '. __( 'to', 'salon-booking-system' ). $to->format('d-M H:i'). '.';
                $timeLog->addLog( $time, $d >= $from && $d <= $to, $debugMessage );
            }

            $avCheck = $avItems->isValidDatetime($d);
            $hCheck = $hItems->isValidDatetime($d);
            $timeCheck = $this->isValidTime($d);
            $rangeCheck = $d >= $from && $d <= $to;
            
            // Check if this is a break slot (gap inside an existing booking allowing nested bookings).
            $time_obj = $this->getDayBookings()->getTime($d->format('H'), $d->format('i'));
            $isBreakSlot = $this->getDayBookings()->isBreakSlot($time_obj);

            // Allow external code (e.g. Google Calendar slot locker) to mark a slot as unavailable.
            // Returning false blocks the slot without affecting the other availability checks.
            $gcalCheck = apply_filters('sln_gcal_time_check', true, $d);

            // Break slots intentionally bypass the lower bound of the booking window ($d >= $from).
            // This allows salon staff to fill a break gap in an existing booking even when that
            // break falls inside the "hours before" minimum advance window. Only the upper bound
            // ($d <= $to) is enforced so slots beyond the max-advance limit are still blocked.
            // Regular slots check the full range (both bounds).
            if ($isBreakSlot && $avCheck && $hCheck && $timeCheck && $gcalCheck && $d <= $to) {
                $ret[$time] = $d;
            } elseif (!$isBreakSlot && $avCheck && $hCheck && $timeCheck && $gcalCheck && $rangeCheck) {
                $ret[$time] = $d;
            }
        }
        if (SLN_Plugin::isDebugEnabled()) {
            SLN_Plugin::addLog(__CLASS__.' getTimes '.print_r($ret, true));
        }
        if ($perf) {
            self::$perf['getTimes_ms'] += (microtime(true) - $perfStart) * 1000;
        }

        return $ret;
    }

    public function  setDate(DateTime $date, SLN_Wrapper_Booking $booking = null)
    {
        if (empty($this->date) || ($this->date->format('Ymd') != $date->format('Ymd'))) {
            $mode = $this->settings->getAvailabilityMode();
            SLN_Plugin::addLog('');
            SLN_Plugin::addLog('==================================================');
            SLN_Plugin::addLog('🔍 AVAILABILITY MODE: '.strtoupper($mode));
            SLN_Plugin::addLog('==================================================');
            // DayBookings must be built against the day boundary (00:00), not the
            // currently selected time. Passing a time-bearing DateTime (e.g. 15:15)
            // causes early-day bookings to be clamped and overcounted in timeslots.
            $dayDate = clone $date;
            $dayDate->setTime(0, 0, 0);
            
            $perf = self::$perfEnabled;
            $buildStart = $perf ? microtime(true) : 0;
            $obj = SLN_Enum_AvailabilityModeProvider::getService(
                $mode,
                $dayDate,
                $booking
            );
            if ($perf) {
                self::$perf['dayBookings_builds']++;
                self::$perf['dayBookings_build_ms'] += (microtime(true) - $buildStart) * 1000;
            }
            SLN_Plugin::addLog(__CLASS__.sprintf(' - Started DayBookings class: %s', get_class($obj)));
            SLN_Plugin::addLog(__CLASS__.sprintf(' - Date: %s', $date->format('Y-m-d H:i')));
            SLN_Plugin::addLog(__CLASS__.sprintf(' - Booking: %s', $booking ? '#'.$booking->getId() : 'none'));
            $this->dayBookings = $obj;
            // A new day model invalidates all memoized per-minute validations.
            $this->slotValidationCache = array();
        }
        $this->dayBookings->setTime($date->format('H'), $date->format('i'));
        $this->date = $date;

        return $this;
    }

    /**
     * @return SLN_Helper_Availability_AbstractDayBookings
     */
    public function getDayBookings()
    {
        return $this->dayBookings;
    }

    public function getBookingsDayCount()
    {
        return $this->getDayBookings()->countBookingsByDay();
    }

    public function getBookingsHourCount($hour = null, $minutes = null)
    {
        return $this->getDayBookings()->countBookingsByHour($hour, $minutes);
    }

    public function getMinutesIntervals()
    {
        return $this->getDayBookings()->getMinutesIntervals();
    }

    public function validateAttendantService($attendant, SLN_Wrapper_ServiceInterface $service)
    {
        if(!is_array($attendant)){
            if (!$attendant->hasAllServices()) {
                if (!$attendant->hasService($service)) {
                    return array(
                        __('This assistant is not available for the selected service', 'salon-booking-system'),
                    );
                }
            }
        }else{
            foreach($attendant as $att){
                if(!$att->hasAllServices()){
                    if(!$att->hasService($service)){
                        return array(
                            __('This assistant is not available for the selected service', 'salon-booking-system'),
                        );
                    }
                }
            }
        }
    }

    public function validateAttendant(
        SLN_Wrapper_AttendantInterface $attendant,
        DateTime $date = null,
        DateTime $duration = null,
        SLN_Wrapper_ServiceInterface $service = null,
        DateTime $breakStartsAt = null,
        DateTime $breakEndsAt = null,
        $isLastService = false
    ) {
        $date = empty($date) ? $this->date : $date;
        $durationMinutes = !empty($duration) ? SLN_Func::getMinutesFromDuration($duration) : 0;

        $ignoreExistingBreaks = $this->getDayBookings()->isIgnoreServiceBreaks();
        $noBreak = $ignoreExistingBreaks || $breakStartsAt == $breakEndsAt || !$breakStartsAt || !$breakEndsAt;
        $nestedBookingsEnabled = SLN_Plugin::getInstance()->getSettings()->isNestedBookingsEnabled();

        SLN_Plugin::addLogVerbose(
            __CLASS__.sprintf(
                ' - validate attendant %s by date(%s) and duration(%s) | nestedBookings=%s',
                $attendant,
                $date->format('Ymd H:i'),
                $durationMinutes,
                $nestedBookingsEnabled ? 'YES' : 'NO'
            )
        );

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'. $durationMinutes .'minutes');

        SLN_Plugin::addLogVerbose(
            sprintf(
                '%s - Multi-attendant validation window %s -> %s | break %s -> %s | ignoreExisting=%s',
                __CLASS__,
                $startAt->format('H:i'),
                $endAt->format('H:i'),
                $breakStartsAt ? $breakStartsAt->format('H:i') : 'n/a',
                $breakEndsAt ? $breakEndsAt->format('H:i') : 'n/a',
                $this->getDayBookings()->isIgnoreServiceBreaks() ? 'YES' : 'NO'
            )
        );

        SLN_Plugin::addLogVerbose(
            sprintf(
                '%s - Attendant #%s validation window %s -> %s | break %s -> %s | ignoreExisting=%s',
                __CLASS__,
                method_exists($attendant, 'getId') ? $attendant->getId() : 'n/a',
                $startAt->format('H:i'),
                $endAt->format('H:i'),
                $breakStartsAt ? $breakStartsAt->format('H:i') : 'n/a',
                $breakEndsAt ? $breakEndsAt->format('H:i') : 'n/a',
                $ignoreExistingBreaks ? 'YES' : 'NO'
            )
        );

        $bookingOffsetEnabled   = SLN_Plugin::getInstance()->getSettings()->get('reservation_interval_enabled');
        $bookingOffset          = SLN_Plugin::getInstance()->getSettings()->get('minutes_between_reservation');

        if ($isLastService && $bookingOffsetEnabled) {
            $endAt = $endAt->modify('+'.$bookingOffset.' minutes');
        }

        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);
        if($duration && $times && $attendant->isNotAvailableOnDateDuration($times[0], $duration, $service)) {
            return SLN_Helper_Availability_ErrorHelper::doAttendantNotAvailable($attendant, $times[0]);
        }
        if (empty($times)) {
            return $this->validateAttendantOnTime($attendant, new SLN_DateTime($startAt->format('Y-m-d H:i:s')), $service);
        }
        foreach ($times as $time) {
            $b = $this->getDayBookings();
            $bTime = $b->getTime($time->format('H'), $time->format('i'));
            $outsideBreak = $noBreak || ($bTime < $breakStartsAt || $bTime >= $breakEndsAt);
            
            // Check if this slot is a break slot in an EXISTING booking (where nested bookings are allowed)
            $isExistingBreakSlot = $b->isBreakSlot($bTime);
            
            SLN_Plugin::addLogVerbose(
                sprintf(
                    '%s - Checking slot %s | outsideBreak=%s | existingBreakSlot=%s',
                    __CLASS__,
                    $time->format('H:i'),
                    $outsideBreak ? 'YES' : 'NO',
                    $isExistingBreakSlot ? 'YES' : 'NO'
                )
            );
            
            // Skip validation only when we are inside another booking's real break AND this booking
            // defines its own break window (nested booking inside someone else's pause).
            // If this booking has NO break, always validate: otherwise a false "break" slot would skip
            // attendant busy checks and allow overlap with another customer's work segment.
            if ($outsideBreak && ($noBreak || !$isExistingBreakSlot)) {
                if ($ret = $this->validateAttendantOnTime($attendant, $time, $service)) {
                    return $ret;
                }
            } elseif ($isExistingBreakSlot && !$noBreak) {
                SLN_Plugin::addLogVerbose(
                    sprintf(
                        '%s - ✅ Skipping validation for %s (existing break slot with nested bookings)',
                        __CLASS__,
                        $time->format('H:i')
                    )
                );
            }
        }
    }

    public function validateAttendants(
        $attendants,
        DateTime $date = null,
        DateTime $duration = null,
        SLN_Wrapper_ServiceInterface $service = null,
        DateTime $breakStartsAt = null,
        DateTime $breakEndsAt = null,
        $isLastService = false
    ){
        $date = empty($date) ? $this->date : $date;
        $durationMinutes = !empty($duration) ? SLN_Func::getMinutesFromDuration($duration) : 0;

        $noBreak = $this->getDayBookings()->isIgnoreServiceBreaks() || $breakStartsAt == $breakEndsAt || !$breakStartsAt || !$breakEndsAt;

        SLN_Plugin::addLogVerbose(
            __CLASS__.sprintf(
                ' - validate attendant %s by date(%s) and duration(%s)',
                print_r($attendants, true),
                $date->format('Ymd H:i'),
                $durationMinutes
            )
        );

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'. $durationMinutes .'minutes');

        $bookingOffsetEnabled   = SLN_Plugin::getInstance()->getSettings()->get('reservation_interval_enabled');
        $bookingOffset          = SLN_Plugin::getInstance()->getSettings()->get('minutes_between_reservation');

        if ($isLastService && $bookingOffsetEnabled) {
            $endAt = $endAt->modify('+'.$bookingOffset.' minutes');
        }

        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);

        foreach($attendants as $attendant){
            if($duration && $times && $attendant->isNotAvailableOnDateDuration($times[0], $duration, $service)) {
                return SLN_Helper_Availability_ErrorHelper::doAttendantNotAvailable($attendant, $times[0]);
            }
            if (empty($times)) {
                if ($ret = $this->validateAttendantOnTime($attendant, new SLN_DateTime($startAt->format('Y-m-d H:i:s')), $service)) {
                    return $ret;
                }
                continue;
            }
            foreach ($times as $time) {
                $b = $this->getDayBookings();
                $bTime = $b->getTime($time->format('H'), $time->format('i'));
                $outsideBreak = $noBreak || ($bTime < $breakStartsAt || $bTime >= $breakEndsAt);
                
                // Check if this slot is a break slot in an EXISTING booking (where nested bookings are allowed)
                $isExistingBreakSlot = $b->isBreakSlot($bTime);
                
                // Same rule as validateAttendant(): no-break bookings must not skip on isBreakSlot alone.
                if ($outsideBreak && ($noBreak || !$isExistingBreakSlot)) {
                    if ($ret = $this->validateAttendantOnTime($attendant, $time, $service)) {
                        return $ret;
                    }
                }
            }
        }
    }

    private function validateAttendantOnTime(SLN_Wrapper_AttendantInterface $attendant, SLN_DateTime $time, SLN_Wrapper_ServiceInterface $service=null)
    {
        // Request-scoped memoization: pure function of the current day model for a
        // given (attendant, minute, service). Collapses the heavy overlap between
        // consecutive candidate slots (see $slotValidationCache doc).
        $cacheKey = 'a|'.$attendant->getId().'|'.$time->format('H:i').'|'.($service ? $service->getId() : '0');
        if (array_key_exists($cacheKey, $this->slotValidationCache)) {
            if (self::$perfEnabled) { self::$perf['validation_hits']++; }
            return $this->slotValidationCache[$cacheKey];
        }
        if (self::$perfEnabled) { self::$perf['validation_misses']++; }

        return $this->slotValidationCache[$cacheKey] = $this->doValidateAttendantOnTime($attendant, $time, $service);
    }

    private function doValidateAttendantOnTime(SLN_Wrapper_AttendantInterface $attendant, SLN_DateTime $time, SLN_Wrapper_ServiceInterface $service=null)
    {
        SLN_Plugin::addLogVerbose(__CLASS__.sprintf(' checking time %s', $time->format('Ymd H:i')));
        $time = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));

        if ($attendant->isNotAvailableOnDate($time, $service)) {
            return SLN_Helper_Availability_ErrorHelper::doAttendantNotAvailable($attendant, $time);
        }

        $ids = $this->getDayBookings()->countAttendantsByHour($time->format('H'), $time->format('i'));
        $isAttendant_booked = isset($ids[$attendant->getId()]);
        $can_multiple = $attendant->canMultipleCustomers();
        
        SLN_Plugin::addLogVerbose(__CLASS__.' - Attendant #'.$attendant->getId().' validation at '.$time->format('H:i').': Booked='.$isAttendant_booked.', Count='.($ids[$attendant->getId()] ?? 0).', Can multiple='.$can_multiple);
        
        if($isAttendant_booked && $can_multiple){
            $plugin = SLN_Plugin::getInstance();
            $setts = $plugin->getSettings();

            $services = $this->getDayBookings()->getAttendantServiceIdsByHour($attendant->getId(),$time->format('H'), $time->format('i'));
            if(!empty($services) && is_array($services))
            foreach($services as $service_id){
                $service = new SLN_Wrapper_Service($service_id);
                $unit = $service->getUnitPerHour();
                $limit = $unit ? $unit : $setts->get('parallels_hour');
                $busy = $isAttendant_booked && $ids[$attendant->getId()] >= $limit;
            }

        }else{
            $busy = $isAttendant_booked && $ids[$attendant->getId()] >= 0;
        }

        if ( $busy ) {
            SLN_Plugin::addLogVerbose(__CLASS__.' - ❌ Attendant BUSY: count='.($ids[$attendant->getId()] ?? 0));
            return SLN_Helper_Availability_ErrorHelper::doAttendantBusy($attendant, $time);
        } else {
            SLN_Plugin::addLogVerbose(__CLASS__.' - ✅ Attendant available');
        }
    }

    public function validateBookingService(SLN_Wrapper_Booking_Service $bookingService, $isLastService = false)
    {
        if($bookingService->getCountServices() > 1){
            return $this->validateService(
                $bookingService->getService(),
                $bookingService->getStartsAt(),
                $bookingService->getTotalDuration(),
                $bookingService->getBreakStartsAt(),
                $bookingService->getBreakEndsAt(),
                $isLastService,
                $bookingService->getCountServices(),
            );
        }
        return $this->validateService(
            $bookingService->getService(),
            $bookingService->getStartsAt(),
            $bookingService->getTotalDuration(),
            $bookingService->getBreakStartsAt(),
            $bookingService->getBreakEndsAt(),
            $isLastService
        );
    }

    public function validateBookingAttendant(SLN_Wrapper_Booking_Service $bookingService, $isLastService = false){
        return $this->validateAttendant(
            $bookingService->getAttendant(),
            $bookingService->getStartsAt(),
            $this->getCountAwareDuration($bookingService),
            $bookingService->getService(),
            $bookingService->getBreakStartsAt(),
            $bookingService->getBreakEndsAt(),
            $isLastService
        );
    }

    public function validateBookingAttendants(SLN_Wrapper_booking_Service $bookingService, $isLastService = false){
        return $this->validateAttendants(
            $bookingService->getAttendant(),
            $bookingService->getStartsAt(),
            $this->getCountAwareDuration($bookingService),
            $bookingService->getService(),
            $bookingService->getBreakStartsAt(),
            $bookingService->getBreakEndsAt(),
            $isLastService
        );
    }

    /**
     * Return the booking service total duration extended by its quantity (count).
     *
     * The attendant must be free for the WHOLE quantity window (units are executed
     * back-to-back on the same attendant). A clone is returned so the booking service's
     * stored duration object is never mutated. For count = 1 the original duration is
     * returned unchanged.
     *
     * @return DateTime|null
     */
    private function getCountAwareDuration(SLN_Wrapper_Booking_Service $bookingService){
        $duration = $bookingService->getTotalDuration();
        $count    = (int) $bookingService->getCountServices();
        if ($count > 1 && $duration instanceof DateTime) {
            $duration     = clone $duration;
            $baseMinutes  = SLN_Func::getMinutesFromDuration($duration);
            $extraMinutes = $baseMinutes * ($count - 1);
            $duration->modify('+'.$extraMinutes.' minutes');
        }
        return $duration;
    }


    public function validateService(SLN_Wrapper_ServiceInterface $service, \DateTimeInterface $date = null, \DateTimeInterface $duration = null, \DateTimeInterface $breakStartsAt = null, \DateTimeInterface $breakEndsAt = null, $isLastService = false, $count = 1)
    {
        $date = empty($date) ? $this->date : $date;
        $totalDuration = $service->getTotalDuration();
        if($count > 1) {
            // Extend by the FULL per-unit duration (hours + minutes). The previous code used
            // only DateTime::format('i'), dropping the hours component, so services longer than
            // 59 minutes were under-reserved when booked with quantity > 1.
            $baseMinutes  = SLN_Func::getMinutesFromDuration($totalDuration);
            $extraMinutes = $baseMinutes * ($count - 1);
            $totalDuration->modify("+{$extraMinutes} minutes");
        }

        $duration = empty($totalDuration) ? $duration : $totalDuration;

        $noBreak = $this->getDayBookings()->isIgnoreServiceBreaks() || $breakStartsAt == $breakEndsAt || !$breakStartsAt || !$breakEndsAt;

        SLN_Plugin::addLog(
            __CLASS__.sprintf(
                ' - validate service %s by date(%s) and duration(%s)',
                $service,
                $date->format('Ymd H:i'),
                $duration->format('H:i')
            )
        );

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'.SLN_Func::getMinutesFromDuration($duration).'minutes');

        $bookingOffsetEnabled   = SLN_Plugin::getInstance()->getSettings()->get('reservation_interval_enabled');
        $bookingOffset          = SLN_Plugin::getInstance()->getSettings()->get('minutes_between_reservation');

        // if offset is considered, process differently
        if ($isLastService && $bookingOffsetEnabled) {
            $endAtWithoutOffset = clone $endAt;
            $endAtWithOffset = $endAt->modify('+'.$bookingOffset.' minutes');
            $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAtWithoutOffset);
            $timesWithOffset = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAtWithOffset);
            if($times && $ret = $this->validateServiceOnTime($service, $times[0], true)){
                return $ret;
            }
            foreach ($timesWithOffset as $time) {
                $bTime = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));
                if ($noBreak || ($bTime < $breakStartsAt || $bTime >= $breakEndsAt)) {
                    // if $time is part of duration without offset, validate normally
                    // if not, validate without booking and holiday rules (offset should consider only other bookings)
                    if ($ret = $this->validateServiceOnTime($service, $time, false, in_array($time, $times))) {
                        return $ret;
                    }
                }
            }
        } else {
            $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);
            if($times && $ret = $this->validateServiceOnTime($service, $times[0], true)){
                return $ret;
            }
            foreach ($times as $time) {
                $bTime = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));
                if ($noBreak || ($bTime < $breakStartsAt || $bTime >= $breakEndsAt)) {
                    if ($ret = $this->validateServiceOnTime($service, $time, false)) {
                        return $ret;
                    }
                }
            }
        }
    }

    private function validateServiceOnTime(SLN_Wrapper_ServiceInterface $service, SLN_DateTime $time, $checkDuration = true, $checkBookingAndHolidayRules = true)
    {
        // Request-scoped memoization: pure function of the current day model for a
        // given (service, minute, flags). See $slotValidationCache doc.
        $cacheKey = 's|'.$service->getId().'|'.$time->format('H:i').'|'.($checkDuration ? '1' : '0').'|'.($checkBookingAndHolidayRules ? '1' : '0');
        if (array_key_exists($cacheKey, $this->slotValidationCache)) {
            if (self::$perfEnabled) { self::$perf['validation_hits']++; }
            return $this->slotValidationCache[$cacheKey];
        }
        if (self::$perfEnabled) { self::$perf['validation_misses']++; }

        return $this->slotValidationCache[$cacheKey] = $this->doValidateServiceOnTime($service, $time, $checkDuration, $checkBookingAndHolidayRules);
    }

    private function doValidateServiceOnTime(SLN_Wrapper_ServiceInterface $service, SLN_DateTime $time, $checkDuration = true, $checkBookingAndHolidayRules = true)
    {
        SLN_Plugin::addLogVerbose(__CLASS__.sprintf(' checking time %s', $time->format('Ymd H:i')));
        $time = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));

        $avItems = $this->getItemsWithoutServiceOffset();
        $hItems  = $this->getHolidaysItems();
        $duration = null;
        if ($checkBookingAndHolidayRules) {
            if ($checkDuration) {
                $duration = $service->getDuration();
                $check = (!$avItems->isValidDatetimeDuration($time, $duration) || !$hItems->isValidDatetimeDuration($time, $duration));
            } else {
                $check = (!$avItems->isValidDatetime($time) || !$hItems->isValidDatetime($time));
            }
            if ($check) {
                return SLN_Helper_Availability_ErrorHelper::doServiceNotEnoughTime($service, $time);
            }
        }

        // Skip parallel booking check if this is a break slot (nested bookings allowed)
        $isBreakSlot = $this->getDayBookings()->isBreakSlot($time);
        if (!$isBreakSlot && !$this->isValidOnlyTime($time)) {
            SLN_Plugin::addLogVerbose(sprintf('[validateServiceOnTime] %s failed parallel check (not a break slot)', $time->format('H:i')));
            return SLN_Helper_Availability_ErrorHelper::doLimitParallelBookings($time);
        } elseif ($isBreakSlot) {
            SLN_Plugin::addLogVerbose(sprintf('[validateServiceOnTime] %s is break slot - skipping parallel check', $time->format('H:i')));
        }
        if ($checkDuration && $service->isNotAvailableOnDate($time)) {
            return SLN_Helper_Availability_ErrorHelper::doServiceNotAvailableOnDate($service, $time);
        }
        if ($ret = $this->validateServiceAttendantsOnTime($service, $time, $duration)) {
            return $ret;
        }
        $ids = $this->getDayBookings()->countServicesByHour($time->format('H'), $time->format('i'));
        $unit = $service->getUnitPerHour();
        
        SLN_Plugin::addLogVerbose(__CLASS__.' - Service #'.$service->getId().' validation at '.$time->format('H:i').': Unit='.$unit.', Current count='.($ids[$service->getId()] ?? 0));
        
        if (
            $unit > 0
            && isset($ids[$service->getId()])
            && $ids[$service->getId()] >= $unit
        ) {
            SLN_Plugin::addLogVerbose(__CLASS__.' - ❌ Service FULL: '.$ids[$service->getId()].' >= '.$unit);
            return SLN_Helper_Availability_ErrorHelper::doServiceFull($service, $time);
        } else {
            SLN_Plugin::addLogVerbose(__CLASS__.' - ✅ Service available: '.($ids[$service->getId()] ?? 0).' < '.$unit);
        }

        if ($ret = $this->validateServiceResourcesOnTime($service, $time, $duration)) {
            return $ret;
        }
    }

    private function validateServiceAttendantsOnTime(SLN_Wrapper_ServiceInterface $service, DateTime $time, DateTime $duration = null)
    {
        if (!$this->attendantsEnabled) {
            return;
        }
        if (!$service->isAttendantsEnabled()) {
            return;
        }
        $attendants = $service->getAttendants();
        foreach ($attendants as $k => $attendant) {
            if ($this->validateAttendant($attendant, $time, $duration, $service)) {
                unset($attendants[$k]);
            }
        }

        if (empty($attendants) || $service->isMultipleAttendantsForServiceEnabled() && count($attendants) < $service->getCountMultipleAttendants()) {
            return SLN_Helper_Availability_ErrorHelper::doServiceAllAttendantsBusy($service, $time);
        }
    }

    public function validateServiceFromOrder(SLN_Wrapper_ServiceInterface $service, SLN_Wrapper_Booking_Services $bookingServices)
    {
        if($service->isSecondary()) {
            $serviceDisplayMode = $service->getMeta('secondary_display_mode');
            if ($serviceDisplayMode !== 'always') {
                foreach($bookingServices->getItems() as $bookingService) {
                    if ($bookingService->getService()->getId() !== $service->getId() && !$bookingService->getService()->isSecondary()) {
                        if ($serviceDisplayMode === 'service') {
                            if(in_array($bookingService->getService()->getId(), (array)$service->getMeta('secondary_parent_services'))) {
                                return array();
                            }
                        }
                        elseif ($serviceDisplayMode === 'category') {
                            $serviceCategories = wp_get_post_terms($service->getId(), 'sln_service_category', array( "fields" => "ids" ) );
                            $serviceCategory   = reset($serviceCategories);

                            $bServiceCategories = wp_get_post_terms($bookingService->getService()->getId(), 'sln_service_category', array( "fields" => "ids" ) );
                            if(in_array($serviceCategory, $bServiceCategories)) {
                                return array();
                            }
                        }
                    }
                }

                if ($serviceDisplayMode === 'service') {
                    return SLN_Helper_Availability_ErrorHelper::doSecondaryServiceNotAvailableWOParentService($service);
                }
                elseif ($serviceDisplayMode === 'category') {
                    return SLN_Helper_Availability_ErrorHelper::doSecondaryServiceNotAvailableWOSameCategoryPrimaryService($service);
                }
            }
        }

	    return array();
    }

    /**
     * @param array $servicesIds
     *
     * @return array of validated services
     */
    public function returnValidatedServices(array $servicesIds)
    {
        $date = $this->date;
        $bb = SLN_Plugin::getInstance()->getBookingBuilder();
        $bookingServices = SLN_Wrapper_Booking_Services::build(array_fill_keys($servicesIds, 0), $date, 0, $bb->getCountServices());
        $validated = array();
        $primaryServicesCount	= $this->settings->get('primary_services_count');
        $secondaryServicesCount = $this->settings->get( 'secondary_services_count' );

        foreach ($bookingServices->getItems() as $bookingService) {

            if($primaryServicesCount) {

                if (!$bookingService->getService()->isSecondary()) {
                    $_validated = array_filter($validated, function ($serviceID) {
                        return !SLN_Plugin::getInstance()->createService($serviceID)->isSecondary();
                    });
                    if (count($_validated) >= $primaryServicesCount) {
                        break;
                    }
                }
	        }

            if( $secondaryServicesCount ){
                if( $bookingService->getService()->isSecondary() ){
                    $_validated = array_filter( $validated, function( $serviceID ){
                        return SLN_Plugin::getInstance()->createService( $serviceID )->isSecondary();
                    });
                   /* if( count( $_services ) >= $secondaryServicesCount ){
                        break;
                    }*/
                }
            }

            $serviceErrors = $this->validateService(
                $bookingService->getService(),
                $bookingService->getStartsAt(),
                null,
                $bookingService->getBreakStartsAt(),
                $bookingService->getBreakEndsAt()
            );
            if (empty($serviceErrors)) {
                $validated[] = $bookingService->getService()->getId();
            } else {
                break;
            }
        }

        $bb = SLN_Plugin::getInstance()->getBookingBuilder();
        $bookingServices = SLN_Wrapper_Booking_Services::build(array_fill_keys($validated, 0), $date, 0, $bb->getCountServices());
        $validated       = array();
        foreach ($bookingServices->getItems() as $bookingService) {
            $serviceErrors = $this->validateServiceFromOrder($bookingService->getService(), $bookingServices);
            if (empty($serviceErrors)) {
                $validated[] = $bookingService->getService()->getId();
            } else {
                break;
            }
        }

        return $validated;
    }

    /**
     * @param array $order
     * @param SLN_Wrapper_ServiceInterface[] $newServices
     *
     * @return array
     */
    public function checkEachOfNewServicesForExistOrder($order, $newServices, $altMode = false)
    {
        $ret = array();
        $date = $this->date;

        $s = $this->settings;
        $bookingOffsetEnabled = $s->get('reservation_interval_enabled');
        $bookingOffset = $s->get('minutes_between_reservation');
        $isMultipleAttSelection = $s->get('m_attendant_enabled');
        $interval = $this->settings->getInterval();
        $primaryServicesCount   = $s->get('primary_services_count');
        $secondaryServicesCount = $s->get( 'secondary_services_count' );

        foreach ($newServices as $service) {
            $services = $order;
            if (!in_array($service->getId(), $services)) {

                if( $primaryServicesCount && !$service->isSecondary() ) {
                    $_services = array_filter($services, function ($serviceID) {
                        return !SLN_Plugin::getInstance()->createService($serviceID)->isSecondary();
                    });

                    if(count($_services) >= $primaryServicesCount) {
                        $ret[$service->getId()] = array(sprintf(
                            // translators: %d will be replaced by the primary services count
                            __('You can select up to %d items', 'salon-booking-system'), $primaryServicesCount));
                        continue;
                    }
                }

                if( $secondaryServicesCount && $service->isSecondary() ){
                    $_services = array_filter( $services, function( $serviceID ){
                        return SLN_Plugin::getInstance()->createService( $serviceID )->isSecondary();
                    } );

                    if( count( $_services ) >= $secondaryServicesCount ){
                        $ret[$service->getId()] = array( sprintf(
                            // translators: %d will be replaced by the secondary services count
                            __('You can select up to %d items', 'salon-booking-system'), $secondaryServicesCount ) );
                        continue;
                    }
                }

                $bb = SLN_Plugin::getInstance()->getBookingBuilder();
                $services[] = $service->getId();
                $bookingServices = SLN_Wrapper_Booking_Services::build(array_fill_keys($services, 0), $date, 0, $bb->getCountServices());
                $availAtts = null;
                foreach ($bookingServices->getItems() as $bookingService) {
                    $serviceErrors = array();
                    $errorMsg = '';

	                $serviceErrors = $this->validateServiceFromOrder($bookingService->getService(), $bookingServices);

	                if (!$altMode) {
		                if (empty($serviceErrors) && $bookingServices->isLast($bookingService) && $bookingOffsetEnabled) {
			                $offsetStart = $bookingService->getEndsAt();
			                $offsetEnd = clone $offsetStart;
			                $offsetEnd->modify('+'.$bookingOffset.' minutes');
			                $serviceErrors = $this->validateTimePeriod($offsetStart, $offsetEnd);
		                }

		                if (empty($serviceErrors)) {
			                $serviceErrors = $this->validateService(
					                $bookingService->getService(),
					                $bookingService->getStartsAt(),
					                null,
					                $bookingService->getBreakStartsAt(),
					                $bookingService->getBreakEndsAt()
			                );
		                }

		                if (empty($serviceErrors) && $this->attendantsEnabled && !$isMultipleAttSelection && $bookingService->getService()->isAttendantsEnabled()) {
			                $availAtts = $this->getAvailableAttendantForService($availAtts, $bookingService);
			                if (empty($availAtts)) {
				                $errorMsg = __(
						                'An assistant for selected services can\'t perform this service',
						                'salon-booking-system'
				                );
				                $serviceErrors = array($errorMsg);
			                }
		                }
	                }

                    if (!empty($serviceErrors)) {
                        $ret[$service->getId()] = $this->processServiceErrors(
                            $bookingServices,
                            $bookingService,
                            $service,
                            $serviceErrors
                        );
                        break;
                    }
                }

                if (!isset($ret[$service->getId()])) {
                    $ret[$service->getId()] = array();
                }
            }
        }
        return $ret;
    }

    public function checkExclusiveServices($order, $services) {
        $ret = array();
        $exclusiveServiceId = false;
        foreach ($services as $service) {
            if (in_array($service->getId(), $order)) {
                if($service->isExclusive()) {
                    $exclusiveServiceId = $service->getId();
                    break;
                }
            }
        }
        foreach ($services as $service) {
            if($exclusiveServiceId && $exclusiveServiceId != $service->getId()) {
                $errorMsg = __(
                    'This service is not available with exclusive service.',
                    'salon-booking-system'
                );
                $ret[$service->getId()] = array($errorMsg);
            }
        }
        return $ret;
    }

    private function processServiceErrors(
        SLN_Wrapper_Booking_Services $bookingServices,
        SLN_Wrapper_Booking_Service $bookingService,
        SLN_Wrapper_ServiceInterface $service,
        $serviceErrors
    ) {
        if ($bookingService->getService()->getId() == $service->getId()) {
            $error = $serviceErrors[0];
        } else {
            $tmp = $bookingServices->findByService($service->getId());
            $error = !empty($errorMsg) ? $errorMsg : __(
                    'You already selected service at',
                    'salon-booking-system'
                ).($tmp ? ' '.$tmp->getStartsAt()->format('H:i') : '');
        }

        return array($error);
    }

    public function getAvailableAttendantForService($availAtts = null, SLN_Wrapper_Booking_Service $bookingService = null)
    {
        $intersect = $this->getAvailableAttsIdsForBookingService($bookingService);
        if (is_null($availAtts)) {
            $availAtts = $intersect;
        }
        $availAtts = array_intersect($availAtts, $intersect);

        return $availAtts;
    }

    public function getAvailableAttsIdsForBookingService(SLN_Wrapper_Booking_Service $bs)
    {
        return $this->getAvailableAttsIdsForServiceOnTime(
            $bs->getService(),
            $bs->getStartsAt(),
            $bs->getTotalDuration(),
            $bs->getBreakStartsAt(),
            $bs->getBreakEndsAt()
        );
    }

    public function getAvailableAttsIdsForServiceOnTime(
        SLN_Wrapper_ServiceInterface $service,
        DateTime $date = null,
        DateTime $duration = null,
        DateTime $breakStartsAt = null,
        DateTime $breakEndsAt = null
    ) {
        $date = empty($date) ? $this->date : $date;
        $duration = empty($duration) ? $service->getTotalDuration() : $duration;
        $ret = array();

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'.SLN_Func::getMinutesFromDuration($duration).'minutes');

        $attendants = apply_filters('sln.availability.getAvailableAttsIdsForServiceOnTime.attendants', $service->getAttendants());
        if ($this->excludeHiddenFromFrontend) {
            $attendants = array_filter($attendants, function ($attendant) {
                // Guard with is_callable: add-ons (e.g. Multi-Shops) may pass
                // shop-wrapped attendant objects through this filter. is_callable
                // covers both real methods and decorators delegating via __call.
                return !(is_callable(array($attendant, 'isHideOnFrontend')) && $attendant->isHideOnFrontend());
            });
        }
        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);
        foreach ($times as $time) {
            foreach ($attendants as $k => $attendant) {
                if ($this->validateAttendant($attendant, $time, null, $service, $breakStartsAt, $breakEndsAt)) {
                    unset($attendants[$k]);
                }
            }
        }
        foreach ($attendants as $attendant) {
            $ret[] = $attendant->getId();
        }

        return $ret;
    }

    public function addAttendantForServices(SLN_Wrapper_Booking_Services $bookingServices)
    {
        if ($this->settings->isMultipleAttendantsEnabled()) {
            $this->addMultipleAttendantForServices($bookingServices);
        } else {
            $this->addSingleAttendantForServices($bookingServices);
        }
    }

    private function addMultipleAttendantForServices(SLN_Wrapper_Booking_Services $bookingServices)
    {
        foreach ($bookingServices->getItems() as $bookingService) {
            $service = $bookingService->getService();
            if (!$service->isAttendantsEnabled()) {
                continue;
            }

            $availAttsForEachService[$service->getId()]
                = $availAttsForCurrentService = $this->getAvailableAttsIdsForBookingService($bookingService);
            if (empty($availAttsForCurrentService)) {
                $msg = sprintf(
                    // translators: %s will be replaced by the service name
                    __('No attendants available for %s. Please try a different date or contact us.', 'salon-booking-system'),
                    $service->getName()
                );
                SLN_Plugin::addLog('[addAttendantForServices] No available attendants for service ' . $service->getId() . ' (' . $service->getName() . ') at ' . ($this->date ? $this->date->format('Y-m-d H:i') : 'no-date'));
                throw new SLN_Exception(esc_html($msg));
            }

            if (is_object($bookingService->getAttendant())) {
                $selectedAttId = $bookingService->getAttendant()->getId();
                if (!in_array($selectedAttId, $availAttsForCurrentService)) {
                    throw new SLN_Exception(
                        sprintf(
                            // translators: s%1$ will be replaced by attendant name, %2$s will be replaced by service name
                            esc_html__('Attendant %1$s isn\'t available for %2$s service', 'salon-booking-system'),
                            esc_html($bookingService->getAttendant()->getName()),
                            esc_html($service->getName())
                        )
                    );
                }
            } else {
                // Check if service requires multiple attendants (e.g., Couple massage)
                if ($service->isMultipleAttendantsForServiceEnabled()) {
                    $countRequired = intval($service->getCountMultipleAttendants());
                    $attendants = array();
                    
                    // Ensure we have enough available attendants
                    if (count($availAttsForCurrentService) < $countRequired) {
                        throw new SLN_Exception(
                            sprintf(
                                // translators: %1$s will be replaced by service name, %2$d will be replaced by required count
                                esc_html__('Not enough attendants available for %1$s service. Required: %2$d', 'salon-booking-system'),
                                esc_html($service->getName()),
                                $countRequired
                            )
                        );
                    }
                    
                    // Randomly select required number of attendants
                    $selectedAttIds = array_rand(array_flip($availAttsForCurrentService), $countRequired);
                    // array_rand returns single value if count=1, array if count>1
                    if (!is_array($selectedAttIds)) {
                        $selectedAttIds = array($selectedAttIds);
                    }
                    
                    foreach ($selectedAttIds as $attId) {
                        $attendants[] = SLN_Plugin::getInstance()->createAttendant($attId);
                    }
                    
                    $bookingService->setAttendant($attendants);
                    
                    // CRITICAL: Also update the Builder's data array with attendant IDs
                    // The Builder stores attendant data separately from BookingService objects
                    $bb = SLN_Plugin::getInstance()->getBookingBuilder();
                    if ($bb && $bb->hasService($service)) {
                        $bb->setAttendants($selectedAttIds, $service);
                    }
                    
                    SLN_Plugin::addLog('[Availability] Assigned ' . count($attendants) . ' attendants (' . implode(', ', $selectedAttIds) . ') for service ' . $service->getName());
                } else {
                    // Single attendant service
                    SLN_Plugin::getInstance();
                    $selectedAttId = $availAttsForCurrentService[array_rand($availAttsForCurrentService)];
                    $attendant     = SLN_Plugin::getInstance()->createAttendant($selectedAttId);
                    $bookingService->setAttendant($attendant);
                }
            }
        }
    }

    private function addSingleAttendantForServices(SLN_Wrapper_Booking_Services $bookingServices)
    {
        $availAttsForAllServices = null;
        foreach ($bookingServices->getItems() as $bookingService) {
            $service = $bookingService->getService();
            if (!$service->isAttendantsEnabled()) {
                continue;
            }

            $availAttsForAllServices = $this->getAvailableAttendantForService(
                $availAttsForAllServices,
                $bookingService
            );
            if (is_object($bookingService->getAttendant())) {
                $availAttsForAllServices = array_intersect(
                    $availAttsForAllServices,
                    array($bookingService->getAttendant()->getId())
                );
            }

            if (empty($availAttsForAllServices)) {
                SLN_Plugin::addLog('[addAttendantForServices] No available attendants for selected services at ' . ($this->date ? $this->date->format('Y-m-d H:i') : 'no-date'));
                throw new SLN_Exception(
                    esc_html__('No attendants available for the selected services. Please try a different date or contact us.', 'salon-booking-system')
                );
            }
        }

        if (!empty($availAttsForAllServices)) {
            foreach ($bookingServices->getItems() as $bookingService) {
                $service = $bookingService->getService();
                if (!$service->isAttendantsEnabled()) {
                    continue;
                }
                
                // CRITICAL FIX: Check if THIS service requires multiple attendants
                if ($service->isMultipleAttendantsForServiceEnabled()) {
                    $countRequired = intval($service->getCountMultipleAttendants());
                    
                    SLN_Plugin::addLog('[Availability-Single] Service ' . $service->getName() . ' requires ' . $countRequired . ' attendants');
                    SLN_Plugin::addLog('[Availability-Single] Available attendants: ' . print_r($availAttsForAllServices, true));
                    
                    // Ensure we have enough available attendants
                    if (count($availAttsForAllServices) < $countRequired) {
                        throw new SLN_Exception(
                            sprintf(
                                // translators: %1$s will be replaced by service name, %2$d will be replaced by required count
                                esc_html__('Not enough attendants available for %1$s service. Required: %2$d', 'salon-booking-system'),
                                esc_html($service->getName()),
                                $countRequired
                            )
                        );
                    }
                    
                    // Randomly select required number of DIFFERENT attendants
                    // array_rand with count > 1 returns array of keys (not values!)
                    $flipped = array_flip($availAttsForAllServices);
                    SLN_Plugin::addLog('[Availability-Single] Flipped array: ' . print_r($flipped, true));
                    
                    $selectedKeys = array_rand($flipped, $countRequired);
                    // array_rand returns single value if count=1, array if count>1
                    if (!is_array($selectedKeys)) {
                        $selectedKeys = array($selectedKeys);
                    }
                    
                    SLN_Plugin::addLog('[Availability-Single] Selected keys from array_rand: ' . print_r($selectedKeys, true));
                    
                    // The keys ARE the attendant IDs (because we flipped the array)
                    $selectedAttIds = $selectedKeys;
                    
                    SLN_Plugin::addLog('[Availability-Single] Final selected attendant IDs: ' . print_r($selectedAttIds, true));
                    
                    $attendants = array();
                    foreach ($selectedAttIds as $attId) {
                        $attendants[] = SLN_Plugin::getInstance()->createAttendant($attId);
                    }
                    
                    $bookingService->setAttendant($attendants);
                    
                    // CRITICAL: Also update the Builder's data array with attendant IDs
                    $bb = SLN_Plugin::getInstance()->getBookingBuilder();
                    if ($bb && $bb->hasService($service)) {
                        $bb->setAttendants($selectedAttIds, $service);
                        SLN_Plugin::addLog('[Availability-Single] Updated Builder with IDs: ' . print_r($selectedAttIds, true));
                    }
                    
                    SLN_Plugin::addLog('[Availability-Single] Assigned ' . count($attendants) . ' attendants (' . implode(', ', $selectedAttIds) . ') for service ' . $service->getName());
                } else {
                    // Single attendant for this service
                    $selectedAttId = $availAttsForAllServices[array_rand($availAttsForAllServices)];
                    $attendant     = SLN_Plugin::getInstance()->createAttendant($selectedAttId);
                    $bookingService->setAttendant($attendant);
                }
            }
        }
    }

    public function validateTimePeriod($start, $end)
    {
        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $start, $end);
        
        // Check if the START time is a break slot with nested bookings allowed
        $startTime = $this->getDayBookings()->getTime($start->format('H'), $start->format('i'));
        $startsInBreak = $this->getDayBookings()->isBreakSlot($startTime);
        
        if ($startsInBreak) {
            return array();
        }
        
        foreach ($times as $time) {
            $time = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));
            
            if (!$this->isValidOnlyTime($time)) {
                return array(__('Limit of parallels bookings at ', 'salon-booking-system').$time->format('H:i'));
            }
        }

        return array();
    }

    /**
     * @return SLN_Helper_AvailabilityItems
     */
    public function getItems()
    {
        if (!isset($this->items)) {
            $this->items = $this->settings->getNewAvailabilityItems();
        }

        return $this->items;
    }

    private function getOffset()
    {
        if(!$this->offset){
            $duration = SLN_Plugin::getInstance()->getRepository(
                SLN_Plugin::POST_TYPE_SERVICE
            )->getMinPrimaryServiceDuration();
            $this->offset = SLN_Func::getMinutesFromDuration($duration);
        }
        return $this->offset;
    }

    public function resetItems()
    {
        $this->hoursBefore                  = null;
        $this->items                        = null;
        $this->itemsWithoutServiceOffset    = null;
        $this->holidayItems                 = null;
        $this->holidayItemsWithWeekDayRules = null;
    }

    /**
     * @return SLN_Helper_AvailabilityItems
     */
    public function getItemsWithoutServiceOffset()
    {
        if (!isset($this->itemsWithoutServiceOffset)) {
            $this->itemsWithoutServiceOffset = $this->settings->getAvailabilityItems();
        }

        return $this->itemsWithoutServiceOffset;
    }

    /**
     * @return SLN_Helper_HolidayItems
     */
    public function getHolidaysItems()
    {
        if(!isset($this->holidayItems)){
            $this->holidayItems = $this->settings->getHolidayItems();
        }
        return $this->holidayItems;
    }

    /**
     * @return SLN_Helper_HolidayItems
     */
    public function getHolidaysItemsWithWeekDayRules($weekDayRules)
    {
        if (!isset($this->holidayItemsWithWeekDayRules)) {
            $this->holidayItemsWithWeekDayRules = $this->settings->getNewHolidayItems();
            $this->holidayItemsWithWeekDayRules->setWeekDayRules($weekDayRules);
        }

        return $this->holidayItemsWithWeekDayRules;
    }

    public function isValidDate(Date $date)
    {
        $this->setDate($date->getDateTime());
        $countDay = $this->settings->get('parallels_day');

        return !($countDay && $this->getBookingsDayCount() >= $countDay);
    }

    public function isValidTime($date)
    {
        if (!$this->isValidDate(Date::create($date))) {
            return false;
        }

        return $this->isValidOnlyTime($date);
    }

    public function isValidOnlyTime($date)
    {
        $countHour = $this->settings->get('parallels_hour');
        
        // Skip parallel booking check for break slots (where nested bookings are allowed)
        $time = $this->getDayBookings()->getTime($date->format('H'), $date->format('i'));
        if ($this->getDayBookings()->isBreakSlot($time)) {
            return $date >= $this->initialDate;
        }

        return ($date >= $this->initialDate) && !($countHour && $this->getBookingsHourCount(
                $date->format('H'),
                $date->format('i')
            ) >= $countHour);
    }

    public function getFreeMinutes($date)
    {
        $date = clone $date;
        $ret = 0;
        $interval = $this->settings->getInterval();
        $max = 24 * 60;

        $avItems = $this->getItems();
        while ($avItems->isValidDatetime($date) && $this->isValidTime($date) && $ret <= $max) {
            $ret += $interval;
            $date->modify(sprintf('+%s minutes', $interval));
        }

        return $ret;
    }

    public function getWorkTimes(Date $date)
    {
        $ret     = array();
        $avItems = $this->settings->getNewAvailabilityItems();
        $hItems  = $this->getHolidaysItems();
        foreach (SLN_Func::getMinutesIntervals() as $time) {
            $d = new SLN_DateTime($date->toString().' '.$time);
            if (
                $avItems->isValidDatetime($d)
                && $hItems->isValidDatetime($d)
            ) {
                $ret[$time] = $d;
            }
        }
        SLN_Plugin::addLog(__CLASS__.' getWorkTimes '.print_r($ret, true));

        return $ret;
    }
    
    /**
     * Get working time slots for a date without time-based restrictions (Hours Before, past dates)
     * Used for calendar stats/bar display to show historical data
     */
    public function getWorkTimesForStats(Date $date)
    {
        $ret     = array();
        $avItems = $this->settings->getNewAvailabilityItems();
        $hItems  = $this->getHolidaysItems();
        
        // Check if the date itself is valid (day of week, availability period, holidays)
        if (!$avItems->isValidDate($date) || !$hItems->isValidDate($date)) {
            return $ret; // Date not valid (wrong day of week or holiday)
        }
        
        // Get all time slots that match the availability rules (ignoring "Hours Before")
        foreach (SLN_Func::getMinutesIntervals() as $time) {
            $d = new SLN_DateTime($date->toString().' '.$time);
            $timeObj = Time::create($d);
            
            // Check if this time is within the business hours (without date/time restrictions)
            $dateSubset = $avItems->getDateSubset($date);
            foreach ($dateSubset as $av) {
                if ($av->isValidDate($date) && $av->isValidTime($timeObj)) {
                    $ret[$time] = $d;
                    break; // Time slot is valid, move to next
                }
            }
        }
        
        return $ret;
    }

    public function getInterval()
    {
        return $this->settings->get('interval');
    }

    public function getAvailableResourcesIdsForBookingService(SLN_Wrapper_Booking_Service $bs)
    {
        return $this->getAvailableResourcesIdsForServiceOnTime(
            $bs->getService(),
            $bs->getStartsAt(),
            $bs->getTotalDuration(),
            $bs->getBreakStartsAt(),
            $bs->getBreakEndsAt()
        );
    }

    public function getAvailableResourcesIdsForServiceOnTime(
        SLN_Wrapper_ServiceInterface $service,
        DateTime $date = null,
        DateTime $duration = null,
        DateTime $breakStartsAt = null,
        DateTime $breakEndsAt = null
    ) {
        $date = empty($date) ? $this->date : $date;
        $duration = empty($duration) ? $service->getTotalDuration() : $duration;
        $ret = array();

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'.SLN_Func::getMinutesFromDuration($duration).'minutes');

        $resources = apply_filters('sln.availability.getAvailableResourcesIdsForServiceOnTime.resources', $service->getResources());
        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);
        foreach ($times as $time) {
            foreach ($resources as $k => $resource) {
                if ($this->validateResource($resource, $time, null, $breakStartsAt, $breakEndsAt)) {
                    unset($resources[$k]);
                }
            }
        }
        foreach ($resources as $resource) {
            $ret[] = $resource->getId();
        }

        return $ret;
    }

    public function validateResource(
        SLN_Wrapper_ResourceInterface $resource,
        DateTime $date = null,
        DateTime $duration = null,
        DateTime $breakStartsAt = null,
        DateTime $breakEndsAt = null,
        $isLastService = false
    ) {
        $date = empty($date) ? $this->date : $date;
        $durationMinutes = !empty($duration) ? SLN_Func::getMinutesFromDuration($duration) : 0;

        $noBreak = $this->getDayBookings()->isIgnoreServiceBreaks() || $breakStartsAt == $breakEndsAt || !$breakStartsAt || !$breakEndsAt;

        SLN_Plugin::addLog(
            __CLASS__.sprintf(
                ' - validate resource %s by date(%s) and duration(%s)',
                $resource,
                $date->format('Ymd H:i'),
                $durationMinutes
            )
        );

        $startAt = clone $date;
        $endAt = clone $date;
        $endAt->modify('+'. $durationMinutes .'minutes');

        $bookingOffsetEnabled   = SLN_Plugin::getInstance()->getSettings()->get('reservation_interval_enabled');
        $bookingOffset          = SLN_Plugin::getInstance()->getSettings()->get('minutes_between_reservation');

        if ($isLastService && $bookingOffsetEnabled) {
            $endAt = $endAt->modify('+'.$bookingOffset.' minutes');
        }

        $times = SLN_Func::filterTimes($this->getMinutesIntervals(), $startAt, $endAt);
        foreach ($times as $time) {
            $b = $this->getDayBookings();
            $bTime = $b->getTime($time->format('H'), $time->format('i'));
            if ($noBreak || ($bTime < $breakStartsAt || $bTime >= $breakEndsAt)) {
                if ($ret = $this->validateResourceOnTime($resource, $time)) {
                    return $ret;
                }
            }
        }
    }

    private function validateResourceOnTime(SLN_Wrapper_ResourceInterface $resource, SLN_DateTime $time)
    {
        SLN_Plugin::addLogVerbose(__CLASS__.sprintf(' checking time %s', $time->format('Ymd H:i')));
        $time = $this->getDayBookings()->getTime($time->format('H'), $time->format('i'));

        $booked_resources = $this->getDayBookings()->countResourcesByHour($time->format('H'), $time->format('i'));
        $resources_unit = isset($booked_resources[$resource->getId()]) ? $resource->getUnitPerHour() - $booked_resources[$resource->getId()] : $resource->getUnitPerHour();
        $free_resources_unit = $resources_unit > 0 ? $resources_unit : 0;

        if (!$free_resources_unit) {
            return SLN_Helper_Availability_ErrorHelper::doResourceBusy($resource, $time);
        }
    }

    private function validateServiceResourcesOnTime(SLN_Wrapper_ServiceInterface $service, DateTime $time, DateTime $duration = null)
    {
        $resources = $service->getResources();

        if (empty($resources)) {
            return;
        }

        foreach ($resources as $k => $resource) {
            if ($this->validateResource($resource, $time, $duration)) {
                unset($resources[$k]);
            }
        }

        if (empty($resources)) {
            return SLN_Helper_Availability_ErrorHelper::doServiceAllResourcesBusy($service, $time);
        }
    }

    public function validateResourceService(SLN_Wrapper_ResourceInterface $resource, SLN_Wrapper_ServiceInterface $service)
    {
        if (!in_array($service->getId(), $resource->getServices())) {
            return array(
                __('This resource is not available for the selected service', 'salon-booking-system'),
            );
        }
    }

}
