<?php
/**
 * Availability-planner range save service.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Saves Working day and exclusion ranges selected in the planner.
 */
final class TutorHelper_Availability_Range_Save {

	/**
	 * Calendar value representing a Working day.
	 *
	 * @var string
	 */
	const TYPE_WORKING_DAY = 'general_availability';

	/**
	 * Calendar value representing outside term time.
	 *
	 * @var string
	 */
	const TYPE_OUTSIDE_TERM_TIME = 'outside_term_time';

	/**
	 * Calendar value representing tutor leave.
	 *
	 * @var string
	 */
	const TYPE_TUTOR_LEAVE = 'tutor_leave';

	/**
	 * Calendar value representing a public holiday.
	 *
	 * @var string
	 */
	const TYPE_PUBLIC_HOLIDAY = 'public_holiday';


    	/**
	 * Return supported planner range types.
	 *
	 * @return array
	 */
	public static function get_types() {
		return array(
			self::TYPE_WORKING_DAY,
			self::TYPE_OUTSIDE_TERM_TIME,
			self::TYPE_TUTOR_LEAVE,
			self::TYPE_PUBLIC_HOLIDAY,
		);
	}


    	/**
	 * Validate and normalise a date.
	 *
	 * @param mixed $date Raw date.
	 *
	 * @return string|WP_Error
	 */
	private static function normalise_date(
		$date
	) {
		$date = sanitize_text_field(
			(string) $date
		);

		if ( empty( $date ) ) {
			return new WP_Error(
				'tutorhelper_range_date_missing',
				__(
					'A selected range date is missing.',
					'tutorhelper-bookings'
				)
			);
		}

		$parsed = DateTimeImmutable::createFromFormat(
			'!Y-m-d',
			$date,
			new DateTimeZone( 'UTC' )
		);

		$errors = DateTimeImmutable::getLastErrors();

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

		return $date;
	}


    	/**
	 * Validate a 24-hour time.
	 *
	 * @param mixed $time Raw time.
	 *
	 * @return string|WP_Error
	 */
	private static function normalise_time(
		$time
	) {
		$time = sanitize_text_field(
			(string) $time
		);

		if (
			! preg_match(
				'/^(?:[01]\d|2[0-3]):[0-5]\d$/',
				$time
			)
		) {
			return new WP_Error(
				'tutorhelper_range_time_invalid',
				__(
					'Enter a valid start and end time.',
					'tutorhelper-bookings'
				)
			);
		}

		return $time;
	}


    	/**
	 * Validate a planner range type.
	 *
	 * @param mixed $type Raw type.
	 *
	 * @return string|WP_Error
	 */
	private static function normalise_type(
		$type
	) {
		$type = sanitize_key(
			(string) $type
		);

		if (
			! in_array(
				$type,
				self::get_types(),
				true
			)
		) {
			return new WP_Error(
				'tutorhelper_range_type_invalid',
				__(
					'The selected range type is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $type;
	}


    	/**
	 * Sanitise an array of keys.
	 *
	 * @param mixed $values Raw values.
	 *
	 * @return array
	 */
	private static function sanitise_keys(
		$values
	) {
		if ( ! is_array( $values ) ) {
			return array();
		}

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

	/**
	 * Sanitise an array of positive IDs.
	 *
	 * @param mixed $values Raw values.
	 *
	 * @return array
	 */
	private static function sanitise_ids(
		$values
	) {
		if ( ! is_array( $values ) ) {
			return array();
		}

		return array_values(
			array_unique(
				array_filter(
					array_map(
						'absint',
						$values
					)
				)
			)
		);
	}


    	/**
	 * Validate common range data.
	 *
	 * @param array $data          Submitted data.
	 * @param int   $tutor_user_id Tutor user ID.
	 *
	 * @return array|WP_Error
	 */
	private static function validate_common_data(
		$data,
		$tutor_user_id
	) {
		$tutor_user_id = absint(
			$tutor_user_id
		);

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

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

		$type = self::normalise_type(
			isset( $data['range_type'] )
				? $data['range_type']
				: ''
		);

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

		$title = isset( $data['title'] )
			? sanitize_text_field(
				$data['title']
			)
			: '';

		if ( empty( $title ) ) {
			return new WP_Error(
				'tutorhelper_range_title_missing',
				__(
					'Enter a title for the selected range.',
					'tutorhelper-bookings'
				)
			);
		}

		$start_date = self::normalise_date(
			isset( $data['start_date'] )
				? $data['start_date']
				: ''
		);

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

		$end_date = self::normalise_date(
			isset( $data['end_date'] )
				? $data['end_date']
				: ''
		);

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

		if ( $end_date < $start_date ) {
			return new WP_Error(
				'tutorhelper_range_end_before_start',
				__(
					'The selected range end date must not be before its start date.',
					'tutorhelper-bookings'
				)
			);
		}

		return array(
			'tutor_user_id' =>
				$tutor_user_id,

			'range_type' =>
				$type,

			'title' =>
				$title,

			'start_date' =>
				$start_date,

			'end_date' =>
				$end_date,

			'notes' => isset( $data['notes'] )
				? sanitize_textarea_field(
					$data['notes']
				)
				: '',
		);
	}


    	/**
	 * Validate Working day-specific data.
	 *
	 * @param array $data   Submitted data.
	 * @param array $common Validated common data.
	 *
	 * @return array|WP_Error
	 */
	private static function validate_working_day_data(
		$data,
		$common
	) {
		$weekdays = self::sanitise_keys(
			isset( $data['weekdays'] )
				? $data['weekdays']
				: array()
		);

		$allowed_weekdays =
			TutorHelper_Booking_Availability::
				ALLOWED_WEEKDAYS;

		foreach ( $weekdays as $weekday ) {
			if (
				! in_array(
					$weekday,
					$allowed_weekdays,
					true
				)
			) {
				return new WP_Error(
					'tutorhelper_range_weekday_invalid',
					__(
						'The selected Working day contains an invalid weekday.',
						'tutorhelper-bookings'
					)
				);
			}
		}

		if ( empty( $weekdays ) ) {
			return new WP_Error(
				'tutorhelper_range_weekdays_missing',
				__(
					'Select at least one weekday.',
					'tutorhelper-bookings'
				)
			);
		}

		$start_time = self::normalise_time(
			isset( $data['start_time'] )
				? $data['start_time']
				: ''
		);

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

		$end_time = self::normalise_time(
			isset( $data['end_time'] )
				? $data['end_time']
				: ''
		);

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

		if ( $end_time <= $start_time ) {
			return new WP_Error(
				'tutorhelper_range_end_time_invalid',
				__(
					'The Working day end time must be after its start time.',
					'tutorhelper-bookings'
				)
			);
		}

		$location_types = self::sanitise_keys(
			isset( $data['location_types'] )
				? $data['location_types']
				: array()
		);

		foreach (
			$location_types as $location_type
		) {
			if (
				! in_array(
					$location_type,
					TutorHelper_Booking_Availability::
						ALLOWED_LOCATIONS,
					true
				)
			) {
				return new WP_Error(
					'tutorhelper_range_location_invalid',
					__(
						'The selected Working day contains an invalid location.',
						'tutorhelper-bookings'
					)
				);
			}
		}

		if ( empty( $location_types ) ) {
			return new WP_Error(
				'tutorhelper_range_locations_missing',
				__(
					'Select at least one available location.',
					'tutorhelper-bookings'
				)
			);
		}

		$primary_location = isset(
			$data['location_type']
		)
			? sanitize_key(
				$data['location_type']
			)
			: '';

		if (
			! in_array(
				$primary_location,
				$location_types,
				true
			)
		) {
			return new WP_Error(
				'tutorhelper_range_primary_location_invalid',
				__(
					'The primary location must be one of the selected available locations.',
					'tutorhelper-bookings'
				)
			);
		}

		return array_merge(
			$common,
			array(
				'weekdays' =>
					$weekdays,

				'start_time' =>
					$start_time,

				'end_time' =>
					$end_time,

				'timezone' =>
					wp_timezone_string(),

				'location_type' =>
					$primary_location,

				'location_types' =>
					$location_types,

				'location_details' => isset(
					$data['location_details']
				)
					? sanitize_textarea_field(
						$data[
							'location_details'
						]
					)
					: '',

				'allowed_booking_type_ids' =>
					self::sanitise_ids(
						isset(
							$data[
								'allowed_booking_type_ids'
							]
						)
							? $data[
								'allowed_booking_type_ids'
							]
							: array()
					),

				'is_active' => 1,
			)
		);
	}


    	/**
	 * Save a Working day range.
	 *
	 * @param array $data Validated data.
	 *
	 * @return array|WP_Error
	 */
	private static function save_working_day(
		$data
	) {
		unset(
			$data['range_type'],
			$data['notes']
		);

		$result =
			TutorHelper_Booking_Availability::create(
				$data
			);

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

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

			'record_id' =>
				absint( $result ),

			'range_type' =>
				self::TYPE_WORKING_DAY,
		);
	}


	/**
	 * Update an existing Working day range.
	 *
	 * @param int   $record_id Availability record ID.
	 * @param array $data      Validated Working day data.
	 *
	 * @return array|WP_Error
	 */
	private static function update_working_day(
		$record_id,
		$data
	) {

		$record_id =
			absint(
				$record_id
			);

		$existing =
			TutorHelper_Booking_Availability::
				get_by_id(
					$record_id
				);

		if (
			! $existing
			|| absint(
				$existing->tutor_user_id
			)
			!== absint(
				$data['tutor_user_id']
			)
		) {
			return new WP_Error(
				'tutorhelper_range_availability_not_found',
				__(
					'The Working day range could not be found.',
					'tutorhelper-bookings'
				)
			);
		}


		unset(
			$data['range_type'],
			$data['notes']
		);


		$result =
			TutorHelper_Booking_Availability::
				update(
					$record_id,
					$data
				);

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


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

			'record_id' =>
				$record_id,

			'range_type' =>
				self::TYPE_WORKING_DAY,

			'updated' =>
				true,
		);
	}

	/**
	 * Update an existing whole-date exclusion range.
	 *
	 * @param int   $record_id Exclusion record ID.
	 * @param array $data      Validated range data.
	 *
	 * @return array|WP_Error
	 */
	private static function update_exclusion(
		$record_id,
		$data
	) {

		$record_id =
			absint(
				$record_id
			);

		$existing =
			TutorHelper_Date_Exclusions::
				get_by_id(
					$record_id
				);

		if (
			! $existing
			|| absint(
				$existing->tutor_user_id
			)
			!== absint(
				$data['tutor_user_id']
			)
		) {
			return new WP_Error(
				'tutorhelper_range_exclusion_not_found',
				__(
					'The date range could not be found.',
					'tutorhelper-bookings'
				)
			);
		}


		$exclusion_type =
			self::get_exclusion_type(
				$data['range_type']
			);

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


		$result =
			TutorHelper_Date_Exclusions::
				update(
					$record_id,
					array(
						'tutor_user_id' =>
							$data[
								'tutor_user_id'
							],

						'title' =>
							$data['title'],

						'start_date' =>
							$data[
								'start_date'
							],

						'end_date' =>
							$data[
								'end_date'
							],

						'exclusion_type' =>
							$exclusion_type,

						'notes' =>
							$data['notes'],

						'is_active' =>
							1,
					)
				);

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


		return array(
			'record_type' =>
				'date_exclusion',

			'record_id' =>
				$record_id,

			'range_type' =>
				$data['range_type'],

			'exclusion_type' =>
				$exclusion_type,

			'updated' =>
				true,
		);
	}


    	/**
	 * Map a planner range type to a stored exclusion type.
	 *
	 * @param string $range_type Planner type.
	 *
	 * @return string|WP_Error
	 */
	private static function get_exclusion_type(
		$range_type
	) {
		switch ( $range_type ) {
			case self::TYPE_OUTSIDE_TERM_TIME:
				return TutorHelper_Date_Exclusions::
					TYPE_OUTSIDE_TERM_TIME;

			case self::TYPE_TUTOR_LEAVE:
				return TutorHelper_Date_Exclusions::
					TYPE_UNAVAILABLE;

			case self::TYPE_PUBLIC_HOLIDAY:
				return TutorHelper_Date_Exclusions::
					TYPE_HOLIDAY;
		}

		return new WP_Error(
			'tutorhelper_range_exclusion_type_invalid',
			__(
				'The selected exclusion type is invalid.',
				'tutorhelper-bookings'
			)
		);
	}


    	/**
	 * Save a whole-date exclusion range.
	 *
	 * @param array $data Validated common data.
	 *
	 * @return array|WP_Error
	 */
	private static function save_exclusion(
		$data
	) {
		$exclusion_type =
			self::get_exclusion_type(
				$data['range_type']
			);

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

		$result =
			TutorHelper_Date_Exclusions::create(
				array(
					'tutor_user_id' =>
						$data['tutor_user_id'],

					'title' =>
						$data['title'],

					'start_date' =>
						$data['start_date'],

					'end_date' =>
						$data['end_date'],

					'exclusion_type' =>
						$exclusion_type,

					'notes' =>
						$data['notes'],

					'is_active' =>
						1,
				)
			);

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

		return array(
			'record_type' =>
				'date_exclusion',

			'record_id' =>
				absint( $result ),

			'range_type' =>
				$data['range_type'],

			'exclusion_type' =>
				$exclusion_type,
		);
	}


    	/**
	 * Save a planner range.
	 *
	 * @param array $data          Submitted data.
	 * @param int   $tutor_user_id Tutor user ID.
	 *
	 * @return array|WP_Error
	 */
	public static function save(
		$data,
		$tutor_user_id
	) {
		$data = is_array( $data )
			? $data
			: array();

		$range_mode =
			isset(
				$data['range_mode']
			)
				? sanitize_key(
					$data['range_mode']
				)
				: 'create';

		$record_type =
			isset(
				$data['record_type']
			)
				? sanitize_key(
					$data['record_type']
				)
				: '';

		$record_id =
			isset(
				$data['record_id']
			)
				? absint(
					$data['record_id']
				)
				: 0;

		$is_edit =
			'edit'
			=== $range_mode;

		$common =
			self::validate_common_data(
				$data,
				$tutor_user_id
			);

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

		if (
			self::TYPE_WORKING_DAY
				=== $common['range_type']
		) {
			$working_day =
				self::validate_working_day_data(
					$data,
					$common
				);

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

			if ( $is_edit ) {

				if (
					'availability'
					!== $record_type
					|| ! $record_id
				) {
					return new WP_Error(
						'tutorhelper_range_edit_record_invalid',
						__(
							'The Working day record selected for editing is invalid.',
							'tutorhelper-bookings'
						)
					);
				}

				return self::update_working_day(
					$record_id,
					$working_day
				);
			}


			return self::save_working_day(
				$working_day
			);
		}

		if ( $is_edit ) {

			if (
				'date_exclusion'
				!== $record_type
				|| ! $record_id
			) {
				return new WP_Error(
					'tutorhelper_range_edit_record_invalid',
					__(
						'The date range selected for editing is invalid.',
						'tutorhelper-bookings'
					)
				);
			}

			return self::update_exclusion(
				$record_id,
				$common
			);
		}


		return self::save_exclusion(
			$common
		);
	}
}
