<?php
/** Begrenztes, verifiziertes Suchen und Ersetzen in WordPress-Inhalten. */

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

define( 'WPAGENT_COMPANION_OPT_CONTENT_OPERATIONS', 'wp_agent_content_replace_operations' );
define( 'WPAGENT_COMPANION_OPT_CONTENT_OPERATION_LOCK', 'wp_agent_content_replace_lock' );
define( 'WPAGENT_COMPANION_CONTENT_OPERATION_TTL', 7 * DAY_IN_SECONDS );
define( 'WPAGENT_COMPANION_CONTENT_OPERATION_MAX_BYTES', 2 * MB_IN_BYTES );
define( 'WPAGENT_COMPANION_CONTENT_LOCK_TTL', 300 );

/** @return array<string,mixed> */
function wpagent_ability_search_replace_content(): array {
	return array(
		'label' => __( 'Inhalte suchen und ersetzen', 'wp-agent-companion' ),
		'description' => __( 'Ersetzt eine literale Textfolge begrenzt in Titel, Auszug und Inhalt. Dry-Run ist Standard, Builder-Fremdspeicher werden abgelehnt und echte Änderungen lassen sich konfliktgeprüft rückgängig machen.', 'wp-agent-companion' ),
		'category' => 'wp-agent',
		'input_schema' => array(
			'type' => 'object',
			'properties' => array(
				'search' => array( 'type' => 'string' ), 'replace' => array( 'type' => 'string' ),
				'fields' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array( 'title', 'excerpt', 'content' ) ), 'maxItems' => 3 ),
				'post_types' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), 'maxItems' => 20 ),
				'statuses' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array( 'publish', 'draft', 'pending', 'private', 'future' ) ), 'maxItems' => 5 ),
				'post_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ), 'maxItems' => 100 ),
				'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 100 ),
				'dry_run' => array( 'type' => 'boolean', 'default' => true ),
			),
			'required' => array( 'search', 'replace' ),
		),
		'output_schema' => array(
			'type' => 'object',
			'properties' => array(
				'dry_run' => array( 'type' => 'boolean' ), 'matched_posts' => array( 'type' => 'integer' ),
				'updated_posts' => array( 'type' => 'integer' ), 'total_replacements' => array( 'type' => 'integer' ),
				'items' => array( 'type' => 'array' ), 'operation_id' => array( 'type' => 'string' ),
				'verified' => array( 'type' => 'boolean' ),
			),
		),
		'execute_callback' => 'wpagent_exec_search_replace_content', 'permission_callback' => 'wpagent_perm_edit_posts',
		'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ) ),
	);
}

/** @return array<string,mixed> */
function wpagent_ability_undo_content_replace(): array {
	return array(
		'label' => __( 'Inhaltsersetzung rückgängig machen', 'wp-agent-companion' ),
		'description' => __( 'Stellt den Vorzustand einer von search-replace-content erzeugten Operation wieder her, sofern seitdem keines der betroffenen Felder verändert wurde.', 'wp-agent-companion' ),
		'category' => 'wp-agent',
		'input_schema' => array(
			'type' => 'object', 'properties' => array(
				'operation_id' => array( 'type' => 'string' ), 'dry_run' => array( 'type' => 'boolean', 'default' => true ),
			), 'required' => array( 'operation_id' ),
		),
		'output_schema' => array( 'type' => 'object', 'properties' => array(
			'dry_run' => array( 'type' => 'boolean' ), 'restorable_posts' => array( 'type' => 'integer' ),
			'undone' => array( 'type' => 'boolean' ), 'verified' => array( 'type' => 'boolean' ),
		) ),
		'execute_callback' => 'wpagent_exec_undo_content_replace', 'permission_callback' => 'wpagent_perm_edit_posts',
		'meta' => array( 'show_in_rest' => true, 'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true ) ),
	);
}

/** @return string|WP_Error */
function wpagent_companion_content_operation_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_COMPANION_OPT_CONTENT_OPERATION_LOCK, WPAGENT_COMPANION_CONTENT_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_content_operation_busy', __( 'Eine andere Inhaltsersetzung läuft bereits. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_companion_content_operation_unlock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_COMPANION_OPT_CONTENT_OPERATION_LOCK, $token );
}

/** @return array<string,array<string,mixed>> */
function wpagent_companion_content_operations(): array {
	$stored = get_option( WPAGENT_COMPANION_OPT_CONTENT_OPERATIONS, array() );
	$result = array();
	foreach ( is_array( $stored ) ? $stored : array() as $id => $operation ) {
		if ( is_string( $id ) && wp_is_uuid( $id ) && is_array( $operation ) && absint( $operation['created_at'] ?? 0 ) + WPAGENT_COMPANION_CONTENT_OPERATION_TTL >= time() ) {
			$result[ $id ] = $operation;
		}
	}
	return $result;
}

/** @return true|WP_Error */
function wpagent_companion_save_content_operations( array $operations ) {
	while ( count( $operations ) > 20 ) {
		array_shift( $operations );
	}
	if ( strlen( serialize( $operations ) ) > WPAGENT_COMPANION_CONTENT_OPERATION_MAX_BYTES ) {
		return new WP_Error( 'wpagent_content_operation_too_large', __( 'Der Rückgängig-Snapshot wäre größer als 2 MiB. Grenze die Ersetzung weiter ein.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	update_option( WPAGENT_COMPANION_OPT_CONTENT_OPERATIONS, $operations, false );
	return wpagent_companion_content_operations() === $operations
		? true
		: new WP_Error( 'wpagent_content_operation_persist_failed', __( 'Der Rückgängig-Snapshot konnte nicht verifiziert gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
}

/** @return array|WP_Error */
function wpagent_companion_content_replace_request( array $input ) {
	if ( ! is_string( $input['search'] ?? null ) || ! is_string( $input['replace'] ?? null ) ) {
		return new WP_Error( 'wpagent_content_replace_text', __( 'Suchtext und Ersatz müssen verschieden sein. Der Suchtext darf nicht leer sein, beide Werte sind auf 200 Bytes begrenzt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$search  = $input['search'];
	$replace = $input['replace'];
	if ( '' === $search || strlen( $search ) > 200 || strlen( $replace ) > 200 || $search === $replace ) {
		return new WP_Error( 'wpagent_content_replace_text', __( 'Suchtext und Ersatz müssen verschieden sein. Der Suchtext darf nicht leer sein, beide Werte sind auf 200 Bytes begrenzt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( preg_match( '/[<>\x00]|<!--|-->|wp:/i', $search . $replace ) ) {
		return new WP_Error( 'wpagent_content_replace_structural', __( 'HTML-Tags, Block-Kommentare und Null-Bytes dürfen nicht strukturell ersetzt werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$raw_fields = array_key_exists( 'fields', $input ) ? $input['fields'] : array( 'title', 'excerpt', 'content' );
	if ( ! is_array( $raw_fields ) || array_filter( $raw_fields, static fn( $value ): bool => ! is_string( $value ) ) ) {
		return new WP_Error( 'wpagent_content_replace_fields', __( 'Mindestens eines der Felder title, excerpt oder content ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$fields = array_values( array_unique( array_map( 'sanitize_key', $raw_fields ) ) );
	if ( array() === $fields || array_diff( $fields, array( 'title', 'excerpt', 'content' ) ) ) {
		return new WP_Error( 'wpagent_content_replace_fields', __( 'Mindestens eines der Felder title, excerpt oder content ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$available_types = array_diff( get_post_types( array( 'public' => true ), 'names' ), array( 'attachment' ) );
	$raw_post_types = array_key_exists( 'post_types', $input ) ? $input['post_types'] : array( 'post', 'page' );
	if ( ! is_array( $raw_post_types ) || array_filter( $raw_post_types, static fn( $value ): bool => ! is_string( $value ) ) ) {
		return new WP_Error( 'wpagent_content_replace_post_types', __( 'Mindestens ein öffentlicher, unterstützter Beitragstyp ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$post_types = array_values( array_unique( array_map( 'sanitize_key', $raw_post_types ) ) );
	if ( array() === $post_types || array_diff( $post_types, $available_types ) ) {
		return new WP_Error( 'wpagent_content_replace_post_types', __( 'Mindestens ein öffentlicher, unterstützter Beitragstyp ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$allowed_statuses = array( 'publish', 'draft', 'pending', 'private', 'future' );
	$raw_statuses = array_key_exists( 'statuses', $input ) ? $input['statuses'] : $allowed_statuses;
	if ( ! is_array( $raw_statuses ) || array_filter( $raw_statuses, static fn( $value ): bool => ! is_string( $value ) ) ) {
		return new WP_Error( 'wpagent_content_replace_statuses', __( 'Mindestens ein unterstützter Beitragsstatus ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$statuses = array_values( array_unique( array_map( 'sanitize_key', $raw_statuses ) ) );
	if ( array() === $statuses || array_diff( $statuses, $allowed_statuses ) ) {
		return new WP_Error( 'wpagent_content_replace_statuses', __( 'Mindestens ein unterstützter Beitragsstatus ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$raw_post_ids = $input['post_ids'] ?? array();
	if ( ! is_array( $raw_post_ids ) || array_filter( $raw_post_ids, static fn( $value ): bool => ! is_int( $value ) || $value < 1 ) ) {
		return new WP_Error( 'wpagent_content_replace_post_ids', __( 'Es sind höchstens 100 konkrete Beitrags-IDs erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$post_ids = array_values( array_unique( $raw_post_ids ) );
	if ( count( $post_ids ) > 100 ) {
		return new WP_Error( 'wpagent_content_replace_post_ids', __( 'Es sind höchstens 100 konkrete Beitrags-IDs erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$limit = $input['limit'] ?? 100;
	if ( ! is_int( $limit ) || $limit < 1 || $limit > 100 ) {
		return new WP_Error( 'wpagent_content_replace_limit_value', __( 'limit muss eine Ganzzahl zwischen 1 und 100 sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return array( 'search' => $search, 'replace' => $replace, 'fields' => $fields, 'post_types' => $post_types, 'statuses' => $statuses, 'post_ids' => $post_ids, 'limit' => $limit );
}

/** @return int[]|WP_Error */
function wpagent_companion_content_replace_candidates( array $request ) {
	global $wpdb;
	$columns = array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' );
	$likes   = array_fill( 0, count( $request['fields'] ), '%' . $wpdb->esc_like( $request['search'] ) . '%' );
	$where   = array(
		'post_type IN (' . implode( ', ', array_fill( 0, count( $request['post_types'] ), '%s' ) ) . ')',
		'post_status IN (' . implode( ', ', array_fill( 0, count( $request['statuses'] ), '%s' ) ) . ')',
		'(' . implode( ' OR ', array_map( static fn( string $field ): string => $columns[ $field ] . ' LIKE %s', $request['fields'] ) ) . ')',
	);
	$args = array_merge( $request['post_types'], $request['statuses'], $likes );
	if ( $request['post_ids'] ) {
		$where[] = 'ID IN (' . implode( ', ', array_fill( 0, count( $request['post_ids'] ), '%d' ) ) . ')';
		$args = array_merge( $args, $request['post_ids'] );
	}
	$query = "SELECT ID FROM {$wpdb->posts} WHERE " . implode( ' AND ', $where ) . ' ORDER BY ID ASC LIMIT 1001';
	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- placeholders and fixed columns only.
	$ids = $wpdb->get_col( $wpdb->prepare( $query, $args ) );
	if ( count( $ids ) > 1000 ) {
		return new WP_Error( 'wpagent_content_replace_scan_limit', __( 'Mehr als 1.000 mögliche Beiträge gefunden. Grenze Beitragstypen, Status oder IDs weiter ein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return array_map( 'absint', $ids );
}

/** @return array|WP_Error */
function wpagent_companion_content_replace_plan( array $request ) {
	$ids = wpagent_companion_content_replace_candidates( $request );
	if ( is_wp_error( $ids ) ) {
		return $ids;
	}
	$map = array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' );
	$items = array();
	foreach ( $ids as $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post || ! current_user_can( 'edit_post', $post_id ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Mindestens ein betroffener Beitrag darf nicht bearbeitet werden.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$before = array(); $after = array(); $matches = array();
		foreach ( $request['fields'] as $field ) {
			$value = (string) $post->{$map[ $field ]};
			$count = substr_count( $value, $request['search'] );
			if ( $count > 0 ) {
				if ( 'content' === $field ) {
					$detection = wpagent_exec_detect_builder( array( 'post_id' => $post_id ) );
					if ( is_wp_error( $detection ) || ! wpagent_builder_content_write_supported( $detection, $post->post_type ) ) {
						return new WP_Error( 'wpagent_content_replace_builder', __( 'Mindestens ein Treffer liegt in einem proprietären Builder-Fremdspeicher. Die Ersetzung wird vollständig abgelehnt.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $post_id ) );
					}
				}
				$before[ $field ] = $value;
				$after[ $field ] = str_replace( $request['search'], $request['replace'], $value );
				$matches[ $field ] = $count;
			}
		}
		if ( $matches ) {
			$items[] = array( 'post_id' => $post_id, 'post_type' => $post->post_type, 'matches' => $matches, 'before' => $before, 'after' => $after );
		}
	}
	if ( count( $items ) > $request['limit'] ) {
		return new WP_Error( 'wpagent_content_replace_limit', __( 'Der Treffer-Deckel wurde überschritten. Grenze die Ersetzung weiter ein.', 'wp-agent-companion' ), array( 'status' => 400, 'matches' => count( $items ), 'limit' => $request['limit'] ) );
	}
	return $items;
}

/** @return true|WP_Error */
function wpagent_companion_write_content_fields( int $post_id, array $values ) {
	$map = array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' );
	$postarr = array( 'ID' => $post_id );
	foreach ( $values as $field => $value ) {
		if ( isset( $map[ $field ] ) ) {
			$postarr[ $map[ $field ] ] = (string) $value;
		}
	}
	$result = wp_update_post( wp_slash( $postarr ), true );
	if ( is_wp_error( $result ) ) {
		return $result;
	}
	$fresh = get_post( $post_id );
	foreach ( $values as $field => $value ) {
		if ( ! $fresh || (string) $fresh->{$map[ $field ]} !== (string) $value ) {
			return new WP_Error( 'wpagent_content_replace_verify_failed', __( 'Ein geändertes Inhaltsfeld bestand die Rückleseprüfung nicht.', 'wp-agent-companion' ), array( 'status' => 500, 'post_id' => $post_id, 'field' => $field ) );
		}
	}
	return true;
}

function wpagent_companion_content_item_matches( array $item, string $state ): bool {
	$map    = array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' );
	$post   = get_post( absint( $item['post_id'] ?? 0 ) );
	$values = $item[ $state ] ?? null;
	if ( ! $post || ! is_array( $values ) ) {
		return false;
	}
	foreach ( $values as $field => $expected ) {
		if ( ! isset( $map[ $field ] ) || (string) $post->{$map[ $field ]} !== (string) $expected ) {
			return false;
		}
	}
	return true;
}

/**
 * Restores one operation state only while every affected field still equals
 * the state written by this operation. This prevents rollback from erasing a
 * concurrent editor's newer value.
 *
 * @return true|WP_Error
 */
function wpagent_companion_restore_content_items( array $items, string $state, string $expected_state ) {
	$first_error = null;
	foreach ( array_reverse( $items ) as $item ) {
		if ( ! wpagent_companion_content_item_matches( $item, $expected_state ) ) {
			if ( null === $first_error ) {
				$first_error = new WP_Error(
					'wpagent_content_operation_conflict',
					__( 'Ein betroffener Beitrag wurde während der Wiederherstellung erneut geändert. Der neuere Zustand bleibt erhalten.', 'wp-agent-companion' ),
					array( 'status' => 409, 'post_id' => absint( $item['post_id'] ?? 0 ) )
				);
			}
			continue;
		}
		$values = is_array( $item[ $state ] ?? null ) ? $item[ $state ] : array();
		$result = wpagent_companion_write_content_fields( absint( $item['post_id'] ?? 0 ), $values );
		if ( is_wp_error( $result ) && null === $first_error ) {
			$first_error = $result;
		}
	}
	return $first_error ?? true;
}

/** @return array|WP_Error */
function wpagent_exec_search_replace_content( $input ) {
	if ( is_array( $input ) && array_key_exists( 'dry_run', $input ) && ! is_bool( $input['dry_run'] ) ) {
		return new WP_Error( 'wpagent_content_replace_dry_run', __( 'dry_run muss ein boolescher Wert sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$request = wpagent_companion_content_replace_request( is_array( $input ) ? $input : array() );
	if ( is_wp_error( $request ) ) { return $request; }
	$items = wpagent_companion_content_replace_plan( $request );
	if ( is_wp_error( $items ) ) { return $items; }
	$public = array_map( static fn( array $item ): array => array( 'post_id' => $item['post_id'], 'post_type' => $item['post_type'], 'matches' => $item['matches'], 'replacement_count' => array_sum( $item['matches'] ) ), $items );
	$total = array_sum( array_column( $public, 'replacement_count' ) );
	$dry_run = ! array_key_exists( 'dry_run', (array) $input ) || (bool) $input['dry_run'];
	if ( $dry_run || ! $items ) {
		return array( 'dry_run' => $dry_run, 'matched_posts' => count( $items ), 'updated_posts' => 0, 'total_replacements' => $total, 'items' => $public, 'operation_id' => '', 'verified' => $dry_run );
	}
	$lock = wpagent_companion_content_operation_lock();
	if ( is_wp_error( $lock ) ) { return $lock; }
	try {
		$id = wp_generate_uuid4();
		$operations = wpagent_companion_content_operations();
		$operations[ $id ] = array( 'id' => $id, 'created_at' => time(), 'user_id' => get_current_user_id(), 'status' => 'prepared', 'items' => $items );
		$saved = wpagent_companion_save_content_operations( $operations );
		if ( is_wp_error( $saved ) ) { return $saved; }
		$written = array();
		foreach ( $items as $item ) {
			if ( ! current_user_can( 'edit_post', $item['post_id'] ) || ! wpagent_companion_content_item_matches( $item, 'before' ) ) {
				$rollback = wpagent_companion_restore_content_items( $written, 'before', 'after' );
				if ( is_wp_error( $rollback ) ) { $operations[ $id ]['status'] = 'recovery_required'; } else { unset( $operations[ $id ] ); }
				$persisted = wpagent_companion_save_content_operations( $operations );
				if ( is_wp_error( $rollback ) || is_wp_error( $persisted ) ) {
					return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Die Inhaltsersetzung wurde gleichzeitig verändert und konnte nicht vollständig zurückgerollt werden. Stelle den Zustand anhand des gespeicherten Snapshots wieder her.', 'wp-agent-companion' ), array( 'status' => 500, 'operation_id' => $id ) );
				}
				return new WP_Error( 'wpagent_content_operation_conflict', __( 'Mindestens ein Feld wurde während der Ersetzung geändert. Die Operation wurde zurückgerollt.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $item['post_id'] ) );
			}
			$result = wpagent_companion_write_content_fields( $item['post_id'], $item['after'] );
			if ( is_wp_error( $result ) ) {
				$attempted = array_merge( $written, array( $item ) );
				$rollback = wpagent_companion_restore_content_items( $attempted, 'before', 'after' );
				if ( is_wp_error( $rollback ) ) { $operations[ $id ]['status'] = 'recovery_required'; } else { unset( $operations[ $id ] ); }
				$persisted = wpagent_companion_save_content_operations( $operations );
				if ( is_wp_error( $rollback ) || is_wp_error( $persisted ) ) {
					return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Die fehlgeschlagene Inhaltsersetzung konnte nicht vollständig zurückgerollt werden. Stelle den Zustand anhand des gespeicherten Snapshots wieder her.', 'wp-agent-companion' ), array( 'status' => 500, 'operation_id' => $id ) );
				}
				return $result;
			}
			$written[] = $item;
		}
		$operations[ $id ]['status'] = 'ready';
		$saved = wpagent_companion_save_content_operations( $operations );
		if ( is_wp_error( $saved ) ) {
			$rollback = wpagent_companion_restore_content_items( $written, 'before', 'after' );
			if ( is_wp_error( $rollback ) ) { $operations[ $id ]['status'] = 'recovery_required'; } else { unset( $operations[ $id ] ); }
			$persisted = wpagent_companion_save_content_operations( $operations );
			if ( is_wp_error( $rollback ) || is_wp_error( $persisted ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Die Inhaltsersetzung konnte nach einem Speicherfehler nicht vollständig zurückgerollt werden. Stelle den Zustand anhand des gespeicherten Snapshots wieder her.', 'wp-agent-companion' ), array( 'status' => 500, 'operation_id' => $id ) );
			}
			return $saved;
		}
		return array( 'dry_run' => false, 'matched_posts' => count( $items ), 'updated_posts' => count( $items ), 'total_replacements' => $total, 'items' => $public, 'operation_id' => $id, 'verified' => true );
	} finally {
		wpagent_companion_content_operation_unlock( $lock );
	}
}

/** @return true|WP_Error */
function wpagent_companion_validate_content_undo( array $operation ) {
	$map = array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' );
	foreach ( $operation['items'] as $item ) {
		if ( ! current_user_can( 'edit_post', $item['post_id'] ) ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Mindestens ein betroffener Beitrag darf nicht bearbeitet werden.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$post = get_post( $item['post_id'] );
		foreach ( $item['after'] as $field => $expected ) {
			if ( ! isset( $map[ $field ] ) || ! $post || (string) $post->{$map[ $field ]} !== (string) $expected ) {
				return new WP_Error( 'wpagent_content_operation_conflict', __( 'Mindestens ein Feld wurde nach der Ersetzung erneut geändert. Rückgängig wird vollständig abgelehnt.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $item['post_id'], 'field' => $field ) );
			}
		}
	}
	return true;
}

/** @return array|WP_Error */
function wpagent_exec_undo_content_replace( $input ) {
	$input = is_array( $input ) ? $input : array();
	if ( ! is_string( $input['operation_id'] ?? null ) || ! wp_is_uuid( $input['operation_id'] ) ) {
		return new WP_Error( 'wpagent_content_operation_id', __( 'operation_id muss eine gültige UUID sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_key_exists( 'dry_run', $input ) && ! is_bool( $input['dry_run'] ) ) {
		return new WP_Error( 'wpagent_content_replace_dry_run', __( 'dry_run muss ein boolescher Wert sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$id = $input['operation_id'];
	$dry_run = ! array_key_exists( 'dry_run', $input ) || (bool) $input['dry_run'];
	$operations = wpagent_companion_content_operations();
	if ( ! isset( $operations[ $id ] ) ) { return new WP_Error( 'wpagent_content_operation_not_found', __( 'Ersetzungsoperation nicht gefunden oder abgelaufen.', 'wp-agent-companion' ), array( 'status' => 404 ) ); }
	$operation = $operations[ $id ];
	if ( 'undone' === ( $operation['status'] ?? '' ) ) { return new WP_Error( 'wpagent_content_operation_already_undone', __( 'Diese Ersetzungsoperation wurde bereits rückgängig gemacht.', 'wp-agent-companion' ), array( 'status' => 409 ) ); }
	if ( 'ready' !== ( $operation['status'] ?? '' ) || ( (int) ( $operation['user_id'] ?? 0 ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) ) {
		return new WP_Error( 'wpagent_content_operation_forbidden', __( 'Diese Ersetzungsoperation kann nicht von diesem Nutzer rückgängig gemacht werden.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$valid = wpagent_companion_validate_content_undo( $operation );
	if ( is_wp_error( $valid ) ) { return $valid; }
	if ( $dry_run ) { return array( 'dry_run' => true, 'restorable_posts' => count( $operation['items'] ), 'undone' => false, 'verified' => true ); }
	$lock = wpagent_companion_content_operation_lock();
	if ( is_wp_error( $lock ) ) { return $lock; }
	try {
		$operations = wpagent_companion_content_operations();
		if ( ! isset( $operations[ $id ] ) || 'ready' !== ( $operations[ $id ]['status'] ?? '' ) ) {
			return new WP_Error( 'wpagent_content_operation_conflict', __( 'Die Ersetzungsoperation hat sich zwischenzeitlich geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$operation = $operations[ $id ];
		if ( (int) ( $operation['user_id'] ?? 0 ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'wpagent_content_operation_forbidden', __( 'Diese Ersetzungsoperation kann nicht von diesem Nutzer rückgängig gemacht werden.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$valid = wpagent_companion_validate_content_undo( $operation );
		if ( is_wp_error( $valid ) ) { return $valid; }
		$restored = array();
		foreach ( $operation['items'] as $item ) {
			$result = wpagent_companion_write_content_fields( $item['post_id'], $item['before'] );
			if ( is_wp_error( $result ) ) {
				$attempted = array_merge( $restored, array( $item ) );
				$rollforward = wpagent_companion_restore_content_items( $attempted, 'after', 'before' );
				if ( is_wp_error( $rollforward ) ) {
					return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Rückgängig ist fehlgeschlagen und der Ersetzungszustand konnte nicht vollständig wiederhergestellt werden. Der Snapshot bleibt für die manuelle Wiederherstellung erhalten.', 'wp-agent-companion' ), array( 'status' => 500, 'operation_id' => $id ) );
				}
				return $result;
			}
			$restored[] = $item;
		}
		$operations[ $id ]['status'] = 'undone';
		$operations[ $id ]['undone_at'] = time();
		$operations[ $id ]['items'] = array_map( static fn( array $item ): array => array( 'post_id' => $item['post_id'], 'post_type' => $item['post_type'], 'matches' => $item['matches'] ), $operation['items'] );
		$saved = wpagent_companion_save_content_operations( $operations );
		if ( is_wp_error( $saved ) ) {
			$rollforward = wpagent_companion_restore_content_items( $restored, 'after', 'before' );
			$recovery_operations = wpagent_companion_content_operations();
			$recovery_operations[ $id ] = $operation;
			$recovered = wpagent_companion_save_content_operations( $recovery_operations );
			if ( is_wp_error( $rollforward ) || is_wp_error( $recovered ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Rückgängig wurde ausgeführt, aber der Operationsstatus konnte nicht sicher gespeichert oder zurückgesetzt werden. Der Snapshot bleibt für die manuelle Wiederherstellung erhalten.', 'wp-agent-companion' ), array( 'status' => 500, 'operation_id' => $id ) );
			}
			return $saved;
		}
		return array( 'dry_run' => false, 'restorable_posts' => count( $restored ), 'undone' => true, 'verified' => true );
	} finally {
		wpagent_companion_content_operation_unlock( $lock );
	}
}
