<?php
/** Abilities für den bestätigten Live-Arbeitsbereich im geöffneten WordPress-Editor. */

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

define( 'WPAGENT_LIVE_EDITOR_MAX_INPUT_BYTES', 100000 );
define( 'WPAGENT_LIVE_EDITOR_MAX_JSON_DEPTH', 12 );
// The outer 100,000-byte request cap is the decisive bound. These limits retain
// Elementor's documented 500-control and 2,000-element document ceilings so a
// read-modify-write command does not discard otherwise valid provider state.
define( 'WPAGENT_LIVE_EDITOR_MAX_JSON_PROPERTIES', 500 );
define( 'WPAGENT_LIVE_EDITOR_MAX_JSON_ITEMS', 2000 );
define( 'WPAGENT_LIVE_EDITOR_JSON_ENVELOPE_FORMAT', 'wpagently.live-editor-json.v1' );

/** @return array<string,mixed> */
function wpagent_live_editor_json_envelope_schema(): array {
	return array(
		'type' => 'object',
		'properties' => array(
			'format' => array( 'type' => 'string', 'enum' => array( WPAGENT_LIVE_EDITOR_JSON_ENVELOPE_FORMAT ), 'maxLength' => 64 ),
			'json'   => array( 'type' => 'string', 'minLength' => 2, 'maxLength' => WPAGENT_COMPANION_LIVE_EDITOR_MAX_RESULT_BYTES ),
			'sha256' => array( 'type' => 'string', 'minLength' => 64, 'maxLength' => 64, 'pattern' => '^[a-f0-9]{64}$' ),
			'bytes'  => array( 'type' => 'integer', 'minimum' => 2, 'maximum' => WPAGENT_COMPANION_LIVE_EDITOR_MAX_RESULT_BYTES ),
		),
		'required'             => array( 'format', 'json', 'sha256', 'bytes' ),
		'maxProperties'        => 4,
		'additionalProperties' => false,
	);
}

/** @return array<string,mixed> */
function wpagent_live_editor_page_schema(): array {
	return array(
		'type' => 'object',
		'properties' => array(
			'kind'       => array( 'type' => 'string', 'enum' => array( 'empty', 'page', 'gutenberg', 'elementor', 'error' ), 'maxLength' => 16 ),
			'title'      => array( 'type' => 'string', 'maxLength' => 200 ),
			'path'       => array( 'type' => 'string', 'maxLength' => 2048 ),
			'post_id'    => array( 'type' => 'integer', 'minimum' => 0 ),
			'ready'      => array( 'type' => 'boolean' ),
			'dirty'      => array( 'type' => 'boolean' ),
			'state_hash' => array( 'type' => 'string', 'maxLength' => 64, 'pattern' => '^(?:|[a-f0-9]{64})$' ),
		),
		'required'             => array( 'kind', 'title', 'path', 'post_id', 'ready', 'dirty', 'state_hash' ),
		'maxProperties'        => 7,
		'additionalProperties' => false,
	);
}

/** @return array<string,mixed> */
function wpagent_live_editor_command_result_schema(): array {
	return wpagent_live_editor_json_envelope_schema();
}

/** @return array<string,mixed> */
function wpagent_live_editor_command_output_schema(): array {
	return array(
		'type' => 'object',
		'properties' => array(
			'command_id' => array( 'type' => 'string', 'minLength' => 24, 'maxLength' => 24, 'pattern' => '^[a-f0-9]{24}$' ),
			'action' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 64, 'enum' => array( 'inspect-page', 'gutenberg-structure', 'list-block-types', 'get-block-schema', 'get-block-attributes', 'open-post', 'create-block', 'update-block', 'delete-block', 'move-block', 'replace-inner-blocks', 'undo', 'redo', 'save-post', 'finalize-provider-attributes', 'open-elementor-document', 'elementor-mutate' ) ),
			'status' => array( 'type' => 'string', 'enum' => array( 'pending', 'running', 'awaiting_confirmation', 'completed', 'failed', 'denied', 'recovery_required' ), 'maxLength' => 24 ),
			'requires_confirmation' => array( 'type' => 'boolean' ),
			'created_at' => array( 'type' => 'string', 'format' => 'date-time', 'maxLength' => 32 ),
			'expires_at' => array( 'type' => 'string', 'format' => 'date-time', 'maxLength' => 32 ),
			'result' => wpagent_live_editor_command_result_schema(),
			'error' => array( 'type' => 'string', 'maxLength' => 2000 ),
		),
		'required'             => array( 'command_id', 'action', 'status', 'requires_confirmation', 'created_at', 'expires_at', 'result', 'error' ),
		'maxProperties'        => 8,
		'additionalProperties' => false,
	);
}

function wpagent_perm_live_editor( $input = null ): bool {
	$user = wp_get_current_user();
	return $user instanceof WP_User && $user->exists() && wpagent_companion_is_safe_connection_user( $user );
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_input_keys( $input, array $allowed, string $error_code = 'wpagent_live_editor_input_fields' ) {
	if ( ! is_array( $input ) || ( $input && array_is_list( $input ) ) ) {
		return new WP_Error( $error_code, __( 'Die Live-Editor-Anfrage muss ein Objekt sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( array_keys( $input ) as $key ) {
		if ( ! is_string( $key ) || ! in_array( $key, $allowed, true ) ) {
			return new WP_Error( $error_code, __( 'Die Live-Editor-Anfrage enthält nicht erlaubte Felder.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_payload_size( array $input ) {
	$encoded = wp_json_encode( $input );
	if ( ! is_string( $encoded ) || strlen( $encoded ) > WPAGENT_LIVE_EDITOR_MAX_INPUT_BYTES ) {
		return new WP_Error( 'wpagent_live_editor_payload', __( 'Die Live-Editor-Anfrage darf höchstens 100.000 Byte groß sein.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	return true;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_live_editor_json_envelope( array $value ) {
	$json = wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	if ( ! is_string( $json ) || strlen( $json ) > WPAGENT_COMPANION_LIVE_EDITOR_MAX_RESULT_BYTES ) {
		return new WP_Error( 'wpagent_live_editor_result_unavailable', __( 'Der Live-Editor-Wert kann nicht sicher als begrenztes JSON ausgegeben werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return array(
		'format' => WPAGENT_LIVE_EDITOR_JSON_ENVELOPE_FORMAT,
		'json'   => $json,
		'sha256' => hash( 'sha256', $json ),
		'bytes'  => strlen( $json ),
	);
}

/** @return array<mixed>|WP_Error */
function wpagent_live_editor_decode_json_envelope( $value, string $field, string $root_type, int $max_properties = WPAGENT_LIVE_EDITOR_MAX_JSON_PROPERTIES ) {
	$keys = wpagent_live_editor_validate_input_keys( $value, array( 'format', 'json', 'sha256', 'bytes' ), 'wpagent_live_editor_' . $field );
	if ( is_wp_error( $keys )
		|| ! is_string( $value['format'] ?? null )
		|| WPAGENT_LIVE_EDITOR_JSON_ENVELOPE_FORMAT !== $value['format']
		|| ! is_string( $value['json'] ?? null )
		|| ! is_string( $value['sha256'] ?? null )
		|| ! is_int( $value['bytes'] ?? null ) ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, __( 'Der Live-Editor-JSON-Umschlag ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$json = $value['json'];
	if ( strlen( $json ) < 2 || strlen( $json ) > WPAGENT_COMPANION_LIVE_EDITOR_MAX_RESULT_BYTES
		|| $value['bytes'] !== strlen( $json )
		|| ! preg_match( '/^[a-f0-9]{64}$/D', $value['sha256'] )
		|| ! hash_equals( $value['sha256'], hash( 'sha256', $json ) ) ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, __( 'Der Live-Editor-JSON-Umschlag besitzt keine gültige Größen- oder Integritätsbindung.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$trimmed = trim( $json );
	if ( ( 'object' === $root_type && ! str_starts_with( $trimmed, '{' ) ) || ( 'array' === $root_type && ! str_starts_with( $trimmed, '[' ) ) ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, __( 'Der Live-Editor-JSON-Umschlag besitzt nicht die erwartete Struktur.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	try {
		$decoded = json_decode( $json, true, WPAGENT_LIVE_EDITOR_MAX_JSON_DEPTH + 2, JSON_THROW_ON_ERROR );
	} catch ( JsonException ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, __( 'Der Live-Editor-JSON-Umschlag enthält ungültiges JSON.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_array( $decoded ) || ( 'array' === $root_type && ! array_is_list( $decoded ) ) || ( 'object' === $root_type && $decoded && array_is_list( $decoded ) ) ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, __( 'Der Live-Editor-JSON-Umschlag besitzt nicht die erwartete Struktur.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$valid = wpagent_live_editor_validate_bounded_json_value( $decoded, 0, $max_properties );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	return $decoded;
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_bounded_json_value( $value, int $depth = 0, ?int $root_max_properties = null ) {
	if ( $depth > WPAGENT_LIVE_EDITOR_MAX_JSON_DEPTH ) {
		return new WP_Error( 'wpagent_live_editor_value_depth', __( 'Ein Live-Editor-Wert ist zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( null === $value || is_bool( $value ) || is_int( $value ) ) {
		return true;
	}
	if ( is_float( $value ) ) {
		return is_finite( $value )
			? true
			: new WP_Error( 'wpagent_live_editor_value', __( 'Live-Editor-Werte müssen JSON-kompatibel sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( is_string( $value ) ) {
		return strlen( $value ) <= 10000 && ! str_contains( $value, "\0" )
			? true
			: new WP_Error( 'wpagent_live_editor_value', __( 'Ein Live-Editor-Textwert ist ungültig oder zu groß.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_array( $value ) ) {
		return new WP_Error( 'wpagent_live_editor_value', __( 'Live-Editor-Werte müssen JSON-kompatibel sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( array_is_list( $value ) ) {
		if ( count( $value ) > WPAGENT_LIVE_EDITOR_MAX_JSON_ITEMS ) {
			return new WP_Error( 'wpagent_live_editor_value_items', __( 'Ein Live-Editor-Array enthält zu viele Einträge.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		foreach ( $value as $item ) {
			$valid = wpagent_live_editor_validate_bounded_json_value( $item, $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
		return true;
	}
	$max_properties = 0 === $depth && null !== $root_max_properties ? $root_max_properties : WPAGENT_LIVE_EDITOR_MAX_JSON_PROPERTIES;
	if ( count( $value ) > $max_properties ) {
		return new WP_Error( 'wpagent_live_editor_value_properties', __( 'Ein Live-Editor-Objekt enthält zu viele Felder.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $value as $key => $item ) {
		// The envelope, size, depth, and provider-specific validators form the
		// boundary here. Rejecting otherwise valid provider keys (for example
		// Elementor's breakpoint and control names) would make a read-modify-write
		// cycle lose unrelated state.
		if ( ! is_string( $key ) || '' === $key || strlen( $key ) > 191 || str_contains( $key, "\0" ) ) {
			return new WP_Error( 'wpagent_live_editor_value_key', __( 'Ein Live-Editor-Objekt enthält einen ungültigen Schlüssel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$valid = wpagent_live_editor_validate_bounded_json_value( $item, $depth + 1 );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_bounded_json_object( $value, string $field, int $max_properties = WPAGENT_LIVE_EDITOR_MAX_JSON_PROPERTIES, bool $require_nonempty = false ) {
	if ( ! is_array( $value ) || ( $value && array_is_list( $value ) ) || ( $require_nonempty && ! $value ) ) {
		return new WP_Error( 'wpagent_live_editor_' . $field, sprintf( __( '%s muss ein begrenztes Objekt sein.', 'wp-agent-companion' ), $field ), array( 'status' => 400 ) );
	}
	return wpagent_live_editor_validate_bounded_json_value( $value, 0, $max_properties );
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_block_descriptors( $blocks, int &$count = 0, int $depth = 0 ) {
	if ( ! is_array( $blocks ) || ! array_is_list( $blocks ) || $depth > 10 ) {
		return new WP_Error( 'wpagent_live_editor_blocks', __( 'blocks muss eine begrenzte Blockliste sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( count( $blocks ) > 200 ) {
		return new WP_Error( 'wpagent_live_editor_blocks', __( 'blocks darf höchstens 200 Einträge enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $blocks as $block ) {
		$keys = wpagent_live_editor_validate_input_keys( $block, array( 'name', 'attributes', 'inner_blocks' ), 'wpagent_live_editor_blocks' );
		if ( is_wp_error( $keys ) ) {
			return $keys;
		}
		$name = wpagent_live_editor_clean_block_name( $block['name'] ?? null );
		if ( is_wp_error( $name ) ) {
			return $name;
		}
		++$count;
		if ( $count > 200 ) {
			return new WP_Error( 'wpagent_live_editor_blocks', __( 'Die gesamte innere Blockstruktur darf höchstens 200 Blöcke enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( array_key_exists( 'attributes', $block ) ) {
			$attributes = wpagent_live_editor_validate_bounded_json_object( $block['attributes'], 'attributes' );
			if ( is_wp_error( $attributes ) ) {
				return $attributes;
			}
		}
		if ( array_key_exists( 'inner_blocks', $block ) ) {
			$children = wpagent_live_editor_validate_block_descriptors( $block['inner_blocks'], $count, $depth + 1 );
			if ( is_wp_error( $children ) ) {
				return $children;
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_elements( $elements, int &$count = 0, int $depth = 0 ) {
	if ( ! is_array( $elements ) || ! array_is_list( $elements ) || $depth > 10 || count( $elements ) > 200 ) {
		return new WP_Error( 'wpagent_live_editor_element', __( 'Die Elementor-Struktur ist ungültig oder zu groß.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $elements as $element ) {
		$keys = wpagent_live_editor_validate_input_keys( $element, array( 'id', 'elType', 'widgetType', 'settings', 'elements', 'isInner' ), 'wpagent_live_editor_element' );
		if ( is_wp_error( $keys ) ) {
			return $keys;
		}
		$el_type = is_string( $element['elType'] ?? null ) ? $element['elType'] : '';
		if ( ! preg_match( '/^[A-Za-z0-9_-]{1,64}$/D', $el_type ) ) {
			return new WP_Error( 'wpagent_live_editor_element', __( 'Jedes Elementor-Element benötigt einen gültigen elType.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		foreach ( array( 'id' => 64, 'widgetType' => 128 ) as $field => $max_length ) {
			if ( array_key_exists( $field, $element ) && ( ! is_string( $element[ $field ] ) || ! preg_match( '/^[A-Za-z0-9_-]{1,' . $max_length . '}$/D', $element[ $field ] ) ) ) {
				return new WP_Error( 'wpagent_live_editor_element', __( 'Ein Elementor-Identifier ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
		}
		if ( array_key_exists( 'settings', $element ) ) {
			$settings = wpagent_live_editor_validate_bounded_json_object( $element['settings'], 'settings', WPAGENT_ELEMENTOR_MAX_CONTROLS );
			if ( is_wp_error( $settings ) ) {
				return $settings;
			}
		}
		if ( array_key_exists( 'isInner', $element ) && ! is_bool( $element['isInner'] ) ) {
			return new WP_Error( 'wpagent_live_editor_element', __( 'isInner muss ein boolescher Wert sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		++$count;
		if ( $count > WPAGENT_ELEMENTOR_MAX_ELEMENTS ) {
			return new WP_Error( 'wpagent_live_editor_element', __( 'Die Elementor-Struktur enthält zu viele Elemente.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( array_key_exists( 'elements', $element ) ) {
			$children = wpagent_live_editor_validate_elements( $element['elements'], $count, $depth + 1 );
			if ( is_wp_error( $children ) ) {
				return $children;
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_live_editor_validate_style_patch( $value ) {
	if ( ! is_array( $value ) || ( $value && array_is_list( $value ) ) || count( $value ) > WPAGENT_ELEMENTOR_MAX_STYLE_PATCHES ) {
		return new WP_Error( 'wpagent_live_editor_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}$/D', $control ) || ! is_array( $devices ) || ! $devices || array_is_list( $devices ) || count( $devices ) > 3 || array_diff( array_keys( $devices ), array( 'desktop', 'tablet', 'mobile' ) ) ) {
			return new WP_Error( 'wpagent_live_editor_style_patch', __( 'style_patch enthält einen ungültigen Control-Patch.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		foreach ( $devices as $device_value ) {
			$valid = wpagent_live_editor_validate_bounded_json_value( $device_value );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	return true;
}
