<?php
/**
 * Pure schema readiness evaluator (Schema Architecture V2 Phase 3).
 *
 * Determines whether a requested entity is ready without emitting or mutating it.
 *
 * @package SEOvaultSEO\Schema\Readiness
 */

namespace SEOvaultSEO\Schema\Readiness;

use SEOvaultSEO\Schema\SchemaTypePolicyRegistry;
use SEOvaultSEO\Schema\Validation\ValueInspector;

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

/**
 * Type-specific readiness policies from the V2 specification.
 */
class SchemaReadinessEvaluator {

	const POLICY_VERSION = '2026.07.1';

	/**
	 * Evaluate readiness for a schema type and field payload.
	 *
	 * @param string $type    Canonical schema type.
	 * @param array  $fields  Entity/instance fields.
	 * @param array  $context Optional context (`now` UNIX timestamp).
	 * @return SchemaReadinessResult
	 */
	public static function evaluate( $type, array $fields = array(), array $context = array() ) {
		$type = (string) $type;
		$now  = isset( $context['now'] ) ? (int) $context['now'] : time();

		$blockers     = array();
		$improvements = array();

		if ( SchemaReadinessValues::is_oversized( $fields ) ) {
			$blockers[] = self::blocker( 'readiness.payload.oversized', 'payload', 'reduce_schema_data', '' );
			return self::finalize( $type, $blockers, $improvements, $fields, $context );
		}

		switch ( $type ) {
			case 'Article':
			case 'BlogPosting':
			case 'NewsArticle':
				self::evaluate_article( $fields, $blockers, $improvements );
				break;
			case 'WebSite':
				self::require_meaningful( $fields, array( 'name', 'headline' ), 'name', 'add_site_name', $blockers );
				self::require_url_or_id( $fields, $blockers, 'url', 'add_site_url' );
				break;
			case 'WebPage':
				self::require_meaningful( $fields, array( 'name', 'headline' ), 'name', 'add_page_name', $blockers );
				self::require_url_or_id( $fields, $blockers, 'url', 'add_page_url' );
				break;
			case 'Organization':
			case 'Person':
				self::require_meaningful( $fields, array( 'name' ), 'name', 'add_identity_name', $blockers );
				self::require_url_or_id( $fields, $blockers, '@id', 'add_identity_id' );
				if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'image' ) )
					&& ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'logo' ) ) ) {
					$improvements[] = self::improvement( 'readiness.identity.image_missing', 'image', 'add_identity_image', 'image' );
				}
				break;
			case 'BreadcrumbList':
				self::evaluate_breadcrumb( $fields, $blockers );
				break;
			case 'SearchAction':
				self::evaluate_search_action( $fields, $blockers );
				break;
			case 'FAQPage':
				self::evaluate_faq( $fields, $blockers );
				break;
			case 'HowTo':
				self::evaluate_howto( $fields, $blockers );
				break;
			case 'Recipe':
				self::evaluate_recipe( $fields, $blockers, $improvements );
				break;
			case 'Event':
				self::evaluate_event( $fields, $blockers, $improvements, $now );
				break;
			case 'Course':
				self::evaluate_course( $fields, $blockers );
				break;
			case 'JobPosting':
				self::evaluate_job( $fields, $blockers, $now );
				break;
			case 'SoftwareApplication':
				self::evaluate_software( $fields, $blockers, $improvements );
				break;
			case 'Review':
				self::evaluate_review( $fields, $blockers );
				break;
			case 'Product':
			case 'ProductGroup':
				self::evaluate_product( $fields, $blockers, $improvements );
				break;
			default:
				$entry = SchemaTypePolicyRegistry::get_entry( $type );
				if ( null === $entry ) {
					$blockers[] = self::blocker( 'readiness.type.unsupported', 'type', 'choose_supported_type', '' );
				} else {
					self::require_meaningful( $fields, array( 'name', 'headline', 'title' ), 'name', 'complete_required_fields', $blockers );
				}
				break;
		}

		if ( ! empty( $context['enforce_visibility'] ) ) {
			self::evaluate_visibility( $type, $context, $blockers );
		}

		return self::finalize( $type, $blockers, $improvements, $fields, $context );
	}

	/**
	 * Require visible-content confirmation for guided/manual instances.
	 *
	 * Context keys: enforce_visibility (bool), source_meta (array).
	 *
	 * @param string                 $type     Schema type.
	 * @param array                  $context  Evaluation context.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_visibility( $type, array $context, array &$blockers ) {
		$entry = SchemaTypePolicyRegistry::get_entry( $type );
		if ( ! is_array( $entry ) || empty( $entry['requires_visible_content'] ) ) {
			return;
		}

		$meta = isset( $context['source_meta'] ) && is_array( $context['source_meta'] ) ? $context['source_meta'] : array();
		$mode = isset( $meta['visibility_mode'] ) ? sanitize_key( (string) $meta['visibility_mode'] ) : '';
		$allowed = array( 'confirm_visible', 'seovault_block' );
		if ( ! in_array( $mode, $allowed, true ) ) {
			$blockers[] = self::blocker(
				'readiness.visibility.unconfirmed',
				'visibility',
				'confirm_visible_content',
				'visibility'
			);
		}
	}

	/**
	 * Apply optional filter and build the result object.
	 *
	 * @param string                 $type          Type.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @param array                  $fields        Fields.
	 * @param array                  $context       Context.
	 * @return SchemaReadinessResult
	 */
	private static function finalize( $type, array $blockers, array $improvements, array $fields, array $context ) {
		$result = new SchemaReadinessResult( $type, self::POLICY_VERSION, $blockers, $improvements );

		$filtered = apply_filters( 'seovaultseo_schema_readiness_result', $result, $type, $fields, $context );
		return \SEOvaultSEO\Schema\SchemaExtensionApi::sanitize_readiness_result( $filtered, $result );
	}

	/**
	 * Evaluate Article readiness.
	 *
	 * @param array                  $fields        Fields.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @return void
	 */
	private static function evaluate_article( array $fields, array &$blockers, array &$improvements ) {
		self::require_meaningful( $fields, array( 'headline', 'name' ), 'headline', 'add_headline', $blockers );
		self::require_url_or_id( $fields, $blockers, 'url', 'add_article_url' );

		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'image' ) ) ) {
			$improvements[] = self::improvement( 'readiness.article.image_missing', 'image', 'add_article_image', 'image' );
		}
		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'author' ) ) ) {
			$improvements[] = self::improvement( 'readiness.article.author_missing', 'author', 'add_article_author', 'author' );
		}
	}

	/**
	 * Evaluate BreadcrumbList readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_breadcrumb( array $fields, array &$blockers ) {
		$items = SchemaReadinessValues::as_list( SchemaReadinessValues::get( $fields, 'itemListElement' ) );
		if ( empty( $items ) ) {
			$blockers[] = self::blocker( 'readiness.breadcrumb.empty', 'itemListElement', 'fix_breadcrumbs', 'itemListElement' );
			return;
		}

		$valid = 0;
		foreach ( $items as $index => $item ) {
			if ( ! is_array( $item ) || ! SchemaReadinessValues::is_meaningful( $item ) ) {
				continue;
			}
			$name = SchemaReadinessValues::first_meaningful( $item, array( 'name' ) );
			$pos  = SchemaReadinessValues::get( $item, 'position' );
			if ( ! SchemaReadinessValues::is_meaningful( $name ) || ( null !== $pos && '' !== $pos && ! is_numeric( $pos ) ) ) {
				continue;
			}
			++$valid;
		}

		if ( $valid < 1 ) {
			$blockers[] = self::blocker( 'readiness.breadcrumb.invalid', 'itemListElement', 'fix_breadcrumbs', 'itemListElement' );
		}
	}

	/**
	 * Evaluate SearchAction readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_search_action( array $fields, array &$blockers ) {
		$target = SchemaReadinessValues::get( $fields, 'target' );
		$query  = SchemaReadinessValues::first_meaningful( $fields, array( 'query-input', 'queryInput' ) );

		if ( ! SchemaReadinessValues::is_meaningful( $target ) ) {
			$blockers[] = self::blocker( 'readiness.search.target_missing', 'target', 'configure_search_target', 'target' );
		}
		if ( ! SchemaReadinessValues::is_meaningful( $query ) ) {
			$blockers[] = self::blocker( 'readiness.search.query_missing', 'query-input', 'configure_search_query', 'query-input' );
		}
	}

	/**
	 * Evaluate FAQPage readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_faq( array $fields, array &$blockers ) {
		$entities = SchemaReadinessValues::as_list( SchemaReadinessValues::get( $fields, 'mainEntity' ) );
		$complete = 0;

		foreach ( $entities as $entity ) {
			if ( ! is_array( $entity ) ) {
				continue;
			}
			$question    = SchemaReadinessValues::first_meaningful( $entity, array( 'name', 'text' ) );
			$answer      = SchemaReadinessValues::get( $entity, 'acceptedAnswer' );
			$answer_text = null;
			if ( is_array( $answer ) ) {
				$answer_text = SchemaReadinessValues::first_meaningful( $answer, array( 'text', 'name' ) );
			} elseif ( SchemaReadinessValues::is_meaningful( $answer ) ) {
				$answer_text = $answer;
			}
			if ( SchemaReadinessValues::is_meaningful( $question ) && SchemaReadinessValues::is_meaningful( $answer_text ) ) {
				++$complete;
			}
		}

		if ( $complete < 1 ) {
			$blockers[] = self::blocker( 'readiness.faq.incomplete_pair', 'mainEntity', 'add_complete_faq_pair', 'mainEntity' );
		}
	}

	/**
	 * Evaluate HowTo readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_howto( array $fields, array &$blockers ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_howto_name', $blockers );

		$steps    = SchemaReadinessValues::as_list( SchemaReadinessValues::get( $fields, 'step' ) );
		$complete = 0;
		foreach ( $steps as $step ) {
			if ( is_string( $step ) && SchemaReadinessValues::is_meaningful( $step ) ) {
				++$complete;
				continue;
			}
			if ( ! is_array( $step ) ) {
				continue;
			}
			if ( SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $step, array( 'text', 'name' ) ) ) ) {
				++$complete;
			}
		}

		if ( $complete < 1 ) {
			$blockers[] = self::blocker( 'readiness.howto.steps_missing', 'step', 'add_howto_step', 'step' );
		}
	}

	/**
	 * Evaluate Recipe readiness.
	 *
	 * @param array                  $fields        Fields.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @return void
	 */
	private static function evaluate_recipe( array $fields, array &$blockers, array &$improvements ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_recipe_name', $blockers );
		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'image' ) ) ) {
			$blockers[] = self::blocker( 'readiness.recipe.image_missing', 'image', 'add_recipe_image', 'image' );
		}

		$raw = SchemaReadinessValues::get( $fields, 'recipeInstructions' );
		if ( ! SchemaReadinessValues::is_meaningful( $raw ) ) {
			$raw = SchemaReadinessValues::get( $fields, 'instructions' );
		}
		$instructions = SchemaReadinessValues::as_list( $raw );

		$complete = 0;
		foreach ( $instructions as $item ) {
			if ( is_string( $item ) && SchemaReadinessValues::is_meaningful( $item ) ) {
				++$complete;
				continue;
			}
			if ( is_array( $item ) && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $item, array( 'text', 'name' ) ) ) ) {
				++$complete;
			}
		}
		if ( $complete < 1 ) {
			$blockers[] = self::blocker( 'readiness.recipe.instructions_missing', 'recipeInstructions', 'add_recipe_instruction', 'recipeInstructions' );
		}

		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'author' ) ) ) {
			$improvements[] = self::improvement( 'readiness.recipe.author_missing', 'author', 'add_recipe_author', 'author' );
		}
	}

	/**
	 * Evaluate Event readiness.
	 *
	 * @param array                  $fields        Fields.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @param int                    $now           Now timestamp.
	 * @return void
	 */
	private static function evaluate_event( array $fields, array &$blockers, array &$improvements, $now ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_event_name', $blockers );

		$start = SchemaReadinessValues::get( $fields, 'startDate' );
		if ( ! SchemaReadinessValues::is_valid_date( $start ) ) {
			$blockers[] = self::blocker( 'readiness.event.start_date_invalid', 'startDate', 'add_event_start_date', 'startDate' );
		}

		$location = SchemaReadinessValues::get( $fields, 'location' );
		if ( ! self::is_complete_event_location( $location ) ) {
			$blockers[] = self::blocker( 'readiness.event.location_incomplete', 'location', 'add_event_location', 'location' );
		}

		$end = SchemaReadinessValues::get( $fields, 'endDate' );
		if ( SchemaReadinessValues::is_meaningful( $end ) ) {
			if ( ! SchemaReadinessValues::is_valid_date( $end ) ) {
				$blockers[] = self::blocker( 'readiness.event.end_date_invalid', 'endDate', 'fix_event_end_date', 'endDate' );
			} else {
				$end_ts = SchemaReadinessValues::to_timestamp( $end );
				if ( false !== $end_ts && $end_ts < $now ) {
					$blockers[] = self::blocker( 'readiness.event.expired', 'endDate', 'update_or_remove_expired_event', 'endDate' );
				}
			}
		}

		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'image' ) ) ) {
			$improvements[] = self::improvement( 'readiness.event.image_missing', 'image', 'add_event_image', 'image' );
		}
	}

	/**
	 * Whether an Event location satisfies policy.
	 *
	 * @param mixed $location Location node.
	 * @return bool
	 */
	private static function is_complete_event_location( $location ) {
		if ( ! SchemaReadinessValues::is_meaningful( $location ) ) {
			return false;
		}
		if ( is_string( $location ) ) {
			return true;
		}
		if ( ! is_array( $location ) ) {
			return false;
		}
		if ( ValueInspector::is_list( $location ) ) {
			foreach ( $location as $item ) {
				if ( self::is_complete_event_location( $item ) ) {
					return true;
				}
			}
			return false;
		}

		$types = ValueInspector::get_types( $location );
		if ( in_array( 'VirtualLocation', $types, true ) ) {
			$url = SchemaReadinessValues::get( $location, 'url' );
			return is_string( $url ) && ValueInspector::is_valid_url( trim( $url ), 'url' );
		}

		return SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $location, array( 'name', 'address' ) ) );
	}

	/**
	 * Evaluate Course readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_course( array $fields, array &$blockers ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_course_name', $blockers );
		self::require_meaningful( $fields, array( 'description' ), 'description', 'add_course_description', $blockers );

		$provider = SchemaReadinessValues::get( $fields, 'provider' );
		$ok       = false;
		if ( is_string( $provider ) && SchemaReadinessValues::is_meaningful( $provider ) ) {
			$ok = true;
		} elseif ( is_array( $provider ) && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $provider, array( 'name' ) ) ) ) {
			$ok = true;
		}
		if ( ! $ok ) {
			$blockers[] = self::blocker( 'readiness.course.provider_incomplete', 'provider', 'add_course_provider', 'provider' );
		}
	}

	/**
	 * Evaluate JobPosting readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @param int                    $now      Now.
	 * @return void
	 */
	private static function evaluate_job( array $fields, array &$blockers, $now ) {
		self::require_meaningful( $fields, array( 'title', 'name' ), 'title', 'add_job_title', $blockers );
		self::require_meaningful( $fields, array( 'description' ), 'description', 'add_job_description', $blockers );

		$posted = SchemaReadinessValues::get( $fields, 'datePosted' );
		if ( ! SchemaReadinessValues::is_valid_date( $posted ) ) {
			$blockers[] = self::blocker( 'readiness.job.date_posted_invalid', 'datePosted', 'add_job_date_posted', 'datePosted' );
		}

		$org    = SchemaReadinessValues::get( $fields, 'hiringOrganization' );
		$org_ok = ( is_string( $org ) && SchemaReadinessValues::is_meaningful( $org ) )
			|| ( is_array( $org ) && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $org, array( 'name' ) ) ) );
		if ( ! $org_ok ) {
			$blockers[] = self::blocker( 'readiness.job.hiring_org_incomplete', 'hiringOrganization', 'add_hiring_organization', 'hiringOrganization' );
		}

		$location   = SchemaReadinessValues::get( $fields, 'jobLocation' );
		$remote     = SchemaReadinessValues::get( $fields, 'jobLocationType' );
		$has_loc    = SchemaReadinessValues::is_meaningful( $location );
		$has_remote = SchemaReadinessValues::is_meaningful( $remote );
		if ( ! $has_loc && ! $has_remote ) {
			$blockers[] = self::blocker( 'readiness.job.location_missing', 'jobLocation', 'add_job_location_or_remote', 'jobLocation' );
		}

		$through = SchemaReadinessValues::get( $fields, 'validThrough' );
		if ( SchemaReadinessValues::is_meaningful( $through ) ) {
			if ( ! SchemaReadinessValues::is_valid_date( $through ) ) {
				$blockers[] = self::blocker( 'readiness.job.valid_through_invalid', 'validThrough', 'fix_job_valid_through', 'validThrough' );
			} else {
				$ts = SchemaReadinessValues::to_timestamp( $through );
				if ( false !== $ts && $ts < $now ) {
					$blockers[] = self::blocker( 'readiness.job.expired', 'validThrough', 'update_or_remove_expired_job', 'validThrough' );
				}
			}
		}
	}

	/**
	 * Evaluate SoftwareApplication readiness.
	 *
	 * @param array                  $fields        Fields.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @return void
	 */
	private static function evaluate_software( array $fields, array &$blockers, array &$improvements ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_software_name', $blockers );

		$offers = SchemaReadinessValues::get( $fields, 'offers' );
		$price  = SchemaReadinessValues::extract_offer_price( $offers );
		if ( null === $price || ! SchemaReadinessValues::is_valid_price( $price ) ) {
			$blockers[] = self::blocker( 'readiness.software.offer_price_missing', 'offers.price', 'add_software_offer_price', 'offers.price' );
		} elseif ( ! self::has_valid_offer_currency( $offers ) ) {
			$blockers[] = self::blocker( 'readiness.software.offer_currency_missing', 'offers.priceCurrency', 'add_software_offer_currency', 'offers.priceCurrency' );
		}

		$rating = SchemaReadinessValues::get( $fields, 'aggregateRating' );
		if ( SchemaReadinessValues::is_meaningful( $rating ) ) {
			self::evaluate_software_rating_branch( $rating, $blockers );
		}

		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'operatingSystem' ) ) ) {
			$improvements[] = self::improvement( 'readiness.software.os_missing', 'operatingSystem', 'add_operating_system', 'operatingSystem' );
		}
	}

	/**
	 * Whether offers include a valid ISO 4217-style currency code.
	 *
	 * @param mixed $offers Offers node.
	 * @return bool
	 */
	private static function has_valid_offer_currency( $offers ) {
		if ( ! is_array( $offers ) ) {
			return false;
		}
		if ( ValueInspector::is_list( $offers ) ) {
			foreach ( $offers as $offer ) {
				if ( self::has_valid_offer_currency( $offer ) ) {
					return true;
				}
			}
			return false;
		}
		$currency = SchemaReadinessValues::get( $offers, 'priceCurrency' );
		if ( ! is_string( $currency ) ) {
			return false;
		}
		$currency = strtoupper( trim( $currency ) );
		return (bool) preg_match( '/^[A-Z]{3}$/', $currency );
	}

	/**
	 * Validate optional SoftwareApplication aggregateRating branch when present.
	 *
	 * @param mixed                  $rating   Rating node.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_software_rating_branch( $rating, array &$blockers ) {
		if ( ! is_array( $rating ) ) {
			$blockers[] = self::blocker( 'readiness.software.rating_invalid', 'aggregateRating', 'fix_software_rating', 'aggregateRating' );
			return;
		}

		$value = SchemaReadinessValues::get( $rating, 'ratingValue' );
		$min   = 1.0;
		$max   = 5.0;
		if ( isset( $rating['bestRating'] ) && is_numeric( $rating['bestRating'] ) ) {
			$max = (float) $rating['bestRating'];
		}
		if ( isset( $rating['worstRating'] ) && is_numeric( $rating['worstRating'] ) ) {
			$min = (float) $rating['worstRating'];
		}
		if ( ! SchemaReadinessValues::is_rating_in_bounds( $value, $min, $max ) ) {
			$blockers[] = self::blocker( 'readiness.software.rating_invalid', 'aggregateRating.ratingValue', 'fix_software_rating', 'aggregateRating.ratingValue' );
		}

		$count = SchemaReadinessValues::get( $rating, 'ratingCount' );
		if ( ! SchemaReadinessValues::is_meaningful( $count ) || ! is_numeric( $count ) || (float) $count < 1 ) {
			$blockers[] = self::blocker( 'readiness.software.rating_count_missing', 'aggregateRating.ratingCount', 'add_software_rating_count', 'aggregateRating.ratingCount' );
		}
	}

	/**
	 * Evaluate Review readiness.
	 *
	 * @param array                  $fields   Fields.
	 * @param SchemaReadinessIssue[] $blockers Blockers.
	 * @return void
	 */
	private static function evaluate_review( array $fields, array &$blockers ) {
		$item    = SchemaReadinessValues::get( $fields, 'itemReviewed' );
		$item_ok = ( is_string( $item ) && SchemaReadinessValues::is_meaningful( $item ) )
			|| ( is_array( $item ) && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $item, array( 'name', 'headline' ) ) ) );
		if ( ! $item_ok ) {
			$blockers[] = self::blocker( 'readiness.review.item_missing', 'itemReviewed', 'add_reviewed_item', 'itemReviewed' );
		} elseif ( is_array( $item ) && ! self::is_allowed_reviewed_item_type( $item ) ) {
			$blockers[] = self::blocker( 'readiness.review.item_type_prohibited', 'itemReviewed.@type', 'choose_allowed_reviewed_item_type', 'itemReviewed.@type' );
		}

		$author    = SchemaReadinessValues::get( $fields, 'author' );
		$author_ok = ( is_string( $author ) && SchemaReadinessValues::is_meaningful( $author ) )
			|| ( is_array( $author ) && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $author, array( 'name' ) ) ) );
		if ( ! $author_ok ) {
			$blockers[] = self::blocker( 'readiness.review.author_missing', 'author', 'add_review_author', 'author' );
		}

		if ( $item_ok && $author_ok && self::is_self_serving_review( $item, $author ) ) {
			$blockers[] = self::blocker( 'readiness.review.self_serving', 'author', 'remove_self_serving_review', 'author' );
		}

		$rating = SchemaReadinessValues::get( $fields, 'reviewRating' );
		$value  = null;
		$min    = 1.0;
		$max    = 5.0;
		if ( is_array( $rating ) ) {
			$value = SchemaReadinessValues::get( $rating, 'ratingValue' );
			if ( isset( $rating['bestRating'] ) && is_numeric( $rating['bestRating'] ) ) {
				$max = (float) $rating['bestRating'];
			}
			if ( isset( $rating['worstRating'] ) && is_numeric( $rating['worstRating'] ) ) {
				$min = (float) $rating['worstRating'];
			}
		} else {
			$value = $rating;
		}
		if ( ! SchemaReadinessValues::is_rating_in_bounds( $value, $min, $max ) ) {
			$blockers[] = self::blocker( 'readiness.review.rating_invalid', 'reviewRating.ratingValue', 'add_valid_rating', 'reviewRating.ratingValue' );
		}

		$body = SchemaReadinessValues::first_meaningful( $fields, array( 'reviewBody', 'description' ) );
		if ( ! SchemaReadinessValues::is_meaningful( $body ) ) {
			$blockers[] = self::blocker( 'readiness.review.body_missing', 'reviewBody', 'add_review_text', 'reviewBody' );
		}
	}

	/**
	 * Allowed itemReviewed @type values for guided Review forms.
	 *
	 * @param array $item Item reviewed node.
	 * @return bool
	 */
	private static function is_allowed_reviewed_item_type( array $item ) {
		$type = SchemaReadinessValues::get( $item, '@type' );
		if ( ! SchemaReadinessValues::is_meaningful( $type ) ) {
			// Missing type is allowed; name is the readiness gate.
			return true;
		}
		$allowed = array(
			'Product',
			'Book',
			'Movie',
			'LocalBusiness',
			'SoftwareApplication',
			'Course',
			'Event',
			'Recipe',
			'Thing',
		);
		if ( is_array( $type ) ) {
			foreach ( $type as $candidate ) {
				if ( is_string( $candidate ) && in_array( $candidate, $allowed, true ) ) {
					return true;
				}
			}
			return false;
		}
		return is_string( $type ) && in_array( $type, $allowed, true );
	}

	/**
	 * Detect self-serving reviews where author and reviewed item share the same name.
	 *
	 * @param mixed $item   Item reviewed.
	 * @param mixed $author Author.
	 * @return bool
	 */
	private static function is_self_serving_review( $item, $author ) {
		$item_name = '';
		if ( is_string( $item ) ) {
			$item_name = $item;
		} elseif ( is_array( $item ) ) {
			$found = SchemaReadinessValues::first_meaningful( $item, array( 'name', 'headline' ) );
			$item_name = is_string( $found ) ? $found : '';
		}

		$author_name = '';
		if ( is_string( $author ) ) {
			$author_name = $author;
		} elseif ( is_array( $author ) ) {
			$found = SchemaReadinessValues::first_meaningful( $author, array( 'name' ) );
			$author_name = is_string( $found ) ? $found : '';
		}

		$item_name   = strtolower( trim( preg_replace( '/\s+/', ' ', $item_name ) ) );
		$author_name = strtolower( trim( preg_replace( '/\s+/', ' ', $author_name ) ) );
		if ( '' === $item_name || '' === $author_name ) {
			return false;
		}
		return $item_name === $author_name;
	}

	/**
	 * Evaluate Product readiness.
	 *
	 * ProductGroup (variable Woo products) may satisfy the offer branch via variants.
	 *
	 * @param array                  $fields        Fields.
	 * @param SchemaReadinessIssue[] $blockers      Blockers.
	 * @param SchemaReadinessIssue[] $improvements  Improvements.
	 * @return void
	 */
	private static function evaluate_product( array $fields, array &$blockers, array &$improvements ) {
		self::require_meaningful( $fields, array( 'name' ), 'name', 'add_product_name', $blockers );

		$has_offer  = SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'offers' ) );
		$has_rating = SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'aggregateRating' ) );
		$has_review = SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'review' ) );
		if ( ! $has_offer && ! $has_rating && ! $has_review && self::product_has_variant_offer_branch( $fields ) ) {
			$has_offer = true;
		}
		if ( ! $has_offer && ! $has_rating && ! $has_review ) {
			$blockers[] = self::blocker( 'readiness.product.offer_or_review_missing', 'offers', 'add_product_offer_or_review', 'offers' );
		}

		if ( $has_offer && SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'offers' ) ) ) {
			$price = SchemaReadinessValues::extract_offer_price( SchemaReadinessValues::get( $fields, 'offers' ) );
			if ( null !== $price && ! SchemaReadinessValues::is_valid_price( $price ) ) {
				$blockers[] = self::blocker( 'readiness.product.price_invalid', 'offers.price', 'fix_product_price', 'offers.price' );
			}
		}

		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $fields, 'image' ) ) ) {
			$improvements[] = self::improvement( 'readiness.product.image_missing', 'image', 'add_product_image', 'image' );
		}
	}

	/**
	 * Whether a ProductGroup (or Product) carries at least one variant offer/rating/review.
	 *
	 * @param array $fields Fields.
	 * @return bool
	 */
	private static function product_has_variant_offer_branch( array $fields ) {
		$variants = SchemaReadinessValues::get( $fields, 'hasVariant' );
		if ( ! SchemaReadinessValues::is_meaningful( $variants ) ) {
			return false;
		}
		if ( ! ValueInspector::is_list( $variants ) ) {
			$variants = array( $variants );
		}
		foreach ( $variants as $variant ) {
			if ( ! is_array( $variant ) ) {
				continue;
			}
			if ( SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $variant, 'offers' ) ) ) {
				return true;
			}
			if ( SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $variant, 'aggregateRating' ) ) ) {
				return true;
			}
			if ( SchemaReadinessValues::is_meaningful( SchemaReadinessValues::get( $variant, 'review' ) ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Require at least one meaningful field among candidates.
	 *
	 * @param array                  $fields      Fields.
	 * @param string[]               $keys        Candidate keys.
	 * @param string                 $field_key   Field key.
	 * @param string                 $action_key  Action key.
	 * @param SchemaReadinessIssue[] $blockers    Blockers.
	 * @return void
	 */
	private static function require_meaningful( array $fields, array $keys, $field_key, $action_key, array &$blockers ) {
		if ( ! SchemaReadinessValues::is_meaningful( SchemaReadinessValues::first_meaningful( $fields, $keys ) ) ) {
			$blockers[] = self::blocker( 'readiness.field.missing.' . $field_key, $field_key, $action_key, $field_key );
		}
	}

	/**
	 * Require a valid url or @id.
	 *
	 * @param array                  $fields     Fields.
	 * @param SchemaReadinessIssue[] $blockers   Blockers.
	 * @param string                 $field_key  Field key.
	 * @param string                 $action_key Action key.
	 * @return void
	 */
	private static function require_url_or_id( array $fields, array &$blockers, $field_key, $action_key ) {
		$url    = SchemaReadinessValues::get( $fields, 'url' );
		$id     = SchemaReadinessValues::get( $fields, '@id' );
		$url_ok = is_string( $url ) && SchemaReadinessValues::is_meaningful( $url ) && ValueInspector::is_valid_url( trim( $url ), 'url' );
		$id_ok  = is_string( $id ) && SchemaReadinessValues::is_meaningful( $id ) && ValueInspector::is_valid_url( trim( $id ), '@id' );
		if ( ! $url_ok && ! $id_ok ) {
			$blockers[] = self::blocker( 'readiness.field.url_or_id_missing', $field_key, $action_key, $field_key );
		}
	}

	/**
	 * Create a blocker issue.
	 *
	 * @param string $code       Code.
	 * @param string $field_key  Field key.
	 * @param string $action_key Action key.
	 * @param string $path       Path.
	 * @return SchemaReadinessIssue
	 */
	private static function blocker( $code, $field_key, $action_key, $path ) {
		return new SchemaReadinessIssue(
			array(
				'code'       => $code,
				'kind'       => SchemaReadinessIssue::KIND_BLOCKER,
				'field_key'  => $field_key,
				'action_key' => $action_key,
				'path'       => $path,
			)
		);
	}

	/**
	 * Create an improvement issue.
	 *
	 * @param string $code       Code.
	 * @param string $field_key  Field key.
	 * @param string $action_key Action key.
	 * @param string $path       Path.
	 * @return SchemaReadinessIssue
	 */
	private static function improvement( $code, $field_key, $action_key, $path ) {
		return new SchemaReadinessIssue(
			array(
				'code'       => $code,
				'kind'       => SchemaReadinessIssue::KIND_IMPROVEMENT,
				'field_key'  => $field_key,
				'action_key' => $action_key,
				'path'       => $path,
			)
		);
	}
}
