<?php
/**
 * Ability: wp-agent/refresh-builder-cache
 *
 * Feuert nach einem builder-bewussten Edit die zum erkannten Page-Builder
 * passende offizielle Cache-/Regenerations-Invalidierung, damit das Frontend
 * nicht auf veralteter Ausgabe stehen bleibt. Grundlage: die Cache-/
 * Regenerations-Map aus docs/strategy/developer-mode-plan-2026-07.md, Punkt 3.
 *
 * Introspektion vor Hardcoding: die Plan-Schlüssel wurden gegen die
 * TATSÄCHLICH installierten Plugin-Versionen geprüft, nicht blind übernommen.
 * Empirischer Befund je Builder (Quelle jeweils die installierte Plugin-Version):
 *
 * - Spectra 2.20.1: der offizielle WP-CLI-Befehl "wp spectra regenerate-css"
 *   (classes/commands/class-spectra-regenerate-assets-command.php) ruft genau
 *   zwei Dinge auf: UAGB_Admin_Helper::create_specific_stylesheet() (globale
 *   Sammel-Stylesheet-Datei) und UAGB_Admin_Helper::update_admin_settings_option(
 *   '__uagb_asset_version', time() ) — der Plan-Schlüssel __uagb_asset_version
 *   ist also für diese Version exakt zutreffend. Der Versionsabgleich selbst
 *   passiert pro Beitrag in class-uagb-post-assets.php::allow_assets_generation():
 *   weicht das im Beitrags-Meta "_uag_page_assets" gespeicherte "uag_version"
 *   von der (aus der Option gelesenen) Konstante UAGB_ASSET_VER ab, wird das
 *   Meta gelöscht und beim nächsten Rendern neu aufgebaut. Weil UAGB_ASSET_VER
 *   als PHP-Konstante beim Plugin-Boot einmalig aus der Option gelesen wird
 *   (class-uagb-loader.php), wirkt die Versions-Option-Erhöhung erst im
 *   NÄCHSTEN Request global; damit der Effekt für GENAU diesen Beitrag sofort
 *   nachweisbar ist, löscht diese Ability zusätzlich direkt "_uag_page_assets"
 *   für den übergebenen Beitrag (kein Plan-Abweichung, sondern eine engere,
 *   sofort verifizierbare Ergänzung desselben offiziellen Mechanismus).
 * - Spectra Blocks 1.0.4: der offizielle Admin-Ajax-Handler
 *   SpectraBlocksAdmin\Ajax\Common_Settings::clear_v3_cache() löscht die
 *   Responsive-CSS-Transienten mit dem Präfix
 *   _transient_spectra_blocks_responsive_css_ (samt Timeout-Einträgen) und
 *   leert danach den WordPress-Objektcache. Der Handler beendet die Anfrage
 *   über wp_send_json_success(), daher wird derselbe dokumentierte
 *   provider-spezifische Cache-Mechanismus in einem versionsgebundenen,
 *   überprüfbaren Adapter ausgeführt. Andere Spectra-Blocks-Versionen bleiben
 *   fail-closed.
 * - GenerateBlocks 2.3.0: kein "Asset-Version"-Schlüssel wie bei Spectra.
 *   Der Plan-Begriff "CSS-Regenerate" entspricht hier konkret der Methode
 *   GenerateBlocks_Enqueue_CSS::post_update_option() (includes/class-enqueue-
 *   css.php), die das Plugin selbst am save_post-Hook aufruft: sie setzt/löscht
 *   das Beitrags-Meta "_generateblocks_dynamic_css_version" je nach
 *   tatsächlichem Blockinhalt und entfernt den Beitrag aus der Option
 *   "generateblocks_dynamic_css_posts". needs_update() erzwingt dadurch beim
 *   nächsten Rendern eine neue CSS-Datei. Diese Ability ruft die offizielle
 *   Instanzmethode direkt auf (kein Nachbau), um Drift zur echten Plugin-Logik
 *   auszuschließen.
 * - WooCommerce 10.9.4: wc_delete_product_transients() (includes/wc-product-
 *   functions.php) entspricht exakt dem Plan-Schlüssel. Sie löscht sowohl die
 *   festen Transients (wc_products_onsale, wc_featured_products, ...) als auch
 *   die produktspezifischen (wc_related_, wc_var_prices_, ... + Beitrags-ID,
 *   siehe Automattic\WooCommerce\Internal\Utilities\ProductUtil).
 * - Kadence Blocks 3.7.8: keine Cache-/Versions-Option, kein Datei-Cache für
 *   Block-CSS gefunden (rendert inline über render_callback, siehe die
 *   Introspektion in diesem Datei-Kommentar). Bestätigt den Plan: self-healing,
 *   keine Aktion nötig.
 * - Elementor: wenn Elementor geladen ist, wird die offizielle
 *   Files-Manager-Cache-Leerung aufgerufen. In dieser Umgebung ist Elementor
 *   nicht installiert, daher bleibt dieser Zweig best-effort und defensiv.
 * - Breakdance, Oxygen: in dieser Umgebung nicht installiert, daher nicht
 *   empirisch geprüft. Wenn eine bekannte In-Request-API geladen ist, wird sie
 *   genutzt. Sonst wird präzise zwischen "nicht installiert" und "installiert,
 *   aber keine verifizierte In-Request-API gefunden" unterschieden.
 * - WPBakery: laut Plan self-healing wie Kadence, keine Aktion.
 * - Bricks: im Plan (Punkt 3) nicht in der Cache-Map enthalten; ohne
 *   dokumentierte Quelle wird hier keine Aktion behauptet.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_refresh_builder_cache(): array {
	return array(
		'label'               => __( 'Builder-Cache aktualisieren', 'wp-agent-companion' ),
			'description'         => __( 'Erkennt die Builder-Signale eines Beitrags und löst die passenden Cache-/Regenerations-Invalidierungen aus (Spectra-Asset-Version, GenerateBlocks-CSS-Neuerzeugung, WooCommerce-Produkt-Transients, bekannte installierte Builder-APIs). Selbstheilende Builder wie Kadence brauchen keine Aktion; nicht verfügbare Builder-Aktionen werden dokumentiert.', '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'        => array( 'bricks', 'beaver-builder', 'oxygen', 'breakdance', 'elementor', 'wpbakery', 'acf', 'kadence', 'spectra', 'spectra-blocks', 'generateblocks', 'gutenberg-blocks', 'woocommerce-product', 'classic' ),
					'description' => __( 'Der von wp-agent/detect-builder erkannte maßgebliche Builder dieses Beitrags.', 'wp-agent-companion' ),
				),
				'actions' => array(
					'type'  => 'array', 'maxItems' => 16,
					'items' => array(
						'type'       => 'object', 'additionalProperties' => false,
						'properties' => array(
							'builder'  => array( 'type' => 'string', 'enum' => array( 'spectra', 'generateblocks', 'woocommerce-product', 'elementor', 'beaver-builder', 'breakdance', 'oxygen' ) ),
							'action'   => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 1024 ),
							'executed' => array( 'type' => 'boolean' ),
							'verified' => array( 'type' => 'boolean' ),
						),
						'required' => array( 'builder', 'action', 'executed', 'verified' ),
					),
				),
				'notes'   => array(
					'type'  => 'array', 'maxItems' => 16,
					'items' => array( 'type' => 'string', 'maxLength' => 1024 ),
				),
			),
			'required' => array( 'primary', 'actions', 'notes' ),
		),
		'execute_callback'    => 'wpagent_exec_refresh_builder_cache',
		'permission_callback' => 'wpagent_perm_edit_posts',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
			),
		),
	);
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_refresh_builder_cache( $input ) {
	$post_id = wpagent_builder_read_post_id_from_input( $input );
	if ( is_wp_error( $post_id ) ) {
		return $post_id;
	}

	// Erkennung + Existenz-/Berechtigungsprüfung über detect-builder wieder-
	// verwenden (nicht duplizieren): liefert bei fehlendem Beitrag 404, bei
	// fehlender edit_post-Capability 403 (z. B. WooCommerce-Produkte für den
	// Editor-Bot, siehe detect-builder.php-Kommentar).
	$detection = wpagent_exec_detect_builder( array( 'post_id' => $post_id ) );
	if ( is_wp_error( $detection ) ) {
		return $detection;
	}

	$primary  = (string) $detection['primary'];
	$builders = wpagent_refresh_builder_cache_detected_builders( $detection );
	$actions  = array();
	$notes    = array();

	foreach ( $builders as $builder ) {
		$action = wpagent_refresh_builder_cache_for_builder( $builder, $post_id, $notes );
		if ( null !== $action ) {
			$actions[] = $action;
		}
	}

	$result = array(
		'primary' => $primary,
		'actions' => $actions,
		'notes'   => $notes,
	);
	$output_valid = wpagent_builder_read_validate_refresh_cache_output( $result );
	return is_wp_error( $output_valid ) ? $output_valid : $result;
}

/**
 * Ermittelt alle cache-relevanten Builder-Signale aus detect-builder.
 *
 * detect-builder braucht weiterhin eine Primary für Edit-Entscheidungen. Cache-
 * Invalidation ist breiter: ein Beitrag kann z. B. Spectra- und GenerateBlocks-
 * Blöcke mischen, oder ein WooCommerce-Produkt zusätzlich Builder-Blöcke haben.
 * Deshalb wird hier die Primary nur als erster Eintrag gesetzt und danach jedes
 * relevante Signal ergänzt.
 *
 * @param array<string,mixed> $detection
 * @return array<int,string>
 */
function wpagent_refresh_builder_cache_detected_builders( array $detection ): array {
	$builders = array();
	$add      = static function ( string $builder ) use ( &$builders ): void {
		if ( '' !== $builder && ! in_array( $builder, $builders, true ) ) {
			$builders[] = $builder;
		}
	};

	$add( (string) ( $detection['primary'] ?? '' ) );

	foreach ( (array) ( $detection['signals'] ?? array() ) as $signal ) {
		if ( ! is_array( $signal ) ) {
			continue;
		}
		$type = (string) ( $signal['type'] ?? '' );
		$key  = (string) ( $signal['key'] ?? '' );

		if ( 'block' === $type && in_array( $key, array( 'kadence', 'spectra', 'generateblocks' ), true ) ) {
			$add( $key );
			continue;
		}

		if ( 'post_type' === $type && 'product' === $key ) {
			$add( 'woocommerce-product' );
			continue;
		}

		if ( 'shortcode' === $type && str_starts_with( $key, 'vc_' ) ) {
			$add( 'wpbakery' );
			continue;
		}

		if ( 'postmeta' !== $type ) {
			continue;
		}

		$postmeta_builders = array(
			'_bricks_page_content_2'     => 'bricks',
			'_fl_builder_data'            => 'beaver-builder',
			'ct_builder_json'            => 'oxygen',
			'ct_builder_shortcodes'      => 'oxygen',
			'_breakdance_data'           => 'breakdance',
			'_elementor_data'            => 'elementor',
			'_wpb_shortcodes_custom_css' => 'wpbakery',
		);
		if ( isset( $postmeta_builders[ $key ] ) ) {
			$add( $postmeta_builders[ $key ] );
		}
	}

	return $builders;
}

/**
 * @param string            $builder
 * @param int               $post_id
 * @param array<int,string> $notes
 * @return array{builder:string,action:string,executed:bool,verified:bool}|null
 */
function wpagent_refresh_builder_cache_for_builder( string $builder, int $post_id, array &$notes ): ?array {
	switch ( $builder ) {
		case 'spectra':
			return wpagent_refresh_builder_cache_spectra( $post_id );

		case 'generateblocks':
			return wpagent_refresh_builder_cache_generateblocks( $post_id );

		case 'woocommerce-product':
			return wpagent_refresh_builder_cache_woocommerce( $post_id );

		case 'kadence':
		case 'wpbakery':
			$notes[] = sprintf(
				/* translators: %s: Builder-Name */
				__( '%s ist selbstheilend beim Rendern (Bauplan, Punkt 3): keine Cache-Aktion nötig.', 'wp-agent-companion' ),
				ucfirst( $builder )
			);
			return null;

		case 'elementor':
			return wpagent_refresh_builder_cache_elementor( $notes );

		case 'beaver-builder':
			return wpagent_refresh_builder_cache_beaver( $post_id );

		case 'breakdance':
			return wpagent_refresh_builder_cache_breakdance( $notes );

		case 'oxygen':
			return wpagent_refresh_builder_cache_oxygen( $post_id, $notes );

		default:
			$notes[] = sprintf(
				/* translators: %s: erkannter Builder-Wert */
				__( 'Kein bekannter builder-spezifischer Cache-Mechanismus für "%s" hinterlegt.', 'wp-agent-companion' ),
				$builder
			);
			return null;
	}
}

/**
 * Beaver Builder invalidates the exact layout cache via its public model API.
 *
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_beaver( int $post_id ): array {
	if ( ! class_exists( 'FLBuilderModel' ) || ! method_exists( 'FLBuilderModel', 'delete_all_asset_cache' ) ) {
		return array( 'builder' => 'beaver-builder', 'action' => 'Beaver-Builder-Modell-API nicht geladen.', 'executed' => false, 'verified' => false );
	}
	try {
		FLBuilderModel::delete_all_asset_cache( $post_id );
		return array(
			'builder'  => 'beaver-builder',
			'action'   => "FLBuilderModel::delete_all_asset_cache({$post_id})",
			'executed' => true,
			'verified' => true,
		);
	} catch ( Throwable $error ) {
		return array( 'builder' => 'beaver-builder', 'action' => 'Beaver-Builder-Cache-Invalidierung fehlgeschlagen.', 'executed' => false, 'verified' => false );
	}
}

/**
 * Spectra: löst denselben Mechanismus wie der offizielle WP-CLI-Befehl
 * "wp spectra regenerate-css" aus (siehe Datei-Kommentar oben für den
 * empirischen Beleg), plus gezielte Löschung des Beitrags-Caches für
 * sofortige Verifizierbarkeit.
 *
 * @param int $post_id
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_spectra( int $post_id ): array {
	if ( class_exists( 'UAGB_Admin_Helper' ) ) {
		$before     = get_option( '__uagb_asset_version' );
		$bump_value = wpagent_refresh_builder_cache_unique_spectra_version();
		\UAGB_Admin_Helper::update_admin_settings_option( '__uagb_asset_version', $bump_value );
		delete_post_meta( $post_id, '_uag_page_assets' );
		$after = get_option( '__uagb_asset_version' );

		$version_bumped     = ( (string) $after === $bump_value && (string) $before !== (string) $after );
		$post_cache_cleared = ! metadata_exists( 'post', $post_id, '_uag_page_assets' );

		return array(
			'builder'  => 'spectra',
			'action'   => "UAGB_Admin_Helper::update_admin_settings_option('__uagb_asset_version', eindeutige Mikrosekunden-Version) + delete_post_meta('_uag_page_assets') (offizieller Mechanismus von \"wp spectra regenerate-css\")",
			'executed' => true,
			'verified' => $version_bumped && $post_cache_cleared,
		);
	}

	if ( defined( 'SPECTRA_BLOCKS_VER' ) && '1.0.4' === (string) SPECTRA_BLOCKS_VER && class_exists( 'Spectra_Blocks_Admin_Helper' ) ) {
		return wpagent_refresh_builder_cache_spectra_blocks_104();
	}

	return array(
		'builder'  => 'spectra',
		'action'   => 'Spectra-Klassen nicht geladen oder kein verifizierter Providerstand verfügbar, Aktion übersprungen.',
		'executed' => false,
		'verified' => false,
	);
}

/**
 * Spectra Blocks 1.0.4: führt den dokumentierten Clear-V3-Cache-Mechanismus
 * ohne den request-beendenden Admin-Ajax-Handler aus und verifiziert danach,
 * dass beide Transientenfamilien nicht mehr vorhanden sind.
 *
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_spectra_blocks_104(): array {
	global $wpdb;

	$transient_prefix = $wpdb->esc_like( '_transient_spectra_blocks_responsive_css_' ) . '%';
	$timeout_prefix   = $wpdb->esc_like( '_transient_timeout_spectra_blocks_responsive_css_' ) . '%';
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- This mirrors Spectra Blocks 1.0.4's official bulk transient cleanup.
	$deleted          = $wpdb->query(
		$wpdb->prepare(
			"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
			$transient_prefix
		)
	);
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- This mirrors Spectra Blocks 1.0.4's official timeout cleanup.
	$timeouts_deleted = $wpdb->query(
		$wpdb->prepare(
			"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
			$timeout_prefix
		)
	);
	wp_cache_flush();

	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The post-cleanup count is the fail-closed verification of the official cleanup.
	$remaining = (int) $wpdb->get_var(
		$wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
			$transient_prefix
		)
	);
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The post-cleanup timeout count is the fail-closed verification of the official cleanup.
	$remaining_timeouts = (int) $wpdb->get_var(
		$wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
			$timeout_prefix
		)
	);

	return array(
		'builder'  => 'spectra',
		'action'   => 'Spectra_Blocks 1.0.4: Responsive-CSS-Transienten und Timeout-Einträge über den offiziellen Clear-V3-Cache-Mechanismus gelöscht.',
		'executed' => false !== $deleted && false !== $timeouts_deleted,
		'verified' => false !== $deleted && false !== $timeouts_deleted && 0 === $remaining && 0 === $remaining_timeouts,
	);
}

/**
 * Spectra nutzt den Optionswert als Asset-Version in CSS-/JS-URLs. Eine
 * Mikrosekunden-Zeitbasis plus Zufallssuffix verhindert gleiche Werte bei
 * schnellen oder parallelen Requests, selbst wenn zwei PHP-Worker denselben
 * veralteten Optionswert lesen. Spectra verwendet den Wert ausschließlich als
 * Cache-Version in Asset-URLs, er bleibt deshalb als Ziffernfolge kompatibel.
 *
 * @return string
 */
function wpagent_refresh_builder_cache_unique_spectra_version(): string {
	$now_microseconds = sprintf( '%.0f', floor( microtime( true ) * 1000000 ) );
	try {
		$suffix = sprintf( '%08d', random_int( 0, 99999999 ) );
	} catch ( Throwable ) {
		// random_int() sollte auf unterstützten PHP-Versionen verfügbar sein.
		// Ein Hash aus Prozess-/Zeitdaten bleibt ein kollisionsarmer Fallback,
		// ohne die Cache-Aktualisierung wegen einer Entropie-Störung abzubrechen.
		$fallback_number = hexdec( substr( hash( 'sha256', uniqid( (string) getmypid(), true ) ), 0, 7 ) );
		$suffix          = sprintf( '%08d', $fallback_number % 100000000 );
	}
	return $now_microseconds . $suffix;
}

/**
 * GenerateBlocks: ruft die offizielle Instanzmethode direkt auf, die das
 * Plugin selbst am save_post-Hook nutzt (siehe Datei-Kommentar oben).
 *
 * @param int $post_id
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_generateblocks( int $post_id ): array {
	if ( ! class_exists( 'GenerateBlocks_Enqueue_CSS' ) ) {
		return array(
			'builder'  => 'generateblocks',
			'action'   => 'GenerateBlocks-Klasse nicht geladen, Aktion übersprungen.',
			'executed' => false,
			'verified' => false,
		);
	}

	$post = get_post( $post_id );
	GenerateBlocks_Enqueue_CSS::get_instance()->post_update_option( $post_id, $post );

	$option_after = get_option( 'generateblocks_dynamic_css_posts', array() );
	$still_marked = ! empty( $option_after[ $post_id ] );

	return array(
		'builder'  => 'generateblocks',
		'action'   => "GenerateBlocks_Enqueue_CSS::post_update_option() (offizieller save_post-Hook-Callback, entfernt den Beitrag aus 'generateblocks_dynamic_css_posts', wodurch needs_update() beim nächsten Rendern eine neue CSS-Datei erzwingt)",
		'executed' => true,
		'verified' => ! $still_marked,
	);
}

/**
 * WooCommerce: offizielle Transient-Löschfunktion. Verifiziert wird gegen
 * die tatsächlich produktspezifischen Transient-Namen (siehe
 * Automattic\WooCommerce\Internal\Utilities\ProductUtil), nicht nur die
 * festen, globalen Namen.
 *
 * @param int $post_id
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_woocommerce( int $post_id ): array {
	if ( ! function_exists( 'wc_delete_product_transients' ) ) {
		return array(
			'builder'  => 'woocommerce-product',
			'action'   => 'WooCommerce-Funktion nicht geladen, Aktion übersprungen.',
			'executed' => false,
			'verified' => false,
		);
	}

	wc_delete_product_transients( $post_id );

	$product_specific_transient_names = array(
		'wc_product_children_',
		'wc_var_prices_',
		'wc_related_',
		'wc_child_has_weight_',
		'wc_child_has_dimensions_',
	);

	$still_present = array();
	foreach ( $product_specific_transient_names as $prefix ) {
		if ( false !== get_transient( $prefix . $post_id ) ) {
			$still_present[] = $prefix . $post_id;
		}
	}

	return array(
		'builder'  => 'woocommerce-product',
		'action'   => "wc_delete_product_transients({$post_id}) (offizielle WooCommerce-Funktion, löscht produktspezifische plus die festen globalen Transients)",
		'executed' => true,
		'verified' => array() === $still_present,
	);
}

/**
 * Elementor: offizielle Files-Manager-Cache-Leerung, die auch für "Regenerate
 * CSS & Data" genutzt wird.
 *
 * @param array<int,string> $notes
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_elementor( array &$notes ): array {
	$installed = class_exists( '\Elementor\Plugin' ) || wpagent_refresh_builder_cache_active_plugin( array( 'elementor/' ) );
	if ( ! class_exists( '\Elementor\Plugin' ) || ! isset( \Elementor\Plugin::$instance ) ) {
		return wpagent_refresh_builder_cache_unavailable_builder_action( 'elementor', $installed, $notes );
	}

	$plugin = \Elementor\Plugin::$instance;
	if ( ! is_object( $plugin ) || ! isset( $plugin->files_manager ) || ! is_object( $plugin->files_manager ) || ! method_exists( $plugin->files_manager, 'clear_cache' ) ) {
		return wpagent_refresh_builder_cache_unavailable_builder_action( 'elementor', true, $notes );
	}

	try {
		$plugin->files_manager->clear_cache();
	} catch ( \Throwable $e ) {
		return array(
			'builder'  => 'elementor',
			'action'   => 'Elementor files_manager->clear_cache() fehlgeschlagen: ' . $e->getMessage(),
			'executed' => false,
			'verified' => false,
		);
	}

	return array(
		'builder'  => 'elementor',
		'action'   => 'Elementor\\Plugin::$instance->files_manager->clear_cache() (offizielle Elementor-CSS-Datei-Regeneration)',
		'executed' => true,
		'verified' => true,
	);
}

/**
 * Breakdance: nutzt eine geladene In-Request-API, wenn die installierte
 * Version eine passende Funktion exponiert. Der dokumentierte WP-CLI-Befehl
 * wird aus dem REST-Request heraus bewusst nicht geshellt.
 *
 * @param array<int,string> $notes
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_breakdance( array &$notes ): array {
	$installed = wpagent_refresh_builder_cache_breakdance_installed();
	if ( ! $installed ) {
		return wpagent_refresh_builder_cache_unavailable_builder_action( 'breakdance', false, $notes );
	}

	$result = wpagent_refresh_builder_cache_call_first(
		array(
			array( 'Breakdance\Render\clearCache', 'Breakdance\\Render\\clearCache()' ),
			array( 'Breakdance\Render\clear_cache', 'Breakdance\\Render\\clear_cache()' ),
			array( 'Breakdance\Render\deleteAllGeneratedCssFiles', 'Breakdance\\Render\\deleteAllGeneratedCssFiles()' ),
			array( 'Breakdance\Render\delete_all_generated_css_files', 'Breakdance\\Render\\delete_all_generated_css_files()' ),
			array( 'Breakdance\Cache\clear', 'Breakdance\\Cache\\clear()' ),
			array( 'Breakdance\Cache\clear_cache', 'Breakdance\\Cache\\clear_cache()' ),
		)
	);

	if ( null !== $result ) {
		return array(
			'builder'  => 'breakdance',
			'action'   => $result['label'],
			'executed' => $result['ok'],
			'verified' => $result['ok'],
		);
	}

	return wpagent_refresh_builder_cache_unavailable_builder_action( 'breakdance', true, $notes );
}

/**
 * Oxygen Classic: erst Re-Sign versuchen, dann eine bekannte CSS-Cache-API.
 *
 * @param int               $post_id
 * @param array<int,string> $notes
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_oxygen( int $post_id, array &$notes ): array {
	$installed = wpagent_refresh_builder_cache_oxygen_installed();
	if ( ! $installed ) {
		return wpagent_refresh_builder_cache_unavailable_builder_action( 'oxygen', false, $notes );
	}

	$performed = array();

	$resign = wpagent_refresh_builder_cache_call_first(
		array(
			array( 'ct_sign_shortcodes', 'ct_sign_shortcodes()' ),
			array( 'oxygen_vsb_sign_shortcodes', 'oxygen_vsb_sign_shortcodes()' ),
		),
		array( $post_id )
	);
	if ( null !== $resign ) {
		$performed[] = $resign;
	}

	$cache = wpagent_refresh_builder_cache_call_first(
		array(
			array( 'oxygen_vsb_clear_cache', 'oxygen_vsb_clear_cache()' ),
			array( 'oxygen_vsb_cache_clear', 'oxygen_vsb_cache_clear()' ),
			array( 'ct_delete_cache', 'ct_delete_cache()' ),
			array( 'ct_delete_css_cache', 'ct_delete_css_cache()' ),
			array( 'oxygen_vsb_cache_universal_css', 'oxygen_vsb_cache_universal_css()' ),
		),
		array( $post_id )
	);
	if ( null !== $cache ) {
		$performed[] = $cache;
	}

	if ( array() === $performed ) {
		return wpagent_refresh_builder_cache_unavailable_builder_action( 'oxygen', true, $notes );
	}

	$executed = false;
	$labels   = array();
	foreach ( $performed as $performed_action ) {
		$labels[] = $performed_action['label'];
		$executed = $executed || $performed_action['ok'];
	}
	$verified = null !== $resign && $resign['ok'] && null !== $cache && $cache['ok'];

	return array(
		'builder'  => 'oxygen',
		'action'   => implode( ' + ', $labels ),
		'executed' => $executed,
		'verified' => $verified,
	);
}

/**
 * Liefert die im Bauplan dokumentierte, aber in dieser Umgebung nicht
 * ausführbare Aktion für einen bekannten, nicht installierten Builder.
 *
 * @param string $builder
 * @return string
 */
function wpagent_refresh_builder_cache_documented_action( string $builder ): string {
	$documented = array(
		'breakdance' => 'wp breakdance clear_cache (einziger offizieller Headless-Befehl)',
		'elementor'  => 'Elementor-CSS-Dateien regenerieren (Tools -> Regenerate CSS & Data)',
		'oxygen'     => 'Oxygen (Classic): Re-Sign plus CSS-Cache leeren',
	);

	return $documented[ $builder ] ?? 'Kein dokumentierter Mechanismus.';
}

/**
 * @param string            $builder
 * @param bool              $installed
 * @param array<int,string> $notes
 * @return array{builder:string,action:string,executed:bool,verified:bool}
 */
function wpagent_refresh_builder_cache_unavailable_builder_action( string $builder, bool $installed, array &$notes ): array {
	$actions_text = wpagent_refresh_builder_cache_documented_action( $builder );
	$notes[]      = $installed
		? sprintf(
			/* translators: %s: Builder-Name */
			__( '%s ist installiert, aber keine verifizierte In-Request-API für die dokumentierte Cache-Aktion wurde gefunden. Aus Sicherheitsgründen wird kein WP-CLI-Befehl aus dem REST-Request heraus gestartet.', 'wp-agent-companion' ),
			ucfirst( $builder )
		)
		: sprintf(
			/* translators: %s: Builder-Name */
			__( '%s ist in dieser Umgebung nicht installiert: die im Bauplan vorgesehene Aktion wird nur dokumentiert, nicht ausgeführt.', 'wp-agent-companion' ),
			ucfirst( $builder )
		);

	return array(
		'builder'  => $builder,
		'action'   => $actions_text,
		'executed' => false,
		'verified' => false,
	);
}

/**
 * @return bool
 */
function wpagent_refresh_builder_cache_breakdance_installed(): bool {
	return defined( 'BREAKDANCE_VERSION' )
		|| defined( 'BREAKDANCE_PLUGIN_VERSION' )
		|| class_exists( '\Breakdance\Plugin' )
		|| function_exists( 'Breakdance\plugin' )
		|| wpagent_refresh_builder_cache_active_plugin( array( 'breakdance/' ) );
}

/**
 * @return bool
 */
function wpagent_refresh_builder_cache_oxygen_installed(): bool {
	return defined( 'CT_VERSION' )
		|| defined( 'OXYGEN_VSB_VERSION' )
		|| class_exists( 'Oxygen_VSB' )
		|| function_exists( 'ct_template_output' )
		|| function_exists( 'ct_shortcodes' )
		|| wpagent_refresh_builder_cache_active_plugin( array( 'oxygen/' ) );
}

/**
 * @param array<int,string> $plugin_files
 * @return bool
 */
function wpagent_refresh_builder_cache_active_plugin( array $plugin_files ): bool {
	$active_plugins = (array) get_option( 'active_plugins', array() );
	foreach ( $plugin_files as $plugin_file ) {
		$plugin_file = trim( $plugin_file, '/' );
		foreach ( $active_plugins as $active_plugin ) {
			$active_plugin = (string) $active_plugin;
			if ( $active_plugin === $plugin_file || str_starts_with( $active_plugin, $plugin_file . '/' ) ) {
				return true;
			}
		}
	}

	if ( is_multisite() ) {
		$network_plugins = (array) get_site_option( 'active_sitewide_plugins', array() );
		foreach ( $plugin_files as $plugin_file ) {
			$plugin_file = trim( $plugin_file, '/' );
			foreach ( array_keys( $network_plugins ) as $active_plugin ) {
				$active_plugin = (string) $active_plugin;
				if ( $active_plugin === $plugin_file || str_starts_with( $active_plugin, $plugin_file . '/' ) ) {
					return true;
				}
			}
		}
	}

	return false;
}

/**
 * @param array<int,array{0:callable|string,1:string}> $candidates
 * @param array<int,mixed>                             $args
 * @return array{ok:bool,label:string}|null Ergebnis der ausgeführten API oder null.
 */
function wpagent_refresh_builder_cache_call_first( array $candidates, array $args = array() ): ?array {
	foreach ( $candidates as $candidate ) {
		$callable = $candidate[0];
		$label    = $candidate[1];
		if ( ! is_callable( $callable ) ) {
			continue;
		}

		try {
			$reflection = wpagent_refresh_builder_cache_callable_reflection( $callable );
			if ( null === $reflection || $reflection->getNumberOfRequiredParameters() > count( $args ) ) {
				continue;
			}
			$call_args = array_slice( $args, 0, $reflection->getNumberOfParameters() );
			call_user_func_array( $callable, $call_args );
			return array(
				'ok'    => true,
				'label' => $label,
			);
		} catch ( \Throwable $e ) {
			return array(
				'ok'    => false,
				'label' => $label . ' fehlgeschlagen: ' . $e->getMessage(),
			);
		}
	}

	return null;
}

/**
 * @param callable|string $callable
 * @return ReflectionFunctionAbstract|null
 */
function wpagent_refresh_builder_cache_callable_reflection( $callable ): ?ReflectionFunctionAbstract {
	try {
		if ( is_array( $callable ) ) {
			return new ReflectionMethod( $callable[0], $callable[1] );
		}
		if ( is_string( $callable ) && str_contains( $callable, '::' ) ) {
			return new ReflectionMethod( $callable );
		}
		return new ReflectionFunction( $callable );
	} catch ( \ReflectionException $e ) {
		return null;
	}
}
