<?php

namespace JET_APB;

use WP_Error;
use JET_APB\Resources\Appointment_Model;

class Appointment_Eligibility_Validator {

	/**
	 * @var WP_Error|null
	 */
	protected $error = null;

	/**
	 * Main entry point.
	 *
	 * @param array|Appointment_Model $appointment
	 * @return bool
	 */
	public function validate( $appointment ): bool {
		$this->reset_error();

		$appointment = $this->normalize_appointment( $appointment );

		$type = $appointment->get( 'type' );

		switch ( $type ) {
			case 'range':
				return $this->validate_range_time( $appointment );

			case 'slot':
			case 'recurring':
				return $this->validate_slot_or_recurring_availability( $appointment );

			default:
				$this->set_error(
					'unknownAppointmentType',
					__( 'Unknown appointment type.', 'jet-appointments-booking' ),
					[ 'type' => $type ]
				);
				return false;
		}
	}

	/**
	 * Returns the last error, if any.
	 *
	 * @return WP_Error|null
	 */
	public function get_error(): ?WP_Error {
		return $this->error;
	}

	public function get_error_code(): ?string {
		return $this->error ? $this->error->get_error_code() : null;
	}

	public function get_error_message(): ?string {
		return $this->error ? $this->error->get_error_message() : null;
	}

	public function get_error_data() {
		return $this->error ? $this->error->get_error_data() : null;
	}

	protected function normalize_appointment( $appointment ): Appointment_Model {
		if ( is_array( $appointment ) ) {
			return new Appointment_Model( $appointment );
		}

		return $appointment;
	}

	protected function validate_range_time( Appointment_Model $appointment ): bool {

		$use_local_time = apply_filters( 'jet-apb/time-slots/use-local-time', false );

		if ( $use_local_time ) {
			$timestamp = time();
		} else {
			$timestamp = time() + current_datetime()->getOffset();
		}

		$settings = Time_Range::get_settings( [
			'service'  => $appointment->get( 'service' ),
			'provider' => $appointment->get( 'provider' ),
			'timezone' => $appointment->get( 'timezone' ),
			'date'     => $appointment->get( 'date' ),
			'time'     => $timestamp,
		] );

		if ( empty( $settings['hours_array'] ) ) {
			return true;
		}

		$slot     = ltrim( date( 'Hi', $appointment->get( 'slot' ) ), '0' );
		$slot_end = ltrim( date( 'Hi', $appointment->get( 'slot_end' ) ), '0' );

		$in_range = false;

		foreach ( (array) $settings['hours_array'] as $interval ) {
			$from = ltrim( str_replace( ':', '', $interval['from'] ), '0' );
			$to   = ltrim( str_replace( ':', '', $interval['to'] ), '0' );

			if (
				( $from <= $slot && $slot <= $to )
				&& ( $from <= $slot_end && $slot_end <= $to )
			) {
				$in_range = true;
				break;
			}
		}

		if ( ! $in_range ) {
			$this->set_error(
				'timeNotAllowedToBook',
				Plugin::instance()->settings->get_custom_label( 'timeNotAllowedToBook', __( 'Selected time is not allowed to book', 'jet-appointments-booking' ) ),
				[
					'slot'     => $appointment->get( 'slot' ),
					'slot_end' => $appointment->get( 'slot_end' ),
				]
			);
			return false;
		}

		if ( ! empty( $settings['busy_hours_array'] ) && is_array( $settings['busy_hours_array'] ) ) {

			foreach ( $settings['busy_hours_array'] as $interval ) {

				$from = ltrim( date( 'Hi', $interval['slot'] ), '0' );
				$to   = ltrim( date( 'Hi', $interval['slot_end'] ), '0' );

				if (
					( $slot <= $from && $from < $slot_end )
					|| ( $slot < $to && $to <= $slot_end )
					|| ( $from <= $slot && $to >= $slot_end )
				) {
					$this->set_error(
						'timeAlreadyTaken',
						Plugin::instance()->settings->get_custom_label( 'timeAlreadyTaken', __( 'Appointment time already taken', 'jet-appointments-booking' ) ),
						[
							'busy_from' => $interval['slot'],
							'busy_to'   => $interval['slot_end'],
						]
					);
					return false;
				}
			}
		}

		return true;
	}

	protected function validate_slot_or_recurring_availability( Appointment_Model $appointment ): bool {

		$service_id    = $appointment->get( 'service' );
		$provider_id   = $appointment->get( 'provider' );
		$buffer_before = Tools::get_time_settings( $service_id, $provider_id, 'buffer_before', 0 );
		$buffer_after  = Tools::get_time_settings( $service_id, $provider_id, 'buffer_after', 0 );

		if ( ! $this->is_appointment_date_allowed( $appointment ) ) {
			$this->set_error(
				'timeAlreadyTaken',
				Plugin::instance()->settings->get_custom_label( 'timeAlreadyTaken', __( 'Appointment time already taken', 'jet-appointments-booking' ) ),
				[ 'date' => $appointment->get( 'date' ) ]
			);
			return false;
		}

		$query_args = Tools::get_service_provider_args( $service_id, $provider_id );

		$query_args['date']   = $appointment->get( 'date' );
		$query_args['status'] = array_merge( Plugin::instance()->statuses->exclude_statuses() );

		$manage_capacity = Plugin::instance()->settings->get( 'manage_capacity' );

		if ( $manage_capacity ) {
			$booked_appointments = Plugin::instance()->db->appointments->query_with_capacity( $query_args );
			$service_count       = Plugin::instance()->tools->get_service_count( $service_id );
		} else {
			$booked_appointments = Plugin::instance()->db->appointments->query( $query_args );
			$service_count       = null;
		}

		$external            = Calendar::get_external( $service_id, $provider_id, $appointment->get( 'date' ) );
		$booked_appointments = Calendar::merge_excluded(
			( ( empty( $booked_appointments ) || ! is_array( $booked_appointments ) ) ? [] : $booked_appointments ),
			$external,
			$service_id,
			$provider_id
		);

		if ( empty( $booked_appointments ) ) {
			return true;
		}

		$appointment_slot     = $appointment->get( 'slot' ) - intval( $buffer_before );
		$appointment_slot_end = $appointment->get( 'slot_end' ) + intval( $buffer_after );
		$slot_count           = $appointment->get( 'count' );

		foreach ( $booked_appointments as $booked_appointment ) {
			$booked_appointment = new Appointment_Model( $booked_appointment );

			if ( $booked_appointment->conditions()->is_in_range(
				[ 'start' => $appointment_slot, 'end' => $appointment_slot_end ],
				[ 'before' => $buffer_before, 'after' => $buffer_after ]
			) ) {

				if ( $appointment->conditions()->is_with_capacity() ) {

					if ( $booked_appointment->conditions()->is_in_range(
						[ 'start' => $appointment->get( 'slot' ), 'end' => $appointment->get( 'slot_end' ) ]
					) ) {
						$slot_count += (int) $booked_appointment->get( 'count' );
					}

					if ( $service_count !== null && $slot_count > (int) $service_count ) {
						$this->set_error(
							'capacityExceeded',
							Plugin::instance()->settings->get_custom_label( 'capacityExceeded', __( 'Capacity exceeded for this time', 'jet-appointments-booking' ) ),
							[
								'count'         => $slot_count,
								'service_count' => $service_count,
							]
						);
						return false;
					}

				} else {
					$this->set_error(
						'timeNotAllowedToBook',
						Plugin::instance()->settings->get_custom_label( 'timeNotAllowedToBook', __( 'Selected time is not allowed to book', 'jet-appointments-booking' ) ),
						[
							'slot'     => $appointment->get( 'slot' ),
							'slot_end' => $appointment->get( 'slot_end' ),
						]
					);
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Check if date of given appointments is in schedule
	 *
	 * @param  object  $appointment [description]
	 * @return boolean              [description]
	 */
	protected function is_appointment_date_allowed( $appointment = null ) {

		if ( ! $appointment ) {
			return false;
		}

		$date     = $appointment->get( 'date' );
		$service  = $appointment->get( 'service' );
		$provider = $appointment->get( 'provider' );

		if ( ! $date ) {
			return false;
		}

		$excluded_dates      = Plugin::instance()->calendar->get_off_dates( $service, $provider );
		$works_dates         = Plugin::instance()->calendar->get_works_dates( $service, $provider );
		$allowed_dates_range = Plugin::instance()->calendar->get_dates_range( $service, $provider );
		$dates_mode          = Plugin::instance()->calendar->get_working_days_mode( $service, $provider );
		$available_week_days = Plugin::instance()->calendar->get_available_week_days( $service, $provider );

		// If date not in allowed range - decline it in any case
		if ( ! empty( $allowed_dates_range ) ) {

			if ( ! empty( $allowed_dates_range['start'] ) && $date < $allowed_dates_range['start'] ) {
				return false;
			}

			if ( ! empty( $allowed_dates_range['end'] ) && $date > $allowed_dates_range['end'] ) {
				return false;
			}
		}

		// Check if this date is set separetely as work date
		$works_dates  = ( ! empty( $works_dates ) && is_array( $works_dates ) ) ? $works_dates : [];
		$is_work_date = false;

		if ( empty( $works_dates ) || empty( $works_dates[0]['start'] ) ) {
			$is_work_date = true;
		}

		foreach ( $works_dates as $works_date ) {
			if ( $works_date['start'] <= $date && $date <= $works_date['end'] ) {
				$is_work_date = true;
			}
		}

		// if date is in $excluded_dates - decline full dates, check other - if they are not in $work_dates - also declie
		$excluded_dates  = ( ! empty( $excluded_dates ) && is_array( $excluded_dates ) ) ? $excluded_dates : [];

		foreach ( $excluded_dates as $excluded_date ) {
			if ( $excluded_date['start'] <= $date
				&& $date <= $excluded_date['end']
				&& ( empty( $excluded_date['service'] )
					|| absint( $excluded_date['service'] ) === absint( $service ) )
				) {

				// Decline full date imideately
				if ( ! empty( $excluded_date['is_full'] ) ) {
					return false;
				}

				// Also ecline if this not is separetely set working date
				if ( ! $is_work_date ) {
					return false;
				}

			}
		}

		// If this date is set separetely as work date - allow it
		if ( $is_work_date ) {
			return true;
		} elseif ( 'override_full' === $dates_mode ) {
			return false;
		}

		// Decline not allowed week days
		if ( ! empty( $available_week_days ) ) {

			if ( ! in_array( strtolower( date( 'l', $date ) ), $available_week_days ) ) {
				return false;
			}
		}

		// Allow other any other date
		return true;

	}

	protected function reset_error(): void {
		$this->error = null;
	}

	protected function set_error( string $code, string $message, array $data = [] ): void {
		$this->error = new WP_Error( $code, $message, $data );
	}

}