<?php
/**
 * Repeater, Flexible Content, and Clone field rendering/sanitization.
 *
 * @package CustomFields
 * @since 1.3.0
 */

namespace CustomFields;

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

/**
 * Nested/composite field handler.
 */
final class NestedFields {

	private const MAX_NESTED_ROWS = 500;

	private Fields $fields;

	public function __construct( Fields $fields ) {
		$this->fields = $fields;
	}

	/**
	 * Whether a type is a nested/composite field.
	 *
	 * @param string $type Field type.
	 */
	public static function isNestedType( string $type ): bool {
		return in_array( $type, [ 'group', 'repeater', 'flexible_content', 'clone' ], true );
	}

	/**
	 * Sub-field types allowed inside repeaters (no nesting repeaters in v1).
	 *
	 * @return array<int, string>
	 */
	public static function allowedSubFieldTypes(): array {
		$blocked = [ 'group', 'repeater', 'flexible_content', 'clone', 'message', 'tab', 'accordion' ];

		return array_values( array_diff( array_keys( Config::fieldTypes() ), $blocked ) );
	}

	/**
	 * Render a nested field control.
	 *
	 * @param array<string, mixed> $field     Field definition.
	 * @param string               $name_attr Base name attribute.
	 * @param mixed                $value     Current value.
	 * @param string               $id_attr   Base element id.
	 * @param array<string, mixed> $group     Parent group (for clone resolution).
	 */
	public function render( array $field, string $name_attr, mixed $value, string $id_attr, array $group = [] ): void {
		$type = (string) ( $field['type'] ?? '' );

		switch ( $type ) {
			case 'group':
				$this->renderGroup( $field, $name_attr, $value, $id_attr, $group );
				break;
			case 'repeater':
				$this->renderRepeater( $field, $name_attr, $value, $id_attr, $group );
				break;
			case 'flexible_content':
				$this->renderFlexible( $field, $name_attr, $value, $id_attr, $group );
				break;
			case 'clone':
				$this->renderClone( $field, $name_attr, $value, $id_attr, $group );
				break;
		}
	}

	/**
	 * Sanitize nested field value.
	 *
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $raw   Raw submitted value.
	 * @param array<string, mixed> $group Parent group.
	 * @return mixed
	 */
	public function sanitize( array $field, mixed $raw, array $group = [] ): mixed {
		$type = (string) ( $field['type'] ?? '' );

		return match ( $type ) {
			'group'            => $this->sanitizeGroup( $field, $raw, $group ),
			'repeater'         => $this->sanitizeRepeater( $field, $raw, $group ),
			'flexible_content' => $this->sanitizeFlexible( $field, $raw, $group ),
			'clone'            => $this->sanitizeClone( $field, $raw, $group ),
			default            => [],
		};
	}

	/**
	 * Resolve a clone field into the field definition(s) used for validation.
	 *
	 * @param array<string, mixed> $field Clone field definition.
	 * @param array<string, mixed> $group Parent group.
	 * @return array{multiple: bool, fields: array<int, array<string, mixed>>}|null
	 */
	public function cloneValidationFields( array $field, array $group ): ?array {
		$source = $this->resolveCloneSource( $field, $group );
		if ( $source === null ) {
			return null;
		}

		if ( is_array( $source ) && isset( $source[0] ) ) {
			$fields = [];
			foreach ( $source as $sub ) {
				if ( ! is_array( $sub ) ) {
					continue;
				}
				$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
				if ( $sub_name === '' ) {
					continue;
				}
				if ( ! empty( $field['clone_prefix_name'] ) && (string) ( $field['label'] ?? '' ) !== '' ) {
					$store_key   = sanitize_key( (string) $field['label'] . '_' . $sub_name );
					$sub['name'] = $store_key;
					$sub['key']  = $store_key;
				}
				$fields[] = $sub;
			}

			return $fields !== [] ? [ 'multiple' => true, 'fields' => $fields ] : null;
		}

		return [ 'multiple' => false, 'fields' => [ $source ] ];
	}

	/**
	 * @param array<string, mixed> $field     Field definition.
	 * @param string               $name_attr Parent name.
	 * @param mixed                $value     Stored values.
	 * @param string               $id_attr   Element id prefix.
	 */
	private function renderGroup( array $field, string $name_attr, mixed $value, string $id_attr, array $group ): void {
		$sub_fields = $this->subFields( $field );
		$row        = is_array( $value ) ? $value : [];
		$layout     = $this->layout( $field );

		echo '<div class="cf-group-field cf-group-field--layout-' . esc_attr( $layout ) . '">';
		foreach ( $sub_fields as $sub ) {
			$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
			if ( $sub_name === '' ) {
				continue;
			}
			$sub_value = $row[ $sub_name ] ?? ( $sub['default'] ?? '' );
			$sub_id    = $id_attr . '-' . sanitize_key( $sub_name );
			$input     = $name_attr . '[' . $sub_name . ']';
			echo '<div class="cf-group-field__subfield ' . esc_attr( $this->widthClass( $sub ) ) . '">';
			$this->fields->render( $sub, $input, $sub_value, $sub_id, $group );
			echo '</div>';
		}
		echo '</div>';
	}

	/**
	 * @param array<string, mixed> $field     Field definition.
	 * @param string               $name_attr Name attribute.
	 * @param mixed                $value     Current rows.
	 * @param string               $id_attr   Element id prefix.
	 */
	private function renderRepeater( array $field, string $name_attr, mixed $value, string $id_attr, array $group ): void {
		$sub_fields = $this->subFields( $field );
		$rows       = is_array( $value ) ? array_slice( array_values( $value ), 0, self::MAX_NESTED_ROWS ) : [];
		$min        = max( 0, (int) ( $field['min'] ?? 0 ) );
		$max        = (int) ( $field['max'] ?? 0 );
		$layout     = $this->layout( $field, 'table' );
		$collapsed  = sanitize_key( (string) ( $field['collapsed'] ?? '' ) );
		$per_page   = max( 1, (int) ( $field['rows_per_page'] ?? 20 ) );
		$label      = trim( (string) ( $field['button_label'] ?? '' ) );
		$label      = $label !== '' ? $label : __( 'Add Row', 'wppowersuite-lite' );

		if ( $rows === [] && $min > 0 ) {
			$rows = array_fill( 0, $min, [] );
		}

		echo '<div class="cf-repeater cf-repeater--layout-' . esc_attr( $layout ) . '" data-min="' . esc_attr( (string) $min ) . '" data-max="' . esc_attr( (string) $max ) . '" data-collapsed="' . esc_attr( $collapsed ) . '" data-rows-per-page="' . esc_attr( (string) $per_page ) . '">';
		echo '<div class="cf-repeater__rows">';

		foreach ( $rows as $i => $row ) {
			$this->renderRepeaterRow( $sub_fields, $name_attr, is_array( $row ) ? $row : [], $id_attr, (int) $i, $collapsed, $group );
		}

		echo '</div>';
		echo '<div class="cf-repeater__footer">';
		echo '<button type="button" class="button cf-repeater__add"><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span><span>' . esc_html( $label ) . '</span></button>';
		echo '<span class="cf-repeater__pager" hidden><button type="button" class="button button-small cf-repeater__page-prev">' . esc_html__( 'Previous', 'wppowersuite-lite' ) . '</button><span class="cf-repeater__page-status"></span><button type="button" class="button button-small cf-repeater__page-next">' . esc_html__( 'Next', 'wppowersuite-lite' ) . '</button></span>';
		echo '<span class="cf-repeater__status" aria-live="polite"></span>';
		echo '</div>';
		echo '<template class="cf-repeater-row-template">';
		$this->renderRepeaterRow( $sub_fields, $name_attr, [], $id_attr, '__ROW__', $collapsed, $group );
		echo '</template>';
		echo '</div>';
	}

	/**
	 * @param array<int, array<string, mixed>> $sub_fields Sub field definitions.
	 * @param string                           $name_attr  Parent name.
	 * @param array<string, mixed>             $row        Row values.
	 * @param string                           $id_attr    Id prefix.
	 * @param int|string                       $index      Row index.
	 * @param string                           $collapsed  Collapsed preview sub-field name.
	 */
	private function renderRepeaterRow( array $sub_fields, string $name_attr, array $row, string $id_attr, int|string $index, string $collapsed, array $group ): void {
		$preview = $collapsed !== '' ? $this->collapsedPreviewValue( $row[ $collapsed ] ?? '' ) : '';
		echo '<div class="cf-repeater__row" data-row="' . esc_attr( (string) $index ) . '">';
		echo '<div class="cf-repeater__row-head">';
		echo '<button type="button" class="button-link cf-repeater__drag" aria-label="' . esc_attr__( 'Move row', 'wppowersuite-lite' ) . '"><span class="dashicons dashicons-menu" aria-hidden="true"></span></button>';
		echo '<button type="button" class="button-link cf-repeater__toggle" aria-expanded="true" aria-label="' . esc_attr__( 'Collapse row', 'wppowersuite-lite' ) . '"><span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span></button>';
		echo '<span class="cf-repeater__row-label">' . esc_html__( 'Row', 'wppowersuite-lite' ) . ' <span class="cf-repeater__row-num"></span><span class="cf-repeater__row-preview">' . ( $preview !== '' ? ' - ' . esc_html( $preview ) : '' ) . '</span></span>';
		echo '<button type="button" class="button-link cf-repeater__remove">' . esc_html__( 'Remove', 'wppowersuite-lite' ) . '</button>';
		echo '</div>';
		echo '<div class="cf-repeater__row-body">';

		foreach ( $sub_fields as $sub ) {
			$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
			if ( $sub_name === '' ) {
				continue;
			}
			$sub_value = $row[ $sub_name ] ?? ( $sub['default'] ?? '' );
			$sub_id    = $id_attr . '-' . sanitize_key( (string) $index ) . '-' . sanitize_key( $sub_name );
			$input     = $name_attr . '[' . $index . '][' . $sub_name . ']';
			echo '<div class="cf-repeater__subfield ' . esc_attr( $this->widthClass( $sub ) ) . '">';
			$this->fields->render( $sub, $input, $sub_value, $sub_id, $group );
			echo '</div>';
		}

		echo '</div></div>';
	}

	/**
	 * @param mixed $value Raw sub-field value.
	 */
	private function collapsedPreviewValue( mixed $value ): string {
		if ( is_scalar( $value ) ) {
			return wp_trim_words( sanitize_text_field( (string) $value ), 10, '...' );
		}

		if ( is_array( $value ) ) {
			$flat = array_filter(
				array_map(
					static fn( mixed $item ): string => is_scalar( $item ) ? sanitize_text_field( (string) $item ) : '',
					$value
				)
			);
			return wp_trim_words( implode( ', ', $flat ), 10, '...' );
		}

		return '';
	}

	/**
	 * @param array<string, mixed> $field     Field definition.
	 * @param string               $name_attr Name attribute.
	 * @param mixed                $value     Rows.
	 * @param string               $id_attr   Id prefix.
	 */
	private function renderFlexible( array $field, string $name_attr, mixed $value, string $id_attr, array $group ): void {
		$layouts = $this->layouts( $field );
		$rows    = is_array( $value ) ? array_slice( array_values( $value ), 0, self::MAX_NESTED_ROWS ) : [];
		$layout  = $this->layout( $field, 'block' );
		$min     = max( 0, (int) ( $field['min'] ?? 0 ) );
		$max     = (int) ( $field['max'] ?? 0 );
		$label   = trim( (string) ( $field['button_label'] ?? '' ) );
		$label   = $label !== '' ? $label : __( 'Add Row', 'wppowersuite-lite' );

		echo '<div class="cf-flexible cf-flexible--layout-' . esc_attr( $layout ) . '" data-min="' . esc_attr( (string) $min ) . '" data-max="' . esc_attr( (string) $max ) . '">';
		echo '<div class="cf-flexible__rows">';

		foreach ( $rows as $i => $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$layout_key = (string) ( $row['_layout'] ?? '' );
			$layout     = $layouts[ $layout_key ] ?? null;
			if ( $layout === null ) {
				continue;
			}
			$this->renderFlexibleRow( $layout, $name_attr, $row, $id_attr, (int) $i, $group );
		}

		echo '</div>';
		echo '<div class="cf-flexible__add-wrap">';
		echo '<select class="cf-flexible__layout-select">';
		echo '<option value="">' . esc_html__( 'Select layout…', 'wppowersuite-lite' ) . '</option>';
		foreach ( $layouts as $layout_key => $layout ) {
			echo '<option value="' . esc_attr( $layout_key ) . '" data-min="' . esc_attr( (string) ( $layout['min'] ?? 0 ) ) . '" data-max="' . esc_attr( (string) ( $layout['max'] ?? 0 ) ) . '">' . esc_html( (string) ( $layout['label'] ?? $layout_key ) ) . '</option>';
		}
		echo '</select> ';
		if ( $layouts !== [] ) {
			echo '<div class="cf-flexible__layout-buttons" role="list">';
			foreach ( $layouts as $layout_key => $layout ) {
				$layout_label = (string) ( $layout['label'] ?? $layout_key );
				$layout_max   = (int) ( $layout['max'] ?? 0 );
				echo '<button type="button" class="button cf-flexible__layout-button" role="listitem" data-layout="' . esc_attr( $layout_key ) . '" data-max="' . esc_attr( (string) $layout_max ) . '">';
				echo '<span class="cf-flexible__layout-button-label">' . esc_html( $layout_label ) . '</span>';
				echo '<span class="cf-flexible__layout-button-count" aria-hidden="true">0' . ( $layout_max > 0 ? '/' . esc_html( (string) $layout_max ) : '' ) . '</span>';
				echo '</button>';
			}
			echo '</div>';
		}
		printf(
			'<button type="button" class="button cf-flexible__add"%1$s><span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span><span>%2$s</span></button>',
			$layouts === [] ? ' disabled' : '',
			esc_html( $label )
		);
		echo '<span class="cf-flexible__status" aria-live="polite"></span>';
		echo '</div>';

		foreach ( $layouts as $layout_key => $layout ) {
			echo '<template class="cf-flexible-row-template" data-layout="' . esc_attr( $layout_key ) . '" data-min="' . esc_attr( (string) ( $layout['min'] ?? 0 ) ) . '" data-max="' . esc_attr( (string) ( $layout['max'] ?? 0 ) ) . '">';
			$this->renderFlexibleRow( $layout, $name_attr, [ '_layout' => $layout_key ], $id_attr, '__ROW__', $group );
			echo '</template>';
		}

		echo '</div>';
	}

	/**
	 * @param array<string, mixed> $layout    Layout definition.
	 * @param string               $name_attr Parent name.
	 * @param array<string, mixed> $row       Row values.
	 * @param string               $id_attr   Id prefix.
	 * @param int|string           $index     Row index.
	 */
	private function renderFlexibleRow( array $layout, string $name_attr, array $row, string $id_attr, int|string $index, array $group ): void {
		$layout_key = (string) ( $layout['key'] ?? ( $row['_layout'] ?? '' ) );
		$sub_fields = $this->subFields( $layout );
		$display    = $this->layout( $layout, 'block' );

		echo '<div class="cf-flexible__row cf-flexible__row--display-' . esc_attr( $display ) . '" data-row="' . esc_attr( (string) $index ) . '" data-layout="' . esc_attr( $layout_key ) . '">';
		echo '<div class="cf-flexible__row-head">';
		echo '<button type="button" class="button-link cf-flexible__drag" aria-label="' . esc_attr__( 'Move layout', 'wppowersuite-lite' ) . '"><span class="dashicons dashicons-menu" aria-hidden="true"></span></button>';
		echo '<button type="button" class="button-link cf-flexible__toggle" aria-expanded="true" aria-label="' . esc_attr__( 'Collapse layout', 'wppowersuite-lite' ) . '"><span class="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span></button>';
		echo '<strong>' . esc_html( (string) ( $layout['label'] ?? $layout_key ) ) . '</strong>';
		echo '<button type="button" class="button-link cf-flexible__remove">' . esc_html__( 'Remove', 'wppowersuite-lite' ) . '</button>';
		echo '</div>';
		echo '<select hidden class="cf-flexible__layout-input" name="' . esc_attr( $name_attr . '[' . $index . '][_layout]' ) . '" data-layout-value="' . esc_attr( $layout_key ) . '" aria-hidden="true" tabindex="-1">';
		echo '<option value="' . esc_attr( $layout_key ) . '" selected>' . esc_html( $layout_key ) . '</option>';
		echo '</select>';
		echo '<div class="cf-flexible__row-body">';

		foreach ( $sub_fields as $sub ) {
			$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
			if ( $sub_name === '' ) {
				continue;
			}
			$sub_value = $row[ $sub_name ] ?? ( $sub['default'] ?? '' );
			$sub_id    = $id_attr . '-' . sanitize_key( (string) $index ) . '-' . sanitize_key( $sub_name );
			$input     = $name_attr . '[' . $index . '][' . $sub_name . ']';
			echo '<div class="cf-flexible__subfield ' . esc_attr( $this->widthClass( $sub ) ) . '">';
			$this->fields->render( $sub, $input, $sub_value, $sub_id, $group );
			echo '</div>';
		}

		echo '</div></div>';
	}

	/**
	 * @param array<string, mixed> $field     Clone field.
	 * @param string               $name_attr Name attribute.
	 * @param mixed                $value     Stored value.
	 * @param string               $id_attr   Id prefix.
	 * @param array<string, mixed> $group     Parent group.
	 */
	private function renderClone( array $field, string $name_attr, mixed $value, string $id_attr, array $group ): void {
		$source = $this->resolveCloneSource( $field, $group );
		if ( $source === null ) {
			echo '<p class="description">' . esc_html__( 'Clone source field not found.', 'wppowersuite-lite' ) . '</p>';
			return;
		}

		$display      = (string) ( $field['clone_display'] ?? 'seamless' ) === 'group' ? 'group' : 'seamless';
		$layout       = sanitize_key( (string) ( $field['clone_layout'] ?? 'block' ) );
		$layout       = in_array( $layout, [ 'block', 'table', 'row' ], true ) ? $layout : 'block';
		$prefix_label = ! empty( $field['clone_prefix_label'] );
		$prefix_name  = ! empty( $field['clone_prefix_name'] );
		$clone_label  = (string) ( $field['label'] ?? '' );

		if ( is_array( $source ) && isset( $source[0] ) ) {
			echo '<div class="cf-clone cf-clone--' . esc_attr( $display ) . ' cf-clone--layout-' . esc_attr( $layout ) . '">';
			if ( $display === 'group' && $clone_label !== '' ) {
				echo '<div class="cf-clone__title">' . esc_html( $clone_label ) . '</div>';
			}
			$row = is_array( $value ) ? $value : [];
			foreach ( $source as $sub ) {
				if ( $prefix_label && $clone_label !== '' ) {
					$sub['label'] = trim( $clone_label . ' ' . (string) ( $sub['label'] ?? '' ) );
				}
				$sub_name  = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
				$store_key = $prefix_name && $clone_label !== '' ? sanitize_key( $clone_label . '_' . $sub_name ) : $sub_name;
				$sub_value = $row[ $sub_name ] ?? ( $sub['default'] ?? '' );
				if ( $prefix_name && array_key_exists( $store_key, $row ) ) {
					$sub_value = $row[ $store_key ];
				}
				$sub_id = $id_attr . '-' . sanitize_key( $store_key );
				$input  = $name_attr . '[' . $store_key . ']';
				echo '<div class="cf-clone__subfield">';
				$this->fields->render( $sub, $input, $sub_value, $sub_id, $group );
				echo '</div>';
			}
			echo '</div>';
			return;
		}

		if ( $prefix_label && $clone_label !== '' ) {
			$source['label'] = trim( $clone_label . ' ' . (string) ( $source['label'] ?? '' ) );
		}
		$this->fields->render( $source, $name_attr, $value, $id_attr, $group );
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $raw   Raw value.
	 * @return array<string, mixed>
	 */
	private function sanitizeGroup( array $field, mixed $raw, array $group ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$out = [];
		foreach ( $this->subFields( $field ) as $sub ) {
			$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
			if ( $sub_name === '' ) {
				continue;
			}
			$out[ $sub_name ] = $this->fields->sanitizeValue( $sub, $raw[ $sub_name ] ?? null, $group );
		}

		return $out;
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $raw   Raw value.
	 * @return array<int, array<string, mixed>>
	 */
	private function sanitizeRepeater( array $field, mixed $raw, array $group ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$sub_fields = $this->subFields( $field );
		$configured_max = (int) ( $field['max'] ?? 0 );
		$max        = $configured_max > 0 ? min( $configured_max, self::MAX_NESTED_ROWS ) : self::MAX_NESTED_ROWS;
		$clean      = [];

		foreach ( array_values( $raw ) as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$item = [];
			foreach ( $sub_fields as $sub ) {
				$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
				if ( $sub_name === '' ) {
					continue;
				}
				$item[ $sub_name ] = $this->fields->sanitizeValue( $sub, $row[ $sub_name ] ?? null, $group );
			}
			if ( $item !== [] ) {
				$clean[] = $item;
			}
			if ( $max > 0 && count( $clean ) >= $max ) {
				break;
			}
		}

		return $clean;
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $raw   Raw value.
	 * @return array<int, array<string, mixed>>
	 */
	private function sanitizeFlexible( array $field, mixed $raw, array $group ): array {
		if ( ! is_array( $raw ) ) {
			return [];
		}

		$layouts = $this->layouts( $field );
		$clean   = [];
		$configured_max = (int) ( $field['max'] ?? 0 );
		$max     = $configured_max > 0 ? min( $configured_max, self::MAX_NESTED_ROWS ) : self::MAX_NESTED_ROWS;

		foreach ( array_values( $raw ) as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$layout_key = (string) ( $row['_layout'] ?? '' );
			$layout     = $layouts[ $layout_key ] ?? null;
			if ( $layout === null ) {
				continue;
			}
			$item = [ '_layout' => $layout_key ];
			foreach ( $this->subFields( $layout ) as $sub ) {
				$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
				if ( $sub_name === '' ) {
					continue;
				}
				$item[ $sub_name ] = $this->fields->sanitizeValue( $sub, $row[ $sub_name ] ?? null, $group );
			}
			$clean[] = $item;
			if ( count( $clean ) >= $max ) {
				break;
			}
		}

		return $clean;
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @param mixed                $raw   Raw value.
	 * @param array<string, mixed> $group Parent group.
	 * @return mixed
	 */
	private function sanitizeClone( array $field, mixed $raw, array $group ): mixed {
		$source = $this->resolveCloneSource( $field, $group );
		if ( $source === null ) {
			return [];
		}

		if ( is_array( $source ) && isset( $source[0] ) ) {
			if ( ! is_array( $raw ) ) {
				return [];
			}
			$out = [];
			foreach ( $source as $sub ) {
				$sub_name = (string) ( $sub['name'] ?? $sub['key'] ?? '' );
				if ( $sub_name === '' ) {
					continue;
				}
				$store_key = ! empty( $field['clone_prefix_name'] ) && (string) ( $field['label'] ?? '' ) !== '' ? sanitize_key( (string) $field['label'] . '_' . $sub_name ) : $sub_name;
				$out[ $store_key ] = $this->fields->sanitizeValue( $sub, $raw[ $store_key ] ?? ( $raw[ $sub_name ] ?? null ), $group );
			}
			return $out;
		}

		return $this->fields->sanitizeValue( $source, $raw, $group );
	}

	/**
	 * @param array<string, mixed> $field Field or layout.
	 * @return array<int, array<string, mixed>>
	 */
	private function subFields( array $field ): array {
		$raw = isset( $field['sub_fields'] ) && is_array( $field['sub_fields'] ) ? $field['sub_fields'] : [];
		$out = [];
		foreach ( $raw as $sub ) {
			if ( is_array( $sub ) ) {
				$out[] = $sub;
			}
			if ( count( $out ) >= Config::MAX_SUB_FIELDS ) {
				break;
			}
		}

		return $out;
	}

	/**
	 * Grid width class for nested field wrappers.
	 *
	 * @param array<string, mixed> $field Field definition.
	 */
	private function widthClass( array $field ): string {
		$width = (string) ( $field['width'] ?? '100' );

		return in_array( $width, [ '25', '33', '50', '75', '100' ], true ) ? 'cf-field--w' . $width : 'cf-field--w100';
	}

	/**
	 * @param array<string, mixed> $field Field definition.
	 * @return array<string, array<string, mixed>>
	 */
	private function layouts( array $field ): array {
		$raw = isset( $field['layouts'] ) && is_array( $field['layouts'] ) ? $field['layouts'] : [];
		$out = [];
		foreach ( $raw as $layout ) {
			if ( ! is_array( $layout ) ) {
				continue;
			}
			$key = (string) ( $layout['key'] ?? '' );
			if ( $key !== '' ) {
				$out[ $key ] = $layout;
			}
			if ( count( $out ) >= Config::MAX_LAYOUTS ) {
				break;
			}
		}

		return $out;
	}

	/**
	 * Field layout constrained to supported imported display modes.
	 *
	 * @param array<string, mixed> $field   Field definition.
	 * @param string               $default Default layout.
	 */
	private function layout( array $field, string $default = 'block' ): string {
		$layout = sanitize_key( (string) ( $field['nested_layout'] ?? $default ) );

		return in_array( $layout, [ 'block', 'table', 'row' ], true ) ? $layout : $default;
	}

	/**
	 * Resolve clone source field(s).
	 *
	 * @param array<string, mixed> $field Clone field.
	 * @param array<string, mixed> $group Parent group.
	 * @return array<string, mixed>|array<int, array<string, mixed>>|null
	 */
	private function resolveCloneSource( array $field, array $group ): array|null {
		$source = (string) ( $field['clone_source'] ?? '' );
		if ( $source === '' ) {
			return null;
		}

		if ( str_contains( $source, ',' ) ) {
			$keys   = array_map( 'trim', explode( ',', $source ) );
			$fields = [];
			foreach ( $keys as $token ) {
				$found = $this->resolveCloneToken( $token, $group );
				if ( is_array( $found ) ) {
					if ( isset( $found[0] ) ) {
						foreach ( $found as $sub ) {
							if ( is_array( $sub ) ) {
								$fields[] = $sub;
							}
						}
					} else {
						$fields[] = $found;
					}
				}
			}

			return $fields !== [] ? $fields : null;
		}

		return $this->resolveCloneToken( $source, $group );
	}

	/**
	 * Resolve a single clone source token.
	 *
	 * @param string               $source Clone source token.
	 * @param array<string, mixed> $group  Current group.
	 * @return array<string, mixed>|array<int, array<string, mixed>>|null
	 */
	private function resolveCloneToken( string $source, array $group ): array|null {
		$source = trim( $source );
		if ( $source === '' ) {
			return null;
		}

		$storage = new Storage();

		if ( str_contains( $source, '|' ) ) {
			[ $group_key, $field_key ] = array_pad( explode( '|', $source, 2 ), 2, '' );
			$remote = $storage->get( sanitize_key( $group_key ) );
			if ( ! is_array( $remote ) ) {
				return null;
			}
			return $this->findFieldByKey( $remote, sanitize_key( $field_key ) );
		}

		$remote_group = $storage->get( sanitize_key( $source ) );
		if ( is_array( $remote_group ) ) {
			if ( (string) ( $remote_group['key'] ?? '' ) === (string) ( $group['key'] ?? '' ) ) {
				return null;
			}

			$fields = [];
			foreach ( (array) ( $remote_group['fields'] ?? [] ) as $remote_field ) {
				$type = is_array( $remote_field ) ? (string) ( $remote_field['type'] ?? '' ) : '';
				if ( is_array( $remote_field ) && ! in_array( $type, Config::displayOnlyTypes(), true ) && $type !== 'clone' ) {
					$fields[] = $remote_field;
				}
			}
			return $fields !== [] ? $fields : null;
		}

		return $this->findFieldByKey( $group, sanitize_key( $source ) );
	}

	/**
	 * @param array<string, mixed> $group     Group definition.
	 * @param string               $field_key Field key.
	 * @return array<string, mixed>|null
	 */
	private function findFieldByKey( array $group, string $field_key ): ?array {
		foreach ( (array) ( $group['fields'] ?? [] ) as $f ) {
			if ( is_array( $f ) && (string) ( $f['key'] ?? '' ) === $field_key ) {
				return $f;
			}
		}

		return null;
	}
}
