<?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_POST_LOCK_PREFIX', 'wp_agent_content_replace_post_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', 'additionalProperties' => false,
			'properties' => array(
				'search' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 200 ), 'replace' => array( 'type' => 'string', 'maxLength' => 200 ),
				'fields' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array( 'title', 'excerpt', 'content' ) ), 'minItems' => 1, 'maxItems' => 3, 'uniqueItems' => true ),
				'post_types' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 20 ), 'minItems' => 1, 'maxItems' => 20, 'uniqueItems' => true ),
				'statuses' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'enum' => array( 'publish', 'draft', 'pending', 'private', 'future' ) ), 'minItems' => 1, 'maxItems' => 5, 'uniqueItems' => true ),
				'post_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'integer', 'minimum' => 1 ), 'maxItems' => 100, 'uniqueItems' => true ),
				'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', 'additionalProperties' => false,
			'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', 'maxItems' => 100, 'items' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1 ), 'post_type' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 20 ), 'matches' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'title' => array( 'type' => 'integer', 'minimum' => 1 ), 'excerpt' => array( 'type' => 'integer', 'minimum' => 1 ), 'content' => array( 'type' => 'integer', 'minimum' => 1 ) ), 'minProperties' => 1 ), 'replacement_count' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 300 ) ), 'required' => array( 'post_id', 'post_type', 'matches', 'replacement_count' ) ) ), 'operation_id' => array( 'type' => 'string', 'maxLength' => 36 ),
				'verified' => array( 'type' => 'boolean' ),
			), 'required' => array( 'dry_run', 'matched_posts', 'updated_posts', 'total_replacements', 'items', 'operation_id', 'verified' ),
		),
		'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', 'additionalProperties' => false, 'properties' => array(
				'operation_id' => array( 'type' => 'string', 'format' => 'uuid' ), 'dry_run' => array( 'type' => 'boolean', 'default' => true ),
			), 'required' => array( 'operation_id' ),
		),
		'output_schema' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array(
			'dry_run' => array( 'type' => 'boolean' ), 'restorable_posts' => array( 'type' => 'integer' ),
			'undone' => array( 'type' => 'boolean' ), 'verified' => array( 'type' => 'boolean' ),
		), 'required' => array( 'dry_run', 'restorable_posts', 'undone', 'verified' ) ),
		'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' => false ) ),
	);
}

/** @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 );
}

function wpagent_companion_content_post_lock_name( int $post_id ): string {
	return WPAGENT_COMPANION_CONTENT_POST_LOCK_PREFIX . $post_id;
}

/** @return string|WP_Error */
function wpagent_companion_content_acquire_post_lock( int $post_id ) {
	$token = $post_id > 0 ? wpagent_companion_acquire_option_lock( wpagent_companion_content_post_lock_name( $post_id ), WPAGENT_COMPANION_CONTENT_LOCK_TTL ) : false;
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_content_post_busy', __( 'Ein betroffener Beitrag wird bereits geändert.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $post_id ) );
}

function wpagent_companion_content_refresh_post_lock( int $post_id, string $token ): bool {
	return $post_id > 0 && '' !== $token
		&& wpagent_companion_refresh_option_lock( wpagent_companion_content_post_lock_name( $post_id ), $token, WPAGENT_COMPANION_CONTENT_LOCK_TTL )
		&& wpagent_companion_option_lock_is_owned( wpagent_companion_content_post_lock_name( $post_id ), $token );
}

function wpagent_companion_content_release_post_lock( int $post_id, string $token ): void {
	if ( $post_id > 0 && '' !== $token ) {
		wpagent_companion_release_option_lock( wpagent_companion_content_post_lock_name( $post_id ), $token );
	}
}

/** @return array<int,string>|WP_Error */
function wpagent_companion_content_acquire_post_locks( array $items ) {
	$post_ids = array_values( array_unique( array_map( static fn( array $item ): int => absint( $item['post_id'] ?? 0 ), $items ) ) );
	sort( $post_ids, SORT_NUMERIC );
	$locks = array();
	foreach ( $post_ids as $post_id ) {
		$token = wpagent_companion_content_acquire_post_lock( $post_id );
		if ( is_wp_error( $token ) ) {
			wpagent_companion_content_release_post_locks( $locks );
			return $token;
		}
		$locks[ $post_id ] = $token;
	}
	return $locks;
}

/** @param array<int,string> $locks */
function wpagent_companion_content_release_post_locks( array $locks ): void {
	foreach ( $locks as $post_id => $token ) {
		if ( is_int( $post_id ) && is_string( $token ) ) {
			wpagent_companion_content_release_post_lock( $post_id, $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 true|WP_Error */
function wpagent_companion_content_store_operation( string $id, array $operation ) {
	$lock = wpagent_companion_content_operation_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$operations        = wpagent_companion_content_operations();
		$operations[ $id ] = $operation;
		return wpagent_companion_save_content_operations( $operations );
	} finally {
		wpagent_companion_content_operation_unlock( $lock );
	}
}

/** @return array<string,mixed>|WP_Error */
function wpagent_companion_content_transition_operation( string $id, string $expected_status, string $next_status, ?array $items = null ) {
	$lock = wpagent_companion_content_operation_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$operations = wpagent_companion_content_operations();
		if ( ! isset( $operations[ $id ] ) || $expected_status !== ( $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_id' => $id ) );
		}
		$operations[ $id ]['status']     = $next_status;
		$operations[ $id ]['updated_at'] = time();
		if ( null !== $items ) {
			$operations[ $id ]['items'] = $items;
		}
		$saved = wpagent_companion_save_content_operations( $operations );
		return is_wp_error( $saved ) ? $saved : $operations[ $id ];
	} finally {
		wpagent_companion_content_operation_unlock( $lock );
	}
}

/** @return array|WP_Error */
function wpagent_companion_content_replace_request( array $input ) {
	if ( array_diff( array_keys( $input ), array( 'search', 'replace', 'fields', 'post_types', 'statuses', 'post_ids', 'limit', 'dry_run' ) ) ) {
		return new WP_Error( 'wpagent_content_replace_input', __( 'Die Inhaltsersetzung akzeptiert keine unbekannten Eingabefelder.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	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 ) || count( $raw_fields ) < 1 || count( $raw_fields ) > 3 || 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 ) );
	}
	$requested_fields = array_map( 'sanitize_key', $raw_fields );
	if ( count( array_unique( $requested_fields ) ) !== count( $requested_fields ) || array_diff( $requested_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 ) );
	}
	$fields = array_values( array_filter( array( 'title', 'excerpt', 'content' ), static fn( string $field ): bool => in_array( $field, $requested_fields, true ) ) );
	$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 ) || count( $raw_post_types ) < 1 || count( $raw_post_types ) > 20 || array_filter( $raw_post_types, static fn( $value ): bool => ! is_string( $value ) || strlen( $value ) < 1 || strlen( $value ) > 20 ) ) {
		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_map( 'sanitize_key', $raw_post_types );
	if ( count( array_unique( $post_types ) ) !== count( $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 ) );
	}
	sort( $post_types, SORT_STRING );
	$allowed_statuses = array( 'publish', 'draft', 'pending', 'private', 'future' );
	$raw_statuses = array_key_exists( 'statuses', $input ) ? $input['statuses'] : $allowed_statuses;
	if ( ! is_array( $raw_statuses ) || count( $raw_statuses ) < 1 || count( $raw_statuses ) > 5 || 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 ) );
	}
	$requested_statuses = array_map( 'sanitize_key', $raw_statuses );
	if ( count( array_unique( $requested_statuses ) ) !== count( $requested_statuses ) || array_diff( $requested_statuses, $allowed_statuses ) ) {
		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_filter( $allowed_statuses, static fn( string $status ): bool => in_array( $status, $requested_statuses, true ) ) );
	$raw_post_ids = $input['post_ids'] ?? array();
	if ( ! is_array( $raw_post_ids ) || count( $raw_post_ids ) > 100 || array_filter( $raw_post_ids, static fn( $value ): bool => ! is_int( $value ) || $value < 1 ) || count( array_unique( $raw_post_ids, SORT_REGULAR ) ) !== count( $raw_post_ids ) ) {
		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( $raw_post_ids );
	sort( $post_ids, SORT_NUMERIC );
	$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 ) {
			$before_state = wpagent_companion_content_post_state( $post_id );
			if ( ! is_array( $before_state ) || (string) $before_state['post_type'] !== (string) $post->post_type ) {
				return new WP_Error( 'wpagent_content_operation_snapshot', __( 'Der vollständige Beitrags-Snapshot konnte nicht gelesen werden.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $post_id ) );
			}
			$after_state = $before_state;
			foreach ( array( 'title' => 'post_title', 'excerpt' => 'post_excerpt', 'content' => 'post_content' ) as $field => $column ) {
				if ( array_key_exists( $field, $after ) ) {
					$after_state[ $column ] = (string) $after[ $field ];
				}
			}
			$after_state['post_modified'] = current_time( 'mysql' );
			$after_state['post_modified_gmt'] = current_time( 'mysql', true );
			$items[] = array( 'post_id' => $post_id, 'post_type' => $post->post_type, 'matches' => $matches, 'before' => $before, 'after' => $after, 'before_state' => $before_state, 'after_state' => $after_state );
		}
	}
	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 array<string,mixed>|null */
function wpagent_companion_content_post_state( int $post_id ): ?array {
	global $wpdb;
	$columns = array( 'ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_excerpt', 'post_status', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order', 'post_type', 'post_mime_type', 'comment_count' );
	$row = $post_id > 0 ? $wpdb->get_row( $wpdb->prepare( "SELECT " . implode( ', ', $columns ) . " FROM {$wpdb->posts} WHERE ID = %d", $post_id ), ARRAY_A ) : null;
	if ( ! is_array( $row ) || count( $row ) !== count( $columns ) ) {
		return null;
	}
	$state = array();
	foreach ( $columns as $column ) {
		$key = 'ID' === $column ? 'post_id' : $column;
		$state[ $key ] = in_array( $column, array( 'ID', 'post_author', 'post_parent', 'menu_order', 'comment_count' ), true ) ? (int) $row[ $column ] : (string) $row[ $column ];
	}
	return $state;
}

function wpagent_companion_content_state_hash( array $state ): string {
	return hash( 'sha256', serialize( $state ) );
}

function wpagent_companion_content_item_matches( array $item, string $state ): bool {
	$post_id = absint( $item['post_id'] ?? 0 );
	$expected = $item[ $state . '_state' ] ?? null;
	$current = wpagent_companion_content_post_state( $post_id );
	return is_array( $expected ) && is_array( $current ) && hash_equals( wpagent_companion_content_state_hash( $expected ), wpagent_companion_content_state_hash( $current ) );
}

/** @return true|WP_Error */
function wpagent_companion_content_compare_and_swap( array $before_state, array $after_state ) {
	global $wpdb;
	$post_id = absint( $before_state['post_id'] ?? 0 );
	if ( $post_id < 1 || $post_id !== absint( $after_state['post_id'] ?? 0 ) ) {
		return new WP_Error( 'wpagent_content_operation_snapshot', __( 'Der gespeicherte Beitrags-Snapshot ist ungültig.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$query = "UPDATE {$wpdb->posts} SET post_title = %s, post_excerpt = %s, post_content = %s, post_modified = %s, post_modified_gmt = %s WHERE ID = %d AND post_type = %s AND post_status = %s AND post_title = %s AND post_excerpt = %s AND post_content = %s AND post_modified = %s AND post_modified_gmt = %s";
	$updated = $wpdb->query( $wpdb->prepare( $query, (string) $after_state['post_title'], (string) $after_state['post_excerpt'], (string) $after_state['post_content'], (string) $after_state['post_modified'], (string) $after_state['post_modified_gmt'], $post_id, (string) $before_state['post_type'], (string) $before_state['post_status'], (string) $before_state['post_title'], (string) $before_state['post_excerpt'], (string) $before_state['post_content'], (string) $before_state['post_modified'], (string) $before_state['post_modified_gmt'] ) );
	if ( false === $updated ) {
		return new WP_Error( 'wpagent_content_operation_write_failed', __( 'Die bedingte Inhaltsänderung konnte nicht gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'post_id' => $post_id ) );
	}
	if ( 1 !== $updated ) {
		return new WP_Error( 'wpagent_content_operation_conflict', __( 'Ein betroffener Beitrag wurde gleichzeitig geändert. Sein neuer Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $post_id ) );
	}
	clean_post_cache( $post_id );
	$current = wpagent_companion_content_post_state( $post_id );
	if ( ! is_array( $current ) || ! hash_equals( wpagent_companion_content_state_hash( $after_state ), wpagent_companion_content_state_hash( $current ) ) ) {
		return new WP_Error( 'wpagent_content_operation_verify_failed', __( 'Die bedingte Inhaltsänderung bestand die vollständige Rückleseprüfung nicht.', 'wp-agent-companion' ), array( 'status' => 500, 'post_id' => $post_id ) );
	}
	return true;
}

/** @return array|WP_Error */
function wpagent_exec_search_replace_content( $input ) {
	if ( ! is_array( $input ) ) {
		return new WP_Error( 'wpagent_content_replace_input', __( 'Die Inhaltsersetzung benötigt ein Objekt als Eingabe.', '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 ) );
	}
	$request = wpagent_companion_content_replace_request( $input );
	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', $input ) || $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' => true );
	}
	$id = wp_generate_uuid4();
	$stored = wpagent_companion_content_store_operation( $id, array( 'id' => $id, 'created_at' => time(), 'user_id' => get_current_user_id(), 'status' => 'recovery_required', 'items' => $items ) );
	if ( is_wp_error( $stored ) ) { return $stored; }
	$locks = wpagent_companion_content_acquire_post_locks( $items );
	if ( is_wp_error( $locks ) ) {
		return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Ein Beitrag konnte nicht exklusiv gesperrt werden. Der gespeicherte Snapshot bleibt für eine manuelle Prüfung erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'post_id' => absint( $locks->get_error_data()['post_id'] ?? 0 ), 'recovery_required' => true ) );
	}
	try {
		foreach ( $items as $item ) {
			$post_id = (int) $item['post_id'];
			$token = $locks[ $post_id ] ?? '';
			if ( ! is_string( $token ) || ! current_user_can( 'edit_post', $post_id ) || ! wpagent_companion_content_refresh_post_lock( $post_id, $token ) || ! wpagent_companion_content_item_matches( $item, 'before' ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Ein betroffener Beitrag wurde während der Ersetzung verändert. Sein neuer Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'post_id' => $post_id, 'recovery_required' => true ) );
			}
			$result = wpagent_companion_content_compare_and_swap( $item['before_state'], $item['after_state'] );
			if ( is_wp_error( $result ) || ! wpagent_companion_content_refresh_post_lock( $post_id, $token ) || ! wpagent_companion_content_item_matches( $item, 'after' ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Eine Inhaltsänderung konnte nicht vollständig verifiziert werden. Es wurde kein automatischer Rollback ausgeführt.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'post_id' => $post_id, 'recovery_required' => true ) );
			}
		}
	} finally {
		wpagent_companion_content_release_post_locks( $locks );
	}
	$ready = wpagent_companion_content_transition_operation( $id, 'recovery_required', 'ready' );
	if ( is_wp_error( $ready ) ) {
		return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Die Inhaltsersetzung wurde ausgeführt, aber ihr Operationsstatus bleibt für die manuelle Wiederherstellung erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'recovery_required' => true ) );
	}
	return array( 'dry_run' => false, 'matched_posts' => count( $items ), 'updated_posts' => count( $items ), 'total_replacements' => $total, 'items' => $public, 'operation_id' => $id, 'verified' => true );
}

/** @return true|WP_Error */
function wpagent_companion_validate_content_undo( array $operation ) {
	if ( ! is_array( $operation['items'] ?? null ) || count( $operation['items'] ) < 1 || count( $operation['items'] ) > 100 ) {
		return new WP_Error( 'wpagent_content_operation_snapshot', __( 'Der gespeicherte Rückgängig-Snapshot ist ungültig.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	foreach ( $operation['items'] as $item ) {
		$post_id = absint( is_array( $item ) ? ( $item['post_id'] ?? 0 ) : 0 );
		if ( ! is_array( $item ) || ! 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 ) );
		}
		if ( ! wpagent_companion_content_item_matches( $item, 'after' ) ) {
			return new WP_Error( 'wpagent_content_operation_conflict', __( 'Mindestens ein Beitrag wurde nach der Ersetzung erneut geändert. Sein neuer Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'post_id' => $post_id ) );
		}
	}
	return true;
}

/** @return array|WP_Error */
function wpagent_exec_undo_content_replace( $input ) {
	if ( ! is_array( $input ) || array_diff( array_keys( $input ), array( 'operation_id', 'dry_run' ) ) ) {
		return new WP_Error( 'wpagent_content_operation_input', __( 'Die Rückgängig-Operation akzeptiert nur operation_id und dry_run.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	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 ); }
	$locks = wpagent_companion_content_acquire_post_locks( $operation['items'] );
	if ( is_wp_error( $locks ) ) { return $locks; }
	try {
		$operations = wpagent_companion_content_operations();
		if ( ! isset( $operations[ $id ] ) || 'ready' !== ( $operations[ $id ]['status'] ?? '' ) || (int) ( $operations[ $id ]['user_id'] ?? 0 ) !== get_current_user_id() && ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'wpagent_content_operation_conflict', __( 'Die Ersetzungsoperation hat sich zwischenzeitlich geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$operation = $operations[ $id ];
		$valid = wpagent_companion_validate_content_undo( $operation );
		if ( is_wp_error( $valid ) ) { return $valid; }
		$armed = wpagent_companion_content_transition_operation( $id, 'ready', 'recovery_required' );
		if ( is_wp_error( $armed ) ) { return $armed; }
		foreach ( $operation['items'] as $item ) {
			$post_id = (int) $item['post_id'];
			$token = $locks[ $post_id ] ?? '';
			if ( ! is_string( $token ) || ! current_user_can( 'edit_post', $post_id ) || ! wpagent_companion_content_refresh_post_lock( $post_id, $token ) || ! wpagent_companion_content_item_matches( $item, 'after' ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Ein Beitrag wurde vor dem Rückgängig erneut geändert. Sein neuer Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'post_id' => $post_id, 'recovery_required' => true ) );
			}
			$result = wpagent_companion_content_compare_and_swap( $item['after_state'], $item['before_state'] );
			if ( is_wp_error( $result ) || ! wpagent_companion_content_refresh_post_lock( $post_id, $token ) || ! wpagent_companion_content_item_matches( $item, 'before' ) ) {
				return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Rückgängig konnte nicht vollständig verifiziert werden. Es wurde kein automatisches Vorwärtsrollen ausgeführt.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'post_id' => $post_id, 'recovery_required' => true ) );
			}
		}
	} finally {
		wpagent_companion_content_release_post_locks( $locks );
	}
	$public_items = array_map( static fn( array $item ): array => array( 'post_id' => $item['post_id'], 'post_type' => $item['post_type'], 'matches' => $item['matches'] ), $operation['items'] );
	$undone = wpagent_companion_content_transition_operation( $id, 'recovery_required', 'undone', $public_items );
	if ( is_wp_error( $undone ) ) {
		return new WP_Error( 'wpagent_content_operation_recovery_required', __( 'Rückgängig wurde ausgeführt, aber der Operationsstatus bleibt für die manuelle Wiederherstellung erhalten.', 'wp-agent-companion' ), array( 'status' => 409, 'operation_id' => $id, 'recovery_required' => true ) );
	}
	return array( 'dry_run' => false, 'restorable_posts' => count( $public_items ), 'undone' => true, 'verified' => true );
}
