<?php
/** Shared safety, validation and transaction helpers for Beaver Builder. */

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

define( 'WPAGENT_BEAVER_MAX_NODES', 500 );
define( 'WPAGENT_BEAVER_MAX_ROWS', 50 );
define( 'WPAGENT_BEAVER_MAX_MODULES', 200 );
define( 'WPAGENT_BEAVER_MAX_FIELDS', 300 );
define( 'WPAGENT_BEAVER_MAX_VALUE_DEPTH', 12 );
define( 'WPAGENT_BEAVER_MAX_STRING_BYTES', 100000 );
define( 'WPAGENT_BEAVER_LOCK_TTL', 120 );

function wpagent_beaver_available(): bool {
	return class_exists( 'FLBuilderModel' ) && class_exists( 'FLBuilder' );
}

/** @return string[] */
function wpagent_beaver_unsafe_module_types(): array {
	return array( 'html', 'reusable-block', 'sidebar', 'widget' );
}

function wpagent_beaver_unavailable_error(): WP_Error {
	return new WP_Error(
		'wpagent_beaver_unavailable',
		__( 'Beaver Builder ist nicht aktiv oder seine öffentliche Modell-API ist nicht verfügbar.', 'wp-agent-companion' ),
		array( 'status' => 409 )
	);
}

/** @return object|WP_Error */
function wpagent_beaver_editable_post( int $post_id, bool $require_layout = true ) {
	if ( ! wpagent_beaver_available() ) {
		return wpagent_beaver_unavailable_error();
	}
	// Check the input-bound meta-cap before loading the post. A caller without
	// access must not distinguish a missing ID from a foreign Beaver document.
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_beaver_forbidden', __( 'Keine Berechtigung zum Bearbeiten dieses Beitrags.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_Error( 'wpagent_beaver_forbidden', __( 'Keine Berechtigung zum Bearbeiten dieses Beitrags.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	if ( ! in_array( $post->post_type, FLBuilderModel::get_post_types(), true ) ) {
		return new WP_Error( 'wpagent_beaver_post_type', __( 'Beaver Builder ist für diesen Beitragstyp nicht aktiviert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( $require_layout && ! FLBuilderModel::is_builder_enabled( $post_id ) ) {
		return new WP_Error( 'wpagent_beaver_not_enabled', __( 'Der Beitrag verwendet kein aktives Beaver-Builder-Layout.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $post;
}

/** @return object|WP_Error */
function wpagent_beaver_module( string $module_type ) {
	$module_type = sanitize_key( $module_type );
	if ( '' === $module_type || in_array( $module_type, wpagent_beaver_unsafe_module_types(), true ) ) {
		return new WP_Error( 'wpagent_beaver_unsafe_module', __( 'Dieser Beaver-Builder-Modultyp ist im sicheren Schreibpfad gesperrt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! isset( FLBuilderModel::$modules[ $module_type ] ) || ! is_object( FLBuilderModel::$modules[ $module_type ] ) ) {
		return new WP_Error( 'wpagent_beaver_module_not_found', __( 'Beaver-Builder-Modultyp nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return FLBuilderModel::$modules[ $module_type ];
}

/** @return mixed */
function wpagent_beaver_canonicalize( $value, int $depth = 0 ) {
	if ( $depth > WPAGENT_BEAVER_MAX_VALUE_DEPTH ) {
		// Preserve conflict sensitivity without returning an unbounded public
		// structure. serialize() also handles recursive PHP references.
		return '[depth-hash:' . hash( 'sha256', serialize( $value ) ) . ']';
	}
	if ( is_object( $value ) ) {
		$value = get_object_vars( $value );
	}
	if ( ! is_array( $value ) ) {
		return is_resource( $value ) ? '[resource]' : $value;
	}
	$is_list = array_is_list( $value );
	if ( ! $is_list ) {
		ksort( $value, SORT_STRING );
	}
	$result = array();
	foreach ( $value as $key => $item ) {
		$result[ $key ] = wpagent_beaver_canonicalize( $item, $depth + 1 );
	}
	return $result;
}

/** @param array<string,object> $data */
function wpagent_beaver_state_hash( array $data, $settings ): string {
	$canonical = array(
		'blog_id'  => function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 0,
		'data'     => wpagent_beaver_canonicalize( $data ),
		'settings' => wpagent_beaver_canonicalize( $settings ),
	);
	$payload = wp_json_encode(
		$canonical,
		JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
	);
	if ( ! is_string( $payload ) ) {
		$payload = 'serialized:' . serialize( $canonical );
	}
	return hash( 'sha256', $payload );
}

/**
 * Beaver's public document hash must include every persisted value that its
 * mutators can change. The published layout alone misses draft data, both
 * settings records, the enabled flag and the rendered post content.
 *
 * @return array{post_content:string,meta:array<int,array{meta_id:int,meta_key:string,meta_value:string}>,fingerprint:array<string,mixed>}|WP_Error
 */
function wpagent_beaver_persisted_mutation_state( int $post_id ) {
	global $wpdb;
	$post_content = $wpdb->get_var( $wpdb->prepare( "SELECT post_content FROM {$wpdb->posts} WHERE ID = %d", $post_id ) );
	if ( null === $post_content || '' !== (string) $wpdb->last_error ) {
		return new WP_Error( 'wpagent_beaver_state_unreadable', __( 'Der persistierte Beaver-Builder-Zustand konnte nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$keys         = array( '_fl_builder_data', '_fl_builder_draft', '_fl_builder_data_settings', '_fl_builder_draft_settings', '_fl_builder_enabled' );
	$placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) );
	$rows         = $wpdb->get_results(
		$wpdb->prepare( "SELECT meta_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key IN ({$placeholders}) ORDER BY meta_key ASC, meta_id ASC", $post_id, ...$keys ),
		ARRAY_A
	);
	if ( ! is_array( $rows ) || '' !== (string) $wpdb->last_error ) {
		return new WP_Error( 'wpagent_beaver_state_unreadable', __( 'Der persistierte Beaver-Builder-Zustand konnte nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$meta        = array();
	$fingerprint = array();
	foreach ( $rows as $row ) {
		if ( ! is_array( $row ) || ! isset( $row['meta_id'], $row['meta_key'], $row['meta_value'] ) || ! in_array( (string) $row['meta_key'], $keys, true ) ) {
			return new WP_Error( 'wpagent_beaver_state_unreadable', __( 'Der persistierte Beaver-Builder-Zustand konnte nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		$meta[]        = array( 'meta_id' => (int) $row['meta_id'], 'meta_key' => (string) $row['meta_key'], 'meta_value' => (string) $row['meta_value'] );
		$fingerprint[] = array( 'meta_key' => (string) $row['meta_key'], 'meta_value' => (string) $row['meta_value'] );
	}
	return array( 'post_content' => (string) $post_content, 'meta' => $meta, 'fingerprint' => array( 'post_content' => (string) $post_content, 'meta' => $fingerprint ) );
}

/** @param array<string,mixed> $persisted */
function wpagent_beaver_full_state_hash( array $data, $settings, array $persisted ): string {
	$canonical = array(
		'blog_id'  => function_exists( 'get_current_blog_id' ) ? (int) get_current_blog_id() : 0,
		'data'     => wpagent_beaver_canonicalize( $data ),
		'settings' => wpagent_beaver_canonicalize( $settings ),
		'persisted' => wpagent_beaver_canonicalize( $persisted['fingerprint'] ?? array() ),
	);
	$payload = wp_json_encode( $canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	return hash( 'sha256', is_string( $payload ) ? $payload : 'serialized:' . serialize( $canonical ) );
}

/**
 * Creates an isolated rollback snapshot of trusted provider state.
 *
 * Beaver Builder caches layout objects in-process. A normal array assignment
 * would retain references to those objects and let the mutation alter the
 * supposed original state as well.
 *
 * @return mixed
 */
function wpagent_beaver_snapshot( $value ) {
	return unserialize( serialize( $value ), array( 'allowed_classes' => true ) );
}

/** @return mixed|WP_Error */
function wpagent_beaver_bounded_public_value( $value, int $depth = 0 ) {
	if ( $depth > WPAGENT_BEAVER_MAX_VALUE_DEPTH ) {
		return new WP_Error( 'wpagent_beaver_value_depth', __( 'Ein Beaver-Builder-Einstellungswert ist zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( is_object( $value ) ) {
		$value = get_object_vars( $value );
	}
	if ( is_array( $value ) ) {
		if ( count( $value ) > WPAGENT_BEAVER_MAX_FIELDS ) {
			return new WP_Error( 'wpagent_beaver_value_size', __( 'Ein Beaver-Builder-Einstellungswert enthält zu viele Einträge.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		$result = array();
		foreach ( $value as $key => $item ) {
			$bounded = wpagent_beaver_bounded_public_value( $item, $depth + 1 );
			if ( is_wp_error( $bounded ) ) {
				return $bounded;
			}
			$result[ $key ] = $bounded;
		}
		return $result;
	}
	if ( is_string( $value ) ) {
		if ( strlen( $value ) > WPAGENT_BEAVER_MAX_STRING_BYTES ) {
			return new WP_Error( 'wpagent_beaver_value_size', __( 'Ein Beaver-Builder-Einstellungswert ist zu groß.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		return $value;
	}
	if ( is_null( $value ) || is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
		return $value;
	}
	return new WP_Error( 'wpagent_beaver_value_type', __( 'Ein Beaver-Builder-Einstellungswert besitzt einen nicht unterstützten Typ.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_beaver_public_settings( object $node ) {
	$fields = wpagent_beaver_fields_for_node( $node );
	if ( is_wp_error( $fields ) ) {
		return $fields;
	}
	$settings = array();
	foreach ( get_object_vars( is_object( $node->settings ?? null ) ? $node->settings : new stdClass() ) as $name => $value ) {
		if ( ! is_string( $name ) || ! isset( $fields[ $name ] ) || ! wpagent_beaver_field_is_writable( $name, $fields[ $name ] ) ) {
			continue;
		}
		$bounded = wpagent_beaver_bounded_public_value( $value );
		if ( is_wp_error( $bounded ) ) {
			return $bounded;
		}
		$settings[ $name ] = wpagent_beaver_canonicalize( $bounded );
	}
	return $settings;
}

/** @return array<int,array<string,mixed>>|WP_Error */
function wpagent_beaver_public_nodes( array $data ) {
	if ( count( $data ) > WPAGENT_BEAVER_MAX_NODES ) {
		return new WP_Error( 'wpagent_beaver_node_limit', __( 'Das Beaver-Builder-Layout überschreitet die sichere Knotengrenze.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$nodes = array();
	foreach ( $data as $node ) {
		if ( ! is_object( $node ) || ! isset( $node->node, $node->type ) ) {
			continue;
		}
		$module_type = 'module' === $node->type && isset( $node->settings->type ) ? sanitize_key( (string) $node->settings->type ) : '';
		$public_settings   = wpagent_beaver_public_settings( $node );
		$writable          = ! is_wp_error( $public_settings );
		$settings_document = wpagent_beaver_settings_document_output( $writable ? $public_settings : array() );
		if ( is_wp_error( $settings_document ) ) {
			return $settings_document;
		}
		$nodes[] = array(
			'id'          => sanitize_key( (string) $node->node ),
			'type'        => sanitize_key( (string) $node->type ),
			'module_type' => $module_type,
			'parent'      => isset( $node->parent ) ? sanitize_key( (string) $node->parent ) : '',
			'position'    => isset( $node->position ) ? max( 0, (int) $node->position ) : 0,
			'writable'    => $writable,
			'settings'    => $settings_document,
		);
	}
	usort(
		$nodes,
		static function ( array $left, array $right ): int {
			$parent = strcmp( $left['parent'], $right['parent'] );
			return 0 !== $parent ? $parent : ( $left['position'] <=> $right['position'] ?: strcmp( $left['id'], $right['id'] ) );
		}
	);
	return $nodes;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_beaver_document_state( int $post_id ) {
	$post = wpagent_beaver_editable_post( $post_id );
	if ( is_wp_error( $post ) ) {
		return $post;
	}
	$data     = FLBuilderModel::get_layout_data( 'published', $post_id );
	$settings = FLBuilderModel::get_layout_settings( 'published', $post_id );
	$nodes    = wpagent_beaver_public_nodes( $data );
	if ( is_wp_error( $nodes ) ) {
		return $nodes;
	}
	$persisted = wpagent_beaver_persisted_mutation_state( $post_id );
	if ( is_wp_error( $persisted ) ) {
		return $persisted;
	}
	return array(
		'post_id'         => $post_id,
		'title'           => get_the_title( $post_id ),
		'status'          => (string) get_post_status( $post_id ),
		'version'         => defined( 'FL_BUILDER_VERSION' ) ? (string) FL_BUILDER_VERSION : '',
		'builder_enabled' => (bool) FLBuilderModel::is_builder_enabled( $post_id ),
		'hash'            => wpagent_beaver_full_state_hash( $data, $settings, $persisted ),
		'node_count'      => count( $nodes ),
		'nodes'           => $nodes,
	);
}

/** @return array<string,array<string,mixed>>|WP_Error */
function wpagent_beaver_fields_for_node( object $node ) {
	static $cache = array();
	if ( 'module' === ( $node->type ?? '' ) ) {
		$module_type = isset( $node->settings->type ) ? sanitize_key( (string) $node->settings->type ) : '';
		$module      = wpagent_beaver_module( $module_type );
		if ( is_wp_error( $module ) ) {
			return $module;
		}
		$cache_key = 'module:' . $module_type;
		if ( isset( $cache[ $cache_key ] ) ) {
			return $cache[ $cache_key ];
		}
		$fields = FLBuilderModel::get_settings_form_fields( $module->form );
	} elseif ( 'row' === ( $node->type ?? '' ) ) {
		$cache_key = 'row';
		if ( isset( $cache[ $cache_key ] ) ) {
			return $cache[ $cache_key ];
		}
		$fields = FLBuilderModel::get_settings_form_fields( 'row', 'general' );
	} elseif ( 'column' === ( $node->type ?? '' ) ) {
		$cache_key = 'column';
		if ( isset( $cache[ $cache_key ] ) ) {
			return $cache[ $cache_key ];
		}
		$fields = FLBuilderModel::get_settings_form_fields( 'col', 'general' );
	} else {
		return new WP_Error( 'wpagent_beaver_node_not_writable', __( 'Dieser Beaver-Builder-Knotentyp besitzt keine sicheren Einstellungen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_array( $fields ) || count( $fields ) > WPAGENT_BEAVER_MAX_FIELDS ) {
		return new WP_Error( 'wpagent_beaver_schema_limit', __( 'Das Beaver-Builder-Einstellungsschema ist nicht verfügbar oder zu groß.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$cache[ $cache_key ] = $fields;
	return $cache[ $cache_key ];
}

/** @param array<string,mixed> $field */
function wpagent_beaver_field_is_writable( string $name, array $field ): bool {
	$type = sanitize_key( is_string( $field['type'] ?? null ) ? $field['type'] : '' );
	if ( in_array( $type, array( 'code', 'form', 'html', 'password', 'raw', 'secret', 'service', 'sidebar', 'widget' ), true ) ) {
		return false;
	}
	return ! preg_match( '/(?:^|_)(?:api(?:_?key)?|authorization|bb_js_code|client_secret|credential|custom_css|custom_js|html|javascript|password|private_key|secret|shortcode|token|webhook)(?:_|$)/i', $name );
}

/** @return true|WP_Error */
function wpagent_beaver_validate_value( $value, int $depth = 0 ) {
	if ( $depth > WPAGENT_BEAVER_MAX_VALUE_DEPTH ) {
		return new WP_Error( 'wpagent_beaver_value_depth', __( 'Ein Beaver-Builder-Einstellungswert ist zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( is_string( $value ) ) {
		if ( strlen( $value ) > WPAGENT_BEAVER_MAX_STRING_BYTES ) {
			return new WP_Error( 'wpagent_beaver_value_size', __( 'Ein Beaver-Builder-Einstellungswert ist zu groß.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		$unslashed = wp_unslash( $value );
		if ( wp_kses_post( $unslashed ) !== $unslashed
			|| preg_match( '/(?:javascript\s*:|data\s*:\s*text\/html|on[a-z]+\s*=)/i', $unslashed )
			|| preg_match( '/\[[A-Za-z][A-Za-z0-9_-]*(?:\s|\]|\/)/', $unslashed )
		) {
			return new WP_Error( 'wpagent_beaver_unsafe_markup', __( 'Ausführbares Markup oder Shortcode-Inhalt ist im sicheren Beaver-Builder-Pfad nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		return true;
	}
	if ( is_array( $value ) ) {
		if ( count( $value ) > WPAGENT_BEAVER_MAX_FIELDS ) {
			return new WP_Error( 'wpagent_beaver_value_size', __( 'Ein Beaver-Builder-Einstellungswert enthält zu viele Einträge.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		foreach ( $value as $item ) {
			$valid = wpagent_beaver_validate_value( $item, $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		return true;
	}
	if ( is_null( $value ) || is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
		return true;
	}
	return new WP_Error( 'wpagent_beaver_value_type', __( 'Ein Beaver-Builder-Einstellungswert besitzt einen nicht unterstützten Typ.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return array<string,mixed> */
function wpagent_beaver_settings_document_schema(): array {
	return wpagent_builder_json_document_schema( WPAGENT_BEAVER_MAX_STRING_BYTES );
}

/** @return true|WP_Error */
function wpagent_beaver_validate_settings_document( $value ) {
	if ( ! is_array( $value ) || ( $value && array_is_list( $value ) ) || count( $value ) > WPAGENT_BEAVER_MAX_FIELDS ) {
		return new WP_Error( 'wpagent_beaver_settings_shape', __( 'Beaver-Builder-Einstellungen müssen ein begrenztes Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_beaver_validate_value( $value );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_beaver_settings_document_input( $value ) {
	if ( wpagent_builder_json_document_is_envelope( $value ) ) {
		return wpagent_builder_json_document_to_value( $value, WPAGENT_BEAVER_MAX_STRING_BYTES, 'wpagent_beaver_validate_settings_document', 'wpagent_beaver' );
	}
	$valid = wpagent_beaver_validate_settings_document( $value );
	return is_wp_error( $valid ) ? $valid : $value;
}

/** @return array{json:string,sha256:string,bytes:int}|WP_Error */
function wpagent_beaver_settings_document_output( $value ) {
	return wpagent_builder_json_document_from_value( $value, WPAGENT_BEAVER_MAX_STRING_BYTES, 'wpagent_beaver_validate_settings_document', 'wpagent_beaver' );
}

/** @return array<int,array<string,mixed>>|WP_Error */
function wpagent_beaver_decode_page_settings( array $rows ) {
	foreach ( $rows as $row_index => $row ) {
		if ( ! is_array( $row ) ) {
			return new WP_Error( 'wpagent_beaver_bad_row', __( 'Ungültige Beaver-Builder-Zeile.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( array_key_exists( 'settings', $row ) ) {
			$settings = wpagent_beaver_settings_document_input( $row['settings'] );
			if ( is_wp_error( $settings ) ) {
				return $settings;
			}
			$row['settings'] = $settings;
		}
		$columns = is_array( $row['columns'] ?? null ) ? $row['columns'] : array();
		foreach ( $columns as $column_index => $column ) {
			if ( ! is_array( $column ) ) {
				return new WP_Error( 'wpagent_beaver_bad_column', __( 'Ungültige Beaver-Builder-Spalte.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			if ( array_key_exists( 'settings', $column ) ) {
				$settings = wpagent_beaver_settings_document_input( $column['settings'] );
				if ( is_wp_error( $settings ) ) {
					return $settings;
				}
				$column['settings'] = $settings;
			}
			$modules = is_array( $column['modules'] ?? null ) ? $column['modules'] : array();
			foreach ( $modules as $module_index => $module ) {
				if ( ! is_array( $module ) ) {
					return new WP_Error( 'wpagent_beaver_bad_module', __( 'Ungültiges Beaver-Builder-Modul.', 'wp-agent-companion' ), array( 'status' => 400 ) );
				}
				if ( array_key_exists( 'settings', $module ) ) {
					$settings = wpagent_beaver_settings_document_input( $module['settings'] );
					if ( is_wp_error( $settings ) ) {
						return $settings;
					}
					$module['settings'] = $settings;
				}
				$modules[ $module_index ] = $module;
			}
			$column['modules'] = $modules;
			$columns[ $column_index ] = $column;
		}
		$row['columns'] = $columns;
		$rows[ $row_index ] = $row;
	}
	return $rows;
}

/**
 * @param array<string,mixed> $patch
 * @param string[] $remove
 * @return object|WP_Error
 */
function wpagent_beaver_prepare_node_settings( object $node, array $patch, array $remove = array() ) {
	if ( count( $patch ) > WPAGENT_BEAVER_MAX_FIELDS || count( $remove ) > WPAGENT_BEAVER_MAX_FIELDS ) {
		return new WP_Error( 'wpagent_beaver_value_size', __( 'Die Beaver-Builder-Änderung enthält zu viele Einstellungsfelder.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$fields = wpagent_beaver_fields_for_node( $node );
	if ( is_wp_error( $fields ) ) {
		return $fields;
	}
	$names = array_unique( array_merge( array_keys( $patch ), $remove ) );
	foreach ( $names as $name ) {
		if ( ! is_string( $name ) || ! isset( $fields[ $name ] ) || ! wpagent_beaver_field_is_writable( $name, $fields[ $name ] ) ) {
			return new WP_Error( 'wpagent_beaver_unknown_setting', __( 'Unbekannte oder gesperrte Beaver-Builder-Einstellung.', 'wp-agent-companion' ), array( 'status' => 400, 'setting' => is_scalar( $name ) ? (string) $name : '' ) );
		}
	}
	foreach ( $patch as $value ) {
		$valid = wpagent_beaver_validate_value( $value );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	$verified = FLBuilderModel::verify_settings( (object) $patch );
	if ( true !== $verified ) {
		return new WP_Error( 'wpagent_beaver_unsafe_markup', __( 'Beaver Builder hat den Einstellungswert als unsicher abgelehnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$settings = (object) array_merge( (array) ( $node->settings ?? new stdClass() ), $patch );
	foreach ( $remove as $name ) {
		unset( $settings->{$name} );
	}
	try {
		return FLBuilderModel::process_node_settings( $node, $settings );
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_beaver_provider_validation', __( 'Beaver Builder konnte die Einstellungen nicht verarbeiten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
}

/** @return string|WP_Error */
function wpagent_beaver_acquire_lock( int $post_id ) {
	$key   = 'wpagent_beaver_lock_' . $post_id;
	$token = wpagent_companion_acquire_option_lock( $key, WPAGENT_BEAVER_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_beaver_locked', __( 'Das Beaver-Builder-Layout wird gerade geändert. Bitte erneut versuchen.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

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

/** @return object|WP_Error */
function wpagent_beaver_mutation_guard( int $post_id, string $token ) {
	$post = wpagent_beaver_editable_post( $post_id );
	if ( is_wp_error( $post ) ) {
		return $post;
	}
	$key = 'wpagent_beaver_lock_' . $post_id;
	if ( ! wpagent_companion_refresh_option_lock( $key, $token, WPAGENT_BEAVER_LOCK_TTL ) || ! wpagent_companion_option_lock_is_owned( $key, $token ) ) {
		return new WP_Error( 'wpagent_beaver_lock_lost', __( 'Die Beaver-Builder-Sperre ging während der Änderung verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $post;
}

/** @return WP_Error */
function wpagent_beaver_recovery_required( string $reason, int $revision_id = 0, $original_error = null ): WP_Error {
	$data = array( 'status' => 500, 'recovery_required' => true, 'rollback_verified' => false, 'reason' => $reason, 'recovery_revision_id' => $revision_id );
	if ( $original_error instanceof WP_Error ) {
		$data['original_error'] = $original_error->get_error_code();
	}
	return new WP_Error( 'wpagent_beaver_recovery_required', __( 'Die Beaver-Builder-Änderung konnte nicht vollständig verifiziert werden. Der aktuelle Zustand bleibt erhalten und muss anhand der Recovery-Revision manuell geprüft werden.', 'wp-agent-companion' ), $data );
}

function wpagent_beaver_create_revision( int $post_id ) {
	if ( ! function_exists( '_wp_put_post_revision' ) ) {
		require_once ABSPATH . WPINC . '/revision.php';
	}
	$revision_id = _wp_put_post_revision( $post_id );
	if ( is_wp_error( $revision_id ) || (int) $revision_id <= 0 ) {
		return new WP_Error( 'wpagent_beaver_revision_failed', __( 'Vor der Änderung konnte keine Beaver-Builder-Revision angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$revision_data = FLBuilderModel::get_layout_data( 'published', (int) $revision_id );
	if ( empty( $revision_data ) ) {
		wp_delete_post_revision( (int) $revision_id );
		return new WP_Error( 'wpagent_beaver_revision_failed', __( 'Die Beaver-Builder-Revision enthält keine wiederherstellbaren Layoutdaten.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return (int) $revision_id;
}

/**
 * Rejects indirect mutation of global, linked or executable descendant nodes.
 *
 * @param array<string,object> $data
 * @param string[]             $root_ids
 * @return true|WP_Error
 */
function wpagent_beaver_validate_safe_subtrees( array $data, array $root_ids, string $error_code = 'wpagent_beaver_unsafe_subtree' ) {
	if ( count( $data ) > WPAGENT_BEAVER_MAX_NODES ) {
		return new WP_Error( 'wpagent_beaver_node_limit', __( 'Das Beaver-Builder-Layout überschreitet die sichere Knotengrenze.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$children = array();
	foreach ( $data as $node_id => $node ) {
		if ( ! is_object( $node ) ) {
			continue;
		}
		$parent = sanitize_key( (string) ( $node->parent ?? '' ) );
		$children[ $parent ][] = sanitize_key( (string) $node_id );
	}
	$queue   = array_values( array_unique( array_map( 'sanitize_key', $root_ids ) ) );
	$visited = array();
	while ( array() !== $queue ) {
		$node_id = array_pop( $queue );
		if ( '' === $node_id || isset( $visited[ $node_id ] ) ) {
			continue;
		}
		if ( ! isset( $data[ $node_id ] ) || ! is_object( $data[ $node_id ] ) ) {
			return new WP_Error( 'wpagent_beaver_node_not_found', __( 'Beaver-Builder-Knoten nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		$visited[ $node_id ] = true;
		$node                = $data[ $node_id ];
		if ( ! empty( $node->global ) || ! empty( $node->template_id ) ) {
			return new WP_Error( $error_code, __( 'Globale, verknüpfte oder unsichere Beaver-Builder-Knoten dürfen nicht indirekt verändert werden.', 'wp-agent-companion' ), array( 'status' => 400, 'node_id' => $node_id ) );
		}
		$type = sanitize_key( (string) ( $node->type ?? '' ) );
		if ( ! in_array( $type, array( 'row', 'column-group', 'column', 'module' ), true ) ) {
			return new WP_Error( $error_code, __( 'Globale, verknüpfte oder unsichere Beaver-Builder-Knoten dürfen nicht indirekt verändert werden.', 'wp-agent-companion' ), array( 'status' => 400, 'node_id' => $node_id ) );
		}
		if ( 'module' === $type ) {
			$module_type = sanitize_key( (string) ( $node->settings->type ?? '' ) );
			if ( is_wp_error( wpagent_beaver_module( $module_type ) ) ) {
				return new WP_Error( $error_code, __( 'Globale, verknüpfte oder unsichere Beaver-Builder-Knoten dürfen nicht indirekt verändert werden.', 'wp-agent-companion' ), array( 'status' => 400, 'node_id' => $node_id ) );
			}
		}
		foreach ( $children[ $node_id ] ?? array() as $child_id ) {
			$queue[] = $child_id;
		}
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_beaver_render_frontend( int $post_id ) {
	if ( ! wpagent_beaver_available() ) {
		return wpagent_beaver_unavailable_error();
	}
	$buffer_level = ob_get_level();
	try {
		ob_start();
		FLBuilder::render_content_by_id( $post_id );
		$content = ob_get_clean();
		return is_string( $content ) ? $content : '';
	} catch ( Throwable $error ) {
		while ( ob_get_level() > $buffer_level ) {
			ob_end_clean();
		}
		return new WP_Error( 'wpagent_beaver_render_failed', __( 'Beaver Builder konnte das Frontend nicht rendern.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
}

/** @return true|WP_Error */
function wpagent_beaver_finalize_layout( int $post_id ) {
	$data     = FLBuilderModel::get_layout_data( 'published', $post_id );
	$settings = FLBuilderModel::get_layout_settings( 'published', $post_id );
	FLBuilderModel::update_layout_data( $data, 'draft', $post_id );
	FLBuilderModel::update_layout_settings( $settings, 'draft', $post_id );
	FLBuilderModel::set_post_id( $post_id );
	try {
		FLBuilderModel::enable();
		FLBuilderModel::delete_all_asset_cache( $post_id );
		$content = FLBuilder::render_editor_content();
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_beaver_finalize_failed', __( 'Beaver Builder konnte das Layout nicht abschließen.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	} finally {
		FLBuilderModel::reset_post_id();
	}
	if ( ! is_string( $content ) ) {
		return new WP_Error( 'wpagent_beaver_finalize_failed', __( 'Beaver Builder lieferte keinen Editor-Fallback.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$post = get_post( $post_id );
	if ( $post && $post->post_content !== $content ) {
		$updated = wp_update_post( array( 'ID' => $post_id, 'post_content' => $content ), true );
		if ( is_wp_error( $updated ) ) {
			return $updated;
		}
	}
	$frontend = wpagent_beaver_render_frontend( $post_id );
	if ( is_wp_error( $frontend ) || '' === trim( wp_strip_all_tags( $frontend ) ) ) {
		return new WP_Error( 'wpagent_beaver_verify_failed', __( 'Das Beaver-Builder-Frontend blieb nach der Änderung leer.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return true;
}

/**
 * @param callable():array<string,mixed>|WP_Error $callback
 * @return array<string,mixed>|WP_Error
 */
function wpagent_beaver_mutate( int $post_id, string $expected_hash, callable $callback ) {
	$post = wpagent_beaver_editable_post( $post_id );
	if ( is_wp_error( $post ) ) {
		return $post;
	}
	if ( ! preg_match( '/^[a-f0-9]{64}$/', $expected_hash ) ) {
		return new WP_Error( 'wpagent_beaver_bad_hash', __( 'Eine gültige erwartete SHA-256-Prüfsumme ist erforderlich.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$lock = wpagent_beaver_acquire_lock( $post_id );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$original_data     = wpagent_beaver_snapshot( FLBuilderModel::get_layout_data( 'published', $post_id ) );
		$original_settings = wpagent_beaver_snapshot( FLBuilderModel::get_layout_settings( 'published', $post_id ) );
	} catch ( Throwable $snapshot_error ) {
		wpagent_beaver_release_lock( $post_id, $lock );
		return new WP_Error( 'wpagent_beaver_snapshot_failed', __( 'Vor der Änderung konnte kein isolierter Beaver-Builder-Rollback-Snapshot erstellt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( ! is_array( $original_data ) ) {
		wpagent_beaver_release_lock( $post_id, $lock );
		return new WP_Error( 'wpagent_beaver_snapshot_failed', __( 'Vor der Änderung konnte kein isolierter Beaver-Builder-Rollback-Snapshot erstellt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$original_persisted = wpagent_beaver_persisted_mutation_state( $post_id );
	if ( is_wp_error( $original_persisted ) ) {
		wpagent_beaver_release_lock( $post_id, $lock );
		return $original_persisted;
	}
	$revision_id       = 0;
	$failure           = null;
	try {
		$current_hash = wpagent_beaver_full_state_hash( $original_data, $original_settings, $original_persisted );
		if ( ! hash_equals( $current_hash, $expected_hash ) ) {
			return new WP_Error( 'wpagent_beaver_conflict', __( 'Das Beaver-Builder-Layout wurde seit dem Lesen verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $current_hash ) );
		}
		$guard = wpagent_beaver_mutation_guard( $post_id, $lock );
		if ( is_wp_error( $guard ) ) {
			return $guard;
		}
		$revision_id = wpagent_beaver_create_revision( $post_id );
		if ( is_wp_error( $revision_id ) ) {
			return $revision_id;
		}
		$guard = wpagent_beaver_mutation_guard( $post_id, $lock );
		if ( is_wp_error( $guard ) ) {
			return $guard;
		}
		FLBuilderModel::set_post_id( $post_id );
		try {
			$result = $callback();
		} finally {
			FLBuilderModel::reset_post_id();
		}
		if ( is_wp_error( $result ) ) {
			$failure = $result;
			throw new RuntimeException( 'callback' );
		}
		if ( ! is_array( $result ) ) {
			$failure = new WP_Error( 'wpagent_beaver_mutation_failed', __( 'Beaver Builder hat kein gültiges Änderungsergebnis geliefert.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			throw new RuntimeException( 'callback-result' );
		}
		$verification = $result['_verification'] ?? array();
		unset( $result['_verification'] );
		if ( ! is_array( $verification ) ) {
			$failure = new WP_Error( 'wpagent_beaver_verify_failed', __( 'Die Beaver-Builder-Änderung entspricht nicht dem angeforderten Zielzustand.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			throw new RuntimeException( 'verify-contract' );
		}
		$guard = wpagent_beaver_mutation_guard( $post_id, $lock );
		if ( is_wp_error( $guard ) ) {
			$failure = $guard;
			throw new RuntimeException( 'guard-before-finalize' );
		}
		$finalized = wpagent_beaver_finalize_layout( $post_id );
		if ( is_wp_error( $finalized ) ) {
			$failure = $finalized;
			throw new RuntimeException( 'finalize' );
		}
		$state = wpagent_beaver_document_state( $post_id );
		if ( is_wp_error( $state ) ) {
			$failure = $state;
			throw new RuntimeException( 'verify' );
		}
		$guard = wpagent_beaver_mutation_guard( $post_id, $lock );
		if ( is_wp_error( $guard ) ) {
			$failure = $guard;
			throw new RuntimeException( 'guard-after-readback' );
		}
		if ( hash_equals( $current_hash, (string) ( $state['hash'] ?? '' ) ) ) {
			$failure = new WP_Error( 'wpagent_beaver_verify_failed', __( 'Die Beaver-Builder-Änderung war im Read-back nicht sichtbar.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			throw new RuntimeException( 'verify' );
		}
		$target_id = sanitize_key( (string) ( $result['target_node_id'] ?? '' ) );
		if ( '' !== $target_id && null === wpagent_beaver_public_node_by_id( $state, $target_id ) ) {
			$failure = new WP_Error( 'wpagent_beaver_verify_failed', __( 'Der geänderte Beaver-Builder-Knoten fehlt im Read-back.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			throw new RuntimeException( 'verify-target' );
		}
		$deleted_id = sanitize_key( (string) ( $result['deleted_node_id'] ?? '' ) );
		if ( '' !== $deleted_id && null !== wpagent_beaver_public_node_by_id( $state, $deleted_id ) ) {
			$failure = new WP_Error( 'wpagent_beaver_verify_failed', __( 'Der gelöschte Beaver-Builder-Knoten ist im Read-back noch vorhanden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
			throw new RuntimeException( 'verify-delete' );
		}
		if ( array() !== $verification ) {
			$verified_contract = wpagent_beaver_verify_mutation_contract(
				$verification,
				FLBuilderModel::get_layout_data( 'published', $post_id ),
				FLBuilderModel::get_layout_settings( 'published', $post_id )
			);
			if ( is_wp_error( $verified_contract ) ) {
				$failure = $verified_contract;
				throw new RuntimeException( 'verify-contract' );
			}
		}
		return array_merge( $state, $result, array( 'changed' => true, 'verified' => true, 'revision_id' => $revision_id ) );
	} catch ( Throwable $error ) {
		$current = wpagent_beaver_document_state( $post_id );
		$guard   = wpagent_beaver_mutation_guard( $post_id, $lock );
		if ( ! is_wp_error( $current ) && ! is_wp_error( $guard ) && hash_equals( $current_hash, (string) ( $current['hash'] ?? '' ) ) ) {
			return $failure instanceof WP_Error
				? $failure
				: new WP_Error( 'wpagent_beaver_mutation_failed', __( 'Die Beaver-Builder-Änderung wurde abgebrochen, bevor ein persistierter Zustand verändert wurde.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		// Beaver has no conditional provider restore. Replaying a snapshot here
		// could overwrite a wp-admin change made after this request's write.
		return wpagent_beaver_recovery_required( 'mutation_not_verified_without_candidate_cas', $revision_id, $failure );
	} finally {
		wpagent_beaver_release_lock( $post_id, $lock );
	}
}

/** @return object|WP_Error */
function wpagent_beaver_node( int $post_id, string $node_id ) {
	$node_id = sanitize_key( $node_id );
	if ( '' === $node_id ) {
		return new WP_Error( 'wpagent_beaver_bad_node', __( 'Ungültige Beaver-Builder-Knoten-ID.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$data = FLBuilderModel::get_layout_data( 'published', $post_id );
	if ( ! isset( $data[ $node_id ] ) || ! is_object( $data[ $node_id ] ) ) {
		return new WP_Error( 'wpagent_beaver_node_not_found', __( 'Beaver-Builder-Knoten nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return $data[ $node_id ];
}

/** @return array<string,mixed>|null */
function wpagent_beaver_public_node_by_id( array $state, string $node_id ): ?array {
	foreach ( $state['nodes'] ?? array() as $node ) {
		if ( $node_id === ( $node['id'] ?? '' ) ) {
			return $node;
		}
	}
	return null;
}
