<?php
/**
 * Student booking slot generator.
 *
 * @package TutorHelper_Bookings
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Generates publicly bookable student lesson slots.
 */
final class TutorHelper_Student_Booking_Slots {

	/**
	 * Return available slots for one Booking Type and local date.
	 *
	 * @param int    $tutor_user_id Tutor WordPress user ID.
	 * @param int    $booking_type_id Booking Type ID.
	 * @param string $date Local date in Y-m-d format.
	 *
	 * @return array|WP_Error
	 */
	public static function get_for_date(
		$tutor_user_id,
		$booking_type_id,
		$date
	) {
		$tutor_user_id =
			absint( $tutor_user_id );

		$booking_type_id =
			absint( $booking_type_id );

		$date =
			sanitize_text_field(
				(string) $date
			);

		if (
			! $tutor_user_id
			|| ! $booking_type_id
		) {
			return new WP_Error(
				'tutorhelper_student_booking_slot_context_missing',
				__(
					'The booking slot context is incomplete.',
					'tutorhelper-bookings'
				)
			);
		}

		$booking_type =
			TutorHelper_Booking_Types::get_by_id(
				$booking_type_id
			);

		if (
			! $booking_type
			|| ! absint(
				$booking_type->is_active
			)
			|| 'bookable'
				!== sanitize_key(
					$booking_type
						->online_booking_visibility
				)
		) {
			return new WP_Error(
				'tutorhelper_student_booking_type_unavailable',
				__(
					'The selected service is not available.',
					'tutorhelper-bookings'
				)
			);
		}

		$timezone =
			wp_timezone();

		$day =
			DateTimeImmutable::createFromFormat(
				'!Y-m-d',
				$date,
				$timezone
			);

		$errors =
			DateTimeImmutable::getLastErrors();

		if (
			false === $day
			|| (
				is_array( $errors )
				&& (
					0 < $errors['warning_count']
					|| 0 < $errors['error_count']
				)
			)
			|| $day->format( 'Y-m-d' )
				!== $date
		) {
			return new WP_Error(
				'tutorhelper_student_booking_date_invalid',
				__(
					'The selected booking date is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		$duration_minutes =
			absint(
				$booking_type
					->duration_minutes
			);

		if ( ! $duration_minutes ) {
			return new WP_Error(
				'tutorhelper_student_booking_duration_invalid',
				__(
					'The selected service has an invalid duration.',
					'tutorhelper-bookings'
				)
			);
		}

		$calendar =
			TutorHelper_Availability_Calendar::
				get_range(
					$tutor_user_id,
					$date,
					$date
				);

		if ( is_wp_error( $calendar ) ) {
			return $calendar;
		}

		$calendar_day =
			self::find_day(
				$calendar,
				$date
			);

		if (
            empty( $calendar_day )
            || empty(
                $calendar_day['availability']
            )
            || ! is_array(
                $calendar_day['availability']
            )
        ) {
            return array();
        }

		$notice_period_hours =
			absint(
				$booking_type
					->notice_period_hours
			);

		$now =
			new DateTimeImmutable(
				'now',
				$timezone
			);

		$earliest_start =
			$now->modify(
				sprintf(
					'+%d hours',
					$notice_period_hours
				)
			);

		$slots = array();

		foreach (
			$calendar_day['availability']
				as $availability
		) {
			if (
				! is_array( $availability )
				|| ! self::availability_allows_type(
					$availability,
					$booking_type_id
				)
			) {
				continue;
			}

			$availability_start =
				self::create_local_datetime(
					$date,
					isset(
						$availability[
							'start_time'
						]
					)
						? $availability[
							'start_time'
						]
						: '',
					$timezone
				);

			$availability_end =
				self::create_local_datetime(
					$date,
					isset(
						$availability[
							'end_time'
						]
					)
						? $availability[
							'end_time'
						]
						: '',
					$timezone
				);

			if (
				! $availability_start
				|| ! $availability_end
				|| $availability_end
					<= $availability_start
			) {
				continue;
			}

			$cursor =
				$availability_start;

			while ( $cursor < $availability_end ) {
				$slot_end =
					$cursor->modify(
						sprintf(
							'+%d minutes',
							$duration_minutes
						)
					);

				if (
					$slot_end
						> $availability_end
				) {
					break;
				}

				if (
					$cursor
						>= $earliest_start
				) {
					$slot =
						self::prepare_slot(
							$tutor_user_id,
							$booking_type,
							$availability,
							$cursor,
							$slot_end,
							$timezone
						);

					if ( is_array( $slot ) ) {
						$slots[
							$slot['start']
						] = $slot;
					}
				}

				/*
				 * Core currently uses the Booking Type duration as the
				 * slot interval, producing consecutive non-overlapping
				 * choices.
				 */
				$cursor =
					$slot_end;
			}
		}

		ksort( $slots );

		return array_values( $slots );
	}

	/**
	 * Prepare and validate one candidate slot.
	 *
	 * @param int               $tutor_user_id Tutor ID.
	 * @param object            $booking_type Booking Type.
	 * @param array             $availability Availability occurrence.
	 * @param DateTimeImmutable $start Local start.
	 * @param DateTimeImmutable $end Local end.
	 * @param DateTimeZone      $timezone Site timezone.
	 *
	 * @return array|null
	 */
	private static function prepare_slot(
		$tutor_user_id,
		$booking_type,
		$availability,
		$start,
		$end,
		$timezone
	) {
		$location_types =
			isset(
				$availability[
					'location_types'
				]
			)
			&& is_array(
				$availability[
					'location_types'
				]
			)
				? array_values(
					array_filter(
						array_map(
							'sanitize_key',
							$availability[
								'location_types'
							]
						)
					)
				)
				: array();

		$booking_locations =
			self::get_booking_type_locations(
				$booking_type
			);

		$matching_locations =
			empty( $location_types )
				? $booking_locations
				: array_values(
					array_intersect(
						$booking_locations,
						$location_types
					)
				);

		if ( empty( $matching_locations ) ) {
			return null;
		}

		$location_type =
			$matching_locations[0];

		$conditions =
			TutorHelper_Booking_Conditions::
				evaluate(
					array(
						'tutor_user_id' =>
							$tutor_user_id,

						'booking_type_id' =>
							absint(
								$booking_type->id
							),

						'booking_start' =>
							$start->format(
								'Y-m-d H:i:s'
							),

						'booking_end' =>
							$end->format(
								'Y-m-d H:i:s'
							),

						'timezone' =>
							$timezone->getName(),

						'location_type' =>
							$location_type,
					)
				);

		if ( is_wp_error( $conditions ) ) {
            return null;
        }

        $is_publicly_available =
            ! empty(
                $conditions[
                    'has_public_availability'
                ]
            )
            && ! empty(
                $conditions[
                    'is_within_public_hours'
                ]
            )
            && ! empty(
                $conditions[
                    'is_booking_type_publicly_allowed'
                ]
            )
            && ! empty(
                $conditions[
                    'is_location_publicly_allowed'
                ]
            );

        $is_blocked =
            ! empty(
                $conditions[
                    'is_tutor_leave'
                ]
            )
            || ! empty(
                $conditions[
                    'has_booking_conflict'
                ]
            );

        if (
            ! $is_publicly_available
            || $is_blocked
        ) {
            return null;
        }

		return array(
			'start' =>
				$start->format(
					'Y-m-d H:i:s'
				),

			'end' =>
				$end->format(
					'Y-m-d H:i:s'
				),

			'start_time' =>
				$start->format(
					'H:i'
				),

			'end_time' =>
				$end->format(
					'H:i'
				),

			'label' =>
				sprintf(
					'%1$s–%2$s',
					wp_date(
						get_option(
							'time_format'
						),
						$start->getTimestamp(),
						$timezone
					),
					wp_date(
						get_option(
							'time_format'
						),
						$end->getTimestamp(),
						$timezone
					)
				),

			'location_type' =>
				$location_type,
		);
	}

	/**
	 * Find one date in calendar data.
	 *
	 * @param array  $calendar Calendar data.
	 * @param string $date Date.
	 *
	 * @return array
	 */
	private static function find_day(
		$calendar,
		$date
	) {
		if (
			empty( $calendar['days'] )
			|| ! is_array(
				$calendar['days']
			)
		) {
			return array();
		}

		foreach (
			$calendar['days']
				as $day
		) {
			if (
				is_array( $day )
				&& isset( $day['date'] )
				&& $date
					=== sanitize_text_field(
						$day['date']
					)
			) {
				return $day;
			}
		}

		return array();
	}

	/**
	 * Determine whether availability permits a Booking Type.
	 *
	 * @param array $availability Availability occurrence.
	 * @param int   $booking_type_id Booking Type ID.
	 *
	 * @return bool
	 */
	private static function availability_allows_type(
		$availability,
		$booking_type_id
	) {
		$allowed_ids =
			isset(
				$availability[
					'allowed_booking_type_ids'
				]
			)
			&& is_array(
				$availability[
					'allowed_booking_type_ids'
				]
			)
				? array_values(
					array_filter(
						array_map(
							'absint',
							$availability[
								'allowed_booking_type_ids'
							]
						)
					)
				)
				: array();

		return empty( $allowed_ids )
			|| in_array(
				absint(
					$booking_type_id
				),
				$allowed_ids,
				true
			);
	}

	/**
	 * Return Booking Type locations.
	 *
	 * @param object $booking_type Booking Type.
	 *
	 * @return array
	 */
	private static function get_booking_type_locations(
		$booking_type
	) {
		$locations = array();

		if (
			! empty(
				$booking_type
					->location_types
			)
		) {
			$locations = explode(
				',',
				$booking_type
					->location_types
			);
		}

		if (
			empty( $locations )
			&& ! empty(
				$booking_type
					->location_type
			)
		) {
			$locations[] =
				$booking_type
					->location_type;
		}

		return array_values(
			array_unique(
				array_filter(
					array_map(
						'sanitize_key',
						$locations
					)
				)
			)
		);
	}

	/**
	 * Create a local datetime from date and availability time.
	 *
	 * @param string       $date Date.
	 * @param string       $time Time.
	 * @param DateTimeZone $timezone Timezone.
	 *
	 * @return DateTimeImmutable|null
	 */
	private static function create_local_datetime(
		$date,
		$time,
		$timezone
	) {
		$time =
			sanitize_text_field(
				(string) $time
			);

		if ( 5 === strlen( $time ) ) {
			$time .= ':00';
		}

		$value =
			$date . ' ' . $time;

		$datetime =
			DateTimeImmutable::createFromFormat(
				'!Y-m-d H:i:s',
				$value,
				$timezone
			);

		$errors =
			DateTimeImmutable::getLastErrors();

		if (
			false === $datetime
			|| (
				is_array( $errors )
				&& (
					0 < $errors['warning_count']
					|| 0 < $errors['error_count']
				)
			)
			|| $datetime->format(
				'Y-m-d H:i:s'
			) !== $value
		) {
			return null;
		}

		return $datetime;
	}
}
