<?php
/**
 * Wide Review schema surface for the post editor.
 *
 * @package SEOvaultSEO\Admin
 */

namespace SEOvaultSEO\Admin;

use SEOvaultSEO\Schema\Validation\SchemaHealthStatus;
use SEOvaultSEO\Schema\Validation\SchemaHealthStatusPresenter;
use SEOvaultSEO\Schema\Validation\SchemaTypeSupportRegistry;
use SEOvaultSEO\Schema\Validation\SchemaValidationFeatures;

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

/**
 * Renders the Review schema dialog shell and detail body.
 */
class SchemaReviewSurface {

	/**
	 * Source kind → author-facing label.
	 *
	 * @var array<string,string>
	 */
	private static $source_labels;

	/**
	 * Empty dialog shell rendered once in the meta box (survives panel replace).
	 *
	 * @return string
	 */
	public static function render_shell() {
		$buffer_level = ob_get_level();
		ob_start();
		try {
			?>
		<dialog
			id="seovaultseo-schema-review"
			class="seovaultseo-schema-review"
			aria-labelledby="seovaultseo-schema-review-title"
			aria-modal="true"
		>
			<div class="seovaultseo-schema-review-chrome">
				<header class="seovaultseo-schema-review-header">
					<h2 id="seovaultseo-schema-review-title"><?php esc_html_e( 'Review schema', 'seovault-ai' ); ?></h2>
					<button
						type="button"
						class="button-link seovaultseo-schema-review-close"
						aria-label="<?php esc_attr_e( 'Close review schema', 'seovault-ai' ); ?>"
					>
						<span aria-hidden="true">&times;</span>
					</button>
				</header>
				<div
					class="seovaultseo-schema-review-body"
					id="seovaultseo-schema-review-body"
					data-schema-review-body
					tabindex="-1"
				>
					<p class="seovaultseo-schema-review-placeholder">
						<?php esc_html_e( 'Loading schema details…', 'seovault-ai' ); ?>
					</p>
				</div>
				<footer class="seovaultseo-schema-review-footer">
					<button type="button" class="button button-secondary seovaultseo-schema-review-copy">
						<?php esc_html_e( 'Copy diagnostics', 'seovault-ai' ); ?>
					</button>
					<button type="button" class="button button-secondary seovaultseo-schema-review-download">
						<?php esc_html_e( 'Download diagnostics', 'seovault-ai' ); ?>
					</button>
					<button type="button" class="button button-primary seovaultseo-schema-review-close">
						<?php esc_html_e( 'Close', 'seovault-ai' ); ?>
					</button>
					<span class="seovaultseo-schema-review-copy-status" role="status" aria-live="polite" hidden></span>
				</footer>
			</div>
		</dialog>
			<?php

			return (string) ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}

	/**
	 * Render the review body from a validation response.
	 *
	 * @param array      $validation Validation response.
	 * @param array|null $live       Optional live comparison payload.
	 * @return string
	 */
	public static function render_body( $validation, $live = null ) {
		$validation = is_array( $validation ) ? $validation : array();
		$presenter  = new SchemaHealthStatusPresenter();
		$published  = $presenter->present_published( $validation );
		$setup      = $presenter->present_setup( $validation );
		$editable   = $presenter->present_editable_entities( $validation );
		$build      = isset( $validation['build'] ) && is_array( $validation['build'] ) ? $validation['build'] : array();
		$result     = isset( $validation['result'] ) && is_array( $validation['result'] ) ? $validation['result'] : array();
		$issues     = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();
		$emitted    = isset( $build['emitted'] ) && is_array( $build['emitted'] ) ? $build['emitted'] : array();
		$conflicts  = isset( $build['conflicts'] ) && is_array( $build['conflicts'] ) ? $build['conflicts'] : array();
		$improvements = isset( $build['improvements'] ) && is_array( $build['improvements'] ) ? $build['improvements'] : array();
		$live_enabled = SchemaValidationFeatures::is_enabled( SchemaValidationFeatures::LIVE );

		if ( null === $live && isset( $validation['live'] ) && is_array( $validation['live'] ) ) {
			$live = $validation['live'];
		}
		if ( ! $live_enabled ) {
			$live = null;
		}

		$diagnostics = self::build_diagnostics_payload( $validation, $live );
		$diagnostics_json = wp_json_encode( $diagnostics );
		if ( ! is_string( $diagnostics_json ) ) {
			$diagnostics_json = '{}';
		}

		$buffer_level = ob_get_level();
		ob_start();
		try {
			?>
		<div
			class="seovaultseo-schema-review-content seovaultseo-schema-review-content-<?php echo esc_attr( $published['status'] ); ?>"
			data-schema-review-diagnostics="<?php echo esc_attr( $diagnostics_json ); ?>"
		>
			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-status-heading">
				<h3 id="seovaultseo-schema-review-status-heading"><?php esc_html_e( 'Status', 'seovault-ai' ); ?></h3>
				<p class="seovaultseo-schema-review-status">
					<span class="seovaultseo-schema-health-icon" aria-hidden="true"><?php echo esc_html( $published['icon'] ); ?></span>
					<strong><?php echo esc_html( $published['label'] ); ?></strong>
				</p>
				<?php if ( '' !== $published['entities_summary'] ) : ?>
					<p><?php echo esc_html( $published['entities_summary'] ); ?></p>
				<?php endif; ?>
				<p><?php echo esc_html( $published['summary'] ); ?></p>
				<p class="seovaultseo-schema-review-muted"><?php echo esc_html( $published['scope'] ); ?></p>
			</section>

			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-emitted-heading">
				<h3 id="seovaultseo-schema-review-emitted-heading"><?php esc_html_e( 'Emitted schema', 'seovault-ai' ); ?></h3>
				<?php if ( empty( $emitted ) && empty( $editable['items'] ) ) : ?>
					<p class="seovaultseo-schema-review-muted"><?php esc_html_e( 'No schema entities are currently emitted for this page.', 'seovault-ai' ); ?></p>
				<?php else : ?>
					<ul class="seovaultseo-schema-review-list">
						<?php foreach ( $emitted as $row ) : ?>
							<?php if ( ! is_array( $row ) ) { continue; } ?>
							<li>
								<span class="seovaultseo-schema-review-type"><?php echo esc_html( (string) ( $row['type'] ?? '' ) ); ?></span>
								<span class="seovaultseo-schema-review-source"><?php echo esc_html( self::resolve_emitted_source_label( $row ) ); ?></span>
								<?php if ( ! empty( $row['entity_id'] ) ) : ?>
									<code class="seovaultseo-schema-review-id"><?php echo esc_html( (string) $row['entity_id'] ); ?></code>
								<?php endif; ?>
								<?php
								$support = self::present_type_support_note( (string) ( $row['type'] ?? '' ) );
								if ( is_array( $support ) ) :
									?>
									<p class="seovaultseo-schema-review-muted seovaultseo-schema-review-support">
										<?php echo esc_html( $support['summary'] ); ?>
										<?php if ( ! empty( $support['learn_more_url'] ) ) : ?>
											<a href="<?php echo esc_url( $support['learn_more_url'] ); ?>" target="_blank" rel="noopener noreferrer">
												<?php esc_html_e( 'Learn more', 'seovault-ai' ); ?>
											</a>
										<?php endif; ?>
									</p>
								<?php endif; ?>
							</li>
						<?php endforeach; ?>
						<?php foreach ( $editable['items'] as $item ) : ?>
							<?php if ( ! is_array( $item ) ) { continue; } ?>
							<li
								data-instance-id="<?php echo esc_attr( (string) ( $item['instance_id'] ?? '' ) ); ?>"
								data-schema-type="<?php echo esc_attr( (string) ( $item['type'] ?? '' ) ); ?>"
							>
								<span class="seovaultseo-schema-review-type"><?php echo esc_html( (string) ( $item['type'] ?? '' ) ); ?></span>
								<span class="seovaultseo-schema-review-badge"><?php echo esc_html( (string) ( $item['status_label'] ?? '' ) ); ?></span>
								<button
									type="button"
									class="button-link seovaultseo-schema-entity-edit"
									data-schema-setup-action="edit"
									data-instance-id="<?php echo esc_attr( (string) ( $item['instance_id'] ?? '' ) ); ?>"
									data-schema-type="<?php echo esc_attr( (string) ( $item['type'] ?? '' ) ); ?>"
								>
									<?php esc_html_e( 'Edit', 'seovault-ai' ); ?>
								</button>
							</li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			</section>

			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-setup-heading">
				<h3 id="seovaultseo-schema-review-setup-heading"><?php esc_html_e( 'Unfinished setup', 'seovault-ai' ); ?></h3>
				<?php if ( empty( $setup['items'] ) ) : ?>
					<p class="seovaultseo-schema-review-muted"><?php esc_html_e( 'No unfinished schema setup items.', 'seovault-ai' ); ?></p>
				<?php else : ?>
					<ul class="seovaultseo-schema-review-list seovaultseo-schema-health-setup-list">
						<?php foreach ( $setup['items'] as $item ) : ?>
							<li
								data-instance-id="<?php echo esc_attr( (string) ( $item['instance_id'] ?? '' ) ); ?>"
								data-schema-type="<?php echo esc_attr( (string) ( $item['type'] ?? '' ) ); ?>"
								data-focus-field="<?php echo esc_attr( (string) ( $item['first_field_key'] ?? '' ) ); ?>"
							>
								<span class="seovaultseo-schema-review-type"><?php echo esc_html( $item['type'] ); ?></span>
								<span class="seovaultseo-schema-review-badge"><?php echo esc_html( $item['status_label'] ); ?></span>
								<span><?php echo esc_html( $item['summary'] ); ?></span>
								<?php if ( ! empty( $item['editable'] ) ) : ?>
									<button
										type="button"
										class="button-link seovaultseo-schema-setup-complete"
										data-schema-setup-action="complete"
										data-instance-id="<?php echo esc_attr( (string) ( $item['instance_id'] ?? '' ) ); ?>"
										data-schema-type="<?php echo esc_attr( (string) ( $item['type'] ?? '' ) ); ?>"
										data-focus-field="<?php echo esc_attr( (string) ( $item['first_field_key'] ?? '' ) ); ?>"
									>
										<?php esc_html_e( 'Complete', 'seovault-ai' ); ?>
									</button>
								<?php endif; ?>
								<?php if ( ! empty( $item['removable'] ) ) : ?>
									<button
										type="button"
										class="button-link seovaultseo-schema-setup-remove"
										data-schema-setup-action="remove"
										data-instance-id="<?php echo esc_attr( (string) ( $item['instance_id'] ?? '' ) ); ?>"
										data-schema-type="<?php echo esc_attr( (string) ( $item['type'] ?? '' ) ); ?>"
									>
										<?php esc_html_e( 'Remove', 'seovault-ai' ); ?>
									</button>
								<?php endif; ?>
							</li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			</section>

			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-improvements-heading">
				<h3 id="seovaultseo-schema-review-improvements-heading"><?php esc_html_e( 'Improvements', 'seovault-ai' ); ?></h3>
				<?php
				$improvement_lines = array();
				if ( SchemaHealthStatus::IMPROVEMENTS_AVAILABLE === $published['status'] && ! empty( $published['actions'] ) ) {
					$improvement_lines = array_slice( $published['actions'], 0, 10 );
				} else {
					foreach ( $improvements as $row ) {
						if ( ! is_array( $row ) ) {
							continue;
						}
						$line = self::present_improvement_line( $row );
						if ( '' !== $line ) {
							$improvement_lines[] = $line;
						}
						if ( count( $improvement_lines ) >= 10 ) {
							break;
						}
					}
				}
				if ( empty( $improvement_lines ) ) :
					?>
					<p class="seovaultseo-schema-review-muted"><?php esc_html_e( 'No improvements suggested right now.', 'seovault-ai' ); ?></p>
				<?php else : ?>
					<ul class="seovaultseo-schema-review-list">
						<?php foreach ( $improvement_lines as $action ) : ?>
							<li><?php echo esc_html( (string) $action ); ?></li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			</section>

			<?php echo self::render_custom_json_section( $validation ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Renderer escapes. ?>

			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-conflicts-heading">
				<h3 id="seovaultseo-schema-review-conflicts-heading"><?php esc_html_e( 'Conflicts and duplicates', 'seovault-ai' ); ?></h3>
				<?php
				$custom_conflicts = array();
				foreach ( $conflicts as $conflict ) {
					if ( ! is_array( $conflict ) ) {
						continue;
					}
					$sources = array();
					if ( isset( $conflict['sources'] ) && is_array( $conflict['sources'] ) ) {
						$sources = $conflict['sources'];
					}
					$losers = isset( $conflict['loser_sources'] ) && is_array( $conflict['loser_sources'] ) ? $conflict['loser_sources'] : array();
					$winner = (string) ( $conflict['winner_source'] ?? '' );
					if ( in_array( 'custom', $sources, true ) || in_array( 'custom', $losers, true ) || 'custom' === $winner ) {
						$custom_conflicts[] = $conflict;
					}
				}
				?>
				<?php if ( empty( $conflicts ) ) : ?>
					<p class="seovaultseo-schema-review-muted"><?php esc_html_e( 'No schema source conflicts detected.', 'seovault-ai' ); ?></p>
				<?php else : ?>
					<ul class="seovaultseo-schema-review-list">
						<?php foreach ( $conflicts as $conflict ) : ?>
							<?php if ( ! is_array( $conflict ) ) { continue; } ?>
							<li>
								<span class="seovaultseo-schema-review-type"><?php echo esc_html( (string) ( $conflict['type'] ?? '' ) ); ?></span>
								<span><?php echo esc_html( self::present_conflict_summary( $conflict ) ); ?></span>
							</li>
						<?php endforeach; ?>
					</ul>
					<?php if ( ! empty( $custom_conflicts ) ) : ?>
						<p class="seovaultseo-schema-review-muted">
							<?php esc_html_e( 'Conflicts involving Custom JSON are listed above. Open Advanced JSON-LD in the sidebar to change Add/Replace mode.', 'seovault-ai' ); ?>
						</p>
					<?php endif; ?>
				<?php endif; ?>
			</section>

			<?php if ( $live_enabled ) : ?>
				<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-live-heading">
					<h3 id="seovaultseo-schema-review-live-heading"><?php esc_html_e( 'Published-page comparison', 'seovault-ai' ); ?></h3>
					<?php
					echo SchemaHealthPanel::render_live_section( $live ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Renderer escapes.
					?>
				</section>
			<?php endif; ?>

			<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-issues-heading">
				<h3 id="seovaultseo-schema-review-issues-heading"><?php esc_html_e( 'Validation details', 'seovault-ai' ); ?></h3>
				<?php if ( empty( $issues ) ) : ?>
					<p class="seovaultseo-schema-review-muted"><?php esc_html_e( 'No validation issues to list.', 'seovault-ai' ); ?></p>
				<?php else : ?>
					<?php echo SchemaHealthPanel::render_details( $issues ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Renderer escapes. ?>
				<?php endif; ?>
			</section>

			<details class="seovaultseo-schema-review-technical">
				<summary><?php esc_html_e( 'Technical disclosures', 'seovault-ai' ); ?></summary>
				<dl>
					<div>
						<dt><?php esc_html_e( 'Internal state', 'seovault-ai' ); ?></dt>
						<dd><code><?php echo esc_html( isset( $validation['state'] ) ? (string) $validation['state'] : '' ); ?></code></dd>
					</div>
					<div>
						<dt><?php esc_html_e( 'Scope', 'seovault-ai' ); ?></dt>
						<dd><code><?php echo esc_html( isset( $validation['scope'] ) ? (string) $validation['scope'] : '' ); ?></code></dd>
					</div>
					<div>
						<dt><?php esc_html_e( 'Graph hash', 'seovault-ai' ); ?></dt>
						<dd><code><?php echo esc_html( (string) ( $build['graph_hash'] ?? '' ) ); ?></code></dd>
					</div>
					<div>
						<dt><?php esc_html_e( 'Rule set', 'seovault-ai' ); ?></dt>
						<dd><code><?php echo esc_html( (string) ( $result['rule_set_version'] ?? '' ) ); ?></code></dd>
					</div>
					<div>
						<dt><?php esc_html_e( 'Emitted count', 'seovault-ai' ); ?></dt>
						<dd><?php echo (int) ( $build['emitted_count'] ?? count( $emitted ) ); ?></dd>
					</div>
					<div>
						<dt><?php esc_html_e( 'Withheld count', 'seovault-ai' ); ?></dt>
						<dd><?php echo (int) ( $build['withheld_count'] ?? count( $setup['items'] ) ); ?></dd>
					</div>
				</dl>
			</details>
		</div>
			<?php

			return (string) ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}

	/**
	 * Custom JSON status section for Review schema.
	 *
	 * Distinguishes invalid saved value, emitted mode, and current published graph.
	 * Unsaved editor dirtiness is handled in the sidebar editor, not here.
	 *
	 * @param array $validation Validation response.
	 * @return string
	 */
	private static function render_custom_json_section( array $validation ) {
		$setup  = isset( $validation['setup'] ) && is_array( $validation['setup'] ) ? $validation['setup'] : array();
		$custom = isset( $setup['custom_json'] ) && is_array( $setup['custom_json'] ) ? $setup['custom_json'] : null;
		$ux     = isset( $custom['ux'] ) && is_array( $custom['ux'] ) ? $custom['ux'] : null;

		$label   = is_array( $ux ) ? (string) ( $ux['label'] ?? '' ) : '';
		$summary = is_array( $ux ) ? (string) ( $ux['summary'] ?? '' ) : '';
		$published = is_array( $ux ) ? (string) ( $ux['published_graph'] ?? 'automatic' ) : 'automatic';
		$mode    = is_array( $custom ) ? (string) ( $custom['mode'] ?? '' ) : '';
		$status  = is_array( $custom ) ? (string) ( $custom['status'] ?? '' ) : '';

		if ( ! is_array( $custom ) ) {
			$label     = __( 'No custom JSON', 'seovault-ai' );
			$summary   = __( 'Automatic schema publishes for this page. Custom JSON is optional.', 'seovault-ai' );
			$published = 'automatic';
		} elseif ( '' === $label ) {
			if ( 'emitted' === $status && 'replace' === $mode ) {
				$label     = __( 'Saved — Replace mode', 'seovault-ai' );
				$summary   = __( 'This custom JSON replaces automatic schema entities on this page.', 'seovault-ai' );
				$published = 'custom_replace';
			} elseif ( 'emitted' === $status ) {
				$label     = __( 'Saved — Add mode', 'seovault-ai' );
				$summary   = __( 'This custom JSON merges with automatic schema in the published graph.', 'seovault-ai' );
				$published = 'automatic_plus_custom';
			} elseif ( 'needs_confirmation' === (string) ( $custom['state'] ?? '' ) || 'needs_confirmation' === (string) ( $custom['action'] ?? '' ) ) {
				$label     = __( 'Replace not confirmed', 'seovault-ai' );
				$summary   = __( 'Replace is selected but not confirmed. Automatic schema still publishes; custom JSON is withheld.', 'seovault-ai' );
			} else {
				$label   = __( 'Invalid saved JSON', 'seovault-ai' );
				$summary = __( 'The saved custom JSON is invalid and withheld. The published graph continues without it.', 'seovault-ai' );
			}
		}

		$buffer_level = ob_get_level();
		ob_start();
		try {
			?>
		<section class="seovaultseo-schema-review-section" aria-labelledby="seovaultseo-schema-review-custom-json-heading">
			<h3 id="seovaultseo-schema-review-custom-json-heading"><?php esc_html_e( 'Custom JSON', 'seovault-ai' ); ?></h3>
			<p>
				<strong><?php echo esc_html( $label ); ?></strong>
				<?php if ( '' !== $mode ) : ?>
					<span class="seovaultseo-schema-review-badge"><?php echo esc_html( $mode ); ?></span>
				<?php endif; ?>
			</p>
			<p><?php echo esc_html( $summary ); ?></p>
			<p class="seovaultseo-schema-review-muted"><?php echo esc_html( SchemaCustomJsonEditor::published_graph_label( $published ) ); ?></p>
			<p class="seovaultseo-schema-review-muted">
				<?php esc_html_e( 'Saved custom JSON is separate from unsaved editor text. Open Advanced JSON-LD in the sidebar to edit, format, preview, or restore the last valid value.', 'seovault-ai' ); ?>
			</p>
			<button
				type="button"
				class="button-link seovaultseo-schema-entity-edit"
				data-schema-setup-action="focus_custom_json"
			>
				<?php esc_html_e( 'Open Advanced JSON-LD', 'seovault-ai' ); ?>
			</button>
		</section>
			<?php

			return (string) ob_get_clean();
		} finally {
			while ( ob_get_level() > $buffer_level ) {
				ob_end_clean();
			}
		}
	}

	/**
	 * Build a support diagnostics payload without secrets or full post content.
	 *
	 * @param array      $validation Validation response.
	 * @param array|null $live       Live payload.
	 * @return array
	 */
	public static function build_diagnostics_payload( array $validation, $live = null ) {
		$result = isset( $validation['result'] ) && is_array( $validation['result'] ) ? $validation['result'] : array();
		$build  = isset( $validation['build'] ) && is_array( $validation['build'] ) ? $validation['build'] : array();
		$setup  = isset( $validation['setup'] ) && is_array( $validation['setup'] ) ? $validation['setup'] : array();
		$issues = isset( $result['issues'] ) && is_array( $result['issues'] ) ? $result['issues'] : array();

		$safe_issues = array();
		foreach ( $issues as $issue ) {
			if ( ! is_array( $issue ) ) {
				continue;
			}
			$safe_issues[] = array(
				'code'        => (string) ( $issue['code'] ?? '' ),
				'severity'    => (string) ( $issue['severity'] ?? '' ),
				'entity_type' => (string) ( $issue['entity_type'] ?? '' ),
				'path'        => (string) ( $issue['path'] ?? '' ),
				'category'    => (string) ( $issue['category'] ?? '' ),
			);
		}

		$live_safe = null;
		if ( is_array( $live ) ) {
			$live_safe = array(
				'state'   => (string) ( $live['state'] ?? '' ),
				'message' => (string) ( $live['message'] ?? '' ),
				'scope'   => (string) ( $live['scope'] ?? '' ),
			);
		}

		return array(
			'generated_at'     => gmdate( 'c' ),
			'plugin'           => 'seovault-ai',
			'state'            => (string) ( $validation['state'] ?? '' ),
			'scope'            => (string) ( $validation['scope'] ?? '' ),
			'rule_set_version' => (string) ( $result['rule_set_version'] ?? '' ),
			'counts'           => array(
				'errors'          => (int) ( $result['errors'] ?? 0 ),
				'warnings'        => (int) ( $result['warnings'] ?? 0 ),
				'recommendations' => (int) ( $result['recommendations'] ?? 0 ),
				'emitted'         => (int) ( $build['emitted_count'] ?? 0 ),
				'withheld'        => (int) ( $build['withheld_count'] ?? 0 ),
			),
			'graph_hash'       => (string) ( $build['graph_hash'] ?? '' ),
			'emitted'          => isset( $build['emitted'] ) && is_array( $build['emitted'] ) ? $build['emitted'] : array(),
			'conflicts'        => isset( $build['conflicts'] ) && is_array( $build['conflicts'] ) ? $build['conflicts'] : array(),
			'improvements'     => isset( $build['improvements'] ) && is_array( $build['improvements'] ) ? $build['improvements'] : array(),
			'setup'            => self::redact_setup( $setup ),
			'issues'           => $safe_issues,
			'live'             => $live_safe,
			// Explicitly omitted: post_content, custom JSON body, credentials, full graph payloads.
			'omitted'          => array( 'post_content', 'custom_json_body', 'secrets', 'full_graph' ),
		);
	}

	/**
	 * Human source label.
	 *
	 * @param string $source Source kind.
	 * @return string
	 */
	public static function get_source_label( $source ) {
		$source = (string) $source;
		if ( null === self::$source_labels ) {
			self::$source_labels = array(
				'foundational'  => __( 'Automatic schema', 'seovault-ai' ),
				'post_instance' => __( 'Guided form', 'seovault-ai' ),
				'block'         => __( 'Content block', 'seovault-ai' ),
				'template'      => __( 'Template', 'seovault-ai' ),
				'integration'   => __( 'Integration', 'seovault-ai' ),
				'custom'        => __( 'Custom JSON', 'seovault-ai' ),
				'extension'     => __( 'Extension', 'seovault-ai' ),
			);
		}

		if ( isset( self::$source_labels[ $source ] ) ) {
			return self::$source_labels[ $source ];
		}
		if ( '' === $source ) {
			return __( 'Unknown source', 'seovault-ai' );
		}

		return $source;
	}

	/**
	 * Resolve an author-facing source label for an emitted descriptor row.
	 *
	 * @param array $row Emitted descriptor.
	 * @return string
	 */
	public static function resolve_emitted_source_label( array $row ) {
		$source     = (string) ( $row['source'] ?? '' );
		$source_ref = (string) ( $row['source_ref'] ?? '' );
		$type       = (string) ( $row['type'] ?? '' );

		if ( 0 === strpos( $source_ref, 'block:seovault/faq' ) || ( 'block' === $source && 'FAQPage' === $type ) ) {
			return __( 'FAQ block', 'seovault-ai' );
		}
		if ( 0 === strpos( $source_ref, 'block:seovault/howto' ) || ( 'block' === $source && 'HowTo' === $type ) ) {
			return __( 'How-to block', 'seovault-ai' );
		}

		return self::get_source_label( $source );
	}

	/**
	 * Resolve an author-facing source label for setup / editable-entity rows.
	 *
	 * Prefers template/integration activation markers on source_meta when present.
	 *
	 * @param array $row Setup or entity row.
	 * @return string
	 */
	public static function resolve_setup_source_label( array $row ) {
		$meta = isset( $row['source_meta'] ) && is_array( $row['source_meta'] ) ? $row['source_meta'] : array();
		$activation = '';
		foreach ( array( 'activation_source', 'origin', 'template_source' ) as $key ) {
			if ( ! empty( $meta[ $key ] ) ) {
				$activation = sanitize_key( (string) $meta[ $key ] );
				break;
			}
		}
		if ( in_array( $activation, array( 'template', 'integration' ), true ) ) {
			return self::get_source_label( $activation );
		}

		$source = (string) ( $row['source'] ?? '' );
		if ( in_array( $source, array( 'template', 'integration', 'block', 'custom', 'extension', 'foundational' ), true ) ) {
			return self::get_source_label( $source );
		}
		if ( in_array( $source, array( 'post_instance', 'manual', 'migration', 'api', 'web', '' ), true ) ) {
			return self::get_source_label( 'post_instance' );
		}

		return self::get_source_label( $source );
	}

	/**
	 * Versioned Google/Schema.org support note for specialized types that need it.
	 *
	 * @param string $type Schema type.
	 * @return array{summary:string,learn_more_url:string}|null
	 */
	public static function present_type_support_note( $type ) {
		$type = (string) $type;
		if ( 'FAQPage' !== $type && 'HowTo' !== $type ) {
			return null;
		}

		$copy = SchemaTypeSupportRegistry::get_author_copy( $type );
		if ( ! is_array( $copy ) || '' === (string) ( $copy['summary'] ?? '' ) ) {
			return null;
		}

		return array(
			'summary'        => (string) $copy['summary'],
			'learn_more_url' => (string) ( $copy['learn_more_url'] ?? '' ),
		);
	}

	/**
	 * @param array $row Improvement row.
	 * @return string
	 */
	private static function present_improvement_line( array $row ) {
		$action_key = (string) ( $row['action_key'] ?? '' );
		$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' ),
			'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_complete_faq_pair'         => __( 'Add at least one complete question and answer in the FAQ block.', 'seovault-ai' ),
			'convert_legacy_faq_rich_block' => __( 'Convert the legacy web FAQ rich block to the SEOvault FAQ block to avoid duplicate schema.', 'seovault-ai' ),
			'convert_thirdparty_blocks'     => __( 'Convert Yoast or Rank Math FAQ/How-to blocks to SEOvault blocks to restore editing and schema.', 'seovault-ai' ),
			'review_partial_howto_steps'   => __( 'Complete or remove unfinished How-to steps. Incomplete steps are excluded from How-to schema.', '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' ),
		);
		if ( isset( $catalog[ $action_key ] ) ) {
			return $catalog[ $action_key ];
		}
		$code = (string) ( $row['code'] ?? '' );
		if ( 'faq.block.partial_rows' === $code ) {
			return $catalog['review_partial_faq_rows'];
		}
		if ( 'faq.block.duplicate_question' === $code || 'faq.duplicate_question' === $code ) {
			return $catalog['remove_duplicate_faq_question'];
		}
		if ( 'faq.legacy_rich_block.conversion_available' === $code ) {
			return $catalog['convert_legacy_faq_rich_block'];
		}
		if ( 'thirdparty.block.conversion_available' === $code ) {
			return $catalog['convert_thirdparty_blocks'];
		}
		if ( 'howto.block.partial_steps' === $code ) {
			return $catalog['review_partial_howto_steps'];
		}
		$type = (string) ( $row['type'] ?? '' );
		if ( '' !== $type && '' !== $code ) {
			return sprintf(
				/* translators: 1: schema type, 2: diagnostic code. */
				__( 'Improve %1$s (%2$s).', 'seovault-ai' ),
				$type,
				$code
			);
		}
		return '' !== $code ? $code : '';
	}

	/**
	 * @param array $conflict Conflict row.
	 * @return string
	 */
	private static function present_conflict_summary( array $conflict ) {
		$reason = (string) ( $conflict['reason'] ?? '' );
		if ( '' !== $reason && false === strpos( $reason, '@id' ) ) {
			// Prefer actionable copy when reason is already human.
			return $reason;
		}

		$winner = self::get_source_label( (string) ( $conflict['winner_source'] ?? '' ) );
		$losers = array();
		if ( isset( $conflict['loser_sources'] ) && is_array( $conflict['loser_sources'] ) ) {
			foreach ( $conflict['loser_sources'] as $source ) {
				$losers[] = self::get_source_label( (string) $source );
			}
		}
		if ( empty( $losers ) && isset( $conflict['sources'] ) && is_array( $conflict['sources'] ) ) {
			foreach ( $conflict['sources'] as $source ) {
				$label = self::get_source_label( (string) $source );
				if ( $label !== $winner ) {
					$losers[] = $label;
				}
			}
		}

		if ( '' !== $winner && ! empty( $losers ) ) {
			return sprintf(
				/* translators: 1: winning source label, 2: losing source labels. */
				__( 'Conflicting data for the same schema identity. %1$s kept; %2$s not used.', 'seovault-ai' ),
				$winner,
				implode( ', ', array_unique( $losers ) )
			);
		}

		return __( 'Conflicting schema sources were detected for the same identity.', 'seovault-ai' );
	}

	/**
	 * Redact setup payload for diagnostics export.
	 *
	 * @param array $setup Setup payload.
	 * @return array
	 */
	private static function redact_setup( array $setup ) {
		$withheld = array();
		if ( isset( $setup['withheld'] ) && is_array( $setup['withheld'] ) ) {
			foreach ( $setup['withheld'] as $row ) {
				if ( ! is_array( $row ) ) {
					continue;
				}
				$withheld[] = array(
					'type'   => (string) ( $row['type'] ?? '' ),
					'source' => (string) ( $row['source'] ?? '' ),
					'reason' => (string) ( $row['reason'] ?? '' ),
					'status' => (string) ( $row['status'] ?? '' ),
					'setup'  => isset( $row['setup'] ) && is_array( $row['setup'] ) ? $row['setup'] : array(),
				);
			}
		}

		$custom = null;
		if ( isset( $setup['custom_json'] ) && is_array( $setup['custom_json'] ) ) {
			$custom = array(
				'status' => (string) ( $setup['custom_json']['status'] ?? '' ),
				'mode'   => (string) ( $setup['custom_json']['mode'] ?? '' ),
				'state'  => (string) ( $setup['custom_json']['state'] ?? '' ),
				'errors' => (int) ( $setup['custom_json']['errors'] ?? 0 ),
				'codes'  => array(),
			);
			if ( isset( $setup['custom_json']['ux'] ) && is_array( $setup['custom_json']['ux'] ) ) {
				$custom['ux_state']           = (string) ( $setup['custom_json']['ux']['state'] ?? '' );
				$custom['published_graph']    = (string) ( $setup['custom_json']['ux']['published_graph'] ?? '' );
			}
			if ( isset( $setup['custom_json']['issues'] ) && is_array( $setup['custom_json']['issues'] ) ) {
				foreach ( $setup['custom_json']['issues'] as $issue ) {
					if ( is_array( $issue ) && isset( $issue['code'] ) ) {
						$custom['codes'][] = (string) $issue['code'];
					}
				}
			}
		}

		return array(
			'withheld'    => $withheld,
			'custom_json' => $custom,
		);
	}
}
