<?php
/**
 * Closed, bounded registration boundary for every public Companion ability.
 *
 * Ability definitions are consumed by REST and MCP. Keeping the hardening at
 * registration time means both transports, and the callback wrapper below,
 * use the exact same finite schema. A source schema without an explicit value
 * shape is rejected rather than being guessed as an empty object.
 *
 * @package WPAgentCompanion
 */

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

/** Largest existing Companion document contracts are intentionally 2 MiB. */
const WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES = 2097152;
const WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS  = 4096;
const WPAGENT_COMPANION_SCHEMA_MAX_PROPERTIES   = 256;
const WPAGENT_COMPANION_SCHEMA_MAX_PROPERTY_NAME_BYTES = 191;
const WPAGENT_COMPANION_SCHEMA_MAX_DEPTH         = 32;
const WPAGENT_COMPANION_SCHEMA_MAX_PAYLOAD_BYTES = 8388608;
const WPAGENT_COMPANION_SCHEMA_MAX_VALUE_NODES   = 16384;

// Per-node limits alone do not bound a nested payload. The 8 MiB aggregate
// preserves the existing two-document Elementor contracts while one linear
// serialization/count pass keeps REST/MCP transformation inside normal memory.

/**
 * Reviewed breaking migration for the raw provider values that existed when
 * the central gate was introduced. New bare shapes are deliberately absent:
 * they fail registration instead of inheriting this exception.
 *
 * @return array<string,array{input:array<string,string>,output:array<string,string>}>
 */
function wpagent_companion_schema_document_allowlist(): array {
	return array(
		'wp-agent/get-seo-settings' => array( 'input' => array(), 'output' => array( 'settings' => 'object' ) ),
		'wp-agent/get-post-schema' => array( 'input' => array(), 'output' => array( 'schemas.*' => 'object' ) ),
		'wp-agent/set-post-schema' => array( 'input' => array( 'schema' => 'object' ), 'output' => array( 'schema' => 'object' ) ),
		'wp-agent/get-fluent-form-fields' => array( 'input' => array(), 'output' => array( 'fields.*.choices.*' => 'object', 'fields.*.subfields' => 'object' ) ),
		'wp-agent/upsert-fluent-form-field' => array( 'input' => array(), 'output' => array( 'fields.*.choices.*' => 'object', 'fields.*.subfields' => 'object' ) ),
		'wp-agent/delete-fluent-form-field' => array( 'input' => array(), 'output' => array( 'fields.*.choices.*' => 'object', 'fields.*.subfields' => 'object' ) ),
		'wp-agent/list-fluent-entries' => array( 'input' => array(), 'output' => array( 'entries.*.values' => 'object' ) ),
		'wp-agent/get-fluent-entry' => array( 'input' => array(), 'output' => array( 'entry.values' => 'object' ) ),
		'wp-agent/set-fluent-entry-status' => array( 'input' => array(), 'output' => array( 'entry.values' => 'object' ) ),
		'wp-agent/set-fluent-entry-favorite' => array( 'input' => array(), 'output' => array( 'entry.values' => 'object' ) ),
		'wp-agent/create-gravity-form' => array( 'input' => array( 'fields.*' => 'object', 'notifications.@' => 'object', 'confirmations.@' => 'object' ), 'output' => array() ),
		'wp-agent/update-gravity-form' => array( 'input' => array( 'fields.*' => 'object', 'notifications.@' => 'object', 'confirmations.@' => 'object' ), 'output' => array() ),
		'wp-agent/list-formidable-forms' => array( 'input' => array(), 'output' => array( 'forms.*' => 'object' ) ),
		'wp-agent/get-formidable-form' => array( 'input' => array(), 'output' => array( 'fields.*' => 'object' ) ),
		'wp-agent/create-formidable-form' => array( 'input' => array( 'fields.*' => 'object' ), 'output' => array( 'form.fields.*' => 'object' ) ),
		'wp-agent/update-formidable-form' => array( 'input' => array( 'fields.*' => 'object' ), 'output' => array( 'form.fields.*' => 'object' ) ),
		'wp-agent/duplicate-formidable-form' => array( 'input' => array(), 'output' => array( 'form.fields.*' => 'object' ) ),
		'wp-agent/preview-form-migration' => array( 'input' => array(), 'output' => array( 'portable_form' => 'object', 'target_form' => 'object', 'unsupported_fields.*' => 'object' ) ),
		'wp-agent/migrate-form' => array( 'input' => array(), 'output' => array( 'portable_form' => 'object', 'target_form' => 'object', 'unsupported_fields.*' => 'object' ) ),
		'wp-agent/get-site-context' => array( 'input' => array(), 'output' => array( 'agent_context.sections.*' => 'object' ) ),
	);
}

/** @param string[] $path */
function wpagent_companion_schema_document_path( array $path ): string {
	return implode( '.', $path );
}

/** @param string[] $path */
function wpagent_companion_schema_document_is_allowlisted( string $ability, string $direction, array $path, string $kind ): bool {
	$allowlist = wpagent_companion_schema_document_allowlist();
	return isset( $allowlist[ $ability ][ $direction ][ wpagent_companion_schema_document_path( $path ) ] )
		&& $kind === $allowlist[ $ability ][ $direction ][ wpagent_companion_schema_document_path( $path ) ];
}

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

/** @return array<string,mixed> */
function wpagent_companion_schema_map_property_names_schema(): array {
	return array(
		'type'      => 'string',
		'minLength' => 1,
		'maxLength' => WPAGENT_COMPANION_SCHEMA_MAX_PROPERTY_NAME_BYTES,
		'pattern'   => '^[^\\x00-\\x1F\\x7F]{1,191}$',
	);
}

/** @return WP_Error */
function wpagent_companion_schema_error( string $code, string $path ) {
	return new WP_Error(
		$code,
		sprintf(
			/* translators: %s: JSON schema path. */
			__( 'Die öffentliche Ability-Schemaform ist an %s nicht endlich beschrieben.', 'wp-agent-companion' ),
			$path
		),
		array( 'status' => 500, 'path' => $path )
	);
}

/**
 * Clamp a declared non-negative schema limit to the Companion-wide ceiling.
 *
 * @return int|WP_Error
 */
function wpagent_companion_schema_limit( $value, int $fallback, int $ceiling, string $path ) {
	if ( null === $value ) {
		return $fallback;
	}
	if ( ! is_int( $value ) || $value < 0 ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_limit', $path );
	}
	return min( $value, $ceiling );
}

/** @return true|WP_Error */
function wpagent_companion_schema_payload_limits( $value, int $depth = 0, int &$nodes = 0 ) {
	if ( $depth > WPAGENT_COMPANION_SCHEMA_MAX_DEPTH || ++$nodes > WPAGENT_COMPANION_SCHEMA_MAX_VALUE_NODES ) {
		return new WP_Error( 'wpagent_companion_schema_payload_nodes', __( 'Die öffentliche Eingabe oder Ausgabe ist zu tief oder enthält zu viele Werte.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( ! is_array( $value ) ) {
		return is_resource( $value ) ? new WP_Error( 'wpagent_companion_schema_payload_type', __( 'Die öffentliche Eingabe oder Ausgabe enthält einen nicht serialisierbaren Wert.', 'wp-agent-companion' ), array( 'status' => 400 ) ) : true;
	}
	foreach ( $value as $item ) {
		$valid = wpagent_companion_schema_payload_limits( $item, $depth + 1, $nodes );
		if ( is_wp_error( $valid ) ) {
			return $valid;
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_companion_schema_payload_size( $value ) {
	$nodes = 0;
	$valid = wpagent_companion_schema_payload_limits( $value, 0, $nodes );
	if ( is_wp_error( $valid ) ) {
		return $valid;
	}
	$encoded = wp_json_encode( $value );
	if ( ! is_string( $encoded ) || strlen( $encoded ) > WPAGENT_COMPANION_SCHEMA_MAX_PAYLOAD_BYTES ) {
		return new WP_Error( 'wpagent_companion_schema_payload_bytes', __( 'Die öffentliche Eingabe oder Ausgabe überschreitet die sichere Gesamtgröße.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	return true;
}

/** @return 'string'|'integer'|'number'|'boolean'|'null'|null */
function wpagent_companion_schema_scalar_type( $value ): ?string {
	if ( is_string( $value ) ) {
		return 'string';
	}
	if ( is_int( $value ) ) {
		return 'integer';
	}
	if ( is_float( $value ) ) {
		return 'number';
	}
	if ( is_bool( $value ) ) {
		return 'boolean';
	}
	return is_null( $value ) ? 'null' : null;
}

/** @return 'object'|'array'|'string'|'integer'|'number'|'boolean'|'null'|null|WP_Error */
function wpagent_companion_schema_infer_type( array $schema, string $path ) {
	$has_properties = array_key_exists( 'properties', $schema );
	$has_items      = array_key_exists( 'items', $schema );
	if ( $has_properties && $has_items ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
	}
	if ( $has_properties ) {
		return 'object';
	}
	if ( $has_items ) {
		return 'array';
	}

	$inferred = null;
	$accept   = static function ( ?string $candidate ) use ( &$inferred, $path ) {
		if ( null === $candidate || ( null !== $inferred && $inferred !== $candidate ) ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
		}
		$inferred = $candidate;
		return true;
	};
	if ( array_key_exists( 'enum', $schema ) ) {
		if ( ! is_array( $schema['enum'] ) || array() === $schema['enum'] ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
		}
		foreach ( $schema['enum'] as $value ) {
			$accepted = $accept( wpagent_companion_schema_scalar_type( $value ) );
			if ( is_wp_error( $accepted ) ) {
				return $accepted;
			}
		}
	}
	foreach ( array( 'const', 'default' ) as $keyword ) {
		if ( ! array_key_exists( $keyword, $schema ) ) {
			continue;
		}
		$accepted = $accept( wpagent_companion_schema_scalar_type( $schema[ $keyword ] ) );
		if ( is_wp_error( $accepted ) ) {
			return $accepted;
		}
	}
	foreach ( array( 'format', 'pattern' ) as $keyword ) {
		if ( ! array_key_exists( $keyword, $schema ) ) {
			continue;
		}
		if ( ! is_string( $schema[ $keyword ] ) || '' === $schema[ $keyword ] ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
		}
		$accepted = $accept( 'string' );
		if ( is_wp_error( $accepted ) ) {
			return $accepted;
		}
	}
	return $inferred;
}

/**
 * JSON Schema ignores object and array keywords for an incompatible union
 * member. Removing those inert keywords keeps the published branch finite
 * without changing the union's accepted values.
 *
 * @param array<string,mixed> $schema
 * @return array<string,mixed>
 */
function wpagent_companion_schema_branch_for_type( array $schema, string $type ): array {
	if ( 'object' !== $type ) {
		foreach ( array( 'properties', 'required', 'additionalProperties', 'propertyNames', 'patternProperties', 'maxProperties', 'minProperties' ) as $keyword ) {
			unset( $schema[ $keyword ] );
		}
	}
	if ( 'array' !== $type ) {
		foreach ( array( 'items', 'maxItems', 'minItems', 'contains', 'minContains', 'maxContains' ) as $keyword ) {
			unset( $schema[ $keyword ] );
		}
	}
	return $schema;
}

/**
 * Recursively closes a declared JSON schema without changing its required set.
 *
 * An object is safe when it declares properties, explicitly declares itself as
 * empty, or supplies an explicit bounded value schema for a map. A bare object
 * and an array without items are ambiguous provider payloads and fail closed.
 *
 * @return array<string,mixed>|WP_Error
 */
function wpagent_companion_harden_schema( array $schema, string $ability, string $direction, array $schema_path = array(), int $depth = 0, array &$documents = array() ) {
	$path = $ability . '.' . $direction . '.' . wpagent_companion_schema_document_path( $schema_path );
	if ( $depth > WPAGENT_COMPANION_SCHEMA_MAX_DEPTH ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_depth', $path );
	}

	foreach ( array( 'oneOf', 'anyOf', 'allOf' ) as $keyword ) {
		if ( ! isset( $schema[ $keyword ] ) ) {
			continue;
		}
		if ( ! is_array( $schema[ $keyword ] ) || array() === $schema[ $keyword ] || count( $schema[ $keyword ] ) > WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_composition', $path . '.' . $keyword );
		}
		$branches = array();
		foreach ( $schema[ $keyword ] as $index => $branch ) {
			if ( ! is_array( $branch ) ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_composition', $path . '.' . $keyword . '[' . $index . ']' );
			}
			$hardened = wpagent_companion_harden_schema( $branch, $ability, $direction, $schema_path, $depth + 1, $documents );
			if ( is_wp_error( $hardened ) ) {
				return $hardened;
			}
			// A nested anyOf used solely as a branch is semantically equivalent to
			// its flattened alternatives. Flattening keeps every published branch
			// explicitly typed without combining constraints from sibling keywords.
			if ( 'anyOf' === $keyword && array_keys( $hardened ) === array( 'anyOf' ) ) {
				foreach ( $hardened['anyOf'] as $nested ) {
					$branches[] = $nested;
				}
			} else {
				$branches[] = $hardened;
			}
		}
		if ( count( $branches ) > WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_composition', $path . '.' . $keyword );
		}
		$schema[ $keyword ] = $branches;
	}

	$has_type = array_key_exists( 'type', $schema );
	$type     = $schema['type'] ?? null;
	if ( is_array( $type ) ) {
		if ( array() === $type || count( $type ) > 8 ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_type_union', $path );
		}
		$branches = array();
		foreach ( $type as $member ) {
			if ( ! is_string( $member ) || '' === $member ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_type_union', $path );
			}
			$branch         = wpagent_companion_schema_branch_for_type( $schema, $member );
			$branch['type'] = $member;
			$hardened       = wpagent_companion_harden_schema( $branch, $ability, $direction, $schema_path, $depth + 1, $documents );
			if ( is_wp_error( $hardened ) ) {
				return $hardened;
			}
			$branches[] = $hardened;
		}
		return array( 'anyOf' => $branches );
	}

	if ( ! $has_type ) {
		$inferred = wpagent_companion_schema_infer_type( $schema, $path );
		if ( is_wp_error( $inferred ) ) {
			return $inferred;
		}
		if ( is_string( $inferred ) ) {
			$schema['type'] = $inferred;
			$type           = $inferred;
		} elseif ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) || isset( $schema['allOf'] ) ) {
			return $schema;
		} else {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
		}
	}
	if ( ! is_string( $type ) || ! in_array( $type, array( 'object', 'array', 'string', 'integer', 'number', 'boolean', 'null' ), true ) ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_type', $path );
	}

	if ( 'object' === $type ) {
		$has_properties = array_key_exists( 'properties', $schema );
		$properties     = $schema['properties'] ?? null;
		$additional     = $schema['additionalProperties'] ?? null;
		if ( $has_properties && ! is_array( $properties ) ) {
			return wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path );
		}
		if ( true === $additional || ( null !== $additional && false !== $additional && ! is_array( $additional ) ) ) {
			if ( wpagent_companion_schema_document_is_allowlisted( $ability, $direction, $schema_path, 'object' ) ) {
				$documents[] = array( 'path' => $schema_path, 'kind' => 'object' );
				return wpagent_companion_schema_document_schema( 'object' );
			}
			return wpagent_companion_schema_error( 'wpagent_companion_schema_dynamic_object', $path );
		}
		if ( ! $has_properties && null === $additional ) {
			if ( wpagent_companion_schema_document_is_allowlisted( $ability, $direction, $schema_path, 'object' ) ) {
				$documents[] = array( 'path' => $schema_path, 'kind' => 'object' );
				return wpagent_companion_schema_document_schema( 'object' );
			}
			return wpagent_companion_schema_error( 'wpagent_companion_schema_dynamic_object', $path );
		}

		$schema['properties'] = is_array( $properties ) ? array() : array();
		foreach ( (array) $properties as $name => $child ) {
			if ( ! is_string( $name ) || '' === $name || ! is_array( $child ) ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path );
			}
			$hardened = wpagent_companion_harden_schema( $child, $ability, $direction, array_merge( $schema_path, array( $name ) ), $depth + 1, $documents );
			if ( is_wp_error( $hardened ) ) {
				return $hardened;
			}
			$schema['properties'][ $name ] = $hardened;
		}

		if ( is_array( $additional ) ) {
			$hardened_additional = wpagent_companion_harden_schema( $additional, $ability, $direction, array_merge( $schema_path, array( '@' ) ), $depth + 1, $documents );
			if ( is_wp_error( $hardened_additional ) ) {
				return $hardened_additional;
			}
			$property_names = $schema['propertyNames'] ?? wpagent_companion_schema_map_property_names_schema();
			if ( ! is_array( $property_names ) ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path . '.propertyNames' );
			}
			$property_names = wpagent_companion_harden_schema( $property_names, $ability, $direction, array_merge( $schema_path, array( '@key' ) ), $depth + 1, $documents );
			if ( is_wp_error( $property_names ) || 'string' !== ( $property_names['type'] ?? null ) ) {
				return is_wp_error( $property_names ) ? $property_names : wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path . '.propertyNames' );
			}
			$minimum = $property_names['minLength'] ?? 1;
			if ( ! is_int( $minimum ) || $minimum < 1 ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path . '.propertyNames.minLength' );
			}
			$maximum = wpagent_companion_schema_limit( $property_names['maxLength'] ?? null, WPAGENT_COMPANION_SCHEMA_MAX_PROPERTY_NAME_BYTES, WPAGENT_COMPANION_SCHEMA_MAX_PROPERTY_NAME_BYTES, $path . '.propertyNames.maxLength' );
			if ( is_wp_error( $maximum ) || $maximum < $minimum ) {
				return is_wp_error( $maximum ) ? $maximum : wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path . '.propertyNames.maxLength' );
			}
			if ( isset( $property_names['pattern'] ) && ( ! is_string( $property_names['pattern'] ) || '' === $property_names['pattern'] ) ) {
				return wpagent_companion_schema_error( 'wpagent_companion_schema_properties', $path . '.propertyNames.pattern' );
			}
			$property_names['minLength'] = $minimum;
			$property_names['maxLength'] = $maximum;
			$property_names['pattern']   = $property_names['pattern'] ?? wpagent_companion_schema_map_property_names_schema()['pattern'];
			$schema['additionalProperties'] = $hardened_additional;
			$schema['propertyNames']        = $property_names;
			$default_properties              = WPAGENT_COMPANION_SCHEMA_MAX_PROPERTIES;
		} else {
			$schema['additionalProperties'] = false;
			$default_properties              = count( $schema['properties'] );
		}
		$maximum = wpagent_companion_schema_limit( $schema['maxProperties'] ?? null, $default_properties, WPAGENT_COMPANION_SCHEMA_MAX_PROPERTIES, $path . '.maxProperties' );
		if ( is_wp_error( $maximum ) ) {
			return $maximum;
		}
		$schema['maxProperties'] = $maximum;
		return $schema;
	}

	if ( 'array' === $type ) {
		if ( ! isset( $schema['items'] ) || ! is_array( $schema['items'] ) ) {
			if ( wpagent_companion_schema_document_is_allowlisted( $ability, $direction, $schema_path, 'array' ) ) {
				$documents[] = array( 'path' => $schema_path, 'kind' => 'array' );
				return wpagent_companion_schema_document_schema( 'array' );
			}
			return wpagent_companion_schema_error( 'wpagent_companion_schema_array_items', $path );
		}
		$items = wpagent_companion_harden_schema( $schema['items'], $ability, $direction, array_merge( $schema_path, array( '*' ) ), $depth + 1, $documents );
		if ( is_wp_error( $items ) ) {
			return $items;
		}
		$maximum = wpagent_companion_schema_limit( $schema['maxItems'] ?? null, WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS, WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS, $path . '.maxItems' );
		if ( is_wp_error( $maximum ) ) {
			return $maximum;
		}
		$schema['items']    = $items;
		$schema['maxItems'] = $maximum;
		return $schema;
	}

	if ( 'string' === $type && ! isset( $schema['enum'] ) ) {
		$maximum = wpagent_companion_schema_limit( $schema['maxLength'] ?? null, WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES, WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES, $path . '.maxLength' );
		if ( is_wp_error( $maximum ) ) {
			return $maximum;
		}
		$schema['maxLength'] = $maximum;
	}
	return $schema;
}

/** @return array<mixed>|WP_Error */
function wpagent_companion_schema_canonical_document_value( $value, int $depth = 0, int &$nodes = 0 ) {
	if ( $depth > WPAGENT_COMPANION_SCHEMA_MAX_DEPTH || ++$nodes > WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS ) {
		return new WP_Error( 'wpagent_companion_schema_document_depth', __( 'Das dynamische Dokument ist zu tief oder zu groß verschachtelt.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( is_string( $value ) ) {
		return strlen( $value ) <= WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES ? $value : new WP_Error( 'wpagent_companion_schema_document_string', __( 'Ein dynamischer Dokumenttext überschreitet die sichere Grenze.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( is_int( $value ) || is_bool( $value ) || is_null( $value ) ) {
		return $value;
	}
	if ( is_float( $value ) ) {
		return is_finite( $value ) ? $value : new WP_Error( 'wpagent_companion_schema_document_number', __( 'Ein dynamisches Dokument enthält eine ungültige Zahl.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( ! is_array( $value ) || count( $value ) > WPAGENT_COMPANION_SCHEMA_MAX_ARRAY_ITEMS ) {
		return new WP_Error( 'wpagent_companion_schema_document_value', __( 'Das dynamische Dokument enthält keinen erlaubten JSON-Wert.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	if ( array_is_list( $value ) ) {
		$result = array();
		foreach ( $value as $item ) {
			$canonical = wpagent_companion_schema_canonical_document_value( $item, $depth + 1, $nodes );
			if ( is_wp_error( $canonical ) ) {
				return $canonical;
			}
			$result[] = $canonical;
		}
		return $result;
	}
	$result = array();
	foreach ( $value as $key => $item ) {
		if ( ! is_string( $key ) && ! is_int( $key ) ) {
			return new WP_Error( 'wpagent_companion_schema_document_key', __( 'Ein dynamisches Dokument enthält einen ungültigen Schlüssel.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		$canonical = wpagent_companion_schema_canonical_document_value( $item, $depth + 1, $nodes );
		if ( is_wp_error( $canonical ) ) {
			return $canonical;
		}
		$result[ (string) $key ] = $canonical;
	}
	ksort( $result, SORT_STRING );
	return $result;
}

/** @return array{format:string,json:string,sha256:string,bytes:int}|WP_Error */
function wpagent_companion_schema_document_encode( $value, string $kind ) {
	if ( ! is_array( $value ) || ( 'array' === $kind && ! array_is_list( $value ) ) || ( 'object' === $kind && array_is_list( $value ) && array() !== $value ) ) {
		return new WP_Error( 'wpagent_companion_schema_document_kind', __( 'Der dynamische Providerwert besitzt nicht die erwartete JSON-Form.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$nodes     = 0;
	$canonical = wpagent_companion_schema_canonical_document_value( $value, 0, $nodes );
	if ( is_wp_error( $canonical ) ) {
		return $canonical;
	}
	try {
		$json = 'object' === $kind && array() === $canonical ? '{}' : json_encode( $canonical, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR );
	} catch ( JsonException ) {
		return new WP_Error( 'wpagent_companion_schema_document_encode', __( 'Der dynamische Providerwert konnte nicht sicher serialisiert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$bytes = strlen( $json );
	if ( $bytes < 2 || $bytes > WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES ) {
		return new WP_Error( 'wpagent_companion_schema_document_size', __( 'Der dynamische Providerwert überschreitet die öffentliche Größenbegrenzung.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	return array( 'format' => 'wpagently-json-' . $kind . '-v1', 'json' => $json, 'sha256' => hash( 'sha256', $json ), 'bytes' => $bytes );
}

/** @return array<mixed>|WP_Error */
function wpagent_companion_schema_document_decode( $document, string $kind ) {
	$format = 'wpagently-json-' . $kind . '-v1';
	if ( ! is_array( $document ) || 4 !== count( $document ) || array_diff( array_keys( $document ), array( 'format', 'json', 'sha256', 'bytes' ) )
		|| ! is_string( $document['format'] ?? null ) || ! is_string( $document['json'] ?? null ) || ! is_string( $document['sha256'] ?? null ) || ! is_int( $document['bytes'] ?? null ) ) {
		return new WP_Error( 'wpagent_companion_schema_document', __( 'Dynamische Werte müssen den geschlossenen Integritätsumschlag verwenden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$json = $document['json'];
	if ( $format !== $document['format'] || strlen( $json ) < 2 || strlen( $json ) > WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES || strlen( $json ) !== $document['bytes'] || 1 !== preg_match( '/^[a-f0-9]{64}$/', $document['sha256'] ) || ! hash_equals( $document['sha256'], hash( 'sha256', $json ) ) ) {
		return new WP_Error( 'wpagent_companion_schema_document_integrity', __( 'Die Integritätsdaten des dynamischen Werts stimmen nicht überein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	try {
		$value = json_decode( $json, true, WPAGENT_COMPANION_SCHEMA_MAX_DEPTH, JSON_THROW_ON_ERROR );
	} catch ( JsonException ) {
		return new WP_Error( 'wpagent_companion_schema_document_json', __( 'Der dynamische Wert enthält kein gültiges JSON.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$first = substr( ltrim( $json ), 0, 1 );
	if ( ! is_array( $value ) || ( 'object' === $kind && '{' !== $first ) || ( 'array' === $kind && '[' !== $first ) ) {
		return new WP_Error( 'wpagent_companion_schema_document_kind', __( 'Der dynamische Wert besitzt nicht die erwartete JSON-Form.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$nodes     = 0;
	$canonical = wpagent_companion_schema_canonical_document_value( $value, 0, $nodes );
	return is_wp_error( $canonical ) ? $canonical : $value;
}

/** @param string[] $path */
function wpagent_companion_schema_path_allows_null( array $schema, array $path ): bool {
	if ( isset( $schema['anyOf'] ) ) {
		foreach ( $schema['anyOf'] as $branch ) {
			if ( is_array( $branch ) && wpagent_companion_schema_path_allows_null( $branch, $path ) ) {
				return true;
			}
		}
		return false;
	}
	if ( isset( $schema['oneOf'] ) ) {
		$matches = 0;
		foreach ( $schema['oneOf'] as $branch ) {
			$matches += (int) ( is_array( $branch ) && wpagent_companion_schema_path_allows_null( $branch, $path ) );
		}
		return 1 === $matches;
	}
	if ( isset( $schema['allOf'] ) ) {
		foreach ( $schema['allOf'] as $branch ) {
			if ( ! is_array( $branch ) || ! wpagent_companion_schema_path_allows_null( $branch, $path ) ) {
				return false;
			}
		}
		return true;
	}
	if ( array() === $path ) {
		return 'null' === ( $schema['type'] ?? null ) || ( is_array( $schema['type'] ?? null ) && in_array( 'null', $schema['type'], true ) );
	}
	$key = array_shift( $path );
	if ( '*' === $key ) {
		$child = 'array' === ( $schema['type'] ?? null ) ? ( $schema['items'] ?? null ) : null;
	} elseif ( '@' === $key ) {
		$child = 'object' === ( $schema['type'] ?? null ) ? ( $schema['additionalProperties'] ?? null ) : null;
	} else {
		$child = 'object' === ( $schema['type'] ?? null ) ? ( $schema['properties'][ $key ] ?? null ) : null;
	}
	return is_array( $child ) && wpagent_companion_schema_path_allows_null( $child, $path );
}

/** @param string[] $path @return mixed|WP_Error */
function wpagent_companion_schema_transform_document_at_path( $value, array $path, string $kind, bool $encode, bool $preserve_null = false ) {
	if ( array() === $path ) {
		if ( $encode && null === $value && $preserve_null ) {
			return null;
		}
		return $encode ? wpagent_companion_schema_document_encode( $value, $kind ) : wpagent_companion_schema_document_decode( $value, $kind );
	}
	if ( ! is_array( $value ) ) {
		return new WP_Error( 'wpagent_companion_schema_document_path', __( 'Der dynamische Wert liegt nicht an der erwarteten öffentlichen Schema-Position.', 'wp-agent-companion' ), array( 'status' => $encode ? 500 : 400 ) );
	}
	$key = array_shift( $path );
	if ( '*' === $key || '@' === $key ) {
		foreach ( $value as $index => $item ) {
			$transformed = wpagent_companion_schema_transform_document_at_path( $item, $path, $kind, $encode, $preserve_null );
			if ( is_wp_error( $transformed ) ) {
				return $transformed;
			}
			$value[ $index ] = $transformed;
		}
		return $value;
	}
	if ( ! array_key_exists( $key, $value ) ) {
		return $value;
	}
	$transformed = wpagent_companion_schema_transform_document_at_path( $value[ $key ], $path, $kind, $encode, $preserve_null );
	if ( is_wp_error( $transformed ) ) {
		return $transformed;
	}
	$value[ $key ] = $transformed;
	return $value;
}

/** @param array<int,array{path:array<int,string>,kind:string}> $documents @param array<string,mixed>|null $published_schema @return mixed|WP_Error */
function wpagent_companion_schema_transform_documents( $value, array $documents, bool $encode, ?array $published_schema = null ) {
	$unique = array();
	foreach ( $documents as $document ) {
		$key = $document['kind'] . "\0" . implode( "\0", $document['path'] );
		$unique[ $key ] = $document;
	}
	foreach ( $unique as $document ) {
		$preserve_null = $encode && is_array( $published_schema ) && wpagent_companion_schema_path_allows_null( $published_schema, $document['path'] );
		$value = wpagent_companion_schema_transform_document_at_path( $value, $document['path'], $document['kind'], $encode, $preserve_null );
		if ( is_wp_error( $value ) ) {
			return $value;
		}
	}
	return $value;
}

/** @param array<string,mixed> $schema */
function wpagent_companion_schema_valid_property_name( string $name, array $schema ): bool {
	$length = strlen( $name );
	if ( $length < (int) ( $schema['minLength'] ?? 0 ) || $length > (int) ( $schema['maxLength'] ?? WPAGENT_COMPANION_SCHEMA_MAX_PROPERTY_NAME_BYTES ) ) {
		return false;
	}
	if ( isset( $schema['enum'] ) && ( ! is_array( $schema['enum'] ) || ! in_array( $name, $schema['enum'], true ) ) ) {
		return false;
	}
	if ( array_key_exists( 'const', $schema ) && $name !== $schema['const'] ) {
		return false;
	}
	if ( isset( $schema['pattern'] ) ) {
		if ( ! is_string( $schema['pattern'] ) || 1 !== @preg_match( '~' . str_replace( '~', '\\~', $schema['pattern'] ) . '~D', $name ) ) {
			return false;
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_companion_validate_schema_limits( $value, array $schema, string $path = 'input', int $depth = 0 ) {
	if ( $depth > WPAGENT_COMPANION_SCHEMA_MAX_DEPTH ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_depth', $path );
	}
	foreach ( array( 'oneOf', 'anyOf' ) as $keyword ) {
		if ( ! isset( $schema[ $keyword ] ) ) {
			continue;
		}
		foreach ( $schema[ $keyword ] as $candidate ) {
			if ( is_array( $candidate ) && ! is_wp_error( wpagent_companion_validate_schema_limits( $value, $candidate, $path, $depth + 1 ) ) ) {
				return true;
			}
		}
		return new WP_Error( 'wpagent_companion_schema_value', __( 'Ein Wert passt zu keiner erlaubten öffentlichen Schemaform.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
	}
	if ( isset( $schema['allOf'] ) ) {
		foreach ( $schema['allOf'] as $candidate ) {
			$valid = wpagent_companion_validate_schema_limits( $value, $candidate, $path, $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}

	$type = $schema['type'] ?? null;
	if ( 'string' === $type && ( ! is_string( $value ) || strlen( $value ) > (int) ( $schema['maxLength'] ?? WPAGENT_COMPANION_SCHEMA_MAX_STRING_BYTES ) ) ) {
		return new WP_Error( 'wpagent_companion_schema_string_limit', __( 'Ein öffentlicher Textwert überschreitet die erlaubte Grenze.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
	}
	if ( 'array' === $type && ( ! is_array( $value ) || ! array_is_list( $value ) || count( $value ) > (int) $schema['maxItems'] ) ) {
		return new WP_Error( 'wpagent_companion_schema_array_limit', __( 'Eine öffentliche Liste überschreitet die erlaubte Grenze.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
	}
	if ( 'array' === $type ) {
		foreach ( $value as $index => $item ) {
			$valid = wpagent_companion_validate_schema_limits( $item, $schema['items'], $path . '[' . $index . ']', $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	if ( 'object' === $type && is_array( $value ) ) {
		if ( count( $value ) > (int) $schema['maxProperties'] ) {
			return new WP_Error( 'wpagent_companion_schema_properties_limit', __( 'Ein öffentliches Objekt enthält zu viele Eigenschaften.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
		}
		$properties = $schema['properties'] ?? array();
		$property_names = $schema['propertyNames'] ?? null;
		foreach ( (array) ( $schema['required'] ?? array() ) as $required ) {
			if ( ! array_key_exists( $required, $value ) ) {
				return new WP_Error( 'wpagent_companion_schema_required', __( 'Eine erforderliche öffentliche Eigenschaft fehlt.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
			}
		}
		foreach ( $value as $key => $item ) {
			if ( is_array( $property_names ) ) {
				$property_name = (string) $key;
				$name_valid    = rest_validate_value_from_schema( $property_name, $property_names, $path . '.<key>' );
				if ( is_wp_error( $name_valid ) || ! wpagent_companion_schema_valid_property_name( $property_name, $property_names ) || is_wp_error( wpagent_companion_validate_schema_limits( $property_name, $property_names, $path . '.<key>', $depth + 1 ) ) ) {
					return new WP_Error( 'wpagent_companion_schema_property_name', __( 'Ein dynamischer öffentlicher Objektschlüssel ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
				}
			}
			if ( array_key_exists( $key, $properties ) ) {
				$child = $properties[ $key ];
			} elseif ( is_array( $schema['additionalProperties'] ?? null ) ) {
				$child = $schema['additionalProperties'];
			} else {
				return new WP_Error( 'wpagent_companion_schema_extra_property', __( 'Eine unbekannte öffentliche Eigenschaft ist nicht erlaubt.', 'wp-agent-companion' ), array( 'status' => 400, 'path' => $path ) );
			}
			$valid = wpagent_companion_validate_schema_limits( $item, $child, $path . '.' . $key, $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_companion_validate_schema_value( $value, array $schema, string $path = 'input' ) {
	if ( ! function_exists( 'rest_validate_value_from_schema' ) ) {
		return new WP_Error( 'wpagent_companion_schema_validator_missing', __( 'Der WordPress-Schema-Validator ist nicht verfügbar.', 'wp-agent-companion' ), array( 'status' => 500 ) );
	}
	$payload = wpagent_companion_schema_payload_size( $value );
	if ( is_wp_error( $payload ) ) {
		return $payload;
	}
	$valid = rest_validate_value_from_schema( $value, $schema, $path );
	return is_wp_error( $valid ) ? $valid : wpagent_companion_validate_schema_limits( $value, $schema, $path );
}

/**
 * Returns only non-sensitive validator diagnostics. Provider values and schema
 * fragments must never be copied into a public REST error response.
 *
 * @return array{status:int,ability:string,validation_code:string,validation_path?:string}
 */
function wpagent_companion_schema_output_error_data( string $ability, WP_Error $error ): array {
	$data = array(
		'status'          => 500,
		'ability'         => $ability,
		'validation_code' => $error->get_error_code(),
	);
	$error_data = $error->get_error_data();
	$path       = is_array( $error_data ) && is_string( $error_data['path'] ?? null )
		? $error_data['path']
		: ( is_array( $error_data ) && is_string( $error_data['param'] ?? null ) ? $error_data['param'] : '' );
	if ( '' !== $path && preg_match( '/^output(?:[.\[][A-Za-z0-9_.\[\]<>-]+)?$/', $path ) ) {
		$data['validation_path'] = $path;
	}
	return $data;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_companion_harden_ability_definition( string $name, array $definition ) {
	$input_schema  = $definition['input_schema'] ?? null;
	$output_schema = $definition['output_schema'] ?? null;
	if ( ! is_array( $input_schema ) || ! is_array( $output_schema ) ) {
		return wpagent_companion_schema_error( 'wpagent_companion_schema_definition', $name );
	}
	$input_documents  = array();
	$output_documents = array();
	$input_schema = wpagent_companion_harden_schema( $input_schema, $name, 'input', array(), 0, $input_documents );
	if ( is_wp_error( $input_schema ) ) {
		return $input_schema;
	}
	$output_schema = wpagent_companion_harden_schema( $output_schema, $name, 'output', array(), 0, $output_documents );
	if ( is_wp_error( $output_schema ) ) {
		return $output_schema;
	}
	$definition['input_schema']  = $input_schema;
	$definition['output_schema'] = $output_schema;
	$callback                    = $definition['execute_callback'] ?? null;
	if ( ! is_callable( $callback ) ) {
		return $definition;
	}
	$definition['execute_callback'] = static function ( $input ) use ( $callback, $input_schema, $output_schema, $input_documents, $output_documents, $name ) {
		$valid = wpagent_companion_validate_schema_value( $input, $input_schema );
		if ( is_wp_error( $valid ) ) {
			return new WP_Error( 'wpagent_companion_schema_input_invalid', __( 'Die Eingabe entspricht nicht dem geschlossenen öffentlichen Schema.', 'wp-agent-companion' ), array( 'status' => 400, 'ability' => $name ) );
		}
		$input = wpagent_companion_schema_transform_documents( $input, $input_documents, false );
		if ( is_wp_error( $input ) ) {
			return $input;
		}
		$result = call_user_func( $callback, $input );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		$result = wpagent_companion_schema_transform_documents( $result, $output_documents, true, $output_schema );
		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'wpagent_companion_schema_output_invalid', __( 'Die Provider-Ausgabe überschreitet die geschlossene öffentliche Schemaform.', 'wp-agent-companion' ), array( 'status' => 500, 'ability' => $name ) );
		}
		$valid = wpagent_companion_validate_schema_value( $result, $output_schema, 'output' );
		return is_wp_error( $valid ) ? new WP_Error( 'wpagent_companion_schema_output_invalid', __( 'Die Provider-Ausgabe entspricht nicht dem geschlossenen öffentlichen Schema.', 'wp-agent-companion' ), wpagent_companion_schema_output_error_data( $name, $valid ) ) : $result;
	};
	return $definition;
}

function wpagent_companion_register_hardened_ability( string $name, array $definition ): void {
	$hardened = wpagent_companion_harden_ability_definition( $name, $definition );
	if ( is_wp_error( $hardened ) ) {
		_doing_it_wrong( __FUNCTION__, $hardened->get_error_message(), WPAGENT_COMPANION_VERSION );
		return;
	}
	wp_register_ability( $name, $hardened );
}
