<?php
/**
 * Booking recurrence planning service.
 *
 * @package TutorHelper_Bookings
 */

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

/**
 * Generates and evaluates recurring booking dates.
 *
 * This class creates a recurrence plan only. It does not insert
 * booking records into the database.
 */
final class TutorHelper_Booking_Recurrence {

	/**
	 * Include every generated date.
	 *
	 * Excluded dates remain in the plan and are marked so the tutor
	 * can see that the date falls outside normal term time.
	 *
	 * @var string
	 */
	const MODE_ALL_DATES = 'all_dates';

	/**
	 * Skip dates covered by active date exclusions.
	 *
	 * @var string
	 */
	const MODE_TERM_TIME_ONLY = 'term_time_only';

	/**
	 * Monday in ISO-8601 weekday numbering.
	 *
	 * @var int
	 */
	const MONDAY = 1;

	/**
	 * Sunday in ISO-8601 weekday numbering.
	 *
	 * @var int
	 */
	const SUNDAY = 7;

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

		if ( empty( $date ) ) {
			return new WP_Error(
				'tutorhelper_recurrence_date_missing',
				__(
					'A recurrence 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_recurrence_date_invalid',
				__(
					'The recurrence date is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $parsed;
	}


    	/**
	 * Validate an ISO weekday number.
	 *
	 * @param mixed $weekday Raw weekday.
	 *
	 * @return int|WP_Error
	 */
	private static function normalise_weekday(
		$weekday
	) {
		$weekday = is_numeric( $weekday )
			? (int) $weekday
			: 0;

		if (
			self::MONDAY > $weekday
			|| self::SUNDAY < $weekday
		) {
			return new WP_Error(
				'tutorhelper_recurrence_weekday_invalid',
				__(
					'The recurrence weekday must be between Monday and Sunday.',
					'tutorhelper-bookings'
				)
			);
		}

		return $weekday;
	}


    	/**
	 * Validate a recurrence mode.
	 *
	 * @param mixed $mode Raw mode.
	 *
	 * @return string|WP_Error
	 */
	private static function normalise_mode(
		$mode
	) {
		$mode = sanitize_key(
			(string) $mode
		);

		$allowed = array(
			self::MODE_ALL_DATES,
			self::MODE_TERM_TIME_ONLY,
		);

		if (
			! in_array(
				$mode,
				$allowed,
				true
			)
		) {
			return new WP_Error(
				'tutorhelper_recurrence_mode_invalid',
				__(
					'The recurrence mode is invalid.',
					'tutorhelper-bookings'
				)
			);
		}

		return $mode;
	}


    	/**
	 * Generate matching weekly dates inclusively.
	 *
	 * @param string $start_date Start date in Y-m-d format.
	 * @param string $end_date   End date in Y-m-d format.
	 * @param int    $weekday    ISO weekday from 1 to 7.
	 *
	 * @return array|WP_Error Array of Y-m-d dates.
	 */
	public static function generate_weekly_dates(
		$start_date,
		$end_date,
		$weekday
	) {
		$start = self::parse_date(
			$start_date
		);

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

		$end = self::parse_date(
			$end_date
		);

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

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

		$weekday = self::normalise_weekday(
			$weekday
		);

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

		$start_weekday = (int) $start->format(
			'N'
		);

		$days_until_first =
			(
				$weekday
				- $start_weekday
				+ 7
			) % 7;

		$current = $start->add(
			new DateInterval(
				'P'
				. $days_until_first
				. 'D'
			)
		);

		$dates = array();

		while ( $current <= $end ) {
			$dates[] = $current->format(
				'Y-m-d'
			);

			$current = $current->add(
				new DateInterval( 'P7D' )
			);
		}

		return $dates;
	}



    	/**
	 * Return exclusions covering a date.
	 *
	 * @param string $date       Date in Y-m-d format.
	 * @param array  $exclusions Exclusion records.
	 *
	 * @return array
	 */
	private static function get_date_exclusions(
		$date,
		$exclusions
	) {
		$matches = array();

		foreach ( $exclusions as $exclusion ) {
			if (
				! is_object( $exclusion )
				|| empty( $exclusion->start_date )
				|| empty( $exclusion->end_date )
			) {
				continue;
			}

			if (
				$exclusion->start_date <= $date
				&& $exclusion->end_date >= $date
			) {
				$matches[] = $exclusion;
			}
		}

		return $matches;
	}


    	/**
	 * Build a weekly recurrence plan.
	 *
	 * Required arguments:
	 * - tutor_user_id
	 * - start_date
	 * - end_date
	 * - weekday
	 * - mode
	 *
	 * @param array $args Recurrence arguments.
	 *
	 * @return array|WP_Error
	 */
	public static function plan_weekly(
		$args
	) {
		$args = wp_parse_args(
			is_array( $args ) ? $args : array(),
			array(
				'tutor_user_id' => 0,
				'start_date'    => '',
				'end_date'      => '',
				'weekday'       => 0,
				'mode'          =>
					self::MODE_ALL_DATES,
			)
		);

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

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

		$tutor = get_user_by(
			'id',
			$tutor_user_id
		);

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

		$mode = self::normalise_mode(
			$args['mode']
		);

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

		$dates = self::generate_weekly_dates(
			$args['start_date'],
			$args['end_date'],
			$args['weekday']
		);

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

		$exclusions =
			TutorHelper_Date_Exclusions::get_all(
				array(
					'tutor_user_id' =>
						$tutor_user_id,
					'active_only' =>
						true,
					'range_start' =>
						$args['start_date'],
					'range_end' =>
						$args['end_date'],
				)
			);

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

		$occurrences = array();

		$included_count = 0;
		$skipped_count  = 0;
		$excluded_count = 0;

		foreach ( $dates as $date ) {
			$matching_exclusions =
				self::get_date_exclusions(
					$date,
					$exclusions
				);

			$is_excluded = ! empty(
				$matching_exclusions
			);

			$should_skip =
				self::MODE_TERM_TIME_ONLY === $mode
				&& $is_excluded;

			$exclusion_data = array();

			foreach (
				$matching_exclusions
				as $exclusion
			) {
				$exclusion_data[] = array(
					'id' => absint(
						$exclusion->id
					),
					'title' =>
						sanitize_text_field(
							$exclusion->title
						),
					'type' =>
						sanitize_key(
							$exclusion
								->exclusion_type
						),
					'start_date' =>
						sanitize_text_field(
							$exclusion->start_date
						),
					'end_date' =>
						sanitize_text_field(
							$exclusion->end_date
						),
				);
			}

			if ( $is_excluded ) {
				$excluded_count++;
			}

			if ( $should_skip ) {
				$skipped_count++;
			} else {
				$included_count++;
			}

			$occurrences[] = array(
				'date' =>
					$date,
				'is_excluded' =>
					$is_excluded,
				'should_create' =>
					! $should_skip,
				'skip_reason' =>
					$should_skip
						? 'date_exclusion'
						: '',
				'exclusions' =>
					$exclusion_data,
			);
		}

		return array(
			'mode' =>
				$mode,
			'tutor_user_id' =>
				$tutor_user_id,
			'start_date' =>
				sanitize_text_field(
					$args['start_date']
				),
			'end_date' =>
				sanitize_text_field(
					$args['end_date']
				),
			'weekday' =>
				(int) $args['weekday'],
			'considered_count' =>
				count( $dates ),
			'included_count' =>
				$included_count,
			'excluded_count' =>
				$excluded_count,
			'skipped_count' =>
				$skipped_count,
			'occurrences' =>
				$occurrences,
		);
	}
}

