<?php
/**
 * Shared date-exclusion data service.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Creates, updates and queries tutor date exclusions.
 */
final class TutorHelper_Date_Exclusions {

	/**
	 * Dates outside the tutor's normal teaching term.
	 *
	 * @var string
	 */
	const TYPE_OUTSIDE_TERM_TIME =
		'outside_term_time';

	/**
	 * Named holiday period.
	 *
	 * @var string
	 */
	const TYPE_HOLIDAY = 'holiday';

	/**
	 * Other unavailable date range.
	 *
	 * @var string
	 */
	const TYPE_UNAVAILABLE = 'unavailable';

	/**
	 * Return the exclusion table name.
	 *
	 * @return string
	 */
	public static function get_table_name() {
		return TutorHelper_Bookings_Database::
			get_table_name(
				'date_exclusions'
			);
	}

    	/**
	 * Return supported exclusion types.
	 *
	 * @return array
	 */
	public static function get_types() {
		return array(
			self::TYPE_OUTSIDE_TERM_TIME =>
				__(
					'Outside term time',
					'tutorhelper-bookings'
				),

			self::TYPE_HOLIDAY =>
				__(
					'Holiday',
					'tutorhelper-bookings'
				),

			self::TYPE_UNAVAILABLE =>
				__(
					'Unavailable',
					'tutorhelper-bookings'
				),
		);
	}

    	/**
	 * Validate and normalise a date.
	 *
	 * @param mixed $date Raw date value.
	 *
	 * @return string|WP_Error Date in Y-m-d format.
	 */
	private static function normalise_date(
		$date
	) {
		$date = sanitize_text_field(
			(string) $date
		);

		if ( empty( $date ) ) {
			return new WP_Error(
				'tutorhelper_exclusion_date_missing',
				__(
					'An exclusion 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_exclusion_date_invalid',
				__(
					'The exclusion date is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $date;
	}

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

		if (
			! array_key_exists(
				$type,
				self::get_types()
			)
		) {
			return new WP_Error(
				'tutorhelper_exclusion_type_invalid',
				__(
					'The exclusion type is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $type;
	}

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

		return array(
			'tutor_user_id' => isset(
				$data['tutor_user_id']
			)
				? absint(
					$data['tutor_user_id']
				)
				: 0,

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

			'start_date' => isset(
				$data['start_date']
			)
				? sanitize_text_field(
					$data['start_date']
				)
				: '',

			'end_date' => isset(
				$data['end_date']
			)
				? sanitize_text_field(
					$data['end_date']
				)
				: '',

			'exclusion_type' => isset(
				$data['exclusion_type']
			)
				? sanitize_key(
					$data['exclusion_type']
				)
				: self::TYPE_OUTSIDE_TERM_TIME,

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

			'is_active' => isset(
				$data['is_active']
			)
				? (
					absint( $data['is_active'] )
						? 1
						: 0
				)
				: 1,
		);
	}

    	/**
	 * Validate sanitised exclusion data.
	 *
	 * @param array $data Sanitised data.
	 *
	 * @return array|WP_Error Validated data.
	 */
	private static function validate_data(
		$data
	) {
		if ( empty( $data['tutor_user_id'] ) ) {
			return new WP_Error(
				'tutorhelper_exclusion_tutor_missing',
				__(
					'A tutor is required.',
					'tutorhelper-bookings'
				)
			);
		}

		$tutor = get_user_by(
			'id',
			$data['tutor_user_id']
		);

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

		if ( empty( $data['title'] ) ) {
			return new WP_Error(
				'tutorhelper_exclusion_title_missing',
				__(
					'An exclusion title is required.',
					'tutorhelper-bookings'
				)
			);
		}

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

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

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

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

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

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

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

		$data['start_date'] =
			$start_date;

		$data['end_date'] =
			$end_date;

		$data['exclusion_type'] =
			$exclusion_type;

		return $data;
	}


    	/**
	 * Retrieve one exclusion by ID.
	 *
	 * @param int $exclusion_id Exclusion ID.
	 *
	 * @return object|null
	 */
	public static function get_by_id(
		$exclusion_id
	) {
		global $wpdb;

		$exclusion_id = absint(
			$exclusion_id
		);

		if ( ! $exclusion_id ) {
			return null;
		}

		$table_name = self::get_table_name();

		return $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Date Exclusions affect live availability and must reflect newly created or changed exclusions immediately.
			$wpdb->prepare(
				'SELECT *
				FROM %i
				WHERE id = %d',
				$table_name,
				$exclusion_id
			)
		);
	}

    	/**
	 * Retrieve exclusion ranges.
	 *
	 * Supported arguments:
	 * - tutor_user_id
	 * - exclusion_type
	 * - active_only
	 * - range_start
	 * - range_end
	 *
	 * When range dates are supplied, any exclusion overlapping that
	 * requested period is returned.
	 *
	 * @param array $args Query arguments.
	 *
	 * @return array|WP_Error
	 */
	public static function get_all(
		$args = array()
	) {
		global $wpdb;

		$args = wp_parse_args(
			is_array( $args ) ? $args : array(),
			array(
				'tutor_user_id' =>
					0,
				'exclusion_type' =>
					'',
				'active_only' =>
					true,
				'range_start' =>
					'',
				'range_end' =>
					'',
			)
		);

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

		$type = '';

		if ( ! empty( $args['exclusion_type'] ) ) {
			$type = self::normalise_type(
				$args['exclusion_type']
			);

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

		$active_only = ! empty(
			$args['active_only']
		)
			? 1
			: 0;

		$range_start = '';
		$range_end   = '';

		if ( ! empty( $args['range_start'] ) ) {
			$range_start =
				self::normalise_date(
					$args['range_start']
				);

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

		if ( ! empty( $args['range_end'] ) ) {
			$range_end =
				self::normalise_date(
					$args['range_end']
				);

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

		if (
			! empty( $range_start )
			&& ! empty( $range_end )
			&& $range_end < $range_start
		) {
			return new WP_Error(
				'tutorhelper_exclusion_query_end_before_start',
				__(
					'The requested range end date must not be before its start date.',
					'tutorhelper-bookings'
				)
			);
		}

		if ( ! empty( $range_start ) ) {
			$where[] = 'end_date >= %s';
			$values[] = $range_start;
		}

		if ( ! empty( $range_end ) ) {
			$where[] = 'start_date <= %s';
			$values[] = $range_end;
		}

		$table_name = self::get_table_name();

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

		$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Date Exclusions affect live availability and must reflect newly created or changed exclusions immediately.
			$wpdb->prepare(
				'SELECT *
				FROM %i
				WHERE (
					%d = 0
					OR tutor_user_id = %d
				)
				AND (
					%s = %s
					OR exclusion_type = %s
				)
				AND (
					%d = 0
					OR is_active = %d
				)
				AND (
					%s = %s
					OR end_date >= %s
				)
				AND (
					%s = %s
					OR start_date <= %s
				)
				ORDER BY
					start_date ASC,
					end_date ASC,
					title ASC',
				$table_name,
				$tutor_user_id,
				$tutor_user_id,
				$type,
				'',
				$type,
				$active_only,
				1,
				$range_start,
				'',
				$range_start,
				$range_end,
				'',
				$range_end
			)
		);

		return is_array( $results )
			? $results
			: array();
	}


    	/**
	 * Check whether a date is excluded.
	 *
	 * @param string $date             Date in Y-m-d format.
	 * @param int    $tutor_user_id    Tutor user ID.
	 * @param string $exclusion_type   Optional exclusion type.
	 *
	 * @return bool|WP_Error
	 */
	public static function is_excluded(
		$date,
		$tutor_user_id,
		$exclusion_type = ''
	) {
		global $wpdb;

		$date = self::normalise_date(
			$date
		);

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

		$tutor_user_id = absint(
			$tutor_user_id
		);

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

		

		if ( ! empty( $exclusion_type ) ) {
			$exclusion_type =
				self::normalise_type(
					$exclusion_type
				);

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

		$table_name = self::get_table_name();

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

		$count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Date Exclusions affect live availability and must reflect newly created or changed exclusions immediately.
			$wpdb->prepare(
				'SELECT COUNT(*)
				FROM %i
				WHERE tutor_user_id = %d
				AND is_active = %d
				AND start_date <= %s
				AND end_date >= %s
				AND (
					%s = %s
					OR exclusion_type = %s
				)',
				$table_name,
				$tutor_user_id,
				1,
				$date,
				$date,
				$exclusion_type,
				'',
				$exclusion_type
			)
		);

		return 0 < absint( $count );
	}

    	/**
	 * Check whether a range overlaps an existing exclusion.
	 *
	 * Overlaps are allowed by the service. This method lets an interface
	 * warn the tutor without preventing legitimate layered exclusions.
	 *
	 * @param int    $tutor_user_id  Tutor user ID.
	 * @param string $start_date     Proposed start date.
	 * @param string $end_date       Proposed end date.
	 * @param string $exclusion_type Optional type filter.
	 * @param int    $exclude_id     Exclusion ID to ignore.
	 *
	 * @return bool|WP_Error
	 */
	public static function has_overlap(
		$tutor_user_id,
		$start_date,
		$end_date,
		$exclusion_type = '',
		$exclude_id = 0
	) {
		global $wpdb;

		$tutor_user_id = absint(
			$tutor_user_id
		);

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

		$start_date =
			self::normalise_date(
				$start_date
			);

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

		$end_date =
			self::normalise_date(
				$end_date
			);

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

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

		$exclude_id = absint(
			$exclude_id
		);


		$table_name = self::get_table_name();

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

		$count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Date Exclusions affect live availability and must reflect newly created or changed exclusions immediately.
			$wpdb->prepare(
				'SELECT COUNT(*)
				FROM %i
				WHERE tutor_user_id = %d
				AND is_active = %d
				AND start_date <= %s
				AND end_date >= %s
				AND (
					%s = %s
					OR exclusion_type = %s
				)
				AND (
					%d = 0
					OR id <> %d
				)',
				$table_name,
				$tutor_user_id,
				1,
				$end_date,
				$start_date,
				$exclusion_type,
				'',
				$exclusion_type,
				$exclude_id,
				$exclude_id
			)
		);

		return 0 < absint( $count );
	}




    	/**
	 * Create an exclusion range.
	 *
	 * @param array $data Raw exclusion data.
	 *
	 * @return int|WP_Error Exclusion ID.
	 */
	public static function create(
		$data
	) {
		global $wpdb;

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

		$clean_data =
			self::validate_data(
				$clean_data
			);

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

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

		$clean_data['created_at'] =
			$timestamp;

		$clean_data['updated_at'] =
			$timestamp;

		$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Date Exclusions are stored in a TutorHelper custom table; caching does not apply to the database write.
			self::get_table_name(),
			$clean_data,
			self::get_insert_formats()
		);

		if ( false === $result ) {
			TutorHelper_Logger::error(
				'Date exclusion creation failed.',
				array(
					'tutor_user_id' =>
						$clean_data[
							'tutor_user_id'
						],
					'exclusion_type' =>
						$clean_data[
							'exclusion_type'
						],
				)
			);

			return new WP_Error(
				'tutorhelper_exclusion_create_failed',
				$wpdb->last_error
					? $wpdb->last_error
					: __(
						'The date exclusion could not be created.',
						'tutorhelper-bookings'
					)
			);
		}

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

		TutorHelper_Logger::success(
			'Date exclusion created.',
			array(
				'exclusion_id' =>
					$exclusion_id,
				'tutor_user_id' =>
					$clean_data[
						'tutor_user_id'
					],
				'exclusion_type' =>
					$clean_data[
						'exclusion_type'
					],
				'start_date' =>
					$clean_data[
						'start_date'
					],
				'end_date' =>
					$clean_data[
						'end_date'
					],
			)
		);

		do_action(
			'tutorhelper_date_exclusion_created',
			$exclusion_id,
			$clean_data
		);

		return $exclusion_id;
	}


    	/**
	 * Update an exclusion range.
	 *
	 * Missing fields retain their current values.
	 *
	 * @param int   $exclusion_id Exclusion ID.
	 * @param array $data         Updated data.
	 *
	 * @return bool|WP_Error
	 */
	public static function update(
		$exclusion_id,
		$data
	) {
		global $wpdb;

		$exclusion_id = absint(
			$exclusion_id
		);

		if ( ! $exclusion_id ) {
			return new WP_Error(
				'tutorhelper_exclusion_invalid_id',
				__(
					'The exclusion ID is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

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

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

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

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

		$clean_data =
			self::validate_data(
				$clean_data
			);

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

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

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Date Exclusions affect live availability and must reflect newly created or changed exclusions immediately.
			self::get_table_name(),
			$clean_data,
			array(
				'id' => $exclusion_id,
			),
			self::get_update_formats(),
			array( '%d' )
		);

		if ( false === $result ) {
			return new WP_Error(
				'tutorhelper_exclusion_update_failed',
				$wpdb->last_error
					? $wpdb->last_error
					: __(
						'The date exclusion could not be updated.',
						'tutorhelper-bookings'
					)
			);
		}

		TutorHelper_Logger::success(
			'Date exclusion updated.',
			array(
				'exclusion_id' =>
					$exclusion_id,
				'tutor_user_id' =>
					$clean_data[
						'tutor_user_id'
					],
				'exclusion_type' =>
					$clean_data[
						'exclusion_type'
					],
			)
		);

		do_action(
			'tutorhelper_date_exclusion_updated',
			$exclusion_id,
			$clean_data
		);

		return true;
	}


    	/**
	 * Archive an exclusion.
	 *
	 * @param int $exclusion_id Exclusion ID.
	 *
	 * @return bool|WP_Error
	 */
	public static function archive(
		$exclusion_id
	) {
		return self::update(
			$exclusion_id,
			array(
				'is_active' => 0,
			)
		);
	}

	/**
	 * Restore an archived exclusion.
	 *
	 * @param int $exclusion_id Exclusion ID.
	 *
	 * @return bool|WP_Error
	 */
	public static function restore(
		$exclusion_id
	) {
		return self::update(
			$exclusion_id,
			array(
				'is_active' => 1,
			)
		);
	}


    	/**
	 * Return insert formats.
	 *
	 * @return array
	 */
	private static function get_insert_formats() {
		return array(
			'%d', // tutor_user_id.
			'%s', // title.
			'%s', // start_date.
			'%s', // end_date.
			'%s', // exclusion_type.
			'%s', // notes.
			'%d', // is_active.
			'%s', // created_at.
			'%s', // updated_at.
		);
	}

	/**
	 * Return update formats.
	 *
	 * @return array
	 */
	private static function get_update_formats() {
		return array(
			'%d', // tutor_user_id.
			'%s', // title.
			'%s', // start_date.
			'%s', // end_date.
			'%s', // exclusion_type.
			'%s', // notes.
			'%d', // is_active.
			'%s', // updated_at.
		);
	}
}
