<?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 {
	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' ),
			),
			'required'   => array( 'post_id' ),
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'primary'          => array( 'type' => 'string' ),
				'signals'          => array(
					'type'  => 'array',
					'items' => array(
						'type'       => 'object',
						'properties' => array(
							'type'     => array( 'type' => 'string' ),
							'key'      => array( 'type' => 'string' ),
							'location' => array( 'type' => 'string' ),
						),
					),
				),
				'storage_location'             => array( 'type' => 'string' ),
				'is_block_based'               => array( 'type' => 'boolean' ),
				'write_supported'              => array( 'type' => 'boolean' ),
				'recommended_ability'          => array( 'type' => 'string' ),
				'unmanaged_block_namespaces'   => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
				'executable_shortcodes'        => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
				'block_types'                  => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
				'notes'                        => array(
					'type'  => 'array',
					'items' => array( 'type' => 'string' ),
				),
			),
		),
		'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 ) {
	$input   = is_array( $input ) ? $input : array();
	$post_id = (int) ( $input['post_id'] ?? 0 );
	$post    = get_post( $post_id );

	if ( ! $post ) {
		return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	if ( ! current_user_can( 'edit_post', $post_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	$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';
	}
	return 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,
	);
}

/**
 * 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;
}
