<?php
/** Shared validation, redaction and transaction helpers for ACF field values. */

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

define( 'WPAGENT_ACF_VALUE_LOCK_TTL', 300 );
define( 'WPAGENT_ACF_VALUE_MAX_BYTES', 100000 );
define( 'WPAGENT_ACF_VALUE_MAX_NODES', 2000 );

/** @return array<string,mixed>|WP_Error */
function wpagent_acf_value_context_from_input( array $input ) {
	$has_post_id    = array_key_exists( 'post_id', $input );
	$has_object_type = array_key_exists( 'object_type', $input );
	$has_object_id   = array_key_exists( 'object_id', $input );
	if ( $has_post_id ) {
		$post_id = filter_var( $input['post_id'], FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) );
		if ( ! $post_id ) return new WP_Error( 'wpagent_acf_object_invalid', __( 'post_id muss eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		if ( $has_object_type || $has_object_id ) {
			$explicit_id = $has_object_id ? filter_var( $input['object_id'], FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) ) : false;
			if ( ! $has_object_type || ! $has_object_id || 'post' !== $input['object_type'] || ! $explicit_id || (int) $explicit_id !== (int) $post_id ) {
				return new WP_Error( 'wpagent_acf_object_conflict', __( 'post_id darf nur allein oder zusammen mit demselben post-Ziel verwendet werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
		}
		return wpagent_acf_value_context( 'post', (int) $post_id );
	}
	if ( ! $has_object_type || ! $has_object_id || ! is_string( $input['object_type'] ) ) {
		return new WP_Error( 'wpagent_acf_object_required', __( 'object_type und object_id müssen gemeinsam angegeben werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return wpagent_acf_value_context( $input['object_type'], $input['object_id'] );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_acf_value_context( string $object_type, $object_id ) {
	if ( ! in_array( $object_type, array( 'post', 'user', 'term', 'comment', 'options' ), true ) ) {
		return new WP_Error( 'wpagent_acf_object_type', __( 'object_type muss post, user, term, comment oder options sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( 'options' === $object_type ) {
		if ( ! is_string( $object_id ) || ! preg_match( '/^[A-Za-z0-9_-]{1,120}$/', $object_id ) ) {
			return new WP_Error( 'wpagent_acf_object_invalid', __( 'Für options muss object_id „options“ oder der registrierte ACF-Optionsseiten-Slug sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$page = null;
		if ( 'options' !== $object_id && function_exists( 'acf_get_options_page' ) ) {
			$page = acf_get_options_page( $object_id );
		}
		if ( 'options' !== $object_id && ! is_array( $page ) ) {
			return new WP_Error( 'wpagent_acf_options_page_missing', __( 'Die angegebene ACF-Optionsseite ist nicht registriert.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		}
		$acf_object_id = is_array( $page ) ? ( $page['post_id'] ?? 'options' ) : 'options';
		$acf_object_id = function_exists( 'acf_get_valid_post_id' ) ? acf_get_valid_post_id( $acf_object_id ) : $acf_object_id;
		$decoded       = function_exists( 'acf_decode_post_id' ) ? acf_decode_post_id( $acf_object_id ) : array( 'type' => 'option', 'id' => $acf_object_id );
		if ( 'option' !== (string) ( $decoded['type'] ?? '' ) || '' === (string) ( $decoded['id'] ?? '' ) ) {
			return new WP_Error( 'wpagent_acf_options_storage_invalid', __( 'Die ACF-Optionsseite verwendet keinen unterstützten Optionsspeicher.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		return array(
			'object_type' => 'options', 'object_id' => $object_id, 'acf_object_id' => $acf_object_id,
			'group_args' => array( 'options_page' => $object_id ), 'post_id' => 0, 'side_effect_object_id' => 0,
			'capability' => is_array( $page ) && ! empty( $page['capability'] ) ? (string) $page['capability'] : 'manage_options',
			'generic_options' => 'options' === $object_id && ! is_array( $page ),
		);
	}
	$id = filter_var( $object_id, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1 ) ) );
	if ( ! $id ) return new WP_Error( 'wpagent_acf_object_invalid', __( 'object_id muss für dieses Ziel eine positive Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	$id = (int) $id;
	if ( 'post' === $object_type ) {
		$post = get_post( $id );
		if ( ! $post ) return new WP_Error( 'wpagent_no_post', __( 'Beitrag nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		return array( 'object_type' => 'post', 'object_id' => $id, 'acf_object_id' => $id, 'group_args' => array( 'post_id' => $id, 'post_type' => $post->post_type ), 'post_id' => $id, 'side_effect_object_id' => $id, 'capability' => array( 'edit_post', $id ), 'generic_options' => false );
	}
	if ( 'user' === $object_type ) {
		if ( ! get_userdata( $id ) ) return new WP_Error( 'wpagent_no_user', __( 'Benutzer nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		return array( 'object_type' => 'user', 'object_id' => $id, 'acf_object_id' => 'user_' . $id, 'group_args' => array( 'user_id' => $id, 'user_form' => 'edit' ), 'post_id' => 0, 'side_effect_object_id' => $id, 'capability' => array( 'edit_user', $id ), 'generic_options' => false );
	}
	if ( 'term' === $object_type ) {
		$term = get_term( $id );
		if ( ! $term instanceof WP_Term ) return new WP_Error( 'wpagent_no_term', __( 'Begriff nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
		return array( 'object_type' => 'term', 'object_id' => $id, 'acf_object_id' => 'term_' . $id, 'group_args' => array( 'taxonomy' => $term->taxonomy ), 'post_id' => 0, 'side_effect_object_id' => $id, 'capability' => array( 'edit_term', $id ), 'generic_options' => false );
	}
	$comment = get_comment( $id );
	if ( ! $comment ) return new WP_Error( 'wpagent_no_comment', __( 'Kommentar nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	return array( 'object_type' => 'comment', 'object_id' => $id, 'acf_object_id' => 'comment_' . $id, 'group_args' => array( 'comment' => get_post_type( (int) $comment->comment_post_ID ) ), 'post_id' => 0, 'side_effect_object_id' => $id, 'capability' => array( 'edit_comment', $id ), 'generic_options' => false );
}

function wpagent_acf_value_can_edit_context( array $context ): bool {
	$capability = $context['capability'] ?? 'do_not_allow';
	$allowed    = is_array( $capability ) ? current_user_can( (string) $capability[0], (int) $capability[1] ) : current_user_can( (string) $capability );
	if ( ! $allowed ) return false;
	if ( function_exists( 'acf_decode_post_id' ) && function_exists( 'acf_current_user_can_edit_in_context' ) ) {
		$decoded = acf_decode_post_id( $context['acf_object_id'] );
		if ( ! is_array( $decoded ) ) return false;
		$options_page = 'options' === (string) ( $context['object_type'] ?? '' ) ? (string) ( $context['object_id'] ?? '' ) : '';
		return acf_current_user_can_edit_in_context( $decoded, $options_page );
	}
	return true;
}

function wpagent_perm_acf_values( $input = null ): bool {
	$context = wpagent_acf_value_context_from_input( is_array( $input ) ? $input : array() );
	// Malformed or missing targets are rejected by the execute callback with a
	// precise 4xx error. The permission layer still requires authentication and
	// never grants access to a valid target without its object-specific cap.
	return is_wp_error( $context ) ? current_user_can( 'read' ) : wpagent_acf_value_can_edit_context( $context );
}

/** @return array<int,array<string,mixed>> */
function wpagent_acf_value_generic_options_groups( string $slug ): array {
	$matched = array();
	$groups  = acf_get_field_groups();
	foreach ( is_array( $groups ) ? $groups : array() as $group ) {
		if ( ! is_array( $group ) ) continue;
		if ( empty( $group['active'] ) ) continue;
		foreach ( is_array( $group['location'] ?? null ) ? $group['location'] : array() as $rules ) {
			if ( 1 !== count( is_array( $rules ) ? $rules : array() ) ) continue;
			$rule = $rules[0] ?? null;
			if ( is_array( $rule ) && 'options_page' === (string) ( $rule['param'] ?? '' ) && '==' === (string) ( $rule['operator'] ?? '==' ) && $slug === (string) ( $rule['value'] ?? '' ) ) {
				$matched[] = $group;
				break;
			}
		}
	}
	return $matched;
}

/** @return array<string,array<string,mixed>>|WP_Error */
function wpagent_acf_value_assigned_fields( array $context ) {
	if ( ! function_exists( 'acf_get_field_groups' ) || ! function_exists( 'acf_get_fields' ) || ! function_exists( 'get_field' ) ) {
		return new WP_Error( 'wpagent_acf_missing', __( 'Advanced Custom Fields ist nicht aktiv.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$fields = array();
	$groups = ! empty( $context['generic_options'] ) ? wpagent_acf_value_generic_options_groups( (string) $context['object_id'] ) : acf_get_field_groups( $context['group_args'] );
	foreach ( is_array( $groups ) ? $groups : array() as $group ) {
		if ( ! is_array( $group ) ) continue;
		$group_fields = acf_get_fields( $group );
		foreach ( is_array( $group_fields ) ? $group_fields : array() as $field ) {
			$key  = is_array( $field ) ? (string) ( $field['key'] ?? '' ) : '';
			$name = is_array( $field ) ? (string) ( $field['name'] ?? '' ) : '';
			if ( preg_match( '/^field_[A-Za-z0-9_-]{1,120}$/', $key ) && '' !== $name && ! isset( $fields[ $key ] ) ) {
				$fields[ $key ] = $field;
			}
		}
	}
	return $fields;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_acf_value_assigned_field( array $context, string $field_key ) {
	if ( ! preg_match( '/^field_[A-Za-z0-9_-]{1,120}$/', $field_key ) ) {
		return new WP_Error( 'wpagent_bad_field_key', __( 'field_key muss ein gültiger ACF-Field-Key sein, der mit „field_“ beginnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$fields = wpagent_acf_value_assigned_fields( $context );
	if ( is_wp_error( $fields ) ) {
		return $fields;
	}
	if ( ! isset( $fields[ $field_key ] ) ) {
		return new WP_Error( 'wpagent_acf_field_unassigned', __( 'Dieses ACF-Feld ist dem Zielobjekt nicht als oberstes Wertefeld zugeordnet.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $fields[ $field_key ];
}

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

/** @return array{exists:bool,raw:mixed,side_effects:array<string,mixed>,state_hash:string} */
function wpagent_acf_value_state( array $context, array $field ): array {
	$name   = (string) ( $field['name'] ?? '' );
	$exists = '' !== $name && wpagent_acf_value_metadata_exists( $context, $name );
	$raw    = get_field( (string) $field['key'], $context['acf_object_id'], false );
	$side_effects = wpagent_acf_value_side_effect_state( $context, $field );
	$hash   = hash(
		'sha256',
		(string) $context['object_type'] . "\0" . (string) $context['acf_object_id'] . "\0" . (string) $field['key'] . "\0" . ( $exists ? '1' : '0' ) . "\0"
		. maybe_serialize( wpagent_acf_value_contract( $field ) ) . "\0"
		. maybe_serialize( wpagent_acf_value_canonicalize( $raw ) ) . "\0"
		. maybe_serialize( $side_effects )
	);
	return array( 'exists' => $exists, 'raw' => $raw, 'side_effects' => $side_effects, 'state_hash' => $hash );
}

function wpagent_acf_value_metadata_exists( array $context, string $name ): bool {
	if ( ! function_exists( 'acf_decode_post_id' ) ) return false;
	$decoded = acf_decode_post_id( $context['acf_object_id'] );
	$type    = (string) ( $decoded['type'] ?? '' );
	$id      = $decoded['id'] ?? 0;
	if ( 'option' === $type ) {
		global $wpdb;
		$value_name     = $id . '_' . $name;
		$reference_name = '_' . $id . '_' . $name;
		$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name IN (%s, %s)", $value_name, $reference_name ) );
		return (int) $count > 0;
	}
	return in_array( $type, array( 'post', 'user', 'term', 'comment' ), true ) && $id && ( metadata_exists( $type, $id, $name ) || metadata_exists( $type, $id, '_' . $name ) );
}

/** @return array<string,mixed> */
function wpagent_acf_value_side_effect_state( array $context, array $field ): array {
	if ( 'taxonomy' !== (string) ( $field['type'] ?? '' ) || empty( $field['save_terms'] ) ) return array();
	$object_id = absint( $context['side_effect_object_id'] ?? 0 );
	if ( ! $object_id ) return array( 'read_error' => 'unsupported_object' );
	$taxonomy = is_array( $field['taxonomy'] ?? null ) ? (string) ( $field['taxonomy'][0] ?? '' ) : (string) ( $field['taxonomy'] ?? '' );
	if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) return array( 'taxonomy' => $taxonomy, 'term_ids' => array() );
	$term_ids = wp_get_object_terms( $object_id, $taxonomy, array( 'fields' => 'ids' ) );
	if ( is_wp_error( $term_ids ) ) return array( 'taxonomy' => $taxonomy, 'term_ids' => array(), 'read_error' => $term_ids->get_error_code() );
	$term_ids = array_values( array_unique( array_map( 'absint', $term_ids ) ) );
	sort( $term_ids, SORT_NUMERIC );
	return array( 'taxonomy' => $taxonomy, 'term_ids' => $term_ids );
}

/** @return array<string,mixed> */
function wpagent_acf_value_contract( array $field ): array {
	$contract = array();
	foreach ( array( 'key', 'name', 'type', 'required', 'maxlength', 'min', 'max', 'step', 'multiple', 'field_type', 'library', 'load_terms', 'save_terms', 'bidirectional', 'bidirectional_target', 'mime_types', 'min_width', 'min_height', 'max_width', 'max_height', 'min_size', 'max_size' ) as $key ) {
		if ( array_key_exists( $key, $field ) ) $contract[ $key ] = wpagent_acf_value_canonicalize( $field[ $key ] );
	}
	foreach ( array( 'post_type', 'taxonomy', 'role' ) as $key ) {
		if ( array_key_exists( $key, $field ) ) {
			$values = is_array( $field[ $key ] ) ? array_map( 'strval', $field[ $key ] ) : array( (string) $field[ $key ] );
			sort( $values, SORT_STRING );
			$contract[ $key ] = $values;
		}
	}
	if ( is_array( $field['choices'] ?? null ) ) {
		$contract['choices'] = array_map( 'strval', array_keys( $field['choices'] ) );
		sort( $contract['choices'], SORT_STRING );
	}
	if ( is_array( $field['sub_fields'] ?? null ) ) {
		$contract['sub_fields'] = array();
		foreach ( $field['sub_fields'] as $child ) {
			if ( is_array( $child ) ) $contract['sub_fields'][] = wpagent_acf_value_contract( $child );
		}
	}
	if ( is_array( $field['layouts'] ?? null ) ) {
		$contract['layouts'] = array();
		foreach ( $field['layouts'] as $layout ) {
			if ( ! is_array( $layout ) ) continue;
			$contract['layouts'][] = array(
				'key'        => (string) ( $layout['key'] ?? '' ),
				'name'       => (string) ( $layout['name'] ?? '' ),
				'sub_fields' => wpagent_acf_value_contract_list( $layout['sub_fields'] ?? array() ),
			);
		}
	}
	return wpagent_acf_value_canonicalize( $contract );
}

/** @return array<int,array<string,mixed>> */
function wpagent_acf_value_contract_list( $fields ): array {
	$contracts = array();
	foreach ( is_array( $fields ) ? $fields : array() as $field ) {
		if ( is_array( $field ) ) $contracts[] = wpagent_acf_value_contract( $field );
	}
	return $contracts;
}

function wpagent_acf_value_is_supported( array $field, int $depth = 0 ): bool {
	if ( $depth > 6 ) {
		return false;
	}
	if ( ! preg_match( '/^field_[A-Za-z0-9_-]{1,120}$/', (string) ( $field['key'] ?? '' ) ) || '' === (string) ( $field['name'] ?? '' ) ) {
		return false;
	}
	$type = (string) ( $field['type'] ?? '' );
	if ( ! empty( $field['bidirectional'] ) ) return false;
	$taxonomies = is_array( $field['taxonomy'] ?? null ) ? $field['taxonomy'] : ( empty( $field['taxonomy'] ) ? array() : array( $field['taxonomy'] ) );
	if ( 'taxonomy' === $type && 1 !== count( $taxonomies ) ) return false;
	if ( 'taxonomy' === $type && ( ( $depth > 0 && ( ! empty( $field['load_terms'] ) || ! empty( $field['save_terms'] ) ) ) || ( ! empty( $field['load_terms'] ) && empty( $field['save_terms'] ) ) ) ) {
		return false;
	}
	$simple = array(
		'text', 'textarea', 'email', 'url', 'number', 'range', 'true_false', 'color_picker',
		'date_picker', 'date_time_picker', 'time_picker', 'select', 'radio', 'button_group', 'checkbox',
		'image', 'file', 'gallery', 'relationship', 'post_object', 'page_link', 'taxonomy', 'user',
	);
	if ( in_array( $type, $simple, true ) ) {
		return true;
	}
	if ( 'group' === $type || 'repeater' === $type ) {
		$children = is_array( $field['sub_fields'] ?? null ) ? $field['sub_fields'] : array();
		return ! empty( $children ) && ! in_array( false, array_map( static fn( $child ): bool => is_array( $child ) && wpagent_acf_value_is_supported( $child, $depth + 1 ), $children ), true );
	}
	if ( 'flexible_content' === $type ) {
		$layouts = is_array( $field['layouts'] ?? null ) ? $field['layouts'] : array();
		if ( ! $layouts ) {
			return false;
		}
		foreach ( $layouts as $layout ) {
			$children = is_array( $layout['sub_fields'] ?? null ) ? $layout['sub_fields'] : array();
			if ( ! $children || in_array( false, array_map( static fn( $child ): bool => is_array( $child ) && wpagent_acf_value_is_supported( $child, $depth + 1 ), $children ), true ) ) {
				return false;
			}
		}
		return true;
	}
	return false;
}

function wpagent_acf_value_is_supported_for_context( array $field, array $context, int $depth = 0 ): bool {
	if ( ! wpagent_acf_value_is_supported( $field, $depth ) ) return false;
	$type = (string) ( $field['type'] ?? '' );
	$object_type = (string) ( $context['object_type'] ?? 'post' );
	if ( 'options' === $object_type && 'taxonomy' === $type && ( ! empty( $field['load_terms'] ) || ! empty( $field['save_terms'] ) ) ) return false;
	if ( 'post' !== $object_type && in_array( $type, array( 'image', 'file', 'gallery' ), true ) && 'uploadedTo' === (string) ( $field['library'] ?? '' ) ) return false;
	if ( in_array( $type, array( 'group', 'repeater' ), true ) ) {
		foreach ( is_array( $field['sub_fields'] ?? null ) ? $field['sub_fields'] : array() as $child ) {
			if ( ! is_array( $child ) || ! wpagent_acf_value_is_supported_for_context( $child, $context, $depth + 1 ) ) return false;
		}
	}
	if ( 'flexible_content' === $type ) {
		foreach ( is_array( $field['layouts'] ?? null ) ? $field['layouts'] : array() as $layout ) {
			foreach ( is_array( $layout['sub_fields'] ?? null ) ? $layout['sub_fields'] : array() as $child ) {
				if ( ! is_array( $child ) || ! wpagent_acf_value_is_supported_for_context( $child, $context, $depth + 1 ) ) return false;
			}
		}
	}
	return true;
}

function wpagent_acf_value_side_effects_match( array $context, array $field, $value ): bool {
	if ( 'taxonomy' !== (string) ( $field['type'] ?? '' ) || empty( $field['save_terms'] ) ) return true;
	$state    = wpagent_acf_value_side_effect_state( $context, $field );
	$expected = is_array( $value ) ? array_map( 'absint', $value ) : ( empty( $value ) ? array() : array( absint( $value ) ) );
	$expected = array_values( array_unique( array_filter( $expected ) ) );
	sort( $expected, SORT_NUMERIC );
	return empty( $state['read_error'] ) && $expected === ( $state['term_ids'] ?? array() );
}

function wpagent_acf_value_side_effect_state_is_safe( array $state ): bool {
	return empty( $state['read_error'] );
}

function wpagent_acf_value_restore_side_effects( array $context, array $state ): bool {
	if ( ! isset( $state['taxonomy'], $state['term_ids'] ) || ! is_array( $state['term_ids'] ) ) return true;
	$object_id = absint( $context['side_effect_object_id'] ?? 0 );
	if ( ! $object_id ) return false;
	$taxonomy = (string) $state['taxonomy'];
	if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) return false;
	$result = wp_set_object_terms( $object_id, array_map( 'absint', $state['term_ids'] ), $taxonomy, false );
	return ! is_wp_error( $result );
}

function wpagent_acf_value_can_write( array $field, array $context, int $depth = 0 ): bool {
	if ( ! wpagent_acf_value_is_supported_for_context( $field, $context, $depth ) ) return false;
	$type = (string) ( $field['type'] ?? '' );
	if ( 'user' === $type && ! current_user_can( 'list_users' ) ) return false;
	if ( 'taxonomy' === $type ) {
		$taxonomies = is_array( $field['taxonomy'] ?? null ) ? $field['taxonomy'] : ( empty( $field['taxonomy'] ) ? array() : array( $field['taxonomy'] ) );
		if ( 1 !== count( $taxonomies ) ) return false;
		foreach ( $taxonomies as $taxonomy_name ) {
			$taxonomy = get_taxonomy( (string) $taxonomy_name );
			if ( ! $taxonomy || ! current_user_can( $taxonomy->cap->assign_terms ) ) return false;
		}
	}
	if ( in_array( $type, array( 'group', 'repeater' ), true ) ) {
		foreach ( is_array( $field['sub_fields'] ?? null ) ? $field['sub_fields'] : array() as $child ) {
			if ( ! is_array( $child ) || ! wpagent_acf_value_can_write( $child, $context, $depth + 1 ) ) return false;
		}
	}
	if ( 'flexible_content' === $type ) {
		foreach ( is_array( $field['layouts'] ?? null ) ? $field['layouts'] : array() as $layout ) {
			foreach ( is_array( $layout['sub_fields'] ?? null ) ? $layout['sub_fields'] : array() as $child ) {
				if ( ! is_array( $child ) || ! wpagent_acf_value_can_write( $child, $context, $depth + 1 ) ) return false;
			}
		}
	}
	return true;
}

function wpagent_acf_value_within_limits( $value, int $depth = 0, int &$nodes = 0 ): bool {
	if ( $depth > 16 || ++$nodes > WPAGENT_ACF_VALUE_MAX_NODES ) return false;
	if ( ! is_array( $value ) && ! is_object( $value ) ) return true;
	foreach ( (array) $value as $item ) {
		if ( ! wpagent_acf_value_within_limits( $item, $depth + 1, $nodes ) ) return false;
	}
	return true;
}

function wpagent_acf_value_is_complete( array $field, $raw, int $depth = 0 ): bool {
	if ( $depth > 6 ) return false;
	$type = (string) ( $field['type'] ?? '' );
	if ( 'group' === $type ) {
		if ( ! is_array( $raw ) ) return false === $raw || null === $raw || '' === $raw;
		return wpagent_acf_value_row_is_complete( $field['sub_fields'] ?? array(), $raw, $depth + 1 );
	}
	if ( 'repeater' === $type ) {
		if ( ! is_array( $raw ) ) return false === $raw || null === $raw || '' === $raw;
		if ( ! array_is_list( $raw ) ) return false;
		foreach ( $raw as $row ) {
			if ( ! is_array( $row ) || ! wpagent_acf_value_row_is_complete( $field['sub_fields'] ?? array(), $row, $depth + 1 ) ) return false;
		}
	}
	if ( 'flexible_content' === $type ) {
		if ( ! is_array( $raw ) ) return false === $raw || null === $raw || '' === $raw;
		if ( ! array_is_list( $raw ) ) return false;
		foreach ( $raw as $row ) {
			$name   = is_array( $row ) ? (string) ( $row['acf_fc_layout'] ?? '' ) : '';
			$layout = wpagent_acf_value_layout( $field, $name );
			if ( ! $layout || ! wpagent_acf_value_row_is_complete( $layout['sub_fields'] ?? array(), is_array( $row ) ? $row : array(), $depth + 1 ) ) return false;
		}
	}
	return true;
}

function wpagent_acf_value_row_is_complete( $sub_fields, array $row, int $depth ): bool {
	foreach ( is_array( $sub_fields ) ? $sub_fields : array() as $child ) {
		if ( ! is_array( $child ) ) return false;
		$key = array_key_exists( (string) ( $child['key'] ?? '' ), $row ) ? (string) $child['key'] : (string) ( $child['name'] ?? '' );
		if ( ! wpagent_acf_value_is_complete( $child, $row[ $key ] ?? null, $depth ) ) return false;
	}
	return true;
}

/** @return array{value:mixed,redacted:int} */
function wpagent_acf_value_export( array $field, $raw, int $depth = 0 ): array {
	if ( $depth > 6 ) return array( 'value' => null, 'redacted' => 1 );
	$type = (string) ( $field['type'] ?? '' );
	if ( in_array( $type, array( 'relationship', 'post_object', 'page_link' ), true ) ) {
		$list      = is_array( $raw ) ? $raw : ( null === $raw || false === $raw || '' === $raw ? array() : array( $raw ) );
		$readable  = array();
		$redacted  = 0;
		foreach ( $list as $item ) {
			$id = is_object( $item ) ? absint( $item->ID ?? 0 ) : absint( $item );
			if ( $id > 0 && current_user_can( 'read_post', $id ) ) {
				$readable[] = $id;
			} elseif ( $id > 0 ) {
				++$redacted;
			}
		}
		$multiple = 'relationship' === $type || ! empty( $field['multiple'] );
		return array( 'value' => $multiple ? $readable : ( $readable[0] ?? null ), 'redacted' => $redacted );
	}
	if ( in_array( $type, array( 'image', 'file', 'gallery' ), true ) ) {
		$list = is_array( $raw ) ? $raw : ( empty( $raw ) ? array() : array( $raw ) );
		$readable = array();
		$redacted = 0;
		foreach ( $list as $item ) {
			$id = is_array( $item ) ? absint( $item['ID'] ?? $item['id'] ?? 0 ) : ( is_object( $item ) ? absint( $item->ID ?? 0 ) : absint( $item ) );
			if ( $id > 0 && current_user_can( 'read_post', $id ) ) $readable[] = $id;
			elseif ( $id > 0 ) ++$redacted;
		}
		return array( 'value' => 'gallery' === $type ? $readable : ( $readable[0] ?? null ), 'redacted' => $redacted );
	}
	if ( 'user' === $type ) {
		$list = is_array( $raw ) ? $raw : ( empty( $raw ) ? array() : array( $raw ) );
		$ids  = array_values( array_filter( array_map( static fn( $item ): int => is_object( $item ) ? absint( $item->ID ?? 0 ) : absint( $item ), $list ) ) );
		if ( ! current_user_can( 'list_users' ) ) {
			return array( 'value' => ! empty( $field['multiple'] ) ? array() : null, 'redacted' => count( $ids ) );
		}
		return array( 'value' => ! empty( $field['multiple'] ) ? $ids : ( $ids[0] ?? null ), 'redacted' => 0 );
	}
	if ( 'group' === $type ) {
		return wpagent_acf_value_export_row( $field['sub_fields'] ?? array(), is_array( $raw ) ? $raw : array(), $depth + 1 );
	}
	if ( 'repeater' === $type ) {
		$rows = array(); $redacted = 0;
		foreach ( is_array( $raw ) ? $raw : array() as $row ) {
			$export = wpagent_acf_value_export_row( $field['sub_fields'] ?? array(), is_array( $row ) ? $row : array(), $depth + 1 );
			$rows[] = $export['value']; $redacted += $export['redacted'];
		}
		return array( 'value' => $rows, 'redacted' => $redacted );
	}
	if ( 'flexible_content' === $type ) {
		$rows = array(); $redacted = 0;
		foreach ( is_array( $raw ) ? $raw : array() as $row ) {
			$name = is_array( $row ) ? (string) ( $row['acf_fc_layout'] ?? '' ) : '';
			$layout = wpagent_acf_value_layout( $field, $name );
			if ( ! $layout ) continue;
			$export = wpagent_acf_value_export_row( $layout['sub_fields'] ?? array(), $row, $depth + 1 );
			$export['value']['acf_fc_layout'] = $name;
			$rows[] = $export['value']; $redacted += $export['redacted'];
		}
		return array( 'value' => $rows, 'redacted' => $redacted );
	}
	if ( 'true_false' === $type ) {
		return array( 'value' => (bool) $raw, 'redacted' => 0 );
	}
	return array( 'value' => wpagent_acf_value_canonicalize( $raw ), 'redacted' => 0 );
}

/** @return array{value:array<string,mixed>,redacted:int} */
function wpagent_acf_value_export_row( $sub_fields, array $row, int $depth ): array {
	$value = array(); $redacted = 0;
	foreach ( is_array( $sub_fields ) ? $sub_fields : array() as $child ) {
		if ( ! is_array( $child ) || '' === (string) ( $child['name'] ?? '' ) ) continue;
		$key = array_key_exists( (string) $child['key'], $row ) ? (string) $child['key'] : (string) $child['name'];
		$export = wpagent_acf_value_export( $child, $row[ $key ] ?? null, $depth );
		$value[ (string) $child['name'] ] = $export['value']; $redacted += $export['redacted'];
	}
	return array( 'value' => $value, 'redacted' => $redacted );
}

/** @return array<string,mixed>|null */
function wpagent_acf_value_layout( array $field, string $name ): ?array {
	foreach ( is_array( $field['layouts'] ?? null ) ? $field['layouts'] : array() as $layout ) {
		if ( is_array( $layout ) && ( $name === (string) ( $layout['name'] ?? '' ) || $name === (string) ( $layout['key'] ?? '' ) ) ) return $layout;
	}
	return null;
}

function wpagent_acf_value_lock_name( $acf_object_id, string $field_key ): string {
	return 'wp_agent_acf_value_lock_' . hash( 'sha256', (string) $acf_object_id . ':' . $field_key );
}

/** @return string|WP_Error */
function wpagent_acf_value_acquire_lock( $acf_object_id, string $field_key ) {
	$name = wpagent_acf_value_lock_name( $acf_object_id, $field_key );
	$token = wpagent_companion_acquire_option_lock( $name, WPAGENT_ACF_VALUE_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_acf_value_locked', __( 'Dieses ACF-Feld wird gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_acf_value_release_lock( $acf_object_id, string $field_key, string $token ): void {
	wpagent_companion_release_option_lock( wpagent_acf_value_lock_name( $acf_object_id, $field_key ), $token );
}
