<?php
/** Narrow, schema-bound attribute writes for GenerateBlocks and Kadence Blocks. */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
const WPAGENT_BUILDER_BLOCK_MAX_CONTENT_BYTES = 1048576;
const WPAGENT_BUILDER_BLOCK_GENERATEBLOCKS_VERSION = '2.3.0';
const WPAGENT_BUILDER_BLOCK_KADENCE_VERSION        = '3.7.8';
/** @return array<string,mixed> */
function wpagent_ability_update_generateblocks_block_attributes(): array {
	return wpagent_builder_block_attribute_ability( 'generateblocks' );
}

/** @return array<string,mixed> */
function wpagent_ability_update_kadence_block_attributes(): array {
	return wpagent_builder_block_attribute_ability( 'kadence' );
}

/** @return array<string,mixed> */
function wpagent_builder_block_attribute_ability( string $provider ): array {
	$labels = array(
		'generateblocks' => array(
			__( 'GenerateBlocks-Blockattribute aktualisieren', 'wp-agent-companion' ),
			__( 'Ändert ausschließlich live registrierte und eng freigegebene Pagination-Attribute sowie kanonische Wrapper-Tags eines dynamischen GenerateBlocks-Queryzweigs. Rohes Markup, CSS, Objektattribute und alle anderen Providerattribute bleiben gesperrt.', 'wp-agent-companion' ),
			'wpagent_exec_update_generateblocks_block_attributes',
		),
		'kadence'        => array(
			__( 'Kadence-Blockattribute aktualisieren', 'wp-agent-companion' ),
			__( 'Ändert ausschließlich live registrierte und eng freigegebene Überschriftenattribute einschließlich begrenzter responsiver Pixel-Schriftgrößen, Zeilenhöhen und Buchstabenabstände, responsive Höhen und primitive Dividerwerte eines kanonischen Spacers, Strichbreiten, Farben und reine Textbeschriftungen einer einfachen Progress Bar sowie Zahlen-, Dauer-, Trennzeichen-, Kurztext- und Farbwerte eines kanonischen Count-Up-Blocks. Rohes Markup, Icons, Links, Masken, freie Texte, freies CSS und alle anderen Providerattribute bleiben gesperrt.', 'wp-agent-companion' ),
			'wpagent_exec_update_kadence_block_attributes',
		),
	);
	list( $label, $description, $callback ) = $labels[ $provider ];
	return array(
		'label'       => $label,
		'description' => $description,
		'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' => wpagent_builder_block_attribute_output_schema(),
		'execute_callback'    => $callback,
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta' => array(
			'show_in_rest' => true,
			// expected_hash bindet den Write an genau einen Dokumentstand. Dieselbe
			// Anfrage darf nach einem erfolgreichen Write deshalb nicht erneut als
			// gleichwertig behandelt werden.
			'annotations'  => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ),
		),
	);
}

/** @return array<string,mixed> */
function wpagent_builder_block_attribute_output_schema(): array {
	$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}$' ),
		'cache'              => array(
			'type' => 'object', 'properties' => array(
				'builder' => array( 'type' => 'string', 'enum' => array( 'generateblocks', 'kadence' ) ),
				'executed' => array( 'type' => 'boolean' ), 'verified' => array( 'type' => 'boolean' ),
			), 'required' => array( 'builder', 'executed', 'verified' ), 'additionalProperties' => false,
		),
	);
	return array( 'type' => 'object', 'properties' => $properties, 'required' => array_keys( $properties ), 'additionalProperties' => false );
}

/**
 * Meldet nur dann eine beschreibbare Builder-Fähigkeit, wenn ein gespeicherter
 * Block zugleich Teil der engen Produkt-Allowlist und live serverseitig
 * registriert ist. Das ist absichtlich keine Freigabe für beliebige Attribute
 * eines Provider-Namespaces.
 *
 * @param array<int,array<string,mixed>> $blocks
 */
function wpagent_builder_block_has_patchable_provider_block( array $blocks, string $provider ): bool {
	if ( is_wp_error( wpagent_builder_block_provider_version( $provider ) ) ) {
		return false;
	}
	$policy = wpagent_builder_block_attribute_policy();
	if ( ! isset( $policy[ $provider ] ) ) {
		return false;
	}
	foreach ( $blocks as $block ) {
		$name = is_string( $block['blockName'] ?? null ) ? $block['blockName'] : '';
		if ( isset( $policy[ $provider ][ $name ] ) ) {
			foreach ( array_keys( $policy[ $provider ][ $name ] ) as $attribute ) {
				if ( ! is_wp_error( wpagent_builder_block_attribute_schema( $provider, $name, $attribute ) ) ) {
					return true;
				}
			}
		}
		if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] )
			&& wpagent_builder_block_has_patchable_provider_block( $block['innerBlocks'], $provider ) ) {
			return true;
		}
	}
	return false;
}

/** @return string|WP_Error */
function wpagent_builder_block_provider_version( string $provider ) {
	$contracts = array(
		'generateblocks' => array( 'constant' => 'GENERATEBLOCKS_VERSION', 'version' => WPAGENT_BUILDER_BLOCK_GENERATEBLOCKS_VERSION ),
		'kadence'        => array( 'constant' => 'KADENCE_BLOCKS_VERSION', 'version' => WPAGENT_BUILDER_BLOCK_KADENCE_VERSION ),
	);
	$contract = $contracts[ $provider ] ?? null;
	if ( ! is_array( $contract ) || ! defined( $contract['constant'] ) ) {
		return new WP_Error( 'wpagent_builder_block_unavailable', __( 'Der Builder stellt den versionsgebundenen Serververtrag nicht bereit.', 'wp-agent-companion' ), array( 'status' => 409, 'provider' => $provider ) );
	}
	$actual = constant( $contract['constant'] );
	if ( ! is_string( $actual ) || ! hash_equals( $contract['version'], $actual ) ) {
		return new WP_Error(
			'wpagent_builder_block_version',
			__( 'Die installierte Builder-Version stimmt nicht mit dem eng geprüften Schreibvertrag überein.', 'wp-agent-companion' ),
			array( 'status' => 409, 'provider' => $provider, 'expected_version' => $contract['version'], 'actual_version' => is_scalar( $actual ) ? (string) $actual : '' )
		);
	}
	return $actual;
}

/** @return true|WP_Error */
function wpagent_builder_block_require_provider( string $provider ) {
	$policy = wpagent_builder_block_attribute_policy();
	if ( ! isset( $policy[ $provider ] ) ) {
		return new WP_Error( 'wpagent_builder_block_provider', __( 'Unbekannter Builder-Provider.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$version = wpagent_builder_block_provider_version( $provider );
	if ( is_wp_error( $version ) ) {
		return $version;
	}
	foreach ( array_keys( $policy[ $provider ] ) as $name ) {
		$type = WP_Block_Type_Registry::get_instance()->get_registered( $name );
		if ( $type instanceof WP_Block_Type && is_callable( $type->render_callback ) ) {
			return true;
		}
	}
	return new WP_Error(
		'wpagent_builder_block_unavailable',
		__( 'Der Builder ist nicht aktiv oder stellt den erforderlichen serverseitig registrierten Block nicht bereit.', 'wp-agent-companion' ),
		array( 'status' => 409, 'provider' => $provider )
	);
}

/** @return array{schema:array<string,mixed>,rule:array<string,mixed>}|WP_Error */
function wpagent_builder_block_attribute_schema( string $provider, string $block_name, string $attribute ) {
	$policy = wpagent_builder_block_attribute_policy();
	$rule   = $policy[ $provider ][ $block_name ][ $attribute ] ?? null;
	$type   = WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
	if ( ! is_array( $rule ) || ! $type instanceof WP_Block_Type || ! is_callable( $type->render_callback )
		|| wpagent_native_block_namespace( $block_name ) !== $provider || ! wpagent_native_block_patchable_attribute_name( $attribute ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_not_allowed', __( 'Das Attribut ist für diesen Builderblock nicht freigegeben.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	$schemas = $type->get_attributes();
	$schema  = $schemas[ $attribute ] ?? null;
	if ( ! is_array( $schema ) || ! wpagent_native_block_patchable_attribute_schema( $schema ) ) {
		return new WP_Error( 'wpagent_builder_block_provider_schema', __( 'Das live registrierte Providerschema ist für dieses Attribut nicht sicher beschreibbar.', 'wp-agent-companion' ), array( 'status' => 409, 'attribute' => $attribute ) );
	}
	return array( 'schema' => $schema, 'rule' => $rule );
}

/** @param mixed $value @return true|WP_Error */
function wpagent_builder_block_validate_attribute_value( string $provider, string $block_name, string $attribute, $value ) {
	$definition = wpagent_builder_block_attribute_schema( $provider, $block_name, $attribute );
	if ( is_wp_error( $definition ) ) {
		return $definition;
	}
	$rule = $definition['rule'];
	if ( 'integer' === ( $rule['type'] ?? '' ) && ! is_int( $value ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_type', __( 'Der Attributwert muss eine ganze Zahl sein.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	if ( 'string' === ( $rule['type'] ?? '' ) && ! is_string( $value ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_type', __( 'Der Attributwert muss eine Zeichenkette sein.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	if ( 'number' === ( $rule['type'] ?? '' ) && ( ( ! is_int( $value ) && ! is_float( $value ) ) || ! is_finite( (float) $value ) ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_type', __( 'Der Attributwert muss eine endliche Zahl sein.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	$valid = wpagent_native_block_validate_patch_value( $attribute, $value, $definition['schema'] );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	if ( ( isset( $rule['minimum'] ) && $value < $rule['minimum'] ) || ( isset( $rule['maximum'] ) && $value > $rule['maximum'] ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_bounds', __( 'Der Attributwert liegt außerhalb des eng freigegebenen Providerbereichs.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	if ( isset( $rule['values'] ) && ( ! is_array( $rule['values'] ) || ! in_array( $value, $rule['values'], true ) ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_values', __( 'Der Attributwert ist im eng freigegebenen Providervertrag nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	if ( isset( $rule['pattern'] ) && ( ! is_string( $rule['pattern'] ) || 1 !== preg_match( $rule['pattern'], $value ) ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_pattern', __( 'Der Attributwert entspricht nicht dem eng freigegebenen Providerformat.', 'wp-agent-companion' ), array( 'status' => 400, 'attribute' => $attribute ) );
	}
	return true;
}

/** @param array<int,array<string,mixed>> $blocks @param array<int,int> $path @return array<string,mixed>|WP_Error */
function wpagent_builder_block_patch_path( array &$blocks, array $path, array $attributes, array $remove_attributes, string $provider, int $depth = 0 ) {
	$index = $path[ $depth ];
	if ( ! isset( $blocks[ $index ] ) || ! is_array( $blocks[ $index ] ) ) {
		return new WP_Error( 'wpagent_builder_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_builder_block_path_not_found', __( 'Der Blockpfad wurde im aktuellen Dokument nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		return wpagent_builder_block_patch_path( $blocks[ $index ]['innerBlocks'], $path, $attributes, $remove_attributes, $provider, $depth + 1 );
	}
	$block_name = is_string( $blocks[ $index ]['blockName'] ?? null ) ? $blocks[ $index ]['blockName'] : '';
	if ( ! isset( wpagent_builder_block_attribute_policy()[ $provider ][ $block_name ] ) ) {
		return new WP_Error( 'wpagent_builder_block_not_allowed', __( 'Am Zielpfad liegt kein für diesen Builder freigegebener Block.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( array_merge( array_keys( $attributes ), $remove_attributes ) as $attribute ) {
		if ( ! is_string( $attribute ) ) {
			return new WP_Error( 'wpagent_builder_block_attribute_not_allowed', __( 'Der Attributname ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$schema = wpagent_builder_block_attribute_schema( $provider, $block_name, $attribute );
		if ( is_wp_error( $schema ) ) {
			return $schema;
		}
	}
	foreach ( $attributes as $attribute => $value ) {
		$valid = wpagent_builder_block_validate_attribute_value( $provider, $block_name, $attribute, $value );
		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 ] );
	}
	if ( 'generateblocks' === $provider && array_key_exists( 'tagName', array_merge( $attributes, array_fill_keys( $remove_attributes, null ) ) ) ) {
		$synced = wpagent_builder_block_sync_generateblocks_markup( $blocks[ $index ], $before, $after );
		if ( is_wp_error( $synced ) ) {
			return $synced;
		}
	}
	if ( 'kadence/countup' === $block_name ) {
		$synced = wpagent_builder_block_sync_kadence_countup_markup( $blocks[ $index ], $before, $after );
		if ( is_wp_error( $synced ) ) {
			return $synced;
		}
	}
	if ( 'kadence/advancedheading' === $block_name
		&& array_intersect( array_merge( array_keys( $attributes ), $remove_attributes ), array( 'size', 'tabSize', 'mobileSize', 'sizeType' ) )
		&& array_key_exists( 'sizeType', $after ) && 'px' !== $after['sizeType'] ) {
		return new WP_Error( 'wpagent_builder_block_heading_size_unit', __( 'Responsive Kadence-Schriftgrößen dürfen nur den eng geprüften Pixelvertrag verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'kadence/advancedheading' === $block_name
		&& array_intersect( array_merge( array_keys( $attributes ), $remove_attributes ), array( 'lineHeight', 'tabLineHeight', 'mobileLineHeight', 'lineType' ) )
		&& array_key_exists( 'lineType', $after ) && 'px' !== $after['lineType'] ) {
		return new WP_Error( 'wpagent_builder_block_heading_line_unit', __( 'Responsive Kadence-Zeilenhöhen dürfen nur den eng geprüften Pixelvertrag verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'kadence/advancedheading' === $block_name
		&& array_intersect( array_merge( array_keys( $attributes ), $remove_attributes ), array( 'letterSpacing', 'tabletLetterSpacing', 'mobileLetterSpacing', 'letterSpacingType' ) )
		&& array_key_exists( 'letterSpacingType', $after ) && 'px' !== $after['letterSpacingType'] ) {
		return new WP_Error( 'wpagent_builder_block_heading_letter_spacing_unit', __( 'Responsive Kadence-Buchstabenabstände dürfen nur den eng geprüften Pixelvertrag verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'kadence/advancedheading' === $block_name
		&& array_intersect( array_merge( array_keys( $attributes ), $remove_attributes ), array( 'topMargin', 'rightMargin', 'bottomMargin', 'leftMargin', 'marginType' ) )
		&& array_key_exists( 'marginType', $after ) && 'px' !== $after['marginType'] ) {
		return new WP_Error( 'wpagent_builder_block_heading_margin_unit', __( 'Kadence-Außenabstände dürfen nur den eng geprüften Pixelvertrag verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'kadence/advancedheading' === $block_name
		&& array_intersect( array_keys( $attributes ), array( 'iconColor', 'iconColorHover', 'iconSide', 'iconVerticalAlign' ) )
		&& ( ! is_string( $after['icon'] ?? null ) || 1 !== preg_match( '/^[A-Za-z0-9_-]{1,100}$/D', $after['icon'] ) ) ) {
		return new WP_Error( 'wpagent_builder_block_heading_icon_required', __( 'Kadence-Iconattribute dürfen nur für eine Advanced Heading mit vorhandenem, sicher geformtem Provider-Icon gesetzt werden.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$blocks[ $index ]['attrs'] = $after;
	return array( 'block_name' => $block_name, 'before' => $before, 'after' => $after, 'changed' => $after !== $before );
}

/**
 * Rebuilds only empirically verified provider branches. GenerateBlocks permits
 * a root text block, a semantic Element wrapper with one canonical Core
 * paragraph, its complete constrained query/page-numbers branch, or the
 * page-numbers child inside that branch. Every other shape is rejected rather
 * than rendered as ambient callback context.
 *
 * @param array<int,array<string,mixed>> $blocks
 * @param array<int,int>                 $path
 * @return array<string,mixed>|WP_Error
 */
function wpagent_builder_block_render_branch( array $blocks, array $path, string $provider, int $depth = 0 ) {
	$index = $path[ $depth ];
	if ( ! isset( $blocks[ $index ] ) || ! is_array( $blocks[ $index ] ) ) {
		return new WP_Error( 'wpagent_builder_block_path_not_found', __( 'Der Blockpfad wurde im aktuellen Dokument nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$block = $blocks[ $index ];
	if ( $depth + 1 === count( $path ) ) {
		if ( 'kadence' === $provider ) {
			if ( 1 !== count( $path ) || ( ! wpagent_builder_block_kadence_heading_valid( $block ) && ! wpagent_builder_block_kadence_spacer_valid( $block ) && ! wpagent_builder_block_kadence_progress_valid( $block ) && ! wpagent_builder_block_kadence_countup_valid( $block ) ) ) {
				return new WP_Error( 'wpagent_builder_block_render_context', __( 'Die erlaubte Builder-Renderform stimmt nicht mit dem eng geprüften Providervertrag überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			return $block;
		}
		if ( 'generateblocks' !== $provider || ! in_array( count( $path ), array( 1, 2 ), true ) ) {
			return new WP_Error( 'wpagent_builder_block_render_context', __( 'Die erlaubte Builder-Renderform stimmt nicht mit dem eng geprüften Providervertrag überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
			if ( 1 === count( $path ) ) {
				if ( 'generateblocks/text' === ( $block['blockName'] ?? null ) ) {
					return wpagent_builder_block_generateblocks_text_valid( $block )
						? $block
						: new WP_Error( 'wpagent_builder_block_render_structure', __( 'Der freigegebene GenerateBlocks-Textblock stimmt nicht mit dem eng geprüften Providervertrag überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
				}
				if ( 'generateblocks/element' === ( $block['blockName'] ?? null ) ) {
					return wpagent_builder_block_generateblocks_element_valid( $block )
						? $block
						: new WP_Error( 'wpagent_builder_block_render_structure', __( 'Die erlaubte Builder-Renderform stimmt nicht mit dem eng geprüften Providervertrag überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
				}
			return wpagent_builder_block_generateblocks_query_branch_valid( $block )
				? $block
				: new WP_Error( 'wpagent_builder_block_render_structure', __( 'Der freigegebene GenerateBlocks-Queryzweig stimmt nicht mit dem eng geprüften Providervertrag überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! wpagent_builder_block_generateblocks_page_numbers_valid( $block ) ) {
			return new WP_Error( 'wpagent_builder_block_render_structure', __( 'Der freigegebene Zielblock enthält unerwartete innere Blöcke und wird nicht isoliert gerendert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		return $block;
	}
	$name = is_string( $block['blockName'] ?? null ) ? $block['blockName'] : '';
	$type = WP_Block_Type_Registry::get_instance()->get_registered( $name );
	if ( 'generateblocks' !== $provider || 0 !== $depth || 2 !== count( $path ) || 'generateblocks/query' !== $name
		|| ! $type instanceof WP_Block_Type || ! wpagent_builder_block_render_callback_matches( $type, array( 'GenerateBlocks_Block_Query', 'render_block' ) )
		|| ! wpagent_builder_block_generateblocks_query_branch_valid( $block ) ) {
		return new WP_Error( 'wpagent_builder_block_render_context', __( 'Der Zielblock benötigt einen nicht freigegebenen oder nicht serverseitig registrierten Vorfahren. Der isolierte Rendervertrag bleibt deshalb gesperrt.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$child = wpagent_builder_block_render_branch( $block['innerBlocks'], $path, $provider, $depth + 1 );
	if ( is_wp_error( $child ) ) {
		return $child;
	}
	$branch = $block;
	$branch['innerBlocks'] = array( $child );
	return $branch;
}

/** @param array<int|string,mixed> $expected */
function wpagent_builder_block_render_callback_matches( WP_Block_Type $type, array $expected ): bool {
	$callback = $type->render_callback;
	return is_array( $callback ) && 2 === count( $callback ) && $expected === array_values( $callback ) && is_callable( $callback );
}

/** @param array<string,mixed> $block */
function wpagent_builder_block_kadence_heading_valid( array $block ): bool {
	$name = is_string( $block['blockName'] ?? null ) ? $block['blockName'] : '';
	$type = WP_Block_Type_Registry::get_instance()->get_registered( $name );
	$callback = $type instanceof WP_Block_Type ? $type->render_callback : null;
	return 'kadence/advancedheading' === $name
		&& $type instanceof WP_Block_Type
		&& is_array( $callback ) && 2 === count( $callback )
		&& isset( $callback[0] ) && $callback[0] instanceof Kadence_Blocks_Advancedheading_Block
		&& 'render_css' === ( $callback[1] ?? null ) && is_callable( $callback )
		&& isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && array() === $block['innerBlocks'];
}

/**
 * Renders only the schema-validated target branch, never the whole document.
 *
 * @param array<int,array<string,mixed>> $blocks
 * @param array<int,int>                 $path
 * @return string|WP_Error
 */
function wpagent_builder_block_render_target( array $blocks, array $path, array $attributes, array $remove_attributes, string $provider ) {
	$validated = $blocks;
	$target = wpagent_builder_block_patch_path( $validated, $path, $attributes, $remove_attributes, $provider );
	if ( is_wp_error( $target ) || $target['changed'] ) {
		return new WP_Error( 'wpagent_builder_block_render_structure', __( 'Der isolierte Renderzweig stimmt nicht mit dem schema-geprüften Zielzustand überein.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$branch = wpagent_builder_block_render_branch( $validated, $path, $provider );
	if ( is_wp_error( $branch ) ) {
		return $branch;
	}
	$content = serialize_blocks( array( $branch ) );
	if ( '' === $content || serialize_blocks( parse_blocks( $content ) ) !== $content ) {
		return new WP_Error( 'wpagent_builder_block_render_structure', __( 'Der isolierte Provider-Renderzweig ist nicht stabil serialisierbar.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	try {
		$rendered = do_blocks( $content );
	} catch ( Throwable $error ) {
		return new WP_Error( 'wpagent_builder_block_render_failed', $error->getMessage(), array( 'status' => 400 ) );
	}
	if ( ! is_string( $rendered ) || '' === trim( $rendered ) ) {
		return new WP_Error( 'wpagent_builder_block_render_failed', __( 'Der isolierte Builder-Renderzweig erzeugt keine sichere serverseitige Frontend-Ausgabe.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $rendered;
}

/** @return array{builder:string,executed:bool,verified:bool} */
function wpagent_builder_block_refresh_cache( string $provider, int $post_id ): array {
	if ( 'kadence' === $provider ) {
		return array( 'builder' => 'kadence', 'executed' => false, 'verified' => true );
	}
	$cache = wpagent_refresh_builder_cache_generateblocks( $post_id );
	return array(
		'builder'  => 'generateblocks',
		'executed' => ! empty( $cache['executed'] ),
		'verified' => ! empty( $cache['verified'] ),
	);
}

/** @return array{builder:string,executed:bool,verified:bool} */
function wpagent_builder_block_noop_cache( string $provider ): array {
	return array( 'builder' => $provider, 'executed' => false, 'verified' => true );
}

/**
 * Restoring a candidate after a provider cache path has started is complete only
 * when both the exact raw content and the provider's rollback cache path verify.
 *
 * @return array{rolled_back:bool,rollback_verified:bool,recovery_required:bool}
 */
function wpagent_builder_block_rollback_after_cache( string $provider, int $post_id, string $before_content, string $candidate_content ): array {
	$rollback = wpagent_native_block_rollback( $post_id, $before_content, $candidate_content );
	if ( ! $rollback['rolled_back'] ) {
		return $rollback;
	}
	$cache = wpagent_builder_block_refresh_cache( $provider, $post_id );
	$raw_content = wpagent_native_block_raw_content( $post_id );
	$state = wpagent_native_block_document_state( $post_id );
	if ( ! $cache['verified'] || is_wp_error( $raw_content ) || $raw_content !== $before_content || is_wp_error( $state ) || $state['content'] !== $before_content ) {
		return array( 'rolled_back' => true, 'rollback_verified' => false, 'recovery_required' => true );
	}
	return $rollback;
}

/** @param array{rolled_back:bool,rollback_verified:bool,recovery_required:bool} $rollback @return WP_Error */
function wpagent_builder_block_write_error( string $code, string $message, int $revision_id, array $rollback ) {
	$data = array(
		'status'            => 500,
		'revision_id'       => $revision_id,
		'rolled_back'       => $rollback['rolled_back'],
		'rollback_verified' => $rollback['rollback_verified'],
		'recovery_required' => $rollback['recovery_required'],
	);
	if ( $rollback['recovery_required'] ) {
		return new WP_Error(
			'wpagent_builder_block_recovery_required',
			__( 'Der aktuelle Dokument- oder Builder-Cachezustand konnte nach der Blockänderung nicht vollständig verifiziert werden. Prüfe die angelegte Revision, bevor du die Änderung erneut ausführst.', 'wp-agent-companion' ),
			$data
		);
	}
	return new WP_Error( $code, $message, $data );
}

/** @return WP_Error */
function wpagent_builder_block_lock_lost_after_write( int $revision_id ) {
	return wpagent_builder_block_write_error(
		'wpagent_builder_block_lock_lost',
		__( 'Die exklusive Sperre ging nach Beginn der Builder-Blockänderung verloren. Die aktuelle Änderung bleibt zur manuellen Prüfung erhalten.', 'wp-agent-companion' ),
		$revision_id,
		array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true )
	);
}

/**
 * A rollback that finishes after the exclusive lock was lost cannot be
 * advertised as verified, even when its immediate read-back matched.
 *
 * @param array{rolled_back:bool,rollback_verified:bool,recovery_required:bool} $rollback
 * @return array{rolled_back:bool,rollback_verified:bool,recovery_required:bool}
 */
function wpagent_builder_block_verify_rollback_lock( string $provider, int $post_id, string $lock, array $rollback ): array {
	if ( wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
		return $rollback;
	}
	$rollback['rollback_verified'] = false;
	$rollback['recovery_required'] = true;
	return $rollback;
}

/** @param array<string,mixed> $before @return array<string,mixed>|WP_Error */
function wpagent_builder_block_revalidate_document( int $post_id, array $before ) {
	$state = wpagent_native_block_document_state( $post_id );
	if ( is_wp_error( $state ) ) {
		return $state;
	}
	if ( ! isset( $before['hash'] ) || ! is_string( $before['hash'] ) || ! hash_equals( $before['hash'], $state['hash'] ) ) {
		return new WP_Error(
			'wpagent_builder_block_conflict',
			__( 'Das Blockdokument wurde unmittelbar vor dem Speichern verändert.', 'wp-agent-companion' ),
			array( 'status' => 409, 'current_hash' => $state['hash'] )
		);
	}
	return $state;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_builder_block_update( $input, string $provider ) {
	$available = wpagent_builder_block_require_provider( $provider );
	if ( is_wp_error( $available ) ) {
		return $available;
	}
	$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}$/D', $expected_hash ) ) {
		return new WP_Error( 'wpagent_builder_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();
	$raw_remove_attributes = is_array( $input['remove_attributes'] ?? null ) ? $input['remove_attributes'] : array();
	if ( array() !== $attributes && wp_is_numeric_array( $attributes ) ) {
		return new WP_Error( 'wpagent_builder_block_attributes', __( 'attributes muss ein Objekt mit Attributnamen sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_filter( array_keys( $attributes ), static fn( $attribute ): bool => ! is_string( $attribute ) ) ) {
		return new WP_Error( 'wpagent_builder_block_attributes', __( 'attributes darf nur Zeichenketten als Attributnamen enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! wp_is_numeric_array( $raw_remove_attributes ) || array_filter( $raw_remove_attributes, static fn( $attribute ): bool => ! is_string( $attribute ) ) ) {
		return new WP_Error( 'wpagent_builder_block_remove_attributes', __( 'remove_attributes darf nur eine Liste von Attributnamen enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$remove_attributes = array_values( $raw_remove_attributes );
	if ( array() === $attributes && array() === $remove_attributes ) {
		return new WP_Error( 'wpagent_builder_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 ) || array_intersect( array_keys( $attributes ), $remove_attributes ) ) {
		return new WP_Error( 'wpagent_builder_block_attribute_limit', __( 'Die Attributänderung überschreitet die sicheren Grenzen oder setzt und entfernt dasselbe Attribut.', '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_builder_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( $provider . '-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_builder_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_BUILDER_BLOCK_MAX_CONTENT_BYTES ) {
			return new WP_Error( 'wpagent_builder_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_builder_block_noncanonical_document', __( 'Das bestehende Builder-Dokument ist nicht exakt parse- und serialisierbar und wird deshalb nicht automatisch verändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$target = wpagent_builder_block_patch_path( $blocks, $path, $attributes, $remove_attributes, $provider );
		if ( is_wp_error( $target ) ) {
			return $target;
		}
		if ( ! $target['changed'] ) {
			$rendered = wpagent_builder_block_render_target( $blocks, $path, $attributes, $remove_attributes, $provider );
			if ( is_wp_error( $rendered ) ) {
				return $rendered;
			}
			if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
				return new WP_Error( 'wpagent_builder_block_lock_lost', __( 'Die exklusive Sperre ging während der Builder-Blockprüfung verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			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'], 'cache' => wpagent_builder_block_noop_cache( $provider ),
			);
		}
		$candidate = serialize_blocks( $blocks );
		if ( strlen( $candidate ) > WPAGENT_BUILDER_BLOCK_MAX_CONTENT_BYTES || serialize_blocks( parse_blocks( $candidate ) ) !== $candidate ) {
			return new WP_Error( 'wpagent_builder_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_builder_block_structure_changed', __( 'Der Attribut-Patch würde die Blockstruktur verändern und wurde abgelehnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$candidate_blocks = parse_blocks( $candidate );
		$rendered = wpagent_builder_block_render_target( $candidate_blocks, $path, $attributes, $remove_attributes, $provider );
		if ( is_wp_error( $rendered ) ) {
			return $rendered;
		}
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return new WP_Error( 'wpagent_builder_block_lock_lost', __( 'Die exklusive Sperre ging vor der Builder-Blockänderung verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$prewrite = wpagent_builder_block_revalidate_document( $post_id, $before );
		if ( is_wp_error( $prewrite ) ) {
			return $prewrite;
		}
		$revision_id = wpagent_native_block_create_revision( $post_id, $before['content'] );
		if ( is_wp_error( $revision_id ) ) {
			return $revision_id;
		}
		$prewrite = wpagent_builder_block_revalidate_document( $post_id, $before );
		if ( is_wp_error( $prewrite ) ) {
			return new WP_Error(
				$prewrite->get_error_code(),
				$prewrite->get_error_message(),
				array_merge( is_array( $prewrite->get_error_data() ) ? $prewrite->get_error_data() : array(), array( 'revision_id' => $revision_id ) )
			);
		}
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return new WP_Error( 'wpagent_builder_block_lock_lost', __( 'Die exklusive Sperre ging vor dem Builder-Block-Write verloren.', 'wp-agent-companion' ), array( 'status' => 409, 'revision_id' => $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 ) )
			);
		}
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return wpagent_builder_block_lock_lost_after_write( $revision_id );
		}
		$after = wpagent_native_block_document_state( $post_id );
		if ( is_wp_error( $after ) ) {
			return wpagent_builder_block_write_error(
				'wpagent_builder_block_verify_interrupted',
				__( 'Die Builder-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 )
			);
		}
		$after_raw = wpagent_native_block_raw_content( $post_id );
		$verified_blocks = ! is_wp_error( $after_raw ) && $after_raw === $candidate && ! is_wp_error( $after ) && $after['content'] === $candidate ? parse_blocks( $after_raw ) : array();
		$verified_target = $verified_blocks ? wpagent_builder_block_patch_path( $verified_blocks, $path, $attributes, $remove_attributes, $provider ) : null;
		$rendered_after = $verified_blocks ? wpagent_builder_block_render_target( $verified_blocks, $path, $attributes, $remove_attributes, $provider ) : null;
		$verified = is_string( $after_raw ) && $after_raw === $candidate && is_array( $after ) && ! is_wp_error( $after ) && $after['content'] === $candidate && is_array( $verified_target ) && ! $verified_target['changed'] && is_string( $rendered_after );
		if ( ! $verified ) {
			if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
				return wpagent_builder_block_lock_lost_after_write( $revision_id );
			}
			$rollback = wpagent_native_block_rollback( $post_id, $before['content'], $candidate );
			$rollback = wpagent_builder_block_verify_rollback_lock( $provider, $post_id, $lock, $rollback );
			return wpagent_builder_block_write_error( 'wpagent_builder_block_verify_failed', __( 'Die Builder-Blockattribute bestanden die Parse-, Rücklese- oder Frontend-Prüfung nicht.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return wpagent_builder_block_lock_lost_after_write( $revision_id );
		}
		$cache = wpagent_builder_block_refresh_cache( $provider, $post_id );
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return wpagent_builder_block_lock_lost_after_write( $revision_id );
		}
		if ( ! $cache['verified'] ) {
			$rollback = wpagent_builder_block_rollback_after_cache( $provider, $post_id, $before['content'], $candidate );
			$rollback = wpagent_builder_block_verify_rollback_lock( $provider, $post_id, $lock, $rollback );
			return wpagent_builder_block_write_error( 'wpagent_builder_block_cache_failed', __( 'Der Builder-Cache konnte nicht verifiziert aktualisiert werden.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		$after_cache_raw = wpagent_native_block_raw_content( $post_id );
		$after_cache = wpagent_native_block_document_state( $post_id );
		if ( is_wp_error( $after_cache ) ) {
			return wpagent_builder_block_write_error(
				'wpagent_builder_block_cache_readback_interrupted',
				__( 'Der Builder-Cache-Nachlauf kann unter der aktuellen Berechtigung nicht mehr vollständig verifiziert werden.', 'wp-agent-companion' ),
				$revision_id,
				array( 'rolled_back' => false, 'rollback_verified' => false, 'recovery_required' => true )
			);
		}
		$after_cache_blocks = ! is_wp_error( $after_cache_raw ) && $after_cache_raw === $candidate && ! is_wp_error( $after_cache ) && $after_cache['content'] === $candidate ? parse_blocks( $after_cache_raw ) : array();
		$after_cache_target = $after_cache_blocks ? wpagent_builder_block_patch_path( $after_cache_blocks, $path, $attributes, $remove_attributes, $provider ) : null;
		$after_cache_rendered = $after_cache_blocks ? wpagent_builder_block_render_target( $after_cache_blocks, $path, $attributes, $remove_attributes, $provider ) : null;
		$cache_readback_verified = is_string( $after_cache_raw ) && $after_cache_raw === $candidate && is_array( $after_cache ) && ! is_wp_error( $after_cache ) && $after_cache['content'] === $candidate
			&& is_array( $after_cache_target ) && ! $after_cache_target['changed'] && is_string( $after_cache_rendered );
		if ( ! $cache_readback_verified ) {
			if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
				return wpagent_builder_block_lock_lost_after_write( $revision_id );
			}
			$rollback = wpagent_builder_block_rollback_after_cache( $provider, $post_id, $before['content'], $candidate );
			$rollback = wpagent_builder_block_verify_rollback_lock( $provider, $post_id, $lock, $rollback );
			return wpagent_builder_block_write_error( 'wpagent_builder_block_cache_readback_failed', __( 'Der Builder-Cache-Nachlauf veränderte den gespeicherten Blockzustand oder konnte ihn nicht erneut verifizieren.', 'wp-agent-companion' ), $revision_id, $rollback );
		}
		if ( ! wpagent_site_editor_refresh_lock( $provider . '-block', (string) $post_id, $lock ) ) {
			return wpagent_builder_block_lock_lost_after_write( $revision_id );
		}
		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( $after_cache_rendered ), 'previous_hash' => $before['hash'], 'hash' => $after_cache['hash'], 'cache' => $cache,
		);
	} finally {
		wpagent_site_editor_release_lock( $provider . '-block', (string) $post_id, $lock );
	}
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_update_generateblocks_block_attributes( $input ) {
	return wpagent_builder_block_update( $input, 'generateblocks' );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_exec_update_kadence_block_attributes( $input ) {
	return wpagent_builder_block_update( $input, 'kadence' );
}
