<?php
/**
 * Accommodation (Room) Schema Module for BW AI Schema Pro
 *
 * Room-level schema for lodging clients — resorts, hotels, villa rentals. Mirrors
 * the People module (BW_Schema_Team_Member): a CPT is nominated as "rooms" in
 * Settings → Rooms, each room post gets a detect-then-override meta box, and ONE
 * provider (get_room_schema_data()) feeds every consumer:
 *
 *   - the single-room page node (HotelRoom / Suite / …)
 *   - Organization.containsPlace[] via BW_Schema_Org_Builder::collect_accommodations()
 *   - the Rooms settings table
 *
 * Unlike the Person side (which is split across BW_Schema_Person and
 * BW_Schema_Team_Member with duplicated fallback chains), this module is
 * deliberately a single class with a single builder. See docs/SPEC-accommodations.md.
 *
 * Meta keys are namespaced _bw_schema_room_* so a room post can never collide
 * with the LocalBusiness path's _bw_schema_amenities or the Person module's
 * _bw_schema_awards / _bw_schema_credentials.
 *
 * @package BW_AI_Schema_Pro
 * @since 2.4.0
 */

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

class BW_Schema_Room {

	/**
	 * Stable @id fragment for a room node.
	 */
	const ID_FRAGMENT = '#room';

	/**
	 * Get the configured room post type.
	 *
	 * @return string|false Post type slug, or false when the module isn't configured.
	 */
	public static function get_room_post_type() {
		$post_type = BW_Schema_Core::get_settings( 'content', 'room_post_type', '' );

		if ( empty( $post_type ) ) {
			$post_type = get_option( 'bw_schema_room_post_type', '' );
		}

		if ( ! empty( $post_type ) && post_type_exists( $post_type ) ) {
			return $post_type;
		}

		return false;
	}

	/**
	 * Module-level settings (single array option, not a row per setting).
	 *
	 * @return array
	 */
	public static function get_module_settings() {
		$defaults = array(
			'currency'        => 'USD',
			'rate_unit'       => 'DAY',
			'rate_stale_days' => 180,
		);

		$stored = get_option( 'bw_schema_accommodation', array() );
		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		return array_merge( $defaults, $stored );
	}

	/**
	 * Supported room @types. Keys are Schema.org types under Accommodation.
	 *
	 * @return array
	 */
	public static function get_room_types() {
		return array(
			'HotelRoom'    => __( 'Hotel Room', 'bw-ai-schema-pro' ),
			'Suite'        => __( 'Suite', 'bw-ai-schema-pro' ),
			'House'        => __( 'House / Villa', 'bw-ai-schema-pro' ),
			'Apartment'    => __( 'Apartment / Condo', 'bw-ai-schema-pro' ),
			'CampingPitch' => __( 'Camping Pitch', 'bw-ai-schema-pro' ),
			'Room'         => __( 'Room (generic)', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Common bed types. Free text is still accepted — this is a convenience list.
	 *
	 * @return array
	 */
	public static function get_bed_types() {
		return array(
			'King', 'Queen', 'Double', 'Single', 'Twin', 'Bunk', 'Sofa Bed', 'Rollaway', 'Crib',
		);
	}

	/**
	 * Floor-size units → UN/CEFACT unit codes.
	 *
	 * @return array
	 */
	public static function get_area_units() {
		return array(
			'FTK' => __( 'Square feet', 'bw-ai-schema-pro' ),
			'MTK' => __( 'Square metres', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Rate units → UN/CEFACT unit codes. Carries the "per night" semantics that a
	 * bare price number can't express.
	 *
	 * @return array
	 */
	public static function get_rate_units() {
		return array(
			'DAY' => __( 'per night', 'bw-ai-schema-pro' ),
			'WK'  => __( 'per week', 'bw-ai-schema-pro' ),
			'MON' => __( 'per month', 'bw-ai-schema-pro' ),
		);
	}

	/**
	 * Initialize the module. No-ops cheaply when no room CPT is configured.
	 */
	public static function init() {
		add_action( 'add_meta_boxes', array( __CLASS__, 'add_room_meta_box' ), 15 );
		add_action( 'save_post', array( __CLASS__, 'save_room_schema' ), 10, 2 );
		add_action( 'wp', array( __CLASS__, 'disable_conflicting_schemas' ), 5 );
		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin_styles' ) );
		add_action( 'admin_notices', array( __CLASS__, 'maybe_show_stale_rate_notice' ) );
	}

	/* ---------------------------------------------------------------------
	 * Admin — meta box
	 * ------------------------------------------------------------------ */

	/**
	 * Enqueue scoped admin styles on the room edit screen only.
	 */
	public static function enqueue_admin_styles( $hook ) {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type ) {
			return;
		}

		$screen = get_current_screen();
		if ( $screen && $screen->post_type === $room_post_type && in_array( $hook, array( 'post.php', 'post-new.php' ), true ) ) {
			wp_add_inline_style( 'wp-admin', self::get_meta_box_styles() );
		}
	}

	/**
	 * Inline styles for the meta box. Scoped to .bw-schema-room-fields so they
	 * can't leak into the team-member box or any other screen.
	 */
	private static function get_meta_box_styles() {
		return '
			.bw-schema-room-fields .bw-schema-section {
				margin-bottom: 20px;
				padding-bottom: 20px;
				border-bottom: 1px solid #ddd;
			}
			.bw-schema-room-fields .bw-schema-section:last-child {
				border-bottom: none;
				margin-bottom: 0;
				padding-bottom: 0;
			}
			.bw-schema-room-fields .bw-schema-section-title {
				font-size: 14px;
				font-weight: 600;
				margin: 0 0 12px 0;
				color: #1d2327;
			}
			.bw-schema-room-fields .bw-schema-field {
				margin-bottom: 12px;
			}
			.bw-schema-room-fields .bw-schema-field label {
				display: block;
				font-weight: 500;
				margin-bottom: 4px;
			}
			.bw-schema-room-fields .bw-schema-field-row {
				display: flex;
				gap: 16px;
				flex-wrap: wrap;
			}
			.bw-schema-room-fields .bw-schema-field-row .bw-schema-field {
				flex: 1;
				min-width: 140px;
			}
			.bw-schema-room-fields .bw-schema-core-field {
				margin-bottom: 16px;
				padding: 12px;
				background: #f9f9f9;
				border-radius: 4px;
				border-left: 3px solid #2271b1;
			}
			.bw-schema-room-fields .bw-schema-core-field:last-child {
				margin-bottom: 0;
			}
			.bw-schema-room-fields .bw-schema-core-field-header {
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 8px;
				flex-wrap: wrap;
				gap: 8px;
			}
			.bw-schema-room-fields .bw-schema-core-field-header label {
				margin: 0;
			}
			.bw-schema-room-fields .bw-schema-detected-value {
				color: #50575e;
				font-size: 13px;
				display: flex;
				align-items: center;
				gap: 4px;
			}
			.bw-schema-room-fields .bw-schema-source {
				color: #787c82;
				font-size: 12px;
				font-style: italic;
			}
			.bw-schema-room-fields .bw-schema-not-detected {
				color: #996800;
			}
			.bw-schema-room-fields .bw-schema-override-row input,
			.bw-schema-room-fields .bw-schema-override-row textarea {
				background: #fff;
			}
			.bw-schema-room-fields .bw-schema-override-row input::placeholder,
			.bw-schema-room-fields .bw-schema-override-row textarea::placeholder {
				color: #a7aaad;
				font-style: italic;
			}
			.bw-schema-room-fields .bw-schema-repeater-item {
				display: flex;
				gap: 8px;
				margin-bottom: 8px;
				align-items: center;
			}
			.bw-schema-room-fields .bw-schema-repeater-item input,
			.bw-schema-room-fields .bw-schema-repeater-item select {
				flex: 1;
			}
			.bw-schema-room-fields .bw-schema-repeater-item .bw-schema-bed-count {
				max-width: 90px;
				flex: 0 0 90px;
			}
			.bw-schema-room-fields .bw-schema-remove-item {
				color: #b32d2e;
				cursor: pointer;
				padding: 4px;
			}
			.bw-schema-room-fields .bw-schema-remove-item:hover {
				color: #a00;
			}
			.bw-schema-room-fields .bw-schema-rate-warning {
				background: #fcf9e8;
				border-left: 4px solid #dba617;
				padding: 10px 12px;
				margin-bottom: 12px;
			}
		';
	}

	/**
	 * Register the meta box on the configured room CPT.
	 */
	public static function add_room_meta_box() {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type ) {
			return;
		}

		add_meta_box(
			'bw_schema_room_fields',
			__( 'Accommodation Schema Settings', 'bw-ai-schema-pro' ),
			array( __CLASS__, 'render_meta_box' ),
			$room_post_type,
			'normal',
			'default'
		);
	}

	/**
	 * Render the room meta box.
	 */
	public static function render_meta_box( $post ) {
		wp_nonce_field( 'bw_schema_room', 'bw_schema_room_nonce' );

		$settings = self::get_module_settings();

		$room_type  = get_post_meta( $post->ID, '_bw_schema_room_type', true );
		if ( empty( $room_type ) ) {
			$room_type = 'HotelRoom';
		}
		$exclude    = get_post_meta( $post->ID, '_bw_schema_room_exclude', true );

		$override_name        = get_post_meta( $post->ID, '_bw_schema_room_override_name', true );
		$override_description = get_post_meta( $post->ID, '_bw_schema_room_override_description', true );
		$override_image       = get_post_meta( $post->ID, '_bw_schema_room_override_image', true );
		$override_url         = get_post_meta( $post->ID, '_bw_schema_room_override_url', true );

		$detected_name        = $post->post_title;
		$detected_description = get_the_excerpt( $post );
		$detected_image_id    = get_post_thumbnail_id( $post->ID );
		$detected_image_url   = $detected_image_id ? wp_get_attachment_image_url( $detected_image_id, 'thumbnail' ) : '';
		$detected_url         = get_permalink( $post );

		$occupancy = get_post_meta( $post->ID, '_bw_schema_room_occupancy', true );
		if ( ! is_array( $occupancy ) ) {
			$occupancy = array();
		}

		$beds = get_post_meta( $post->ID, '_bw_schema_room_beds', true );
		if ( ! is_array( $beds ) || empty( $beds ) ) {
			$beds = array( array( 'type' => '', 'count' => '' ) );
		}

		$layout = get_post_meta( $post->ID, '_bw_schema_room_layout', true );
		if ( ! is_array( $layout ) ) {
			$layout = array();
		}

		$amenities = get_post_meta( $post->ID, '_bw_schema_room_amenities', true );
		if ( ! is_array( $amenities ) || empty( $amenities ) ) {
			$amenities = array( '' );
		}

		$policies = get_post_meta( $post->ID, '_bw_schema_room_policies', true );
		if ( ! is_array( $policies ) ) {
			$policies = array();
		}

		$offer = get_post_meta( $post->ID, '_bw_schema_room_offer', true );
		if ( ! is_array( $offer ) ) {
			$offer = array();
		}

		$floorplan_image = get_post_meta( $post->ID, '_bw_schema_room_floorplan_image', true );
		$tour_page       = get_post_meta( $post->ID, '_bw_schema_room_tour_booking_page', true );

		$video = get_post_meta( $post->ID, '_bw_schema_room_video', true );
		if ( ! is_array( $video ) ) {
			$video = array();
		}

		$bed_types  = self::get_bed_types();
		$area_units = self::get_area_units();
		$rate_units = self::get_rate_units();
		?>
		<div class="bw-schema-room-fields">

			<!-- Room Type -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Accommodation Type', 'bw-ai-schema-pro' ); ?></h4>
				<div class="bw-schema-field">
					<label for="bw_schema_room_type"><?php esc_html_e( 'Schema.org type', 'bw-ai-schema-pro' ); ?></label>
					<select id="bw_schema_room_type" name="bw_schema_room_type" class="widefat">
						<?php foreach ( self::get_room_types() as $value => $label ) : ?>
							<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $room_type, $value ); ?>><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php esc_html_e( 'Use Suite for multi-room units, House/Villa for standalone properties.', 'bw-ai-schema-pro' ); ?></p>
				</div>

				<div class="bw-schema-field">
					<label>
						<input type="checkbox" name="bw_schema_room_exclude" value="1" <?php checked( $exclude, '1' ); ?> />
						<strong><?php esc_html_e( 'Exclude from schema', 'bw-ai-schema-pro' ); ?></strong>
					</label>
					<p class="description"><?php esc_html_e( 'Keeps the page visible but emits no room schema and drops it from the property\'s containsPlace list. Use for placeholder or "coming soon" entries.', 'bw-ai-schema-pro' ); ?></p>
				</div>
			</div>

			<!-- Core Fields -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Core Fields', 'bw-ai-schema-pro' ); ?></h4>
				<p class="description" style="margin-bottom: 12px;">
					<?php esc_html_e( 'Auto-detected from the post. Fill an override only when the schema should differ from what the page shows.', 'bw-ai-schema-pro' ); ?>
				</p>

				<div class="bw-schema-core-field">
					<div class="bw-schema-core-field-header">
						<label><strong><?php esc_html_e( 'Name', 'bw-ai-schema-pro' ); ?></strong></label>
						<span class="bw-schema-detected-value">
							<?php echo esc_html( $detected_name ); ?>
							<span class="bw-schema-source"><?php esc_html_e( '(Post Title)', 'bw-ai-schema-pro' ); ?></span>
						</span>
					</div>
					<div class="bw-schema-override-row">
						<input type="text" name="bw_schema_room_override_name" value="<?php echo esc_attr( $override_name ); ?>" placeholder="<?php esc_attr_e( 'Override name…', 'bw-ai-schema-pro' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-core-field">
					<div class="bw-schema-core-field-header">
						<label><strong><?php esc_html_e( 'Description', 'bw-ai-schema-pro' ); ?></strong></label>
						<span class="bw-schema-detected-value">
							<?php if ( ! empty( $detected_description ) ) : ?>
								<?php echo esc_html( wp_trim_words( $detected_description, 10, '…' ) ); ?>
								<span class="bw-schema-source"><?php esc_html_e( '(Excerpt)', 'bw-ai-schema-pro' ); ?></span>
							<?php else : ?>
								<em class="bw-schema-not-detected"><?php esc_html_e( 'No excerpt set', 'bw-ai-schema-pro' ); ?></em>
							<?php endif; ?>
						</span>
					</div>
					<div class="bw-schema-override-row">
						<textarea name="bw_schema_room_override_description" rows="2" placeholder="<?php esc_attr_e( 'Override description…', 'bw-ai-schema-pro' ); ?>" class="widefat"><?php echo esc_textarea( $override_description ); ?></textarea>
					</div>
				</div>

				<div class="bw-schema-core-field">
					<div class="bw-schema-core-field-header">
						<label><strong><?php esc_html_e( 'Image', 'bw-ai-schema-pro' ); ?></strong></label>
						<span class="bw-schema-detected-value">
							<?php if ( ! empty( $detected_image_url ) ) : ?>
								<img src="<?php echo esc_url( $detected_image_url ); ?>" alt="" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px; vertical-align: middle; margin-right: 8px;" />
								<span class="bw-schema-source"><?php esc_html_e( '(Featured Image)', 'bw-ai-schema-pro' ); ?></span>
							<?php else : ?>
								<em class="bw-schema-not-detected"><?php esc_html_e( 'No featured image', 'bw-ai-schema-pro' ); ?></em>
							<?php endif; ?>
						</span>
					</div>
					<div class="bw-schema-override-row">
						<input type="url" name="bw_schema_room_override_image" value="<?php echo esc_url( $override_image ); ?>" placeholder="<?php esc_attr_e( 'Override image URL…', 'bw-ai-schema-pro' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-core-field">
					<div class="bw-schema-core-field-header">
						<label><strong><?php esc_html_e( 'URL', 'bw-ai-schema-pro' ); ?></strong></label>
						<span class="bw-schema-detected-value">
							<?php echo esc_html( $detected_url ); ?>
							<span class="bw-schema-source"><?php esc_html_e( '(Permalink)', 'bw-ai-schema-pro' ); ?></span>
						</span>
					</div>
					<div class="bw-schema-override-row">
						<input type="url" name="bw_schema_room_override_url" value="<?php echo esc_url( $override_url ); ?>" placeholder="<?php esc_attr_e( 'Override URL…', 'bw-ai-schema-pro' ); ?>" class="widefat" />
					</div>
				</div>
			</div>

			<!-- Capacity -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Capacity (occupancy)', 'bw-ai-schema-pro' ); ?></h4>
				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_occupancy_max"><?php esc_html_e( 'Maximum guests', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_occupancy_max" name="bw_schema_room_occupancy[max]" value="<?php echo esc_attr( $occupancy['max'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_occupancy_min"><?php esc_html_e( 'Minimum guests (optional)', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_occupancy_min" name="bw_schema_room_occupancy[min]" value="<?php echo esc_attr( $occupancy['min'] ?? '' ); ?>" class="widefat" />
					</div>
				</div>
			</div>

			<!-- Beds -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Bed Configuration', 'bw-ai-schema-pro' ); ?></h4>
				<p class="description" style="margin-bottom: 12px;">
					<?php esc_html_e( 'Each row becomes a BedDetails entry. Bed type is free text — the list is a shortcut.', 'bw-ai-schema-pro' ); ?>
				</p>

				<div class="bw-schema-repeater" data-field="beds">
					<?php foreach ( $beds as $bed ) : ?>
					<div class="bw-schema-repeater-item">
						<input type="text" name="bw_schema_room_beds[type][]" value="<?php echo esc_attr( $bed['type'] ?? '' ); ?>" list="bw_schema_bed_types" placeholder="<?php esc_attr_e( 'e.g. King', 'bw-ai-schema-pro' ); ?>" class="widefat" />
						<input type="number" min="0" step="1" class="bw-schema-bed-count" name="bw_schema_room_beds[count][]" value="<?php echo esc_attr( $bed['count'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'Qty', 'bw-ai-schema-pro' ); ?>" />
						<span class="bw-schema-remove-item dashicons dashicons-no-alt" title="<?php esc_attr_e( 'Remove', 'bw-ai-schema-pro' ); ?>"></span>
					</div>
					<?php endforeach; ?>
				</div>
				<datalist id="bw_schema_bed_types">
					<?php foreach ( $bed_types as $bt ) : ?>
						<option value="<?php echo esc_attr( $bt ); ?>"></option>
					<?php endforeach; ?>
				</datalist>
				<button type="button" class="button bw-schema-add-item" data-field="beds">
					<span class="dashicons dashicons-plus-alt2"></span> <?php esc_html_e( 'Add Bed', 'bw-ai-schema-pro' ); ?>
				</button>
			</div>

			<!-- Layout -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Layout & Size', 'bw-ai-schema-pro' ); ?></h4>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_bedrooms"><?php esc_html_e( 'Bedrooms', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_bedrooms" name="bw_schema_room_layout[bedrooms]" value="<?php echo esc_attr( $layout['bedrooms'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_rooms"><?php esc_html_e( 'Rooms (excl. bathrooms)', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_rooms" name="bw_schema_room_layout[rooms]" value="<?php echo esc_attr( $layout['rooms'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_floor_level"><?php esc_html_e( 'Floor level', 'bw-ai-schema-pro' ); ?></label>
						<input type="text" id="bw_schema_room_floor_level" name="bw_schema_room_layout[floor_level]" value="<?php echo esc_attr( $layout['floor_level'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'e.g. 3, Ground', 'bw-ai-schema-pro' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_baths_total"><?php esc_html_e( 'Bathrooms (total)', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_baths_total" name="bw_schema_room_layout[bathrooms_total]" value="<?php echo esc_attr( $layout['bathrooms_total'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_baths_full"><?php esc_html_e( 'Full bathrooms', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_baths_full" name="bw_schema_room_layout[bathrooms_full]" value="<?php echo esc_attr( $layout['bathrooms_full'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_baths_partial"><?php esc_html_e( 'Half bathrooms', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="1" id="bw_schema_room_baths_partial" name="bw_schema_room_layout[bathrooms_partial]" value="<?php echo esc_attr( $layout['bathrooms_partial'] ?? '' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_floor_size"><?php esc_html_e( 'Floor size', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="any" id="bw_schema_room_floor_size" name="bw_schema_room_layout[floor_size]" value="<?php echo esc_attr( $layout['floor_size'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_floor_size_unit"><?php esc_html_e( 'Unit', 'bw-ai-schema-pro' ); ?></label>
						<select id="bw_schema_room_floor_size_unit" name="bw_schema_room_layout[floor_size_unit]" class="widefat">
							<?php foreach ( $area_units as $code => $label ) : ?>
								<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $layout['floor_size_unit'] ?? 'FTK', $code ); ?>><?php echo esc_html( $label ); ?></option>
							<?php endforeach; ?>
						</select>
					</div>
				</div>

				<div class="bw-schema-field">
					<label for="bw_schema_room_floorplan_image"><?php esc_html_e( 'Floor plan image URL (optional)', 'bw-ai-schema-pro' ); ?></label>
					<input type="url" id="bw_schema_room_floorplan_image" name="bw_schema_room_floorplan_image" value="<?php echo esc_url( $floorplan_image ); ?>" placeholder="https://…" class="widefat" />
					<p class="description"><?php esc_html_e( 'Emits accommodationFloorPlan built from the layout numbers above.', 'bw-ai-schema-pro' ); ?></p>
				</div>
			</div>

			<!-- Amenities -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Amenities (amenityFeature)', 'bw-ai-schema-pro' ); ?></h4>
				<p class="description" style="margin-bottom: 12px;">
					<?php esc_html_e( 'One per line item — private pool, ocean view, kitchenette, air conditioning.', 'bw-ai-schema-pro' ); ?>
				</p>

				<div class="bw-schema-repeater" data-field="amenities">
					<?php foreach ( $amenities as $amenity ) : ?>
					<div class="bw-schema-repeater-item">
						<input type="text" name="bw_schema_room_amenities[]" value="<?php echo esc_attr( $amenity ); ?>" placeholder="<?php esc_attr_e( 'e.g. Private plunge pool', 'bw-ai-schema-pro' ); ?>" class="widefat" />
						<span class="bw-schema-remove-item dashicons dashicons-no-alt" title="<?php esc_attr_e( 'Remove', 'bw-ai-schema-pro' ); ?>"></span>
					</div>
					<?php endforeach; ?>
				</div>
				<button type="button" class="button bw-schema-add-item" data-field="amenities">
					<span class="dashicons dashicons-plus-alt2"></span> <?php esc_html_e( 'Add Amenity', 'bw-ai-schema-pro' ); ?>
				</button>
			</div>

			<!-- Policies -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Policies', 'bw-ai-schema-pro' ); ?></h4>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_pets"><?php esc_html_e( 'Pets allowed', 'bw-ai-schema-pro' ); ?></label>
						<select id="bw_schema_room_pets" name="bw_schema_room_policies[pets]" class="widefat">
							<option value="" <?php selected( $policies['pets'] ?? '', '' ); ?>><?php esc_html_e( '— Not stated —', 'bw-ai-schema-pro' ); ?></option>
							<option value="yes" <?php selected( $policies['pets'] ?? '', 'yes' ); ?>><?php esc_html_e( 'Yes', 'bw-ai-schema-pro' ); ?></option>
							<option value="no" <?php selected( $policies['pets'] ?? '', 'no' ); ?>><?php esc_html_e( 'No', 'bw-ai-schema-pro' ); ?></option>
						</select>
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_smoking"><?php esc_html_e( 'Smoking allowed', 'bw-ai-schema-pro' ); ?></label>
						<select id="bw_schema_room_smoking" name="bw_schema_room_policies[smoking]" class="widefat">
							<option value="" <?php selected( $policies['smoking'] ?? '', '' ); ?>><?php esc_html_e( '— Not stated —', 'bw-ai-schema-pro' ); ?></option>
							<option value="yes" <?php selected( $policies['smoking'] ?? '', 'yes' ); ?>><?php esc_html_e( 'Yes', 'bw-ai-schema-pro' ); ?></option>
							<option value="no" <?php selected( $policies['smoking'] ?? '', 'no' ); ?>><?php esc_html_e( 'No', 'bw-ai-schema-pro' ); ?></option>
						</select>
					</div>
				</div>

				<div class="bw-schema-field">
					<label for="bw_schema_room_permitted_usage"><?php esc_html_e( 'Permitted usage (optional)', 'bw-ai-schema-pro' ); ?></label>
					<input type="text" id="bw_schema_room_permitted_usage" name="bw_schema_room_policies[permitted_usage]" value="<?php echo esc_attr( $policies['permitted_usage'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'e.g. Maximum 6 guests including children', 'bw-ai-schema-pro' ); ?>" class="widefat" />
				</div>
			</div>

			<!-- Rates -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Rates (offers)', 'bw-ai-schema-pro' ); ?></h4>
				<?php if ( self::rate_is_stale( $post->ID ) ) : ?>
					<div class="bw-schema-rate-warning">
						<strong><?php esc_html_e( 'This rate is stale.', 'bw-ai-schema-pro' ); ?></strong>
						<?php
						printf(
							/* translators: %d: number of days */
							esc_html__( 'It was last confirmed more than %d days ago, so the offers block is being withheld from the schema output. Re-confirm the date below to publish it again.', 'bw-ai-schema-pro' ),
							(int) $settings['rate_stale_days']
						);
						?>
					</div>
				<?php endif; ?>
				<p class="description" style="margin-bottom: 12px;">
					<?php esc_html_e( 'Published as a starting rate (minPrice), never as a bookable price. Leave blank to omit pricing entirely.', 'bw-ai-schema-pro' ); ?>
				</p>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_from_price"><?php esc_html_e( 'From rate', 'bw-ai-schema-pro' ); ?></label>
						<input type="number" min="0" step="any" id="bw_schema_room_from_price" name="bw_schema_room_offer[from_price]" value="<?php echo esc_attr( $offer['from_price'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_currency"><?php esc_html_e( 'Currency', 'bw-ai-schema-pro' ); ?></label>
						<input type="text" maxlength="3" id="bw_schema_room_currency" name="bw_schema_room_offer[currency]" value="<?php echo esc_attr( $offer['currency'] ?? $settings['currency'] ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_rate_unit"><?php esc_html_e( 'Per', 'bw-ai-schema-pro' ); ?></label>
						<select id="bw_schema_room_rate_unit" name="bw_schema_room_offer[unit]" class="widefat">
							<?php foreach ( $rate_units as $code => $label ) : ?>
								<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $offer['unit'] ?? $settings['rate_unit'], $code ); ?>><?php echo esc_html( $label ); ?></option>
							<?php endforeach; ?>
						</select>
					</div>
				</div>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_availability"><?php esc_html_e( 'Availability', 'bw-ai-schema-pro' ); ?></label>
						<select id="bw_schema_room_availability" name="bw_schema_room_offer[availability]" class="widefat">
							<option value="" <?php selected( $offer['availability'] ?? '', '' ); ?>><?php esc_html_e( '— Not stated —', 'bw-ai-schema-pro' ); ?></option>
							<option value="InStock" <?php selected( $offer['availability'] ?? '', 'InStock' ); ?>><?php esc_html_e( 'Available', 'bw-ai-schema-pro' ); ?></option>
							<option value="LimitedAvailability" <?php selected( $offer['availability'] ?? '', 'LimitedAvailability' ); ?>><?php esc_html_e( 'Limited availability', 'bw-ai-schema-pro' ); ?></option>
							<option value="SoldOut" <?php selected( $offer['availability'] ?? '', 'SoldOut' ); ?>><?php esc_html_e( 'Sold out', 'bw-ai-schema-pro' ); ?></option>
						</select>
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_rate_reviewed"><?php esc_html_e( 'Rate last confirmed', 'bw-ai-schema-pro' ); ?></label>
						<input type="date" id="bw_schema_room_rate_reviewed" name="bw_schema_room_offer[reviewed]" value="<?php echo esc_attr( $offer['reviewed'] ?? '' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-field">
					<label for="bw_schema_room_booking_url"><?php esc_html_e( 'Booking URL', 'bw-ai-schema-pro' ); ?></label>
					<input type="url" id="bw_schema_room_booking_url" name="bw_schema_room_offer[booking_url]" value="<?php echo esc_url( $offer['booking_url'] ?? '' ); ?>" placeholder="https://…" class="widefat" />
				</div>

				<div class="bw-schema-field">
					<label for="bw_schema_room_tour_booking_page"><?php esc_html_e( 'Tour / viewing page (tourBookingPage)', 'bw-ai-schema-pro' ); ?></label>
					<input type="url" id="bw_schema_room_tour_booking_page" name="bw_schema_room_tour_booking_page" value="<?php echo esc_url( $tour_page ); ?>" placeholder="https://…" class="widefat" />
				</div>
			</div>

			<!-- Video -->
			<div class="bw-schema-section">
				<h4 class="bw-schema-section-title"><?php esc_html_e( 'Video (subjectOf)', 'bw-ai-schema-pro' ); ?></h4>
				<p class="description" style="margin-bottom: 12px;">
					<?php esc_html_e( 'A room tour video. Generates a VideoObject in the schema.', 'bw-ai-schema-pro' ); ?>
				</p>

				<div class="bw-schema-field">
					<label for="bw_schema_room_video_name"><?php esc_html_e( 'Video title', 'bw-ai-schema-pro' ); ?></label>
					<input type="text" id="bw_schema_room_video_name" name="bw_schema_room_video[name]" value="<?php echo esc_attr( $video['name'] ?? '' ); ?>" placeholder="<?php esc_attr_e( 'e.g. Oceanfront Suite Tour', 'bw-ai-schema-pro' ); ?>" class="widefat" />
				</div>

				<div class="bw-schema-field">
					<label for="bw_schema_room_video_description"><?php esc_html_e( 'Video description', 'bw-ai-schema-pro' ); ?></label>
					<textarea id="bw_schema_room_video_description" name="bw_schema_room_video[description]" class="widefat" rows="2"><?php echo esc_textarea( $video['description'] ?? '' ); ?></textarea>
				</div>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_video_thumbnail"><?php esc_html_e( 'Thumbnail URL', 'bw-ai-schema-pro' ); ?></label>
						<input type="url" id="bw_schema_room_video_thumbnail" name="bw_schema_room_video[thumbnail]" value="<?php echo esc_url( $video['thumbnail'] ?? '' ); ?>" placeholder="https://…" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_video_upload_date"><?php esc_html_e( 'Upload date', 'bw-ai-schema-pro' ); ?></label>
						<input type="date" id="bw_schema_room_video_upload_date" name="bw_schema_room_video[upload_date]" value="<?php echo esc_attr( $video['upload_date'] ?? '' ); ?>" class="widefat" />
					</div>
				</div>

				<div class="bw-schema-field-row">
					<div class="bw-schema-field">
						<label for="bw_schema_room_video_content_url"><?php esc_html_e( 'Video file URL (optional)', 'bw-ai-schema-pro' ); ?></label>
						<input type="url" id="bw_schema_room_video_content_url" name="bw_schema_room_video[content_url]" value="<?php echo esc_url( $video['content_url'] ?? '' ); ?>" class="widefat" />
					</div>
					<div class="bw-schema-field">
						<label for="bw_schema_room_video_embed_url"><?php esc_html_e( 'Embed URL (YouTube/Vimeo)', 'bw-ai-schema-pro' ); ?></label>
						<input type="url" id="bw_schema_room_video_embed_url" name="bw_schema_room_video[embed_url]" value="<?php echo esc_url( $video['embed_url'] ?? '' ); ?>" class="widefat" />
					</div>
				</div>
			</div>

		</div>

		<script>
		jQuery(document).ready(function($) {
			$('.bw-schema-room-fields .bw-schema-add-item').on('click', function() {
				var field = $(this).data('field');
				var $repeater = $(this).siblings('.bw-schema-repeater[data-field="' + field + '"]');
				var $template = $repeater.find('.bw-schema-repeater-item').first().clone();
				$template.find('input').val('');
				$repeater.append($template);
			});

			$(document).on('click', '.bw-schema-room-fields .bw-schema-remove-item', function() {
				var $repeater = $(this).closest('.bw-schema-repeater');
				if ($repeater.find('.bw-schema-repeater-item').length > 1) {
					$(this).closest('.bw-schema-repeater-item').remove();
				} else {
					$(this).siblings('input').val('');
				}
			});
		});
		</script>
		<?php
	}

	/* ---------------------------------------------------------------------
	 * Admin — save
	 * ------------------------------------------------------------------ */

	/**
	 * Persist the room meta box.
	 */
	public static function save_room_schema( $post_id, $post ) {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type || $post->post_type !== $room_post_type ) {
			return;
		}

		if ( ! isset( $_POST['bw_schema_room_nonce'] ) ||
		     ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['bw_schema_room_nonce'] ) ), 'bw_schema_room' ) ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		// Room @type — only accept a value we actually offer.
		if ( isset( $_POST['bw_schema_room_type'] ) ) {
			$type = sanitize_text_field( wp_unslash( $_POST['bw_schema_room_type'] ) );
			if ( ! array_key_exists( $type, self::get_room_types() ) ) {
				$type = 'HotelRoom';
			}
			update_post_meta( $post_id, '_bw_schema_room_type', $type );
		}

		// Exclude flag.
		if ( ! empty( $_POST['bw_schema_room_exclude'] ) ) {
			update_post_meta( $post_id, '_bw_schema_room_exclude', '1' );
		} else {
			delete_post_meta( $post_id, '_bw_schema_room_exclude' );
		}

		// Core overrides.
		$overrides = array(
			'_bw_schema_room_override_name'        => array( 'bw_schema_room_override_name', 'text' ),
			'_bw_schema_room_override_description' => array( 'bw_schema_room_override_description', 'textarea' ),
			'_bw_schema_room_override_image'       => array( 'bw_schema_room_override_image', 'url' ),
			'_bw_schema_room_override_url'         => array( 'bw_schema_room_override_url', 'url' ),
		);
		foreach ( $overrides as $meta_key => $spec ) {
			list( $post_key, $kind ) = $spec;
			if ( ! isset( $_POST[ $post_key ] ) ) {
				continue;
			}
			$raw = wp_unslash( $_POST[ $post_key ] );
			switch ( $kind ) {
				case 'textarea':
					$value = sanitize_textarea_field( $raw );
					break;
				case 'url':
					$value = esc_url_raw( $raw );
					break;
				default:
					$value = sanitize_text_field( $raw );
			}
			if ( '' !== $value ) {
				update_post_meta( $post_id, $meta_key, $value );
			} else {
				delete_post_meta( $post_id, $meta_key );
			}
		}

		// Occupancy.
		if ( isset( $_POST['bw_schema_room_occupancy'] ) && is_array( $_POST['bw_schema_room_occupancy'] ) ) {
			$raw       = wp_unslash( $_POST['bw_schema_room_occupancy'] );
			$occupancy = array();
			foreach ( array( 'min', 'max' ) as $key ) {
				$value = isset( $raw[ $key ] ) ? trim( (string) $raw[ $key ] ) : '';
				if ( '' !== $value && is_numeric( $value ) && (int) $value > 0 ) {
					$occupancy[ $key ] = (int) $value;
				}
			}
			if ( ! empty( $occupancy ) ) {
				update_post_meta( $post_id, '_bw_schema_room_occupancy', $occupancy );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_occupancy' );
			}
		}

		// Beds — parallel arrays of type + count.
		if ( isset( $_POST['bw_schema_room_beds'] ) && is_array( $_POST['bw_schema_room_beds'] ) ) {
			$raw    = wp_unslash( $_POST['bw_schema_room_beds'] );
			$types  = isset( $raw['type'] ) && is_array( $raw['type'] ) ? $raw['type'] : array();
			$counts = isset( $raw['count'] ) && is_array( $raw['count'] ) ? $raw['count'] : array();
			$beds   = array();
			foreach ( $types as $index => $type ) {
				$type = sanitize_text_field( $type );
				if ( '' === $type ) {
					continue;
				}
				$count = isset( $counts[ $index ] ) && is_numeric( $counts[ $index ] ) ? (int) $counts[ $index ] : 0;
				$beds[] = array(
					'type'  => $type,
					'count' => $count > 0 ? $count : 1,
				);
			}
			if ( ! empty( $beds ) ) {
				update_post_meta( $post_id, '_bw_schema_room_beds', $beds );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_beds' );
			}
		}

		// Layout.
		if ( isset( $_POST['bw_schema_room_layout'] ) && is_array( $_POST['bw_schema_room_layout'] ) ) {
			$raw    = wp_unslash( $_POST['bw_schema_room_layout'] );
			$layout = array();
			foreach ( array( 'bedrooms', 'rooms', 'bathrooms_total', 'bathrooms_full', 'bathrooms_partial' ) as $key ) {
				$value = isset( $raw[ $key ] ) ? trim( (string) $raw[ $key ] ) : '';
				if ( '' !== $value && is_numeric( $value ) ) {
					$layout[ $key ] = (int) $value;
				}
			}
			$floor_size = isset( $raw['floor_size'] ) ? trim( (string) $raw['floor_size'] ) : '';
			if ( '' !== $floor_size && is_numeric( $floor_size ) && (float) $floor_size > 0 ) {
				$layout['floor_size'] = (float) $floor_size;
			}
			$unit = isset( $raw['floor_size_unit'] ) ? sanitize_text_field( $raw['floor_size_unit'] ) : '';
			if ( array_key_exists( $unit, self::get_area_units() ) ) {
				$layout['floor_size_unit'] = $unit;
			}
			$floor_level = isset( $raw['floor_level'] ) ? sanitize_text_field( $raw['floor_level'] ) : '';
			if ( '' !== $floor_level ) {
				$layout['floor_level'] = $floor_level;
			}

			if ( ! empty( $layout ) ) {
				update_post_meta( $post_id, '_bw_schema_room_layout', $layout );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_layout' );
			}
		}

		// Amenities.
		if ( isset( $_POST['bw_schema_room_amenities'] ) && is_array( $_POST['bw_schema_room_amenities'] ) ) {
			$amenities = array_filter( array_map( 'sanitize_text_field', wp_unslash( $_POST['bw_schema_room_amenities'] ) ) );
			if ( ! empty( $amenities ) ) {
				update_post_meta( $post_id, '_bw_schema_room_amenities', array_values( $amenities ) );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_amenities' );
			}
		}

		// Policies.
		if ( isset( $_POST['bw_schema_room_policies'] ) && is_array( $_POST['bw_schema_room_policies'] ) ) {
			$raw      = wp_unslash( $_POST['bw_schema_room_policies'] );
			$policies = array();
			foreach ( array( 'pets', 'smoking' ) as $key ) {
				$value = isset( $raw[ $key ] ) ? sanitize_text_field( $raw[ $key ] ) : '';
				if ( in_array( $value, array( 'yes', 'no' ), true ) ) {
					$policies[ $key ] = $value;
				}
			}
			$usage = isset( $raw['permitted_usage'] ) ? sanitize_text_field( $raw['permitted_usage'] ) : '';
			if ( '' !== $usage ) {
				$policies['permitted_usage'] = $usage;
			}
			if ( ! empty( $policies ) ) {
				update_post_meta( $post_id, '_bw_schema_room_policies', $policies );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_policies' );
			}
		}

		// Offer.
		if ( isset( $_POST['bw_schema_room_offer'] ) && is_array( $_POST['bw_schema_room_offer'] ) ) {
			$raw   = wp_unslash( $_POST['bw_schema_room_offer'] );
			$offer = array();

			$price = isset( $raw['from_price'] ) ? trim( (string) $raw['from_price'] ) : '';
			if ( '' !== $price && is_numeric( $price ) && (float) $price > 0 ) {
				$offer['from_price'] = (float) $price;
			}

			$currency = isset( $raw['currency'] ) ? strtoupper( sanitize_text_field( $raw['currency'] ) ) : '';
			if ( preg_match( '/^[A-Z]{3}$/', $currency ) ) {
				$offer['currency'] = $currency;
			}

			$unit = isset( $raw['unit'] ) ? sanitize_text_field( $raw['unit'] ) : '';
			if ( array_key_exists( $unit, self::get_rate_units() ) ) {
				$offer['unit'] = $unit;
			}

			$availability = isset( $raw['availability'] ) ? sanitize_text_field( $raw['availability'] ) : '';
			if ( in_array( $availability, array( 'InStock', 'LimitedAvailability', 'SoldOut' ), true ) ) {
				$offer['availability'] = $availability;
			}

			$booking_url = isset( $raw['booking_url'] ) ? esc_url_raw( $raw['booking_url'] ) : '';
			if ( '' !== $booking_url ) {
				$offer['booking_url'] = $booking_url;
			}

			$reviewed = isset( $raw['reviewed'] ) ? sanitize_text_field( $raw['reviewed'] ) : '';
			if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $reviewed ) ) {
				$offer['reviewed'] = $reviewed;
			}

			if ( ! empty( $offer ) ) {
				update_post_meta( $post_id, '_bw_schema_room_offer', $offer );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_offer' );
			}
		}

		// Floor plan image + tour booking page.
		foreach ( array(
			'_bw_schema_room_floorplan_image'    => 'bw_schema_room_floorplan_image',
			'_bw_schema_room_tour_booking_page'  => 'bw_schema_room_tour_booking_page',
		) as $meta_key => $post_key ) {
			if ( ! isset( $_POST[ $post_key ] ) ) {
				continue;
			}
			$value = esc_url_raw( wp_unslash( $_POST[ $post_key ] ) );
			if ( '' !== $value ) {
				update_post_meta( $post_id, $meta_key, $value );
			} else {
				delete_post_meta( $post_id, $meta_key );
			}
		}

		// Video.
		if ( isset( $_POST['bw_schema_room_video'] ) && is_array( $_POST['bw_schema_room_video'] ) ) {
			$raw   = wp_unslash( $_POST['bw_schema_room_video'] );
			$video = array(
				'name'        => isset( $raw['name'] ) ? sanitize_text_field( $raw['name'] ) : '',
				'description' => isset( $raw['description'] ) ? sanitize_textarea_field( $raw['description'] ) : '',
				'thumbnail'   => isset( $raw['thumbnail'] ) ? esc_url_raw( $raw['thumbnail'] ) : '',
				'upload_date' => isset( $raw['upload_date'] ) ? sanitize_text_field( $raw['upload_date'] ) : '',
				'content_url' => isset( $raw['content_url'] ) ? esc_url_raw( $raw['content_url'] ) : '',
				'embed_url'   => isset( $raw['embed_url'] ) ? esc_url_raw( $raw['embed_url'] ) : '',
			);

			if ( '' !== $video['name'] ) {
				update_post_meta( $post_id, '_bw_schema_room_video', $video );
			} else {
				delete_post_meta( $post_id, '_bw_schema_room_video' );
			}
		}

		// The room's data feeds the Organization node's containsPlace[], which is
		// cached per-post elsewhere — clear so the next render rebuilds.
		if ( class_exists( 'BW_Schema_Cache' ) ) {
			BW_Schema_Cache::delete( BW_Schema_Cache::get_cache_key( 'post', $post_id ) );
		}
	}

	/* ---------------------------------------------------------------------
	 * Rate staleness
	 * ------------------------------------------------------------------ */

	/**
	 * Whether a room's rate is older than the configured staleness window.
	 *
	 * A room with a price but NO confirmation date counts as stale — otherwise a
	 * rate entered once in 2024 would publish forever with nothing prompting a
	 * review. A room with no price at all is never stale (nothing to go wrong).
	 *
	 * @param int $post_id Room post ID.
	 * @return bool
	 */
	public static function rate_is_stale( $post_id ) {
		$offer = get_post_meta( (int) $post_id, '_bw_schema_room_offer', true );

		if ( ! is_array( $offer ) || empty( $offer['from_price'] ) ) {
			return false;
		}

		$settings = self::get_module_settings();
		$window   = (int) $settings['rate_stale_days'];

		if ( $window <= 0 ) {
			return false;
		}

		if ( empty( $offer['reviewed'] ) ) {
			return true;
		}

		$reviewed = strtotime( $offer['reviewed'] );
		if ( ! $reviewed ) {
			return true;
		}

		return ( time() - $reviewed ) > ( $window * DAY_IN_SECONDS );
	}

	/**
	 * Admin notice listing rooms whose rates have gone stale. Shown on the room
	 * CPT list screen only, so it can't nag on unrelated screens.
	 */
	public static function maybe_show_stale_rate_notice() {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type || ! current_user_can( 'edit_posts' ) ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || 'edit' !== $screen->base || $screen->post_type !== $room_post_type ) {
			return;
		}

		$stale = array();
		foreach ( self::get_room_ids() as $room_id ) {
			if ( self::rate_is_stale( $room_id ) ) {
				$stale[] = get_the_title( $room_id );
			}
		}

		if ( empty( $stale ) ) {
			return;
		}

		printf(
			'<div class="notice notice-warning"><p><strong>%s</strong> %s</p><p>%s</p></div>',
			esc_html__( 'BW AI Schema Pro:', 'bw-ai-schema-pro' ),
			esc_html(
				sprintf(
					/* translators: %d: number of rooms */
					_n(
						'%d room has a stale rate — its offers block is being withheld from the schema output.',
						'%d rooms have stale rates — their offers blocks are being withheld from the schema output.',
						count( $stale ),
						'bw-ai-schema-pro'
					),
					count( $stale )
				)
			),
			esc_html( implode( ', ', $stale ) )
		);
	}

	/* ---------------------------------------------------------------------
	 * Data provider — the single source every consumer reads
	 * ------------------------------------------------------------------ */

	/**
	 * Whether a room is flagged out of the schema graph.
	 *
	 * @param int $room_id Room post ID.
	 * @return bool
	 */
	public static function is_excluded( $room_id ) {
		return '1' === (string) get_post_meta( (int) $room_id, '_bw_schema_room_exclude', true );
	}

	/**
	 * Stable @id for a room node.
	 *
	 * @param WP_Post|int $room Room post or ID.
	 * @return string
	 */
	public static function id_for( $room ) {
		return get_permalink( $room ) . self::ID_FRAGMENT;
	}

	/**
	 * All published room IDs, in menu order.
	 *
	 * @return int[]
	 */
	public static function get_room_ids() {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type ) {
			return array();
		}

		return get_posts( array(
			'post_type'      => $room_post_type,
			'posts_per_page' => -1,
			'post_status'    => 'publish',
			'orderby'        => 'menu_order title',
			'order'          => 'ASC',
			'fields'         => 'ids',
		) );
	}

	/**
	 * Build the full room schema data for a room post.
	 *
	 * This is THE builder — the single-room page node, the settings table, and
	 * Organization.containsPlace[] all derive from it. Returns an empty array for
	 * an excluded or missing room.
	 *
	 * @param WP_Post|int $room Room post or ID.
	 * @return array
	 */
	public static function get_room_schema_data( $room ) {
		if ( is_numeric( $room ) ) {
			$room = get_post( $room );
		}

		if ( ! $room || self::is_excluded( $room->ID ) ) {
			return array();
		}

		$room_type = get_post_meta( $room->ID, '_bw_schema_room_type', true );
		if ( ! array_key_exists( $room_type, self::get_room_types() ) ) {
			$room_type = 'HotelRoom';
		}

		$override_name        = get_post_meta( $room->ID, '_bw_schema_room_override_name', true );
		$override_description = get_post_meta( $room->ID, '_bw_schema_room_override_description', true );
		$override_image       = get_post_meta( $room->ID, '_bw_schema_room_override_image', true );
		$override_url         = get_post_meta( $room->ID, '_bw_schema_room_override_url', true );

		$data = array(
			'@type' => $room_type,
			'@id'   => self::id_for( $room ),
			'name'  => ! empty( $override_name ) ? $override_name : $room->post_title,
			'url'   => ! empty( $override_url ) ? $override_url : get_permalink( $room ),
		);

		$description = ! empty( $override_description ) ? $override_description : get_the_excerpt( $room );
		if ( ! empty( $description ) ) {
			$data['description'] = wp_strip_all_tags( $description );
		}

		$image = ! empty( $override_image ) ? $override_image : get_the_post_thumbnail_url( $room, 'full' );
		if ( ! empty( $image ) ) {
			$data['image'] = $image;
		}

		// containedInPlace — the site Organization is the property (see SPEC).
		$data['containedInPlace'] = class_exists( 'BW_Schema_Org_Builder' )
			? BW_Schema_Org_Builder::reference()
			: array( '@id' => home_url( '/' ) . '#organization' );

		// Occupancy — QuantitativeValue, unitCode C62 (each/person).
		$occupancy = get_post_meta( $room->ID, '_bw_schema_room_occupancy', true );
		if ( is_array( $occupancy ) && ( ! empty( $occupancy['max'] ) || ! empty( $occupancy['min'] ) ) ) {
			$node = array(
				'@type'    => 'QuantitativeValue',
				'unitCode' => 'C62',
			);
			if ( ! empty( $occupancy['min'] ) ) {
				$node['minValue'] = (int) $occupancy['min'];
			}
			if ( ! empty( $occupancy['max'] ) ) {
				$node['maxValue'] = (int) $occupancy['max'];
			}
			$data['occupancy'] = $node;
		}

		// Beds — BedDetails per configured row.
		$beds = get_post_meta( $room->ID, '_bw_schema_room_beds', true );
		if ( is_array( $beds ) && ! empty( $beds ) ) {
			$bed_nodes = array();
			foreach ( $beds as $bed ) {
				if ( empty( $bed['type'] ) ) {
					continue;
				}
				$bed_nodes[] = array(
					'@type'        => 'BedDetails',
					'typeOfBed'    => $bed['type'],
					'numberOfBeds' => ! empty( $bed['count'] ) ? (int) $bed['count'] : 1,
				);
			}
			if ( ! empty( $bed_nodes ) ) {
				$data['bed'] = $bed_nodes;
			}
		}

		// Layout.
		$layout     = get_post_meta( $room->ID, '_bw_schema_room_layout', true );
		$floor_size = null;
		if ( is_array( $layout ) ) {
			$map = array(
				'bedrooms'          => 'numberOfBedrooms',
				'rooms'             => 'numberOfRooms',
				'bathrooms_total'   => 'numberOfBathroomsTotal',
				'bathrooms_full'    => 'numberOfFullBathrooms',
				'bathrooms_partial' => 'numberOfPartialBathrooms',
			);
			foreach ( $map as $src => $dst ) {
				if ( isset( $layout[ $src ] ) && '' !== $layout[ $src ] ) {
					$data[ $dst ] = (int) $layout[ $src ];
				}
			}

			if ( ! empty( $layout['floor_size'] ) ) {
				$floor_size = array(
					'@type'    => 'QuantitativeValue',
					'value'    => (float) $layout['floor_size'],
					'unitCode' => ! empty( $layout['floor_size_unit'] ) ? $layout['floor_size_unit'] : 'FTK',
				);
				$data['floorSize'] = $floor_size;
			}

			if ( ! empty( $layout['floor_level'] ) ) {
				$data['floorLevel'] = $layout['floor_level'];
			}
		}

		// Amenities.
		$amenities = get_post_meta( $room->ID, '_bw_schema_room_amenities', true );
		if ( is_array( $amenities ) && ! empty( $amenities ) ) {
			$features = array();
			foreach ( $amenities as $amenity ) {
				if ( '' === trim( (string) $amenity ) ) {
					continue;
				}
				$features[] = array(
					'@type' => 'LocationFeatureSpecification',
					'name'  => $amenity,
					'value' => true,
				);
			}
			if ( ! empty( $features ) ) {
				$data['amenityFeature'] = $features;
			}
		}

		// Policies.
		$policies = get_post_meta( $room->ID, '_bw_schema_room_policies', true );
		if ( is_array( $policies ) ) {
			if ( isset( $policies['pets'] ) && '' !== $policies['pets'] ) {
				$data['petsAllowed'] = ( 'yes' === $policies['pets'] );
			}
			if ( isset( $policies['smoking'] ) && '' !== $policies['smoking'] ) {
				$data['smokingAllowed'] = ( 'yes' === $policies['smoking'] );
			}
			if ( ! empty( $policies['permitted_usage'] ) ) {
				$data['permittedUsage'] = $policies['permitted_usage'];
			}
		}

		// Floor plan — built from the layout numbers plus an optional image.
		$floorplan_image = get_post_meta( $room->ID, '_bw_schema_room_floorplan_image', true );
		if ( ! empty( $floorplan_image ) ) {
			$plan = array( '@type' => 'FloorPlan' );
			if ( isset( $data['numberOfBedrooms'] ) ) {
				$plan['numberOfBedrooms'] = $data['numberOfBedrooms'];
			}
			if ( isset( $data['numberOfBathroomsTotal'] ) ) {
				$plan['numberOfBathroomsTotal'] = $data['numberOfBathroomsTotal'];
			}
			if ( null !== $floor_size ) {
				$plan['floorSize'] = $floor_size;
			}
			$plan['layoutImage'] = $floorplan_image;
			$data['accommodationFloorPlan'] = $plan;
		}

		// Tour booking page.
		$tour_page = get_post_meta( $room->ID, '_bw_schema_room_tour_booking_page', true );
		if ( ! empty( $tour_page ) ) {
			$data['tourBookingPage'] = $tour_page;
		}

		// NOTE: no `offers` here. `offers` is not a valid property of
		// Accommodation (its domain is Product / Service / CreativeWork / Event /
		// MenuItem / Trip / AggregateOffer / EducationalOccupationalProgram), so
		// the relationship is inverted: build_offer_node() emits a sibling Offer
		// whose itemOffered references this room's @id. Same for datePublished /
		// dateModified — those are CreativeWork properties and have no meaning on
		// a Place. schema.org validator flagged all three (2026-07-22).

		// Video.
		$video = get_post_meta( $room->ID, '_bw_schema_room_video', true );
		if ( is_array( $video ) && ! empty( $video['name'] ) ) {
			$video_node = array(
				'@type' => 'VideoObject',
				'name'  => $video['name'],
			);
			if ( ! empty( $video['description'] ) ) {
				$video_node['description'] = $video['description'];
			}
			if ( ! empty( $video['thumbnail'] ) ) {
				$video_node['thumbnailUrl'] = $video['thumbnail'];
			}
			if ( ! empty( $video['upload_date'] ) ) {
				$video_node['uploadDate'] = $video['upload_date'] . 'T00:00:00+00:00';
			}
			if ( ! empty( $video['content_url'] ) ) {
				$video_node['contentUrl'] = $video['content_url'];
			}
			if ( ! empty( $video['embed_url'] ) ) {
				$video_node['embedUrl'] = $video['embed_url'];
			}
			$data['subjectOf'] = $video_node;
		}

		return apply_filters( 'bw_schema_room_data', $data, $room );
	}

	/**
	 * Build the standalone Offer node for a room.
	 *
	 * This is a SIBLING node in the graph, not a property of the room —
	 * `offers` is not valid on Accommodation, so the relationship runs the other
	 * way: Offer.itemOffered references the room's @id. That is also the shape
	 * Google's hotel examples use (an Offer wrapping a HotelRoom).
	 *
	 * The price is emitted as minPrice on a UnitPriceSpecification, not as
	 * Offer.price — a resort's nightly rate is seasonal, so a "from" rate is a
	 * floor, not an assertion of the price a guest will pay. The unitCode
	 * carries the per-night / per-week semantics.
	 *
	 * The price is omitted when the rate is stale, so a rate nobody has
	 * confirmed in months stops being published rather than quietly going wrong.
	 *
	 * @param WP_Post|int $room Room post or ID.
	 * @return array Offer node, or empty array when there's nothing to say.
	 */
	public static function build_offer_node( $room ) {
		if ( is_numeric( $room ) ) {
			$room = get_post( $room );
		}

		if ( ! $room || self::is_excluded( $room->ID ) ) {
			return array();
		}

		$offer = get_post_meta( $room->ID, '_bw_schema_room_offer', true );

		if ( ! is_array( $offer ) || empty( $offer ) ) {
			return array();
		}

		$settings = self::get_module_settings();
		$node     = array(
			'@type'       => 'Offer',
			'@id'         => get_permalink( $room ) . '#offer',
			'itemOffered' => array( '@id' => self::id_for( $room ) ),
		);

		if ( ! empty( $offer['availability'] ) ) {
			$node['availability'] = 'https://schema.org/' . $offer['availability'];
		}

		if ( ! empty( $offer['booking_url'] ) ) {
			$node['url'] = $offer['booking_url'];
		}

		if ( ! empty( $offer['from_price'] ) && ! self::rate_is_stale( $room->ID ) ) {
			$node['priceSpecification'] = array(
				'@type'         => 'UnitPriceSpecification',
				'minPrice'      => (float) $offer['from_price'],
				'priceCurrency' => ! empty( $offer['currency'] ) ? $offer['currency'] : $settings['currency'],
				'unitCode'      => ! empty( $offer['unit'] ) ? $offer['unit'] : $settings['rate_unit'],
			);
		}

		// An Offer with neither a price nor a booking URL says nothing — drop it.
		if ( ! isset( $node['priceSpecification'] ) && ! isset( $node['url'] ) ) {
			return array();
		}

		return apply_filters( 'bw_schema_room_offer_node', $node, $room );
	}

	/* ---------------------------------------------------------------------
	 * Graph consumers
	 * ------------------------------------------------------------------ */

	/**
	 * The node for a single-room page.
	 *
	 * Just the room. The Offer is NOT emitted here as a sibling node — a
	 * top-level Offer is referenced by nothing, which makes it an additional
	 * root of the graph, and validators that root their tree at an unreferenced
	 * node then render the property as a nested detail of the Offer instead of
	 * the headline entity. Offers hang off the Resort's makesOffer[] instead
	 * (see collect_offers()), which is both valid and keeps the property as the
	 * root. Regression caught in review, 2026-07-22.
	 *
	 * No datePublished / dateModified here either: those are CreativeWork
	 * properties, invalid on an Accommodation (which descends from Place).
	 *
	 * @param WP_Post $room Room post.
	 * @return array Node, or empty array when the room is excluded.
	 */
	public static function build_page_node( $room ) {
		$data = self::get_room_schema_data( $room );

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

		$data['mainEntityOfPage'] = get_permalink( $room );

		return apply_filters( 'bw_schema_room_page_node', $data, $room );
	}

	/**
	 * Offer nodes for every published room that has rate or booking data.
	 *
	 * Consumed by BW_Schema_Org_Builder for Organization.makesOffer[] — the
	 * property makes the offers, each pointing at its room via itemOffered.
	 *
	 * @return array
	 */
	public static function collect_offers() {
		$out = array();

		foreach ( self::get_room_ids() as $room_id ) {
			$offer = self::build_offer_node( $room_id );
			if ( ! empty( $offer ) ) {
				$out[] = $offer;
			}
		}

		return apply_filters( 'bw_schema_room_offers', $out );
	}

	/**
	 * Compact reference nodes for Organization.containsPlace[].
	 *
	 * Deliberately NOT the full room description — that lives on the room's own
	 * page under the same @id, so the graph stays connected without duplicating
	 * every field onto the homepage.
	 *
	 * @return array
	 */
	public static function collect_accommodations() {
		$out = array();

		foreach ( self::get_room_ids() as $room_id ) {
			if ( self::is_excluded( $room_id ) ) {
				continue;
			}

			$room = get_post( $room_id );
			if ( ! $room ) {
				continue;
			}

			$room_type = get_post_meta( $room_id, '_bw_schema_room_type', true );
			if ( ! array_key_exists( $room_type, self::get_room_types() ) ) {
				$room_type = 'HotelRoom';
			}

			$override_name = get_post_meta( $room_id, '_bw_schema_room_override_name', true );

			$node = array(
				'@type' => $room_type,
				'@id'   => self::id_for( $room ),
				'name'  => ! empty( $override_name ) ? $override_name : $room->post_title,
				'url'   => get_permalink( $room ),
			);

			$image = get_the_post_thumbnail_url( $room, 'thumbnail' );
			if ( ! empty( $image ) ) {
				$node['image'] = $image;
			}

			$out[] = $node;
		}

		return apply_filters( 'bw_schema_room_collection', $out );
	}

	/**
	 * Suppress competing SEO plugins' schema on single-room pages, matching what
	 * the team-member module does on profile pages. Without this, Yoast layers a
	 * generic WebPage/Article over the room node.
	 */
	public static function disable_conflicting_schemas() {
		$room_post_type = self::get_room_post_type();

		if ( ! $room_post_type || ! is_singular( $room_post_type ) ) {
			return;
		}

		// Yoast.
		add_filter( 'wpseo_json_ld_output', '__return_false', 999 );
		add_filter( 'wpseo_schema_graph', '__return_empty_array', 999 );
		add_filter( 'wpseo_schema_webpage', '__return_empty_array', 999 );

		// Rank Math.
		add_filter( 'rank_math/json_ld', '__return_empty_array', 999 );

		// All in One SEO.
		add_filter( 'aioseo_schema_output', '__return_false', 999 );

		// SEOPress.
		add_filter( 'seopress_schemas_jsonld_output', '__return_false', 999 );
	}
}
