<?php

class OsBookingHelper {


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 * Returns full amount to charge in database format 1999.0000
	 *
	 */
	public static function calculate_full_amount_for_booking( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$amount = self::calculate_full_amount_for_service( $booking );
		$amount = apply_filters( 'latepoint_calculate_full_amount_for_booking', $amount, $booking );
		$amount = OsMoneyHelper::pad_to_db_format( $amount );

		return $amount;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 */
	public static function calculate_full_amount_for_service( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$service            = new OsServiceModel( $booking->service_id );
		$amount_for_service = $service->get_full_amount_for_duration( $booking->duration );
		$amount_for_service = apply_filters( 'latepoint_full_amount_for_service', $amount_for_service, $booking );

		return $amount_for_service;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return mixed|void
	 *
	 * Returns deposit amount to charge in database format 1999.0000
	 *
	 */
	public static function calculate_deposit_amount_to_charge( OsBookingModel $booking ) {
		if ( ! $booking->service_id ) {
			return 0;
		}
		$service = new OsServiceModel( $booking->service_id );
		$amount  = $service->get_deposit_amount_for_duration( $booking->duration );
		$amount  = apply_filters( 'latepoint_deposit_amount_for_service', $amount, $booking );
		$amount  = OsMoneyHelper::pad_to_db_format( $amount );

		return $amount;
	}


	/**
	 * @param array $item_data
	 *
	 * @return OsBookingModel
	 */
	public static function build_booking_model_from_item_data( array $item_data ): OsBookingModel {
		$booking = new OsBookingModel();
		if ( $item_data['id'] ) {
			$booking = $booking->load_by_id( $item_data['id'] );
			if ( ! $booking ) {
				$booking = new OsBookingModel();
			}
		}
		$booking->set_data( $item_data );
		// get buffers from service and set to booking object
		if ( ! isset( $item_data['buffer_before'] ) && ! isset( $item_data['buffer_after'] ) ) {
			$booking->set_buffers();
		}
		if ( empty( $booking->end_time ) ) {
			$booking->calculate_end_date_and_time();
		}
		if ( empty( $booking->end_date ) ) {
			$booking->calculate_end_date();
		}
		$booking->set_utc_datetimes();

		return $booking;
	}

	public static function get_booking_id_and_manage_ability_by_key( string $key ) {
		$booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_agent', $key );
		if ( $booking_id ) {
			return [
				'booking_id' => $booking_id,
				'for'        => 'agent',
			];
		}

		$booking_id = OsMetaHelper::get_booking_id_by_meta_value( 'key_to_manage_for_customer', $key );
		if ( $booking_id ) {
			return [
				'booking_id' => $booking_id,
				'for'        => 'customer',
			];
		}

		return false;
	}

	public static function is_action_allowed( string $action, OsBookingModel $booking, string $key = '' ) {
		$is_allowed = false;
		if ( empty( $booking->id ) ) {
			return false;
		}
		if ( ! in_array( $action, [ 'cancel', 'reschedule' ] ) ) {
			return false;
		}
		$action_result = false;
		switch ( $action ) {
			case 'cancel':
				$action_result = OsCustomerHelper::can_cancel_booking( $booking );
				break;
			case 'reschedule':
				$action_result = OsCustomerHelper::can_reschedule_booking( $booking );
				break;
		}
		if ( ! empty( $key ) ) {
			// key is passed, check if allowed through key
			$agent_key_meta    = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_agent', $booking->id );
			$customer_key_meta = OsMetaHelper::get_booking_meta_by_key( 'key_to_manage_for_customer', $booking->id );
			if ( $key == $agent_key_meta ) {
				// agent can do everything, no need to check for action
				$is_allowed = true;
			} elseif ( $key == $customer_key_meta ) {
				// customer
				$is_allowed = $action_result;
			}
		} elseif ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) {
			$is_allowed = $action_result;
		}

		return $is_allowed;
	}

	public static function generate_add_to_calendar_links( OsBookingModel $booking, $key = false ): string {
		$html = '<div class="add-to-calendar-types">
							<div class="atc-heading-wrapper">
								<div class="atc-heading">' . esc_html__( 'Calendar Type', 'latepoint' ) . '</div>
								<div class="close-calendar-types"></div>
							</div>
							<a href="' . esc_url( $booking->get_ical_download_link( $key ) ) . '" target="_blank" class="atc-type atc-type-apple">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Apple Calendar', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'google' ) ) . '" target="_blank" class="atc-type atc-type-google">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Google Calendar', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-outlook">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Outlook.com', 'latepoint' ) . '</div>
							</a>
							<a href="' . esc_url( $booking->get_url_for_add_to_calendar_button( 'outlook' ) ) . '" target="_blank" class="atc-type atc-type-office-365">
								<div class="atc-type-image"></div>
								<div class="atc-type-name">' . esc_html__( 'Microsoft 365', 'latepoint' ) . '</div>
							</a>
						</div>';

		return $html;
	}


	public static function get_bookings_for_select( $should_be_in_future = false ) {
		$bookings = new OsBookingModel();
		if ( $should_be_in_future ) {
			$bookings = $bookings->should_be_in_future();
		}
		$bookings         = $bookings->order_by( 'id desc' )->set_limit( 100 )->get_results_as_models();
		$bookings_options = [];
		foreach ( $bookings as $booking ) {
			$name               = $booking->service->name . ', ' . $booking->agent->full_name . ', ' . $booking->customer->full_name . ' [' . $booking->booking_code . ' : ' . $booking->id . ']';
			$bookings_options[] = [
				'value' => $booking->id,
				'label' => esc_html( $name ),
			];
		}

		return $bookings_options;
	}

	/**
	 *
	 * Determine whether to show
	 *
	 * @param $rows
	 *
	 * @return bool
	 */
	public static function is_breakdown_free( $rows ) {
		return ( ( empty( $rows['subtotal']['raw_value'] ) || ( (float) $rows['subtotal']['raw_value'] <= 0 ) ) && ( empty( $rows['total']['raw_value'] ) || ( (float) $rows['total']['raw_value'] <= 0 ) ) );
	}

	public static function output_price_breakdown( $rows ) {
		foreach ( $rows['before_subtotal'] as $row ) {
			self::output_price_breakdown_row( $row );
		}
		// if there is nothing between subtotal and total - don't show subtotal as it will be identical to total
		if ( ! empty( $rows['after_subtotal'] ) ) {
			if ( ! empty( $rows['subtotal'] ) ) {
				echo '<div class="subtotal-separator"></div>';
				self::output_price_breakdown_row( $rows['subtotal'] );
			}
			foreach ( $rows['after_subtotal'] as $row ) {
				self::output_price_breakdown_row( $row );
			}
		}
		if ( ! empty( $rows['total'] ) ) {
			self::output_price_breakdown_row( $rows['total'] );
		}
		if ( ! empty( $rows['payments'] ) ) {
			foreach ( $rows['payments'] as $row ) {
				self::output_price_breakdown_row( $row );
			}
		}
		if ( ! empty( $rows['balance'] ) ) {
			self::output_price_breakdown_row( $rows['balance'] );
		}
	}

	public static function output_price_breakdown_row( $row ) {
		if ( ! empty( $row['items'] ) ) {
			if ( ! empty( $row['heading'] ) ) {
				echo '<div class="summary-box-heading"><div class="sbh-item">' . esc_html( $row['heading'] ) . '</div><div class="sbh-line"></div></div>';
			}
			foreach ( $row['items'] as $row_item ) {
				self::output_price_breakdown_row( $row_item );
			}
		} else {
			$extra_class = '';
			if ( isset( $row['style'] ) && $row['style'] == 'strong' ) {
				$extra_class .= ' spi-strong';
			}
			if ( isset( $row['style'] ) && $row['style'] == 'total' ) {
				$extra_class .= ' spi-total';
			}
			if ( isset( $row['type'] ) && $row['type'] == 'credit' ) {
				$extra_class .= ' spi-positive';
			}
			?>
			<div class="summary-price-item-w <?php echo esc_attr( $extra_class ); ?>">
				<div class="spi-name">
					<?php echo $row['label']; ?>
					<?php if ( ! empty( $row['note'] ) ) {
						echo '<span class="pi-note">' . esc_html( $row['note'] ) . '</span>';
					} ?>
					<?php if ( ! empty( $row['badge'] ) ) {
						echo '<span class="pi-badge">' . esc_html( $row['badge'] ) . '</span>';
					} ?>
				</div>
				<div class="spi-price"><?php echo esc_html( $row['value'] ); ?></div>
			</div>
			<?php
		}
	}

	public static function output_price_breakdown_row_as_input_field( $row, $base_name ) {
		$field_name = $base_name . '[' . OsUtilHelper::random_text( 'alnum', 8 ) . ']';
		if ( ! empty( $row['items'] ) ) {
			echo OsFormHelper::hidden_field( $field_name . '[heading]', $row['heading'] ?? '' );
			foreach ( $row['items'] as $row_item ) {
				self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[items]' );
			}
		} else {
			$is_negative   = ( $row['raw_value'] < 0 );
			$wrapper_class = $is_negative ? [ 'class' => 'green-value-input' ] : [];
			$label         = $row['label'] ?? '';
			if ( ! empty( $row['note'] ) ) {
				$label .= ' ' . $row['note'];
			}
			// Use abs() for money_field because inputmask cannot parse negative initial values; the sign is preserved via the 'type' hidden field
			$field_value = $is_negative ? abs( $row['raw_value'] ) : $row['raw_value'];
			echo OsFormHelper::money_field( $field_name . '[value]', $label, $field_value, [ 'theme' => 'right-aligned' ], [], $wrapper_class );
			echo OsFormHelper::hidden_field( $field_name . '[label]', $row['label'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[style]', $row['style'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[type]', $row['type'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[note]', $row['note'] ?? '' );
			echo OsFormHelper::hidden_field( $field_name . '[badge]', $row['badge'] ?? '' );
		}
		if ( ! empty( $row['sub_items'] ) ) {
			foreach ( $row['sub_items'] as $row_item ) {
				self::output_price_breakdown_row_as_input_field( $row_item, $field_name . '[sub_items]' );
			}
		}
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 * @param bool $accessed_from_backend
	 *
	 * @return array
	 */
	public static function get_blocked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter, bool $accessed_from_backend = false ): array {
		$grouped_blocked_periods = [];

		if ( $filter->date_from ) {
			$date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
			$date_to   = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );

			# Loop through days to fill in days that might have no bookings
			for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				$grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] = [];
			}
		}
		if ( ! $accessed_from_backend ) {
			$today                     = new OsWpDateTime( 'today' );
			$earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $filter->service_id );

			$block_end_datetime = OsTimeHelper::now_datetime_object();
			if ( $earliest_possible_booking ) {
				try {
					$block_end_datetime->modify( $earliest_possible_booking );
				} catch ( Exception $e ) {
					$block_end_datetime = OsTimeHelper::now_datetime_object();
				}
			}
			for ( $day = clone $today; $day->format( 'Y-m-d' ) <= $block_end_datetime->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				// loop days from now to the earliest possible booking and block timeslots if these days were actually requested
				if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
					$grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
						[
							'start_time' => 0,
							'end_time'   => ( $day->format( 'Y-m-d' ) < $block_end_datetime->format( 'Y-m-d' ) ) ? 24 * 60 : OsTimeHelper::convert_datetime_to_minutes( $block_end_datetime ),
							'start_date' => $day->format( 'Y-m-d' ),
							'end_date'   => $day->format( 'Y-m-d' ),
						]
					);
				}
			}
			$latest_possible_booking = OsSettingsHelper::get_latest_possible_booking_restriction( $filter->service_id );
			if ( $latest_possible_booking ) {
				try {
					$latest_booking_datetime = OsTimeHelper::now_datetime_object();
					$latest_booking_datetime->modify( $latest_possible_booking );
				} catch ( Exception $e ) {
					$latest_booking_datetime = null;
				}
				if ( $latest_booking_datetime && $filter->date_from ) {
					$date_to = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
					// Start from the latest_booking_datetime day
					for ( $day = clone $latest_booking_datetime; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
						if ( isset( $grouped_blocked_periods[ $day->format( 'Y-m-d' ) ] ) ) {
							$grouped_blocked_periods[ $day->format( 'Y-m-d' ) ][] = new \LatePoint\Misc\BlockedPeriod(
								[
									'start_time' => ( $day->format( 'Y-m-d' ) == $latest_booking_datetime->format( 'Y-m-d' ) ) ? OsTimeHelper::convert_datetime_to_minutes( $latest_booking_datetime ) : 0,
									'end_time'   => 24 * 60,
									'start_date' => $day->format( 'Y-m-d' ),
									'end_date'   => $day->format( 'Y-m-d' ),
								]
							);
						}
					}
				}
			}
		}

		$grouped_blocked_periods = apply_filters( 'latepoint_blocked_periods_for_range', $grouped_blocked_periods, $filter, $accessed_from_backend );

		return $grouped_blocked_periods;
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return array
	 */
	public static function get_booked_periods_grouped_by_day( \LatePoint\Misc\Filter $filter ): array {
		$booked_periods = self::get_booked_periods( $filter );

		$grouped_booked_periods = [];
		if ( $filter->date_from ) {
			$date_from = OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );
			$date_to   = ( $filter->date_to ) ? OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_to ) : OsWpDateTime::os_createFromFormat( 'Y-m-d', $filter->date_from );

			# Loop through days to fill in days that might have no bookings
			for ( $day = clone $date_from; $day->format( 'Y-m-d' ) <= $date_to->format( 'Y-m-d' ); $day->modify( '+1 day' ) ) {
				$grouped_booked_periods[ $day->format( 'Y-m-d' ) ] = [];
			}
			foreach ( $booked_periods as $booked_period ) {
				$grouped_booked_periods[ $booked_period->start_date ][] = $booked_period;
				// if event spans multiple days - add to other days as well
				if ( $booked_period->end_date && ( $booked_period->start_date != $booked_period->end_date ) ) {
					$grouped_booked_periods[ $booked_period->end_date ][] = $booked_period;
				}
			}
		}

		return $grouped_booked_periods;
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return \LatePoint\Misc\BookedPeriod[]
	 */
	public static function get_booked_periods( \LatePoint\Misc\Filter $filter ): array {


		$bookings       = self::get_bookings( $filter, true );
		$booked_periods = [];

		foreach ( $bookings as $booking ) {
			$booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $booking );
		}


		if ( $filter->consider_cart_items ) {
			$cart             = OsCartsHelper::get_or_create_cart();
			$bookings_in_cart = $cart->get_bookings_from_cart_items();

			foreach ( $bookings_in_cart as $cart_booking ) {
				$booked_periods[] = \LatePoint\Misc\BookedPeriod::create_from_booking_model( $cart_booking );
			}
		}

		// TODO Update all filters to accept new "filter" variable (In Google Calendar addon)
		$booked_periods = apply_filters( 'latepoint_get_booked_periods', $booked_periods, $filter );

		return $booked_periods;
	}


	/**
	 * @param \LatePoint\Misc\Filter $filter
	 * @param bool $as_models
	 *
	 * @return array
	 */
	public static function get_bookings( \LatePoint\Misc\Filter $filter, bool $as_models = false ): array {
		$bookings = new OsBookingModel();
		if ( $filter->date_from ) {
			if ( $filter->date_from && $filter->date_to ) {
				# both start and end date provided - means it's a range
				$bookings->where(
					[
						'start_date >=' => $filter->date_from,
						'start_date <=' => $filter->date_to,
					]
				);
			} else {
				# only start_date provided - means it's a specific date requested
				$bookings->where( [ 'start_date' => $filter->date_from ] );
			}
		}


		if ( $filter->connections ) {
			$connection_conditions = [];
			foreach ( $filter->connections as $connection ) {
				$connection_conditions[] = [
					'AND' =>
						[
							'agent_id'    => $connection->agent_id,
							'service_id'  => $connection->service_id,
							'location_id' => $connection->location_id,
						],
				];
			}
			$bookings->where( [ 'OR' => $connection_conditions ] );
		} else {
			if ( $filter->agent_id ) {
				$bookings->where( [ 'agent_id' => $filter->agent_id ] );
			}
			if ( $filter->location_id ) {
				$bookings->where( [ 'location_id' => $filter->location_id ] );
			}
			if ( $filter->service_id ) {
				$bookings->where( [ 'service_id' => $filter->service_id ] );
			}
		}
		if ( $filter->statuses ) {
			$bookings->where( [ 'status' => $filter->statuses ] );
		}
		if ( $filter->exclude_booking_ids ) {
			$bookings->where( [ 'id NOT IN' => $filter->exclude_booking_ids ] );
		}
		$bookings->order_by( 'start_time asc, end_time asc, service_id asc' );
		$bookings = ( $as_models ) ? $bookings->get_results_as_models() : $bookings->get_results();

		// make sure to return empty array if nothing is found
		if ( empty( $bookings ) ) {
			$bookings = [];
		}

		return $bookings;
	}

	public static function generate_ical_event_string( $booking ) {
		// translators: %1$s is agent name, %2$s is service name
		$booking_description = sprintf( __( 'Appointment with %1$s for %2$s', 'latepoint' ), $booking->agent->full_name, $booking->service->name );

		$ics = new ICS(
			array(
				'location'    => $booking->location->full_address,
				'description' => '',
				'dtstart'     => $booking->format_start_date_and_time_for_google(),
				'dtend'       => $booking->format_end_date_and_time_for_google(),
				'summary'     => $booking_description,
				'url'         => get_site_url(),
			)
		);

		return $ics->to_string();
	}

	/**
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 *
	 * @return bool
	 *
	 * Checks if requested booking slot is available, loads work periods and booked periods from database and checks availability against them
	 */
	public static function is_booking_request_available( \LatePoint\Misc\BookingRequest $booking_request, $settings = [] ): bool {
		try {
			$requested_date = new OsWpDateTime( $booking_request->start_date );
		} catch ( Exception $e ) {
			return false;
		}
		$resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $requested_date, $requested_date, $settings );
		if ( empty( $resources[ $requested_date->format( 'Y-m-d' ) ] ) ) {
			return false;
		}
		$is_available = false;


		// check if satisfies earliest and latest bookings - check per-service settings first, then global
		$earliest_possible_booking = OsSettingsHelper::get_earliest_possible_booking_restriction( $booking_request->service_id );
		$latest_possible_booking   = OsSettingsHelper::get_latest_possible_booking_restriction( $booking_request->service_id );


		if ( $earliest_possible_booking || $latest_possible_booking ) {
			// check earliest
			if ( ! empty( $earliest_possible_booking ) ) {
				try {
					$earliest_possible_booking_date = new OsWpDateTime( $earliest_possible_booking );
					if ( $earliest_possible_booking_date > $booking_request->get_start_datetime() ) {
						return false;
					}
				} catch ( Exception $e ) {

				}
			}
			if ( ! empty( $latest_possible_booking ) ) {
				// check latest
				try {
					$latest_possible_booking_date = new OsWpDateTime( $latest_possible_booking );
					if ( $latest_possible_booking_date < $booking_request->get_start_datetime() ) {
						return false;
					}
				} catch ( Exception $e ) {

				}
			}
		}

		foreach ( $resources[ $requested_date->format( 'Y-m-d' ) ] as $resource ) {
			foreach ( $resource->slots as $slot ) {
				if ( $slot->start_time == $booking_request->start_time && $slot->can_accomodate( $booking_request->total_attendees ) ) {
					$is_available = true;
				}
				if ( $is_available ) {
					break;
				}
			}
			if ( $is_available ) {
				break;
			}
		}

		return $is_available;
	}

	/**
	 *
	 * Checks if two bookings are part of the same group appointment
	 *
	 * @param bool|OsBookingModel $booking
	 * @param bool|OsBookingModel $compare_booking
	 *
	 * @return bool
	 */
	public static function check_if_group_bookings( $booking, $compare_booking ): bool {
		if ( $booking && $compare_booking && ( $compare_booking->start_time == $booking->start_time ) && ( $compare_booking->end_time == $booking->end_time ) && ( $compare_booking->service_id == $booking->service_id ) && ( $compare_booking->location_id == $booking->location_id ) ) {
			return true;
		} else {
			return false;
		}
	}


	public static function process_actions_after_save( $booking_id ) {
	}

	/**
	 * @param DateTime $start_date
	 * @param DateTime $end_date
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 * @param \LatePoint\Misc\BookingResource[] $resources
	 * @param array $settings
	 *
	 * @return string
	 * @throws Exception
	 */
	public static function get_quick_availability_days( DateTime $start_date, DateTime $end_date, \LatePoint\Misc\BookingRequest $booking_request, array $resources = [], array $settings = [] ) {
		$default_settings = [
			'work_boundaries'     => false,
			'exclude_booking_ids' => [],
		];
		$settings         = array_merge( $default_settings, $settings );

		$html = '';

		if ( ! $resources ) {
			$resources = OsResourceHelper::get_resources_grouped_by_day( $booking_request, $start_date, $end_date, $settings );
		}
		if ( ! $settings['work_boundaries'] ) {
			$settings['work_boundaries'] = OsResourceHelper::get_work_boundaries_for_groups_of_resources( $resources );
		}

		if ( $start_date->format( 'j' ) != '1' ) {
			$html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $start_date->format( 'n' ) ) . '</div>';
		}

		for ( $day_date = clone $start_date; $day_date <= $end_date; $day_date->modify( '+1 day' ) ) {
			// first day of month, output month name
			if ( $day_date->format( 'j' ) == '1' ) {
				$html .= '<div class="ma-month-label">' . OsUtilHelper::get_month_name_by_number( $day_date->format( 'n' ) ) . '</div>';
			}
			$html .= '<div class="ma-day ma-day-number-' . $day_date->format( 'N' ) . '">';
			$html .= '<div class="ma-day-info">';
			$html .= '<span class="ma-day-number">' . $day_date->format( 'j' ) . '</span>';
			$html .= '<span class="ma-day-weekday">' . OsUtilHelper::get_weekday_name_by_number( $day_date->format( 'N' ), true ) . '</span>';
			$html .= '</div>';
			$html .= OsTimelineHelper::availability_timeline( $booking_request, $settings['work_boundaries'], $resources[ $day_date->format( 'Y-m-d' ) ], [ 'book_on_click' => false ] );
			$html .= '</div>';
		}

		return $html;
	}

	public static function count_pending_bookings() {
		$bookings = new OsBookingModel();

		return $bookings->filter_allowed_records()->where( [ 'status IN' => OsBookingHelper::get_booking_statuses_for_pending_page() ] )->count();
	}


	public static function generate_bundles_folder( ?array $bundles = null, array $restrictions = [] ): void {
		if ( null === $bundles ) {
			$bundles_model = new OsBundleModel();
			$bundles       = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
		}

		/**
		 * Filters the list of bundles shown on the services step of the booking form.
		 *
		 * @since 5.6.3
		 * @hook latepoint_booking_generate_bundles_folder
		 *
		 * @param {OsBundleModel[]} $bundles List of active, non-hidden bundles to be displayed
		 *
		 * @returns {OsBundleModel[]} Filtered list of bundles to display
		 */
		$bundles = apply_filters( 'latepoint_booking_generate_bundles_folder', $bundles );

		if ( $bundles ) {
			/**
			 * Whether to skip the "Bundle & Save" folder header and render bundle
			 * tiles directly (without a collapsible category wrapper).
			 *
			 * @param bool  $skip         Whether to skip the folder wrapper. Default false.
			 * @param array $restrictions Active booking-form restrictions.
			 *
			 * @hook latepoint_bundles_folder_skip_wrapper
			 */
			$skip_folder_wrapper = (bool) apply_filters( 'latepoint_bundles_folder_skip_wrapper', false, $restrictions );

			if ( ! $skip_folder_wrapper ) {
				?>
				<div class="os-item-category-w os-items os-as-rows os-animated-child">
					<div class="os-item-category-info-w os-item os-animated-self with-plus">
						<div class="os-item-category-info os-item-i" tabindex="0">
							<div class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></div>
							<div class="os-item-name-w">
								<div class="os-item-name"><?php echo esc_html__( 'Bundle & Save', 'latepoint' ); ?></div>
							</div>
							<?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $bundles ) ) { ?>
								<div class="os-item-child-count">
									<span><?php echo count( $bundles ); ?></span> <?php esc_html_e( 'Bundles', 'latepoint' ); ?>
								</div>
							<?php } ?>
						</div>
					</div>
				<?php
			}
			?>
			<div class="os-bundles os-animated-parent os-items os-as-rows os-selectable-items">
				<?php
				foreach ( $bundles as $bundle ) { ?>
					<div class="os-animated-child os-item os-selectable-item <?php echo ( $bundle->charge_amount ) ? 'os-priced-item' : ''; ?> <?php if ( $bundle->short_description ) {
						echo 'with-description'; } ?>"
							tabindex="0"
							data-item-price="<?php echo esc_attr( $bundle->charge_amount ); ?>"
							data-priced-item-type="bundle"
							data-summary-field-name="bundle"
							data-summary-value="<?php echo esc_attr( $bundle->name ); ?>"
							data-item-id="<?php echo esc_attr( $bundle->id ); ?>"
							data-cart-item-item-data-key="bundle_id"
							data-os-call-func="latepoint_bundle_selected">
						<div class="os-service-selector os-item-i os-animated-self"
							 data-bundle-id="<?php echo esc_attr( $bundle->id ); ?>">
							<span class="os-item-img-w"><i class="latepoint-icon latepoint-icon-shopping-bag"></i></span>
							<span class="os-item-name-w">
					<span class="os-item-name"><?php echo esc_html( $bundle->name ); ?></span>
					<?php if ( $bundle->short_description ) { ?>
						<span class="os-item-desc"><?php echo wp_kses_post( $bundle->short_description ); ?></span>
					<?php } ?>
				  </span>

						<?php if ( $bundle->price_min > 0 ) { ?>
							<span class="os-item-price-w">
								<span class="os-item-price">
								<?php
								/**
								 * Filters the display price value shown on the bundle tile on a booking form
								 *
								 * @since 5.1.94
								 * @hook latepoint_booking_form_display_bundle_price
								 *
								 * @param {string} $price displayed price that will be outputted
								 * @param {OsBundleModel} $bundle Bundle that the price is displayed for
								 *
								 * @returns {string} Filtered displayed price
								 */
								$display_price = apply_filters( 'latepoint_booking_form_display_bundle_price', $bundle->price_min_formatted, $bundle );
								echo esc_html( $display_price ); ?>
								</span>
								<?php if ( $bundle->price_min != $bundle->price_max ) { ?>
									<span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
								<?php } ?>
							</span>
						<?php } elseif ( $bundle->charge_amount > 0 ) { ?>
							<span class="os-item-price-w">
								<span class="os-item-price">
									<?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
								</span>
							</span>
						<?php } ?>
						</div>
					</div>
					<?php
				}
				?>
			</div>
			<?php
			if ( ! $skip_folder_wrapper ) {
				echo '</div>';
			}
		}
	}

	public static function generate_services_list( $services = false, $preselected_service = false ) {
		if ( $services && is_array( $services ) && ! empty( $services ) ) { ?>
			<div class="os-services os-animated-parent os-items os-as-rows os-selectable-items">
				<?php foreach ( $services as $service ) {
					// if service is preselected - only output that service, skip the rest
					if ( $preselected_service && $service->id != $preselected_service->id ) {
						continue;
					}
					$service_durations = $service->get_all_durations_arr();
					$is_priced         = ( ! ( count( $service_durations ) > 1 ) && $service->charge_amount ) ? true : false;
					?>
					<div class="os-animated-child os-item os-selectable-item <?php echo ( $preselected_service && $service->id == $preselected_service->id ) ? 'selected is-preselected' : ''; ?> <?php echo ( $is_priced ) ? 'os-priced-item' : ''; ?> <?php if ( $service->short_description ) {
						echo 'with-description'; } ?>"
							tabindex="0"
							data-item-price="<?php echo esc_attr( $service->charge_amount ); ?>"
							data-priced-item-type="service"
							data-summary-field-name="service"
							data-summary-value="<?php echo esc_attr( $service->name ); ?>"
							data-item-id="<?php echo esc_attr( $service->id ); ?>"
							data-cart-item-item-data-key="service_id"
							data-os-call-func="latepoint_service_selected"
							data-id-holder=".latepoint_service_id">
						<div class="os-service-selector os-item-i os-animated-self" data-service-id="<?php echo esc_attr( $service->id ); ?>">
							<?php if ( $service->selection_image_id ) { ?>
								<span class="os-item-img-w" style="background-image: url(<?php echo esc_url( $service->selection_image_url ); ?>);"></span>
							<?php } ?>
							<span class="os-item-name-w">
				<span class="os-item-name"><?php echo esc_html( $service->name ); ?></span>
					<?php if ( $service->short_description ) { ?>
					<span class="os-item-desc"><?php echo wp_kses_post( $service->short_description ); ?></span>
				<?php } ?>
			  </span>
							<?php if ( $service->price_min > 0 ) { ?>
								<span class="os-item-price-w">
				  <span class="os-item-price">
								<?php
								/**
								 * Filters the display price value shown on the service tile on a booking form
								 *
								 * @since 5.1.94
								 * @hook latepoint_booking_form_display_service_price
								 *
								 * @param {string} $price displayed price that will be outputted
								 * @param {OsServiceModel} $service Service that the price is displayed for
								 *
								 * @returns {string} Filtered displayed price
								 */
								$display_price = apply_filters( 'latepoint_booking_form_display_service_price', $service->price_min_formatted, $service );
								echo esc_html( $display_price ) ?>
				  </span>
								<?php if ( $service->price_min != $service->price_max ) { ?>
					  <span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
				  <?php } ?>
				</span>
							<?php } ?>
						</div>
					</div>
				<?php } ?>
			</div>
		<?php }
	}

	public static function generate_services_bundles_and_categories_list( $parent_id = false, array $settings = [] ) {
		$default_settings = [
			'show_service_categories_arr' => false,
			'show_services_arr'           => false,
			'preselected_service'         => false,
			'preselected_category'        => false,
			'bundles'                     => null,
			'service_display_mode'        => 'all',
		];
		$settings         = array_merge( $default_settings, $settings );

		if ( $settings['preselected_service'] ) {
			OsBookingHelper::generate_services_list( [ $settings['preselected_service'] ], $settings['preselected_service'] );

			return;
		}

		$service_categories = new OsServiceCategoryModel();
		$args               = array();
		if ( $settings['show_service_categories_arr'] && is_array( $settings['show_service_categories_arr'] ) ) {
			if ( $parent_id ) {
				$service_categories->where( [ 'parent_id' => $parent_id ] );
			} else {
				if ( $settings['preselected_category'] ) {
					$service_categories->where( [ 'id' => $settings['preselected_category'] ] );
				} else {
					$service_categories->where_in( 'id', $settings['show_service_categories_arr'] );
					$service_categories->where(
						[
							'parent_id' => [
								'OR' => [
									'IS NULL',
									' NOT IN' => $settings['show_service_categories_arr'],
								],
							],
						]
					);
				}
			}
		} else {
			if ( $settings['preselected_category'] ) {
				$service_categories->where( [ 'id' => $settings['preselected_category'] ] );
			} else {
				$args['parent_id'] = $parent_id ? $parent_id : 'IS NULL';
			}
		}
		$service_categories = $service_categories->where( $args )->order_by( 'order_number asc' )->get_results_as_models();

		$main_parent_class = ( $parent_id ) ? 'os-animated-parent' : 'os-item-categories-main-parent os-animated-parent';
		if ( ! $settings['preselected_category'] ) {
			echo '<div class="os-item-categories-holder ' . esc_attr( $main_parent_class ) . '">';
		}

		// generate services that have no category
		if ( $parent_id == false && $settings['preselected_category'] == false ) { ?>
			<?php
			$services_without_category = new OsServiceModel();
			if ( $settings['show_services_arr'] ) {
				$services_without_category->where_in( 'id', $settings['show_services_arr'] );
			}
			$services_without_category = $services_without_category->where( [ 'category_id' => 0 ] )->should_be_active()->should_not_be_hidden()->get_results_as_models();
			/** This filter is documented in lib/helpers/steps_helper.php */
			$services_without_category = apply_filters( 'latepoint_step_services', $services_without_category, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
			if ( $services_without_category ) {
				OsBookingHelper::generate_services_list( $services_without_category, false );
			}
		}

		if ( is_array( $service_categories ) ) {
			foreach ( $service_categories as $service_category ) { ?>
				<?php
				$services          = [];
				$category_services = $service_category->get_active_services();
				if ( is_array( $category_services ) ) {
					// if show selected services restriction is set - filter
					if ( $settings['show_services_arr'] ) {
						foreach ( $category_services as $category_service ) {
							if ( in_array( $category_service->id, $settings['show_services_arr'] ) ) {
								$services[] = $category_service;
							}
						}
					} else {
						$services = $category_services;
					}
				}
				/** This filter is documented in lib/helpers/steps_helper.php */
				$services               = apply_filters( 'latepoint_step_services', $services, $settings['bundles'], [ 'service_display_mode' => $settings['service_display_mode'] ] );
				$child_categories       = new OsServiceCategoryModel();
				$count_child_categories = $child_categories->where( [ 'parent_id' => $service_category->id ] )->count();
				// show only if it has either at least one child category or service
				if ( $count_child_categories || count( $services ) ) {
					// preselected category, just show contents, not the wrapper
					if ( $service_category->id == $settings['preselected_category'] ) {
						OsBookingHelper::generate_services_list( $services, false );
						OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) );
					} else { ?>
					<div class="os-item-category-w os-items os-as-rows os-animated-child"
						 data-id="<?php echo esc_attr( $service_category->id ); ?>">
						<div class="os-item-category-info-w os-item os-animated-self with-plus">
							<div class="os-item-category-info os-item-i">
								<div class="os-item-img-w"
									 style="background-image: url(<?php echo esc_url( $service_category->selection_image_url ); ?>);"></div>
								<div class="os-item-name-w">
									<div class="os-item-name"><?php echo esc_html( $service_category->name ); ?></div>
									<?php if ( ! empty( $service_category->short_description ) ) { ?>
										<div class="os-item-desc"><?php echo $service_category->short_description; ?></div>
									<?php } ?>
								</div>
								<?php if ( OsSettingsHelper::is_on( 'show_service_categories_count' ) && count( $services ) ) { ?>
									<div class="os-item-child-count">
										<span><?php echo count( $services ); ?></span> <?php esc_html_e( 'Services', 'latepoint' ); ?>
									</div>
								<?php } ?>
							</div>
						</div>
						<?php OsBookingHelper::generate_services_list( $services, false ); ?>
						<?php OsBookingHelper::generate_services_bundles_and_categories_list( $service_category->id, array_merge( $settings, [ 'preselected_category' => false ] ) ); ?>
						</div><?php
					}
				}
			}
		}
		if ( ! $settings['preselected_category'] && ! $parent_id ) {
			$category_bundles = $settings['bundles'];
			if ( null === $category_bundles ) {
				$bundles_model    = new OsBundleModel();
				$category_bundles = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
				/** This filter is documented in lib/helpers/steps_helper.php */
				$category_bundles = apply_filters( 'latepoint_step_bundles', $category_bundles, [], [ 'service_display_mode' => $settings['service_display_mode'] ] );
			}
			OsBookingHelper::generate_bundles_folder( $category_bundles, [ 'service_display_mode' => $settings['service_display_mode'] ] );
		}
		if ( ! $settings['preselected_category'] ) {
			echo '</div>';
		}
	}

	public static function group_booking_btn_html( $booking_id = false ) {
		$html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
                  data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
                  data-os-output-target="lightbox"
                  data-os-lightbox-classes="width-500"
                  data-os-after-call="latepoint_init_grouped_bookings_form"';

		return $html;
	}

	public static function quick_booking_btn_html( $booking_id = false, $params = array() ) {
		$html = '';
		if ( $booking_id ) {
			$params['booking_id'] = $booking_id;
		}
		$route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );

		$params_str = http_build_query( $params );
		$html       = 'data-os-params="' . esc_attr( $params_str ) . '"
    data-os-action="' . esc_attr( $route ) . '"
    data-os-output-target="side-panel"
    data-os-after-call="latepoint_init_quick_order_form"';

		return $html;
	}


	/**
	 * @param OsBookingModel $booking
	 *
	 * @return false|mixed
	 *
	 * Search for available location based on booking requirements. Will return false if no available location found.
	 */
	public static function get_any_location_for_booking_by_rule( OsBookingModel $booking ) {
		// ANY LOCATION SELECTED
		// get available locations
		$connected_ids = OsLocationHelper::get_location_ids_for_service_and_agent( $booking->service_id, $booking->agent_id );

		// If date/time is selected - filter locations who are available at that time
		if ( $booking->start_date && $booking->start_time ) {
			$available_location_ids = [];
			$booking_request        = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
			foreach ( $connected_ids as $location_id ) {
				$booking_request->location_id = $location_id;
				if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
					$available_location_ids[] = $location_id;
				}
			}
			$connected_ids = array_intersect( $available_location_ids, $connected_ids );
		}


		$locations_model = new OsLocationModel();
		if ( ! empty( $connected_ids ) ) {
			$locations_model->where_in( 'id', $connected_ids );
			$locations = $locations_model->should_be_active()->get_results_as_models();
		} else {
			$locations = [];
		}

		if ( empty( $locations ) ) {
			return false;
		}

		$selected_location_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
		$booking->location_id = $selected_location_id;

		return $selected_location_id;
	}

	/**
	 * @param OsBookingModel $booking
	 *
	 * @return false|mixed
	 *
	 * Search for available agent based on booking requirements and agent picking preferences. Will return false if no available agent found.
	 */
	public static function get_any_agent_for_booking_by_rule( OsBookingModel $booking ) {
		// ANY AGENT SELECTED
		// get available agents
		$connected_ids = OsAgentHelper::get_agent_ids_for_service_and_location( $booking->service_id, $booking->location_id );

		// If date/time is selected - filter agents who are available at that time
		if ( $booking->start_date && $booking->start_time ) {
			$available_agent_ids = [];
			$booking_request     = \LatePoint\Misc\BookingRequest::create_from_booking_model( $booking );
			foreach ( $connected_ids as $agent_id ) {
				$booking_request->agent_id = $agent_id;
				if ( OsBookingHelper::is_booking_request_available( $booking_request ) ) {
					$available_agent_ids[] = $agent_id;
				}
			}
			$connected_ids = array_intersect( $available_agent_ids, $connected_ids );
		}


		/**
		 * Get IDs of agents that are eligible to be assigned a booking that has "ANY" agent pre-selected
		 *
		 * @param {array} $connected_ids Array of eligible Agent IDs
		 * @param {OsBookingModel} $booking Booking that needs agent ID
		 *
		 * @returns {array} Filtered array of IDs of eligible agents
		 * @since 4.7.6
		 * @hook latepoint_agent_ids_assignable_to_any_agent_booking
		 *
		 */
		$connected_ids = apply_filters( 'latepoint_agent_ids_assignable_to_any_agent_booking', $connected_ids, $booking );

		if ( ! empty( $connected_ids ) ) {
			$agents_model = new OsAgentModel();
			$agents_model->where_in( 'id', $connected_ids );
			$agents = $agents_model->should_be_active()->get_results_as_models();
		} else {
			$agents = [];
		}

		if ( empty( $agents ) ) {
			return false;
		}


		$selected_agent_id = false;
		$agent_order_rule  = OsSettingsHelper::get_any_agent_order();
		switch ( $agent_order_rule ) {
			case LATEPOINT_ANY_AGENT_ORDER_RANDOM:
				$selected_agent_id = $connected_ids[ wp_rand( 0, count( $connected_ids ) - 1 ) ];
				break;
			case LATEPOINT_ANY_AGENT_ORDER_PRICE_HIGH:
				$highest_price = false;
				foreach ( $agents as $agent ) {
					$booking->agent_id = $agent->id;
					$price             = OsBookingHelper::calculate_full_amount_for_booking( $booking );
					if ( $highest_price === false && $selected_agent_id === false ) {
						$highest_price     = $price;
						$selected_agent_id = $agent->id;
					} else {
						if ( $highest_price < $price ) {
							$highest_price     = $price;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_PRICE_LOW:
				$lowest_price = false;
				foreach ( $agents as $agent ) {
					$booking->agent_id = $agent->id;
					$price             = OsBookingHelper::calculate_full_amount_for_booking( $booking );
					if ( $lowest_price === false && $selected_agent_id === false ) {
						$lowest_price      = $price;
						$selected_agent_id = $agent->id;
					} else {
						if ( $lowest_price > $price ) {
							$lowest_price      = $price;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_BUSY_HIGH:
				$max_bookings = false;
				foreach ( $agents as $agent ) {
					$agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
					if ( $max_bookings === false && $selected_agent_id === false ) {
						$max_bookings      = $agent_total_bookings;
						$selected_agent_id = $agent->id;
					} else {
						if ( $max_bookings < $agent_total_bookings ) {
							$max_bookings      = $agent_total_bookings;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
			case LATEPOINT_ANY_AGENT_ORDER_BUSY_LOW:
				$min_bookings = false;
				foreach ( $agents as $agent ) {
					$agent_total_bookings = OsBookingHelper::get_total_bookings_for_date( $booking->start_date, [ 'agent_id' => $agent->id ] );
					if ( $min_bookings === false && $selected_agent_id === false ) {
						$min_bookings      = $agent_total_bookings;
						$selected_agent_id = $agent->id;
					} else {
						if ( $min_bookings > $agent_total_bookings ) {
							$min_bookings      = $agent_total_bookings;
							$selected_agent_id = $agent->id;
						}
					}
				}
				break;
		}
		/**
		 * Get ID of agent that will be assigned to a booking, depending on order rules, where agent is set to ANY
		 *
		 * @param {integer} $selected_agent_id Currently selected agent ID
		 * @param {OsAgentModel[]} $agents Array of eligible agent models to pick from
		 * @param {OsBookingModel} $booking Booking that needs agent ID
		 * @param {string} $agent_order_rule Rule of agent ordering
		 *
		 * @returns {integer} ID of the agent that will be assigned to this booking
		 * @since 4.7.6
		 * @hook latepoint_get_any_agent_id_for_booking_by_rule
		 *
		 */
		$selected_agent_id = apply_filters( 'latepoint_get_any_agent_id_for_booking_by_rule', $selected_agent_id, $agents, $booking, $agent_order_rule );
		$booking->agent_id = $selected_agent_id;

		return $selected_agent_id;
	}


	public static function get_total_bookings_for_date( $date, $conditions = [], $grouped = false ) {
		$args = [ 'start_date' => $date ];
		if ( isset( $conditions['agent_id'] ) && $conditions['agent_id'] ) {
			$args['agent_id'] = $conditions['agent_id'];
		}
		if ( isset( $conditions['service_id'] ) && $conditions['service_id'] ) {
			$args['service_id'] = $conditions['service_id'];
		}
		if ( isset( $conditions['location_id'] ) && $conditions['location_id'] ) {
			$args['location_id'] = $conditions['location_id'];
		}


		$bookings = new OsBookingModel();
		if ( $grouped ) {
			$bookings->group_by( 'start_date, start_time, end_time, service_id, location_id' );
		}
		$bookings = $bookings->where( $args );

		return $bookings->count();
	}


	/**
	 *
	 * Get list of statuses that block timeslot availability
	 *
	 * @return array
	 */
	public static function get_timeslot_blocking_statuses(): array {
		$statuses = explode( ',', OsSettingsHelper::get_settings_value( 'timeslot_blocking_statuses', '' ) );

		/**
		 * Get list of statuses that block timeslot availability
		 *
		 * @param {array} $statuses array of status codes that block timeslot availability
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 4.7.0
		 * @hook latepoint_get_timeslot_blocking_statuses
		 *
		 */
		return apply_filters( 'latepoint_get_timeslot_blocking_statuses', $statuses );
	}


	/**
	 *
	 * Get list of statuses that appear on pending page
	 *
	 * @return array
	 */
	public static function get_booking_statuses_for_pending_page(): array {
		$statuses = explode( ',', OsSettingsHelper::get_settings_value( 'need_action_statuses', '' ) );

		/**
		 * Get list of statuses that appear on pending page
		 *
		 * @param {array} $statuses array of status codes that appear on pending page
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 4.7.0
		 * @hook latepoint_get_booking_statuses_for_pending_page
		 *
		 */
		return apply_filters( 'latepoint_get_booking_statuses_for_pending_page', $statuses );
	}

	/**
	 *
	 * Get list of statuses that are not cancelled
	 *
	 * @return array
	 */
	public static function get_non_cancelled_booking_statuses(): array {
		$statuses = self::get_statuses_list();
		if ( isset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] ) ) {
			unset( $statuses[ LATEPOINT_BOOKING_STATUS_CANCELLED ] );
		}
		$statuses = array_keys( $statuses );

		/**
		 * Get list of statuses that are not cancelled
		 *
		 * @param {array} $statuses array of status codes that are not cancelled
		 * @returns {array} The filtered array of status codes
		 *
		 * @since 5.0.5
		 * @hook get_non_cancelled_booking_statuses
		 *
		 */
		return apply_filters( 'get_non_cancelled_booking_statuses', $statuses );
	}


	public static function get_default_booking_status( $service_id = false ) {
		if ( $service_id ) {
			$service = new OsServiceModel( $service_id );
			if ( $service && ! empty( $service->id ) ) {
				return $service->get_default_booking_status();
			}
		}
		$default_status = OsSettingsHelper::get_settings_value( 'default_booking_status' );
		if ( $default_status ) {
			return $default_status;
		} else {
			return LATEPOINT_BOOKING_STATUS_APPROVED;
		}
	}


	public static function change_booking_status( $booking_id, $new_status ) {
		$booking = new OsBookingModel( $booking_id );
		if ( ! $booking_id || ! $booking ) {
			return false;
		}

		if ( $new_status == $booking->status ) {
			return true;
		} else {
			$old_booking = clone $booking;
			if ( $booking->update_status( $new_status ) ) {
				do_action( 'latepoint_booking_updated', $booking, $old_booking );

				return true;
			} else {
				return false;
			}
		}
	}


	/**
	 * @param \LatePoint\Misc\BookingRequest $booking_request
	 * @param \LatePoint\Misc\BookedPeriod[]
	 * @param int $capacity
	 *
	 * @return bool
	 */
	public static function is_timeframe_in_booked_periods( \LatePoint\Misc\BookingRequest $booking_request, array $booked_periods, OsServiceModel $service ): bool {
		if ( empty( $booked_periods ) ) {
			return false;
		}
		$count_existing_attendees = 0;
		foreach ( $booked_periods as $period ) {
			if ( self::is_period_overlapping( $booking_request->get_start_time_with_buffer(), $booking_request->get_end_time_with_buffer(), $period->start_time_with_buffer(), $period->end_time_with_buffer() ) ) {
				// if it's the same service overlapping - count how many times
				// TODO maybe add an option to toggle on/off ability to share a timeslot capacity between two different services
				if ( $booking_request->service_id == $period->service_id ) {
					$count_existing_attendees += $period->total_attendees;
				} else {
					return true;
				}
			}
		}
		if ( $count_existing_attendees > 0 ) {
			// if there are attendees, check if they are below minimum need for timeslot to be blocked, if they are - then the slot is considered booked
			if ( ( $count_existing_attendees + $booking_request->total_attendees ) <= $service->get_capacity_needed_before_slot_is_blocked() ) {
				return false;
			} else {
				return true;
			}
		} else {
			// no attendees in the overlapping booked periods yet, just check if the requested number of attendees is within the service capacity
			if ( $booking_request->total_attendees <= $service->capacity_max ) {
				return false;
			} else {
				return true;
			}
		}
	}


	public static function is_period_overlapping( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
		// https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap/
		return ( ( $period_one_start < $period_two_end ) && ( $period_two_start < $period_one_end ) );
	}

	public static function is_period_inside_another( $period_one_start, $period_one_end, $period_two_start, $period_two_end ) {
		return ( ( $period_one_start >= $period_two_start ) && ( $period_one_end <= $period_two_end ) );
	}

	// args = [agent_id, 'service_id', 'location_id']
	public static function get_bookings_for_date( $date, $args = [] ) {
		$bookings           = new OsBookingModel();
		$args['start_date'] = $date;
		// if any of these are false or 0 - remove it from arguments list
		if ( isset( $args['location_id'] ) && empty( $args['location_id'] ) ) {
			unset( $args['location_id'] );
		}
		if ( isset( $args['agent_id'] ) && empty( $args['agent_id'] ) ) {
			unset( $args['agent_id'] );
		}
		if ( isset( $args['service_id'] ) && empty( $args['service_id'] ) ) {
			unset( $args['service_id'] );
		}

		$bookings->where( $args )->order_by( 'start_time asc, end_time asc, service_id asc' );

		return $bookings->get_results_as_models();
	}

	/**
	 * @param \LatePoint\Misc\Filter $filter
	 *
	 * @return int
	 */
	public static function count_bookings( \LatePoint\Misc\Filter $filter ) {
		$bookings   = new OsBookingModel();
		$query_args = [];
		if ( $filter->date_from ) {
			$query_args['start_date'] = $filter->date_from;
		}
		if ( $filter->location_id ) {
			$query_args['location_id'] = $filter->location_id;
		}
		if ( $filter->agent_id ) {
			$query_args['agent_id'] = $filter->agent_id;
		}
		if ( $filter->service_id ) {
			$query_args['service_id'] = $filter->service_id;
		}

		return $bookings->should_not_be_cancelled()->where( $query_args )->count();
	}


	public static function get_nice_status_name( $status ) {
		$statuses_list = OsBookingHelper::get_statuses_list();
		if ( $status && isset( $statuses_list[ $status ] ) ) {
			return $statuses_list[ $status ];
		} else {
			return __( 'Undefined Status', 'latepoint' );
		}
	}


	public static function get_statuses_list() {
		$statuses            = [
			LATEPOINT_BOOKING_STATUS_APPROVED  => __( 'Approved', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_PENDING   => __( 'Pending Approval', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_NO_SHOW   => __( 'No Show', 'latepoint' ),
			LATEPOINT_BOOKING_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
		];
		$additional_statuses = array_map( 'trim', explode( ',', OsSettingsHelper::get_settings_value( 'additional_booking_statuses', '' ) ) );
		if ( ! empty( $additional_statuses ) ) {
			foreach ( $additional_statuses as $status ) {
				if ( ! empty( $status ) ) {
					$statuses[ str_replace( ' ', '_', strtolower( $status ) ) ] = $status;
				}
			}
		}
		$statuses = apply_filters( 'latepoint_booking_statuses', $statuses );

		return $statuses;
	}


	public static function get_weekdays_arr( $full_name = false ) {
		if ( $full_name ) {
			$weekdays = array(
				__( 'Monday', 'latepoint' ),
				__( 'Tuesday', 'latepoint' ),
				__( 'Wednesday', 'latepoint' ),
				__( 'Thursday', 'latepoint' ),
				__( 'Friday', 'latepoint' ),
				__( 'Saturday', 'latepoint' ),
				__( 'Sunday', 'latepoint' ),
			);
		} else {
			$weekdays = array(
				__( 'Mon', 'latepoint' ),
				__( 'Tue', 'latepoint' ),
				__( 'Wed', 'latepoint' ),
				__( 'Thu', 'latepoint' ),
				__( 'Fri', 'latepoint' ),
				__( 'Sat', 'latepoint' ),
				__( 'Sun', 'latepoint' ),
			);
		}

		return $weekdays;
	}

	public static function get_weekday_name_by_number( $weekday_number, $full_name = false ) {
		$weekdays = OsBookingHelper::get_weekdays_arr( $full_name );
		if ( ! isset( $weekday_number ) || $weekday_number < 1 || $weekday_number > 7 ) {
			return '';
		} else {
			return $weekdays[ $weekday_number - 1 ];
		}
	}

	public static function get_stat( $stat, $args = [] ) {
		if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
			return false;
		}
		$defaults   = [
			'customer_id'    => false,
			'agent_id'       => false,
			'service_id'     => false,
			'location_id'    => false,
			'date_from'      => false,
			'date_to'        => false,
			'group_by'       => false,
			'exclude_status' => false,
		];
		$args       = array_merge( $defaults, $args );
		$bookings   = new OsBookingModel();
		$query_args = array( $args['date_from'], $args['date_to'] );
		switch ( $stat ) {
			case 'duration':
				$stat_query = 'SUM(end_time - start_time)';
				break;
			case 'price':
				$stat_query = 'sum(total)';
				break;
			case 'bookings':
				$stat_query = 'count(id)';
				break;
		}
		$select_query = $stat_query . ' as stat';
		if ( $args['group_by'] ) {
			$select_query .= ',' . $args['group_by'];
		}
		$bookings->select( $select_query );


		if ( $args['date_from'] ) {
			$bookings->where( [ 'start_date >=' => $args['date_from'] ] );
		}
		if ( $args['date_to'] ) {
			$bookings->where( [ 'start_date <=' => $args['date_to'] ] );
		}
		if ( $args['service_id'] ) {
			$bookings->where( [ 'service_id' => $args['service_id'] ] );
		}
		if ( $args['agent_id'] ) {
			$bookings->where( [ 'agent_id' => $args['agent_id'] ] );
		}
		if ( $args['location_id'] ) {
			$bookings->where( [ 'location_id' => $args['location_id'] ] );
		}
		if ( $args['customer_id'] ) {
			$bookings->where( [ 'customer_id' => $args['customer_id'] ] );
		}
		if ( $args['group_by'] ) {
			$bookings->group_by( $args['group_by'] );
		}
		// TODO, need to support custom status exclusions
		if ( $args['exclude_status'] == LATEPOINT_BOOKING_STATUS_CANCELLED ) {
			$bookings->should_not_be_cancelled();
		}

		$stat_total = $bookings->get_results( ARRAY_A );
		if ( $args['group_by'] ) {
			return $stat_total;
		} else {
			return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
		}
	}

	public static function get_new_customer_stat_for_period( DateTime $date_from, DateTime $date_to, \LatePoint\Misc\Filter $filter ) {
		// TODO make sure filter is respected
		$customers = new OsCustomerModel();

		return $customers->filter_allowed_records()->where(
			[
				'created_at >=' => $date_from->format( 'Y-m-d' ),
				'created_at <=' => $date_to->format( 'Y-m-d' ),
			]
		)->count();
	}

	public static function get_stat_for_period( $stat, $date_from, $date_to, \LatePoint\Misc\Filter $filter, $group_by = false ) {
		if ( ! in_array( $stat, [ 'duration', 'price', 'bookings' ] ) ) {
			return false;
		}
		if ( ! in_array( $group_by, [ false, 'agent_id', 'service_id', 'location_id' ] ) ) {
			return false;
		}
		$bookings = new OsBookingModel();
		switch ( $stat ) {
			case 'duration':
				$stat_query = 'SUM(end_time - start_time)';
				break;
			case 'price':
				$stat_query = 'sum(' . LATEPOINT_TABLE_ORDER_ITEMS . '.total)';
				$bookings->join( LATEPOINT_TABLE_ORDER_ITEMS, [ 'id' => $bookings->table_name . '.order_item_id' ] );
				$bookings->join( LATEPOINT_TABLE_ORDERS, [ 'id' => LATEPOINT_TABLE_ORDER_ITEMS . '.order_id' ] );
				break;
			case 'bookings':
				$stat_query = 'count(id)';
				break;
		}
		$select_query = $stat_query . ' as stat';
		if ( $group_by ) {
			$select_query .= ',' . $group_by;
		}
		$bookings->select( $select_query )->where(
			[
				'start_date >='  => $date_from,
				'start_date <= ' => $date_to,
			]
		);

		if ( $filter->service_id ) {
			$bookings->where( [ 'service_id' => $filter->service_id ] );
		}
		if ( $filter->agent_id ) {
			$bookings->where( [ 'agent_id' => $filter->agent_id ] );
		}
		if ( $filter->location_id ) {
			$bookings->where( [ 'location_id' => $filter->location_id ] );
		}

		$bookings->should_not_be_cancelled();

		if ( $group_by ) {
			$bookings->group_by( $group_by );
		}

		$stat_total = $bookings->get_results( ARRAY_A );
		if ( $group_by ) {
			return $stat_total;
		} else {
			return isset( $stat_total[0]['stat'] ) ? $stat_total[0]['stat'] : 0;
		}
	}

	public static function get_total_bookings_per_day_for_period( $date_from, $date_to, \LatePoint\Misc\Filter $filter ) {
		$bookings = new OsBookingModel();
		$bookings->select( 'count(id) as bookings_per_day, start_date' )
				->where(
					[
						'start_date >=' => $date_from,
						'start_date <=' => $date_to,
					]
				)
				 ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
		if ( $filter->service_id ) {
			$bookings->where( [ 'service_id' => $filter->service_id ] );
		}
		if ( $filter->agent_id ) {
			$bookings->where( [ 'agent_id' => $filter->agent_id ] );
		}
		if ( $filter->location_id ) {
			$bookings->where( [ 'location_id' => $filter->location_id ] );
		}
		$bookings->group_by( 'start_date' );

		return $bookings->get_results();
	}


	public static function get_min_max_work_periods( $specific_weekdays = false, $service_id = false, $agent_id = false ) {
		$select_string = 'MIN(start_time) as start_time, MAX(end_time) as end_time';
		$work_periods  = new OsWorkPeriodModel();
		$work_periods  = $work_periods->select( $select_string );
		$query_args    = array(
			'service_id' => 0,
			'agent_id'   => 0,
		);
		if ( $service_id ) {
			$query_args['service_id'] = $service_id;
		}
		if ( $agent_id ) {
			$query_args['agent_id'] = $agent_id;
		}
		if ( $specific_weekdays && ! empty( $specific_weekdays ) ) {
			$query_args['week_day'] = $specific_weekdays;
		}
		$results = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
		if ( ( $service_id || $agent_id ) && empty( $results['min_start_time'] ) ) {
			if ( $service_id && empty( $results['min_start_time'] ) ) {
				$query_args['service_id'] = 0;
				$work_periods             = new OsWorkPeriodModel();
				$work_periods             = $work_periods->select( $select_string );
				$results                  = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
			}
			if ( $agent_id && empty( $results['min_start_time'] ) ) {
				$query_args['agent_id'] = 0;
				$work_periods           = new OsWorkPeriodModel();
				$work_periods           = $work_periods->select( $select_string );
				$results                = $work_periods->set_limit( 1 )->where( $query_args )->get_results( ARRAY_A );
			}
		}
		if ( $results ) {
			return array( $results['start_time'], $results['end_time'] );
		} else {
			return false;
		}
	}


	public static function get_work_start_end_time_for_multiple_dates( $dates = false, $service_id = false, $agent_id = false ) {
		$specific_weekdays = array();
		if ( $dates ) {
			foreach ( $dates as $date ) {
				$target_date = new OsWpDateTime( $date );
				$weekday     = $target_date->format( 'N' );
				if ( ! in_array( $weekday, $specific_weekdays ) ) {
					$specific_weekdays[] = $weekday;
				}
			}
		}
		$work_minmax_start_end = self::get_min_max_work_periods( $specific_weekdays, $service_id, $agent_id );

		return $work_minmax_start_end;
	}

	/**
	 * @param int $minute
	 * @param \LatePoint\Misc\WorkPeriod[] $work_periods_arr
	 *
	 * @return bool
	 */
	public static function is_minute_in_work_periods( int $minute, array $work_periods_arr ): bool {
		// print_r($work_periods_arr);
		if ( empty( $work_periods_arr ) ) {
			return false;
		}
		foreach ( $work_periods_arr as $work_period ) {
			// end of period does not count because we cant make appointment with 0 duration
			if ( $work_period->start_time <= $minute && $work_period->end_time > $minute ) {
				return true;
			}
		}

		return false;
	}

	public static function get_calendar_start_end_time( $bookings, $work_start_minutes, $work_end_minutes ) {
		$calendar_start_minutes = $work_start_minutes;
		$calendar_end_minutes   = $work_end_minutes;
		if ( $bookings ) {
			foreach ( $bookings as $bookings_for_agent ) {
				if ( $bookings_for_agent ) {
					foreach ( $bookings_for_agent as $booking ) {
						if ( $booking->start_time < $calendar_start_minutes ) {
							$calendar_start_minutes = $booking->start_time;
						}
						if ( $booking->end_time > $calendar_end_minutes ) {
							$calendar_end_minutes = $booking->end_time;
						}
					}
				}
			}
		}

		return [ $calendar_start_minutes, $calendar_end_minutes ];
	}

	public static function generate_direct_manage_booking_url( OsBookingModel $booking, string $for ): string {
		if ( ! in_array( $for, [ 'agent', 'customer' ] ) ) {
			return '';
		}
		$key = $booking->get_key_to_manage_for( $for );
		$url = OsRouterHelper::build_admin_post_link( [ 'manage_booking_by_key', 'show' ], [ 'key' => $key ] );

		return $url;
	}

	/**
	 * Renders the customer-facing "Cancel" button for a booking, behind a filter so addons
	 * (e.g. the pro cancellation-reason feature) can replace its behavior without editing core.
	 *
	 * @param OsBookingModel $booking
	 * @param string         $route_prefix controller prefix for the cancel route ('customer_cabinet' or 'manage_booking_by_key')
	 * @param string|null    $key          manage-by-key key (null for the logged-in customer cabinet)
	 * @param string         $css_classes  button CSS classes
	 */
	public static function generate_cancel_booking_button( OsBookingModel $booking, string $route_prefix, ?string $key = null, string $css_classes = 'latepoint-btn latepoint-btn-danger latepoint-btn-link' ): string {
		$os_params = OsUtilHelper::build_os_params( $key ? [ 'key' => $key ] : [ 'id' => $booking->id ], 'cancel_booking_' . $booking->id );
		$route     = OsRouterHelper::build_route_name( $route_prefix, 'request_cancellation' );
		ob_start();
		?>
		<a href="#" class="<?php echo esc_attr( $css_classes ); ?> os-confirm-alert"
		   data-os-confirm-title="<?php esc_attr_e( 'Cancel Appointment?', 'latepoint' ); ?>"
		   data-os-prompt="<?php esc_attr_e( 'Are you sure you want to cancel this appointment?', 'latepoint' ); ?>"
		   data-os-confirm-button="<?php esc_attr_e( 'Confirm Cancellation', 'latepoint' ); ?>"
		   data-os-success-action="reload"
		   data-os-action="<?php echo esc_attr( $route ); ?>"
		   data-os-params="<?php echo esc_attr( $os_params ); ?>">
			<i class="latepoint-icon latepoint-icon-ui-24"></i>
			<span><?php esc_html_e( 'Cancel', 'latepoint' ); ?></span>
		</a>
		<?php
		$html = ob_get_clean();

		/**
		 * Filters the customer-facing cancel-booking button HTML.
		 *
		 * @param string         $html
		 * @param OsBookingModel $booking
		 * @param string         $route_prefix
		 * @param string|null    $key
		 *
		 * @since 5.2.0
		 * @hook latepoint_customer_cancel_booking_button
		 */
		return apply_filters( 'latepoint_customer_cancel_booking_button', $html, $booking, $route_prefix, $key );
	}

	public static function generate_summary_actions_for_booking( OsBookingModel $booking, ?string $key = null ) {
		?>
		<div class="booking-full-summary-actions">
		  <div class="add-to-calendar-wrapper">
			<a href="#" class="open-calendar-types booking-summary-action-btn"><i class="latepoint-icon latepoint-icon-calendar"></i><span><?php esc_html_e( 'Add to Calendar', 'latepoint' ); ?></span></a>
			  <?php echo OsBookingHelper::generate_add_to_calendar_links( $booking, $key ?? $booking->get_key_to_manage_for( 'customer' ) ); ?>
		  </div>
		<a href="<?php echo esc_url( $booking->get_print_link( $key ?? $booking->get_key_to_manage_for( 'customer' ) ) ); ?>" class="print-booking-btn booking-summary-action-btn" target="_blank"><i class="latepoint-icon latepoint-icon-printer"></i><span><?php esc_html_e( 'Print', 'latepoint' ); ?></span></a>
		  <?php
			if ( $booking->is_upcoming() ) {
				if ( OsCustomerHelper::can_reschedule_booking( $booking ) ) { ?>
					<a href="#" class="latepoint-request-booking-reschedule booking-summary-action-btn" data-os-after-call="latepoint_init_reschedule" data-os-lightbox-classes="width-450 reschedule-calendar-wrapper" data-os-action="<?php echo esc_attr( OsRouterHelper::build_route_name( 'manage_booking_by_key', 'request_reschedule_calendar' ) ); ?>" data-os-params="<?php echo esc_attr( OsUtilHelper::build_os_params( [ 'key' => $key ?? $booking->get_key_to_manage_for( 'customer' ) ] ) ); ?>" data-os-output-target="lightbox">
						<i class="latepoint-icon latepoint-icon-calendar"></i>
						<span><?php esc_html_e( 'Reschedule', 'latepoint' ); ?></span>
					</a>
					<?php
				}
				if ( OsCustomerHelper::can_cancel_booking( $booking ) ) {
					echo self::generate_cancel_booking_button( $booking, 'manage_booking_by_key', $key ?? $booking->get_key_to_manage_for( 'customer' ), 'booking-summary-action-btn cancel-appointment-btn' );
				}
			}
			do_action( 'latepoint_booking_summary_after_booking_actions', $booking );
			?>
	  </div>
		<?php
	}

	public static function generate_summary_for_booking( OsBookingModel $booking, $cart_item_id = false, ?string $viewer = 'customer' ): string {
		$summary_html         = '';
		$summary_html        .= apply_filters( 'latepoint_booking_summary_before_summary_box', '', $booking );
		$summary_html        .= '<div class="summary-box main-box" ' . ( ( $cart_item_id ) ? 'data-cart-item-id="' . $cart_item_id . '"' : '' ) . '>';
		$output_timezone_name = $viewer == 'customer' ? $booking->get_customer_timezone_name() : OsTimeHelper::get_wp_timezone_name();
		if ( ! empty( $booking->start_datetime_utc ) ) {
			$summary_html .= '<div class="summary-box-booking-date-box">';
			$summary_html .= '<div class="summary-box-booking-date-day">' . $booking->start_datetime_in_format( 'j', $output_timezone_name ) . '</div>';
			$summary_html .= '<div class="summary-box-booking-date-month">' . OsUtilHelper::get_month_name_by_number( $booking->start_datetime_in_format( 'n', $output_timezone_name ), true ) . '</div>';
			$summary_html .= '</div>';
		}
		$summary_html    .= '<div class="summary-box-inner">';
		$service_headings = [];
		$service_headings = apply_filters( 'latepoint_booking_summary_service_headings', $service_headings, $booking );
		if ( $service_headings ) {
			$summary_html .= '<div class="summary-box-heading">';
			foreach ( $service_headings as $heading ) {
				$summary_html .= '<div class="sbh-item">' . $heading . '</div>';
			}
			$summary_html .= '<div class="sbh-line"></div>';
			$summary_html .= '</div>';
		}
		$summary_html .= '<div class="summary-box-content os-cart-item">';
		if ( $cart_item_id && OsCartsHelper::can_checkout_multiple_items() ) {
			$summary_html .= '<div class="os-remove-item-from-cart" role="button" tabindex="0" data-confirm-text="' . __( 'Are you sure you want to remove this item from your cart?', 'latepoint' ) . '" data-cart-item-id="' . $cart_item_id . '" data-nonce="' . esc_attr( wp_create_nonce( 'remove_item_from_cart' ) ) . '" data-route="' . OsRouterHelper::build_route_name( 'carts', 'remove_item_from_cart' ) . '">
															<div class="os-remove-from-cart-icon"></div>
														</div>';
		}
		$summary_html .= '<div class="sbc-big-item">' . $booking->get_service_name_for_summary() . '</div>';
		if ( $booking->start_date ) {
			$summary_html .= '<div class="sbc-highlighted-item">' . $booking->get_nice_datetime_for_summary( $viewer ) . '</div>';
		}
		/**
		 * Output summary of the booking data after a start date and time
		 *
		 * @since 5.2.0
		 * @hook latepoint_summary_booking_info_after_start_date
		 *
		 * @param {string} $summary_html HTML of the summary
		 * @param {OsBookingModel} $booking Booking object that is being outputted
		 * @param {string} $cart_item_id ID of a cart item this booking belongs to
		 * @param {string} $viewer determines who is viewing this summary, can be customer or agent
		 *
		 * @returns {string} Filtered HTML
		 */
		$summary_html  = apply_filters( 'latepoint_summary_booking_info_after_start_date', $summary_html, $booking, $cart_item_id, $viewer );
		$summary_html .= '</div>';

		$service_attributes = [];
		$service_attributes = apply_filters( 'latepoint_booking_summary_service_attributes', $service_attributes, $booking );
		if ( $service_attributes ) {
			$summary_html .= '<div class="summary-attributes sa-clean">';
			foreach ( $service_attributes as $attribute ) {
				$summary_html .= '<span>' . $attribute['label'] . ': <strong>' . $attribute['value'] . '</strong></span>';
			}
			$summary_html .= '</div>';
		}
		$summary_html .= '</div>';
		$summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box_inner', '', $booking );
		$summary_html .= '</div>';
		$summary_html .= apply_filters( 'latepoint_booking_summary_after_summary_box', '', $booking );

		return $summary_html;
	}


	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_agent( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'agent_id' ) ) ) == 1 );
	}

	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_location( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'location_id' ) ) ) == 1 );
	}

	/**
	 * @param OsBookingModel[] $bookings
	 *
	 * @return bool
	 */
	public static function bookings_have_same_service( array $bookings ): bool {
		return ( count( array_unique( array_column( $bookings, 'service_id' ) ) ) == 1 );
	}

	public static function prepare_new_from_params( array $params ): OsBookingModel {
		$booking = new OsBookingModel();

		$services = OsServiceHelper::get_allowed_active_services();
		$agents   = OsAgentHelper::get_allowed_active_agents();

		// LOAD FROM PASSED PARAMS
		$booking->order_item_id = $params['order_item_id'] ?? '';
		$booking->service_id    = ! empty( $params['service_id'] ) ? OsUtilHelper::first_value_if_array( $params['service_id'] ) : '';
		if ( empty( $booking->service_id ) && ! empty( $services ) ) {
			$booking->service_id = $services[0]->id;
		}

		$booking->agent_id = ! empty( $params['agent_id'] ) ? OsUtilHelper::first_value_if_array( $params['agent_id'] ) : '';
		if ( empty( $booking->agent_id ) && ! empty( $agents ) ) {
			$booking->agent_id = $agents[0]->id;
		}

		if ( ! empty( $params['order_id'] ) ) {
			$order                = new OsOrderModel( $params['order_id'] );
			$booking->customer_id = $order->customer_id;
		} else {
			$booking->customer_id = ! empty( $params['customer_id'] ) ? OsUtilHelper::first_value_if_array( $params['customer_id'] ) : '';
		}

		$booking->location_id = ! empty( $params['location_id'] ) ? OsUtilHelper::first_value_if_array( $params['location_id'] ) : OsLocationHelper::get_default_location_id( true );
		$booking->start_date  = $params['start_date'] ?? OsTimeHelper::today_date( 'Y-m-d' );
		$booking->start_time  = $params['start_time'] ?? 600;

		$booking->end_time      = ( $booking->service_id ) ? $booking->calculate_end_time() : $booking->start_time + 60;
		$booking->end_date      = $booking->calculate_end_date();
		$booking->buffer_before = ( $booking->service_id ) ? $booking->service->buffer_before : 0;
		$booking->buffer_after  = ( $booking->service_id ) ? $booking->service->buffer_after : 0;
		$booking->status        = LATEPOINT_BOOKING_STATUS_APPROVED;

		return $booking;
	}

	/**
	 * Returns the <th> HTML for a single column header cell in the bookings table.
	 *
	 * @param string $col_key         Unified column key (e.g. 'id', 'datetime', 'customer.email').
	 * @param array  $col_def         Column definition from get_all_bookings_table_columns().
	 * @param string $order_key       Currently active sort key.
	 * @param string $order_dir       Currently active sort direction ('asc'|'desc').
	 * @return string
	 */
	public static function render_table_header_cell( string $col_key, array $col_def, string $order_key, string $order_dir ): string {
		switch ( $col_key ) {
			case 'id':
				$active_class = ( $order_key === 'booking_id' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
				return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_id">' . esc_html__( 'ID', 'latepoint' ) . '</th>';

			case 'service':
				return '<th>' . esc_html__( 'Service', 'latepoint' ) . '</th>';

			case 'datetime':
				$active_class = ( $order_key === 'booking_start_datetime' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
				return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_start_datetime">' . esc_html__( 'Date/Time', 'latepoint' ) . '</th>';

			case 'time_left':
				$active_class = ( $order_key === 'booking_time_left' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
				return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_time_left">' . esc_html__( 'Time Left', 'latepoint' ) . '</th>';

			case 'agent':
				return '<th>' . esc_html__( 'Agent', 'latepoint' ) . '</th>';

			case 'location':
				return '<th>' . esc_html__( 'Location', 'latepoint' ) . '</th>';

			case 'customer':
				return '<th>' . esc_html__( 'Customer', 'latepoint' ) . '</th>';

			case 'status':
				return '<th>' . esc_html__( 'Status', 'latepoint' ) . '</th>';

			case 'payment_status':
				return '<th>' . esc_html__( 'Payment Status', 'latepoint' ) . '</th>';

			case 'created_on':
				$active_class = ( $order_key === 'booking_created_on' ) ? ' ordered-' . esc_attr( $order_dir ) : '';
				return '<th class="os-sortable-column' . $active_class . '" data-order-key="booking_created_on">' . esc_html__( 'Created On', 'latepoint' ) . '</th>';

			default:
				// Extra columns.
				return '<th>' . esc_html( $col_def['label'] ) . '</th>';
		}
	}

	/**
	 * Returns the <th> HTML for a single column filter cell in the bookings table header.
	 *
	 * @param string $col_key       Unified column key.
	 * @param array  $col_def       Column definition.
	 * @param array  $services_list Keyed list of services for the service filter select.
	 * @param array  $agents_list   Keyed list of agents for the agent filter select.
	 * @param array  $locations_list Keyed list of locations for the location filter select.
	 * @return string
	 */
	public static function render_table_filter_cell( string $col_key, array $col_def, array $services_list, array $agents_list, array $locations_list ): string {
		switch ( $col_key ) {
			case 'id':
				return '<th>' . OsFormHelper::text_field(
					'filter[id]',
					false,
					'',
					[
						'placeholder' => __( 'ID', 'latepoint' ),
						'theme'       => 'bordered',
						'style'       => 'width: 60px;',
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'service':
				return '<th>' . OsFormHelper::select_field(
					'filter[service_id]',
					false,
					$services_list,
					'',
					[
						'placeholder' => __( 'All Services', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'datetime':
				ob_start();
				?>
				<th>
					<div class="os-form-group">
						<div class="os-date-range-picker os-table-filter-datepicker" data-can-be-cleared="yes" data-no-value-label="<?php esc_attr_e( 'Search by Appointment Date', 'latepoint' ); ?>" data-clear-btn-label="<?php esc_attr_e( 'Reset Date Search', 'latepoint' ); ?>">
							<span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
							<i class="latepoint-icon latepoint-icon-chevron-down"></i>
							<input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[booking_date_from]" value=""/>
							<input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[booking_date_to]" value=""/>
						</div>
					</div>
				</th>
				<?php
				return ob_get_clean();

			case 'time_left':
				return '<th>' . OsFormHelper::select_field(
					'filter[time_status]',
					false,
					[
						'upcoming' => __( 'Upcoming', 'latepoint' ),
						'past'     => __( 'Past', 'latepoint' ),
						'now'      => __( 'Happening Now', 'latepoint' ),
					],
					'',
					[
						'placeholder' => __( 'Show All', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'agent':
				return '<th>' . OsFormHelper::select_field(
					'filter[agent_id]',
					false,
					$agents_list,
					'',
					[
						'placeholder' => __( 'All Agents', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'location':
				return '<th>' . OsFormHelper::select_field(
					'filter[location_id]',
					false,
					$locations_list,
					'',
					[
						'placeholder' => __( 'All Locations', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'customer':
				return '<th>' . OsFormHelper::text_field(
					'filter[customer][full_name]',
					false,
					'',
					[
						'class'       => 'os-table-filter',
						'theme'       => 'bordered',
						'placeholder' => __( 'Search by Customer', 'latepoint' ),
					]
				) . '</th>';

			case 'status':
				return '<th>' . OsFormHelper::select_field(
					'filter[status]',
					false,
					OsBookingHelper::get_statuses_list(),
					'',
					[
						'placeholder' => __( 'Show All', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'payment_status':
				return '<th>' . OsFormHelper::select_field(
					'filter[order][payment_status]',
					false,
					OsOrdersHelper::get_order_payment_statuses_list(),
					'',
					[
						'placeholder' => __( 'Show All', 'latepoint' ),
						'class'       => 'os-table-filter',
					]
				) . '</th>';

			case 'created_on':
				ob_start();
				?>
				<th>
					<div class="os-form-group">
						<div class="os-date-range-picker os-table-filter-datepicker" data-can-be-cleared="yes" data-no-value-label="<?php esc_attr_e( 'Filter Date', 'latepoint' ); ?>" data-clear-btn-label="<?php esc_attr_e( 'Reset Date Search', 'latepoint' ); ?>">
							<span class="range-picker-value"><?php esc_html_e( 'Filter Date', 'latepoint' ); ?></span>
							<i class="latepoint-icon latepoint-icon-chevron-down"></i>
							<input type="hidden" class="os-table-filter os-datepicker-date-from" name="filter[created_date_from]" value=""/>
							<input type="hidden" class="os-table-filter os-datepicker-date-to" name="filter[created_date_to]" value=""/>
						</div>
					</div>
				</th>
				<?php
				return ob_get_clean();

			default:
				// Extra columns.
				if ( 'extra' !== $col_def['type'] ) {
					return '<th></th>';
				}
				$extra_type  = $col_def['extra_type'];
				$extra_key   = $col_def['extra_key'];
				$extra_label = $col_def['label'];
				$field_name  = ( 'booking' !== $extra_type ) ? 'filter[' . $extra_type . '][' . $extra_key . ']' : 'filter[' . $extra_key . ']';
				// Skip the filter input for magic properties that can't be queried.
				if ( 'booking' === $extra_type && ! property_exists( 'OsBookingModel', $extra_key ) ) {
					return '<th></th>';
				}
				return '<th>' . OsFormHelper::text_field(
					$field_name,
					false,
					'',
					[
						'class'       => 'os-table-filter',
						'theme'       => 'bordered',
						'placeholder' => $extra_label,
					]
				) . '</th>';
		}
	}

	/**
	 * Returns the <td> HTML for a single data cell in the bookings table body.
	 *
	 * @param string          $col_key  Unified column key.
	 * @param array           $col_def  Column definition.
	 * @param OsBookingModel  $booking  Current booking row.
	 * @return string
	 */
	public static function render_table_body_cell( string $col_key, array $col_def, OsBookingModel $booking ): string {
		switch ( $col_key ) {
			case 'id':
				return '<td class="os-column-faded text-right has-floating-button">' .
					esc_html( $booking->id ) .
					'<div class="os-floating-button"><i class="latepoint-icon latepoint-icon-edit-3"></i></div>' .
					'</td>';

			case 'service':
				return '<td><div class="os-with-service-color"><span class="cell-link-content">' .
					'<span class="os-column-service-color" style="background-color: ' . esc_attr( $booking->service->bg_color ) . '"></span>' .
					'<span>' . esc_html( $booking->service->name ) . '</span>' .
					'</span></div></td>';

			case 'datetime':
				return '<td><strong>' . esc_html( $booking->nice_start_date ) . '</strong> <span class="os-dot"></span> <span>' . esc_html( $booking->nice_start_time ) . '</span></td>';

			case 'time_left':
				$time_html = '';
				switch ( $booking->time_status() ) {
					case 'upcoming':
						$time_html = $booking->time_left;
						break;
					case 'now':
						$time_html = '<span class="time-left is-now">' . esc_html__( 'Now', 'latepoint' ) . '</span>';
						break;
					case 'past':
						$time_html = '<span class="time-left is-past">' . esc_html__( 'Past', 'latepoint' ) . '</span>';
						break;
				}
				return '<td><span class="in-table-time-left">' . $time_html . '</span></td>';

			case 'agent':
				return '<td><div class="os-with-avatar">' .
					'<span class="cell-link-content">' .
					'<span class="os-avatar" style="background-image: url(' . esc_url( $booking->agent->get_avatar_url() ) . ')"></span>' .
					'<span class="os-name">' . esc_html( $booking->agent->full_name ) . '</span>' .
					'</span>' .
					'<div class="os-clickable-popup-trigger"' .
					' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'agents', 'mini_profile' ) ) . '"' .
					' data-os-params="' . esc_attr(
						OsUtilHelper::build_os_params(
							[
								'agent_id'   => $booking->agent_id,
								'booking_id' => $booking->id,
							]
						)
					) . '">' .
					'<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
					'</div>' .
					'</div></td>';

			case 'location':
				return '<td>' . esc_html( $booking->location->name ) . '</td>';

			case 'customer':
				return '<td><div class="os-with-avatar">' .
					'<span class="cell-link-content">' .
					'<span class="os-avatar" style="background-image: url(' . esc_url( $booking->customer->get_avatar_url() ) . ')"></span>' .
					'<span class="os-name">' . esc_html( $booking->customer->full_name ) . '</span>' .
					'</span>' .
					'<div class="os-clickable-popup-trigger"' .
					' data-route="' . esc_attr( OsRouterHelper::build_route_name( 'customers', 'mini_profile' ) ) . '"' .
					' data-os-params="' . esc_attr(
						OsUtilHelper::build_os_params(
							[
								'customer_id' => $booking->customer_id,
								'booking_id'  => $booking->id,
							]
						)
					) . '">' .
					'<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
					'</div>' .
					'</div></td>';

			case 'status':
				return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->status ) . '">' . esc_html( $booking->nice_status ) . '</span></td>';

			case 'payment_status':
				return '<td><span class="os-column-status os-column-status-' . esc_attr( $booking->order->payment_status ) . '">' . esc_html( OsOrdersHelper::get_nice_order_payment_status_name( $booking->order->payment_status ) ) . '</span></td>';

			case 'created_on':
				return '<td>' . esc_html( $booking->nice_created_at ) . '</td>';

			default:
				// Extra columns: read from the appropriate model via property, getter, or meta.
				if ( 'extra' !== $col_def['type'] ) {
					return '<td></td>';
				}
				$extra_type = $col_def['extra_type'];
				$extra_key  = $col_def['extra_key'];
				$model_obj  = ( 'booking' === $extra_type ) ? $booking : $booking->$extra_type;

				if ( property_exists( $model_obj, $extra_key ) || method_exists( $model_obj, 'get_' . $extra_key ) ) {
					return '<td>' . esc_html( $model_obj->$extra_key ) . '</td>';
				}

				$column_value = $model_obj->get_meta_by_key( $extra_key );
				if ( $column_value &&
					( $custom_fields_raw = OsSettingsHelper::get_settings_value( 'custom_fields_for_' . $extra_type, false ) ) &&
					( $custom_fields_arr = json_decode( $custom_fields_raw, true ) ) &&
					isset( $custom_fields_arr[ $extra_key ]['type'] ) &&
					'multiselect' === $custom_fields_arr[ $extra_key ]['type']
				) {
					$decoded      = json_decode( $column_value );
					$column_value = is_array( $decoded ) ? implode( ', ', $decoded ) : $column_value;
				}
				return '<td>' . esc_html( $column_value ) . '</td>';
		}
	}

	/**
	 * Returns the <th> HTML for the bulk-selection column header (with the select-all checkbox).
	 *
	 * @return string
	 */
	public static function render_bulk_select_header_cell(): string {
		return '<th class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
			'<label class="os-bulk-row-check-w">' .
			'<input type="checkbox" class="os-bulk-select-all" aria-label="' . esc_attr__( 'Select all appointments on this page', 'latepoint' ) . '" />' .
			'<span class="os-bulk-check-box"></span>' .
			'</label>' .
			'</th>';
	}

	/**
	 * Returns the empty spacer <th> HTML for the bulk-selection column in filter and footer rows.
	 *
	 * @return string
	 */
	public static function render_bulk_select_spacer_cell(): string {
		return '<th class="os-bulk-select-cell"></th>';
	}

	/**
	 * Returns the <td> HTML for the bulk-selection column on a single body row.
	 *
	 * @param OsBookingModel $booking Current booking row.
	 * @return string
	 */
	public static function render_bulk_select_body_cell( OsBookingModel $booking ): string {
		/* translators: %d: appointment ID */
		$aria_label = sprintf( __( 'Select appointment %d', 'latepoint' ), $booking->id );
		return '<td class="os-bulk-select-cell" onclick="event.stopPropagation();">' .
			'<label class="os-bulk-row-check-w">' .
			'<input type="checkbox" class="os-bulk-row-check" value="' . esc_attr( $booking->id ) . '" aria-label="' . esc_attr( $aria_label ) . '" />' .
			'<span class="os-bulk-check-box"></span>' .
			'</label>' .
			'</td>';
	}

	/**
	 * Returns the HTML for the bulk-actions toolbar shown above the appointments table.
	 *
	 * @return string
	 */
	public static function render_bulk_actions_bar(): string {
		return '<div class="os-bulk-actions-bar" data-bulk-nonce="' . esc_attr( wp_create_nonce( 'bulk_destroy_bookings' ) ) . '">' .
			'<div class="os-bulk-actions-info">' .
			'<span class="os-bulk-selected-count">0</span>' .
			// Label is swapped between singular and plural by JS based on selection count.
			'<span class="os-bulk-selected-label">' . esc_html__( 'Appointments selected', 'latepoint' ) . '</span>' .
			'</div>' .
			'<div class="os-bulk-actions-controls">' .
			'<a href="#" class="latepoint-btn latepoint-btn-danger os-bulk-action-delete">' .
			'<i class="latepoint-icon latepoint-icon-trash-2"></i><span>' . esc_html__( 'Delete Selected', 'latepoint' ) . '</span>' .
			'</a>' .
			'<a href="#" class="os-bulk-actions-clear">' . esc_html__( 'Clear selection', 'latepoint' ) . '</a>' .
			'</div>' .
			'</div>';
	}
}
