<?php
/** Shared Site Editor content helpers. */

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

const WPAGENT_SITE_EDITOR_MAX_CONTENT_BYTES = 1048576;
const WPAGENT_SITE_EDITOR_MAX_COMPONENTS    = 50;
const WPAGENT_SITE_EDITOR_MAX_NAV_ITEMS     = 100;

/** @return array<string,mixed> */
function wpagent_site_editor_component_schema(): array {
	return array(
		'type'                 => 'object',
		'properties'           => array(
			'type'                => array(
				'type' => 'string',
				'enum' => array( 'markdown', 'template-part', 'pattern', 'site-logo', 'site-title', 'navigation', 'post-title', 'post-featured-image', 'post-content', 'post-excerpt', 'post-date', 'post-author-name', 'post-terms', 'query-title', 'query-loop', 'separator', 'spacer' ),
			),
			'markdown'            => array( 'type' => 'string', 'maxLength' => 50000 ),
			'slug'                => array( 'type' => 'string', 'maxLength' => 200 ),
			'navigation_id'       => array( 'type' => 'integer', 'minimum' => 1 ),
			'level'               => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 6 ),
			'is_link'             => array( 'type' => 'boolean' ),
			'width'               => array( 'type' => 'integer', 'minimum' => 16, 'maximum' => 600 ),
			'taxonomy'            => array( 'type' => 'string', 'maxLength' => 32 ),
			'query_title_type'     => array( 'type' => 'string', 'enum' => array( 'archive', 'search' ) ),
			'per_page'            => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 50 ),
			'inherit_query'       => array( 'type' => 'boolean' ),
			'post_type'           => array( 'type' => 'string', 'maxLength' => 20 ),
			'order'               => array( 'type' => 'string', 'enum' => array( 'asc', 'desc' ) ),
			'order_by'            => array( 'type' => 'string', 'enum' => array( 'date', 'modified', 'title', 'menu_order' ) ),
			'show_excerpt'        => array( 'type' => 'boolean' ),
			'show_date'           => array( 'type' => 'boolean' ),
			'show_featured_image' => array( 'type' => 'boolean' ),
			'no_results_text'     => array( 'type' => 'string', 'maxLength' => 500 ),
			'height'              => array( 'type' => 'integer', 'minimum' => 0, 'maximum' => 400 ),
		),
		'required'             => array( 'type' ),
		'additionalProperties' => false,
	);
}

/** @return array<string,mixed> */
function wpagent_site_editor_components_schema(): array {
	return array(
		'type'     => 'array',
		'minItems' => 1,
		'maxItems' => WPAGENT_SITE_EDITOR_MAX_COMPONENTS,
		'items'    => wpagent_site_editor_component_schema(),
	);
}

/** @return array<string,mixed> */
function wpagent_site_editor_resource_schema(): array {
	$properties = array(
		'id'                    => array( 'type' => 'string' ),
		'wp_id'                 => array( 'type' => 'integer' ),
		'theme'                 => array( 'type' => 'string' ),
		'slug'                  => array( 'type' => 'string' ),
		'type'                  => array( 'type' => 'string', 'enum' => array( 'wp_template', 'wp_template_part' ) ),
		'source'                => array( 'type' => 'string' ),
		'origin'                => array( 'type' => array( 'string', 'null' ) ),
		'title'                 => array( 'type' => 'string' ),
		'description'           => array( 'type' => 'string' ),
		'status'                => array( 'type' => 'string' ),
		'content'               => array( 'type' => 'string' ),
		'area'                  => array( 'type' => array( 'string', 'null' ) ),
		'has_theme_file'        => array( 'type' => 'boolean' ),
		'is_custom'             => array( 'type' => 'boolean' ),
		'modified'              => array( 'type' => array( 'string', 'null' ) ),
		'block_count'           => array( 'type' => 'integer' ),
		'freeform_count'        => array( 'type' => 'integer' ),
		'block_types'           => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
		'disallowed_blocks'     => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
		'has_disallowed_blocks' => array( 'type' => 'boolean' ),
		'hash'                  => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
	);
	return array( 'type' => 'object', 'properties' => $properties, 'required' => array_keys( $properties ), 'additionalProperties' => false );
}

/** @return array<string,mixed> */
function wpagent_site_editor_navigation_schema(): array {
	$properties = array(
		'id'                    => array( 'type' => 'integer' ),
		'slug'                  => array( 'type' => 'string' ),
		'title'                 => array( 'type' => 'string' ),
		'status'                => array( 'type' => 'string' ),
		'content'               => array( 'type' => 'string' ),
		'modified'              => array( 'type' => 'string' ),
		'item_count'            => array( 'type' => 'integer' ),
		'block_count'           => array( 'type' => 'integer' ),
		'freeform_count'        => array( 'type' => 'integer' ),
		'block_types'           => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
		'disallowed_blocks'     => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
		'has_disallowed_blocks' => array( 'type' => 'boolean' ),
		'hash'                  => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
	);
	return array( 'type' => 'object', 'properties' => $properties, 'required' => array_keys( $properties ), 'additionalProperties' => false );
}

/** @return array<string,mixed> */
function wpagent_site_editor_nav_item_schema( int $depth = 0 ): array {
	$properties = array(
		'type'                => array( 'type' => 'string', 'enum' => array( 'custom', 'home', 'page', 'post', 'category', 'tag', 'submenu' ) ),
		'label'               => array( 'type' => 'string', 'maxLength' => 200 ),
		'url'                 => array( 'type' => 'string', 'maxLength' => 2048 ),
		'id'                  => array( 'type' => 'integer', 'minimum' => 1 ),
		'opens_in_new_tab'    => array( 'type' => 'boolean' ),
		'description'         => array( 'type' => 'string', 'maxLength' => 500 ),
	);
	if ( $depth < 2 ) {
		$properties['items'] = array( 'type' => 'array', 'minItems' => 1, 'maxItems' => 50, 'items' => wpagent_site_editor_nav_item_schema( $depth + 1 ) );
	}
	return array( 'type' => 'object', 'properties' => $properties, 'required' => array( 'type' ), 'additionalProperties' => false );
}

function wpagent_site_editor_text_length( string $value ): int {
	return function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
}

/** @return string|WP_Error */
function wpagent_site_editor_plain_text( $value, string $field, int $max, bool $allow_empty = false ) {
	$text = is_string( $value ) ? trim( $value ) : '';
	if ( ( ! $allow_empty && '' === $text ) || wpagent_site_editor_text_length( $text ) > $max || sanitize_text_field( $text ) !== $text ) {
		/* translators: 1: input field name, 2: maximum number of characters. */
		return new WP_Error( 'wpagent_site_editor_text', sprintf( __( '%1$s muss reiner Text mit höchstens %2$d Zeichen sein.', 'wp-agent-companion' ), $field, $max ), array( 'status' => 400 ) );
	}
	return $text;
}

/** @return string|WP_Error */
function wpagent_site_editor_slug( $value ) {
	$slug = is_string( $value ) ? trim( $value ) : '';
	if ( '' === $slug || strlen( $slug ) > 200 || sanitize_title( $slug ) !== $slug || str_contains( $slug, '/' ) ) {
		return new WP_Error( 'wpagent_site_editor_slug', __( 'slug muss ein gültiger WordPress-Slug mit höchstens 200 Zeichen sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $slug;
}

function wpagent_site_editor_template_id( string $slug ): string {
	return get_stylesheet() . '//' . $slug;
}

/** @return true|WP_Error */
function wpagent_site_editor_require_block_theme() {
	if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
		return true;
	}
	return new WP_Error( 'wpagent_site_editor_classic_theme', __( 'Blocktemplates und Template-Teile sind nur mit einem aktiven Blocktheme wirksam und werden deshalb nicht geschrieben.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

/** @return WP_Block_Template|WP_Error */
function wpagent_site_editor_get_template( string $type, string $slug ) {
	if ( ! in_array( $type, array( 'wp_template', 'wp_template_part' ), true ) ) {
		return new WP_Error( 'wpagent_site_editor_type', __( 'Unbekannte Site-Editor-Ressourcenart.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$template = get_block_template( wpagent_site_editor_template_id( $slug ), $type );
	if ( ! $template instanceof WP_Block_Template || 'trash' === $template->status ) {
		return new WP_Error( 'wpagent_site_editor_not_found', __( 'Site-Editor-Ressource nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return $template;
}

/** @param array<int,array<string,mixed>> $blocks */
function wpagent_site_editor_walk_blocks( array $blocks, array &$report ): void {
	$registry = WP_Block_Type_Registry::get_instance();
	foreach ( $blocks as $block ) {
		$name  = $block['blockName'] ?? null;
		$inner = trim( (string) ( $block['innerHTML'] ?? '' ) );
		if ( null === $name ) {
			if ( '' !== $inner ) {
				++$report['block_count'];
				++$report['freeform_count'];
				$report['block_types'][]       = 'core/freeform';
				$report['disallowed_blocks'][] = 'core/freeform';
			}
			continue;
		}
		++$report['block_count'];
		$report['block_types'][] = $name;
		if ( 'core/freeform' === $name ) {
			++$report['freeform_count'];
		}
		if ( ! str_starts_with( $name, 'core/' ) || in_array( $name, array( 'core/freeform', 'core/html', 'core/shortcode' ), true ) || ! $registry->is_registered( $name ) ) {
			$report['disallowed_blocks'][] = $name;
		}
		if ( ! empty( $block['innerBlocks'] ) ) {
			wpagent_site_editor_walk_blocks( $block['innerBlocks'], $report );
		}
	}
}

/** @return array{block_count:int,freeform_count:int,block_types:array<int,string>,disallowed_blocks:array<int,string>,has_disallowed_blocks:bool} */
function wpagent_site_editor_block_report( string $content ): array {
	$report = array( 'block_count' => 0, 'freeform_count' => 0, 'block_types' => array(), 'disallowed_blocks' => array() );
	wpagent_site_editor_walk_blocks( parse_blocks( $content ), $report );
	$report['block_types']       = array_values( array_unique( $report['block_types'] ) );
	$report['disallowed_blocks'] = array_values( array_unique( $report['disallowed_blocks'] ) );
	sort( $report['block_types'], SORT_STRING );
	sort( $report['disallowed_blocks'], SORT_STRING );
	$report['has_disallowed_blocks'] = array() !== $report['disallowed_blocks'];
	return $report;
}

/** @return array<int,string> */
function wpagent_site_editor_executable_shortcodes( string $content ): array {
	return wpagent_executable_shortcodes( $content );
}

function wpagent_site_editor_effective_post_content( WP_Post $post ): string {
	$content = (string) $post->post_content;
	return function_exists( 'apply_block_hooks_to_content_from_post_object' )
		? apply_block_hooks_to_content_from_post_object( $content, $post )
		: $content;
}

/** @return true|WP_Error */
function wpagent_site_editor_validate_content( string $content, array $visited = array(), int $depth = 0, bool $verify_references = true ) {
	if ( '' === trim( $content ) || strlen( $content ) > WPAGENT_SITE_EDITOR_MAX_CONTENT_BYTES ) {
		return new WP_Error( 'wpagent_site_editor_content_size', __( 'Der erzeugte Blockinhalt muss zwischen 1 Byte und 1 MiB groß sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( $depth > 12 ) {
		return new WP_Error( 'wpagent_site_editor_reference_depth', __( 'Die Site-Editor-Referenzen sind zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$shortcodes = wpagent_site_editor_executable_shortcodes( $content );
	if ( array() !== $shortcodes ) {
		return new WP_Error( 'wpagent_site_editor_shortcode', __( 'Der Site-Editor-Inhalt enthält einen ausführbaren Shortcode.', 'wp-agent-companion' ), array( 'status' => 400, 'shortcodes' => $shortcodes ) );
	}
	$report = wpagent_site_editor_block_report( $content );
	if ( $report['has_disallowed_blocks'] ) {
		return new WP_Error( 'wpagent_site_editor_disallowed_block', __( 'Der Site-Editor-Inhalt enthält einen nicht freigegebenen, nicht registrierten oder nicht nativen Block.', 'wp-agent-companion' ), array( 'status' => 400, 'blocks' => $report['disallowed_blocks'] ) );
	}
	if ( $verify_references ) {
		foreach ( parse_blocks( $content ) as $block ) {
			$check = wpagent_site_editor_validate_block_references( $block, $visited, $depth );
			if ( is_wp_error( $check ) ) {
				return $check;
			}
		}
	}
	return true;
}

/** @return array<string,bool> */
function wpagent_site_editor_validation_root( string $type, string $slug ): array {
	return 'wp_template_part' === $type ? array( 'part:' . $slug => true ) : array();
}

/** @return true|WP_Error */
function wpagent_site_editor_validate_block_references( array $block, array $visited, int $depth ) {
	$name  = (string) ( $block['blockName'] ?? '' );
	$attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
	$key   = '';
	$body  = '';
	if ( 'core/template-part' === $name ) {
		$slug = wpagent_site_editor_slug( $attrs['slug'] ?? '' );
		if ( is_wp_error( $slug ) ) {
			return $slug;
		}
		$part = wpagent_site_editor_get_template( 'wp_template_part', $slug );
		if ( is_wp_error( $part ) ) {
			return new WP_Error( 'wpagent_site_editor_part_reference', __( 'Ein referenziertes Template-Teil ist nicht vorhanden.', 'wp-agent-companion' ), array( 'status' => 400, 'slug' => $slug ) );
		}
		$key  = 'part:' . $slug;
		$body = (string) $part->content;
	} elseif ( 'core/navigation' === $name && isset( $attrs['ref'] ) ) {
		$post = get_post( absint( $attrs['ref'] ) );
		if ( ! $post instanceof WP_Post || 'wp_navigation' !== $post->post_type || 'publish' !== $post->post_status ) {
			return new WP_Error( 'wpagent_site_editor_navigation_reference', __( 'Eine referenzierte Blocknavigation ist nicht veröffentlicht oder nicht vorhanden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$key  = 'navigation:' . $post->ID;
		$body = wpagent_site_editor_effective_post_content( $post );
	} elseif ( 'core/block' === $name ) {
		$post = get_post( absint( $attrs['ref'] ?? 0 ) );
		if ( ! $post instanceof WP_Post || 'wp_block' !== $post->post_type || 'publish' !== $post->post_status || '' !== $post->post_password ) {
			return new WP_Error( 'wpagent_site_editor_synced_pattern_reference', __( 'Ein referenzierter synchronisierter Block ist nicht öffentlich verfügbar oder nicht vorhanden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$key  = 'block:' . $post->ID;
		$body = wpagent_site_editor_effective_post_content( $post );
	} elseif ( 'core/pattern' === $name ) {
		$slug = is_string( $attrs['slug'] ?? null ) ? $attrs['slug'] : '';
		$pattern = WP_Block_Patterns_Registry::get_instance()->get_registered( $slug );
		if ( ! is_array( $pattern ) || ! is_string( $pattern['content'] ?? null ) ) {
			return new WP_Error( 'wpagent_site_editor_pattern_reference', __( 'Ein referenziertes Blockmuster ist nicht registriert.', 'wp-agent-companion' ), array( 'status' => 400, 'slug' => $slug ) );
		}
		$key  = 'pattern:' . $slug;
		$body = $pattern['content'];
	}
	if ( '' !== $key && isset( $visited[ $key ] ) ) {
		return new WP_Error( 'wpagent_site_editor_reference_cycle', __( 'Der Site-Editor-Inhalt enthält eine zyklische Referenz.', 'wp-agent-companion' ), array( 'status' => 400, 'reference' => $key ) );
	}
	if ( '' !== $key ) {
		$visited[ $key ] = true;
		$check = wpagent_site_editor_validate_content( $body, $visited, $depth + 1 );
		if ( is_wp_error( $check ) ) {
			return $check;
		}
	}
	foreach ( $block['innerBlocks'] ?? array() as $inner ) {
		$check = wpagent_site_editor_validate_block_references( $inner, $visited, $depth );
		if ( is_wp_error( $check ) ) {
			return $check;
		}
	}
	return true;
}

function wpagent_site_editor_template_raw_content( WP_Block_Template $template ): string {
	if ( $template->wp_id ) {
		$post = get_post( $template->wp_id );
		if ( $post instanceof WP_Post ) {
			return $post->post_content;
		}
	}
	return (string) $template->content;
}

/** @return array<string,mixed> */
function wpagent_site_editor_resource_state( WP_Block_Template $template ): array {
	$content = wpagent_site_editor_template_raw_content( $template );
	$report  = wpagent_site_editor_block_report( $content );
	$state   = array(
		'id' => (string) $template->id, 'wp_id' => (int) $template->wp_id, 'theme' => (string) $template->theme,
		'slug' => (string) $template->slug, 'type' => (string) $template->type, 'source' => (string) $template->source,
		'origin' => empty( $template->origin ) ? null : (string) $template->origin, 'title' => (string) $template->title,
		'description' => (string) $template->description, 'status' => (string) $template->status, 'content' => $content,
		'area' => 'wp_template_part' === $template->type && ! empty( $template->area ) ? (string) $template->area : null,
		'has_theme_file' => (bool) $template->has_theme_file, 'is_custom' => (bool) $template->is_custom,
		'modified' => empty( $template->modified ) ? null : mysql_to_rfc3339( $template->modified ),
		'block_count' => $report['block_count'], 'freeform_count' => $report['freeform_count'], 'block_types' => $report['block_types'],
		'disallowed_blocks' => $report['disallowed_blocks'], 'has_disallowed_blocks' => $report['has_disallowed_blocks'],
	);
	$state['hash'] = hash( 'sha256', wp_json_encode( $state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
	return $state;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_site_editor_navigation_state( int $post_id ) {
	$post = get_post( $post_id );
	if ( ! $post instanceof WP_Post || 'wp_navigation' !== $post->post_type || 'trash' === $post->post_status ) {
		return new WP_Error( 'wpagent_site_editor_navigation_not_found', __( 'Blocknavigation nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	$report = wpagent_site_editor_block_report( $post->post_content );
	$item_count = 0;
	foreach ( parse_blocks( $post->post_content ) as $block ) {
		$item_count += wpagent_site_editor_navigation_item_count( $block );
	}
	$state = array(
		'id' => (int) $post->ID, 'slug' => (string) $post->post_name, 'title' => (string) $post->post_title,
		'status' => (string) $post->post_status, 'content' => (string) $post->post_content,
		'modified' => mysql_to_rfc3339( $post->post_modified ), 'item_count' => $item_count,
		'block_count' => $report['block_count'], 'freeform_count' => $report['freeform_count'], 'block_types' => $report['block_types'],
		'disallowed_blocks' => $report['disallowed_blocks'], 'has_disallowed_blocks' => $report['has_disallowed_blocks'],
	);
	$state['hash'] = hash( 'sha256', wp_json_encode( $state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
	return $state;
}

function wpagent_site_editor_navigation_item_count( array $block ): int {
	$count = in_array( $block['blockName'] ?? '', array( 'core/navigation-link', 'core/navigation-submenu', 'core/page-list' ), true ) ? 1 : 0;
	foreach ( $block['innerBlocks'] ?? array() as $inner ) {
		$count += wpagent_site_editor_navigation_item_count( $inner );
	}
	return $count;
}

function wpagent_site_editor_lock_name( string $kind, string $identity ): string {
	return 'wp_agent_site_editor_lock_' . hash( 'sha256', $kind . ':' . $identity );
}

/** @return string|WP_Error */
function wpagent_site_editor_acquire_lock( string $kind, string $identity ) {
	$option = wpagent_site_editor_lock_name( $kind, $identity );
	$token  = wpagent_companion_acquire_option_lock( $option, 120 );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_site_editor_locked', __( 'Diese Site-Editor-Ressource wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_site_editor_refresh_lock( string $kind, string $identity, string $token ): bool {
	return wpagent_companion_refresh_option_lock( wpagent_site_editor_lock_name( $kind, $identity ), $token, 120 );
}

function wpagent_site_editor_lock_is_owned( string $kind, string $identity, string $token ): bool {
	return wpagent_companion_option_lock_is_owned( wpagent_site_editor_lock_name( $kind, $identity ), $token );
}

function wpagent_site_editor_release_lock( string $kind, string $identity, string $token ): void {
	$option = wpagent_site_editor_lock_name( $kind, $identity );
	wpagent_companion_release_option_lock( $option, $token );
}

/** @return string|WP_Error */
function wpagent_site_editor_build_content( $components, string $container = 'none', bool $verify_references = true ) {
	if ( ! is_array( $components ) || ! array_is_list( $components ) || array() === $components || count( $components ) > WPAGENT_SITE_EDITOR_MAX_COMPONENTS ) {
		return new WP_Error( 'wpagent_site_editor_components', __( 'components muss 1-50 strukturierte Komponenten enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$parts = array();
	foreach ( $components as $component ) {
		if ( ! is_array( $component ) ) {
			return new WP_Error( 'wpagent_site_editor_component', __( 'Jede Komponente muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$part = wpagent_site_editor_build_component( $component, $verify_references );
		if ( is_wp_error( $part ) ) {
			return $part;
		}
		$parts[] = $part;
	}
	$content = implode( "\n\n", $parts );
	$allowed = array( 'none', 'group', 'main', 'header', 'footer' );
	if ( ! in_array( $container, $allowed, true ) ) {
		return new WP_Error( 'wpagent_site_editor_container', __( 'container ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'none' !== $container ) {
		$tag   = 'group' === $container ? 'div' : $container;
		$attrs = array( 'layout' => array( 'type' => 'constrained' ) );
		if ( 'div' !== $tag ) {
			$attrs['tagName'] = $tag;
		}
		$content = get_comment_delimited_block_content( 'core/group', $attrs, '<' . $tag . ' class="wp-block-group">' . "\n" . $content . "\n" . '</' . $tag . '>' );
	}
	$valid = wpagent_site_editor_validate_content( $content, array(), 0, $verify_references );
	return is_wp_error( $valid ) ? $valid : $content;
}

/** @return string|WP_Error */
function wpagent_site_editor_build_component( array $component, bool $verify_references = true ) {
	$type = is_string( $component['type'] ?? null ) ? $component['type'] : '';
	switch ( $type ) {
		case 'markdown':
			$markdown = is_string( $component['markdown'] ?? null ) ? $component['markdown'] : '';
			if ( '' === trim( $markdown ) || strlen( $markdown ) > 50000 ) {
				return new WP_Error( 'wpagent_site_editor_markdown', __( 'markdown muss 1-50.000 Bytes enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return ( new WPAgent_Markdown_To_Blocks() )->convert( $markdown );
		case 'template-part':
			$slug = wpagent_site_editor_slug( $component['slug'] ?? '' );
			if ( is_wp_error( $slug ) ) return $slug;
			if ( $verify_references && is_wp_error( wpagent_site_editor_get_template( 'wp_template_part', $slug ) ) ) {
				return new WP_Error( 'wpagent_site_editor_part_reference', __( 'Das referenzierte Template-Teil ist nicht vorhanden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return get_comment_delimited_block_content( 'core/template-part', array( 'slug' => $slug, 'theme' => get_stylesheet() ), '' );
		case 'pattern':
			$slug = is_string( $component['slug'] ?? null ) ? trim( $component['slug'] ) : '';
			if ( '' === $slug || ! WP_Block_Patterns_Registry::get_instance()->is_registered( $slug ) ) {
				return new WP_Error( 'wpagent_site_editor_pattern_reference', __( 'Das referenzierte Blockmuster ist nicht registriert.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return get_comment_delimited_block_content( 'core/pattern', array( 'slug' => $slug ), '' );
		case 'site-logo':
			$attrs = array();
			if ( isset( $component['width'] ) ) $attrs['width'] = max( 16, min( 600, absint( $component['width'] ) ) );
			return get_comment_delimited_block_content( 'core/site-logo', $attrs, '' );
		case 'site-title':
		case 'post-title':
			$attrs = array( 'level' => max( 1, min( 6, absint( $component['level'] ?? ( 'site-title' === $type ? 2 : 1 ) ) ) ) );
			if ( isset( $component['is_link'] ) ) $attrs['isLink'] = (bool) $component['is_link'];
			return get_comment_delimited_block_content( 'site-title' === $type ? 'core/site-title' : 'core/post-title', $attrs, '' );
		case 'navigation':
			$id   = absint( $component['navigation_id'] ?? 0 );
			$post = get_post( $id );
			if ( $verify_references && ( ! $post instanceof WP_Post || 'wp_navigation' !== $post->post_type || 'publish' !== $post->post_status ) ) {
				return new WP_Error( 'wpagent_site_editor_navigation_reference', __( 'navigation_id verweist nicht auf eine veröffentlichte Blocknavigation.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return get_comment_delimited_block_content( 'core/navigation', array( 'ref' => $id ), '' );
		case 'post-featured-image':
			$attrs = isset( $component['is_link'] ) ? array( 'isLink' => (bool) $component['is_link'] ) : array();
			return get_comment_delimited_block_content( 'core/post-featured-image', $attrs, '' );
		case 'post-content':
			return get_comment_delimited_block_content( 'core/post-content', array( 'layout' => array( 'type' => 'constrained' ) ), '' );
		case 'post-excerpt':
		case 'post-date':
		case 'post-author-name':
			$attrs = isset( $component['is_link'] ) ? array( 'isLink' => (bool) $component['is_link'] ) : array();
			$name  = array( 'post-excerpt' => 'core/post-excerpt', 'post-date' => 'core/post-date', 'post-author-name' => 'core/post-author-name' )[ $type ];
			return get_comment_delimited_block_content( $name, $attrs, '' );
		case 'post-terms':
			$taxonomy = is_string( $component['taxonomy'] ?? null ) ? sanitize_key( $component['taxonomy'] ) : 'category';
			if ( ! taxonomy_exists( $taxonomy ) ) {
				return new WP_Error( 'wpagent_site_editor_taxonomy', __( 'taxonomy ist nicht registriert.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			return get_comment_delimited_block_content( 'core/post-terms', array( 'term' => $taxonomy ), '' );
		case 'query-title':
			$query_type = in_array( $component['query_title_type'] ?? '', array( 'archive', 'search' ), true ) ? $component['query_title_type'] : 'archive';
			return get_comment_delimited_block_content( 'core/query-title', array( 'type' => $query_type, 'showPrefix' => false ), '' );
		case 'query-loop':
			return wpagent_site_editor_build_query_loop( $component );
		case 'separator':
			return get_comment_delimited_block_content( 'core/separator', array(), '<hr class="wp-block-separator has-alpha-channel-opacity"/>' );
		case 'spacer':
			$height = max( 0, min( 400, absint( $component['height'] ?? 40 ) ) );
			return get_comment_delimited_block_content( 'core/spacer', array( 'height' => $height . 'px' ), '<div style="height:' . $height . 'px" aria-hidden="true" class="wp-block-spacer"></div>' );
	}
	return new WP_Error( 'wpagent_site_editor_component_type', __( 'Unbekannter Site-Editor-Komponententyp.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return string|WP_Error */
function wpagent_site_editor_build_query_loop( array $component ) {
	$post_type = is_string( $component['post_type'] ?? null ) ? sanitize_key( $component['post_type'] ) : 'post';
	$object    = get_post_type_object( $post_type );
	if ( ! $object || ! $object->public ) {
		return new WP_Error( 'wpagent_site_editor_query_post_type', __( 'post_type muss ein öffentlicher Inhaltstyp sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$query = array(
		'perPage' => max( 1, min( 50, absint( $component['per_page'] ?? 10 ) ) ), 'pages' => 0, 'offset' => 0,
		'postType' => $post_type, 'order' => in_array( $component['order'] ?? '', array( 'asc', 'desc' ), true ) ? $component['order'] : 'desc',
		'orderBy' => in_array( $component['order_by'] ?? '', array( 'date', 'modified', 'title', 'menu_order' ), true ) ? $component['order_by'] : 'date',
		'author' => '', 'search' => '', 'exclude' => array(), 'sticky' => '', 'inherit' => ! empty( $component['inherit_query'] ), 'taxQuery' => null, 'parents' => array(),
	);
	$item_parts = array();
	if ( ! empty( $component['show_featured_image'] ) ) $item_parts[] = get_comment_delimited_block_content( 'core/post-featured-image', array( 'isLink' => true ), '' );
	$item_parts[] = get_comment_delimited_block_content( 'core/post-title', array( 'isLink' => true ), '' );
	if ( ! isset( $component['show_excerpt'] ) || $component['show_excerpt'] ) $item_parts[] = get_comment_delimited_block_content( 'core/post-excerpt', array(), '' );
	if ( ! empty( $component['show_date'] ) ) $item_parts[] = get_comment_delimited_block_content( 'core/post-date', array( 'isLink' => true ), '' );
	$post_template = get_comment_delimited_block_content( 'core/post-template', array(), implode( "\n", $item_parts ) );
	$pagination = get_comment_delimited_block_content( 'core/query-pagination', array( 'layout' => array( 'type' => 'flex', 'justifyContent' => 'space-between' ) ), implode( "\n", array(
		get_comment_delimited_block_content( 'core/query-pagination-previous', array(), '' ),
		get_comment_delimited_block_content( 'core/query-pagination-numbers', array(), '' ),
		get_comment_delimited_block_content( 'core/query-pagination-next', array(), '' ),
	) ) );
	$no_results = is_string( $component['no_results_text'] ?? null ) ? trim( $component['no_results_text'] ) : __( 'Es wurden keine Inhalte gefunden.', 'wp-agent-companion' );
	if ( '' === $no_results || wpagent_site_editor_text_length( $no_results ) > 500 ) {
		return new WP_Error( 'wpagent_site_editor_no_results', __( 'no_results_text muss 1-500 Zeichen enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$paragraph  = get_comment_delimited_block_content( 'core/paragraph', array(), '<p>' . esc_html( $no_results ) . '</p>' );
	$no_results = get_comment_delimited_block_content( 'core/query-no-results', array(), $paragraph );
	$inner      = implode( "\n", array( $post_template, $pagination, $no_results ) );
	return get_comment_delimited_block_content( 'core/query', array( 'query' => $query, 'layout' => array( 'type' => 'default' ) ), '<div class="wp-block-query">' . "\n" . $inner . "\n</div>" );
}

/** @return string|WP_Error */
function wpagent_site_editor_build_navigation_content( $items ) {
	if ( ! is_array( $items ) || ! array_is_list( $items ) || array() === $items ) {
		return new WP_Error( 'wpagent_site_editor_navigation_items', __( 'items muss eine nicht leere Liste sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$count = 0;
	$content = wpagent_site_editor_build_navigation_items( $items, 0, $count );
	if ( is_wp_error( $content ) ) return $content;
	if ( $count > WPAGENT_SITE_EDITOR_MAX_NAV_ITEMS ) {
		return new WP_Error( 'wpagent_site_editor_navigation_limit', __( 'Eine Blocknavigation darf höchstens 100 Einträge enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$valid = wpagent_site_editor_validate_content( $content );
	return is_wp_error( $valid ) ? $valid : $content;
}

/** @return string|WP_Error */
function wpagent_site_editor_build_navigation_items( array $items, int $depth, int &$count ) {
	if ( $depth > 2 ) {
		return new WP_Error( 'wpagent_site_editor_navigation_depth', __( 'Untermenüs dürfen höchstens drei Ebenen tief sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$blocks = array();
	foreach ( $items as $item ) {
		if ( ! is_array( $item ) ) return new WP_Error( 'wpagent_site_editor_navigation_item', __( 'Jeder Navigationseintrag muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		++$count;
		$type  = is_string( $item['type'] ?? null ) ? $item['type'] : '';
		$attrs = wpagent_site_editor_navigation_attributes( $item );
		if ( is_wp_error( $attrs ) ) return $attrs;
		if ( 'submenu' === $type ) {
			$children = $item['items'] ?? null;
			if ( ! is_array( $children ) || array() === $children ) return new WP_Error( 'wpagent_site_editor_submenu_items', __( 'Ein Untermenü benötigt mindestens einen Eintrag.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			$inner = wpagent_site_editor_build_navigation_items( $children, $depth + 1, $count );
			if ( is_wp_error( $inner ) ) return $inner;
			$blocks[] = get_comment_delimited_block_content( 'core/navigation-submenu', $attrs, $inner );
		} else {
			$blocks[] = get_comment_delimited_block_content( 'core/navigation-link', $attrs, '' );
		}
	}
	return implode( "\n", $blocks );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_site_editor_navigation_attributes( array $item ) {
	$type  = is_string( $item['type'] ?? null ) ? $item['type'] : '';
	$label = wpagent_site_editor_plain_text( $item['label'] ?? '', 'label', 200, in_array( $type, array( 'home', 'page', 'post', 'category', 'tag' ), true ) );
	if ( is_wp_error( $label ) ) return $label;
	$url = '';
	$kind = 'custom';
	$entity_type = 'custom';
	$id = absint( $item['id'] ?? 0 );
	if ( 'home' === $type ) {
		$url = home_url( '/' );
		if ( '' === $label ) $label = get_bloginfo( 'name' );
	} elseif ( in_array( $type, array( 'page', 'post' ), true ) ) {
		$post = get_post( $id );
		if ( ! $post instanceof WP_Post || $type !== $post->post_type || 'publish' !== $post->post_status ) return new WP_Error( 'wpagent_site_editor_navigation_post', __( 'Der verknüpfte Beitrag ist nicht veröffentlicht oder hat den falschen Inhaltstyp.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		$url = get_permalink( $post ); $kind = 'post-type'; $entity_type = $type;
		if ( '' === $label ) $label = get_the_title( $post );
	} elseif ( in_array( $type, array( 'category', 'tag' ), true ) ) {
		$taxonomy = 'category' === $type ? 'category' : 'post_tag';
		$term = get_term( $id, $taxonomy );
		if ( ! $term instanceof WP_Term ) return new WP_Error( 'wpagent_site_editor_navigation_term', __( 'Der verknüpfte Begriff ist nicht vorhanden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		$url = get_term_link( $term );
		if ( is_wp_error( $url ) ) return $url;
		$kind = 'taxonomy'; $entity_type = $taxonomy;
		if ( '' === $label ) $label = $term->name;
	} elseif ( in_array( $type, array( 'custom', 'submenu' ), true ) ) {
		$url = wpagent_site_editor_navigation_url( $item['url'] ?? ( 'submenu' === $type ? '#' : '' ) );
		if ( is_wp_error( $url ) ) return $url;
	} else {
		return new WP_Error( 'wpagent_site_editor_navigation_type', __( 'Unbekannter Navigationseintragstyp.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$attrs = array( 'label' => $label, 'url' => $url, 'kind' => $kind, 'type' => $entity_type );
	if ( $id > 0 && 'custom' !== $kind ) $attrs['id'] = $id;
	if ( ! empty( $item['opens_in_new_tab'] ) ) $attrs['opensInNewTab'] = true;
	if ( isset( $item['description'] ) ) {
		$description = wpagent_site_editor_plain_text( $item['description'], 'description', 500, true );
		if ( is_wp_error( $description ) ) return $description;
		if ( '' !== $description ) $attrs['description'] = $description;
	}
	return $attrs;
}

/** @return string|WP_Error */
function wpagent_site_editor_navigation_url( $value ) {
	$url = is_string( $value ) ? trim( $value ) : '';
	if ( '' === $url || strlen( $url ) > 2048 || str_contains( $url, "\0" ) || str_starts_with( $url, '//' ) ) {
		return new WP_Error( 'wpagent_site_editor_navigation_url', __( 'url ist leer, zu lang oder unsicher.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( str_starts_with( $url, '/' ) || str_starts_with( $url, '#' ) || str_starts_with( $url, '?' ) ) {
		return esc_url_raw( $url, array( 'http', 'https' ) );
	}
	$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
	if ( ! in_array( $scheme, array( 'http', 'https', 'mailto', 'tel' ), true ) ) {
		return new WP_Error( 'wpagent_site_editor_navigation_scheme', __( 'url verwendet kein erlaubtes Protokoll.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$clean = esc_url_raw( $url, array( 'http', 'https', 'mailto', 'tel' ) );
	return '' === $clean ? new WP_Error( 'wpagent_site_editor_navigation_url', __( 'url ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) ) : $clean;
}

/** @return array{post_id:int,post_type:string,title:string} */
function wpagent_site_editor_reference_item( int $post_id, string $post_type, string $title ): array {
	$title = wp_strip_all_tags( $title, true );
	if ( function_exists( 'mb_strcut' ) ) {
		$title = (string) mb_strcut( $title, 0, 500, 'UTF-8' );
	} else {
		$title = substr( $title, 0, 500 );
	}
	return array( 'post_id' => max( 0, $post_id ), 'post_type' => substr( sanitize_key( $post_type ), 0, 20 ), 'title' => $title );
}

/** @return array{items:array<int,array{post_id:int,post_type:string,title:string}>,truncated:bool}|WP_Error */
function wpagent_site_editor_find_references( string $kind, string $identity ): array|WP_Error {
	global $wpdb;
	$marker  = 'navigation' === $kind ? '<!-- wp:navigation' : '<!-- wp:template-part';
	$items = array();
	$last_id = 0;
	do {
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT ID, post_type, post_title, post_content FROM {$wpdb->posts} WHERE ID > %d AND post_status NOT IN ('trash','auto-draft','inherit') AND post_content LIKE %s ORDER BY ID ASC LIMIT 500",
				$last_id,
				'%' . $wpdb->esc_like( $marker ) . '%'
			)
		);
		if ( ! is_array( $rows ) ) {
			return new WP_Error( 'wpagent_site_editor_reference_query', __( 'Die Site-Editor-Referenzen konnten nicht vollständig gelesen werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
		}
		foreach ( $rows as $row ) {
			$last_id = (int) $row->ID;
			if ( wpagent_site_editor_content_references( (string) $row->post_content, $kind, $identity ) ) {
				$items[] = wpagent_site_editor_reference_item( (int) $row->ID, (string) $row->post_type, (string) $row->post_title );
			}
		}
	} while ( 500 === count( $rows ) );
	$seen = array_fill_keys( array_map( static fn( array $item ): string => $item['post_type'] . ':' . $item['post_id'], $items ), true );
	foreach ( array( 'wp_template', 'wp_template_part' ) as $template_type ) {
		foreach ( get_block_templates( array(), $template_type ) as $template ) {
			$key = $template_type . ':' . (int) $template->wp_id;
			if ( $template->wp_id && isset( $seen[ $key ] ) ) continue;
			if ( wpagent_site_editor_content_references( (string) $template->content, $kind, $identity ) ) {
				$items[] = wpagent_site_editor_reference_item( (int) $template->wp_id, $template_type, (string) ( $template->title ?: $template->slug ) );
				$seen[ $key ] = true;
			}
		}
	}
	return array( 'items' => array_slice( $items, 0, 100 ), 'truncated' => count( $items ) > 100 );
}

function wpagent_site_editor_content_references( string $content, string $kind, string $identity ): bool {
	foreach ( parse_blocks( $content ) as $block ) {
		if ( wpagent_site_editor_block_references( $block, $kind, $identity ) ) return true;
	}
	return false;
}

function wpagent_site_editor_block_references( array $block, string $kind, string $identity ): bool {
	$attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
	if ( 'navigation' === $kind && 'core/navigation' === ( $block['blockName'] ?? '' ) && absint( $attrs['ref'] ?? 0 ) === absint( $identity ) ) return true;
	if ( 'template_part' === $kind && 'core/template-part' === ( $block['blockName'] ?? '' ) && (string) ( $attrs['slug'] ?? '' ) === $identity ) return true;
	foreach ( $block['innerBlocks'] ?? array() as $inner ) {
		if ( wpagent_site_editor_block_references( $inner, $kind, $identity ) ) return true;
	}
	return false;
}
