<?php
/**
 * Shared booking condition evaluator.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Evaluates availability, exclusions and conflicts without saving.
 */
final class TutorHelper_Booking_Conditions {

	/**
	 * Condition severity: information only.
	 *
	 * @var string
	 */
	const SEVERITY_INFO = 'info';

	/**
	 * Condition severity: tutor may override.
	 *
	 * @var string
	 */
	const SEVERITY_WARNING = 'warning';

	/**
	 * Condition severity: save must be blocked.
	 *
	 * @var string
	 */
	const SEVERITY_BLOCKING = 'blocking';

	/**
	 * Parse and validate a timezone identifier.
	 *
	 * @param mixed $value Raw timezone.
	 *
	 * @return DateTimeZone|WP_Error
	 */
	private static function parse_timezone(
		$value
	) {
		$value = sanitize_text_field(
			(string) $value
		);

		if ( '' === $value ) {
			$value = wp_timezone()->getName();
		}

		try {
			return new DateTimeZone(
				$value
			);
		} catch ( Exception $exception ) {
			return new WP_Error(
				'tutorhelper_booking_conditions_timezone_invalid',
				__(
					'The booking timezone is invalid.',
					'tutorhelper-bookings'
				)
			);
		}
	}

	/**
	 * Parse one exact local datetime.
	 *
	 * @param mixed        $value    Date and time.
	 * @param DateTimeZone $timezone Source timezone.
	 * @param string       $code     Error code.
	 * @param string       $message  Error message.
	 *
	 * @return DateTimeImmutable|WP_Error
	 */
	private static function parse_datetime(
		$value,
		DateTimeZone $timezone,
		$code,
		$message
	) {
		$value = sanitize_text_field(
			(string) $value
		);

		foreach (
			array(
				'!Y-m-d H:i:s',
				'!Y-m-d H:i',
			)
			as $format
		) {
			$date =
				DateTimeImmutable::createFromFormat(
					$format,
					$value,
					$timezone
				);

			$errors =
				DateTimeImmutable::getLastErrors();

			if (
				false === $date
				|| (
					is_array( $errors )
					&& (
						0 < $errors['warning_count']
						|| 0 < $errors['error_count']
					)
				)
			) {
				continue;
			}

			$expected =
				'!Y-m-d H:i:s' === $format
					? $date->format(
						'Y-m-d H:i:s'
					)
					: $date->format(
						'Y-m-d H:i'
					);

			if ( $expected === $value ) {
				return $date;
			}
		}

		return new WP_Error(
			$code,
			$message
		);
	}

	/**
	 * Convert an HH:MM[:SS] value into seconds from local midnight.
	 *
	 * @param mixed $value Raw time.
	 *
	 * @return int|null
	 */
	private static function time_to_seconds(
		$value
	) {
		$value = sanitize_text_field(
			(string) $value
		);

		$time =
			DateTimeImmutable::createFromFormat(
				'!H:i:s',
				strlen( $value ) === 5
					? $value . ':00'
					: $value,
				new DateTimeZone( 'UTC' )
			);

		$errors =
			DateTimeImmutable::getLastErrors();

		if (
			false === $time
			|| (
				is_array( $errors )
				&& (
					0 < $errors['warning_count']
					|| 0 < $errors['error_count']
				)
			)
		) {
			return null;
		}

		return (
			((int) $time->format( 'H' ))
			* HOUR_IN_SECONDS
		)
		+ (
			((int) $time->format( 'i' ))
			* MINUTE_IN_SECONDS
		)
		+ (int) $time->format( 's' );
	}


    /**
     * Find one date inside an availability-calendar result.
     *
     * @param array  $calendar Calendar result.
     * @param string $date     Date in Y-m-d format.
     *
     * @return array
     */
    private static function find_calendar_day(
        $calendar,
        $date
    ) {
        if (
            ! is_array( $calendar )
            || empty( $calendar['days'] )
            || ! is_array(
                $calendar['days']
            )
        ) {
            return array(
                'availability' =>
                    array(),

                'exclusions' =>
                    array(),

                'display_types' =>
                    array(),
            );
        }

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

        return array(
            'availability' =>
                array(),

            'exclusions' =>
                array(),

            'display_types' =>
                array(),
        );
    }
    

	/**
	 * Determine whether an occurrence permits the Booking Type.
	 *
	 * An empty allowed list means every active Booking Type is permitted.
	 *
	 * @param array $occurrence      Availability occurrence.
	 * @param int   $booking_type_id Booking Type ID.
	 *
	 * @return bool
	 */
	private static function occurrence_allows_type(
		$occurrence,
		$booking_type_id
	) {
		$allowed = isset(
			$occurrence[
				'allowed_booking_type_ids'
			]
		)
		&& is_array(
			$occurrence[
				'allowed_booking_type_ids'
			]
		)
			? array_values(
				array_filter(
					array_map(
						'absint',
						$occurrence[
							'allowed_booking_type_ids'
						]
					)
				)
			)
			: array();

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

	/**
	 * Determine whether an occurrence permits a location.
	 *
	 * An empty location list means no location-specific restriction.
	 *
	 * @param array  $occurrence   Availability occurrence.
	 * @param string $location_type Booking location.
	 *
	 * @return bool
	 */
	private static function occurrence_allows_location(
		$occurrence,
		$location_type
	) {
		$allowed = isset(
			$occurrence['location_types']
		)
		&& is_array(
			$occurrence['location_types']
		)
			? array_values(
				array_filter(
					array_map(
						'sanitize_key',
						$occurrence[
							'location_types'
						]
					)
				)
			)
			: array();

		return empty( $allowed )
			|| in_array(
				sanitize_key(
					$location_type
				),
				$allowed,
				true
			);
	}

	/**
	 * Determine whether an occurrence fully contains a booking.
	 *
	 * Initial B1 behaviour supports same-local-day bookings. Overnight
	 * availability can be handled separately if later required.
	 *
	 * @param array             $occurrence Availability occurrence.
	 * @param DateTimeImmutable $start      Local booking start.
	 * @param DateTimeImmutable $end        Local booking end.
	 *
	 * @return bool
	 */
	private static function occurrence_contains_booking(
		$occurrence,
		$start,
		$end
	) {
		if (
			$start->format( 'Y-m-d' )
				!== $end->format( 'Y-m-d' )
		) {
			return false;
		}

		$occurrence_start =
			self::time_to_seconds(
				isset(
					$occurrence['start_time']
				)
					? $occurrence[
						'start_time'
					]
					: ''
			);

		$occurrence_end =
			self::time_to_seconds(
				isset(
					$occurrence['end_time']
				)
					? $occurrence[
						'end_time'
					]
					: ''
			);

		if (
			null === $occurrence_start
			|| null === $occurrence_end
		) {
			return false;
		}

		$booking_start =
			self::time_to_seconds(
				$start->format(
					'H:i:s'
				)
			);

		$booking_end =
			self::time_to_seconds(
				$end->format(
					'H:i:s'
				)
			);

		return null !== $booking_start
			&& null !== $booking_end
			&& $booking_start
				>= $occurrence_start
			&& $booking_end
				<= $occurrence_end;
	}

	/**
	 * Return exclusions matching one calendar display type.
	 *
	 * @param array  $day          Calendar day.
	 * @param string $display_type Display classification.
	 *
	 * @return array
	 */
	private static function get_exclusions_by_type(
		$day,
		$display_type
	) {
		$matches = array();

		$exclusions = isset(
			$day['exclusions']
		)
		&& is_array(
			$day['exclusions']
		)
			? $day['exclusions']
			: array();

		foreach ( $exclusions as $exclusion ) {
			if (
				! is_array( $exclusion )
				|| empty(
					$exclusion[
						'display_type'
					]
				)
			) {
				continue;
			}

			if (
				$display_type
					=== sanitize_key(
						$exclusion[
							'display_type'
						]
					)
			) {
				$matches[] = $exclusion;
			}
		}

		return $matches;
	}

	/**
	 * Add one structured condition without duplication.
	 *
	 * @param array  $conditions Condition collection.
	 * @param string $code       Condition code.
	 * @param string $severity   Severity.
	 * @param string $message    User-facing message.
	 * @param array  $context    Additional context.
	 *
	 * @return array
	 */
	private static function add_condition(
		$conditions,
		$code,
		$severity,
		$message,
		$context = array()
	) {
		if ( isset( $conditions[ $code ] ) ) {
			return $conditions;
		}

		$conditions[ $code ] = array(
			'code' =>
				sanitize_key( $code ),

			'severity' =>
				sanitize_key(
					$severity
				),

			'message' =>
				sanitize_text_field(
					$message
				),

			'context' =>
				is_array( $context )
					? $context
					: array(),
		);

		return $conditions;
	}

	/**
	 * Evaluate a proposed booking without saving it.
	 *
	 * Expected values:
	 * - tutor_user_id
	 * - booking_type_id
	 * - booking_start, local Y-m-d H:i[:s]
	 * - booking_end, local Y-m-d H:i[:s]
	 * - timezone
	 * - location_type
	 * - exclude_booking_id, optional when editing
	 *
	 * @param array $args Proposed booking details.
	 *
	 * @return array|WP_Error
	 */
	public static function evaluate(
		$args
	) {
		$args = wp_parse_args(
			is_array( $args )
				? $args
				: array(),
			array(
				'tutor_user_id' =>
					0,

				'booking_type_id' =>
					0,

				'booking_start' =>
					'',

				'booking_end' =>
					'',

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

				'location_type' =>
					'',

				'exclude_booking_id' =>
					0,
			)
		);

		$tutor_user_id = absint(
			$args['tutor_user_id']
		);

		if ( ! $tutor_user_id ) {
			return new WP_Error(
				'tutorhelper_booking_conditions_tutor_missing',
				__(
					'A tutor is required.',
					'tutorhelper-bookings'
				)
			);
		}

		if (
			! get_user_by(
				'id',
				$tutor_user_id
			)
		) {
			return new WP_Error(
				'tutorhelper_booking_conditions_tutor_not_found',
				__(
					'The selected tutor could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

		$booking_type_id = absint(
			$args['booking_type_id']
		);

		if ( ! $booking_type_id ) {
			return new WP_Error(
				'tutorhelper_booking_conditions_type_missing',
				__(
					'A Booking Type is required.',
					'tutorhelper-bookings'
				)
			);
		}

		if (
			! TutorHelper_Booking_Types::get_by_id(
				$booking_type_id
			)
		) {
			return new WP_Error(
				'tutorhelper_booking_conditions_type_not_found',
				__(
					'The selected Booking Type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

		$timezone =
			self::parse_timezone(
				$args['timezone']
			);

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

		$start =
			self::parse_datetime(
				$args['booking_start'],
				$timezone,
				'tutorhelper_booking_conditions_start_invalid',
				__(
					'The booking start is invalid.',
					'tutorhelper-bookings'
				)
			);

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

		$end =
			self::parse_datetime(
				$args['booking_end'],
				$timezone,
				'tutorhelper_booking_conditions_end_invalid',
				__(
					'The booking end is invalid.',
					'tutorhelper-bookings'
				)
			);

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

		if ( $end <= $start ) {
			return new WP_Error(
				'tutorhelper_booking_conditions_end_before_start',
				__(
					'The booking end must be after its start.',
					'tutorhelper-bookings'
				)
			);
		}

		$date_from =
			$start->format(
				'Y-m-d'
			);

		$date_to =
			$end->format(
				'Y-m-d'
			);

		$calendar =
			TutorHelper_Availability_Calendar::
				get_range(
					$tutor_user_id,
					$date_from,
					$date_to
				);

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

		$day =
            self::find_calendar_day(
                $calendar,
                $date_from
            );

		$availability = isset(
			$day['availability']
		)
		&& is_array(
			$day['availability']
		)
			? $day['availability']
			: array();

		$matching_time = array();
		$matching_type = array();
		$matching_location = array();
		$fully_matching = array();

		foreach (
			$availability as $occurrence
		) {
			if ( ! is_array( $occurrence ) ) {
				continue;
			}

			if (
				! self::
					occurrence_contains_booking(
						$occurrence,
						$start,
						$end
					)
			) {
				continue;
			}

			$matching_time[] =
				$occurrence;

			if (
				! self::
					occurrence_allows_type(
						$occurrence,
						$booking_type_id
					)
			) {
				continue;
			}

			$matching_type[] =
				$occurrence;

			if (
				! self::
					occurrence_allows_location(
						$occurrence,
						$args[
							'location_type'
						]
					)
			) {
				continue;
			}

			$matching_location[] =
				$occurrence;

			$fully_matching[] =
				$occurrence;
		}

		$outside_term =
			self::get_exclusions_by_type(
				$day,
				TutorHelper_Availability_Calendar::
					TYPE_OUTSIDE_TERM_TIME
			);

		$tutor_leave =
			self::get_exclusions_by_type(
				$day,
				TutorHelper_Availability_Calendar::
					TYPE_TUTOR_LEAVE
			);

		$public_holiday =
			self::get_exclusions_by_type(
				$day,
				TutorHelper_Availability_Calendar::
					TYPE_PUBLIC_HOLIDAY
			);

		$utc = new DateTimeZone( 'UTC' );

		$utc_start =
			$start->setTimezone(
				$utc
			)->format(
				'Y-m-d H:i:s'
			);

		$utc_end =
			$end->setTimezone(
				$utc
			)->format(
				'Y-m-d H:i:s'
			);

		$has_conflict =
			TutorHelper_Booking_Records::
				has_conflict(
					$tutor_user_id,
					$utc_start,
					$utc_end,
					absint(
						$args[
							'exclude_booking_id'
						]
					)
				);

		$conditions = array();

		if ( empty( $availability ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'no_public_availability',
					self::SEVERITY_WARNING,
					__(
						'No public availability is configured for this date.',
						'tutorhelper-bookings'
					)
				);
		} elseif ( empty( $matching_time ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'outside_public_hours',
					self::SEVERITY_WARNING,
					__(
						'This time is outside the tutor’s public availability.',
						'tutorhelper-bookings'
					)
				);
		} elseif ( empty( $matching_type ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'booking_type_not_publicly_available',
					self::SEVERITY_WARNING,
					__(
						'This Booking Type is not publicly offered during the selected availability.',
						'tutorhelper-bookings'
					)
				);
		} elseif (
			empty( $matching_location )
		) {
			$conditions =
				self::add_condition(
					$conditions,
					'location_not_publicly_available',
					self::SEVERITY_WARNING,
					__(
						'This location is not publicly offered during the selected availability.',
						'tutorhelper-bookings'
					)
				);
		}

		if ( ! empty( $outside_term ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'outside_term_time',
					self::SEVERITY_WARNING,
					__(
						'This booking is outside the tutor’s normal term dates.',
						'tutorhelper-bookings'
					),
					array(
						'exclusions' =>
							$outside_term,
					)
				);
		}

		if ( ! empty( $public_holiday ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'public_holiday',
					self::SEVERITY_WARNING,
					__(
						'This booking falls on a public holiday.',
						'tutorhelper-bookings'
					),
					array(
						'exclusions' =>
							$public_holiday,
					)
				);
		}

		if ( ! empty( $tutor_leave ) ) {
			$conditions =
				self::add_condition(
					$conditions,
					'tutor_leave',
					self::SEVERITY_BLOCKING,
					__(
						'The tutor is marked as unavailable on this date.',
						'tutorhelper-bookings'
					),
					array(
						'exclusions' =>
							$tutor_leave,
					)
				);
		}

		if ( $has_conflict ) {
			$conditions =
				self::add_condition(
					$conditions,
					'booking_conflict',
					self::SEVERITY_BLOCKING,
					__(
						'The tutor already has another booking during this time.',
						'tutorhelper-bookings'
					)
				);
		}

		$condition_list =
			array_values(
				$conditions
			);

		$blocking = array_values(
			array_filter(
				$condition_list,
				static function (
					$condition
				) {
					return isset(
						$condition[
							'severity'
						]
					)
					&& self::SEVERITY_BLOCKING
						=== $condition[
							'severity'
						];
				}
			)
		);

		$warnings = array_values(
			array_filter(
				$condition_list,
				static function (
					$condition
				) {
					return isset(
						$condition[
							'severity'
						]
					)
					&& self::SEVERITY_WARNING
						=== $condition[
							'severity'
						];
				}
			)
		);

		return array(
			'tutor_user_id' =>
				$tutor_user_id,

			'booking_type_id' =>
				$booking_type_id,

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

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

			'utc_start' =>
				$utc_start,

			'utc_end' =>
				$utc_end,

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

			'has_public_availability' =>
				! empty( $availability ),

			'is_within_public_hours' =>
				! empty( $matching_time ),

			'is_booking_type_publicly_allowed' =>
				! empty( $matching_type ),

			'is_location_publicly_allowed' =>
				! empty( $matching_location ),

			'is_fully_publicly_bookable' =>
				! empty( $fully_matching )
				&& empty( $tutor_leave )
				&& empty( $public_holiday ),

			'is_outside_term_time' =>
				! empty( $outside_term ),

			'is_tutor_leave' =>
				! empty( $tutor_leave ),

			'is_public_holiday' =>
				! empty( $public_holiday ),

			'has_booking_conflict' =>
				$has_conflict,

			'can_tutor_save' =>
				empty( $blocking ),

			'conditions' =>
				$condition_list,

			'warnings' =>
				$warnings,

			'blocking_conditions' =>
				$blocking,

			'matching_availability' =>
				$fully_matching,

			'calendar_day' =>
				$day,
		);
	}
}
