<?php
/**
 * Order Delivery Date module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Checkout;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Order Delivery Date module.
 *
 * Adds a preferred delivery-date field on Classic checkout with blocked
 * weekdays, configurable lead time, holiday exclusions, and a required toggle.
 * Chosen date is stored on the order and displayed in admin, emails, and the
 * order-received / My Account order view.
 *
 * Note: the Classic `woocommerce_before_order_notes` hook does NOT fire inside
 * the WooCommerce Blocks checkout. Phase 2 would wire this up via the Store API
 * `woocommerce_register_additional_checkout_field`.
 *
 * @since 1.0.0
 */
final class Order_Delivery_Date extends \Kramar\Module {

	/**
	 * Order meta key for the Y-m-d date string.
	 *
	 * @since 1.0.0
	 */
	private const META_DATE = '_kramar_delivery_date';

	/**
	 * Order meta key for the Unix timestamp.
	 *
	 * @since 1.0.0
	 */
	private const META_TIMESTAMP = '_kramar_delivery_timestamp';

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'order-delivery-date';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_category(): string {
		return 'checkout';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'Order Delivery Date', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Add a preferred delivery-date field to Classic checkout with blocked weekdays, lead time, and holiday exclusions. Classic checkout only — Blocks checkout (Phase 2) is out of scope.', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'          => false,
			'label'            => 'Gewenste bezorgdatum',
			'required'         => false,
			'hide_for_virtual' => true,
			'lead_time_days'   => 1,
			'lead_time_hours'  => 0,
			'blocked_weekdays' => [],
			'holidays'         => '',
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		$blocked = [];
		for ( $i = 0; $i <= 6; $i++ ) {
			if ( ! empty( $input['blocked_weekdays'][ $i ] ) ) {
				$blocked[] = $i;
			}
		}

		return [
			'enabled'          => ! empty( $input['enabled'] ),
			'label'            => sanitize_text_field( $input['label'] ?? 'Gewenste bezorgdatum' ),
			'required'         => ! empty( $input['required'] ),
			'hide_for_virtual' => ! empty( $input['hide_for_virtual'] ),
			'lead_time_days'   => absint( $input['lead_time_days'] ?? 1 ),
			'lead_time_hours'  => absint( $input['lead_time_hours'] ?? 0 ),
			'blocked_weekdays' => $blocked,
			'holidays'         => sanitize_textarea_field( $input['holidays'] ?? '' ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$s = $this->get_settings_array();

		$days_of_week = [
			0 => __( 'Sunday', 'kramar' ),
			1 => __( 'Monday', 'kramar' ),
			2 => __( 'Tuesday', 'kramar' ),
			3 => __( 'Wednesday', 'kramar' ),
			4 => __( 'Thursday', 'kramar' ),
			5 => __( 'Friday', 'kramar' ),
			6 => __( 'Saturday', 'kramar' ),
		];

		?>
		<tr>
			<th scope="row"><?php esc_html_e( 'Field label', 'kramar' ); ?></th>
			<td>
				<input
					type="text"
					name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][label]"
					value="<?php echo esc_attr( $s['label'] ); ?>"
					class="regular-text"
				/>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Required field', 'kramar' ); ?></th>
			<td>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][required]"
						value="1"
						<?php checked( $s['required'] ); ?>
					/>
					<?php esc_html_e( 'Make delivery date required', 'kramar' ); ?>
				</label>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Hide for virtual-only carts', 'kramar' ); ?></th>
			<td>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][hide_for_virtual]"
						value="1"
						<?php checked( $s['hide_for_virtual'] ); ?>
					/>
					<?php esc_html_e( 'Hide field when cart contains only virtual/downloadable products', 'kramar' ); ?>
				</label>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Lead time (days)', 'kramar' ); ?></th>
			<td>
				<input
					type="number"
					name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][lead_time_days]"
					value="<?php echo esc_attr( (string) $s['lead_time_days'] ); ?>"
					min="0"
					class="small-text"
				/>
				<p class="description"><?php esc_html_e( 'Minimum number of calendar days from today before the earliest selectable date.', 'kramar' ); ?></p>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Lead time (hours)', 'kramar' ); ?></th>
			<td>
				<input
					type="number"
					name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][lead_time_hours]"
					value="<?php echo esc_attr( (string) $s['lead_time_hours'] ); ?>"
					min="0"
					class="small-text"
				/>
				<p class="description"><?php esc_html_e( 'Additional hours added on top of the lead-time days.', 'kramar' ); ?></p>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Blocked weekdays', 'kramar' ); ?></th>
			<td>
				<?php foreach ( $days_of_week as $day_num => $day_label ) : ?>
					<label style="display:inline-block;margin-right:12px;">
						<input
							type="checkbox"
							name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][blocked_weekdays][<?php echo esc_attr( (string) $day_num ); ?>]"
							value="1"
							<?php checked( in_array( $day_num, $s['blocked_weekdays'], true ) ); ?>
						/>
						<?php echo esc_html( $day_label ); ?>
					</label>
				<?php endforeach; ?>
				<p class="description"><?php esc_html_e( 'Customers cannot select checked days as delivery date.', 'kramar' ); ?></p>
			</td>
		</tr>
		<tr>
			<th scope="row"><?php esc_html_e( 'Holidays', 'kramar' ); ?></th>
			<td>
				<textarea
					name="<?php echo esc_attr( $option_name ); ?>[checkout][order-delivery-date][holidays]"
					rows="4"
					class="large-text"
				><?php echo esc_textarea( $s['holidays'] ); ?></textarea>
				<p class="description"><?php esc_html_e( 'Enter one date per line in YYYY-MM-DD format. These dates will be blocked regardless of weekday.', 'kramar' ); ?></p>
			</td>
		</tr>
		<?php
	}

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		// Checkout field.
		add_action( 'woocommerce_before_order_notes', [ $this, 'render_field' ] );

		// Enqueue JS constraints.
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_checkout_assets' ] );

		// Server-side validation (authoritative).
		add_action( 'woocommerce_checkout_process', [ $this, 'validate_field' ] );

		// Save to order meta — classic checkout.
		add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'save_field' ] );

		// Display in admin order screen (order edit page).
		add_action( 'woocommerce_admin_order_data_after_shipping_address', [ $this, 'render_admin_order_meta' ] );

		// Orders list: add column.
		add_filter( 'manage_woocommerce_page_wc-orders_columns', [ $this, 'add_orders_column' ] );
		add_action( 'manage_woocommerce_page_wc-orders_custom_column', [ $this, 'render_orders_column' ], 10, 2 );

		// Legacy (non-HPOS) orders list.
		add_filter( 'manage_edit-shop_order_columns', [ $this, 'add_orders_column' ] );
		add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_orders_column_legacy' ], 10, 2 );

		// Display in order emails.
		add_filter( 'woocommerce_email_order_meta_fields', [ $this, 'email_order_meta_fields' ], 10, 3 );

		// Kramar Email Manager merge field (builder templates bypass the WC hook above).
		add_filter( 'kramar_email_merge_fields', [ $this, 'register_merge_field' ], 10, 2 );
		add_filter( 'kramar_email_merge_field_values', [ $this, 'resolve_merge_field' ], 10, 2 );

		// Display on order-received / My Account order detail.
		add_action( 'woocommerce_order_details_after_order_table', [ $this, 'render_order_details' ] );
	}

	// -------------------------------------------------------------------------
	// Checkout field rendering
	// -------------------------------------------------------------------------

	/**
	 * Render the delivery date input field on Classic checkout.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Checkout $checkout WooCommerce checkout object.
	 */
	public function render_field( \WC_Checkout $checkout ): void {
		if ( $this->should_hide() ) {
			return;
		}

		$s        = $this->get_settings_array();
		$label    = $s['label'];
		$required = (bool) $s['required'];
		$min_date = $this->compute_min_date();

		woocommerce_form_field(
			'kramar_delivery_date',
			[
				// 'date', not 'text' with a second type in custom_attributes. That
				// printed two type attributes on one tag; a browser keeps the first,
				// so the field was always a plain text box — no calendar, and the
				// min below inert, since min only applies to a date input. The
				// field's own script documents itself as enforcing rules "on the
				// native <input type="date"> field" that never existed.
				'type'        => 'date',
				'label'       => $label,
				'placeholder' => 'jjjj-mm-dd',
				'required'    => $required,
				'class'       => [ 'form-row-wide' ],
				'input_class' => [ 'kramar-delivery-date-input' ],
				'custom_attributes' => [
					'min'           => $min_date,
					'autocomplete'  => 'off',
				],
			],
			$checkout->get_value( 'kramar_delivery_date' )
		);
	}

	// -------------------------------------------------------------------------
	// JS enqueue
	// -------------------------------------------------------------------------

	/**
	 * Enqueue the delivery date constraint script on checkout.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_checkout_assets(): void {
		if ( ! is_checkout() || $this->should_hide() ) {
			return;
		}

		$s         = $this->get_settings_array();
		$min_date  = $this->compute_min_date();
		$holidays  = $this->parse_holidays( $s['holidays'] );
		$blocked   = array_map( 'intval', $s['blocked_weekdays'] );

		wp_enqueue_script(
			'kramar-delivery-date',
			KRAMAR_URL . 'assets/js/delivery-date.js',
			[],
			KRAMAR_VERSION,
			true
		);

		$lead_days = max( 0, (int) ( $s['lead_time_days'] ?? 1 ) );

		wp_localize_script(
			'kramar-delivery-date',
			'kramarDeliveryDate',
			[
				'minDate'         => $min_date,
				'blockedWeekdays' => $blocked,
				'holidays'        => $holidays,
				'required'        => $s['required'],
				'i18n'            => [
					'blocked'  => __( 'This day is not available for delivery. Please select another date.', 'kramar' ),
					/* translators: shown in JS when the selected delivery date is before the allowed minimum */
					// Substituted here, not in JS: delivery-date.js returned this
					// string verbatim, so the customer saw a literal "%d".
					'tooSoon'  => sprintf(
						/* translators: %d: minimum number of days ahead a delivery date must be */
						_n(
							'Please select a date at least %d day in the future.',
							'Please select a date at least %d days in the future.',
							$lead_days,
							'kramar'
						),
						$lead_days
					),
					'required' => __( 'Please choose a delivery date.', 'kramar' ),
				],
			]
		);
	}

	// -------------------------------------------------------------------------
	// Server-side validation (authoritative)
	// -------------------------------------------------------------------------

	/**
	 * Validate the delivery date on Classic checkout submission.
	 *
	 * @since 1.0.0
	 */
	public function validate_field(): void {
		if ( $this->should_hide() ) {
			return;
		}

		$s        = $this->get_settings_array();
		$required = (bool) $s['required'];

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC checkout nonce is verified by WooCommerce before woocommerce_checkout_process fires.
		$raw = sanitize_text_field( wp_unslash( $_POST['kramar_delivery_date'] ?? '' ) );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( '' === $raw ) {
			if ( $required ) {
				wc_add_notice( __( 'Please choose a delivery date.', 'kramar' ), 'error' );
			}
			return;
		}

		// Validate format (must parse as Y-m-d).
		$date = \DateTime::createFromFormat( 'Y-m-d', $raw );
		if ( false === $date || $date->format( 'Y-m-d' ) !== $raw ) {
			wc_add_notice( __( 'The delivery date is not valid. Please use the format YYYY-MM-DD.', 'kramar' ), 'error' );
			return;
		}

		// Validate not before min date.
		$min_date_str = $this->compute_min_date();
		$min          = \DateTime::createFromFormat( 'Y-m-d', $min_date_str );
		if ( false !== $min ) {
			$min->setTime( 0, 0, 0 );
			$date->setTime( 0, 0, 0 );
			if ( $date < $min ) {
				wc_add_notice(
					sprintf(
						/* translators: %s: earliest allowed date in YYYY-MM-DD format */
						__( 'The selected delivery date is too soon. The earliest available date is %s.', 'kramar' ),
						esc_html( $min_date_str )
					),
					'error'
				);
				return;
			}
		}

		// Validate not a blocked weekday.
		$blocked_weekdays = array_map( 'intval', $s['blocked_weekdays'] );
		$day_of_week      = (int) $date->format( 'w' ); // 0 = Sunday, 6 = Saturday.
		if ( in_array( $day_of_week, $blocked_weekdays, true ) ) {
			wc_add_notice( __( 'Delivery is not available on the selected day. Please choose a different date.', 'kramar' ), 'error' );
			return;
		}

		// Validate not a holiday.
		$holidays = $this->parse_holidays( $s['holidays'] );
		if ( in_array( $raw, $holidays, true ) ) {
			wc_add_notice( __( 'The selected date is a holiday and not available for delivery. Please choose a different date.', 'kramar' ), 'error' );
			return;
		}
	}

	// -------------------------------------------------------------------------
	// Save to order meta
	// -------------------------------------------------------------------------

	/**
	 * Save the delivery date to order meta on Classic checkout.
	 *
	 * @since 1.0.0
	 *
	 * @param int $order_id Order ID.
	 */
	public function save_field( int $order_id ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC checkout nonce already verified.
		$raw = sanitize_text_field( wp_unslash( $_POST['kramar_delivery_date'] ?? '' ) );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		if ( '' === $raw ) {
			return;
		}

		$date = \DateTime::createFromFormat( 'Y-m-d', $raw );
		if ( false === $date || $date->format( 'Y-m-d' ) !== $raw ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		$order->update_meta_data( self::META_DATE, $raw );
		$order->update_meta_data( self::META_TIMESTAMP, (string) $date->getTimestamp() );
		$order->save();
	}

	// -------------------------------------------------------------------------
	// Admin order screen
	// -------------------------------------------------------------------------

	/**
	 * Display the delivery date in the admin order edit screen.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order The order object.
	 */
	public function render_admin_order_meta( \WC_Order $order ): void {
		$date = $order->get_meta( self::META_DATE );
		if ( '' === (string) $date ) {
			return;
		}

		$s     = $this->get_settings_array();
		$label = esc_html( $s['label'] );

		printf(
			'<p><strong>%s:</strong> %s</p>',
			esc_html( $label ),
			esc_html( $this->format_date( (string) $date ) )
		);
	}

	/**
	 * Add a delivery date column to the HPOS orders list.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $columns Existing columns.
	 * @return array<string, string> Modified columns.
	 */
	public function add_orders_column( array $columns ): array {
		$new = [];

		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			if ( 'order_date' === $key ) {
				$new['kramar_delivery_date'] = __( 'Delivery date', 'kramar' );
			}
		}

		return $new;
	}

	/**
	 * Render delivery date in the HPOS orders list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string    $column   Column key.
	 * @param \WC_Order $order    Order object.
	 */
	public function render_orders_column( string $column, \WC_Order $order ): void {
		if ( 'kramar_delivery_date' !== $column ) {
			return;
		}

		echo wp_kses_post( $this->render_date_badge( (string) $order->get_meta( self::META_DATE ) ) );
	}

	/**
	 * Render delivery date in the legacy (post-based) orders list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string $column   Column key.
	 * @param int    $post_id  Post ID.
	 */
	public function render_orders_column_legacy( string $column, int $post_id ): void {
		if ( 'kramar_delivery_date' !== $column ) {
			return;
		}

		$order = wc_get_order( $post_id );
		if ( ! $order ) {
			return;
		}

		echo wp_kses_post( $this->render_date_badge( (string) $order->get_meta( self::META_DATE ) ) );
	}

	/**
	 * Format a stored Y-m-d delivery date using the site's date format.
	 *
	 * @since 1.0.0
	 *
	 * @param string $raw Stored Y-m-d date string.
	 * @return string Localised date, or the raw value if unparseable.
	 */
	/**
	 * Register the {delivery_date} merge field in the Email Manager picker.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, array<string, mixed>> $fields  Merge field groups.
	 * @param string                              $context Email context.
	 * @return array<string, array<string, mixed>>
	 */
	public function register_merge_field( array $fields, string $context ): array {
		if ( 'order' === $context && isset( $fields['kramar']['fields'] ) ) {
			$fields['kramar']['fields']['{delivery_date}'] = __( 'Delivery Date', 'kramar' );
		}
		return $fields;
	}

	/**
	 * Resolve {delivery_date} for an order.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $replacements Tag → value map.
	 * @param \WC_Order             $order        Order.
	 * @return array<string, string>
	 */
	public function resolve_merge_field( array $replacements, \WC_Order $order ): array {
		$replacements['{delivery_date}'] = $this->format_date( (string) $order->get_meta( self::META_DATE ) );
		return $replacements;
	}

	private function format_date( string $raw ): string {
		if ( '' === $raw ) {
			return '';
		}
		$ts = strtotime( $raw );
		return false !== $ts ? wp_date( (string) get_option( 'date_format', 'Y-m-d' ), $ts ) : $raw;
	}

	/**
	 * Render the delivery date as a clean pill badge for the orders list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string $raw Stored Y-m-d date string.
	 * @return string Badge HTML.
	 */
	private function render_date_badge( string $raw ): string {
		if ( '' === $raw ) {
			return '<span style="color:#a7aaad;">&mdash;</span>';
		}
		return '<span style="display:inline-block;padding:2px 8px;background:#eef2ff;color:#3730a3;border-radius:10px;font-size:12px;font-weight:600;white-space:nowrap;">'
			. esc_html( $this->format_date( $raw ) )
			. '</span>';
	}

	// -------------------------------------------------------------------------
	// Email display
	// -------------------------------------------------------------------------

	/**
	 * Inject the delivery date into order emails.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, array<string, mixed>> $fields        Existing email meta fields.
	 * @param bool                                $sent_to_admin True when email goes to admin.
	 * @param \WC_Order                           $order         Order object.
	 * @return array<string, array<string, mixed>> Extended fields.
	 */
	public function email_order_meta_fields( array $fields, bool $sent_to_admin, \WC_Order $order ): array {
		$date = $order->get_meta( self::META_DATE );
		if ( '' === (string) $date ) {
			return $fields;
		}

		$s = $this->get_settings_array();

		$fields['kramar_delivery_date'] = [
			'label' => $s['label'],
			'value' => (string) $date,
		];

		return $fields;
	}

	// -------------------------------------------------------------------------
	// Order-received / My Account display
	// -------------------------------------------------------------------------

	/**
	 * Display the delivery date on the order-received page and My Account order detail.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function render_order_details( \WC_Order $order ): void {
		$date = $order->get_meta( self::META_DATE );
		if ( '' === (string) $date ) {
			return;
		}

		$s = $this->get_settings_array();

		echo '<section class="kramar-delivery-date woocommerce-order-delivery-date">';
		echo '<h2>' . esc_html( $s['label'] ) . '</h2>';
		echo '<p>' . esc_html( (string) $date ) . '</p>';
		echo '</section>';
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/**
	 * Get all module settings as an array.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string, mixed> Module settings.
	 */
	private function get_settings_array(): array {
		$defaults = $this->get_default_settings();
		$saved    = $this->plugin->get_setting( $this->get_category(), $this->get_slug() );

		if ( ! is_array( $saved ) ) {
			return $defaults;
		}

		return array_merge( $defaults, $saved );
	}

	/**
	 * Determine whether the field should be hidden (virtual-only cart setting).
	 *
	 * @since 1.0.0
	 *
	 * @return bool True when the field should not be shown/processed.
	 */
	private function should_hide(): bool {
		$s = $this->get_settings_array();

		if ( ! $s['hide_for_virtual'] ) {
			return false;
		}

		// Only hide during front-end cart context.
		if ( ! function_exists( 'WC' ) || null === WC()->cart ) {
			return false;
		}

		if ( WC()->cart->is_empty() ) {
			return false;
		}

		foreach ( WC()->cart->get_cart() as $item ) {
			$product = $item['data'] ?? null;
			if ( ! ( $product instanceof \WC_Product ) ) {
				continue;
			}
			if ( ! $product->is_virtual() && ! $product->is_downloadable() ) {
				return false;
			}
		}

		// All items are virtual/downloadable.
		return true;
	}

	/**
	 * Compute the earliest selectable date string (Y-m-d), honouring lead time,
	 * blocked weekdays, and holidays.
	 *
	 * Starting from now + lead_time_days + lead_time_hours, the method advances
	 * one day at a time until it lands on a day that is neither a blocked
	 * weekday nor a holiday.
	 *
	 * @since 1.0.0
	 *
	 * @return string Y-m-d string of the earliest valid date.
	 */
	public function compute_min_date(): string {
		$s = $this->get_settings_array();

		$lead_days  = max( 0, (int) $s['lead_time_days'] );
		$lead_hours = max( 0, (int) $s['lead_time_hours'] );
		$blocked    = array_map( 'intval', $s['blocked_weekdays'] );
		$holidays   = $this->parse_holidays( $s['holidays'] );

		// Convert lead time to total seconds.
		$lead_seconds = ( $lead_days * DAY_IN_SECONDS ) + ( $lead_hours * HOUR_IN_SECONDS );

		// Candidate = now + lead time, normalised to midnight.
		$candidate = new \DateTime( 'now', wp_timezone() );
		$candidate->modify( '+' . $lead_seconds . ' seconds' );
		$candidate->setTime( 0, 0, 0 );

		// Advance past blocked weekdays and holidays.
		for ( $safety = 0; $safety < 365; $safety++ ) {
			$dow     = (int) $candidate->format( 'w' ); // 0=Sunday … 6=Saturday.
			$ymd     = $candidate->format( 'Y-m-d' );

			if ( ! in_array( $dow, $blocked, true ) && ! in_array( $ymd, $holidays, true ) ) {
				break;
			}

			$candidate->modify( '+1 day' );
		}

		return $candidate->format( 'Y-m-d' );
	}

	/**
	 * Parse the holidays textarea into an array of Y-m-d strings.
	 *
	 * Invalid or malformed lines are silently skipped.
	 *
	 * @since 1.0.0
	 *
	 * @param string $raw Raw newline-separated date list.
	 * @return list<string> Array of Y-m-d strings.
	 */
	private function parse_holidays( string $raw ): array {
		if ( '' === $raw ) {
			return [];
		}

		$holidays = [];

		foreach ( explode( "\n", $raw ) as $line ) {
			$line = trim( $line );
			if ( '' === $line ) {
				continue;
			}

			$dt = \DateTime::createFromFormat( 'Y-m-d', $line );
			if ( false !== $dt && $dt->format( 'Y-m-d' ) === $line ) {
				$holidays[] = $line;
			}
		}

		return $holidays;
	}
}
