<?php
/**
 * Ability: wp-agent/detect-builder
 *
 * Read-only. Erkennt pro Beitrag, mit welchem Page-Builder/Format sein Inhalt
 * tatsächlich gespeichert ist, statt nur zu prüfen, welche Builder-Plugins
 * aktiv sind (is_plugin_active() allein sagt nichts darüber, WIE ein
 * konkreter Post gespeichert wurde). Grundlage: die drei Speicher-Familien
 * aus dem Kompatibilitäts-Bauplan (docs/strategy/developer-mode-plan-2026-07.md,
 * Punkt 2) — JSON in postmeta (Elementor, Bricks, Breakdance, Oxygen),
 * Gutenberg-Block-Kommentare in post_content (Core, Kadence, Spectra,
 * GenerateBlocks) und Shortcodes (WPBakery). Introspektion vor Hardcoding:
 * jeder postmeta-Schlüssel wird per metadata_exists()/get_post_meta() live
 * geprüft, nie nur angenommen.
 *
 * Kaskade bei mehreren postmeta-Signalen (sie können sich überlappen):
 * Bricks vor Oxygen vor Breakdance vor Elementor. Block-, Shortcode- und
 * post_type-Signale sind schwächer als ein gefundener postmeta-Builder-
 * Schlüssel; ACF ist immer nur additiv (eigenes Signal, nie primary),
 * außer der Beitrag ist selbst eine ACF-Feldgruppe.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_detect_builder(): array {
	$identifier = array(
		'type'      => 'string',
		'minLength' => 1,
		'maxLength' => 128,
		'pattern'   => '^[A-Za-z0-9][A-Za-z0-9._:/-]*$',
	);
	$signal     = array(
		'type'                 => 'object',
		'additionalProperties' => false,
		'properties'           => array(
			'type'     => array( 'type' => 'string', 'enum' => array( 'postmeta', 'shortcode', 'executable-shortcode', 'block', 'post_type', 'acf' ) ),
			'key'      => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 128, 'pattern' => '^[A-Za-z0-9_][A-Za-z0-9._:/-]*$' ),
			'location' => array( 'type' => 'string', 'enum' => array( 'postmeta', 'post_content', 'post_type' ) ),
		),
		'required'             => array( 'type', 'key', 'location' ),
	);
	$builder_ids = array( 'bricks', 'beaver-builder', 'oxygen', 'breakdance', 'elementor', 'wpbakery', 'acf', 'kadence', 'spectra', 'spectra-blocks', 'generateblocks', 'gutenberg-blocks', 'woocommerce-product', 'classic' );
	return array(
		'label'               => __( 'Builder erkennen', 'wp-agent-companion' ),
		'description'         => __( 'Erkennt pro Beitrag den maßgeblichen Page-Builder/Format anhand des tatsächlichen Speicherschlüssels (postmeta-JSON, Block-Kommentare, Shortcodes, Post-Type), nicht nur anhand aktiver Plugins.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'post_id' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 2147483647 ),
			),
			'required'   => array( 'post_id' ),
			'additionalProperties' => false,
		),
		'output_schema'       => array(
			'type'       => 'object',
			'additionalProperties' => false,
			'properties' => array(
				'primary'          => array( 'type' => 'string', 'enum' => $builder_ids ),
				'signals'          => array(
					'type'  => 'array',
					'maxItems' => 64,
					'items' => $signal,
				),
				'storage_location'             => array( 'type' => 'string', 'enum' => array( 'postmeta', 'post_content', 'gemischt' ) ),
				'is_block_based'               => array( 'type' => 'boolean' ),
				'write_supported'              => array( 'type' => 'boolean' ),
				'recommended_ability'          => array( 'type' => 'string', 'enum' => array( '', 'wp-agent/update-post', 'wp-agent/update-beaver-node', 'wp-agent/update-spectra-block-attributes', 'wp-agent/update-spectra-static-heading-alignment', 'wp-agent/update-spectra-blocks-separator', 'wp-agent/update-generateblocks-block-attributes', 'wp-agent/update-kadence-block-attributes' ) ),
				'unmanaged_block_namespaces'   => array(
					'type'  => 'array',
					'maxItems' => 32,
					'items' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64, 'pattern' => '^[a-z0-9][a-z0-9-]*$' ),
				),
				'executable_shortcodes'        => array(
					'type'  => 'array',
					'maxItems' => 32,
					'items' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64, 'pattern' => '^[A-Za-z0-9_-]+$' ),
				),
				'block_types'                  => array(
					'type'  => 'array',
					'maxItems' => 128,
					'items' => $identifier,
				),
				'notes'                        => array(
					'type'  => 'array',
					'maxItems' => 64,
					'items' => array( 'type' => 'string', 'maxLength' => 1024 ),
				),
			),
			'required'   => array( 'primary', 'signals', 'storage_location', 'is_block_based', 'write_supported', 'recommended_ability', 'unmanaged_block_namespaces', 'executable_shortcodes', 'block_types', 'notes' ),
		),
		'execute_callback'    => 'wpagent_exec_detect_builder',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => true,
				'destructive' => false,
				'idempotent'  => true,
			),
		),
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_detect_builder( $input ) {
	$post_id = wpagent_builder_read_post_id_from_input( $input );
	if ( is_wp_error( $post_id ) ) {
		return $post_id;
	}
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$content_valid = wpagent_builder_read_validate_post_content( $post->post_content );
	if ( is_wp_error( $content_valid ) ) {
		return $content_valid;
	}

	$signals = array();
	$notes   = array();

	// 1. JSON-/Shortcode-Builder in postmeta. Kaskade bei Mehrfachtreffern:
	// Bricks vor Beaver Builder vor Oxygen vor Breakdance vor Elementor.
	$postmeta_families = array(
		'bricks'         => array( '_bricks_page_content_2' ),
		'beaver-builder' => array( '_fl_builder_data' ),
		'oxygen'         => array( 'ct_builder_json', 'ct_builder_shortcodes' ),
		'breakdance'     => array( '_breakdance_data' ),
		'elementor'      => array( '_elementor_data' ),
	);

	$postmeta_primary = '';
	foreach ( $postmeta_families as $family => $keys ) {
		foreach ( $keys as $key ) {
			if ( ! wpagent_detect_builder_meta_signal_present( $post_id, $key ) ) {
				continue;
			}
			$signals[] = array(
				'type'     => 'postmeta',
				'key'      => $key,
				'location' => 'postmeta',
			);
			if ( '' === $postmeta_primary ) {
				$postmeta_primary = $family;
			}
		}
	}

	// 2. WPBakery-Shortcodes in post_content (dritte Speicher-Familie neben
	// postmeta-JSON und Block-Kommentaren). Nicht has_shortcode() verwenden:
	// das hängt an registrierten Shortcode-Providern und übersieht gespeicherte
	// vc_-Shortcodes, wenn WPBakery inaktiv ist.
	$wpbakery_tag   = wpagent_detect_builder_wpbakery_shortcode_tag( $post->post_content );
	$wpbakery_found = null !== $wpbakery_tag;
	if ( $wpbakery_found ) {
		$signals[] = array(
			'type'     => 'shortcode',
			'key'      => $wpbakery_tag,
			'location' => 'post_content',
		);
	}
	if ( $wpbakery_found && wpagent_detect_builder_meta_signal_present( $post_id, '_wpb_shortcodes_custom_css' ) ) {
		$signals[] = array(
			'type'     => 'postmeta',
			'key'      => '_wpb_shortcodes_custom_css',
			'location' => 'postmeta',
		);
	}
	$executable_shortcodes = wpagent_executable_shortcodes( $post->post_content );
	foreach ( $executable_shortcodes as $shortcode ) {
		$signals[] = array(
			'type'     => 'executable-shortcode',
			'key'      => $shortcode,
			'location' => 'post_content',
		);
	}
	if ( array() !== $executable_shortcodes ) {
		$notes[] = __( 'Aktiv registrierte Shortcodes im Inhalt werden bei generischen Inhaltsänderungen nicht überschrieben.', 'wp-agent-companion' );
	}

	// 3. Block-basiert (Gutenberg-Block-Kommentare in post_content).
	$report         = wpagent_block_report( $post->post_content );
	$non_freeform   = $report['block_count'] - $report['freeform_count'];
	$is_block_based = $non_freeform > 0;
	$families       = wpagent_detect_builder_block_families( $post->post_content );

	foreach ( array( 'kadence', 'spectra', 'spectra-blocks', 'generateblocks' ) as $family ) {
		if ( $families[ $family ] > 0 ) {
			$signals[] = array(
				'type'     => 'block',
				'key'      => $family,
				'location' => 'post_content',
			);
		}
	}
	if ( $families['core'] > 0 ) {
		$signals[] = array(
			'type'     => 'block',
			'key'      => 'core',
			'location' => 'post_content',
		);
	}
	foreach ( $families['other'] as $namespace => $count ) {
		$signals[] = array(
			'type'     => 'block',
			'key'      => $namespace . '/',
			'location' => 'post_content',
		);
		$notes[] = sprintf(
			/* translators: 1: Block-Namespace, 2: Anzahl Vorkommen */
			__( 'Unbekannter Block-Namespace "%1$s/" (%2$d Vorkommen) ist keiner bekannten Familie zugeordnet.', 'wp-agent-companion' ),
			$namespace,
			$count
		);
	}
	$unmanaged_block_namespaces = array_keys( $families['other'] );
	sort( $unmanaged_block_namespaces, SORT_STRING );

	$builder_block_counts = array(
		'kadence'        => $families['kadence'],
		'spectra'        => $families['spectra'],
		'spectra-blocks' => $families['spectra-blocks'],
		'generateblocks' => $families['generateblocks'],
	);
	arsort( $builder_block_counts );
	$top_block_family = array_key_first( $builder_block_counts );
	$block_primary     = ( $builder_block_counts[ $top_block_family ] > 0 ) ? $top_block_family : 'gutenberg-blocks';

	// 4. post_type-basiert.
	$post_type_signal = '';
	if ( 'product' === $post->post_type ) {
		$post_type_signal = 'woocommerce-product';
		$signals[]         = array(
			'type'     => 'post_type',
			'key'      => 'product',
			'location' => 'post_type',
		);
	} elseif ( 'wpcf7_contact_form' === $post->post_type ) {
		$signals[] = array(
			'type'     => 'post_type',
			'key'      => 'wpcf7_contact_form',
			'location' => 'post_type',
		);
		$notes[] = __( 'Contact Form 7: Die Formularkonfiguration liegt in postmeta ("_form"), post_content ist bei CF7 kein wirksamer Inhalt.', 'wp-agent-companion' );
	}

	// 5. ACF, immer additiv, außer der Beitrag ist selbst eine Feldgruppe.
	$acf_meta_key = wpagent_detect_builder_acf_meta_key( $post_id );
	if ( null !== $acf_meta_key ) {
		$signals[] = array(
			'type'     => 'acf',
			'key'      => $acf_meta_key,
			'location' => 'postmeta',
		);
	}
	$is_acf_field_group = ( 'acf-field-group' === $post->post_type );
	if ( $is_acf_field_group ) {
		$signals[] = array(
			'type'     => 'post_type',
			'key'      => 'acf-field-group',
			'location' => 'post_type',
		);
	}

	// Kaskade für primary: postmeta-Builder > WPBakery-Shortcodes >
	// ACF-Feldgruppe (Post-Type selbst) > Block-basiert > post_type > Classic.
	if ( '' !== $postmeta_primary ) {
		$primary = $postmeta_primary;
	} elseif ( $wpbakery_found ) {
		$primary = 'wpbakery';
	} elseif ( $is_acf_field_group ) {
		$primary = 'acf';
	} elseif ( $is_block_based ) {
		$primary = $block_primary;
	} elseif ( '' !== $post_type_signal ) {
		$primary = $post_type_signal;
	} else {
		$primary = 'classic';
	}

	// storage_location spiegelt, WO Builder-relevante Daten tatsächlich
	// liegen — unabhängig davon, welches Signal die Kaskade oben gewinnt.
	$has_postmeta_signal = wpagent_detect_builder_has_signal_location( $signals, 'postmeta' );
	$has_content_signal  = wpagent_detect_builder_has_signal_location( $signals, 'post_content' );
	if ( $has_postmeta_signal && $has_content_signal ) {
		$storage_location = 'gemischt';
	} elseif ( $has_postmeta_signal ) {
		$storage_location = 'postmeta';
	} else {
		$storage_location = 'post_content';
	}

	$write_supported = wpagent_builder_content_write_supported(
		array(
			'primary'                    => $primary,
			'unmanaged_block_namespaces' => $unmanaged_block_namespaces,
			'executable_shortcodes'       => $executable_shortcodes,
		),
		$post->post_type
	);
	$recommended_ability = $write_supported ? 'wp-agent/update-post' : '';
	if ( 'beaver-builder' === $primary ) {
		$recommended_ability = 'wp-agent/update-beaver-node';
	} elseif ( 'spectra' === $primary && function_exists( 'wpagent_native_block_has_patchable_spectra_block' )
		&& wpagent_native_block_has_patchable_spectra_block( parse_blocks( $post->post_content ) ) ) {
		$write_supported = true;
		$recommended_ability = 'wp-agent/update-spectra-block-attributes';
	} elseif ( 'spectra' === $primary && function_exists( 'wpagent_spectra_static_advanced_heading_has_patchable_block' )
		&& wpagent_spectra_static_advanced_heading_has_patchable_block( parse_blocks( $post->post_content ) ) ) {
		$write_supported = true;
		$recommended_ability = 'wp-agent/update-spectra-static-heading-alignment';
	} elseif ( 'spectra-blocks' === $primary && function_exists( 'wpagent_spectra_blocks_separator_has_patchable_block' )
		&& wpagent_spectra_blocks_separator_has_patchable_block( parse_blocks( $post->post_content ) ) ) {
		$write_supported = true;
		$recommended_ability = 'wp-agent/update-spectra-blocks-separator';
	} elseif ( 'generateblocks' === $primary && function_exists( 'wpagent_builder_block_has_patchable_provider_block' )
		&& wpagent_builder_block_has_patchable_provider_block( parse_blocks( $post->post_content ), 'generateblocks' ) ) {
		$write_supported = true;
		$recommended_ability = 'wp-agent/update-generateblocks-block-attributes';
	} elseif ( 'kadence' === $primary && function_exists( 'wpagent_builder_block_has_patchable_provider_block' )
		&& wpagent_builder_block_has_patchable_provider_block( parse_blocks( $post->post_content ), 'kadence' ) ) {
		$write_supported = true;
		$recommended_ability = 'wp-agent/update-kadence-block-attributes';
	}
	$result = array(
		'primary'          => $primary,
		'signals'          => $signals,
		'storage_location' => $storage_location,
		'is_block_based'   => $is_block_based,
		'write_supported'  => $write_supported,
		'recommended_ability' => $recommended_ability,
		'unmanaged_block_namespaces' => $unmanaged_block_namespaces,
		'executable_shortcodes' => $executable_shortcodes,
		'block_types'      => $is_block_based ? $report['block_types'] : array(),
		'notes'            => $notes,
	);
	$output_valid = wpagent_builder_read_validate_detection_output( $result );
	return is_wp_error( $output_valid ) ? $output_valid : $result;
}

/**
 * Ob post_content der wirksame, von WPAgently sicher beschreibbare Speicher ist.
 * Unbekannte Builder-Daten werden fail-closed behandelt.
 *
 * @param array<string,mixed> $detection
 */
function wpagent_builder_content_write_supported( array $detection, string $post_type ): bool {
	if ( 'wpcf7_contact_form' === $post_type || 'acf-field-group' === $post_type ) {
		return false;
	}
	if ( ! empty( $detection['unmanaged_block_namespaces'] ) || ! empty( $detection['executable_shortcodes'] ) ) {
		return false;
	}
	return in_array(
		(string) ( $detection['primary'] ?? '' ),
		array( 'classic', 'gutenberg-blocks', 'woocommerce-product' ),
		true
	);
}

/** @return WP_Error */
function wpagent_builder_content_write_error( array $detection ): WP_Error {
	$builder = (string) ( $detection['primary'] ?? 'unknown' );
	$unmanaged_block_namespaces = is_array( $detection['unmanaged_block_namespaces'] ?? null ) ? $detection['unmanaged_block_namespaces'] : array();
	$executable_shortcodes      = is_array( $detection['executable_shortcodes'] ?? null ) ? $detection['executable_shortcodes'] : array();
	if ( array() !== $unmanaged_block_namespaces || array() !== $executable_shortcodes ) {
		return new WP_Error(
			'wpagent_builder_storage_mismatch',
			__( 'Der Inhalt enthält nicht verwaltete Block-Namensräume oder ausführbare Shortcodes. content_markdown wird nicht geschrieben, damit fremde Builder-Daten und dynamischer Inhalt unverändert bleiben.', 'wp-agent-companion' ),
			array(
				'status'                     => 409,
				'builder'                    => $builder,
				'unmanaged_block_namespaces' => $unmanaged_block_namespaces,
				'executable_shortcodes'       => $executable_shortcodes,
			)
		);
	}
	return new WP_Error(
		'wpagent_builder_storage_mismatch',
		sprintf(
			/* translators: %s: detected page builder name. */
			__( 'Der Inhalt wird von „%s“ außerhalb des normalen WordPress-Inhaltsfelds verwaltet. content_markdown wird nicht geschrieben, weil die sichtbare Seite sonst unverändert bliebe oder Builder-Daten auseinanderlaufen könnten.', 'wp-agent-companion' ),
			$builder
		),
		array( 'status' => 409, 'builder' => $builder )
	);
}

/**
 * Erkennt gespeicherte WPBakery-Shortcodes direkt im Rohinhalt.
 *
 * WordPress' has_shortcode() ist hier ungeeignet, weil es zuerst
 * shortcode_exists() prüft. Bei inaktivem WPBakery-Plugin bleibt der gespeicherte
 * [vc_*]-Inhalt trotzdem ein Builder-Signal.
 *
 * @param string $content
 * @return string|null Der erste gefundene vc_-Shortcode-Tag oder null.
 */
function wpagent_detect_builder_wpbakery_shortcode_tag( string $content ): ?string {
	if ( '' === $content || ( ! str_contains( $content, '[vc_' ) && ! str_contains( $content, '[/vc_' ) ) ) {
		return null;
	}

	if ( ! preg_match( '/(?<!\[)\[(?!\[)\/?(vc_[A-Za-z0-9_-]+)(?![\w-])(?=[\s\]\/])/', $content, $matches ) ) {
		return null;
	}

	return $matches[1];
}

/**
 * @param array<int,array<string,string>> $signals
 * @param string                          $location
 * @return bool
 */
function wpagent_detect_builder_has_signal_location( array $signals, string $location ): bool {
	foreach ( $signals as $signal ) {
		if ( $location === ( $signal['location'] ?? '' ) ) {
			return true;
		}
	}

	return false;
}

/**
 * Introspektiv geprüft: der Meta-Schlüssel muss nicht nur existieren
 * (metadata_exists), sein Wert muss auch tatsächlich Inhalt tragen. Ein
 * angelegter, aber leerer Schlüssel (leerer String, leeres Array, leer
 * serialisiert) ist kein echtes Builder-Signal.
 *
 * @param int    $post_id
 * @param string $key
 * @return bool
 */
function wpagent_detect_builder_meta_signal_present( int $post_id, string $key ): bool {
	if ( ! metadata_exists( 'post', $post_id, $key ) ) {
		return false;
	}

	$value = get_post_meta( $post_id, $key, true );

	if ( is_array( $value ) ) {
		return array() !== $value;
	}

	if ( ! is_string( $value ) ) {
		return null !== $value && false !== $value;
	}

	$trimmed = trim( $value );
	if ( '' === $trimmed || 'a:0:{}' === $trimmed || '[]' === $trimmed || '{}' === $trimmed ) {
		return false;
	}

	return true;
}

/**
 * Zählt Blöcke pro bekannter Familie (Kaskaden-Grundlage für die
 * Block-basierte Primary-Wahl). Verschachtelte Drittanbieter-Blöcke zählen
 * ebenfalls, damit ihre Cache-Invalidierung nicht wegen eines äußeren
 * Core-Containers ausbleibt.
 *
 * @param string $content Serialisiertes Block-Markup (post_content).
 * @return array{kadence:int,spectra:int,spectra-blocks:int,generateblocks:int,core:int,other:array<string,int>}
 */
function wpagent_detect_builder_block_families( string $content ): array {
	$counts = array(
		'kadence'        => 0,
		'spectra'        => 0,
		'spectra-blocks' => 0,
		'generateblocks' => 0,
		'core'           => 0,
		'other'          => array(),
	);

	wpagent_detect_builder_walk_block_families( parse_blocks( $content ), $counts );

	return $counts;
}

/**
 * @param array<int,array<string,mixed>> $blocks
 * @param array{kadence:int,spectra:int,spectra-blocks:int,generateblocks:int,core:int,other:array<string,int>} $counts
 */
function wpagent_detect_builder_walk_block_families( array $blocks, array &$counts ): void {
	foreach ( $blocks as $block ) {
		$name = $block['blockName'];
		if ( null === $name ) {
			// Freeform-Anteile sind bereits über wpagent_block_report erfasst.
			continue;
		}

		if ( str_starts_with( $name, 'kadence/' ) ) {
			$counts['kadence']++;
		} elseif ( str_starts_with( $name, 'uagb/' ) ) {
			$counts['spectra']++;
		} elseif ( str_starts_with( $name, 'spectra/' ) ) {
			$counts['spectra-blocks']++;
		} elseif ( str_starts_with( $name, 'generateblocks/' ) ) {
			$counts['generateblocks']++;
		} elseif ( str_starts_with( $name, 'core/' ) ) {
			$counts['core']++;
		} else {
			$namespace                     = strtok( $name, '/' );
			$counts['other'][ $namespace ] = ( $counts['other'][ $namespace ] ?? 0 ) + 1;
		}

		if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
			wpagent_detect_builder_walk_block_families( $block['innerBlocks'], $counts );
		}
	}
}

/**
 * Sucht ein additives ACF-Signal: einen postmeta-Schlüssel mit führendem
 * Unterstrich, dessen Wert einem ACF-Field-Key entspricht (Muster
 * "field_..."). ACF legt bei jedem gespeicherten Feld genau so ein
 * Schatten-Meta an, das auf die Feldgruppen-Definition verweist — ein
 * zuverlässiges Signal, ohne die ACF-Klassen selbst laden zu müssen.
 *
 * @param int $post_id
 * @return string|null Der gefundene Meta-Schlüssel oder null.
 */
function wpagent_detect_builder_acf_meta_key( int $post_id ): ?string {
	$all_meta = get_post_meta( $post_id );
	if ( ! is_array( $all_meta ) ) {
		return null;
	}

	foreach ( $all_meta as $key => $values ) {
		if ( ! is_string( $key ) || ! str_starts_with( $key, '_' ) || empty( $values ) ) {
			continue;
		}
		$value = is_array( $values ) ? reset( $values ) : $values;
		if ( is_string( $value ) && str_starts_with( $value, 'field_' ) ) {
			return $key;
		}
	}

	return null;
}

const WPAGENT_BUILDER_READ_MAX_POST_CONTENT_BYTES  = 2097152;
const WPAGENT_BUILDER_READ_MAX_RENDERED_HTML_BYTES = 5242880;
const WPAGENT_BUILDER_READ_MAX_BLOCKS              = 10000;

/** @return int|WP_Error */
function wpagent_builder_read_post_id_from_input( $input ) {
	if ( ! is_array( $input ) || array( 'post_id' ) !== array_keys( $input ) ) {
		return new WP_Error( 'wpagent_builder_bad_input', __( 'Es wird genau eine gültige post_id benötigt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$value = $input['post_id'];
	if ( is_int( $value ) ) {
		$post_id = $value;
	} elseif ( is_string( $value ) && preg_match( '/^[1-9][0-9]{0,9}$/', $value ) ) {
		$post_id = (int) $value;
	} else {
		return new WP_Error( 'wpagent_builder_bad_input', __( 'post_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( $post_id < 1 || $post_id > 2147483647 ) {
		return new WP_Error( 'wpagent_builder_bad_input', __( 'post_id liegt außerhalb des unterstützten Bereichs.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $post_id;
}

/** @return true|WP_Error */
function wpagent_builder_read_empty_object_input( $input ) {
	if ( null === $input || array() === $input ) {
		return true;
	}
	return new WP_Error( 'wpagent_builder_bad_input', __( 'Diese Ability akzeptiert keine Eingabefelder.', 'wp-agent-companion' ), array( 'status' => 400 ) );
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_post_content( string $content ) {
	if ( strlen( $content ) > WPAGENT_BUILDER_READ_MAX_POST_CONTENT_BYTES || wp_check_invalid_utf8( $content, false ) !== $content ) {
		return new WP_Error( 'wpagent_builder_read_too_large', __( 'Der Builder-Inhalt überschreitet die sichere Lesegrenze oder enthält ungültiges UTF-8.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	return true;
}

function wpagent_builder_read_valid_text( $value, int $minimum, int $maximum, ?string $pattern = null ): bool {
	if ( ! is_string( $value ) || strlen( $value ) < $minimum || strlen( $value ) > $maximum || wp_check_invalid_utf8( $value, false ) !== $value ) {
		return false;
	}
	if ( preg_match( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value ) ) {
		return false;
	}
	return null === $pattern || 1 === preg_match( $pattern, $value );
}

/** @return true|WP_Error */
function wpagent_builder_read_invalid_output() {
	return new WP_Error( 'wpagent_builder_read_output_invalid', __( 'Die Builder-Lesedaten überschreiten den vertraglich sicheren Bereich und werden nicht ausgegeben.', 'wp-agent-companion' ), array( 'status' => 500 ) );
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_block_report( array $report ) {
	if ( array( 'block_count', 'freeform_count', 'block_types' ) !== array_keys( $report )
		|| ! is_int( $report['block_count'] ) || ! is_int( $report['freeform_count'] )
		|| $report['block_count'] < 0 || $report['block_count'] > WPAGENT_BUILDER_READ_MAX_BLOCKS
		|| $report['freeform_count'] < 0 || $report['freeform_count'] > $report['block_count']
		|| ! is_array( $report['block_types'] ) || count( $report['block_types'] ) > 128 ) {
		return wpagent_builder_read_invalid_output();
	}
	foreach ( $report['block_types'] as $name ) {
		if ( ! wpagent_builder_read_valid_text( $name, 1, 128, '/^[A-Za-z0-9][A-Za-z0-9._:\/-]*$/' ) ) {
			return wpagent_builder_read_invalid_output();
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_detection_output( array $result ) {
	$keys = array( 'primary', 'signals', 'storage_location', 'is_block_based', 'write_supported', 'recommended_ability', 'unmanaged_block_namespaces', 'executable_shortcodes', 'block_types', 'notes' );
	$builders = array( 'bricks', 'beaver-builder', 'oxygen', 'breakdance', 'elementor', 'wpbakery', 'acf', 'kadence', 'spectra', 'spectra-blocks', 'generateblocks', 'gutenberg-blocks', 'woocommerce-product', 'classic' );
	$recommended = array( '', 'wp-agent/update-post', 'wp-agent/update-beaver-node', 'wp-agent/update-spectra-block-attributes', 'wp-agent/update-spectra-static-heading-alignment', 'wp-agent/update-spectra-blocks-separator', 'wp-agent/update-generateblocks-block-attributes', 'wp-agent/update-kadence-block-attributes' );
	if ( $keys !== array_keys( $result ) || ! in_array( $result['primary'], $builders, true )
		|| ! in_array( $result['storage_location'], array( 'postmeta', 'post_content', 'gemischt' ), true )
		|| ! is_bool( $result['is_block_based'] ) || ! is_bool( $result['write_supported'] )
		|| ! in_array( $result['recommended_ability'], $recommended, true )
		|| ! is_array( $result['signals'] ) || count( $result['signals'] ) > 64 ) {
		return wpagent_builder_read_invalid_output();
	}
	foreach ( $result['signals'] as $signal ) {
		if ( ! is_array( $signal ) || array( 'type', 'key', 'location' ) !== array_keys( $signal )
			|| ! in_array( $signal['type'], array( 'postmeta', 'shortcode', 'executable-shortcode', 'block', 'post_type', 'acf' ), true )
			|| ! wpagent_builder_read_valid_text( $signal['key'], 1, 128, '/^[A-Za-z0-9_][A-Za-z0-9._:\/-]*$/' )
			|| ! in_array( $signal['location'], array( 'postmeta', 'post_content', 'post_type' ), true ) ) {
			return wpagent_builder_read_invalid_output();
		}
	}
	$lists = array(
		array( $result['unmanaged_block_namespaces'], 32, 64, '/^[a-z0-9][a-z0-9-]*$/' ),
		array( $result['executable_shortcodes'], 32, 64, '/^[A-Za-z0-9_-]+$/' ),
		array( $result['block_types'], 128, 128, '/^[A-Za-z0-9][A-Za-z0-9._:\/-]*$/' ),
		array( $result['notes'], 64, 1024, null ),
	);
	foreach ( $lists as $list ) {
		if ( ! is_array( $list[0] ) || count( $list[0] ) > $list[1] ) {
			return wpagent_builder_read_invalid_output();
		}
		foreach ( $list[0] as $value ) {
			if ( ! wpagent_builder_read_valid_text( $value, 0, $list[2], $list[3] ) ) {
				return wpagent_builder_read_invalid_output();
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_refresh_cache_output( array $result ) {
	$primary = array( 'bricks', 'beaver-builder', 'oxygen', 'breakdance', 'elementor', 'wpbakery', 'acf', 'kadence', 'spectra', 'spectra-blocks', 'generateblocks', 'gutenberg-blocks', 'woocommerce-product', 'classic' );
	$action_builders = array( 'spectra', 'generateblocks', 'woocommerce-product', 'elementor', 'beaver-builder', 'breakdance', 'oxygen' );
	if ( array( 'primary', 'actions', 'notes' ) !== array_keys( $result ) || ! in_array( $result['primary'], $primary, true ) || ! is_array( $result['actions'] ) || count( $result['actions'] ) > 16 || ! is_array( $result['notes'] ) || count( $result['notes'] ) > 16 ) {
		return wpagent_builder_read_invalid_output();
	}
	foreach ( $result['actions'] as $action ) {
		if ( ! is_array( $action ) || array( 'builder', 'action', 'executed', 'verified' ) !== array_keys( $action ) || ! in_array( $action['builder'], $action_builders, true ) || ! wpagent_builder_read_valid_text( $action['action'], 1, 1024 ) || ! is_bool( $action['executed'] ) || ! is_bool( $action['verified'] ) ) {
			return wpagent_builder_read_invalid_output();
		}
	}
	foreach ( $result['notes'] as $note ) {
		if ( ! wpagent_builder_read_valid_text( $note, 0, 1024 ) ) {
			return wpagent_builder_read_invalid_output();
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_render_report( array $result ) {
	$keys = array( 'post_id', 'block_count', 'freeform_count', 'has_freeform', 'block_types', 'rendered_html_length', 'word_count' );
	$block_report = array(
		'block_count'   => $result['block_count'] ?? null,
		'freeform_count' => $result['freeform_count'] ?? null,
		'block_types'   => $result['block_types'] ?? null,
	);
	if ( $keys !== array_keys( $result ) || ! is_int( $result['post_id'] ) || $result['post_id'] < 1 || $result['post_id'] > 2147483647 || ! is_bool( $result['has_freeform'] ) || $result['has_freeform'] !== ( $result['freeform_count'] > 0 ) || ! is_int( $result['rendered_html_length'] ) || $result['rendered_html_length'] < 0 || $result['rendered_html_length'] > WPAGENT_BUILDER_READ_MAX_RENDERED_HTML_BYTES || ! is_int( $result['word_count'] ) || $result['word_count'] < 0 || $result['word_count'] > 250000 ) {
		return wpagent_builder_read_invalid_output();
	}
	return wpagent_builder_read_validate_block_report( $block_report );
}

/** @return true|WP_Error */
function wpagent_builder_read_validate_render_verify_output( array $result ) {
	$keys = array( 'post_id', 'rendered', 'checks', 'verdict', 'notes' );
	if ( $keys !== array_keys( $result ) || ! is_array( $result['rendered'] ) || array( 'html_length', 'word_count', 'block_count', 'freeform_count', 'has_freeform', 'block_types' ) !== array_keys( $result['rendered'] ) || ! is_array( $result['checks'] ) || count( $result['checks'] ) > 4 || ! is_array( $result['verdict'] ) || ! is_array( $result['notes'] ) || count( $result['notes'] ) > 16 ) {
		return wpagent_builder_read_invalid_output();
	}
	$rendered = $result['rendered'];
	$rendered['post_id'] = $result['post_id'];
	if ( ! is_array( $rendered ) || ! array_key_exists( 'html_length', $rendered ) ) {
		return wpagent_builder_read_invalid_output();
	}
	$report = array(
		'post_id'              => $result['post_id'],
		'block_count'          => $rendered['block_count'] ?? null,
		'freeform_count'       => $rendered['freeform_count'] ?? null,
		'has_freeform'         => $rendered['has_freeform'] ?? null,
		'block_types'          => $rendered['block_types'] ?? null,
		'rendered_html_length' => $rendered['html_length'],
		'word_count'           => $rendered['word_count'] ?? null,
	);
	if ( is_wp_error( wpagent_builder_read_validate_render_report( $report ) ) || array( 'renders_ok', 'warnings' ) !== array_keys( $result['verdict'] ) || ! is_bool( $result['verdict']['renders_ok'] ) || ! is_int( $result['verdict']['warnings'] ) || $result['verdict']['warnings'] < 0 || $result['verdict']['warnings'] > 4 ) {
		return wpagent_builder_read_invalid_output();
	}
	$warnings                    = 0;
	$has_active_builder_mismatch = false;
	foreach ( $result['checks'] as $check ) {
		if ( ! is_array( $check ) || array( 'check', 'subject', 'agrees', 'severity', 'note' ) !== array_keys( $check ) || ! in_array( $check['check'], array( 'freeform', 'builder_render_mismatch', 'cf7_post_content_inert', 'seo_read_back' ), true ) || ! wpagent_builder_read_valid_text( $check['subject'], 1, 256 ) || ! is_bool( $check['agrees'] ) || ! in_array( $check['severity'], array( 'info', 'warn' ), true ) || ! wpagent_builder_read_valid_text( $check['note'], 1, 1024 ) ) {
			return wpagent_builder_read_invalid_output();
		}
		if ( 'warn' === $check['severity'] ) {
			$warnings++;
		}
		if ( 'builder_render_mismatch' === $check['check'] && ! $check['agrees'] ) {
			$has_active_builder_mismatch = true;
		}
	}
	if ( $result['verdict']['warnings'] !== $warnings || $result['verdict']['renders_ok'] !== ( ! $rendered['has_freeform'] && ! $has_active_builder_mismatch ) ) {
		return wpagent_builder_read_invalid_output();
	}
	foreach ( $result['notes'] as $note ) {
		if ( ! wpagent_builder_read_valid_text( $note, 0, 1024 ) ) {
			return wpagent_builder_read_invalid_output();
		}
	}
	return true;
}
