<?php
/** Shared, fail-closed helpers for native Elementor document access. */

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

define( 'WPAGENT_ELEMENTOR_MAX_BYTES', 2 * MB_IN_BYTES );
define( 'WPAGENT_ELEMENTOR_MAX_ELEMENTS', 2000 );
define( 'WPAGENT_ELEMENTOR_MAX_DEPTH', 64 );
define( 'WPAGENT_ELEMENTOR_LOCK_TTL', 300 );
define( 'WPAGENT_ELEMENTOR_MAX_DATA_PROPERTIES', 500 );
define( 'WPAGENT_ELEMENTOR_MAX_STRING_BYTES', 65535 );

/** @return array<string,mixed> */
function wpagent_elementor_bounded_data_object_schema( int $max_properties = WPAGENT_ELEMENTOR_MAX_DATA_PROPERTIES ): array {
	unset( $max_properties );
	return wpagent_builder_json_document_schema( WPAGENT_ELEMENTOR_MAX_BYTES );
}

/** @return array<string,mixed> */
function wpagent_elementor_bounded_element_schema(): array {
	return wpagent_builder_json_document_schema( WPAGENT_ELEMENTOR_MAX_BYTES );
}

/** @return array<string,mixed> */
function wpagent_elementor_json_document_schema(): array {
	return wpagent_builder_json_document_schema( WPAGENT_ELEMENTOR_MAX_BYTES );
}

/** @return string[] */
function wpagent_elementor_unsafe_widget_types(): array {
	return array( 'html', 'shortcode', 'wp-widget-custom_html', 'wp-widget-text', 'wp-widget-block' );
}

function wpagent_elementor_widget_type_is_unsafe( string $widget_type ): bool {
	$normalized_type = strtolower( $widget_type );
	return in_array( $normalized_type, wpagent_elementor_unsafe_widget_types(), true ) || str_starts_with( $normalized_type, 'wp-widget-' );
}

/** @return WP_Error */
function wpagent_elementor_unavailable_error(): WP_Error {
	return new WP_Error(
		'wpagent_elementor_unavailable',
		__( 'Elementor ist nicht aktiv oder seine öffentliche Dokument-API ist nicht verfügbar.', 'wp-agent-companion' ),
		array( 'status' => 409 )
	);
}

function wpagent_elementor_available(): bool {
	return class_exists( '\\Elementor\\Plugin' )
		&& isset( \Elementor\Plugin::$instance )
		&& is_object( \Elementor\Plugin::$instance )
		&& isset( \Elementor\Plugin::$instance->documents )
		&& is_object( \Elementor\Plugin::$instance->documents )
		&& method_exists( \Elementor\Plugin::$instance->documents, 'get' )
		&& isset( \Elementor\Plugin::$instance->elements_manager )
		&& is_object( \Elementor\Plugin::$instance->elements_manager )
		&& method_exists( \Elementor\Plugin::$instance->elements_manager, 'get_element' )
		&& isset( \Elementor\Plugin::$instance->frontend )
		&& is_object( \Elementor\Plugin::$instance->frontend )
		&& method_exists( \Elementor\Plugin::$instance->frontend, 'get_builder_content_for_display' );
}

/**
 * @return array{post:WP_Post,document:object}|WP_Error
 */
function wpagent_elementor_editable_document( int $post_id, bool $require_built = true ) {
	if ( ! wpagent_elementor_available() ) {
		return wpagent_elementor_unavailable_error();
	}
	// WordPress resolves edit_post without exposing object state. Keep this
	// input-bound meta-cap before get_post() so inaccessible and absent IDs
	// fail closed through the same public error.
	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 ) );
	}
	$document = \Elementor\Plugin::$instance->documents->get( $post_id, false );
	if ( ! is_object( $document ) || ! method_exists( $document, 'is_editable_by_current_user' ) || ! $document->is_editable_by_current_user() ) {
		return new WP_Error( 'wpagent_elementor_document_unavailable', __( 'Das Elementor-Dokument ist nicht bearbeitbar.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( $require_built && ( ! method_exists( $document, 'is_built_with_elementor' ) || ! $document->is_built_with_elementor() ) ) {
		return new WP_Error( 'wpagent_elementor_not_document', __( 'Der Beitrag wurde nicht mit Elementor gespeichert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return array( 'post' => $post, 'document' => $document );
}

/** @return mixed */
function wpagent_elementor_canonicalize( $value ) {
	if ( ! is_array( $value ) ) {
		return $value;
	}
	if ( ! array_is_list( $value ) ) {
		ksort( $value, SORT_STRING );
	}
	foreach ( $value as $key => $item ) {
		$value[ $key ] = wpagent_elementor_canonicalize( $item );
	}
	return $value;
}

/** @param array<string,mixed> $state */
function wpagent_elementor_state_hash( array $state ): string {
	$canonical = wpagent_elementor_canonicalize(
		array(
			'elements' => wpagent_elementor_hashable_elements( is_array( $state['elements'] ?? null ) ? $state['elements'] : array() ),
			'settings' => $state['settings'] ?? array(),
		)
	);
	return hash( 'sha256', (string) wp_json_encode( $canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
}

/**
 * Elementor v4 materializes its own element-version marker while saving a
 * revision. It has no user-visible semantic effect, so bind the state hash to
 * all element data except that exact, strictly formatted provider marker.
 * Unknown keys and malformed versions remain hash-bound.
 *
 * @param array<int,array<string,mixed>> $elements
 * @return array<int,array<string,mixed>>
 */
function wpagent_elementor_hashable_elements( array $elements ): array {
	foreach ( $elements as $index => $element ) {
		if ( ! is_array( $element ) ) {
			continue;
		}
		if ( is_string( $element['version'] ?? null ) && preg_match( '/^[0-9]+(?:\.[0-9]+){1,3}$/D', $element['version'] ) ) {
			unset( $element['version'] );
		}
		if ( is_array( $element['elements'] ?? null ) ) {
			$element['elements'] = wpagent_elementor_hashable_elements( $element['elements'] );
		}
		$elements[ $index ] = $element;
	}
	return $elements;
}

/**
 * @param mixed $value
 * @return true|WP_Error
 */
function wpagent_elementor_validate_json_value( $value, int $depth = 0 ) {
	if ( $depth > WPAGENT_ELEMENTOR_MAX_DEPTH ) {
		return new WP_Error( 'wpagent_elementor_depth', __( 'Die Elementor-Struktur ist zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( null === $value || is_bool( $value ) || is_int( $value ) || is_float( $value ) ) {
		return true;
	}
	if ( is_string( $value ) ) {
		if ( strlen( $value ) > WPAGENT_ELEMENTOR_MAX_STRING_BYTES ) {
			return new WP_Error( 'wpagent_elementor_string_size', __( 'Elementor-Daten enthalten einen zu langen Textwert.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		if ( str_contains( $value, "\0" ) ) {
			return new WP_Error( 'wpagent_elementor_null_byte', __( 'Elementor-Daten dürfen keine Null-Bytes enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$unsafe_patterns = array(
			'/<\s*(?:script|iframe|object|embed|style|link|meta|base)\b/i',
			'/\bon[a-z][a-z0-9_-]*\s*=/i',
			'/\bsrcdoc\s*=/i',
			'/(?:javascript|vbscript)\s*:/i',
			'/data\s*:\s*text\/html/i',
			'/expression\s*\(/i',
			'/<!--\s*\/?wp:/i',
		);
		foreach ( $unsafe_patterns as $pattern ) {
			if ( preg_match( $pattern, $value ) ) {
				return new WP_Error( 'wpagent_elementor_unsafe_markup', __( 'Elementor-Daten enthalten ausführbares oder nicht erlaubtes Markup.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
		}
		return true;
	}
	if ( ! is_array( $value ) ) {
		return new WP_Error( 'wpagent_elementor_bad_value', __( 'Elementor-Daten dürfen nur JSON-kompatible Werte enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( count( $value ) > WPAGENT_ELEMENTOR_MAX_DATA_PROPERTIES ) {
		return new WP_Error( 'wpagent_elementor_value_size', __( 'Elementor-Daten enthalten zu viele Werte auf einer Ebene.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	foreach ( $value as $key => $item ) {
		if ( ! is_int( $key ) && ! is_string( $key ) ) {
			return new WP_Error( 'wpagent_elementor_bad_key', __( 'Elementor-Daten enthalten einen ungültigen Schlüssel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$valid = wpagent_elementor_validate_json_value( $item, $depth + 1 );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_elementor_validate_document_elements( $value ) {
	if ( ! is_array( $value ) || ! array_is_list( $value ) ) {
		return new WP_Error( 'wpagent_elementor_bad_shape', __( 'elements muss eine Liste sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_elementor_validate_elements( $value );
}

/** @return true|WP_Error */
function wpagent_elementor_validate_document_settings( $value ) {
	if ( ! is_array( $value ) || ( $value && array_is_list( $value ) ) ) {
		return new WP_Error( 'wpagent_elementor_bad_shape', __( 'settings muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_elementor_validate_settings( $value );
}

/** @return true|WP_Error */
function wpagent_elementor_validate_style_patch_document( $value ) {
	if ( ! is_array( $value ) || ( $value && array_is_list( $value ) ) || count( $value ) > WPAGENT_ELEMENTOR_MAX_STYLE_PATCHES ) {
		return new WP_Error( 'wpagent_elementor_bad_style_patch', __( 'style_patch muss ein begrenztes Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $value as $control => $devices ) {
		if ( ! is_string( $control ) || ! preg_match( '/^[A-Za-z0-9_-]{1,191}$/', $control ) || ! is_array( $devices ) || ( $devices && array_is_list( $devices ) ) || count( $devices ) > 3 ) {
			return new WP_Error( 'wpagent_elementor_bad_style_patch', __( 'style_patch enthält ungültige Control- oder Gerätewerte.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		foreach ( array_keys( $devices ) as $device ) {
			if ( ! in_array( $device, array( 'desktop', 'tablet', 'mobile' ), true ) ) {
				return new WP_Error( 'wpagent_elementor_bad_style_patch', __( 'style_patch enthält eine nicht unterstützte Geräteebene.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_elementor_validate_single_element( $value ) {
	if ( ! is_array( $value ) || array_is_list( $value ) ) {
		return new WP_Error( 'wpagent_elementor_bad_element', __( 'element muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_elementor_validate_elements( array( $value ) );
}

/**
 * Validate a not-yet-persisted subtree before its provider-specific creation
 * path assigns native IDs. Structural and writable-control checks remain in
 * wpagent_elementor_prepare_new_subtree(), which deliberately accepts caller
 * IDs only to replace them with fresh native IDs.
 *
 * @return true|WP_Error
 */
function wpagent_elementor_validate_new_subtree_input( $value ) {
	if ( ! is_array( $value ) || array_is_list( $value ) ) {
		return new WP_Error( 'wpagent_elementor_bad_element', __( 'element muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_elementor_validate_json_value( $value );
}

/** @return mixed|WP_Error */
function wpagent_elementor_json_document_input( $value, callable $validator ) {
	if ( wpagent_builder_json_document_is_envelope( $value ) ) {
		return wpagent_builder_json_document_to_value( $value, WPAGENT_ELEMENTOR_MAX_BYTES, $validator, 'wpagent_elementor' );
	}
	// Direct PHP callers are retained for native provider smoke coverage. Public
	// REST/MCP input is closed by the envelope schema above.
	$valid = $validator( $value );
	return is_wp_error( $valid ) ? $valid : $value;
}

/** @return array{json:string,sha256:string,bytes:int}|WP_Error */
function wpagent_elementor_json_document_output( $value, callable $validator ) {
	return wpagent_builder_json_document_from_value( $value, WPAGENT_ELEMENTOR_MAX_BYTES, $validator, 'wpagent_elementor' );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_elementor_publicize_result( array $result ) {
	$validators = array(
		'elements'        => 'wpagent_elementor_validate_document_elements',
		'settings'        => 'wpagent_elementor_validate_document_settings',
		'element'         => 'wpagent_elementor_validate_single_element',
		'deleted_element' => 'wpagent_elementor_validate_single_element',
	);
	foreach ( $validators as $key => $validator ) {
		if ( ! array_key_exists( $key, $result ) ) {
			continue;
		}
		$document = wpagent_elementor_json_document_output( $result[ $key ], $validator );
		if ( is_wp_error( $document ) ) {
			return $document;
		}
		$result[ $key ] = $document;
	}
	return $result;
}

/**
 * @param array<int,array<string,mixed>> $elements
 * @return array{element_count:int,widget_types:string[]}|WP_Error
 */
function wpagent_elementor_validate_elements( array $elements ) {
	if ( ! wpagent_elementor_available() ) {
		return wpagent_elementor_unavailable_error();
	}
	$encoded = wp_json_encode( $elements );
	if ( false === $encoded || strlen( $encoded ) > WPAGENT_ELEMENTOR_MAX_BYTES ) {
		return new WP_Error( 'wpagent_elementor_payload_size', __( 'Die Elementor-Struktur darf höchstens 2 MiB groß sein.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$ids = array();
	$widget_types = array();
	$count = 0;
	$walk = function ( array $items, int $depth ) use ( &$walk, &$ids, &$widget_types, &$count ) {
		if ( $depth > WPAGENT_ELEMENTOR_MAX_DEPTH ) {
			return new WP_Error( 'wpagent_elementor_depth', __( 'Die Elementor-Struktur ist zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		foreach ( $items as $element ) {
			if ( ! is_array( $element ) || array_is_list( $element ) ) {
				return new WP_Error( 'wpagent_elementor_bad_element', __( 'Jedes Elementor-Element muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			++$count;
			if ( $count > WPAGENT_ELEMENTOR_MAX_ELEMENTS ) {
				return new WP_Error( 'wpagent_elementor_element_limit', __( 'Ein Elementor-Dokument darf über diesen Pfad höchstens 2.000 Elemente enthalten.', 'wp-agent-companion' ), array( 'status' => 413 ) );
			}
			$id = $element['id'] ?? null;
			$el_type = $element['elType'] ?? null;
			if ( ! is_string( $id ) || ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/', $id ) || isset( $ids[ $id ] ) ) {
				return new WP_Error( 'wpagent_elementor_bad_id', __( 'Elementor-Element-IDs müssen eindeutig und 1-64 Zeichen lang sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			if ( ! is_string( $el_type ) || ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/', $el_type ) ) {
				return new WP_Error( 'wpagent_elementor_bad_type', __( 'Ein Elementor-Element enthält keinen gültigen elType.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$ids[ $id ] = true;
			$widget_type = null;
			if ( 'widget' === $el_type ) {
				$widget_type = $element['widgetType'] ?? null;
				if ( ! is_string( $widget_type ) || ! preg_match( '/^[A-Za-z0-9_-]{1,128}$/', $widget_type ) ) {
					return new WP_Error( 'wpagent_elementor_bad_widget', __( 'Ein Elementor-Widget enthält keinen gültigen widgetType.', 'wp-agent-companion' ), array( 'status' => 400 ) );
				}
				if ( wpagent_elementor_widget_type_is_unsafe( $widget_type ) ) {
					return new WP_Error( 'wpagent_elementor_unsafe_widget', __( 'HTML-, Shortcode- und ungefilterte Legacy-Widgets sind über diesen sicheren Pfad nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'element_id' => $id, 'widget_type' => $widget_type ) );
				}
				$widget_types[ $widget_type ] = true;
			}
			$registered = \Elementor\Plugin::$instance->elements_manager->get_element( $el_type, $widget_type );
			if ( ! is_object( $registered ) ) {
				return new WP_Error(
					'wpagent_elementor_unknown_element',
					/* translators: %s: Elementor element or widget type identifier. */
					sprintf( __( 'Elementor kennt den Elementtyp „%s“ in der aktiven Installation nicht.', 'wp-agent-companion' ), $widget_type ?? $el_type ),
					array( 'status' => 400, 'element_id' => $id, 'element_type' => $widget_type ?? $el_type )
				);
			}
			$settings = $element['settings'] ?? array();
			$children = $element['elements'] ?? array();
			if ( ! is_array( $settings ) || ( $settings && array_is_list( $settings ) ) || count( $settings ) > WPAGENT_ELEMENTOR_MAX_DATA_PROPERTIES || ! is_array( $children ) || ! array_is_list( $children ) ) {
				return new WP_Error( 'wpagent_elementor_bad_shape', __( 'settings muss ein Objekt und elements eine Liste sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$settings_valid = wpagent_elementor_validate_json_value( $settings, $depth + 1 );
			if ( is_wp_error( $settings_valid ) ) {
				return $settings_valid;
			}
			$children_valid = $walk( $children, $depth + 1 );
			if ( is_wp_error( $children_valid ) ) {
				return $children_valid;
			}
		}
		return true;
	};
	$valid = $walk( $elements, 0 );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	return array( 'element_count' => $count, 'widget_types' => array_values( array_keys( $widget_types ) ) );
}

/**
 * @param object $document
 * @return array<int,array<string,mixed>>|WP_Error
 */
function wpagent_elementor_normalize_elements( $document, array $elements ) {
	$valid = wpagent_elementor_validate_elements( $elements );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	try {
		$normalized = $document->get_elements_raw_data( $elements );
	} catch ( Throwable ) {
		return new WP_Error( 'wpagent_elementor_normalize_failed', __( 'Elementor konnte die Struktur nicht normalisieren.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_array( $normalized ) ) {
		return new WP_Error( 'wpagent_elementor_normalize_failed', __( 'Elementor konnte die Struktur nicht normalisieren.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $normalized;
}

/** @return true|WP_Error */
function wpagent_elementor_validate_settings( array $settings ) {
	if ( ( $settings && array_is_list( $settings ) ) || count( $settings ) > WPAGENT_ELEMENTOR_MAX_DATA_PROPERTIES ) {
		return new WP_Error( 'wpagent_elementor_bad_shape', __( 'settings muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$encoded = wp_json_encode( $settings );
	if ( false === $encoded || strlen( $encoded ) > WPAGENT_ELEMENTOR_MAX_BYTES ) {
		return new WP_Error( 'wpagent_elementor_payload_size', __( 'Die Elementor-Einstellungen dürfen höchstens 2 MiB groß sein.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	return wpagent_elementor_validate_json_value( $settings );
}

/** @param array<string,mixed> $expected */
function wpagent_elementor_array_contains( array $actual, array $expected ): bool {
	if ( array() === $expected ) {
		return array() === $actual;
	}
	foreach ( $expected as $key => $value ) {
		if ( ! array_key_exists( $key, $actual ) ) {
			return false;
		}
		if ( is_array( $value ) ) {
			if ( ! is_array( $actual[ $key ] ) ) {
				return false;
			}
			if ( array_is_list( $value ) ? $actual[ $key ] !== $value : ! wpagent_elementor_array_contains( $actual[ $key ], $value ) ) {
				return false;
			}
		} elseif ( $actual[ $key ] !== $value ) {
			return false;
		}
	}
	return true;
}

/**
 * Elementor v4 adds provider-owned metadata such as an atomic element version
 * while saving. Compare the complete requested tree without requiring those
 * additional, schema-owned fields to have been supplied by the caller.
 *
 * @param array<int,array<string,mixed>> $actual
 * @param array<int,array<string,mixed>> $expected
 */
function wpagent_elementor_elements_contain_requested( array $actual, array $expected ): bool {
	if ( count( $actual ) !== count( $expected ) ) {
		return false;
	}
	foreach ( $expected as $index => $expected_element ) {
		$actual_element = $actual[ $index ] ?? null;
		if ( ! is_array( $expected_element ) || ! is_array( $actual_element ) ) {
			return false;
		}
		$expected_children = is_array( $expected_element['elements'] ?? null ) ? $expected_element['elements'] : array();
		$actual_children = is_array( $actual_element['elements'] ?? null ) ? $actual_element['elements'] : array();
		unset( $expected_element['elements'], $actual_element['elements'] );
		$extra_keys = array_values( array_diff( array_keys( $actual_element ), array_keys( $expected_element ) ) );
		if ( array_diff( $extra_keys, array( 'version' ) )
			|| ( in_array( 'version', $extra_keys, true ) && ( ! is_string( $actual_element['version'] ) || ! preg_match( '/^[0-9]+(?:\.[0-9]+){1,3}$/', $actual_element['version'] ) ) )
			|| ! wpagent_elementor_array_contains( $actual_element, $expected_element )
			|| ! wpagent_elementor_elements_contain_requested( $actual_children, $expected_children ) ) {
			return false;
		}
	}
	return true;
}

/**
 * Save through Elementor's public document API. Elementor intentionally skips an
 * empty settings object, so clear it through its public page-settings manager.
 *
 * @param object $document
 * @param array<string,mixed> $data
 * @throws RuntimeException When the public page-settings API is unavailable.
 */
function wpagent_elementor_save_document( $document, array $data, bool $has_settings ): bool {
	$page_settings_manager = null;
	if ( $has_settings && array() === ( $data['settings'] ?? null ) ) {
		if ( ! class_exists( '\\Elementor\\Core\\Settings\\Manager' ) ) {
			throw new RuntimeException( 'Elementor page settings manager is unavailable.' );
		}
		$page_settings_manager = \Elementor\Core\Settings\Manager::get_settings_managers( 'page' );
		if ( ! is_object( $page_settings_manager ) || ! method_exists( $page_settings_manager, 'save_settings' ) ) {
			throw new RuntimeException( 'Elementor page settings manager is unavailable.' );
		}
	}
	$saved = $document->save( $data );
	if ( true !== $saved ) {
		return false;
	}
	if ( $page_settings_manager ) {
		$page_settings_manager->save_settings( array(), $document->get_post()->ID );
	}
	return true;
}

/**
 * @param object $document
 * @return array<string,mixed>
 */
function wpagent_elementor_document_state( int $post_id, $document ): array {
	$elements = $document->get_elements_data();
	$settings = method_exists( $document, 'get_db_document_settings' ) ? $document->get_db_document_settings() : array();
	$elements = is_array( $elements ) ? $elements : array();
	$settings = is_array( $settings ) ? $settings : array();
	$summary = wpagent_elementor_summarize_elements( $elements );
	$state = array(
		'post_id'                => $post_id,
		'built_with_elementor'   => (bool) $document->is_built_with_elementor(),
		'elements'               => $elements,
		'settings'               => $settings,
		'element_count'          => $summary['element_count'],
		'widget_types'           => $summary['widget_types'],
		'elementor_version'      => defined( 'ELEMENTOR_VERSION' ) ? (string) ELEMENTOR_VERSION : '',
		'edit_url'               => method_exists( $document, 'get_edit_url' ) ? (string) $document->get_edit_url() : '',
	);
	$state['hash'] = wpagent_elementor_state_hash( $state );
	return $state;
}

/** @return array{element_count:int,widget_types:string[]} */
function wpagent_elementor_summarize_elements( array $elements ): array {
	$count = 0;
	$widget_types = array();
	$walk = function ( array $items ) use ( &$walk, &$count, &$widget_types ): void {
		foreach ( $items as $element ) {
			if ( ! is_array( $element ) ) {
				continue;
			}
			++$count;
			if ( is_string( $element['widgetType'] ?? null ) ) {
				$widget_types[ $element['widgetType'] ] = true;
			}
			if ( is_array( $element['elements'] ?? null ) ) {
				$walk( $element['elements'] );
			}
		}
	};
	$walk( $elements );
	return array( 'element_count' => $count, 'widget_types' => array_values( array_keys( $widget_types ) ) );
}

/** @return true|WP_Error */
function wpagent_elementor_verify_frontend( int $post_id, int $element_count ) {
	try {
		$html = \Elementor\Plugin::$instance->frontend->get_builder_content_for_display( $post_id, true );
	} catch ( Throwable ) {
		return new WP_Error( 'wpagent_elementor_render_failed', __( 'Elementor konnte die Frontend-Ausgabe nicht erzeugen.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	if ( $element_count > 0 && ( ! is_string( $html ) || '' === trim( $html ) ) ) {
		return new WP_Error( 'wpagent_elementor_render_empty', __( 'Elementor lieferte für die gespeicherte Struktur keine Frontend-Ausgabe.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return true;
}

/** @return string|WP_Error */
function wpagent_elementor_acquire_lock( int $post_id ) {
	$option = 'wp_agent_elementor_lock_' . $post_id;
	$token  = wpagent_companion_acquire_option_lock( $option, WPAGENT_ELEMENTOR_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_elementor_busy', __( 'Dieses Elementor-Dokument wird gerade geändert. Versuche es gleich erneut.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_elementor_release_lock( int $post_id, string $token ): void {
	wpagent_companion_release_option_lock( 'wp_agent_elementor_lock_' . $post_id, $token );
}

function wpagent_elementor_lock_is_owned( int $post_id, string $token ): bool {
	return wpagent_companion_option_lock_is_owned( 'wp_agent_elementor_lock_' . $post_id, $token );
}

function wpagent_elementor_refresh_lock( int $post_id, string $token ): bool {
	return wpagent_companion_refresh_option_lock( 'wp_agent_elementor_lock_' . $post_id, $token, WPAGENT_ELEMENTOR_LOCK_TTL );
}

/**
 * Re-check the token before and after every Elementor provider write/readback.
 * Losing the lock makes the provider state ambiguous, so callers must preserve
 * it and return recovery_required instead of attempting a stale compensation.
 */
function wpagent_elementor_lock_checkpoint( int $post_id, string $token ): bool {
	return wpagent_elementor_lock_is_owned( $post_id, $token ) && wpagent_elementor_refresh_lock( $post_id, $token );
}

/** @return WP_Error */
function wpagent_elementor_recovery_required( int $post_id, string $previous_hash, string $reason ): WP_Error {
	return new WP_Error(
		'wpagent_elementor_recovery_required',
		__( 'Der Elementor-Providerzustand konnte nach einer unsicheren Änderung nicht automatisch wiederhergestellt werden. Prüfe das Dokument im Elementor-Editor.', 'wp-agent-companion' ),
		array( 'status' => 500, 'post_id' => $post_id, 'previous_hash' => $previous_hash, 'recovery_required' => true, 'reason' => $reason )
	);
}

/** @return int|WP_Error */
function wpagent_elementor_save_revision( int $post_id, string $expected_hash ) {
	$revision_id = wp_save_post_revision( $post_id );
	if ( is_wp_error( $revision_id ) ) {
		return $revision_id;
	}
	$revision_id = absint( $revision_id );
	if ( $revision_id > 0 && $post_id === (int) wp_is_post_revision( $revision_id ) ) {
		$revision_document = \Elementor\Plugin::$instance->documents->get( $revision_id, false );
		if ( is_object( $revision_document ) && $revision_document->is_built_with_elementor() && hash_equals( $expected_hash, wpagent_elementor_document_state( $revision_id, $revision_document )['hash'] ) ) {
			return $revision_id;
		}
	}
	foreach ( wp_get_post_revisions( $post_id, array( 'numberposts' => 20, 'orderby' => 'ID', 'order' => 'DESC' ) ) as $revision ) {
		$revision_document = \Elementor\Plugin::$instance->documents->get( $revision->ID, false );
		if ( is_object( $revision_document ) && $revision_document->is_built_with_elementor() && hash_equals( $expected_hash, wpagent_elementor_document_state( $revision->ID, $revision_document )['hash'] ) ) {
			return (int) $revision->ID;
		}
	}
	return new WP_Error( 'wpagent_elementor_revision_failed', __( 'Vor der Elementor-Änderung konnte keine wiederherstellbare Revision angelegt werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
}

/**
 * Restore a revision only when the live document still is exactly the
 * operation-owned candidate. This is deliberately not a generic rollback:
 * a foreign editor update must survive an uncertain provider write.
 */
function wpagent_elementor_restore_revision_internal( int $post_id, int $revision_id, string $expected_hash, string $candidate_hash, string $token ): bool {
	if ( ! wpagent_elementor_lock_checkpoint( $post_id, $token ) ) {
		return false;
	}
	if ( $post_id !== (int) wp_is_post_revision( $revision_id ) ) {
		return false;
	}
	$revision_document = \Elementor\Plugin::$instance->documents->get( $revision_id, false );
	$loaded = wpagent_elementor_editable_document( $post_id );
	if ( ! is_object( $revision_document ) || is_wp_error( $loaded ) || ! $revision_document->is_built_with_elementor() ) {
		return false;
	}
	$document = $loaded['document'];
	$current = wpagent_elementor_document_state( $post_id, $document );
	if ( ! hash_equals( $candidate_hash, $current['hash'] ) ) {
		return false;
	}
	$target = wpagent_elementor_document_state( $revision_id, $revision_document );
	if ( ! hash_equals( $expected_hash, $target['hash'] ) || ! wpagent_elementor_lock_checkpoint( $post_id, $token ) ) {
		return false;
	}
	try {
		$document->set_is_built_with_elementor( true );
		$saved = wpagent_elementor_save_document( $document, array( 'elements' => $target['elements'], 'settings' => $target['settings'] ), true );
	} catch ( Throwable ) {
		return false;
	}
	if ( ! wpagent_elementor_lock_checkpoint( $post_id, $token ) ) {
		return false;
	}
	$fresh = \Elementor\Plugin::$instance->documents->get( $post_id, false );
	return true === $saved && is_object( $fresh ) && hash_equals( $expected_hash, wpagent_elementor_document_state( $post_id, $fresh )['hash'] );
}
