<?php
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * WC_Appointments_Admin_Calendar class.
 */
class WC_Appointments_Admin_Calendar {

	/**
	 * Stores events.
	 *
	 * @var array
	 */
	private array $events = [];

	/**
	 * Output the calendar view
	 */
	public function output(): void {
		$filter_view  = apply_filters( 'woocommerce_appointments_calendar_view', 'week' );
		$user_view    = get_user_meta( get_current_user_id(), 'calendar_view', true );
		$default_view = $user_view ?: $filter_view;
		$view         = $_REQUEST['view'] ?? $default_view;
		$react_mode   = (bool) apply_filters( 'wc_appointments_admin_enable_react_calendar', true );
		$legacy_toggle = get_user_meta( get_current_user_id(), 'wc_appointments_legacy_calendar', true );
		if ( '1' === $legacy_toggle ) {
			$react_mode = false;
		}
		$staff_list   = WC_Appointments_Admin::get_appointment_staff();

		$product_filter = isset( $_REQUEST['filter_appointable_product'] ) ? absint( $_REQUEST['filter_appointable_product'] ) : '';
		$staff_filter   = isset( $_REQUEST['filter_appointable_staff'] ) ? absint( $_REQUEST['filter_appointable_staff'] ) : '';

		// Override to only show appointments for current staff member.
		if ( ! current_user_can( 'manage_others_appointments' ) ) {
			$staff_filter = get_current_user_id();
		}

		// Update calendar view seletion.
		if ( isset( $_REQUEST['view'] ) ) {
			update_user_meta( get_current_user_id(), 'calendar_view', $_REQUEST['view'] );
		}

		if ( $react_mode ) {
			$add_url = admin_url( 'edit.php?post_type=wc_appointment&page=add_appointment' );
			echo '<div class="wrap woocommerce">'
				. '<h2>' . esc_html__( 'Calendar', 'woocommerce-appointments' ) . ' '
				. '<a href="' . esc_url( $add_url ) . '" class="page-title-action">' . esc_html__( 'Add New Appointment', 'woocommerce-appointments' ) . '</a>'
				. '</h2>';
			echo '<div id="wca-react-calendar-root"></div></div>';
			return;
		}

		// No additional warning on page; the Screen Options label carries the deprecation text.

		if ( in_array( $view, [ 'day', 'staff' ] ) ) {
			$day           = isset( $_REQUEST['calendar_day'] ) ? wc_clean( $_REQUEST['calendar_day'] ) : date_i18n( 'Y-m-d' );
			$day_formatted = date( 'Y-m-d', strtotime( $day ) );
			$prev_day      = date( 'Y-m-d', strtotime( '-1 day', strtotime( $day ) ) );
			$next_day      = date( 'Y-m-d', strtotime( '+1 day', strtotime( $day ) ) );

			$args_filters = [
				'strict'   => true,
				'order_by' => 'start_date',
				'order'    => 'ASC',
			];

			$this->events = WC_Appointments_Availability_Data_Store::get_events_in_date_range(
			    strtotime( 'midnight', strtotime( $day ) ),
			    strtotime( 'midnight +1 day', strtotime( $day ) ),
			    $product_filter,
			    $staff_filter,
			    false,
			    $args_filters,
			);
		} elseif ( 'week' === $view ) {
			$day            = isset( $_REQUEST['calendar_day'] ) && $_REQUEST['calendar_day'] ? wc_clean( $_REQUEST['calendar_day'] ) : date_i18n( 'Y-m-d' );
			$day_formatted  = date( 'Y-m-d', strtotime( $day ) );
			$week           = date_i18n( 'w', strtotime( $day ) );
			$start_of_week  = absint( get_option( 'start_of_week', 1 ) );
			$week_start     = strtotime( "previous sunday +{$start_of_week} day", strtotime( $day ) );
			// Use midnight of the day after the week ends to ensure appointments spanning midnight are included
			$week_end       = strtotime( '+1 week', $week_start );
			$week_formatted = date_i18n( wc_appointments_date_format(), $week_start ) . ' &mdash; ' . date_i18n( wc_appointments_date_format(), strtotime( '-1 day', $week_end ) );
			$prev_week      = date( 'Y-m-d', strtotime( '-1 week', strtotime( $day ) ) );
			$next_week      = date( 'Y-m-d', strtotime( '+1 week', strtotime( $day ) ) );

			#$prev_day = date_i18n( wc_appointments_date_format(), strtotime( '-1 day', strtotime( $day ) ) );
			#print '<pre>'; print_r( $_REQUEST ); print '</pre>';

			$args_filters = [
				'strict'   => true,
				'order_by' => 'start_date',
				'order'    => 'ASC',
			];

			$this->events = WC_Appointments_Availability_Data_Store::get_events_in_date_range(
			    $week_start,
			    $week_end,
			    $product_filter,
			    $staff_filter,
			    false,
			    $args_filters,
			);
		} else {
			$month = isset( $_REQUEST['calendar_month'] ) ? absint( $_REQUEST['calendar_month'] ) : date_i18n( 'n' );
			$year  = isset( $_REQUEST['calendar_year'] ) ? absint( $_REQUEST['calendar_year'] ) : date_i18n( 'Y' );

			if ($year < ( date_i18n( 'Y' ) - 10 ) || 2100 < $year) {
                $year = date_i18n( 'Y' );
            }

			if ( 12 < $month ) {
				$month = 1;
				$year ++;
			}

			if ( 1 > $month ) {
				$month = 12;
				$year --;
			}

			$start_of_week = absint( get_option( 'start_of_week', 1 ) );
			$last_day      = date( 't', strtotime( "$year-$month-01" ) );
			$start_date_w  = absint( date( 'w', strtotime( "$year-$month-01" ) ) );
			$end_date_w    = absint( date( 'w', strtotime( "$year-$month-$last_day" ) ) );

			// Calc day offset
			$day_offset = $start_date_w - $start_of_week;
			$day_offset = 0 <= $day_offset ? $day_offset : 7 - abs( $day_offset );

			// Calc end day offset
			$end_day_offset = 7 - ( $last_day % 7 ) - $day_offset;
			$end_day_offset = 0 <= $end_day_offset && 7 > $end_day_offset ? $end_day_offset : 7 - abs( $end_day_offset );

			// We want to get the last minute of the day, so we will go forward one day to midnight and subtract a min
			$end_day_offset += 1;

			$start_time = strtotime( "-{$day_offset} day", strtotime( "$year-$month-01" ) );
			// Use midnight of the day after the last day shown to ensure appointments spanning midnight are included
			$end_time   = strtotime( "+{$end_day_offset} day", strtotime( "$year-$month-$last_day" ) );

			$args_filters = [
				'strict'   => true,
				'order_by' => 'start_date',
				'order'    => 'ASC',
			];

			$this->events = WC_Appointments_Availability_Data_Store::get_events_in_date_range(
			    $start_time,
			    $end_time,
			    $product_filter,
			    $staff_filter,
			    false,
			    $args_filters,
			);
		}

		include 'views/html-calendar-' . $view . '.php';
		include __DIR__ . '/views/html-calendar-dialog.php';
	}

	/**
	 * List appointments for a day
	 *
	 * @param  [type] $day
	 * @param  [type] $month
	 * @param  [type] $year
	 * @return [type]
	 */
	public function list_events( $day, $month, $year, $list = 'by_time', $staff_id = '' ): void {
		$date_start = strtotime( "$year-$month-$day midnight" ); // Midnight today.
		$date_end   = strtotime( "$year-$month-$day tomorrow" ); // Midnight next day.

		foreach ( $this->events as $event ) {
			$event_type       = is_a( $event, 'WC_Appointment' ) ? 'appointment' : 'availability';
			$event_is_all_day = $event->is_all_day();
			// Get start and end timestamps.
			if ( 'appointment' === $event_type ) {
				$event_start = $event->get_start();
				$event_end   = $event->get_end();
			} else {
				$range = $event->get_time_range_for_date( $date_start );
				if ( is_null( $range ) ) {
					continue;
				}
				$event_start      = $range['start'];
				$event_end        = $range['end'];
				$event_is_all_day = false; #Set all availability to be displayed as hourly.
			}

			if ( 'all_day' === $list && $event_is_all_day && $event_start < $date_end && $event_end > $date_start ) {
				if ( $staff_id && 'appointment' === $event_type ) {
					$staff_ids = $event->get_staff_ids();
					$staff_ids = wc_appointments_normalize_staff_ids( $staff_ids );
					if ( in_array( $staff_id, $staff_ids ) ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'all_day' );
					} elseif ( ! $staff_ids && 'unassigned' === $staff_id ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'all_day' );
					}
				} else {
					$this->event_card( $event, $event_start, $event_end, $list = 'all_day' );
				}
			} elseif ( 'by_time' === $list && ! $event_is_all_day && $event_start < $date_end && $event_end > $date_start ) {
				if ( $staff_id && 'appointment' === $event_type ) {
					$staff_ids = $event->get_staff_ids();
					$staff_ids = wc_appointments_normalize_staff_ids( $staff_ids );
					if ( in_array( $staff_id, $staff_ids ) ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'by_time' );
					} elseif ( ! $staff_ids && 'unassigned' === $staff_id ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'by_time' );
					}
				} else {
					$this->event_card( $event, $event_start, $event_end, $list = 'by_time' );
				}
			} elseif ( 'by_month' === $list && $event_start < $date_end && $event_end > $date_start ) {
				if ( $staff_id && 'appointment' === $event_type ) {
					$staff_ids = $event->get_staff_ids();
					$staff_ids = wc_appointments_normalize_staff_ids( $staff_ids );
					if ( in_array( $staff_id, $staff_ids ) ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'by_month' );
					} elseif ( ! $staff_ids && 'unassigned' === $staff_id ) {
						$this->event_card( $event, $event_start, $event_end, $list = 'by_month' );
					}
				} else {
					$this->event_card( $event, $event_start, $event_end, $list = 'by_month' );
				}
			}
		}
	}

	/**
	 * Event card.
	 */
	public function event_card( $event, $event_start, $event_end, $list = '' ): void {
		// Event defaults.
		$datarray               = [];
		$datarray['id']         = $event->get_id();
		$datarray['classes']    = [ 'event_card' ];
		$datarray['type']       = is_a( $event, 'WC_Appointment' ) ? 'appointment' : 'availability';
		$datarray['start']      = $event_start;
		$datarray['end']        = $event_end;
		$datarray['is_all_day'] = $event->is_all_day();
		$datarray['when']       = wc_appointment_format_timestamp( $datarray['start'], $datarray['is_all_day'] ) . ' &mdash; ' . wc_appointment_format_timestamp( $datarray['end'], $datarray['is_all_day'] );
		if ( date_i18n( 'ymd', $datarray['start'] ) === date_i18n( 'ymd', $datarray['start'] ) && ! $datarray['is_all_day'] ) {
			$datarray['when'] = wc_appointment_format_timestamp( $datarray['start'], $datarray['is_all_day'] ) . ' &mdash; ' . date_i18n( wc_appointments_time_format(), $datarray['end'] );
		}
		$datarray['duration']       = WC_Appointment_Duration::calculate_minutes( $datarray['start'], $datarray['end'] );
		$datarray['duration_unit']  = WC_Appointments_Constants::DURATION_MINUTE;
		$datarray['event_customer'] = '';
		$datarray['event_status']   = '';
		$datarray['event_name']     = '';
		$datarray['addons']         = '';
		$datarray['event_datetime'] = $datarray['is_all_day'] ? '' : date( wc_appointments_time_format(), $datarray['start'] );
		if ( 'all_day' === $list ) {
			$datarray['start_time'] = date( 'Y-m-d', $datarray['start'] );
			$datarray['end_time']   = date( 'Y-m-d', $datarray['start'] );
		} else {
			$datarray['start_time'] = date( 'Hi', $datarray['start'] );
			$datarray['end_time']   = date( 'Hi', $datarray['end'] );
		}
		if ( 'appointment' === $datarray['type'] ) {
			$event_product        = $event->get_product();
			$datarray['status']   = $event->get_status();
			$datarray['order_id'] = wp_get_post_parent_id( $event->get_id() );
			$datarray['staff_id'] = $event->get_staff_ids();
			if ( ! is_array( $datarray['staff_id'] ) ) {
				$datarray['staff_id'] = [ $datarray['staff_id'] ];
			}
			$datarray['staff_name']    = $event->get_staff_members( true ) ? htmlentities( $event->get_staff_members( true, true ) ) : '';
			$datarray['duration']      = WC_Appointment_Duration::calculate_minutes( $datarray['start'], $datarray['end'] );
			$datarray['duration_unit'] = $event->get_duration_unit();
			$datarray['event_qty']     = $event->get_qty();
			$datarray['order_id']      = wp_get_post_parent_id( $event->get_id() );
			if ( $datarray['order_id'] ) {
				$order                    = wc_get_order( $datarray['order_id'] );
				$datarray['order_status'] = is_a( $order, 'WC_Order' ) ? $order->get_status() : '';
			}
			$datarray['event_cost'] = esc_html( wc_price( (float) $event->get_cost() ) );
			if ( $datarray['order_id'] ) {
				$order                  = wc_get_order( $datarray['order_id'] );
				$datarray['event_cost'] = is_a( $order, 'WC_Order' ) ? esc_html( wc_price( (float) $order->get_total() ) ) : $datarray['event_cost'];
			}
			$datarray['event_status']       = $event->get_status();
			$datarray['classes'][]          = $datarray['event_status'];
			$datarray['event_status_label'] = wc_appointments_get_status_label( $event->get_status() );
			$customer_status                = $event->get_customer_status();
			$datarray['customer_status']    = $customer_status ?: 'expected';
			$datarray['classes'][]          = $datarray['customer_status'];
			$customer                       = $event->get_customer();
			if ( $customer ) {
				$datarray['event_customer'] = $customer->full_name;
				$datarray['customer_name']  = $customer->full_name;
				$datarray['customer_phone'] = preg_replace( '/\s+/', '', $customer->phone );
				$datarray['customer_email'] = $customer->email;
				if ( $customer->user_id ) {
					$datarray['customer_id']     = $customer->user_id;
					$datarray['customer_url']    = get_edit_user_link( $customer->user_id );
					$datarray['customer_avatar'] = get_avatar_url(
					    $customer->user_id,
					    [
							'size'    => 110,
							'default' => 'mm',
						],
					);
				}
			}
			$datarray['product_id']    = $event->get_product_id();
			$datarray['product_title'] = is_object( $event_product ) ? $event_product->get_title() : '';
			$datarray['addons']        = esc_html( $event->get_addons() );
			$datarray['edit_link']     = esc_url( admin_url( 'post.php?post=' . $event->get_id() . '&action=edit' ) );
			$datarray['color']         = is_object( $event_product ) && $event_product->get_cal_color() ? $event_product->get_cal_color() : '#0073aa';
		} else {
			$is_rrule    = 'rrule' === $event->get_range_type();
			$is_google   = ! empty( $event->get_event_id() );
			$is_all_day  = false === strpos( $event->get_from_range(), ':' );
			$rrule_str   = wc_appointments_esc_rrule( $event->get_rrule(), $is_all_day );
			$date_format = $is_all_day ? 'Y-m-d' : 'Y-m-d g:i A';
			$from_date   = new WC_DateTime( $event->get_from_range() );
			$to_date     = new WC_DateTime( $event->get_to_range() );
			$timezone    = new DateTimeZone( wc_timezone_string() );
			$from_date->setTimezone( $timezone );
			$to_date->setTimezone( $timezone );
			$human_readable_options = [
				'date_formatter' => fn($date) => $date->format( $date_format ),
				'locale'         => 'en',
			];
			$datarray['event_name'] = empty( $event->get_title() ) ? '' : $event->get_title() . ' - ';
			if ( $is_google ) {
				if ( $is_rrule ) {
					$datarray['event_name'] .= '<small>' . esc_html__( 'Google Recurring Event', 'woocommerce-appointments' ) . '</small>';
				} else {
					$datarray['event_name'] .= '<small>' . esc_html__( 'Google Event', 'woocommerce-appointments' ) . '</small>';
				}
			}
			if ( $is_rrule ) {
				$rset             = new \RRule\RSet( $rrule_str, $is_all_day ? $from_date->format( $date_format ) : $from_date );
				$datarray['when'] = esc_html__( 'Repeating ', 'woocommerce-appointments' );
				foreach ( $rset->getRRules() as $rrule ) {
					$datarray['when'] .= esc_html( $rrule->humanReadable( $human_readable_options ) );
				}
				if ( $rset->getExDates() ) {
					$datarray['when'] .= esc_html__( ', except ', 'woocommerce-appointments' );
					$datarray['when'] .= esc_html(
					    implode(
					        ' and ',
					        array_map(
					            fn($date) => $date->format( $date_format ),
					            $rset->getExDates(),
					        ),
					    ),
					);
				}
				$datarray['duration'] = '';
			}
			if ( $is_all_day ) {
				$datarray['duration']       = '';
				$datarray['event_datetime'] = '';
			}
			$datarray['edit_link'] = esc_url( admin_url( 'admin.php?page=wc-settings&tab=appointments&view=synced' ) );
			$datarray['color']     = '#555';
		}

		// Card -data attributes.
		$card_data_attr = '';
		foreach ( $datarray as $attribute => $value ) {
			if ( is_array( $value ) ) {
				$attrs = '';
				foreach ( $value as $attr_key => $attr_val ) {
					$attrs .= "{$attr_key}: {$attr_val};";
				}
				$value = $attrs;
			}

			$card_data_attr .= "data-{$attribute}=\"{$value}\" ";
		}
		$card_data_html = apply_filters( 'woocommerce_appointments_calendar_single_card_data', $card_data_attr, $datarray, $event );

		// Card style.
		$calendar_scale = apply_filters( 'woocommerce_appointments_calendar_view_day_scale', 60 );
		$event_top      = ( ( intval( substr( $datarray['start_time'], 0, 2 ) ) * 60 ) + intval( substr( $datarray['start_time'], -2 ) ) ) / 60 * $calendar_scale;
		$card_style     = '';
		if ( 'by_time' === $list ) {
			$duration_minutes = WC_Appointment_Duration::calculate_minutes( $datarray['start'], $datarray['end'] );
			$height           = intval( ( $duration_minutes / 60 ) * $calendar_scale );
			$card_style      .= ' background: ' . $datarray['color'] . '; top: ' . $event_top . 'px; height: ' . $height . 'px;';
		} else {
			$card_style .= ' background: ' . $datarray['color'];
		}

		// Build card variables.
		$card_title     = __( 'View / Edit', 'woocommerce-appointments' );
		$card_classes   = implode( ' ', $datarray['classes'] );
		$card_edit_link = $datarray['edit_link'];
		#$card_header     = $datarray['event_datetime'] ? '<strong class="event_datetime">' . $datarray['event_datetime'] . '</strong>' : '';
		$card_header     = $datarray['event_customer'] && $datarray['customer_status'] ? '<strong class="event_customer status-' . $datarray['customer_status'] . '">' . $datarray['event_customer'] . '</strong>' : '';
		$card_content_li = '';
		if ( '' !== $datarray['event_datetime'] && '0' !== $datarray['event_datetime'] ) {
			$card_content_li .= '<li class="event_datetime">' . $datarray['event_datetime'] . '</li>';
		} elseif ( '' !== $datarray['event_name'] && '0' !== $datarray['event_name'] ) {
			$card_content_li .= '<li class="event_availability">' . $datarray['event_name'] . '</li>';
		}
		if ( $datarray['event_status'] && $datarray['event_status_label'] ) {
			$card_content_li .= '<li class="event_status status-' . $datarray['event_status'] . '" data-tip="' . wc_sanitize_tooltip( $datarray['event_status_label'] ) . '"></li>';
		}

		// Build card html.
		$card_html = "
			<div class='$card_classes' title='$card_title' $card_data_html style='$card_style'>
				<a href='$card_edit_link'>
					$card_header
					<ul>
					$card_content_li
					</ul>
				</a>
			</div>
		";

		echo apply_filters( 'woocommerce_appointments_calendar_view_single_card', $card_html, $datarray, $event );
	}

	/**
     * Filters staff for narrowing search
     * @return mixed[]
     */
    public function staff_filters(): array {
		$filters = [];

		// Only show staff filter if current user can see other staff's appointments.
		if ( ! current_user_can( 'manage_others_appointments' ) ) {
			return $filters;
		}

		$staff = WC_Appointments_Admin::get_appointment_staff();

		foreach ( $staff as $staff_member ) {
			$filters[ $staff_member->ID ] = $staff_member->display_name;
		}

		return $filters;
	}

	/**
     * Enqueue calendar scripts and styles for React calendar mode.
     *
     * Handles script registration, localization, and configuration for the admin calendar.
     * This method is called from WC_Appointments_Admin_Menus::admin_scripts().
     */
    public static function enqueue_react_calendar_scripts(): void {
		// Load calendar class if not already loaded
		if ( ! class_exists( 'WC_Appointments_Admin_Calendar' ) ) {
			require_once __DIR__ . '/class-wc-appointments-admin-calendar.php';
		}

		wp_enqueue_script( 'wp-element' );
		// FullCalendar v6 global JS + CSS
		wp_enqueue_script( 'fullcalendar-global' );
		wp_enqueue_script( 'fullcalendar-locales' );
		// React calendar styles moved to assets/css/admin.scss
		$script_handle = 'wca-admin-calendar-react';
		wp_register_script(
		    $script_handle,
		    WC_APPOINTMENTS_PLUGIN_URL . '/assets/js/appointment-admin-calendar.js',
		    [ 'wp-element', 'fullcalendar-global' ],
		    WC_APPOINTMENTS_VERSION,
		    true,
		);

		// Build months arrays
		$months_full = [];
		$months_short = [];
		for ( $i = 1; 12 >= $i; $i++ ) {
			$months_full[]  = date_i18n( 'F', mktime( 0, 0, 0, $i, 1, 2020 ) );
			$months_short[] = date_i18n( 'M', mktime( 0, 0, 0, $i, 1, 2020 ) );
		}

		// Build currency symbols map
		$symbols_map = [];
		if ( function_exists( 'get_woocommerce_currencies' ) ) {
			foreach ( get_woocommerce_currencies() as $code => $name ) {
				$symbols_map[ $code ] = html_entity_decode( get_woocommerce_currency_symbol( $code ), ENT_NOQUOTES, 'UTF-8' );
			}
		}

		$locale_bcp47 = str_replace( '_', '-', get_locale() );

		// Preload appointments server-side for instant calendar rendering
		// Use saved user preference if no view specified in URL
		$filter_view  = apply_filters( 'woocommerce_appointments_calendar_view', 'week' );
		$user_view    = get_user_meta( get_current_user_id(), 'calendar_view', true );
		$default_view = $user_view ?: $filter_view;
		$initial_view = isset( $_GET['view'] ) ? sanitize_text_field( wp_unslash( $_GET['view'] ) ) : $default_view;
		$initial_date = isset( $_GET['calendar_day'] ) ? sanitize_text_field( wp_unslash( $_GET['calendar_day'] ) ) : '';
		$product_filter = isset( $_GET['filter_appointable_product'] ) ? absint( $_GET['filter_appointable_product'] ) : 0;
		$staff_filter = isset( $_GET['filter_appointable_staff'] ) ? absint( $_GET['filter_appointable_staff'] ) : 0;

		// Override to only show appointments for current staff member
		if ( ! current_user_can( 'manage_others_appointments' ) ) {
			$staff_filter = get_current_user_id();
		}

		$preloaded_appointments = self::get_preloaded_appointments(
		    $initial_view,
		    $initial_date,
		    $product_filter,
		    $staff_filter,
		);

		// Build configuration array
		$config = [
			'restUrl'   => esc_url_raw( rest_url() ),
			'nonce'     => wp_create_nonce( 'wp_rest' ),
			'adminPost' => admin_url( 'post.php' ),
			'ajaxUrl'   => admin_url( 'admin-ajax.php' ),
			'viewNonce' => wp_create_nonce( 'wc_appointments_calendar_view' ),
			'blogId'    => get_current_blog_id(), // Site-specific identifier for multisite localStorage keys
			'initialView' => $initial_view,
			'initialDate' => $initial_date,
			'preloadedAppointments' => $preloaded_appointments,
			'canManageOthers' => current_user_can( 'manage_others_appointments' ),
			'dateFormat' => wc_appointments_date_format(),
			'timeFormat' => wc_appointments_time_format(),
			'locale' => $locale_bcp47,
			'firstDay' => (int) get_option( 'start_of_week' ),
			'monthsFull' => $months_full,
			'monthsShort' => $months_short,
			'sseDurationMs' => apply_filters( 'wc_appointments_sse_duration_ms', 60000 ), // 1 minute default, filterable
			'i18n' => [
				'product' => __( 'Product', 'woocommerce-appointments' ),
				'staff' => __( 'Staff', 'woocommerce-appointments' ),
				'when' => __( 'When', 'woocommerce-appointments' ),
				'duration' => __( 'Duration', 'woocommerce-appointments' ),
				'addons' => __( 'Add-ons', 'woocommerce-appointments' ),
				'cost' => __( 'Cost', 'woocommerce-appointments' ),
				'coupon' => __( 'Coupon', 'woocommerce' ),
				'coupons' => __( 'Coupons', 'woocommerce' ),
				'discount' => __( 'Discount', 'woocommerce' ),
				'tax' => __( 'Tax', 'woocommerce' ),
				'currencySymbols' => $symbols_map,
				'currencyFormat' => [
					'decimalSeparator'  => wc_get_price_decimal_separator(),
					'thousandSeparator' => wc_get_price_thousand_separator(),
					'priceDecimals'     => wc_get_price_decimals(),
					'currencyPosition'  => get_option( 'woocommerce_currency_pos' ),
				],
				'close' => __( 'Close', 'woocommerce-appointments' ),
				'editAppointment' => __( 'Edit Appointment', 'woocommerce-appointments' ),
				'unavailableRule' => __( 'Unavailable Rule', 'woocommerce-appointments' ),
				'ruleId' => __( 'Rule ID', 'woocommerce-appointments' ),
				'scope' => __( 'Scope', 'woocommerce-appointments' ),
				'start' => __( 'Start', 'woocommerce-appointments' ),
				'end' => __( 'End', 'woocommerce-appointments' ),
				'month' => __( 'Month', 'woocommerce-appointments' ),
				'week' => __( 'Week', 'woocommerce-appointments' ),
				'day' => __( 'Day', 'woocommerce-appointments' ),
				'list' => __( 'List', 'woocommerce-appointments' ),
				'prev' => __( 'Prev', 'woocommerce-appointments' ),
				'today' => __( 'Today', 'woocommerce-appointments' ),
				'next' => __( 'Next', 'woocommerce-appointments' ),
				'allProducts' => __( 'All Products', 'woocommerce-appointments' ),
				'allStaff' => __( 'All Staff', 'woocommerce-appointments' ),
				'guest' => __( 'Guest', 'woocommerce-appointments' ),
				'customer' => __( 'Customer', 'woocommerce-appointments' ),
				'unassigned' => __( 'Unassigned', 'woocommerce-appointments' ),
				'calendarLibFailed' => __( 'Calendar library failed to load.', 'woocommerce-appointments' ),
				'clear' => __( 'Clear', 'woocommerce-appointments' ),
				'loading' => __( 'Loading…', 'woocommerce-appointments' ),
				'typeToSearch' => __( 'Type to search', 'woocommerce-appointments' ),
				'editCustomer' => __( 'Edit Customer', 'woocommerce-appointments' ),
				'confirmCancel' => __( 'Are you sure you want to cancel this appointment?', 'woocommerce-appointments' ),
				'cancelFailed' => __( 'Failed to cancel appointment.', 'woocommerce-appointments' ),
				'cancelAppointment' => __( 'Cancel', 'woocommerce-appointments' ),
				'restFetchFailed' => __( 'Failed to load data from REST API: ', 'woocommerce-appointments' ),
				'restApiUnavailable' => __( 'REST API is not available. Calendar will display preloaded appointments only. Please contact your site administrator if this persists.', 'woocommerce-appointments' ),
				'reloadPage' => __( 'Reload page', 'woocommerce-appointments' ),
				'appointment' => __( 'Appointment', 'woocommerce-appointments' ),
				'order_total' => __( 'Order Total', 'woocommerce' ),
				'confirm' => __( 'Confirm', 'woocommerce-appointments' ),
				'confirmAppointmentTitle' => __( 'Confirm this appointment', 'woocommerce-appointments' ),
				'cancelAppointmentTitle' => __( 'Cancel this appointment', 'woocommerce-appointments' ),
				'durationDay' => __( 'day', 'woocommerce-appointments' ),
				'durationDays' => __( 'days', 'woocommerce-appointments' ),
				'durationMonth' => __( 'month', 'woocommerce-appointments' ),
				'durationMonths' => __( 'months', 'woocommerce-appointments' ),
				'durationHour' => __( 'hour', 'woocommerce-appointments' ),
				'durationHours' => __( 'hours', 'woocommerce-appointments' ),
				'durationHr' => __( 'hr', 'woocommerce-appointments' ),
				'durationHrs' => __( 'hrs', 'woocommerce-appointments' ),
				'durationMin' => __( 'min', 'woocommerce-appointments' ),
				'dismiss' => __( 'Dismiss', 'woocommerce-appointments' ),
				// Toast notification messages (semantic, translatable)
				// Placeholders: {customer} = customer name, {datetime} = appointment date/time, {status} = status label
				'toastAvailabilityCreated' => __( 'Availability rule created', 'woocommerce-appointments' ),
				'toastAvailabilityUpdated' => __( 'Availability rule updated', 'woocommerce-appointments' ),
				'toastAvailabilityDeleted' => __( 'Availability rule deleted', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {datetime} = appointment date/time */
				'toastAddedToCart' => __( 'Appointment with {customer} added to cart for {datetime}', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {datetime} = appointment date/time */
				'toastAppointmentCreatedSemantic' => __( 'Appointment with {customer} created for {datetime}', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {datetime} = appointment date/time */
				'toastAppointmentCancelledSemantic' => __( 'Appointment with {customer} for {datetime} was cancelled', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name */
				'toastAppointmentDeletedSemantic' => __( 'Appointment with {customer} was removed', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {datetime} = appointment date/time */
				'toastAppointmentRescheduledSemantic' => __( 'Appointment with {customer} rescheduled to {datetime}', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {datetime} = appointment date/time */
				'toastAppointmentBooked' => __( '{customer} booked an appointment for {datetime}', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name */
				'toastRemovedFromCart' => __( 'Appointment with {customer} removed from cart', 'woocommerce-appointments' ),
				/* translators: {customer} = customer name, {status} = status label */
				'toastAppointmentStatusChanged' => __( 'Appointment with {customer} status changed to {status}', 'woocommerce-appointments' ),
				'statuses' => [
					'unpaid' => __( 'Unpaid', 'woocommerce-appointments' ),
					'pending-confirmation' => __( 'Pending Confirmation', 'woocommerce-appointments' ),
					'confirmed' => __( 'Confirmed', 'woocommerce-appointments' ),
					'paid' => __( 'Paid', 'woocommerce-appointments' ),
					'cancelled' => __( 'Cancelled', 'woocommerce-appointments' ),
					'complete' => __( 'Complete', 'woocommerce-appointments' ),
					'in-cart' => __( 'In Cart', 'woocommerce-appointments' ),
					'was-in-cart' => __( 'Was In Cart', 'woocommerce-appointments' ),
				],
				'customerStatuses' => get_wc_appointment_statuses( 'customer', true ),
			],
		];

		wp_localize_script( $script_handle, 'wcAppointmentsReactCalendar', $config );
		wp_enqueue_script( $script_handle );
	}

	/**
	 * Get preloaded appointments for initial calendar view.
	 * 
	 * Preloads appointments server-side to eliminate initial API call and enable
	 * instant calendar rendering. Similar to how Google Calendar and other major
	 * calendar apps handle initial data loading.
	 *
	 * @param string $view The calendar view (week, month, day, list).
	 * @param string $initial_date Optional initial date (Y-m-d format).
	 * @param int    $product_id Optional product filter.
	 * @param int    $staff_id Optional staff filter.
	 * @return array Preloaded appointments data formatted for FullCalendar.
	 */
		public static function get_preloaded_appointments( $view = 'week', $initial_date = '', $product_id = 0, $staff_id = 0 ): array {
		// Ensure REST API internal class is loaded
		if ( ! class_exists( 'WC_Appointments_REST_API_Internal' ) ) {
			// Try to load via REST API class if available
			if ( class_exists( 'WC_Appointments_REST_API' ) ) {
				$rest_api = new WC_Appointments_REST_API();
				$rest_api->rest_api_includes();
			} else {
				// Fallback: directly include the file
				$internal_api_file = WC_APPOINTMENTS_ABSPATH . 'includes/api/class-wc-appointments-rest-api-internal.php';
				if ( file_exists( $internal_api_file ) ) {
					include_once $internal_api_file;
				}
			}
		}

		// If class still doesn't exist, return empty array
		if ( ! class_exists( 'WC_Appointments_REST_API_Internal' ) ) {
			return [];
		}

		// Calculate date range based on view
		$start_ts = 0;
		$end_ts   = 0;

		if ( empty( $initial_date ) ) {
			$initial_date = date_i18n( 'Y-m-d' );
		}

		$day_timestamp = strtotime( $initial_date );

		switch ( $view ) {
			case 'day':
				$start_ts = strtotime( 'midnight', $day_timestamp );
				$end_ts   = strtotime( 'midnight +1 day', $day_timestamp );
				break;

			case 'month':

			case 'week':
				$year  = date( 'Y', $day_timestamp );
				$month = date( 'n', $day_timestamp );
				$start_of_week = absint( get_option( 'start_of_week', 1 ) );
				$last_day = date( 't', strtotime( "$year-$month-01" ) );
				$start_date_w = absint( date( 'w', strtotime( "$year-$month-01" ) ) );
				$end_date_w = absint( date( 'w', strtotime( "$year-$month-$last_day" ) ) );

				$day_offset = $start_date_w - $start_of_week;
				$day_offset = 0 <= $day_offset ? $day_offset : 7 - abs( $day_offset );

				$end_day_offset = 7 - ( $last_day % 7 ) - $day_offset;
				$end_day_offset = 0 <= $end_day_offset && 7 > $end_day_offset ? $end_day_offset : 7 - abs( $end_day_offset );
				$end_day_offset += 1;

				$start_ts = strtotime( "-{$day_offset} day", strtotime( "$year-$month-01" ) );
				// Use midnight of the day after the last day shown to ensure appointments spanning midnight are included
				$end_ts   = strtotime( "+{$end_day_offset} day", strtotime( "$year-$month-$last_day" ) );
				break;

			default:
				$start_of_week = absint( get_option( 'start_of_week', 1 ) );
				$start_ts = strtotime( "previous sunday +{$start_of_week} day", $day_timestamp );
				// Use midnight of the day after the week ends to ensure appointments spanning midnight are included
				$end_ts   = strtotime( '+1 week', $start_ts );
				break;
		}

		// Expand range slightly to preload adjacent periods (like Google Calendar does)
		// This reduces API calls when navigating
		$buffer_days = 7; // Preload 1 week before and after
		$start_ts = strtotime( "-{$buffer_days} days", $start_ts );
		$end_ts   = strtotime( "+{$buffer_days} days", $end_ts );

		// Build request parameters for the optimized calendar API
		// Include all fields needed for calendar display AND modal popup to avoid subsequent API calls
		$request_params = [
			'date_from'             => '@' . $start_ts,
			'date_to'               => '@' . $end_ts,
			'include_order_details' => true, // Include order currency, total, billing for modal
			'_fields'               => 'id,start,end,product_id,product_title,staff_id,staff_ids,staff_name,staff_avatar,status,customer_id,order_id,order_item_id,order_info,cost,all_day,qty,customer_status,customer_name,customer_first_name,customer_last_name,customer_full_name,customer_avatar,customer_email,customer_phone,cal_color',
		];

		if ( 0 < $product_id ) {
			$request_params['product_id'] = $product_id;
		}

		if ( 0 < $staff_id ) {
			$request_params['staff_id'] = $staff_id;
		} elseif ( 'unassigned' === $staff_id ) {
			$request_params['staff_id'] = 0;
		}

		// Use the new optimized calendar API (v2) - uses direct SQL with JOINs
		// This avoids N+1 queries and heavy object hydration for much faster loading
		$all_appointments = WC_Appointments_REST_API_Internal::calendar()->get(
			$request_params,
			[
				'auto_paginate' => false, // Calendar endpoint returns all results in one call
			]
		);

		// Handle errors - fall back to legacy appointments API if calendar endpoint fails
		if ( is_wp_error( $all_appointments ) ) {
			// Fallback to legacy appointments API
			$all_appointments = WC_Appointments_REST_API_Internal::appointments( 'v2' )->get(
				$request_params,
				[
					'per_page'  => 100,
					'max_pages' => 20,
				]
			);

			if ( is_wp_error( $all_appointments ) ) {
				return [];
			}

			// Filter out excluded statuses for legacy API
			$excluded_statuses = [ 'trash', 'in-cart', 'was-in-cart' ];
			$all_appointments  = array_filter( $all_appointments, function ( array $appt ) use ( $excluded_statuses ): bool {
				if ( ! isset( $appt['status'] ) ) {
					return false;
				}
				$status = strtolower( $appt['status'] );
				return ! in_array( $status, $excluded_statuses, true );
			} );
		}

		return array_values( $all_appointments );
	}

}
