<?php
/**
 * Gemeinsame SEO-Meta-Normalisierung für Rank Math, Yoast, AIOSEO und SEOPress.
 *
 * @package WPAgentCompanion
 */

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

define( 'WPAGENT_SEO_LOCK_TTL', 120 );

/**
 * @return 'rankmath'|'yoast'|'aioseo'|'seopress'|'none'
 */
function wpagent_detect_seo_plugin(): string {
	if ( defined( 'RANK_MATH_VERSION' ) || class_exists( 'RankMath\\Helper' ) || function_exists( 'rank_math' ) ) {
		return 'rankmath';
	}
	if ( defined( 'WPSEO_VERSION' ) || class_exists( 'WPSEO_Options' ) ) {
		return 'yoast';
	}
	if ( defined( 'AIOSEO_VERSION' ) || function_exists( 'aioseo' ) ) {
		return 'aioseo';
	}
	if ( defined( 'SEOPRESS_VERSION' ) || function_exists( 'seopress_get_service' ) ) {
		return 'seopress';
	}
	return 'none';
}

/**
 * Wandelt die öffentlich benannten Eingabefelder in den internen Vertrag um.
 * Nicht übergebene Werte bleiben null, `false` bleibt dagegen ein echter Wert.
 *
 * @param array<string,mixed> $input
 * @return array<string,string|bool|null>|WP_Error
 */
function wpagent_seo_fields_from_input( array $input ) {
	$text_fields = array(
		'title'                    => 'seo_title',
		'description'              => 'seo_description',
		'focus_keyword'            => 'focus_keyword',
		'open_graph_title'         => 'open_graph_title',
		'open_graph_description'   => 'open_graph_description',
		'twitter_title'            => 'twitter_title',
		'twitter_description'      => 'twitter_description',
	);
	$fields      = array();
	foreach ( $text_fields as $field => $input_key ) {
		$fields[ $field ] = array_key_exists( $input_key, $input )
			? sanitize_text_field( (string) $input[ $input_key ] )
			: null;
	}

	$fields['canonical_url'] = null;
	if ( array_key_exists( 'canonical_url', $input ) ) {
		$raw = trim( (string) $input['canonical_url'] );
		if ( '' === $raw ) {
			$fields['canonical_url'] = '';
		} else {
			$parts = wp_parse_url( $raw );
			if (
				! is_array( $parts )
				|| ! isset( $parts['scheme'], $parts['host'] )
				|| ! in_array( strtolower( (string) $parts['scheme'] ), array( 'http', 'https' ), true )
				|| '' === (string) $parts['host']
			) {
				return new WP_Error( 'wpagent_invalid_canonical_url', __( 'Die Canonical-URL muss leer oder eine absolute HTTP- beziehungsweise HTTPS-URL sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$canonical = esc_url_raw( $raw, array( 'http', 'https' ) );
			if ( '' === $canonical ) {
				return new WP_Error( 'wpagent_invalid_canonical_url', __( 'Die Canonical-URL ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$fields['canonical_url'] = $canonical;
		}
	}

	foreach ( array( 'robots_noindex', 'robots_nofollow' ) as $field ) {
		$fields[ $field ] = array_key_exists( $field, $input ) ? (bool) $input[ $field ] : null;
	}
	$fields['cornerstone'] = array_key_exists( 'cornerstone', $input ) ? (bool) $input['cornerstone'] : null;

	return $fields;
}

/**
 * @param array<string,mixed> $fields
 */
function wpagent_seo_fields_have_value( array $fields ): bool {
	foreach ( $fields as $value ) {
		if ( null !== $value ) {
			return true;
		}
	}
	return false;
}

/** @return string|WP_Error */
function wpagent_seo_acquire_lock( int $post_id ) {
	$option = 'wp_agent_seo_lock_' . $post_id;
	$token  = wpagent_companion_acquire_option_lock( $option, WPAGENT_SEO_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_seo_locked', __( 'Die SEO-Daten dieses Beitrags werden gerade geändert. Bitte erneut versuchen.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_seo_release_lock( int $post_id, string $token ): void {
	wpagent_companion_release_option_lock( 'wp_agent_seo_lock_' . $post_id, $token );
}

/**
 * Sichert Provider-Metafelder einschließlich ihrer Existenz und eventueller
 * Mehrfachwerte. Das ist genauer als ein normalisierter SEO-Snapshot und
 * erlaubt einen verlustfreien Rollback nach einem Teilfehler.
 *
 * @param string[] $meta_keys
 * @return array<string,array<int,mixed>>
 */
function wpagent_capture_seo_postmeta( int $post_id, array $meta_keys ): array {
	$state = array();
	foreach ( array_values( array_unique( $meta_keys ) ) as $meta_key ) {
		$state[ $meta_key ] = get_post_meta( $post_id, $meta_key, false );
	}
	return $state;
}

/**
 * @param array<string,array<int,mixed>> $state
 */
function wpagent_restore_seo_postmeta( int $post_id, array $state ): bool {
	foreach ( $state as $meta_key => $values ) {
		delete_post_meta( $post_id, $meta_key );
		foreach ( $values as $value ) {
			add_post_meta( $post_id, $meta_key, $value );
		}
	}

	foreach ( $state as $meta_key => $values ) {
		if ( get_post_meta( $post_id, $meta_key, false ) !== $values ) {
			return false;
		}
	}
	return true;
}

/**
 * @return string
 */
function wpagent_aioseo_posts_table(): string {
	global $wpdb;
	return $wpdb->prefix . 'aioseo_posts';
}

/**
 * @return string
 */
function wpagent_aioseo_quote_identifier( string $identifier ): string {
	return '`' . str_replace( '`', '``', $identifier ) . '`';
}

/**
 * AIOSEO's posts table deliberately has only a non-unique post_id index. Safe
 * initialization and rollback therefore require an InnoDB transaction. A
 * serializable missing-row lookup reserves the indexed gap until COMMIT.
 *
 * @return true|WP_Error
 */
function wpagent_aioseo_require_transactional_storage() {
	global $wpdb;
	$server = method_exists( $wpdb, 'db_server_info' ) ? strtolower( (string) $wpdb->db_server_info() ) : '';
	if ( str_contains( $server, 'sqlite' ) ) {
		return new WP_Error( 'wpagent_aioseo_storage_unsupported', __( 'AIOSEO kann auf dieser Datenbank nicht nebenläufigkeitssicher geändert werden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}

	$tables  = array( wpagent_aioseo_posts_table(), $wpdb->postmeta );
	$engines = $wpdb->get_results(
		$wpdb->prepare(
			'SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN (%s, %s)',
			$tables[0],
			$tables[1]
		),
		OBJECT_K
	);
	if ( ! is_array( $engines ) || 2 !== count( $engines ) ) {
		return new WP_Error( 'wpagent_aioseo_storage_unsupported', __( 'Die AIOSEO-Datenbanktabelle unterstützt keinen sicheren transaktionalen Rollback.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	foreach ( $tables as $table ) {
		if ( ! isset( $engines[ $table ]->ENGINE ) || 'innodb' !== strtolower( (string) $engines[ $table ]->ENGINE ) ) {
			return new WP_Error( 'wpagent_aioseo_storage_unsupported', __( 'Die AIOSEO- und WordPress-Metatabellen unterstützen keinen gemeinsamen sicheren Rollback.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
	}
	return true;
}

/**
 * Verhindert, dass AIOSEOs eigene Transaktionsgrenze eine bereits laufende
 * Transaktion des aufrufenden Plugins implizit festschreibt oder zurückrollt.
 *
 * @return true|WP_Error
 */
function wpagent_aioseo_require_idle_transaction( string $isolation_level = 'REPEATABLE READ' ) {
	global $wpdb;
	// MySQL has no MariaDB-compatible @@in_transaction variable. SET TRANSACTION
	// is portable, has no implicit commit and fails while a transaction is active.
	$isolation_level = 'SERIALIZABLE' === $isolation_level ? 'SERIALIZABLE' : 'REPEATABLE READ';
	$suppress_errors = $wpdb->suppress_errors( true );
	$can_start       = false !== $wpdb->query( "SET TRANSACTION ISOLATION LEVEL {$isolation_level}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	$wpdb->suppress_errors( $suppress_errors );
	if ( ! $can_start ) {
		return new WP_Error( 'wpagent_aioseo_transaction_busy', __( 'AIOSEO kann innerhalb einer bereits laufenden Datenbanktransaktion nicht sicher geändert werden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return true;
}

/**
 * @return array<int,array<string,mixed>>|WP_Error
 */
function wpagent_aioseo_storage_rows( int $post_id, bool $for_update = false ) {
	global $wpdb;
	$table = wpagent_aioseo_quote_identifier( wpagent_aioseo_posts_table() );
	$sql   = $wpdb->prepare( "SELECT * FROM {$table} WHERE post_id = %d ORDER BY id", $post_id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	if ( $for_update ) {
		$sql .= ' FOR UPDATE';
	}
	$rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	if ( ! is_array( $rows ) ) {
		return new WP_Error( 'wpagent_aioseo_storage_read_failed', __( 'Der AIOSEO-Datenbankzustand konnte nicht sicher gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return $rows;
}

/**
 * Verwirft request-lokale Provider- und WordPress-Metacaches nach COMMIT oder
 * ROLLBACK, damit die anschließende Verifikation tatsächlich die DB liest.
 */
function wpagent_aioseo_reset_read_caches( int $post_id ): void {
	wp_cache_delete( $post_id, 'post_meta' );
	clean_post_cache( $post_id );
	if ( function_exists( 'aioseo' ) && isset( aioseo()->core->db ) ) {
		try {
			if ( isset( aioseo()->meta->metaData ) && method_exists( aioseo()->meta->metaData, 'bustPostCache' ) ) {
				aioseo()->meta->metaData->bustPostCache( $post_id );
			}
			aioseo()->core->db
				->start( 'aioseo_posts' )
				->where( 'post_id', $post_id )
				->resetCache()
				->run();
		} catch ( Throwable ) {
			// Der nachfolgende native Read-back meldet einen echten Lesefehler.
		}
	}
}

/**
 * Startet die AIOSEO-Mutation und sperrt sowohl den Provider-Row als auch alle
 * Postmeta-Zeilen des Beitrags. Fremde AIOSEO-, UI-, Cron- und Import-Writes
 * können dadurch nicht zwischen Snapshot, Write und Rollback gelangen.
 *
 * @return array<string,mixed>|WP_Error
 */
function wpagent_aioseo_begin_mutation( int $post_id ) {
	global $wpdb;
	$supported = wpagent_aioseo_require_transactional_storage();
	if ( is_wp_error( $supported ) ) {
		return $supported;
	}

	$idle = wpagent_aioseo_require_idle_transaction();
	if ( is_wp_error( $idle ) ) {
		return $idle;
	}
	if ( false === $wpdb->query( 'START TRANSACTION' ) ) {
		return new WP_Error( 'wpagent_aioseo_transaction_failed', __( 'Die sichere AIOSEO-Datenbanktransaktion konnte nicht gestartet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$rows = wpagent_aioseo_storage_rows( $post_id, true );
	if ( is_wp_error( $rows ) || 1 !== count( $rows ) ) {
		$wpdb->query( 'ROLLBACK' );
		if ( is_wp_error( $rows ) ) {
			return $rows;
		}
		return new WP_Error( 'wpagent_aioseo_ambiguous_record', __( 'AIOSEO besitzt für diesen Beitrag keinen eindeutigen SEO-Datensatz. Die Änderung wurde abgebrochen.', 'wp-agent-companion' ), array( 'status' => 409, 'record_count' => count( $rows ) ) );
	}

	$postmeta = wpagent_aioseo_quote_identifier( $wpdb->postmeta );
	$locked   = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$postmeta} WHERE post_id = %d FOR UPDATE", $post_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
	if ( ! is_array( $locked ) ) {
		$wpdb->query( 'ROLLBACK' );
		return new WP_Error( 'wpagent_aioseo_transaction_failed', __( 'Die gespiegelten AIOSEO-Metadaten konnten nicht sicher gesperrt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	wpagent_aioseo_reset_read_caches( $post_id );
	$before = wpagent_read_aioseo_meta( $post_id );
	if ( is_wp_error( $before ) ) {
		$wpdb->query( 'ROLLBACK' );
		return $before;
	}

	return array(
		'row'     => $rows[0],
		'mirrors' => wpagent_capture_seo_postmeta( $post_id, wpagent_aioseo_mirrored_meta_keys() ),
		'before'  => $before,
	);
}

/**
 * @param array<string,mixed> $context
 */
function wpagent_aioseo_rollback_mutation( int $post_id, array $context ): bool {
	global $wpdb;
	if ( false === $wpdb->query( 'ROLLBACK' ) ) {
		return false;
	}

	wpagent_aioseo_reset_read_caches( $post_id );
	$rows = wpagent_aioseo_storage_rows( $post_id );
	$expected_rows = null === $context['row'] ? array() : array( $context['row'] );
	if ( is_wp_error( $rows ) || $expected_rows !== $rows ) {
		return false;
	}
	if ( wpagent_capture_seo_postmeta( $post_id, wpagent_aioseo_mirrored_meta_keys() ) !== $context['mirrors'] ) {
		return false;
	}
	if ( null === $context['row'] ) {
		return true;
	}
	$fresh = wpagent_read_aioseo_meta( $post_id );
	return ! is_wp_error( $fresh ) && $fresh === $context['before'];
}

function wpagent_aioseo_commit_mutation( int $post_id ): bool {
	global $wpdb;
	if ( false === $wpdb->query( 'COMMIT' ) ) {
		$wpdb->query( 'ROLLBACK' );
		wpagent_aioseo_reset_read_caches( $post_id );
		return false;
	}
	wpagent_aioseo_reset_read_caches( $post_id );
	return true;
}

/**
 * Verifiziert die von AIOSEO selbst gepflegten Übersetzungs-Spiegel für alle
 * Felder des aktuellen Patches. Ein Provider-Row und sein Spiegel dürfen nie
 * mit unterschiedlichen sichtbaren Werten festgeschrieben werden.
 *
 * @param array<string,string|bool|null> $fields
 * @return array<string,bool>|WP_Error
 */
function wpagent_aioseo_verify_touched_mirrors( int $post_id, array $fields ) {
	$rows = wpagent_aioseo_storage_rows( $post_id );
	if ( is_wp_error( $rows ) || 1 !== count( $rows ) ) {
		return is_wp_error( $rows )
			? $rows
			: new WP_Error( 'wpagent_aioseo_ambiguous_record', __( 'AIOSEO besitzt für diesen Beitrag keinen eindeutigen SEO-Datensatz.', 'wp-agent-companion' ), array( 'status' => 409, 'record_count' => count( $rows ) ) );
	}
	$map = array(
		'title'                  => array( 'title', '_aioseo_title' ),
		'description'            => array( 'description', '_aioseo_description' ),
		'open_graph_title'       => array( 'og_title', '_aioseo_og_title' ),
		'open_graph_description' => array( 'og_description', '_aioseo_og_description' ),
		'twitter_title'          => array( 'twitter_title', '_aioseo_twitter_title' ),
		'twitter_description'    => array( 'twitter_description', '_aioseo_twitter_description' ),
	);
	$result = array();
	foreach ( $map as $field => $keys ) {
		if ( null === ( $fields[ $field ] ?? null ) ) {
			continue;
		}
		$values           = get_post_meta( $post_id, $keys[1], false );
		$result[ $field ] = 1 === count( $values ) && (string) $values[0] === (string) ( $rows[0][ $keys[0] ] ?? '' );
	}
	return $result;
}

/**
 * Ergänzt einen Provider-Fehler um den verifizierten Rollback-Status, ohne
 * dessen ursprünglichen HTTP-Status oder Diagnosedaten zu verlieren.
 *
 * @param array<string,array<int,mixed>> $state
 */
function wpagent_seo_error_with_postmeta_rollback( WP_Error $error, int $post_id, array $state ): WP_Error {
	$rollback_verified = wpagent_restore_seo_postmeta( $post_id, $state );
	$data              = $error->get_error_data();
	$data              = is_array( $data ) ? $data : array();
	$error->add_data(
		array_merge(
			$data,
			array(
				'rolled_back'      => true,
				'rollback_verified' => $rollback_verified,
			)
		)
	);
	return $error;
}

/**
 * @return array<string,mixed>|WP_Error
 */
function wpagent_read_aioseo_meta( int $post_id ) {
	$rows = wpagent_aioseo_storage_rows( $post_id );
	if ( is_wp_error( $rows ) ) {
		return $rows;
	}
	if ( count( $rows ) > 1 ) {
		return new WP_Error( 'wpagent_aioseo_ambiguous_record', __( 'AIOSEO besitzt für diesen Beitrag mehrere SEO-Datensätze. Ein eindeutiger Read-back ist nicht möglich.', 'wp-agent-companion' ), array( 'status' => 409, 'record_count' => count( $rows ) ) );
	}
	$read = wpagent_companion_get_registered_ability( 'aioseo-posts/seo-data-get' );
	if ( ! $read instanceof WP_Ability ) {
		return new WP_Error( 'wpagent_aioseo_api_missing', __( 'AIOSEO stellt seine native SEO-Ability nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$fresh = $read->execute( array( 'postId' => $post_id ) );
	if ( is_wp_error( $fresh ) ) {
		$data = $fresh->get_error_data();
		if ( ! is_array( $data ) || ! isset( $data['status'] ) ) {
			$data = is_array( $data ) ? $data : array();
			$status = 'ability_invalid_permissions' === $fresh->get_error_code() ? 403 : 500;
			$fresh->add_data( $data + array( 'status' => $status ) );
		}
		return $fresh;
	}
	if ( ! is_array( $fresh ) ) {
		return new WP_Error( 'wpagent_aioseo_readback_failed', __( 'AIOSEO hat beim erneuten Lesen keine gültigen SEO-Daten geliefert.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$robots = is_array( $fresh['robots'] ?? null ) ? $fresh['robots'] : null;
	$social = is_array( $fresh['social'] ?? null ) ? $fresh['social'] : null;
	if (
		null === $robots ||
		null === $social ||
		array_diff( array( 'use_default', 'noindex', 'nofollow' ), array_keys( $robots ) ) ||
		array_diff( array( 'og_title', 'og_description', 'twitter_title', 'twitter_description' ), array_keys( $social ) )
	) {
		return new WP_Error( 'wpagent_aioseo_readback_failed', __( 'AIOSEO hat beim erneuten Lesen keine gültigen SEO-Daten geliefert.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return $fresh;
}

/**
 * Liefert für alle vier Provider denselben gespeicherten Post-SEO-Vertrag.
 * Die Robots-Bools geben den gespeicherten Override wieder. Globale Defaults
 * werden bewusst nicht als postbezogener Wert ausgegeben.
 *
 * @param 'rankmath'|'yoast'|'aioseo'|'seopress' $plugin
 * @return array<string,mixed>|WP_Error
 */
function wpagent_read_seo_meta( int $post_id, string $plugin ) {
	if ( 'aioseo' === $plugin ) {
		$fresh = wpagent_read_aioseo_meta( $post_id );
		if ( is_wp_error( $fresh ) ) {
			return $fresh;
		}
		$robots = is_array( $fresh['robots'] ?? null ) ? $fresh['robots'] : array();
		$social = is_array( $fresh['social'] ?? null ) ? $fresh['social'] : array();
		return array(
			'post_id'                  => $post_id,
			'plugin'                   => $plugin,
			'seo_title'                => (string) ( $fresh['title'] ?? '' ),
			'seo_description'          => (string) ( $fresh['description'] ?? '' ),
			'focus_keyword'            => (string) ( $fresh['focus_keyphrase'] ?? '' ),
			'canonical_url'            => (string) ( $fresh['canonical_url'] ?? '' ),
			'robots_noindex'           => ! empty( $robots['noindex'] ),
			'robots_nofollow'          => ! empty( $robots['nofollow'] ),
			'open_graph_title'         => (string) ( $social['og_title'] ?? '' ),
			'open_graph_description'   => (string) ( $social['og_description'] ?? '' ),
			'twitter_title'            => (string) ( $social['twitter_title'] ?? '' ),
			'twitter_description'      => (string) ( $social['twitter_description'] ?? '' ),
			'cornerstone_supported'    => false,
			'cornerstone'              => null,
		);
	}

	$maps = array(
		'rankmath' => array(
			'seo_title'               => 'rank_math_title',
			'seo_description'         => 'rank_math_description',
			'focus_keyword'           => 'rank_math_focus_keyword',
			'canonical_url'           => 'rank_math_canonical_url',
			'open_graph_title'        => 'rank_math_facebook_title',
			'open_graph_description'  => 'rank_math_facebook_description',
			'twitter_title'           => 'rank_math_twitter_title',
			'twitter_description'     => 'rank_math_twitter_description',
		),
		'yoast'    => array(
			'seo_title'               => '_yoast_wpseo_title',
			'seo_description'         => '_yoast_wpseo_metadesc',
			'focus_keyword'           => '_yoast_wpseo_focuskw',
			'canonical_url'           => '_yoast_wpseo_canonical',
			'open_graph_title'        => '_yoast_wpseo_opengraph-title',
			'open_graph_description'  => '_yoast_wpseo_opengraph-description',
			'twitter_title'           => '_yoast_wpseo_twitter-title',
			'twitter_description'     => '_yoast_wpseo_twitter-description',
		),
		'seopress' => array(
			'seo_title'               => '_seopress_titles_title',
			'seo_description'         => '_seopress_titles_desc',
			'focus_keyword'           => '_seopress_analysis_target_kw',
			'canonical_url'           => '_seopress_robots_canonical',
			'open_graph_title'        => '_seopress_social_fb_title',
			'open_graph_description'  => '_seopress_social_fb_desc',
			'twitter_title'           => '_seopress_social_twitter_title',
			'twitter_description'     => '_seopress_social_twitter_desc',
		),
	);
	if ( ! isset( $maps[ $plugin ] ) ) {
		return new WP_Error( 'wpagent_seo_plugin_missing', __( 'Kein unterstütztes SEO-Plugin aktiv.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}

	$snapshot = array( 'post_id' => $post_id, 'plugin' => $plugin );
	foreach ( $maps[ $plugin ] as $field => $meta_key ) {
		$snapshot[ $field ] = (string) get_post_meta( $post_id, $meta_key, true );
	}
	if ( 'rankmath' === $plugin ) {
		$robots = get_post_meta( $post_id, 'rank_math_robots', true );
		$robots = is_array( $robots ) ? $robots : array();
		$snapshot['robots_noindex']  = in_array( 'noindex', $robots, true );
		$snapshot['robots_nofollow'] = in_array( 'nofollow', $robots, true );
		$snapshot['cornerstone_supported'] = true;
		$snapshot['cornerstone'] = 'on' === (string) get_post_meta( $post_id, 'rank_math_pillar_content', true );
	} elseif ( 'yoast' === $plugin ) {
		$snapshot['robots_noindex']  = '1' === (string) get_post_meta( $post_id, '_yoast_wpseo_meta-robots-noindex', true );
		$snapshot['robots_nofollow'] = '1' === (string) get_post_meta( $post_id, '_yoast_wpseo_meta-robots-nofollow', true );
		$snapshot['cornerstone_supported'] = true;
		$snapshot['cornerstone'] = '1' === (string) get_post_meta( $post_id, '_yoast_wpseo_is_cornerstone', true );
	} else {
		$snapshot['robots_noindex']  = 'yes' === (string) get_post_meta( $post_id, '_seopress_robots_index', true );
		$snapshot['robots_nofollow'] = 'yes' === (string) get_post_meta( $post_id, '_seopress_robots_follow', true );
		$snapshot['cornerstone_supported'] = false;
		$snapshot['cornerstone'] = null;
	}
	return $snapshot;
}

/**
 * @param array<string,string|bool|null> $fields
 * @param array<string,mixed>            $snapshot
 * @return array{written:array<string,array<string,mixed>>,verified:bool}
 */
function wpagent_verify_seo_snapshot( array $fields, array $snapshot ): array {
	$snapshot_keys = array(
		'title'                   => 'seo_title',
		'description'             => 'seo_description',
		'focus_keyword'           => 'focus_keyword',
		'canonical_url'           => 'canonical_url',
		'robots_noindex'          => 'robots_noindex',
		'robots_nofollow'         => 'robots_nofollow',
		'open_graph_title'        => 'open_graph_title',
		'open_graph_description'  => 'open_graph_description',
		'twitter_title'           => 'twitter_title',
		'twitter_description'     => 'twitter_description',
		'cornerstone'             => 'cornerstone',
	);
	$written      = array();
	$verified     = true;
	foreach ( $snapshot_keys as $field => $snapshot_key ) {
		if ( null === ( $fields[ $field ] ?? null ) ) {
			continue;
		}
		$expected = $fields[ $field ];
		$actual   = $snapshot[ $snapshot_key ] ?? null;
		$ok       = is_bool( $expected ) ? $actual === $expected : (string) $actual === (string) $expected;
		$written[ $field ] = array( 'persisted' => $ok );
		$verified          = $verified && $ok;
	}
	return array( 'written' => $written, 'verified' => $verified );
}
