<?php
/** Targeted, schema-bound writes for server-registered Spectra blocks. */

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

const WPAGENT_NATIVE_BLOCK_MAX_PATCH_ATTRIBUTES = 50;
const WPAGENT_NATIVE_BLOCK_MAX_ARRAY_ITEMS      = 100;
const WPAGENT_NATIVE_BLOCK_MAX_STRING_LENGTH    = 4096;

/** @return array<string,mixed> */
function wpagent_ability_update_spectra_block_attributes(): array {
	return array(
		'label'       => __( 'Spectra-Blockattribute aktualisieren', 'wp-agent-companion' ),
		'description' => __( 'Ändert ausschließlich registrierte, schema-bekannte skalare Attribute eines dynamischen Spectra-Blocks an einem expliziten Blockpfad. Rohes Markup, HTML, freie CSS-Deklarationen, Objektattribute und nicht registrierte Blöcke bleiben gesperrt.', 'wp-agent-companion' ),
		'category'    => 'wp-agent',
		'input_schema' => array(
			'type'       => 'object',
			'properties' => array(
				'post_id' => array( 'type' => 'integer', 'minimum' => 1 ),
				'path' => array(
					'type' => 'array', 'minItems' => 1, 'maxItems' => 32,
					'items' => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 10000 ),
				),
				'attributes' => array( 'type' => 'object', 'maxProperties' => WPAGENT_NATIVE_BLOCK_MAX_PATCH_ATTRIBUTES, 'additionalProperties' => true ),
				'remove_attributes' => array(
					'type' => 'array', 'maxItems' => WPAGENT_NATIVE_BLOCK_MAX_PATCH_ATTRIBUTES, 'uniqueItems' => true,
					'items' => array( 'type' => 'string', 'pattern' => '^[A-Za-z][A-Za-z0-9_-]{0,127}$' ),
				),
				'expected_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required'             => array( 'post_id', 'path', 'expected_hash' ),
			'additionalProperties' => false,
		),
		'output_schema' => array(
			'type'       => 'object',
			'properties' => array(
				'post_id' => array( 'type' => 'integer' ),
				'block_name' => array( 'type' => 'string' ),
				'path' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ),
				'written_attributes' => array( 'type' => 'object', 'additionalProperties' => true ),
				'removed_attributes' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'changed' => array( 'type' => 'boolean' ),
				'verified' => array( 'type' => 'boolean' ),
				'revision_id' => array( 'type' => 'integer' ),
				'rendered_html_length' => array( 'type' => 'integer' ),
				'previous_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required' => array( 'post_id', 'block_name', 'path', 'written_attributes', 'removed_attributes', 'changed', 'verified', 'revision_id', 'rendered_html_length', 'previous_hash', 'hash' ),
			'additionalProperties' => false,
		),
		'execute_callback'    => 'wpagent_exec_update_spectra_block_attributes',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta' => array(
			'show_in_rest' => true,
			'annotations'  => array( 'readonly' => false, 'destructive' => false, 'idempotent' => true ),
		),
	);
}

function wpagent_native_block_patchable_attribute_name( string $name ): bool {
	if ( ! preg_match( '/^[A-Za-z][A-Za-z0-9_-]{0,127}$/', $name ) || wpagent_native_block_sensitive_key( $name ) ) {
		return false;
	}
	return ! in_array( strtolower( $name ), array( 'block_id', 'blockid', 'classname', 'style', 'metadata', 'lock', 'ispreview', 'customcss', 'custom_css' ), true )
		&& 0 === preg_match( '/(?:^|_)(?:custom[-_]?css|raw[-_]?html)(?:$|_)/i', $name );
}

/** @param array<string,mixed> $schema */
function wpagent_native_block_patchable_attribute_schema( array $schema, int $depth = 0 ): bool {
	$type = $schema['type'] ?? null;
	if ( ! is_string( $type ) || ! in_array( $type, array( 'boolean', 'integer', 'number', 'string', 'array' ), true ) ) {
		return false;
	}
	if ( 'array' !== $type ) {
		return true;
	}
	return $depth < 2
		&& is_array( $schema['items'] ?? null )
		&& wpagent_native_block_patchable_attribute_schema( $schema['items'], $depth + 1 );
}

/** @param mixed $value @param array<string,mixed> $schema @return true|WP_Error */
function wpagent_native_block_validate_patch_value( string $name, $value, array $schema, int $depth = 0 ) {
	$type = $schema['type'] ?? null;
	if ( ! wpagent_native_block_patchable_attribute_schema( $schema, $depth ) ) {
		return new WP_Error( 'wpagent_native_block_attribute_schema', __( 'Das Attribut besitzt kein freigegebenes skalares Providerschema.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
	}
	$valid_type = match ( $type ) {
		'boolean' => is_bool( $value ),
		'integer' => is_int( $value ),
		'number'  => ( is_int( $value ) || is_float( $value ) ) && is_finite( (float) $value ),
		'string'  => is_string( $value ),
		'array'   => is_array( $value ) && wp_is_numeric_array( $value ),
	};
	if ( ! $valid_type ) {
		return new WP_Error( 'wpagent_native_block_attribute_type', __( 'Der Attributwert entspricht nicht dem registrierten Providertyp.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name, 'expected_type' => $type ) );
	}
	$provider_valid = rest_validate_value_from_schema( $value, $schema, $name );
	if ( is_wp_error( $provider_valid ) ) {
		return new WP_Error( 'wpagent_native_block_attribute_provider_schema', __( 'Der Attributwert verletzt eine Begrenzung des registrierten Providerschemas.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
	}
	if ( isset( $schema['enum'] ) && is_array( $schema['enum'] ) && ! in_array( $value, $schema['enum'], true ) ) {
		return new WP_Error( 'wpagent_native_block_attribute_enum', __( 'Der Attributwert ist im registrierten Providerschema nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
	}
	if ( 'integer' === $type || 'number' === $type ) {
		if ( isset( $schema['minimum'] ) && $value < $schema['minimum'] ) {
			return new WP_Error( 'wpagent_native_block_attribute_minimum', __( 'Der Attributwert unterschreitet die registrierte Untergrenze.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
		if ( isset( $schema['maximum'] ) && $value > $schema['maximum'] ) {
			return new WP_Error( 'wpagent_native_block_attribute_maximum', __( 'Der Attributwert überschreitet die registrierte Obergrenze.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
		if ( abs( (float) $value ) > 1000000 ) {
			return new WP_Error( 'wpagent_native_block_attribute_number', __( 'Numerische Blockattribute sind auf den Bereich -1.000.000 bis 1.000.000 begrenzt.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
	}
	if ( 'string' === $type ) {
		$length = function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
		$maximum = min( WPAGENT_NATIVE_BLOCK_MAX_STRING_LENGTH, isset( $schema['maxLength'] ) ? max( 0, (int) $schema['maxLength'] ) : WPAGENT_NATIVE_BLOCK_MAX_STRING_LENGTH );
		if ( $length > $maximum || ( isset( $schema['minLength'] ) && $length < (int) $schema['minLength'] ) ) {
			return new WP_Error( 'wpagent_native_block_attribute_length', __( 'Der Attributtext liegt außerhalb der erlaubten Länge.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
		$decoded = html_entity_decode( $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		if ( preg_match( '/^\s*(?:javascript|vbscript):|^\s*data:\s*(?:text\/(?:html|xml)|image\/svg\+xml|application\/(?:xml|xhtml\+xml))/iu', $decoded ) ) {
			return new WP_Error( 'wpagent_native_block_attribute_uri', __( 'Aktive Skript- und HTML-Daten-URIs sind in Blockattributen nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
		if ( preg_match( '/[<>]|<!--|-->|[;{}]|(?:expression|url)\s*\(|@import|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/iu', $decoded ) ) {
			return new WP_Error( 'wpagent_native_block_attribute_markup', __( 'HTML, Block-Markup, CSS-Deklarationen und steuernde Kontrollzeichen sind in Blockattributen nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
	}
	if ( 'array' === $type ) {
		if ( $depth >= 2 || count( $value ) > WPAGENT_NATIVE_BLOCK_MAX_ARRAY_ITEMS || ! is_array( $schema['items'] ?? null ) ) {
			return new WP_Error( 'wpagent_native_block_attribute_array', __( 'Das Array-Attribut ist nicht ausreichend begrenzt oder verschachtelt zu tief.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $name ) );
		}
		foreach ( $value as $item ) {
			$valid = wpagent_native_block_validate_patch_value( $name, $item, $schema['items'], $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	return true;
}

/** @param array<int,mixed> $path @return true|WP_Error */
function wpagent_native_block_validate_path( array $path ) {
	if ( array() === $path || count( $path ) > 32 || ! wp_is_numeric_array( $path ) ) {
		return new WP_Error( 'wpagent_native_block_path', __( 'path muss 1-32 explizite Blockindizes enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $path as $index ) {
		if ( ! is_int( $index ) || $index < 0 || $index > 10000 ) {
			return new WP_Error( 'wpagent_native_block_path', __( 'Jeder Blockindex muss eine ganze Zahl zwischen 0 und 10.000 sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	return true;
}

/** @param array<int,array<string,mixed>> $blocks @param array<int,int> $path @return array<string,mixed>|WP_Error */
function wpagent_native_block_patch_path( array &$blocks, array $path, array $attributes, array $remove_attributes, int $depth = 0 ) {
	$index = $path[ $depth ];
	if ( ! isset( $blocks[ $index ] ) || ! is_array( $blocks[ $index ] ) ) {
		return new WP_Error( 'wpagent_native_block_path_not_found', __( 'Der Blockpfad wurde im aktuellen Dokument nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	if ( $depth + 1 < count( $path ) ) {
		if ( ! isset( $blocks[ $index ]['innerBlocks'] ) || ! is_array( $blocks[ $index ]['innerBlocks'] ) ) {
			return new WP_Error( 'wpagent_native_block_path_not_found', __( 'Der Blockpfad wurde im aktuellen Dokument nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		return wpagent_native_block_patch_path( $blocks[ $index ]['innerBlocks'], $path, $attributes, $remove_attributes, $depth + 1 );
	}

	$name = is_string( $blocks[ $index ]['blockName'] ?? null ) ? $blocks[ $index ]['blockName'] : '';
	$registry = wpagent_spectra_block_registry();
	if ( is_wp_error( $registry ) ) {
		return $registry;
	}
	$slug = str_starts_with( $name, 'uagb/' ) ? substr( $name, 5 ) : '';
	if ( '' === $slug || ! isset( $registry[ $slug ] ) || $registry[ $slug ]['name'] !== $name ) {
		return new WP_Error( 'wpagent_native_block_not_safe_spectra', __( 'Am Zielpfad liegt kein freigegebener sichtbarer Spectra-Block.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$type = WP_Block_Type_Registry::get_instance()->get_registered( $name );
	if ( ! $type instanceof WP_Block_Type || ! is_callable( $type->render_callback ) ) {
		return new WP_Error( 'wpagent_native_block_not_server_registered', __( 'Dieser Spectra-Block besitzt kein serverseitig registriertes dynamisches Schema und kann deshalb nicht sicher geändert werden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$schemas = $type->get_attributes();
	foreach ( array_merge( array_keys( $attributes ), $remove_attributes ) as $attribute ) {
		if ( ! is_string( $attribute ) || ! wpagent_native_block_patchable_attribute_name( $attribute ) || ! isset( $schemas[ $attribute ] ) || ! is_array( $schemas[ $attribute ] ) ) {
			return new WP_Error( 'wpagent_native_block_attribute_not_allowed', __( 'Das Attribut ist unbekannt, intern, sensibel oder nicht sicher beschreibbar.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => is_scalar( $attribute ) ? (string) $attribute : '' ) );
		}
	}
	foreach ( $attributes as $attribute => $value ) {
		$valid = wpagent_native_block_validate_patch_value( $attribute, $value, $schemas[ $attribute ] );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	$before = is_array( $blocks[ $index ]['attrs'] ?? null ) ? $blocks[ $index ]['attrs'] : array();
	$after  = $before;
	foreach ( $attributes as $attribute => $value ) {
		$after[ $attribute ] = $value;
	}
	foreach ( $remove_attributes as $attribute ) {
		unset( $after[ $attribute ] );
	}
	$blocks[ $index ]['attrs'] = $after;
	return array( 'block_name' => $name, 'before' => $before, 'after' => $after, 'changed' => $after !== $before );
}

/** @return int|WP_Error */
function wpagent_native_block_create_revision( int $post_id, string $content ) {
	$revision_id = wp_save_post_revision( $post_id );
	if ( is_wp_error( $revision_id ) ) {
		return $revision_id;
	}
	$candidates = array();
	if ( (int) $revision_id > 0 ) {
		$candidates[] = get_post( (int) $revision_id );
	}
	$candidates = array_merge( $candidates, wp_get_post_revisions( $post_id, array( 'numberposts' => 20, 'orderby' => 'ID', 'order' => 'DESC' ) ) );
	foreach ( $candidates as $revision ) {
		if ( $revision instanceof WP_Post && $post_id === (int) $revision->post_parent && $revision->post_content === $content ) {
			return (int) $revision->ID;
		}
	}
	$fallback = _wp_put_post_revision( $post_id );
	if ( is_wp_error( $fallback ) || (int) $fallback <= 0 ) {
		return new WP_Error( 'wpagent_native_block_revision_failed', __( 'Vor der Blockänderung konnte keine wiederherstellbare Revision angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$revision = get_post( (int) $fallback );
	if ( ! $revision instanceof WP_Post || $post_id !== (int) $revision->post_parent || $revision->post_content !== $content ) {
		wp_delete_post_revision( (int) $fallback );
		return new WP_Error( 'wpagent_native_block_revision_failed', __( 'Die angelegte Blockrevision bestand die Rückleseprüfung nicht.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return (int) $fallback;
}

/**
 * Schreibt nur gegen den beim Lesen bestätigten Bytezustand. `HEX()` erzwingt
 * auf MySQL/MariaDB und SQLite einen bytegenauen Vergleich und vermeidet damit
 * Kollationsfallen wie reine Groß-/Kleinschreibung oder abschließende Leer-
 * zeichen. Die Schreibpfade rufen absichtlich keine allgemeinen post-save-
 * Hooks auf: ihre Nebenwirkungen wären weder an diesen CAS noch an den
 * Providervertrag gebunden. Revisions-, Cache- und Read-back-Schritte folgen
 * explizit beim jeweiligen Aufrufer.
 *
 * @return true|WP_Error
 */
function wpagent_native_block_write_if_current( int $post_id, string $before_content, string $candidate_content ) {
	$state = wpagent_native_block_document_state( $post_id );
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	if ( $state['content'] !== $before_content ) {
		return new WP_Error(
			'wpagent_native_block_conflict',
			__( 'Das Blockdokument wurde unmittelbar vor dem Speichern verändert.', 'wp-agent-companion' ),
			array( 'status' => 409, 'current_hash' => $state['hash'] )
		);
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! isset( $wpdb->posts ) ) {
		return new WP_Error( 'wpagent_native_block_write_failed', __( 'Der atomare Block-Write ist in dieser WordPress-Datenbank nicht verfügbar.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$modified     = current_time( 'mysql' );
	$modified_gmt = current_time( 'mysql', true );
	$updated = $wpdb->query(
		$wpdb->prepare(
			"UPDATE {$wpdb->posts} SET post_content = %s, post_modified = %s, post_modified_gmt = %s WHERE ID = %d AND HEX(post_content) = HEX(%s)",
			$candidate_content,
			$modified,
			$modified_gmt,
			$post_id,
			$before_content
		)
	);
	if ( false === $updated ) {
		return new WP_Error( 'wpagent_native_block_write_failed', __( 'Die atomare Blockänderung konnte nicht gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( 1 !== $updated ) {
		$state = wpagent_native_block_document_state( $post_id );
		return new WP_Error(
			'wpagent_native_block_conflict',
			__( 'Das Blockdokument wurde unmittelbar vor dem Speichern verändert.', 'wp-agent-companion' ),
			array( 'status' => 409, 'current_hash' => is_array( $state ) ? $state['hash'] : '' )
		);
	}
	clean_post_cache( $post_id );
	return true;
}

/**
 * Reads post_content directly from the database, bypassing WordPress object
 * cache. Cache hooks and external writers are not bound to WP Agently's lock,
 * so post-cache verification must not rely solely on get_post().
 *
 * @return string|WP_Error
 */
function wpagent_native_block_raw_content( int $post_id ) {
	global $wpdb;
	if ( ! isset( $wpdb ) || ! isset( $wpdb->posts ) ) {
		return new WP_Error( 'wpagent_native_block_read_failed', __( 'Der rohe Blockinhalt ist in dieser WordPress-Datenbank nicht verfügbar.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$row = $wpdb->get_row( $wpdb->prepare( "SELECT post_content FROM {$wpdb->posts} WHERE ID = %d LIMIT 1", $post_id ), ARRAY_A );
	if ( ! is_array( $row ) || ! array_key_exists( 'post_content', $row ) ) {
		return new WP_Error( 'wpagent_native_block_not_found', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return (string) $row['post_content'];
}

/**
 * Stellt nur dann automatisch wieder her, wenn der aktuelle Rohinhalt exakt
 * dem eigenen Kandidaten entspricht. Die lokale WP-Agently-Sperre schützt
 * keine nativen WordPress- oder Provider-Writer.
 *
 * @return array{rolled_back:bool,rollback_verified:bool,recovery_required:bool}
 */
function wpagent_native_block_rollback( int $post_id, string $content, string $candidate_content ): array {
	$state = wpagent_native_block_document_state( $post_id );
	if ( is_wp_error( $state ) || $state['content'] !== $candidate_content ) {
		return array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true );
	}
	global $wpdb;
	if ( ! isset( $wpdb ) || ! isset( $wpdb->posts ) ) {
		return array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true );
	}
	$updated = $wpdb->query(
		$wpdb->prepare(
			"UPDATE {$wpdb->posts} SET post_content = %s, post_modified = %s, post_modified_gmt = %s WHERE ID = %d AND HEX(post_content) = HEX(%s)",
			$content,
			current_time( 'mysql' ),
			current_time( 'mysql', true ),
			$post_id,
			$candidate_content
		)
	);
	if ( 1 !== $updated ) {
		return array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true );
	}
	clean_post_cache( $post_id );
	$fresh_content = wpagent_native_block_raw_content( $post_id );
	if ( is_wp_error( $fresh_content ) || $fresh_content !== $content ) {
		return array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true );
	}
	return array( 'rolled_back' => true, 'rollback_verified' => true, 'recovery_required' => false );
}

/** @param array{rolled_back:bool,rollback_verified:bool,recovery_required:bool} $rollback @return WP_Error */
function wpagent_native_block_write_failure( string $code, string $message, int $revision_id, array $rollback ) {
	$data = array(
		'status'            => 500,
		'rolled_back'       => $rollback['rolled_back'],
		'rollback_verified' => $rollback['rollback_verified'],
		'recovery_required' => $rollback['recovery_required'],
		'revision_id'       => $revision_id,
	);
	if ( $rollback['recovery_required'] ) {
		return new WP_Error(
			'wpagent_native_block_recovery_required',
			__( 'Die Blockänderung konnte nicht verifiziert werden. Der Beitrag wurde inzwischen fremd verändert und wird nicht automatisch überschrieben. Stelle ihn bei Bedarf über die angelegte Revision wieder her.', 'wp-agent-companion' ),
			$data
		);
	}
	return new WP_Error( $code, $message, $data );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_update_spectra_block_attributes( $input ) {
	$input    = is_array( $input ) ? $input : array();
	$post_id  = absint( $input['post_id'] ?? 0 );
	$path     = is_array( $input['path'] ?? null ) ? $input['path'] : array();
	$path_ok  = wpagent_native_block_validate_path( $path );
	if ( is_wp_error( $path_ok ) ) {
		return $path_ok;
	}
	$expected_hash = is_string( $input['expected_hash'] ?? null ) ? $input['expected_hash'] : '';
	if ( ! preg_match( '/^[a-f0-9]{64}$/', $expected_hash ) ) {
		return new WP_Error( 'wpagent_native_block_hash', __( 'expected_hash muss ein SHA-256-Hash sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$attributes = is_array( $input['attributes'] ?? null ) ? $input['attributes'] : array();
	$remove_attributes = is_array( $input['remove_attributes'] ?? null ) ? array_values( $input['remove_attributes'] ) : array();
	if ( ! wp_is_numeric_array( $remove_attributes ) || array_filter( $remove_attributes, static fn ( $attribute ): bool => ! is_string( $attribute ) ) ) {
		return new WP_Error( 'wpagent_native_block_remove_attributes', __( 'remove_attributes darf nur eine Liste von Attributnamen enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array() === $attributes && array() === $remove_attributes ) {
		return new WP_Error( 'wpagent_native_block_no_attributes', __( 'Mindestens ein Attribut muss gesetzt oder entfernt werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( count( $attributes ) > WPAGENT_NATIVE_BLOCK_MAX_PATCH_ATTRIBUTES || count( $remove_attributes ) > WPAGENT_NATIVE_BLOCK_MAX_PATCH_ATTRIBUTES || count( array_unique( $remove_attributes ) ) !== count( $remove_attributes ) ) {
		return new WP_Error( 'wpagent_native_block_attribute_limit', __( 'Pro Änderung sind höchstens 50 eindeutige gesetzte und 50 eindeutige entfernte Attribute erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_intersect( array_keys( $attributes ), $remove_attributes ) ) {
		return new WP_Error( 'wpagent_native_block_attribute_overlap', __( 'Dasselbe Attribut kann nicht gleichzeitig gesetzt und entfernt werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$payload = wp_json_encode( array( 'attributes' => $attributes, 'remove_attributes' => $remove_attributes ) );
	if ( false === $payload || strlen( $payload ) > 65536 ) {
		return new WP_Error( 'wpagent_native_block_payload_limit', __( 'Der Attribut-Patch ist nicht JSON-kodierbar oder größer als 64 KB.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}

	$lock = wpagent_site_editor_acquire_lock( 'native-block', (string) $post_id );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$before = wpagent_native_block_document_state( $post_id );
		if ( is_wp_error( $before ) ) {
			return $before;
		}
		if ( ! hash_equals( $before['hash'], $expected_hash ) ) {
			return new WP_Error( 'wpagent_native_block_conflict', __( 'Das Blockdokument wurde seit dem Lesen verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $before['hash'] ) );
		}
		if ( strlen( $before['content'] ) > WPAGENT_SPECTRA_MAX_CONTENT_BYTES ) {
			return new WP_Error( 'wpagent_native_block_content_limit', __( 'Das Blockdokument ist größer als 1 MB und wird nicht automatisch geändert.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		$blocks = parse_blocks( $before['content'] );
		if ( serialize_blocks( $blocks ) !== $before['content'] ) {
			return new WP_Error( 'wpagent_native_block_noncanonical_document', __( 'Das bestehende Blockdokument ist nicht exakt parse- und serialisierbar und wird deshalb nicht automatisch verändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$target = wpagent_native_block_patch_path( $blocks, $path, $attributes, $remove_attributes );
		if ( is_wp_error( $target ) ) {
			return $target;
		}
		if ( ! $target['changed'] ) {
			try {
				$rendered = do_blocks( $before['content'] );
			} catch ( Throwable $error ) {
				return new WP_Error( 'wpagent_native_block_render_failed', $error->getMessage(), array( 'status' => 400 ) );
			}
			if ( ! is_string( $rendered ) ) {
				return new WP_Error( 'wpagent_native_block_render_failed', __( 'Das Blockdokument konnte nicht serverseitig gerendert werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return array(
				'post_id' => $post_id, 'block_name' => $target['block_name'], 'path' => $path,
				'written_attributes' => $attributes, 'removed_attributes' => $remove_attributes,
				'changed' => false, 'verified' => true, 'revision_id' => 0,
				'rendered_html_length' => strlen( $rendered ),
				'previous_hash' => $before['hash'], 'hash' => $before['hash'],
			);
		}
		$candidate = serialize_blocks( $blocks );
		if ( strlen( $candidate ) > WPAGENT_SPECTRA_MAX_CONTENT_BYTES || serialize_blocks( parse_blocks( $candidate ) ) !== $candidate ) {
			return new WP_Error( 'wpagent_native_block_serialization', __( 'Das geänderte Blockdokument ist nicht stabil serialisierbar oder überschreitet 1 MB.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$before_report = array( 'block_count' => 0, 'freeform_count' => 0, 'block_types' => array() );
		$after_report  = array( 'block_count' => 0, 'freeform_count' => 0, 'block_types' => array() );
		wpagent_native_block_report_walk( parse_blocks( $before['content'] ), $before_report );
		wpagent_native_block_report_walk( parse_blocks( $candidate ), $after_report );
		if ( $before_report !== $after_report ) {
			return new WP_Error( 'wpagent_native_block_structure_changed', __( 'Der Attribut-Patch würde die Blockstruktur verändern und wurde abgelehnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		try {
			$rendered = do_blocks( $candidate );
		} catch ( Throwable $error ) {
			return new WP_Error( 'wpagent_native_block_render_failed', $error->getMessage(), array( 'status' => 400 ) );
		}
		if ( ! is_string( $rendered ) ) {
			return new WP_Error( 'wpagent_native_block_render_failed', __( 'Das geänderte Blockdokument konnte nicht serverseitig gerendert werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$prewrite = wpagent_native_block_document_state( $post_id );
		if ( is_wp_error( $prewrite ) ) {
			return $prewrite;
		}
		if ( ! hash_equals( $before['hash'], $prewrite['hash'] ) ) {
			return new WP_Error( 'wpagent_native_block_conflict', __( 'Das Blockdokument wurde unmittelbar vor dem Speichern verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $prewrite['hash'] ) );
		}
		$revision_id = wpagent_native_block_create_revision( $post_id, $before['content'] );
		if ( is_wp_error( $revision_id ) ) {
			return $revision_id;
		}
		$saved = wpagent_native_block_write_if_current( $post_id, $before['content'], $candidate );
		if ( is_wp_error( $saved ) ) {
			$error_data = $saved->get_error_data();
			return new WP_Error(
				$saved->get_error_code(),
				$saved->get_error_message(),
				array_merge( is_array( $error_data ) ? $error_data : array(), array( 'revision_id' => $revision_id ) )
			);
		}
		$after = wpagent_native_block_document_state( $post_id );
		if ( is_wp_error( $after ) ) {
			return wpagent_native_block_write_failure(
				'wpagent_native_block_verify_interrupted',
				__( 'Die Blockänderung kann nach dem Speichern nicht mehr unter der aktuellen Berechtigung verifiziert werden.', 'wp-agent-companion' ),
				$revision_id,
				array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true )
			);
		}
		$fresh = get_post( $post_id );
		if ( ! $fresh instanceof WP_Post || $fresh->post_content !== $candidate ) {
			$rollback = wpagent_native_block_rollback( $post_id, $before['content'], $candidate );
			return wpagent_native_block_write_failure( 'wpagent_native_block_verify_failed', __( 'Die Blockänderung bestand die exakte Rückleseprüfung nicht.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		$verified_blocks = parse_blocks( $fresh->post_content );
		$verified_target = wpagent_native_block_patch_path( $verified_blocks, $path, $attributes, $remove_attributes );
		$verified = ! is_wp_error( $verified_target ) && ! $verified_target['changed'];
		if ( ! $verified ) {
			$rollback = wpagent_native_block_rollback( $post_id, $before['content'], $candidate );
			return wpagent_native_block_write_failure( 'wpagent_native_block_verify_failed', __( 'Die Blockattribute bestanden die Parse- und Rückleseprüfung nicht.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		$cache = wpagent_refresh_builder_cache_spectra( $post_id );
		if ( empty( $cache['executed'] ) || empty( $cache['verified'] ) ) {
			$rollback = wpagent_native_block_rollback( $post_id, $before['content'], $candidate );
			if ( $rollback['rolled_back'] ) {
				wpagent_refresh_builder_cache_spectra( $post_id );
			}
			return wpagent_native_block_write_failure( 'wpagent_native_block_cache_failed', __( 'Spectras Beitrags- und Asset-Cache konnte nicht verifiziert aktualisiert werden.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		return array(
			'post_id' => $post_id, 'block_name' => $target['block_name'], 'path' => $path,
			'written_attributes' => $attributes, 'removed_attributes' => $remove_attributes,
			'changed' => true, 'verified' => true, 'revision_id' => $revision_id,
			'rendered_html_length' => strlen( $rendered ), 'previous_hash' => $before['hash'], 'hash' => $after['hash'],
		);
	} finally {
		wpagent_site_editor_release_lock( 'native-block', (string) $post_id, $lock );
	}
}
