<?php
/**
 * User-facing Schema Health status and setup copy.
 *
 * @package SEOvaultSEO\Schema\Validation
 */

namespace SEOvaultSEO\Schema\Validation;

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

/**
 * Presents Healthy / Improvements available / Action required and setup rows.
 */
class SchemaHealthStatusPresenter {

	/**
	 * Present the compact published-schema status block.
	 *
	 * @param array $validation Validation response.
	 * @return array{status:string,icon:string,label:string,summary:string,scope:string,entities_summary:string,actions:array}
	 */
	public function present_published( array $validation ) {
		$status = SchemaHealthStatus::resolve_published( $validation );
		$scope  = isset( $validation['scope'] ) ? (string) $validation['scope'] : PostSchemaValidationService::SCOPE_GENERATED;
		$result = isset( $validation['result'] ) && is_array( $validation['result'] ) ? $validation['result'] : array();
		$build  = isset( $validation['build'] ) && is_array( $validation['build'] ) ? $validation['build'] : array();

		$entity_labels    = $this->collect_entity_labels( isset( $result['entities'] ) ? $result['entities'] : array() );
		$entities_summary = $this->join_entity_labels( $entity_labels );
		$scope_copy       = $this->get_scope_copy( $scope );
		$copy             = $this->get_status_copy( $status, $result, $build, $entities_summary, count( $entity_labels ) );

		return array(
			'status'           => $status,
			'icon'             => $copy['icon'],
			'label'            => $copy['label'],
			'summary'          => $copy['summary'],
			'scope'            => $scope_copy,
			'entities_summary' => $entities_summary,
			'actions'          => $copy['actions'],
		);
	}

	/**
	 * Present unfinished setup rows independently from published status.
	 *
	 * @param array $validation Validation response with setup payload.
	 * @return array{items:array,count:int}
	 */
	public function present_setup( array $validation ) {
		$setup = isset( $validation['setup'] ) && is_array( $validation['setup'] ) ? $validation['setup'] : array();
		$items = array();

		$withheld = isset( $setup['withheld'] ) && is_array( $setup['withheld'] ) ? $setup['withheld'] : array();
		foreach ( $withheld as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$status      = SchemaSetupStatus::resolve_withheld_item( $row );
			$type        = isset( $row['type'] ) && '' !== (string) $row['type'] ? (string) $row['type'] : __( 'Schema', 'seovault-ai' );
			$instance_id = $this->resolve_instance_id( $row );
			$items[]     = array(
				'type'            => $type,
				'status'          => $status,
				'status_label'    => $this->get_setup_status_label( $status ),
				'summary'         => $this->get_withheld_summary( $row ),
				'technical_code'  => (string) ( $row['reason'] ?? '' ),
				'needs_action'    => SchemaSetupStatus::needs_action( $status ),
				'instance_id'     => $instance_id,
				'source'          => (string) ( $row['source'] ?? '' ),
				'source_label'    => \SEOvaultSEO\Admin\SchemaReviewSurface::resolve_setup_source_label( $row ),
				'first_field_key' => $this->resolve_first_field_key( $row ),
				'editable'        => $instance_id !== '' && ! empty( $row['editable'] ),
				'removable'       => $instance_id !== '',
				'fields'          => isset( $row['fields'] ) && is_array( $row['fields'] ) ? $row['fields'] : array(),
				'source_meta'     => isset( $row['source_meta'] ) && is_array( $row['source_meta'] ) ? $row['source_meta'] : array(),
			);
		}

		$custom = isset( $setup['custom_json'] ) && is_array( $setup['custom_json'] ) ? $setup['custom_json'] : null;
		if ( is_array( $custom ) ) {
			$status = SchemaSetupStatus::resolve_custom_json( $custom );
			if ( '' !== $status && SchemaSetupStatus::EMITTED !== $status ) {
				$items[] = array(
					'type'            => __( 'Custom JSON', 'seovault-ai' ),
					'status'          => $status,
					'status_label'    => $this->get_setup_status_label( $status ),
					'summary'         => $this->get_custom_json_summary( $custom, $status ),
					'technical_code'  => $this->first_issue_code( $custom ),
					'needs_action'    => SchemaSetupStatus::needs_action( $status ),
					'instance_id'     => '',
					'source'          => 'custom',
					'source_label'    => \SEOvaultSEO\Admin\SchemaReviewSurface::get_source_label( 'custom' ),
					'first_field_key' => '',
					'editable'        => false,
					'removable'       => false,
					'fields'          => array(),
					'source_meta'     => array(),
					'action'          => 'focus_custom_json',
				);
			}
		}

		return array(
			'items' => $items,
			'count' => count( $items ),
		);
	}

	/**
	 * Present ready guided instances that authors can edit from the sidebar.
	 *
	 * @param array $validation Validation response.
	 * @return array{items:array,count:int}
	 */
	public function present_editable_entities( array $validation ) {
		$setup = isset( $validation['setup'] ) && is_array( $validation['setup'] ) ? $validation['setup'] : array();
		$ready = isset( $setup['ready_instances'] ) && is_array( $setup['ready_instances'] ) ? $setup['ready_instances'] : array();
		$items = array();

		foreach ( $ready as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$instance_id = $this->resolve_instance_id( $row );
			if ( '' === $instance_id ) {
				continue;
			}
			$type    = isset( $row['type'] ) && '' !== (string) $row['type'] ? (string) $row['type'] : __( 'Schema', 'seovault-ai' );
			$items[] = array(
				'type'            => $type,
				'instance_id'     => $instance_id,
				'source'          => (string) ( $row['source'] ?? 'post_instance' ),
				'source_label'    => \SEOvaultSEO\Admin\SchemaReviewSurface::resolve_setup_source_label( $row ),
				'status'          => SchemaSetupStatus::READY,
				'status_label'    => $this->get_setup_status_label( SchemaSetupStatus::READY ),
				'summary'         => __( 'Ready for published schema output.', 'seovault-ai' ),
				'editable'        => true,
				'fields'          => isset( $row['fields'] ) && is_array( $row['fields'] ) ? $row['fields'] : array(),
				'source_meta'     => isset( $row['source_meta'] ) && is_array( $row['source_meta'] ) ? $row['source_meta'] : array(),
				'first_field_key' => '',
			);
		}

		return array(
			'items' => $items,
			'count' => count( $items ),
		);
	}

	/**
	 * Resolve guided instance id from a setup row.
	 *
	 * @param array $row Setup row.
	 * @return string
	 */
	private function resolve_instance_id( array $row ) {
		if ( ! empty( $row['instance_id'] ) ) {
			return (string) $row['instance_id'];
		}
		$ref = (string) ( $row['source_ref'] ?? '' );
		if ( 0 === strpos( $ref, 'instance:' ) ) {
			return substr( $ref, strlen( 'instance:' ) );
		}
		return '';
	}

	/**
	 * First blocking field key for Complete focus.
	 *
	 * @param array $row Setup row.
	 * @return string
	 */
	private function resolve_first_field_key( array $row ) {
		if ( ! empty( $row['first_field_key'] ) ) {
			return (string) $row['first_field_key'];
		}
		$setup = isset( $row['setup'] ) && is_array( $row['setup'] ) ? $row['setup'] : array();
		foreach ( $setup as $diag ) {
			if ( ! is_array( $diag ) ) {
				continue;
			}
			$field_key = (string) ( $diag['field_key'] ?? '' );
			if ( '' !== $field_key ) {
				return $field_key;
			}
		}
		return '';
	}

	/**
	 * Present live/published-page comparison copy.
	 *
	 * @param array|null $live Live response.
	 * @return array{status:string,state:string,icon:string,label:string,summary:string,details:array}
	 */
	public function present_live( $live ) {
		$status  = SchemaHealthStatus::resolve_live( $live );
		$state   = is_array( $live ) && isset( $live['state'] ) ? (string) $live['state'] : 'not_checked';
		$message = is_array( $live ) && isset( $live['message'] ) ? (string) $live['message'] : '';
		$details = $this->get_live_details( $live );

		switch ( $status ) {
			case SchemaHealthStatus::HEALTHY:
				return array(
					'status'  => $status,
					'state'   => $state,
					'icon'    => '✓',
					'label'   => __( 'Healthy', 'seovault-ai' ),
					'summary' => $message !== '' ? $message : __( 'Published-page schema matches SEOvault’s expected output.', 'seovault-ai' ),
					'details' => $details,
				);
			case SchemaHealthStatus::ACTION_REQUIRED:
				return array(
					'status'  => $status,
					'state'   => $state,
					'icon'    => '×',
					'label'   => __( 'Action required', 'seovault-ai' ),
					'summary' => $message !== '' ? $message : __( 'The live page JSON-LD differs from SEOvault’s expected output.', 'seovault-ai' ),
					'details' => $details,
				);
			case SchemaHealthStatus::AVAILABLE_AFTER_PUBLISH:
				return array(
					'status'  => $status,
					'state'   => $state,
					'icon'    => '—',
					'label'   => __( 'Available after publishing', 'seovault-ai' ),
					'summary' => $message !== '' ? $message : __( 'Published-page validation becomes available when the page is publicly accessible.', 'seovault-ai' ),
					'details' => array(),
				);
			case SchemaHealthStatus::UNAVAILABLE:
				return array(
					'status'  => $status,
					'state'   => $state,
					'icon'    => '!',
					'label'   => __( 'Check unavailable', 'seovault-ai' ),
					'summary' => $message !== '' ? $message : __( 'The published page could not be checked. This is not a schema validation result.', 'seovault-ai' ),
					'details' => array(),
				);
			case SchemaHealthStatus::NOT_CHECKED:
			default:
				return array(
					'status'  => SchemaHealthStatus::NOT_CHECKED,
					'state'   => 'not_checked',
					'icon'    => '—',
					'label'   => __( 'Not checked', 'seovault-ai' ),
					'summary' => $message !== '' ? $message : __( 'Compare the public page JSON-LD to SEOvault’s expected output.', 'seovault-ai' ),
					'details' => array(),
				);
		}
	}

	/**
	 * Build published status label/summary/actions.
	 *
	 * @param string $status           User status.
	 * @param array  $result           Result payload.
	 * @param array  $build            Build payload.
	 * @param string $entities_summary Entity label sentence.
	 * @param int    $entity_count     Distinct entity label count.
	 * @return array
	 */
	private function get_status_copy( $status, array $result, array $build, $entities_summary, $entity_count = 0 ) {
		$errors          = isset( $result['errors'] ) ? (int) $result['errors'] : 0;
		$warnings        = isset( $result['warnings'] ) ? (int) $result['warnings'] : 0;
		$recommendations = isset( $result['recommendations'] ) ? (int) $result['recommendations'] : 0;
		$improvement_n   = isset( $build['improvements'] ) && is_array( $build['improvements'] ) ? count( $build['improvements'] ) : 0;
		$improvement_n   = max( $recommendations, $improvement_n );

		switch ( $status ) {
			case SchemaHealthStatus::HEALTHY:
				if ( '' !== $entities_summary ) {
					$summary = sprintf(
						/* translators: %s: comma-separated schema type labels. */
						_n( '%s schema is ready.', '%s schema are ready.', max( 1, (int) $entity_count ), 'seovault-ai' ),
						$entities_summary
					);
				} else {
					$summary = __( 'Published schema looks healthy.', 'seovault-ai' );
				}
				return array(
					'icon'    => '✓',
					'label'   => __( 'Healthy', 'seovault-ai' ),
					'summary' => $summary,
					'actions' => array(),
				);
			case SchemaHealthStatus::IMPROVEMENTS_AVAILABLE:
				$summary = $this->get_improvement_summary( $result, $build, $improvement_n );
				return array(
					'icon'    => '●',
					'label'   => __( 'Improvements available', 'seovault-ai' ),
					'summary' => $summary,
					'actions' => $this->get_improvement_actions( $result, $build ),
				);
			case SchemaHealthStatus::ACTION_REQUIRED:
				$actions = $this->get_attention_actions( $result );
				if ( $errors > 0 ) {
					$summary = sprintf(
						/* translators: %d: error count. */
						_n( '%d issue needs attention before this schema can be trusted.', '%d issues need attention before this schema can be trusted.', $errors, 'seovault-ai' ),
						$errors
					);
				} else {
					$summary = sprintf(
						/* translators: %d: warning count. */
						_n( '%d warning needs a review.', '%d warnings need a review.', max( 1, $warnings ), 'seovault-ai' ),
						max( 1, $warnings )
					);
				}
				if ( ! empty( $actions ) ) {
					$summary = (string) $actions[0];
				}
				return array(
					'icon'    => '×',
					'label'   => __( 'Action required', 'seovault-ai' ),
					'summary' => $summary,
					'actions' => ! empty( $actions )
						? $actions
						: array(
							__( 'Review the reported schema issues and correct the content or settings they describe.', 'seovault-ai' ),
						),
				);
			case SchemaHealthStatus::NO_SCHEMA:
				return array(
					'icon'    => '—',
					'label'   => __( 'No schema detected', 'seovault-ai' ),
					'summary' => __( 'Schema output is currently disabled.', 'seovault-ai' ),
					'actions' => array(
						__( 'Enable schema output to run Schema Health checks.', 'seovault-ai' ),
					),
				);
			case SchemaHealthStatus::UNAVAILABLE:
				return array(
					'icon'    => '!',
					'label'   => __( 'Check unavailable', 'seovault-ai' ),
					'summary' => __( 'SEOvault could not complete the local schema check.', 'seovault-ai' ),
					'actions' => array(
						__( 'Try again. This is not a schema validation result.', 'seovault-ai' ),
					),
				);
			case SchemaHealthStatus::NOT_CHECKED:
			default:
				return array(
					'icon'    => '—',
					'label'   => __( 'Not checked', 'seovault-ai' ),
					'summary' => __( 'Schema has not been checked yet.', 'seovault-ai' ),
					'actions' => array(),
				);
		}
	}

	/**
	 * Prefer a concrete improvement action over a bare count.
	 *
	 * @param array $result         Result payload.
	 * @param array $build          Build payload.
	 * @param int   $improvement_n  Improvement count.
	 * @return string
	 */
	private function get_improvement_summary( array $result, array $build, $improvement_n ) {
		$actions = $this->get_improvement_actions( $result, $build );
		if ( ! empty( $actions ) ) {
			return (string) $actions[0];
		}

		return sprintf(
			/* translators: %d: recommendation count. */
			_n( '%d improvement is available.', '%d improvements are available.', max( 1, (int) $improvement_n ), 'seovault-ai' ),
			max( 1, (int) $improvement_n )
		);
	}

	/**
	 * Collect up to three user-action improvement lines.
	 *
	 * @param array $result Result payload.
	 * @param array $build  Build payload.
	 * @return string[]
	 */
	private function get_improvement_actions( array $result, array $build ) {
		$actions   = array();
		$presenter = new ValidationIssuePresenter();
		$issues    = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();

		foreach ( $issues as $issue ) {
			if ( ! is_array( $issue ) ) {
				continue;
			}
			if ( Severity::RECOMMENDATION !== ( $issue['severity'] ?? '' ) ) {
				continue;
			}
			$presented = $presenter->present( $issue );
			if ( ! empty( $presented['action'] ) ) {
				$actions[] = (string) $presented['action'];
			}
			if ( count( $actions ) >= 3 ) {
				return array_values( array_unique( $actions ) );
			}
		}

		$improvements = isset( $build['improvements'] ) && is_array( $build['improvements'] ) ? $build['improvements'] : array();
		foreach ( $improvements as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$line = $this->present_readiness_action( $row );
			if ( '' !== $line ) {
				$actions[] = $line;
			}
			if ( count( $actions ) >= 3 ) {
				break;
			}
		}

		return array_values( array_unique( $actions ) );
	}

	/**
	 * Collect up to three concrete actions for errors/warnings (no paths).
	 *
	 * @param array $result Result payload.
	 * @return string[]
	 */
	private function get_attention_actions( array $result ) {
		$actions   = array();
		$presenter = new ValidationIssuePresenter();
		$issues    = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();

		foreach ( $issues as $issue ) {
			if ( ! is_array( $issue ) ) {
				continue;
			}
			$severity = isset( $issue['severity'] ) ? (string) $issue['severity'] : '';
			if ( Severity::ERROR !== $severity && Severity::WARNING !== $severity ) {
				continue;
			}
			$presented = $presenter->present( $issue );
			if ( ! empty( $presented['is_plugin_fault'] ) && ! empty( $presented['message'] ) ) {
				$actions[] = (string) $presented['message'];
			} elseif ( ! empty( $presented['action'] ) ) {
				$actions[] = (string) $presented['action'];
			}
			if ( ! empty( $presented['repair_actions'] ) && is_array( $presented['repair_actions'] ) ) {
				foreach ( $presented['repair_actions'] as $repair ) {
					$repair = (string) $repair;
					if ( '' !== $repair ) {
						$actions[] = $repair;
					}
					if ( count( $actions ) >= 3 ) {
						return array_values( array_unique( $actions ) );
					}
				}
			}
			if ( count( $actions ) >= 3 ) {
				return array_values( array_unique( $actions ) );
			}
		}

		return array_values( array_unique( $actions ) );
	}

	/**
	 * Human summary for a withheld setup row.
	 *
	 * @param array $row Withheld descriptor.
	 * @return string
	 */
	private function get_withheld_summary( array $row ) {
		$setup = isset( $row['setup'] ) && is_array( $row['setup'] ) ? $row['setup'] : array();
		$parts = array();
		foreach ( $setup as $diag ) {
			if ( ! is_array( $diag ) ) {
				continue;
			}
			$line = $this->present_readiness_action( $diag );
			if ( '' !== $line ) {
				$parts[] = $line;
			}
			if ( count( $parts ) >= 2 ) {
				break;
			}
		}
		if ( ! empty( $parts ) ) {
			return implode( ' ', $parts );
		}

		$type = isset( $row['type'] ) ? (string) $row['type'] : '';
		$reason = isset( $row['reason'] ) ? (string) $row['reason'] : '';
		if ( 'faq_block_incomplete' === $reason ) {
			return __( 'Add at least one complete question and answer in the FAQ block before SEOvault can publish FAQ schema.', 'seovault-ai' );
		}
		if ( 'howto_block_incomplete' === $reason ) {
			return __( 'Add a How-to name and at least one complete step in the How-to block before SEOvault can publish How-to schema.', 'seovault-ai' );
		}
		if ( '' !== $type ) {
			return sprintf(
				/* translators: %s: schema type. */
				__( 'Finish the %s setup before SEOvault can publish it.', 'seovault-ai' ),
				$type
			);
		}

		return __( 'Finish this schema setup before SEOvault can publish it.', 'seovault-ai' );
	}

	/**
	 * Human summary for custom JSON setup.
	 *
	 * @param array  $custom Custom payload.
	 * @param string $status Setup status.
	 * @return string
	 */
	private function get_custom_json_summary( array $custom, $status ) {
		if ( 'needs_confirmation' === (string) ( $custom['state'] ?? '' ) || 'needs_confirmation' === (string) ( $custom['action'] ?? '' ) ) {
			return __( 'Confirm Replace mode, or switch to Add. Automatic schema still publishes until Replace is confirmed.', 'seovault-ai' );
		}
		if ( SchemaSetupStatus::INVALID === $status || SchemaSetupStatus::WITHHELD === $status ) {
			$code = $this->first_issue_code( $custom );
			if ( 'json.syntax.invalid' === $code ) {
				return __( 'Fix the custom JSON syntax. SEOvault withheld it from the published graph.', 'seovault-ai' );
			}
			if ( 'json.input.too_large' === $code ) {
				return __( 'Reduce the custom JSON size. SEOvault withheld it from the published graph.', 'seovault-ai' );
			}
			return __( 'Review the custom JSON. SEOvault withheld it from the published graph.', 'seovault-ai' );
		}

		return __( 'Custom JSON setup needs attention.', 'seovault-ai' );
	}

	/**
	 * Map readiness field/action keys to author-facing instructions.
	 *
	 * @param array $row Readiness diagnostic row.
	 * @return string
	 */
	private function present_readiness_action( array $row ) {
		$code       = isset( $row['code'] ) ? (string) $row['code'] : '';
		$action_key = isset( $row['action_key'] ) ? (string) $row['action_key'] : '';
		$field_key  = isset( $row['field_key'] ) ? (string) $row['field_key'] : '';
		$type       = isset( $row['type'] ) ? (string) $row['type'] : '';

		$catalog = array(
			'add_article_image'             => __( 'Add a featured image to improve Article markup.', 'seovault-ai' ),
			'add_article_author'            => __( 'Add an author to improve Article markup.', 'seovault-ai' ),
			'add_identity_image'            => __( 'Add an organization or person image to improve site identity markup.', 'seovault-ai' ),
			'add_event_image'               => __( 'Add an Event image.', 'seovault-ai' ),
			'add_recipe_image'              => __( 'Add a Recipe image.', 'seovault-ai' ),
			'add_product_image'             => __( 'Add a Product image.', 'seovault-ai' ),
			'add_recipe_author'             => __( 'Add a Recipe author.', 'seovault-ai' ),
			'add_operating_system'          => __( 'Add the supported operating system.', 'seovault-ai' ),
			'add_event_start_date'          => __( 'Add a start date.', 'seovault-ai' ),
			'add_event_location'            => __( 'Add a location.', 'seovault-ai' ),
			'fix_breadcrumbs'               => __( 'SEOvault could not build one breadcrumb item correctly.', 'seovault-ai' ),
			'configure_search_target'       => __( 'Configure a working site search URL.', 'seovault-ai' ),
			'configure_search_query'        => __( 'Configure the site search query input.', 'seovault-ai' ),
			'choose_supported_type'         => __( 'Choose a supported schema type.', 'seovault-ai' ),
			'provide_valid_schema_fields'   => __( 'Provide the required fields for this schema type.', 'seovault-ai' ),
			'add_complete_faq_pair'         => __( 'Add at least one complete question and answer in the FAQ block.', 'seovault-ai' ),
			'review_partial_faq_rows'       => __( 'Complete or remove unfinished FAQ rows. Incomplete pairs are excluded from FAQ schema.', 'seovault-ai' ),
			'remove_duplicate_faq_question' => __( 'Remove duplicate FAQ questions. Only the first matching answer is kept.', 'seovault-ai' ),
			'add_howto_name'               => __( 'Add a How-to name in the How-to block.', 'seovault-ai' ),
			'add_howto_step'               => __( 'Add at least one complete step in the How-to block.', 'seovault-ai' ),
			'review_partial_howto_steps'   => __( 'Complete or remove unfinished How-to steps. Incomplete steps are excluded from How-to schema.', 'seovault-ai' ),
			'confirm_visible_content'      => __( 'Confirm that matching content is visible on this page (or keep a SEOvault block/summary).', 'seovault-ai' ),
			'add_event_name'               => __( 'Add an Event name.', 'seovault-ai' ),
			'add_course_name'              => __( 'Add a Course name.', 'seovault-ai' ),
			'add_course_description'       => __( 'Add a Course description.', 'seovault-ai' ),
			'add_course_provider'          => __( 'Add a Course provider.', 'seovault-ai' ),
			'add_job_title'                => __( 'Add a job title.', 'seovault-ai' ),
			'add_job_description'          => __( 'Add a job description.', 'seovault-ai' ),
			'add_job_date_posted'          => __( 'Add the date the job was posted.', 'seovault-ai' ),
			'add_hiring_organization'      => __( 'Add the hiring organization.', 'seovault-ai' ),
			'add_job_location_or_remote'   => __( 'Add a job location or mark the role as remote.', 'seovault-ai' ),
			'update_or_remove_expired_event' => __( 'Update or remove this expired Event.', 'seovault-ai' ),
			'update_or_remove_expired_job'   => __( 'Update or remove this expired Job posting.', 'seovault-ai' ),
		);

		if ( isset( $catalog[ $action_key ] ) ) {
			return $catalog[ $action_key ];
		}

		$field_labels = array(
			'image'            => __( 'image', 'seovault-ai' ),
			'author'           => __( 'author', 'seovault-ai' ),
			'startDate'        => __( 'start date', 'seovault-ai' ),
			'location'         => __( 'location', 'seovault-ai' ),
			'name'             => __( 'name', 'seovault-ai' ),
			'description'      => __( 'description', 'seovault-ai' ),
			'title'            => __( 'title', 'seovault-ai' ),
			'provider'         => __( 'provider', 'seovault-ai' ),
			'datePosted'       => __( 'date posted', 'seovault-ai' ),
			'hiringOrganization' => __( 'hiring organization', 'seovault-ai' ),
			'jobLocation'      => __( 'job location', 'seovault-ai' ),
			'visibility'       => __( 'visibility confirmation', 'seovault-ai' ),
			'operatingSystem'  => __( 'operating system', 'seovault-ai' ),
			'itemListElement'  => __( 'breadcrumb items', 'seovault-ai' ),
			'target'           => __( 'search target', 'seovault-ai' ),
			'query-input'      => __( 'search query input', 'seovault-ai' ),
		);

		if ( isset( $field_labels[ $field_key ] ) ) {
			$field = $field_labels[ $field_key ];
			if ( '' !== $type ) {
				return sprintf(
					/* translators: 1: schema type, 2: field label. */
					__( 'Add a %2$s for %1$s.', 'seovault-ai' ),
					$type,
					$field
				);
			}
			return sprintf(
				/* translators: %s: field label. */
				__( 'Add a %s.', 'seovault-ai' ),
				$field
			);
		}

		if ( false !== strpos( $code, 'image_missing' ) ) {
			return __( 'Add a featured image to improve Article markup.', 'seovault-ai' );
		}

		return '';
	}

	/**
	 * Setup status labels.
	 *
	 * @param string $status Setup status.
	 * @return string
	 */
	private function get_setup_status_label( $status ) {
		$labels = array(
			SchemaSetupStatus::DRAFT             => __( 'Draft', 'seovault-ai' ),
			SchemaSetupStatus::NEEDS_INFORMATION => __( 'Needs information', 'seovault-ai' ),
			SchemaSetupStatus::READY             => __( 'Ready', 'seovault-ai' ),
			SchemaSetupStatus::WITHHELD          => __( 'Withheld', 'seovault-ai' ),
			SchemaSetupStatus::INVALID           => __( 'Withheld', 'seovault-ai' ),
			SchemaSetupStatus::EMITTED           => __( 'Ready', 'seovault-ai' ),
		);

		return isset( $labels[ $status ] ) ? $labels[ $status ] : __( 'Needs information', 'seovault-ai' );
	}

	/**
	 * Scope explanation copy.
	 *
	 * @param string $scope Scope key.
	 * @return string
	 */
	private function get_scope_copy( $scope ) {
		if ( PostSchemaValidationService::SCOPE_SAVED_CUSTOM === $scope ) {
			return __( 'Result reflects the most recently saved custom schema.', 'seovault-ai' );
		}
		if ( PostSchemaValidationService::SCOPE_EDITOR_GENERATED === $scope ) {
			return __( 'The custom schema field is empty in the editor. This previews the generated graph; save the post to remove the saved custom override.', 'seovault-ai' );
		}
		if ( PostSchemaValidationService::SCOPE_EDITOR_CUSTOM === $scope ) {
			return __( 'Result reflects the current custom schema field. Save the post to persist changes.', 'seovault-ai' );
		}

		return __( 'Result reflects the saved graph used by SEOvault’s frontend renderer.', 'seovault-ai' );
	}

	/**
	 * Collect distinct friendly entity labels.
	 *
	 * @param array $entities Entity summaries.
	 * @return string[]
	 */
	private function collect_entity_labels( $entities ) {
		if ( ! is_array( $entities ) || empty( $entities ) ) {
			return array();
		}

		$labels = array();
		foreach ( $entities as $entity ) {
			if ( ! is_array( $entity ) ) {
				continue;
			}
			$type = isset( $entity['type'] ) ? (string) $entity['type'] : '';
			if ( '' === $type ) {
				continue;
			}
			$primary = trim( explode( ',', $type )[0] );
			$primary = $this->friendly_entity_label( $primary );
			if ( '' !== $primary ) {
				$labels[ $primary ] = $primary;
			}
		}

		return array_values( $labels );
	}

	/**
	 * Join entity labels with localized and.
	 *
	 * @param string[] $labels Labels.
	 * @return string
	 */
	private function join_entity_labels( array $labels ) {
		$count = count( $labels );
		if ( 0 === $count ) {
			return '';
		}
		if ( 1 === $count ) {
			return $labels[0];
		}
		if ( 2 === $count ) {
			return $labels[0] . ' ' . __( 'and', 'seovault-ai' ) . ' ' . $labels[1];
		}

		$last = array_pop( $labels );
		return implode( ', ', $labels ) . ', ' . __( 'and', 'seovault-ai' ) . ' ' . $last;
	}

	/**
	 * Friendly display label for a Schema.org type.
	 *
	 * @param string $type Type name.
	 * @return string
	 */
	private function friendly_entity_label( $type ) {
		$map = array(
			'BreadcrumbList' => __( 'Breadcrumb', 'seovault-ai' ),
			'WebPage'        => 'WebPage',
			'WebSite'        => 'WebSite',
			'Article'        => 'Article',
			'BlogPosting'    => 'Article',
			'NewsArticle'    => 'Article',
			'Organization'   => __( 'Organization', 'seovault-ai' ),
			'Person'         => __( 'Person', 'seovault-ai' ),
			'FAQPage'        => 'FAQ',
			'HowTo'          => 'HowTo',
			'SearchAction'   => __( 'Search', 'seovault-ai' ),
		);

		return isset( $map[ $type ] ) ? $map[ $type ] : $type;
	}

	/**
	 * First custom JSON issue code.
	 *
	 * @param array $custom Custom payload.
	 * @return string
	 */
	private function first_issue_code( array $custom ) {
		if ( empty( $custom['issues'] ) || ! is_array( $custom['issues'] ) ) {
			return '';
		}
		$first = $custom['issues'][0];
		return is_array( $first ) && isset( $first['code'] ) ? (string) $first['code'] : '';
	}

	/**
	 * Live comparison detail lines.
	 *
	 * @param array|null $live Live response.
	 * @return string[]
	 */
	private function get_live_details( $live ) {
		if ( ! is_array( $live ) ) {
			return array();
		}

		$details    = array();
		$live_data  = isset( $live['live'] ) && is_array( $live['live'] ) ? $live['live'] : array();
		$comparison = isset( $live_data['comparison'] ) && is_array( $live_data['comparison'] ) ? $live_data['comparison'] : array();
		if ( empty( $comparison ) ) {
			return $details;
		}

		$expected_only = isset( $comparison['expected_only'] ) ? count( $comparison['expected_only'] ) : 0;
		$observed_only = isset( $comparison['observed_only'] ) ? count( $comparison['observed_only'] ) : 0;
		$changed       = isset( $comparison['changed'] ) ? count( $comparison['changed'] ) : 0;
		$faq_conflicts = isset( $comparison['faq_conflicts'] ) && is_array( $comparison['faq_conflicts'] )
			? $comparison['faq_conflicts']
			: ( isset( $live_data['faq_conflicts'] ) && is_array( $live_data['faq_conflicts'] ) ? $live_data['faq_conflicts'] : array() );
		$public_duplicates = isset( $comparison['public_duplicates'] ) && is_array( $comparison['public_duplicates'] )
			? $comparison['public_duplicates']
			: ( isset( $live_data['public_duplicates'] ) && is_array( $live_data['public_duplicates'] ) ? $live_data['public_duplicates'] : array() );

		if ( ! empty( $faq_conflicts['has_duplicate_faq'] ) ) {
			$details[] = __( 'Another FAQPage was found on the published page (custom or third-party markup) in addition to SEOvault FAQ output.', 'seovault-ai' );
		}

		if ( ! empty( $public_duplicates['has_duplicates'] ) ) {
			$types = isset( $public_duplicates['duplicate_types'] ) && is_array( $public_duplicates['duplicate_types'] )
				? $public_duplicates['duplicate_types']
				: array();
			$types = array_values( array_filter( $types, 'is_string' ) );
			// FAQ already has a dedicated line above.
			$types = array_values( array_diff( $types, array( 'FAQPage' ) ) );
			if ( ! empty( $types ) ) {
				$details[] = sprintf(
					/* translators: %s: schema types. */
					__( 'Extra public schema was found on the published page (custom or third-party markup) for: %s. SEOvault does not rewrite third-party markup — choose one owner or disable SEOvault schema output.', 'seovault-ai' ),
					implode( ', ', $types )
				);
			}
		}

		if ( $expected_only > 0 ) {
			$details[] = sprintf(
				/* translators: %d: entity count. */
				_n( '%d expected entity missing from the page', '%d expected entities missing from the page', $expected_only, 'seovault-ai' ),
				$expected_only
			);
		}
		if ( $observed_only > 0 ) {
			$details[] = sprintf(
				/* translators: %d: entity count. */
				_n( '%d extra entity found on the page', '%d extra entities found on the page', $observed_only, 'seovault-ai' ),
				$observed_only
			);
		}
		if ( $changed > 0 ) {
			$details[] = sprintf(
				/* translators: %d: entity count. */
				_n( '%d entity differs from the expected output', '%d entities differ from the expected output', $changed, 'seovault-ai' ),
				$changed
			);
		}

		return $details;
	}
}
