<?php
/**
 * Ability: wp-agent/set-seo-meta
 *
 * Detects Rank Math, Yoast, AIOSEO or SEOPress and exposes the supported SEO
 * fields for inspection. Remote writes remain manual-only because no provider
 * exposes an atomic compare-and-write operation for the full metadata surface.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_set_seo_meta(): array {
	return array(
		'label'               => __( 'SEO-Meta setzen und verifizieren', 'wp-agent-companion' ),
		'description'         => __( 'Prüft Titel, Description, Fokus-Keyword, Canonical, Robots-Overrides, Open-Graph- und X/Twitter-Texte sowie Cornerstone-Inhalte gegen unterstützte Provider. Remote-Änderungen sind nicht verfügbar, weil kein Provider eine atomare Vergleichs-und-Schreib-Operation für die vollständige Meta-Fläche bereitstellt. Nimm Änderungen im SEO-Plugin vor und lies den Zustand anschließend neu.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'                 => 'object',
			'properties'           => array(
				'post_id'                => array( 'type' => 'integer' ),
				'expected_hash'          => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'seo_title'              => wpagent_seo_text_input_schema( 'title' ),
				'seo_description'        => wpagent_seo_text_input_schema( 'description' ),
				'focus_keyword'          => wpagent_seo_text_input_schema( 'focus_keyword' ),
				'canonical_url'          => wpagent_seo_text_input_schema( 'canonical_url' ),
				'robots_noindex'         => array( 'type' => 'boolean' ),
				'robots_nofollow'        => array( 'type' => 'boolean' ),
				'open_graph_title'       => wpagent_seo_text_input_schema( 'open_graph_title' ),
				'open_graph_description' => wpagent_seo_text_input_schema( 'open_graph_description' ),
				'twitter_title'          => wpagent_seo_text_input_schema( 'twitter_title' ),
				'twitter_description'    => wpagent_seo_text_input_schema( 'twitter_description' ),
				'cornerstone'            => array( 'type' => 'boolean' ),
			),
			'required'             => array( 'post_id', 'expected_hash' ),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'                 => 'object',
			'properties'           => array(
				'post_id'            => array( 'type' => 'integer' ),
				'plugin'             => array( 'type' => 'string', 'enum' => array( 'rankmath', 'yoast', 'aioseo', 'seopress' ) ),
				'written'            => wpagent_seo_written_schema(),
				'verified'           => array( 'type' => 'boolean' ),
				'indexable_verified' => array( 'type' => array( 'boolean', 'null' ) ),
				'rolled_back'        => array( 'type' => 'boolean' ),
				'rollback_verified'  => array( 'type' => array( 'boolean', 'null' ) ),
				'previous_hash'      => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'state_hash'         => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required'             => array( 'post_id', 'plugin', 'written', 'verified', 'indexable_verified', 'rolled_back', 'rollback_verified', 'previous_hash', 'state_hash' ),
			'additionalProperties' => false,
		),
		'execute_callback'    => 'wpagent_exec_set_seo_meta',
		'permission_callback' => 'wpagent_perm_edit_post_target',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
				'manual_only' => true,
			),
		),
	);
}

/** @return WP_Error */
function wpagent_seo_manual_write_required( int $post_id, string $plugin, string $current_hash ) {
	return new WP_Error(
		'wpagent_seo_manual_write_required',
		__( 'Der aktive SEO-Provider stellt für diese vollständige Meta-Fläche keine atomare Vergleichs-und-Schreib-Operation bereit. WPAgently hat nichts geändert. Nimm die Änderung im SEO-Plugin vor und lies den Zustand anschließend neu.', 'wp-agent-companion' ),
		array( 'status' => 409, 'post_id' => $post_id, 'plugin' => $plugin, 'current_hash' => $current_hash, 'manual_only' => true )
	);
}

/**
 * @param array<string,string|bool|null> $fields
 * @return array|WP_Error
 */
function wpagent_set_aioseo_meta( int $post_id, array $fields ) {
	$update = wpagent_companion_get_registered_ability( 'aioseo-posts/seo-data-update' );
	if ( ! $update 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 ) );
	}
	$input = array( 'postId' => $post_id );
	$map   = array( 'title' => 'title', 'description' => 'description', 'focus_keyword' => 'focus_keyphrase' );
	foreach ( $map as $field => $target ) {
		if ( null !== $fields[ $field ] ) {
			$input[ $target ] = $fields[ $field ];
		}
	}
	if ( null !== $fields['canonical_url'] ) {
		$input['canonical_url'] = $fields['canonical_url'];
	}
	if ( null !== $fields['robots_noindex'] || null !== $fields['robots_nofollow'] ) {
		$input['robots'] = array( 'use_default' => false );
		if ( null !== $fields['robots_noindex'] ) {
			$input['robots']['noindex'] = $fields['robots_noindex'];
		}
		if ( null !== $fields['robots_nofollow'] ) {
			$input['robots']['nofollow'] = $fields['robots_nofollow'];
		}
	}
	$social_map = array(
		'open_graph_title'        => 'og_title',
		'open_graph_description'  => 'og_description',
		'twitter_title'           => 'twitter_title',
		'twitter_description'     => 'twitter_description',
	);
	foreach ( $social_map as $field => $target ) {
		if ( null !== $fields[ $field ] ) {
			$input['social'][ $target ] = $fields[ $field ];
		}
	}
	if ( 1 === count( $input ) ) {
		return new WP_Error( 'wpagent_no_fields', __( 'Kein SEO-Feld übergeben.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( true !== $update->check_permissions( $input ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'AIOSEO erlaubt diesem Benutzer keine Änderungen an den SEO-Daten.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	$mutation = null;
	$before   = wpagent_read_aioseo_meta( $post_id );
	if ( is_wp_error( $before ) && 'not_found' === $before->get_error_code() ) {
		$mutation = wpagent_initialize_aioseo_post_record( $post_id );
		if ( is_wp_error( $mutation ) ) {
			return $mutation;
		}
	}
	if ( is_wp_error( $before ) ) {
		if ( 'not_found' !== $before->get_error_code() ) {
			return $before;
		}
	}

	if ( null === $mutation ) {
		$mutation = wpagent_aioseo_begin_mutation( $post_id );
		if ( is_wp_error( $mutation ) ) {
			return $mutation;
		}
	}
	$result = $update->execute( $input );
	if ( is_wp_error( $result ) ) {
		return wpagent_aioseo_error_with_transaction_rollback( $result, $post_id, $mutation, 400 );
	}
	$fresh = wpagent_read_seo_meta( $post_id, 'aioseo' );
	if ( is_wp_error( $fresh ) ) {
		return wpagent_aioseo_error_with_transaction_rollback( $fresh, $post_id, $mutation, 500 );
	}
	$verification = wpagent_verify_seo_snapshot( $fields, $fresh );
	$mirrors      = wpagent_aioseo_verify_touched_mirrors( $post_id, $fields );
	if ( is_wp_error( $mirrors ) ) {
		return wpagent_aioseo_error_with_transaction_rollback( $mirrors, $post_id, $mutation, 500 );
	}
	foreach ( $mirrors as $field => $persisted ) {
		if ( isset( $verification['written'][ $field ] ) ) {
			$verification['written'][ $field ]['persisted'] = $verification['written'][ $field ]['persisted'] && $persisted;
		}
		$verification['verified'] = $verification['verified'] && $persisted;
	}
	if ( ! $verification['verified'] ) {
		$rollback_verified = wpagent_aioseo_rollback_mutation( $post_id, $mutation );
		return array(
			'post_id'            => $post_id,
			'plugin'             => 'aioseo',
			'written'            => $verification['written'],
			'verified'           => false,
			'indexable_verified' => null,
			'rolled_back'        => true,
			'rollback_verified'  => $rollback_verified,
		);
	}
	if ( ! wpagent_aioseo_commit_mutation( $post_id ) ) {
		return new WP_Error( 'wpagent_aioseo_commit_failed', __( 'Die verifizierten AIOSEO-Daten konnten nicht sicher festgeschrieben werden.', 'wp-agent-companion' ), array( 'status' => 500, 'rolled_back' => true, 'rollback_verified' => false ) );
	}
	return array(
		'post_id'            => $post_id,
		'plugin'             => 'aioseo',
		'written'            => $verification['written'],
		'verified'           => $verification['verified'],
		'indexable_verified' => null,
		'rolled_back'        => false,
		'rollback_verified'  => null,
	);
}

/**
 * AIOSEO spiegelt einige Modellfelder für Übersetzungs-Plugins zusätzlich in
 * WordPress-Postmeta. Ein exakter Rollback muss deren Existenz und alle Werte
 * unabhängig vom eigentlichen AIOSEO-Datensatz wiederherstellen.
 *
 * @return string[]
 */
function wpagent_aioseo_mirrored_meta_keys(): array {
	return array(
		'_aioseo_title',
		'_aioseo_description',
		'_aioseo_og_title',
		'_aioseo_og_description',
		'_aioseo_og_article_section',
		'_aioseo_twitter_title',
		'_aioseo_twitter_description',
		'_aioseo_keywords',
		'_aioseo_og_article_tags',
	);
}

/** @return array<string,mixed>|WP_Error */
function wpagent_initialize_aioseo_post_record( int $post_id ) {
	global $wpdb;
	$supported = wpagent_aioseo_require_transactional_storage();
	if ( is_wp_error( $supported ) ) {
		return $supported;
	}
	$model = 'AIOSEO\\Plugin\\Common\\Models\\Post';
	if ( ! is_callable( array( $model, 'savePost' ) ) ) {
		return new WP_Error( 'wpagent_aioseo_api_missing', __( 'AIOSEO stellt sein natives Beitragsmodell nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$idle = wpagent_aioseo_require_idle_transaction( 'SERIALIZABLE' );
	if ( is_wp_error( $idle ) ) {
		return $idle;
	}
	if ( false === $wpdb->query( 'START TRANSACTION' ) ) {
		return new WP_Error( 'wpagent_aioseo_transaction_failed', __( 'Die sichere AIOSEO-Initialisierung konnte nicht gestartet werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}

	$rows = wpagent_aioseo_storage_rows( $post_id, true );
	if ( is_wp_error( $rows ) ) {
		$wpdb->query( 'ROLLBACK' );
		return $rows;
	}
	if ( count( $rows ) > 1 ) {
		$wpdb->query( 'ROLLBACK' );
		return new WP_Error( 'wpagent_aioseo_ambiguous_record', __( 'AIOSEO besitzt für diesen Beitrag mehrere SEO-Datensätze. Die Änderung wurde abgebrochen.', 'wp-agent-companion' ), array( 'status' => 409, 'record_count' => count( $rows ) ) );
	}
	$row_before = 1 === count( $rows ) ? $rows[0] : null;

	$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 ) );
	}
	$mirrors_before = wpagent_capture_seo_postmeta( $post_id, wpagent_aioseo_mirrored_meta_keys() );
	if ( null !== $row_before ) {
		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'     => $row_before,
			'mirrors' => $mirrors_before,
			'before'  => $before,
		);
	}

	try {
		$result = $model::savePost( $post_id, array( 'canonicalUrl' => null ) );
	} catch ( Throwable $throwable ) {
		return wpagent_aioseo_initialization_error_with_rollback(
			new WP_Error( 'wpagent_aioseo_initialize_failed', __( 'AIOSEO konnte den SEO-Datensatz für diesen Beitrag nicht anlegen.', 'wp-agent-companion' ), array( 'status' => 500, 'provider_error' => $throwable->getMessage() ) ),
			$post_id,
			$mirrors_before
		);
	}
	if ( is_string( $result ) && '' !== $result ) {
		return wpagent_aioseo_initialization_error_with_rollback(
			new WP_Error( 'wpagent_aioseo_initialize_failed', __( 'AIOSEO konnte den SEO-Datensatz für diesen Beitrag nicht anlegen.', 'wp-agent-companion' ), array( 'status' => 500, 'provider_error' => $result ) ),
			$post_id,
			$mirrors_before
		);
	}

	$created = wpagent_aioseo_storage_rows( $post_id, true );
	if ( is_wp_error( $created ) || 1 !== count( $created ) ) {
		return wpagent_aioseo_initialization_error_with_rollback(
			new WP_Error( 'wpagent_aioseo_initialize_failed', __( 'AIOSEO konnte den neuen SEO-Datensatz nicht eindeutig bestätigen.', 'wp-agent-companion' ), array( 'status' => 500, 'record_count' => is_array( $created ) ? count( $created ) : null ) ),
			$post_id,
			$mirrors_before
		);
	}
	wpagent_aioseo_reset_read_caches( $post_id );
	return array(
		'row'     => null,
		'mirrors' => $mirrors_before,
		'before'  => null,
	);
}

/**
 * @param array<string,array<int,mixed>> $mirrors_before
 */
function wpagent_aioseo_initialization_error_with_rollback( WP_Error $error, int $post_id, array $mirrors_before ): WP_Error {
	global $wpdb;
	$rolled_back = false !== $wpdb->query( 'ROLLBACK' );
	wpagent_aioseo_reset_read_caches( $post_id );
	$rows = wpagent_aioseo_storage_rows( $post_id );
	$rollback_verified = $rolled_back
		&& is_array( $rows )
		&& 0 === count( $rows )
		&& $mirrors_before === wpagent_capture_seo_postmeta( $post_id, wpagent_aioseo_mirrored_meta_keys() );
	$data = $error->get_error_data();
	$data = is_array( $data ) ? $data : array();
	$error->add_data(
		array_merge(
			array( 'status' => 500 ),
			$data,
			array(
				'rolled_back'      => $rolled_back,
				'rollback_verified' => $rollback_verified,
			)
		)
	);
	return $error;
}

/**
 * @param array<string,mixed> $mutation
 */
function wpagent_aioseo_error_with_transaction_rollback( WP_Error $error, int $post_id, array $mutation, int $default_status ): WP_Error {
	$rollback_verified = wpagent_aioseo_rollback_mutation( $post_id, $mutation );
	$data              = $error->get_error_data();
	$data              = is_array( $data ) ? $data : array();
	$error->add_data(
		array_merge(
			array( 'status' => $default_status ),
			$data,
			array(
				'rolled_back'      => true,
				'rollback_verified' => $rollback_verified,
			)
		)
	);
	return $error;
}

/**
 * @param array<string,string|bool|null> $fields
 * @return array|WP_Error
 */
function wpagent_set_seopress_meta( int $post_id, array $fields ) {
	$meta_map = array(
		'title'                   => '_seopress_titles_title',
		'description'             => '_seopress_titles_desc',
		'focus_keyword'           => '_seopress_analysis_target_kw',
		'canonical_url'           => '_seopress_robots_canonical',
		'robots_noindex'          => '_seopress_robots_index',
		'robots_nofollow'         => '_seopress_robots_follow',
		'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',
	);
	$touched_meta_keys = array();
	foreach ( $meta_map as $field => $meta_key ) {
		if ( null !== $fields[ $field ] ) {
			$touched_meta_keys[] = $meta_key;
		}
	}
	$before = wpagent_capture_seo_postmeta( $post_id, $touched_meta_keys );

	$global_fields = array(
		$fields['title'],
		$fields['description'],
		$fields['canonical_url'],
		$fields['robots_noindex'],
		$fields['robots_nofollow'],
		$fields['open_graph_title'],
		$fields['open_graph_description'],
		$fields['twitter_title'],
		$fields['twitter_description'],
	);
	if ( wpagent_seo_fields_have_value( $global_fields ) && function_exists( 'seopress_metabox_role_is_blocked' ) && seopress_metabox_role_is_blocked( 'GLOBAL' ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'SEOPress erlaubt dieser Rolle keine Änderungen an Titel oder Description.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( null !== $fields['focus_keyword'] && function_exists( 'seopress_metabox_role_is_blocked' ) && seopress_metabox_role_is_blocked( 'CONTENT_ANALYSIS' ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'SEOPress erlaubt dieser Rolle keine Änderungen am Fokus-Keyword.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( null !== $fields['title'] || null !== $fields['description'] ) {
		$path    = "/seopress/v1/posts/{$post_id}/title-description-metas";
		$request = new WP_REST_Request( 'PUT', $path );
		if ( null !== $fields['title'] ) {
			$request->set_param( 'title', $fields['title'] );
		}
		if ( null !== $fields['description'] ) {
			$request->set_param( 'description', $fields['description'] );
		}
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			$error = new WP_Error( 'wpagent_seopress_write_failed', __( 'SEOPress hat Titel oder Description abgelehnt.', 'wp-agent-companion' ), array( 'status' => $response->get_status(), 'response' => $response->get_data() ) );
			return wpagent_seo_error_with_postmeta_rollback( $error, $post_id, $before );
		}
	}
	if ( null !== $fields['focus_keyword'] ) {
		$path    = "/seopress/v1/posts/{$post_id}/target-keywords";
		$request = new WP_REST_Request( 'PUT', $path );
		$request->set_param( '_seopress_analysis_target_kw', $fields['focus_keyword'] );
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			$error = new WP_Error( 'wpagent_seopress_write_failed', __( 'SEOPress hat das Fokus-Keyword abgelehnt.', 'wp-agent-companion' ), array( 'status' => $response->get_status(), 'response' => $response->get_data() ) );
			return wpagent_seo_error_with_postmeta_rollback( $error, $post_id, $before );
		}
	}

	$robots_map = array(
		'canonical_url'   => '_seopress_robots_canonical',
		'robots_noindex'  => '_seopress_robots_index',
		'robots_nofollow' => '_seopress_robots_follow',
	);
	$robots_request = new WP_REST_Request( 'PUT', "/seopress/v1/posts/{$post_id}/meta-robot-settings" );
	$has_robots     = false;
	foreach ( $robots_map as $field => $target ) {
		if ( null === $fields[ $field ] ) {
			continue;
		}
		$value = str_starts_with( $field, 'robots_' ) ? ( $fields[ $field ] ? 'yes' : '' ) : $fields[ $field ];
		$robots_request->set_param( $target, $value );
		$has_robots = true;
	}
	if ( $has_robots ) {
		$response = rest_do_request( $robots_request );
		if ( $response->is_error() ) {
			$error = new WP_Error( 'wpagent_seopress_write_failed', __( 'SEOPress hat Canonical- oder Robots-Angaben abgelehnt.', 'wp-agent-companion' ), array( 'status' => $response->get_status(), 'response' => $response->get_data() ) );
			return wpagent_seo_error_with_postmeta_rollback( $error, $post_id, $before );
		}
	}

	$social_map = array(
		'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',
	);
	$social_request = new WP_REST_Request( 'PUT', "/seopress/v1/posts/{$post_id}/social-settings" );
	$has_social     = false;
	foreach ( $social_map as $field => $target ) {
		if ( null === $fields[ $field ] ) {
			continue;
		}
		$social_request->set_param( $target, $fields[ $field ] );
		$has_social = true;
	}
	if ( $has_social ) {
		$response = rest_do_request( $social_request );
		if ( $response->is_error() ) {
			$error = new WP_Error( 'wpagent_seopress_write_failed', __( 'SEOPress hat Social-Meta abgelehnt.', 'wp-agent-companion' ), array( 'status' => $response->get_status(), 'response' => $response->get_data() ) );
			return wpagent_seo_error_with_postmeta_rollback( $error, $post_id, $before );
		}
	}

	$fresh = wpagent_read_seo_meta( $post_id, 'seopress' );
	if ( is_wp_error( $fresh ) ) {
		return wpagent_seo_error_with_postmeta_rollback( $fresh, $post_id, $before );
	}
	$verification = wpagent_verify_seo_snapshot( $fields, $fresh );
	if ( ! $verification['verified'] ) {
		return new WP_Error(
			'wpagent_seo_recovery_required',
			__( 'SEOPress hat die Änderung nicht exakt bestätigt. Der Zustand wird nicht blind zurückgesetzt. Prüfe den Beitrag im WordPress-Admin.', 'wp-agent-companion' ),
			array( 'status' => 500, 'post_id' => $post_id, 'written' => $verification['written'], 'recovery_required' => true )
		);
	}
	return array(
		'post_id'            => $post_id,
		'plugin'             => 'seopress',
		'written'            => $verification['written'],
		'verified'           => $verification['verified'],
		'indexable_verified' => null,
		'rolled_back'        => false,
		'rollback_verified'  => null,
	);
}

/**
 * Rebuild one Yoast indexable through Yoast's own container services.
 *
 * Yoast's public REST API is read-only. Its normal wp_insert_post watcher is
 * attempted first by the caller; this guarded fallback mirrors the current
 * watcher implementation and is accepted only when our independent Meta
 * Surface read-back succeeds afterwards.
 */
function wpagent_refresh_yoast_indexable( int $post_id, ?bool $expected_cornerstone = null ): bool {
	if ( ! function_exists( 'YoastSEO' ) ) {
		return false;
	}
	try {
		$classes    = YoastSEO()->classes;
		$repository = $classes->get( 'Yoast\\WP\\SEO\\Repositories\\Indexable_Repository' );
		$builder    = $classes->get( 'Yoast\\WP\\SEO\\Builders\\Indexable_Builder' );
		$existing   = $repository->find_by_id_and_type( $post_id, 'post', false );
		$indexable  = $builder->build_for_id_and_type( $post_id, 'post', $existing );
		if ( ! is_object( $indexable ) ) {
			return false;
		}
		if ( null !== $expected_cornerstone && (bool) ( $indexable->is_cornerstone ?? false ) !== $expected_cornerstone ) {
			return false;
		}

		// Yoast speichert Indexables bewusst nur in Produktionsumgebungen. Wenn
		// sie aktiviert sind, muss zusätzlich der erneut gelesene Datensatz den
		// Cornerstone-Wert bestätigen. Lokal genügt das vom nativen Builder
		// erzeugte Objekt, da Yoast selbst dort keine Tabellenzeile persistiert.
		$helper = YoastSEO()->helpers->indexable ?? null;
		if ( is_object( $helper ) && method_exists( $helper, 'should_index_indexables' ) && $helper->should_index_indexables() ) {
			$persisted = $repository->find_by_id_and_type( $post_id, 'post', false );
			if ( ! is_object( $persisted ) ) {
				return false;
			}
			if ( null !== $expected_cornerstone && (bool) ( $persisted->is_cornerstone ?? false ) !== $expected_cornerstone ) {
				return false;
			}
		}
		return true;
	} catch ( Throwable ) {
		return false;
	}
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_set_seo_meta( $input ) {
	$input   = is_array( $input ) ? $input : array();
	$post_id = (int) ( $input['post_id'] ?? 0 );

	// The concrete post capability must be checked before get_post() so direct
	// callbacks retain the same no-oracle property as the Ability/REST gate.
	if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! get_post( $post_id ) ) {
		return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}

	$plugin = wpagent_detect_seo_plugin();
	$fields = wpagent_seo_fields_from_input( $input );
	if ( is_wp_error( $fields ) ) {
		return $fields;
	}
	if ( ! wpagent_seo_fields_have_value( $fields ) ) {
		return new WP_Error( 'wpagent_no_fields', __( 'Kein SEO-Feld übergeben.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'none' === $plugin ) {
		return new WP_Error( 'wpagent_seo_plugin_missing', __( 'Kein unterstütztes SEO-Plugin aktiv. WPAgently schreibt keine wirkungslosen Ersatz-Metafelder.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$expected_hash = strtolower( (string) ( $input['expected_hash'] ?? '' ) );
	if ( ! preg_match( '/^[a-f0-9]{64}$/', $expected_hash ) ) {
		return new WP_Error( 'wpagent_seo_hash', __( 'expected_hash muss ein vollständiger SHA-256-Hash aus get-seo-meta sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( null !== $fields['cornerstone'] && ! in_array( $plugin, array( 'rankmath', 'yoast' ), true ) ) {
		return new WP_Error( 'wpagent_seo_field_unsupported', __( 'Das aktive SEO-Plugin stellt keinen bestätigten Cornerstone-Pfad bereit. WPAgently schreibt deshalb kein wirkungsloses Ersatz-Metafeld.', 'wp-agent-companion' ), array( 'status' => 409, 'field' => 'cornerstone', 'plugin' => $plugin ) );
	}

	$lock = wpagent_seo_acquire_lock( $post_id );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$before = wpagent_read_seo_meta( $post_id, $plugin );
		if ( is_wp_error( $before ) ) {
			return $before;
		}
		if ( ! hash_equals( $before['state_hash'], $expected_hash ) ) {
			return new WP_Error( 'wpagent_seo_conflict', __( 'Die SEO-Daten wurden seit dem Lesen verändert. Bitte erneut lesen.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $before['state_hash'] ) );
		}
		if ( ! wpagent_seo_lock_checkpoint( $post_id, $lock ) ) {
			return wpagent_seo_recovery_required( $post_id, $before['state_hash'], 'lock_lost_before_write' );
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		// The option lock serializes WPAgently itself, not the provider dashboard,
		// imports or third-party hooks. Re-read the complete provider surface at
		// the operation boundary, then fail closed until the provider exposes a
		// genuine conditional full-state mutation primitive.
		$prewrite = wpagent_read_seo_meta( $post_id, $plugin );
		if ( is_wp_error( $prewrite ) ) {
			return $prewrite;
		}
		if ( ! hash_equals( $expected_hash, $prewrite['state_hash'] ) ) {
			return new WP_Error( 'wpagent_seo_conflict', __( 'Die SEO-Daten wurden vor dem Speichern verändert. Bitte erneut lesen.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $prewrite['state_hash'] ) );
		}
		if ( ! wpagent_seo_lock_checkpoint( $post_id, $lock ) || ! current_user_can( 'edit_post', $post_id ) ) {
			return wpagent_seo_recovery_required( $post_id, $prewrite['state_hash'], 'lock_or_capability_lost_before_manual_only' );
		}
		return wpagent_seo_manual_write_required( $post_id, $plugin, $prewrite['state_hash'] );
	} finally {
		wpagent_seo_release_lock( $post_id, $lock );
	}
}
