<?php
/**
 * Booking types data manager.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Handles booking type database operations.
 */
final class TutorHelper_Booking_Types {

	/**
	 * WordPress object-cache group for Booking Type reference data.
	 *
	 * @var string
	 */
	const CACHE_GROUP = 'tutorhelper_booking_types';

	/**
	 * Get the booking types table name.
	 *
	 * @return string
	 */
	public static function get_table_name() {
		return TutorHelper_Bookings_Database::get_table_name( 'types' );
	}

	/**
	 * Retrieve all booking types.
	 *
	 * @param array $args Query options.
	 *
	 * @return array
	 */
	public static function get_all( $args = array() ) {
		global $wpdb;

		$args = wp_parse_args(
			$args,
			array(
				'active_only' => false,
			)
		);

		$table_name = self::get_table_name();

		if ( empty( $table_name ) ) {
			return array();
		}

		$cache_key = ! empty( $args['active_only'] )
			? 'active'
			: 'all';

		$found  = false;
		$cached = wp_cache_get(
			$cache_key,
			self::CACHE_GROUP,
			false,
			$found
		);

		if ( $found && is_array( $cached ) ) {
			return $cached;
		}

		if ( ! empty( $args['active_only'] ) ) {
			$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- TutorHelper stores Booking Types in a custom table; the result is cached immediately below.
				$wpdb->prepare(
					'SELECT *
					FROM %i
					WHERE is_active = %d
					ORDER BY sort_order ASC, title ASC',
					$table_name,
					1
				)
			);
		} else {
			$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- TutorHelper stores Booking Types in a custom table; the result is cached immediately below.
				$wpdb->prepare(
					'SELECT *
					FROM %i
					ORDER BY sort_order ASC, title ASC',
					$table_name
				)
			);
		}

		if ( ! is_array( $results ) ) {
			$results = array();
		}

		wp_cache_set(
			$cache_key,
			$results,
			self::CACHE_GROUP
		);

		return $results;
	}

	/**
	 * Retrieve all active booking types.
	 *
	 * @return array
	 */
	public static function get_active() {
		return self::get_all(
			array(
				'active_only' => true,
			)
		);
	}

	/**
	 * Retrieve one booking type by ID.
	 *
	 * @param int $booking_type_id Booking type ID.
	 *
	 * @return object|null
	 */
	public static function get_by_id( $booking_type_id ) {
		global $wpdb;

		$booking_type_id = absint( $booking_type_id );

		if ( ! $booking_type_id ) {
			return null;
		}

		$table_name = self::get_table_name();

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

		$cache_key = 'id_' . $booking_type_id;
		$found     = false;
		$cached    = wp_cache_get(
			$cache_key,
			self::CACHE_GROUP,
			false,
			$found
		);

		if ( $found ) {
			return false === $cached
				? null
				: $cached;
		}

		$result = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- TutorHelper stores Booking Types in a custom table; the result is cached immediately below.
			$wpdb->prepare(
				'SELECT *
				FROM %i
				WHERE id = %d',
				$table_name,
				$booking_type_id
			)
		);

		wp_cache_set(
			$cache_key,
			null === $result ? false : $result,
			self::CACHE_GROUP
		);

		return $result;
	}
	
	/**
	 * Calculate a Booking Type price for an actual duration.
	 *
	 * The stored Booking Type price represents its standard duration.
	 * A different duration is charged proportionally. The result is
	 * returned in integer minor currency units.
	 *
	 * Examples:
	 * - 60 minutes at 5000 for 60 minutes returns 5000.
	 * - 60 minutes at 5000 for 90 minutes returns 7500.
	 *
	 * The actual duration should be derived from the booking start and
	 * end values before this method is called.
	 *
	 * @param int $booking_type_id Booking Type ID.
	 * @param int $duration_minutes Actual booking duration in minutes.
	 *
	 * @return int|WP_Error Price in minor currency units.
	 */
	public static function calculate_price(
		$booking_type_id,
		$duration_minutes
	) {
		$booking_type_id = absint(
			$booking_type_id
		);

		$duration_minutes = absint(
			$duration_minutes
		);

		if ( ! $booking_type_id ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_id',
				__(
					'The Booking Type ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		if ( ! $duration_minutes ) {
			return new WP_Error(
				'tutorhelper_booking_duration_invalid',
				__(
					'The booking duration must be greater than zero.',
					'tutorhelper-bookings'
				)
			);
		}

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

		if ( ! $booking_type ) {
			return new WP_Error(
				'tutorhelper_booking_type_not_found',
				__(
					'The Booking Type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

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

		$standard_price = absint(
			$booking_type->price_minor_units
		);

		if ( ! $standard_duration ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_duration',
				__(
					'The Booking Type duration is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		if ( 0 === $standard_price ) {

			/**
			 * Filters the calculated Booking Type price.
			 *
			 * Extensions may adjust the calculated price using
			 * additional booking context.
			 *
			 * @param int    $calculated_price Price in minor currency units.
			 * @param int    $booking_type_id  Booking Type ID.
			 * @param int    $duration_minutes Actual booking duration.
			 * @param object $booking_type     Booking Type record.
			 */
			return apply_filters(
				'tutorhelper_booking_type_calculated_price',
				0,
				$booking_type_id,
				$duration_minutes,
				$booking_type
			);
		}

		/*
		* Calculate entirely in integer minor units.
		*
		* Adding half of the divisor before intdiv() rounds positive
		* results to the nearest minor currency unit.
		*/
		$price_numerator =
			$standard_price * $duration_minutes;

		$calculated_price = intdiv(
			$price_numerator
				+ intdiv(
					$standard_duration,
					2
				),
			$standard_duration
		);

		/**
		 * Filters the calculated Booking Type price.
		 *
		 * Extensions may adjust the calculated price using
		 * additional booking context.
		 *
		 * @param int    $calculated_price Price in minor currency units.
		 * @param int    $booking_type_id  Booking Type ID.
		 * @param int    $duration_minutes Actual booking duration.
		 * @param object $booking_type     Booking Type record.
		 */
		return apply_filters(
			'tutorhelper_booking_type_calculated_price',
			$calculated_price,
			$booking_type_id,
			$duration_minutes,
			$booking_type
		);
	}


	/**
	 * Return the standard price for a Booking Type.
	 *
	 * @param int $booking_type_id Booking Type ID.
	 *
	 * @return int|WP_Error Price in minor currency units.
	 */
	public static function get_standard_price(
		$booking_type_id
	) {
		$booking_type_id = absint(
			$booking_type_id
		);

		if ( ! $booking_type_id ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_id',
				__(
					'The Booking Type ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

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

		if ( ! $booking_type ) {
			return new WP_Error(
				'tutorhelper_booking_type_not_found',
				__(
					'The Booking Type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

		return absint(
			$booking_type->price_minor_units
		);
	}


	/**
	 * Return the standard duration for a Booking Type.
	 *
	 * @param int $booking_type_id Booking Type ID.
	 *
	 * @return int|WP_Error Duration in minutes.
	 */
	public static function get_standard_duration(
		$booking_type_id
	) {
		$booking_type_id = absint(
			$booking_type_id
		);

		if ( ! $booking_type_id ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_id',
				__(
					'The Booking Type ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

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

		if ( ! $booking_type ) {
			return new WP_Error(
				'tutorhelper_booking_type_not_found',
				__(
					'The Booking Type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

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

		if ( ! $duration_minutes ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_duration',
				__(
					'The Booking Type duration is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $duration_minutes;
	}



	/**
	 * Check whether an actual duration differs from the standard duration.
	 *
	 * The actual duration should be derived from booking_start and
	 * booking_end before this method is called.
	 *
	 * A non-standard duration should only be accepted for a tutor-created
	 * manual booking.
	 *
	 * @param int $booking_type_id Booking Type ID.
	 * @param int $duration_minutes Actual booking duration in minutes.
	 *
	 * @return bool|WP_Error
	 */
	public static function is_duration_exception(
		$booking_type_id,
		$duration_minutes
	) {
		$duration_minutes = absint(
			$duration_minutes
		);

		if ( ! $duration_minutes ) {
			return new WP_Error(
				'tutorhelper_booking_duration_invalid',
				__(
					'The booking duration must be greater than zero.',
					'tutorhelper-bookings'
				)
			);
		}

		$standard_duration =
			self::get_standard_duration(
				$booking_type_id
			);

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

		return $duration_minutes
			!== $standard_duration;
	}






	/**
	 * Create a booking type.
	 *
	 * @param array $data Raw booking type data.
	 *
	 * @return int|WP_Error Booking type ID on success.
	 */
	public static function create( $data ) {
		global $wpdb;

		$clean_data = self::sanitize_data( $data );
		$validation = self::validate_data( $clean_data );

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

		if ( empty( $clean_data['sort_order'] ) ) {
			$clean_data['sort_order'] =
				self::get_next_sort_order();
		}

		$timestamp = current_time( 'mysql', true );

		$clean_data['created_at'] = $timestamp;
		$clean_data['updated_at'] = $timestamp;

		$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Booking Types are stored in a TutorHelper custom table; cache entries are invalidated after successful writes.
			self::get_table_name(),
			$clean_data,
			self::get_insert_formats()
		);

		if ( false === $result ) {
			return new WP_Error(
				'tutorhelper_booking_type_create_failed',
				$wpdb->last_error
					? $wpdb->last_error
					: __(
						'The booking type could not be created.',
						'tutorhelper-bookings'
					)
			);
		}

		$booking_type_id = absint( $wpdb->insert_id );

		self::clear_cache( array( $booking_type_id ) );

		/**
		 * Fires after a booking type is created.
		 *
		 * @param int   $booking_type_id Created booking type ID.
		 * @param array $clean_data      Sanitised booking type data.
		 */
		do_action(
			'tutorhelper_booking_type_created',
			$booking_type_id,
			$clean_data
		);

		return $booking_type_id;
	}


	/**
	 * Return the next available active Booking Type sort order.
	 *
	 * @return int
	 */
	private static function get_next_sort_order() {
		global $wpdb;

		$table_name = self::get_table_name();

		$highest_order = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Creating a Booking Type must use the current maximum sort order; a cached value could assign a stale position.
			$wpdb->prepare(
				'SELECT MAX(sort_order)
				FROM %i
				WHERE is_active = %d',
				$table_name,
				1
			)
		);

		return absint( $highest_order ) + 10;
	}

	/**
	 * Update an existing booking type.
	 *
	 * Missing values are taken from the existing record so that partial
	 * updates do not unintentionally reset other fields.
	 *
	 * @param int   $booking_type_id Booking type ID.
	 * @param array $data            New booking type data.
	 *
	 * @return bool|WP_Error
	 */
	public static function update( $booking_type_id, $data ) {
		global $wpdb;

		$booking_type_id = absint( $booking_type_id );

		if ( ! $booking_type_id ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_id',
				__(
					'The booking type ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		$existing = self::get_by_id( $booking_type_id );

		if ( ! $existing ) {
			return new WP_Error(
				'tutorhelper_booking_type_not_found',
				__(
					'The booking type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

		$existing_data = get_object_vars( $existing );

        $merged_data = array_merge(
            $existing_data,
            is_array( $data ) ? $data : array()
        );

		$clean_data = self::sanitize_data( $merged_data );
		$validation = self::validate_data( $clean_data );

		if ( is_wp_error( $validation ) ) {
			TutorHelper_Logger::warning(
				'Booking Type update was rejected.',
				array(
					'booking_type_id' => $booking_type_id,
					'error_code'      => $validation->get_error_code(),
				)
			);

			return $validation;
		}

		$clean_data['updated_at'] = current_time( 'mysql', true );

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Booking Types are stored in a TutorHelper custom table; cache entries are invalidated after successful writes.
			self::get_table_name(),
			$clean_data,
			array(
				'id' => $booking_type_id,
			),
			self::get_update_formats(),
			array(
				'%d',
			)
		);

		if ( false === $result ) {
			TutorHelper_Logger::error(
				'Booking Type update failed.',
				array(
					'booking_type_id' => $booking_type_id,
					'database_error'  => $wpdb->last_error,
				)
			);

			return new WP_Error(
				'tutorhelper_booking_type_update_failed',
				$wpdb->last_error
					? $wpdb->last_error
					: __(
						'The booking type could not be updated.',
						'tutorhelper-bookings'
					)
			);
		}

		self::clear_cache( array( $booking_type_id ) );

		/**
		 * Fires after a booking type is updated.
		 *
		 * @param int   $booking_type_id Updated booking type ID.
		 * @param array $clean_data      Sanitised booking type data.
		 */
		do_action(
			'tutorhelper_booking_type_updated',
			$booking_type_id,
			$clean_data
		);

		return true;
	}



	/**
	 * Update the display order of active Booking Types.
	 *
	 * IDs are stored in increments of ten so that future records can be
	 * inserted between existing positions if needed.
	 *
	 * @param array $ordered_ids Booking Type IDs in the required order.
	 *
	 * @return true|WP_Error
	 */
	public static function update_sort_order(
		$ordered_ids
	) {
		global $wpdb;

		if ( ! is_array( $ordered_ids ) ) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_order',
				__(
					'The Booking Type order is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		$ordered_ids = array_values(
			array_unique(
				array_filter(
					array_map(
						'absint',
						$ordered_ids
					)
				)
			)
		);

		if ( empty( $ordered_ids ) ) {
			return new WP_Error(
				'tutorhelper_booking_type_empty_order',
				__(
					'No Booking Types were supplied.',
					'tutorhelper-bookings'
				)
			);
		}

		$active_types = self::get_active();

		$active_ids = array_map(
			'absint',
			wp_list_pluck(
				$active_types,
				'id'
			)
		);

		$submitted_ids = $ordered_ids;

		sort( $active_ids );
		sort( $submitted_ids );

		if ( $active_ids !== $submitted_ids ) {
			return new WP_Error(
				'tutorhelper_booking_type_order_mismatch',
				__(
					'The Booking Type order does not match the active Booking Types.',
					'tutorhelper-bookings'
				)
			);
		}

		$table_name = self::get_table_name();
		$sort_order = 10;

		foreach ( $ordered_ids as $booking_type_id ) {
			$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Booking Type sort order is stored in a TutorHelper custom table; cache entries are invalidated after the reorder completes.
				$table_name,
				array(
					'sort_order' => $sort_order,
					'updated_at' => current_time(
						'mysql',
						true
					),
				),
				array(
					'id'        => $booking_type_id,
					'is_active' => 1,
				),
				array(
					'%d',
					'%s',
				),
				array(
					'%d',
					'%d',
				)
			);

			if ( false === $result ) {
				self::clear_cache( $ordered_ids );

				TutorHelper_Logger::error(
					'Booking Type sort order update failed.',
					array(
						'booking_type_id' =>
							$booking_type_id,
						'database_error' =>
							$wpdb->last_error,
					)
				);

				return new WP_Error(
					'tutorhelper_booking_type_order_failed',
					__(
						'The Booking Type order could not be saved.',
						'tutorhelper-bookings'
					)
				);
			}

			$sort_order += 10;
		}

		self::clear_cache( $ordered_ids );

		TutorHelper_Logger::success(
			'Booking Type sort order updated.',
			array(
				'booking_type_count' =>
					count( $ordered_ids ),
			)
		);

		do_action(
			'tutorhelper_booking_types_reordered',
			$ordered_ids
		);

		return true;
	}



	/**
	 * Archive a booking type.
	 *
	 * Existing bookings may still reference the booking type, so records
	 * are made inactive rather than permanently deleted.
	 *
	 * @param int $booking_type_id Booking type ID.
	 *
	 * @return bool|WP_Error
	 */
	public static function archive( $booking_type_id ) {
		return self::set_active_status( $booking_type_id, false );
	}

	/**
	 * Restore an archived booking type.
	 *
	 * @param int $booking_type_id Booking type ID.
	 *
	 * @return bool|WP_Error
	 */
	public static function restore( $booking_type_id ) {
		return self::set_active_status( $booking_type_id, true );
	}

	/**
	 * Change the active status of a booking type.
	 *
	 * @param int  $booking_type_id Booking type ID.
	 * @param bool $is_active       Whether the record should be active.
	 *
	 * @return bool|WP_Error
	 */
	private static function set_active_status( $booking_type_id, $is_active ) {
		global $wpdb;

		$booking_type_id = absint( $booking_type_id );

		if ( ! $booking_type_id ) {
			TutorHelper_Logger::error(
				'Booking Type status update failed.',
				array(
					'booking_type_id' => $booking_type_id,
					'is_active'       => $is_active,
					'database_error'  => $wpdb->last_error,
				)
			);
			return new WP_Error(
				'tutorhelper_booking_type_invalid_id',
				__(
					'The booking type ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		if ( ! self::get_by_id( $booking_type_id ) ) {

		TutorHelper_Logger::error(
			'Booking Type status update failed.',
			array(
				'booking_type_id' => $booking_type_id,
				'is_active'       => $is_active,
				'database_error'  => $wpdb->last_error,
			)
		);
			
			return new WP_Error(
				'tutorhelper_booking_type_not_found',
				__(
					'The booking type could not be found.',
					'tutorhelper-bookings'
				)
			);
		}

		$active_value = $is_active ? 1 : 0;

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Booking Type status is stored in a TutorHelper custom table; cache entries are invalidated after the successful write.
			self::get_table_name(),
			array(
				'is_active' => $active_value,
				'updated_at' => current_time( 'mysql', true ),
			),
			array(
				'id' => $booking_type_id,
			),
			array(
				'%d',
				'%s',
			),
			array(
				'%d',
			)
		);

		if ( false === $result ) {
			TutorHelper_Logger::error(
				'Booking Type status update failed.',
				array(
					'booking_type_id' => $booking_type_id,
					'is_active'       => $is_active,
					'database_error'  => $wpdb->last_error,
				)
			);

			return new WP_Error(
				'tutorhelper_booking_type_status_update_failed',
				$wpdb->last_error
					? $wpdb->last_error
					: __(
						'The booking type status could not be updated.',
						'tutorhelper-bookings'
					)
			);
		}

		self::clear_cache( array( $booking_type_id ) );

		/**
		 * Fires after a booking type's active status changes.
		 *
		 * @param int  $booking_type_id Booking type ID.
		 * @param bool $is_active       New active status.
		 */
		do_action(
			'tutorhelper_booking_type_status_changed',
			$booking_type_id,
			$is_active
		);

		return true;
	}

	/**
	 * Clear cached Booking Type reference data after a write.
	 *
	 * @param array $booking_type_ids Booking Type IDs whose individual caches changed.
	 *
	 * @return void
	 */
	private static function clear_cache( $booking_type_ids = array() ) {
		wp_cache_delete( 'all', self::CACHE_GROUP );
		wp_cache_delete( 'active', self::CACHE_GROUP );

		foreach ( (array) $booking_type_ids as $booking_type_id ) {
			$booking_type_id = absint( $booking_type_id );

			if ( $booking_type_id ) {
				wp_cache_delete(
					'id_' . $booking_type_id,
					self::CACHE_GROUP
				);
			}
		}
	}

	/**
	 * Sanitise booking type data.
	 *
	 * @param array $data Raw booking type data.
	 *
	 * @return array
	 */
	public static function sanitize_data( $data ) {
		$data = is_array( $data ) ? $data : array();

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

		$description = isset( $data['description'] )
			? sanitize_textarea_field( wp_unslash( $data['description'] ) )
			: '';

		$duration_minutes = isset( $data['duration_minutes'] )
			? absint( $data['duration_minutes'] )
			: 60;

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

		$location_types = self::sanitize_location_types(
			isset( $data['location_types'] )
				? $data['location_types']
				: 'online'
		);

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

		if (
			empty( $location_type )
			|| ! in_array( $location_type, $location_types, true )
		) {
			$location_type = $location_types[0];
		}

		$location_details = isset( $data['location_details'] )
			? sanitize_textarea_field( wp_unslash( $data['location_details'] ) )
			: '';

		$colour = isset( $data['colour'] )
			? sanitize_hex_color( wp_unslash( $data['colour'] ) )
			: '#FF6326';

		if ( empty( $colour ) ) {
			$colour = '#FF6326';
		}

		$visibility = isset( $data['online_booking_visibility'] )
			? sanitize_key( wp_unslash( $data['online_booking_visibility'] ) )
			: 'bookable';

		$allowed_visibility = array(
			'bookable',
			'private_url',
			'visible_not_bookable',
			'hidden',
		);

		if ( ! in_array( $visibility, $allowed_visibility, true ) ) {
			$visibility = 'bookable';
		}

		return array(
			'title'                     => $title,
			'description'               => $description,
			'duration_minutes'          => $duration_minutes,
			'price_minor_units'         => $price_minor_units,
			'location_type'             => $location_type,
			'location_types'            => implode( ',', $location_types ),
			'location_details'          => $location_details,
			'colour'                    => $colour,
			'online_booking_visibility' => $visibility,
			'notice_period_hours'       => isset( $data['notice_period_hours'] )
				? absint( $data['notice_period_hours'] )
				: 48,
			'cancellations_allowed'     => ! empty( $data['cancellations_allowed'] )
				? 1
				: 0,
			'cancellation_notice_hours' => isset( $data['cancellation_notice_hours'] )
				? absint( $data['cancellation_notice_hours'] )
				: 24,
			'is_active'                 => isset( $data['is_active'] )
				? min( 1, absint( $data['is_active'] ) )
				: 1,
			'sort_order'                => isset( $data['sort_order'] )
				? absint( $data['sort_order'] )
				: 0,
		);
	}

	/**
	 * Validate sanitised booking type data.
	 *
	 * @param array $data Sanitised booking type data.
	 *
	 * @return true|WP_Error
	 */
	public static function validate_data( $data ) {
		if ( empty( $data['title'] ) ) {
			return new WP_Error(
				'tutorhelper_booking_type_missing_title',
				__(
					'A booking type title is required.',
					'tutorhelper-bookings'
				)
			);
		}

		if (
			empty( $data['duration_minutes'] )
			|| 5 > $data['duration_minutes']
		) {
			return new WP_Error(
				'tutorhelper_booking_type_invalid_duration',
				__(
					'The booking duration must be at least five minutes.',
					'tutorhelper-bookings'
				)
			);
		}

		if ( 1440 < $data['duration_minutes'] ) {
			return new WP_Error(
				'tutorhelper_booking_type_duration_too_long',
				__(
					'The booking duration cannot be longer than 24 hours.',
					'tutorhelper-bookings'
				)
			);
		}

		return true;
	}

	/**
	 * Sanitise one or more location types.
	 *
	 * @param array|string $location_types Raw location types.
	 *
	 * @return array
	 */
	private static function sanitize_location_types( $location_types ) {
		if ( is_string( $location_types ) ) {
			$location_types = explode( ',', $location_types );
		}

		if ( ! is_array( $location_types ) ) {
			$location_types = array();
		}

		$allowed_types = array(
			'online',
			'phone',
			'in_person',
			'custom',
		);

		$clean_types = array();

		foreach ( $location_types as $location_type ) {
			$location_type = sanitize_key( wp_unslash( $location_type ) );

			if (
				in_array( $location_type, $allowed_types, true )
				&& ! in_array( $location_type, $clean_types, true )
			) {
				$clean_types[] = $location_type;
			}
		}

		if ( empty( $clean_types ) ) {
			$clean_types[] = 'online';
		}

		return $clean_types;
	}

	/**
	 * Database formats used when creating a booking type.
	 *
	 * @return array
	 */
	private static function get_insert_formats() {
		return array(
			'%s', // title.
			'%s', // description.
			'%d', // duration_minutes.
			'%d', // price_minor_units.
			'%s', // location_type.
			'%s', // location_types.
			'%s', // location_details.
			'%s', // colour.
			'%s', // online_booking_visibility.
			'%d', // notice_period_hours.
			'%d', // cancellations_allowed.
			'%d', // cancellation_notice_hours.
			'%d', // is_active.
			'%d', // sort_order.
			'%s', // created_at.
			'%s', // updated_at.
		);
	}

	/**
	 * Database formats used when updating a booking type.
	 *
	 * @return array
	 */
	private static function get_update_formats() {
		return array(
			'%s', // title.
			'%s', // description.
			'%d', // duration_minutes.
			'%d', // price_minor_units.
			'%s', // location_type.
			'%s', // location_types.
			'%s', // location_details.
			'%s', // colour.
			'%s', // online_booking_visibility.
			'%d', // notice_period_hours.
			'%d', // cancellations_allowed.
			'%d', // cancellation_notice_hours.
			'%d', // is_active.
			'%d', // sort_order.
			'%s', // updated_at.
		);
	}
}
