<?php
/**
 * After Divi migrates D4 `dsm_flipbox` + nested `dsm_flipbox_child` shortcodes, merge the first two
 * child representations into the parent `dsm/flipbox` block (`front_*` / `back_*`) and drop those inner blocks.
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Includes\Divi5Conversion;

use ET\Builder\Packages\Conversion\Conversion;
use ET\Builder\Packages\Conversion\ValueExpansion;

/**
 * Portability / import-time merge for Flipbox inner children.
 */
final class FlipboxChildMergeMigrator
{

	/**
	 * Register the migrated post content filter.
	 *
	 * @return void
	 */
	public static function register(): void
	{
		add_filter(
			'divi_framework_portability_import_migrated_post_content',
			array( self::class, 'migrate_import_content' ),
			20,
			1
		);

		// Visual Builder / raw post load (same merge as portability import).
		add_filter(
			'et_fb_load_raw_post_content',
			array( self::class, 'migrate_import_content' ),
			20,
			2
		);
	}

	/**
	 * Walk block JSON and merge flipbox children into parent attrs.
	 *
	 * @param string $content Post content (block markup).
	 * @return string
	 */
	public static function migrate_import_content( $content, $post_id = null )
	{
		unset( $post_id );

		if ( ! is_string( $content ) || '' === $content ) {
			return $content;
		}

		if ( false === strpos( $content, 'dsm/flipbox' ) ) {
			return $content;
		}

		if ( ! function_exists( 'parse_blocks' ) || ! function_exists( 'serialize_blocks' ) ) {
			return $content;
		}

		$blocks = parse_blocks( $content );
		if ( ! is_array( $blocks ) || array() === $blocks ) {
			return $content;
		}

		$blocks = self::process_blocks( $blocks );

		return serialize_blocks( $blocks );
	}

	/**
	 * @param array<int, array<string, mixed>> $blocks Parsed blocks.
	 * @return array<int, array<string, mixed>>
	 */
	private static function process_blocks( array $blocks ): array
	{
		$out = array();
		foreach ( $blocks as $block ) {
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$block['innerBlocks'] = self::process_blocks( $block['innerBlocks'] );
			}

			if ( isset( $block['blockName'] ) && 'dsm/flipbox' === $block['blockName'] ) {
				$block = self::maybe_merge_flipbox_inner_children( $block );
			}

			$out[] = $block;
		}
		return $out;
	}

	/**
	 * @param array<string, mixed> $block Flipbox block.
	 * @return array<string, mixed>
	 */
	private static function maybe_merge_flipbox_inner_children( array $block ): array
	{
		if ( empty( $block['innerBlocks'] ) || ! is_array( $block['innerBlocks'] ) ) {
			return $block;
		}

		$to_merge = self::collect_first_two_flipbox_child_candidates_depth_first( $block['innerBlocks'] );

		if ( count( $to_merge ) === 0 ) {
			return $block;
		}

		$attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();

		self::merge_child_block_into_attrs( $attrs, $to_merge[0], 'front' );
		if ( isset( $to_merge[1] ) ) {
			self::merge_child_block_into_attrs( $attrs, $to_merge[1], 'back' );
		}

		self::normalize_flipbox_font_icon_attr( $attrs, 'front_font_icon' );
		self::normalize_flipbox_font_icon_attr( $attrs, 'back_font_icon' );

		ConversionAttrs::hydrate_flipbox_face_shared_fonts_into_per_face_when_empty( $attrs );
		ConversionAttrs::apply_flipbox_d4_parent_fonts_to_both_faces( $attrs );

		// VB Rich Text expects `innerContent.*.value.text`; merged child `content` may be a bare HTML string at `value`.
		ConversionAttrs::normalize_flipbox_rich_content_inner_content_for_vb( $attrs );
		ConversionAttrs::normalize_inner_value_to_heading_link_text_shape( $attrs, 'front_title' );
		ConversionAttrs::normalize_inner_value_to_heading_link_text_shape( $attrs, 'back_title' );

		ConversionAttrs::mirror_flipbox_phone_module_sizing_height_to_phone_wide_when_empty( $attrs );

		$block['attrs'] = $attrs;

		$remove_count         = count( $to_merge );
		$remaining_to_remove  = $remove_count;
		$block['innerBlocks'] = self::remove_first_n_flipbox_candidates_depth_first( $block['innerBlocks'], $remaining_to_remove );

		// Keep serialized markup consistent when no inner blocks remain.
		if ( array() === $block['innerBlocks'] ) {
			$block['innerHTML']    = '';
			$block['innerContent'] = array();
		}

		return $block;
	}

	/**
	 * Depth-first: first two flipbox child candidates (under row/column or direct).
	 *
	 * @param array<int, array<string, mixed>> $blocks Inner blocks tree.
	 * @return array<int, array<string, mixed>>
	 */
	private static function collect_first_two_flipbox_child_candidates_depth_first( array $blocks ): array
	{
		$found = array();
		foreach ( $blocks as $block ) {
			if ( count( $found ) >= 2 ) {
				break;
			}
			if ( self::is_flipbox_child_candidate_block( $block ) ) {
				$found[] = $block;
				continue;
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$nested = self::collect_first_two_flipbox_child_candidates_depth_first( $block['innerBlocks'] );
				foreach ( $nested as $nb ) {
					$found[] = $nb;
					if ( count( $found ) >= 2 ) {
						break 2;
					}
				}
			}
		}
		return $found;
	}

	/**
	 * Remove the first N flipbox-child candidates in depth-first order from a tree.
	 *
	 * @param array<int, array<string, mixed>> $blocks Inner blocks tree.
	 * @param int                               $remaining Remaining removals (by ref).
	 * @return array<int, array<string, mixed>>
	 */
	private static function remove_first_n_flipbox_candidates_depth_first( array $blocks, int &$remaining ): array
	{
		$out = array();
		foreach ( $blocks as $block ) {
			if ( $remaining <= 0 ) {
				$out[] = $block;
				continue;
			}
			if ( self::is_flipbox_child_candidate_block( $block ) ) {
				$remaining--;
				continue;
			}
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$block['innerBlocks'] = self::remove_first_n_flipbox_candidates_depth_first( $block['innerBlocks'], $remaining );
			}
			$out[] = $block;
		}
		return $out;
	}

	/**
	 * @param array<string, mixed> $block Parsed block.
	 */
	private static function is_flipbox_child_candidate_block( array $block ): bool
	{
		$name = $block['blockName'] ?? '';
		if ( 'dsm/flipbox-child' === $name ) {
			return true;
		}
		if ( 'divi/shortcode-module' !== $name ) {
			return false;
		}
		$html = self::get_block_inner_html_string( $block );

		return false !== strpos( $html, 'dsm_flipbox_child' );
	}

	/**
	 * @param array<string, mixed> $block Block.
	 */
	private static function get_block_inner_html_string( array $block ): string
	{
		if ( isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) {
			return $block['innerHTML'];
		}
		if ( isset( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) {
			$chunks = array_filter(
				$block['innerContent'],
				static function ( $p ) {
					return is_string( $p );
				}
			);
			if ( array() !== $chunks ) {
				return implode( '', $chunks );
			}
		}

		return '';
	}

	/**
	 * @param array<string, mixed> $attrs  Parent attrs (by ref).
	 * @param array<string, mixed> $child  Inner block.
	 * @param string               $prefix front|back.
	 */
	private static function merge_child_block_into_attrs( array &$attrs, array $child, string $prefix ): void
	{
		$name = $child['blockName'] ?? '';

		if ( 'dsm/flipbox-child' === $name && isset( $child['attrs'] ) && is_array( $child['attrs'] ) ) {
			self::apply_face_attrs_from_child_module( $attrs, $child['attrs'], $prefix, $child );
			self::maybe_merge_flipbox_child_from_shortcode_fallback( $attrs, $child, $prefix );
			return;
		}

		$d4 = self::extract_d4_child_attrs_from_block( $child );
		if ( array() === $d4 ) {
			return;
		}

		self::map_d4_child_face_into_attrs( $attrs, $d4, $prefix );
	}

	/**
	 * Copy converted `dsm/flipbox-child` attribute branches onto parent `front_*` / `back_*`.
	 *
	 * @param array<string, mixed> $parent_attrs Parent attrs (by ref).
	 * @param array<string, mixed> $child_attrs  Child block attrs.
	 * @param string                 $prefix     front|back.
	 * @param array<string, mixed> $child_block Full inner block (for `[dsm_flipbox_child]` HTML parse).
	 */
	private static function apply_face_attrs_from_child_module( array &$parent_attrs, array $child_attrs, string $prefix, array $child_block ): void
	{
		$pairs = array(
			'title'                   => "{$prefix}_title",
			'subtitle'                => "{$prefix}_subtitle",
			'content'                 => "{$prefix}_content",
			'use_icon'                => "{$prefix}_use_icon",
			'font_icon'               => "{$prefix}_font_icon",
			'image'                   => "{$prefix}_image",
			'icon_color'              => "{$prefix}_icon_color",
			'use_circle'              => "{$prefix}_use_circle",
			'circle_color'            => "{$prefix}_circle_color",
			'use_circle_border'       => "{$prefix}_use_circle_border",
			'circle_border_color'     => "{$prefix}_circle_border_color",
			'use_icon_font_size'      => "{$prefix}_use_icon_font_size",
			'icon_font_size'          => "{$prefix}_icon_font_size",
			'image_max_width'         => "{$prefix}_image_max_width",
			'icon_placement'          => "{$prefix}_icon_placement",
			'button_text'             => "{$prefix}_button_text",
			'button_url'              => "{$prefix}_button_url",
			'button_url_new_window'   => "{$prefix}_button_url_new_window",
			'content_orientation'     => "{$prefix}_content_orientation",
		);

		foreach ( $pairs as $from => $to ) {
			if ( ! isset( $child_attrs[ $from ] ) ) {
				continue;
			}
			$branch = $child_attrs[ $from ];
			if ( is_object( $branch ) ) {
				$encoded = wp_json_encode( $branch );
				$branch  = is_string( $encoded ) ? json_decode( $encoded, true ) : null;
			}
			if ( ! is_array( $branch ) ) {
				continue;
			}
			$parent_attrs[ $to ] = $branch;
		}

		$image_style_key = "{$prefix}_image_style";
		if ( isset( $child_attrs['image_style'] ) && is_array( $child_attrs['image_style'] ) ) {
			$parent_attrs[ $image_style_key ] = $child_attrs['image_style'];
		}

		$icon_style_key = "{$prefix}_icon_style";
		if ( isset( $child_attrs['icon_style'] ) && is_array( $child_attrs['icon_style'] ) ) {
			$parent_attrs[ $icon_style_key ] = $child_attrs['icon_style'];
		}

		self::normalize_flipbox_font_icon_attr( $parent_attrs, "{$prefix}_font_icon" );

		$button_key = 'front' === $prefix ? 'front_button' : 'back_button';
		if ( isset( $child_attrs['button'] ) && is_array( $child_attrs['button'] ) ) {
			$parent_attrs[ $button_key ] = $child_attrs['button'];
		}

		/*
		 * Do not re-apply D4 `button_icon` from embedded `[dsm_flipbox_child]` HTML: that string is static
		 * after migration and would overwrite VB-saved `front_button` / `back_button` on every load (wrong glyph).
		 * Button glyph + decoration come from the child block attrs (Divi conversion). Face icon only:
		 */
		$d4_from_html = self::extract_d4_child_attrs_from_block( $child_block );
		if ( array() !== $d4_from_html ) {
			if ( isset( $d4_from_html['font_icon'] ) && is_string( $d4_from_html['font_icon'] ) && '' !== trim( $d4_from_html['font_icon'] ) ) {
				if ( ! self::flipbox_font_icon_inner_has_unicode( $parent_attrs, "{$prefix}_font_icon" ) ) {
					self::hydrate_font_icon_from_d4( $parent_attrs, "{$prefix}_font_icon", trim( $d4_from_html['font_icon'] ) );
				}
			}
		}

		self::merge_flipbox_child_face_text_group_from_module_branch( $parent_attrs, $child_attrs, $prefix );

		$face_attr_key = 'front' === $prefix ? 'front_face' : 'back_face';
		self::merge_child_module_decoration_into_parent_face( $parent_attrs, $child_attrs, $face_attr_key );
	}

	/**
	 * D4 Text options (background layout, orientation, text shadow) live on the child `module.advanced.text`.
	 * D5 unified flipbox stores the same TextGroup on `front_text` / `back_text` — copy across after merge.
	 *
	 * @param array<string, mixed> $parent_attrs Parent attrs (by ref).
	 * @param array<string, mixed> $child_attrs  Child block attrs.
	 * @param string                 $prefix     front|back.
	 */
	private static function merge_flipbox_child_face_text_group_from_module_branch( array &$parent_attrs, array $child_attrs, string $prefix ): void {
		if ( ! isset( $child_attrs['module'] ) || ! is_array( $child_attrs['module'] ) ) {
			return;
		}
		$mod = $child_attrs['module'];
		if ( ! isset( $mod['advanced']['text'] ) || ! is_array( $mod['advanced']['text'] ) ) {
			return;
		}
		if ( array() === $mod['advanced']['text'] ) {
			return;
		}
		$text_key = "{$prefix}_text";
		if ( ! isset( $parent_attrs[ $text_key ] ) || ! is_array( $parent_attrs[ $text_key ] ) ) {
			$parent_attrs[ $text_key ] = array();
		}
		if ( ! isset( $parent_attrs[ $text_key ]['advanced'] ) || ! is_array( $parent_attrs[ $text_key ]['advanced'] ) ) {
			$parent_attrs[ $text_key ]['advanced'] = array();
		}
		$existing = isset( $parent_attrs[ $text_key ]['advanced']['text'] ) && is_array( $parent_attrs[ $text_key ]['advanced']['text'] )
			? $parent_attrs[ $text_key ]['advanced']['text']
			: array();
		$parent_attrs[ $text_key ]['advanced']['text'] = array_replace_recursive( $existing, $mod['advanced']['text'] );
	}

	/**
	 * D4 flipbox child design (background, padding/margin, border, shadow, sizing, filters, transform,
	 * transition, animation, position, scroll, sticky, overflow, z-index) lives on `module.decoration.*`
	 * of `dsm/flipbox-child`. Copy those branches onto the parent `front_face` / `back_face` after D5
	 * conversion (not only background).
	 *
	 * @param array<string, mixed> $parent_attrs Parent attrs (by ref).
	 * @param array<string, mixed> $child_attrs  Child block attrs.
	 * @param string               $face_attr_key front_face|back_face.
	 */
	private static function merge_child_module_decoration_into_parent_face( array &$parent_attrs, array $child_attrs, string $face_attr_key ): void {
		if ( ! isset( $child_attrs['module'] ) || ! is_array( $child_attrs['module'] ) ) {
			return;
		}
		$decoration = $child_attrs['module']['decoration'] ?? null;
		self::merge_flipbox_face_decoration_branches_from_module( $parent_attrs, $face_attr_key, $decoration );
	}

	/**
	 * Copy D4 flipbox-child `module.decoration` branches onto parent `front_face` / `back_face`
	 * (spacing, border, box shadow, sizing, filters, background, animation, transform, transition,
	 * position, scroll, sticky, overflow, z-index).
	 *
	 * @param array<string, mixed> $parent_attrs  Parent attrs (by ref).
	 * @param string               $face_attr_key front_face|back_face.
	 * @param mixed                $decoration    `module.decoration` array or null.
	 */
	private static function merge_flipbox_face_decoration_branches_from_module( array &$parent_attrs, string $face_attr_key, $decoration ): void {
		if ( ! is_array( $decoration ) ) {
			return;
		}

		$keys = array(
			'background',
			'spacing',
			'border',
			'boxShadow',
			'sizing',
			'filters',
			'animation',
			'transform',
			'transition',
			'position',
			'scroll',
			'sticky',
			'overflow',
			'zIndex',
		);

		foreach ( $keys as $key ) {
			if ( ! isset( $decoration[ $key ] ) || ! is_array( $decoration[ $key ] ) ) {
				continue;
			}
			if ( array() === $decoration[ $key ] ) {
				continue;
			}

			if ( ! isset( $parent_attrs[ $face_attr_key ] ) || ! is_array( $parent_attrs[ $face_attr_key ] ) ) {
				$parent_attrs[ $face_attr_key ] = array();
			}
			if ( ! isset( $parent_attrs[ $face_attr_key ]['decoration'] ) || ! is_array( $parent_attrs[ $face_attr_key ]['decoration'] ) ) {
				$parent_attrs[ $face_attr_key ]['decoration'] = array();
			}
			$parent_attrs[ $face_attr_key ]['decoration'][ $key ] = $decoration[ $key ];
		}
	}

	/**
	 * @param array<string, mixed> $child Inner block.
	 * @return array<string, mixed>
	 */
	private static function extract_d4_child_attrs_from_block( array $child ): array
	{
		$html = self::get_block_inner_html_string( $child );
		if ( '' === $html ) {
			return array();
		}

		return self::parse_dsm_flipbox_child_shortcode( self::normalize_shortcode_html( $html ) );
	}

	/**
	 * Unwrap shortcodes from paragraphs / decode entities for reliable parsing.
	 *
	 * @param string $html Raw inner HTML from shortcode-module.
	 */
	private static function normalize_shortcode_html( string $html ): string
	{
		$html = html_entity_decode( $html, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		if ( function_exists( 'shortcode_unautop' ) ) {
			$html = shortcode_unautop( $html );
		}
		$html = preg_replace( '/<p>\s*(\[)/', '$1', $html );
		$html = preg_replace( '/(\])\s*<\/p>/', '$1', $html );
		return trim( (string) $html );
	}

	/**
	 * Parse `[dsm_flipbox_child ...]` from HTML (self-closing or paired shortcode).
	 *
	 * @param string $html Inner HTML.
	 * @return array<string, mixed> Attribute list plus optional `_inner_content` body.
	 */
	private static function parse_dsm_flipbox_child_shortcode( string $html ): array
	{
		$html = trim( $html );
		if ( '' === $html ) {
			return array();
		}

		$atts = array();
		$body = '';

		// Opening tag: optional attributes (handles `[dsm_flipbox_child]`, `[dsm_flipbox_child /]`, `[dsm_flipbox_child foo="bar"]`).
		if ( 1 === preg_match( '/\[\s*dsm_flipbox_child\s*([^\]]*)\]/i', $html, $m ) ) {
			$attr_chunk = trim( $m[1] );
			if ( '' !== $attr_chunk && '/' !== $attr_chunk ) {
				$attr_chunk = rtrim( $attr_chunk, '/' );
				$attr_chunk = trim( $attr_chunk );
				if ( '' !== $attr_chunk ) {
					$parsed = shortcode_parse_atts( $attr_chunk );
					if ( is_array( $parsed ) ) {
						$atts = $parsed;
					}
				}
			}
		}

		if ( preg_match( '/\[\s*dsm_flipbox_child\s*[^\]]*\]\s*(.*?)\s*\[\s*\/\s*dsm_flipbox_child\s*\]/is', $html, $mb ) ) {
			$body = $mb[1];
		}

		if ( '' !== $body ) {
			$atts['_inner_content'] = $body;
		}

		return $atts;
	}

	/**
	 * @param array<string, mixed> $attrs  Parent attrs (by ref).
	 * @param array<string, mixed> $d4     Parsed D4-like attrs.
	 * @param string               $prefix front|back.
	 */
	private static function map_d4_child_face_into_attrs( array &$attrs, array $d4, string $prefix ): void
	{
		$title = isset( $d4['title'] ) ? (string) $d4['title'] : '';
		ConversionAttrs::set_inner_desktop_heading_link_text( $attrs, "{$prefix}_title", $title, true );

		if ( isset( $d4['subtitle'] ) && is_string( $d4['subtitle'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_subtitle", (string) $d4['subtitle'], true );
		}

		$content = '';
		if ( isset( $d4['content'] ) && is_string( $d4['content'] ) ) {
			$content = $d4['content'];
		}
		if ( '' === $content && isset( $d4['_inner_content'] ) && is_string( $d4['_inner_content'] ) ) {
			$content = $d4['_inner_content'];
		}
		if ( '' !== $content ) {
			self::set_inner_desktop_rich_text( $attrs, "{$prefix}_content", $content );
		}

		$use_icon = isset( $d4['use_icon'] ) ? strtolower( (string) $d4['use_icon'] ) : 'off';
		$use_icon = 'on' === $use_icon ? 'on' : 'off';
		ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_use_icon", $use_icon, true );

		if ( isset( $d4['image'] ) && is_string( $d4['image'] ) && '' !== trim( $d4['image'] ) ) {
			self::hydrate_upload_from_d4_string( $attrs, "{$prefix}_image", trim( $d4['image'] ) );
		}

		if ( isset( $d4['font_icon'] ) && is_string( $d4['font_icon'] ) && '' !== trim( $d4['font_icon'] ) ) {
			self::hydrate_font_icon_from_d4( $attrs, "{$prefix}_font_icon", trim( $d4['font_icon'] ) );
		}

		if ( isset( $d4['button_text'] ) && is_string( $d4['button_text'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_button_text", $d4['button_text'], true );
		}
		if ( isset( $d4['button_url'] ) && is_string( $d4['button_url'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_button_url", $d4['button_url'], true );
		}

		$nw = isset( $d4['button_url_new_window'] ) ? strtolower( (string) $d4['button_url_new_window'] ) : 'off';
		$nw = 'on' === $nw ? 'on' : 'off';
		ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_button_url_new_window", $nw, true );

		if ( isset( $d4['content_orientation'] ) && is_string( $d4['content_orientation'] ) ) {
			$co = $d4['content_orientation'];
			if ( in_array( $co, array( 'flex-start', 'center', 'flex-end' ), true ) ) {
				ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_content_orientation", $co, true );
			}
		}

		if ( isset( $d4['icon_color'] ) && is_string( $d4['icon_color'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_icon_color", $d4['icon_color'], true );
		}

		$use_circle = isset( $d4['use_circle'] ) ? strtolower( (string) $d4['use_circle'] ) : 'off';
		ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_use_circle", 'on' === $use_circle ? 'on' : 'off', true );

		if ( isset( $d4['circle_color'] ) && is_string( $d4['circle_color'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_circle_color", $d4['circle_color'], true );
		}

		$use_cb = isset( $d4['use_circle_border'] ) ? strtolower( (string) $d4['use_circle_border'] ) : 'off';
		ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_use_circle_border", 'on' === $use_cb ? 'on' : 'off', true );

		if ( isset( $d4['circle_border_color'] ) && is_string( $d4['circle_border_color'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_circle_border_color", $d4['circle_border_color'], true );
		}

		$use_ifs = isset( $d4['use_icon_font_size'] ) ? strtolower( (string) $d4['use_icon_font_size'] ) : 'off';
		ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_use_icon_font_size", 'on' === $use_ifs ? 'on' : 'off', true );

		if ( isset( $d4['icon_font_size'] ) && is_string( $d4['icon_font_size'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_icon_font_size", $d4['icon_font_size'], true );
		}

		if ( isset( $d4['image_max_width'] ) && is_string( $d4['image_max_width'] ) ) {
			ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_image_max_width", $d4['image_max_width'], true );
		}

		if ( isset( $d4['icon_placement'] ) && is_string( $d4['icon_placement'] ) ) {
			$ip = $d4['icon_placement'];
			if ( in_array( $ip, array( 'top', 'left', 'right' ), true ) ) {
				ConversionAttrs::set_inner_desktop_value( $attrs, "{$prefix}_icon_placement", $ip, true );
			}
		}

		self::apply_flipbox_child_design_from_d4_shortcode_conversion( $attrs, $d4, $prefix );
		self::normalize_flipbox_font_icon_attr( $attrs, "{$prefix}_font_icon" );
	}

	/**
	 * Build D4 attribute list suitable for `dsm/flipbox-child` conversion (fonts, background, etc.).
	 *
	 * @param array<string, mixed> $d4 Parsed shortcode attributes (may include `_inner_content`).
	 * @return array<string, mixed>
	 */
	private static function build_d4_attrs_for_flipbox_child_conversion( array $d4 ): array {
		$out = $d4;
		unset( $out['_inner_content'] );
		if ( ( ! isset( $out['content'] ) || '' === trim( (string) $out['content'] ) ) && isset( $d4['_inner_content'] ) && is_string( $d4['_inner_content'] ) ) {
			$out['content'] = $d4['_inner_content'];
		}
		return $out;
	}

	/**
	 * Merge `title` / `subtitle` / `content` decoration (fonts, etc.) from converted child onto parent `front_*` / `back_*`.
	 *
	 * @param array<string, mixed> $attrs     Parent flipbox attrs (by ref).
	 * @param array<string, mixed> $converted Converted `dsm/flipbox-child` attrs.
	 * @param string               $prefix    front|back.
	 */
	private static function merge_flipbox_child_face_text_decoration_from_conversion( array &$attrs, array $converted, string $prefix ): void {
		$map = array(
			'title'    => "{$prefix}_title",
			'subtitle' => "{$prefix}_subtitle",
			'content'  => "{$prefix}_content",
		);
		foreach ( $map as $from => $to ) {
			if ( ! isset( $converted[ $from ] ) || ! is_array( $converted[ $from ] ) ) {
				continue;
			}
			$dec = $converted[ $from ]['decoration'] ?? null;
			if ( ! is_array( $dec ) || array() === $dec ) {
				continue;
			}
			if ( ! isset( $attrs[ $to ] ) || ! is_array( $attrs[ $to ] ) ) {
				$attrs[ $to ] = array();
			}
			if ( ! isset( $attrs[ $to ]['decoration'] ) || ! is_array( $attrs[ $to ]['decoration'] ) ) {
				$attrs[ $to ]['decoration'] = array();
			}
			foreach ( $dec as $dec_key => $dec_val ) {
				if ( is_array( $dec_val ) ) {
					if ( array() !== $dec_val ) {
						$attrs[ $to ]['decoration'][ $dec_key ] = $dec_val;
					}
					continue;
				}
				if ( '' !== $dec_val && null !== $dec_val ) {
					$attrs[ $to ]['decoration'][ $dec_key ] = $dec_val;
				}
			}
		}
	}

	/**
	 * Copy converted `button` (D4 advanced → `button` decoration) onto parent `front_button` / `back_button`.
	 *
	 * @param array<string, mixed> $attrs     Parent flipbox attrs (by ref).
	 * @param array<string, mixed> $converted Converted `dsm/flipbox-child` attrs.
	 * @param string               $prefix    front|back.
	 */
	private static function merge_flipbox_child_button_from_conversion( array &$attrs, array $converted, string $prefix ): void {
		if ( ! isset( $converted['button'] ) || ! is_array( $converted['button'] ) ) {
			return;
		}
		if ( array() === $converted['button'] ) {
			return;
		}
		$button_key           = 'front' === $prefix ? 'front_button' : 'back_button';
		$attrs[ $button_key ] = $converted['button'];
	}

	/**
	 * Map D4 flipbox child through `dsm/flipbox-child` conversion: text fonts (header / subhead / body),
	 * button styles, and `module.decoration` on the face (background, spacing/margin & padding, border, box shadow, sizing, filters).
	 *
	 * Previously only `module.decoration.background` was merged onto `front_face` / `back_face`, so D4 margin,
	 * padding, border, and box shadow from the child never migrated.
	 *
	 * @param array<string, mixed> $attrs  Parent flipbox attrs (by ref).
	 * @param array<string, mixed> $d4     Parsed shortcode attributes (may include `_inner_content`).
	 * @param string               $prefix front|back.
	 */
	private static function apply_flipbox_child_design_from_d4_shortcode_conversion( array &$attrs, array $d4, string $prefix ): void {
		if ( ! class_exists( Conversion::class ) ) {
			return;
		}

		$d4_for_conv = self::build_d4_attrs_for_flipbox_child_conversion( $d4 );
		if ( array() === $d4_for_conv ) {
			return;
		}

		try {
			$converted = Conversion::convertAttrs( $d4_for_conv, 'dsm/flipbox-child', null, false, false );
		} catch ( \Throwable $e ) {
			return;
		}

		if ( ! is_array( $converted ) ) {
			return;
		}

		self::merge_flipbox_child_face_text_decoration_from_conversion( $attrs, $converted, $prefix );
		self::merge_flipbox_face_text_group_from_child_conversion( $attrs, $converted, $d4, $prefix );
		self::merge_flipbox_child_button_from_conversion( $attrs, $converted, $prefix );

		$face_key   = 'front' === $prefix ? 'front_face' : 'back_face';
		$module_dec = $converted['module']['decoration'] ?? null;
		self::merge_flipbox_face_decoration_branches_from_module( $attrs, $face_key, $module_dec );

		$icon_key = "{$prefix}_font_icon";
		if ( isset( $converted['font_icon'] ) && is_array( $converted['font_icon'] ) ) {
			$attrs[ $icon_key ] = $converted['font_icon'];
		}
		self::normalize_flipbox_font_icon_attr( $attrs, $icon_key );

		if ( isset( $d4['font_icon'] ) && is_string( $d4['font_icon'] ) && '' !== trim( $d4['font_icon'] ) ) {
			if ( ! self::flipbox_font_icon_inner_has_unicode( $attrs, $icon_key ) ) {
				self::hydrate_font_icon_from_d4( $attrs, $icon_key, trim( $d4['font_icon'] ) );
			}
		}

	}

	/**
	 * Map child `module.advanced.text` from `dsm/flipbox-child` conversion onto parent `front_text` / `back_text`.
	 * Falls back to flat D4 `background_layout` / `text_orientation` when conversion did not emit text attrs.
	 *
	 * @param array<string, mixed> $attrs     Parent attrs (by ref).
	 * @param array<string, mixed> $converted Converted child attrs.
	 * @param array<string, mixed> $d4        Original D4 shortcode attrs.
	 * @param string                 $prefix  front|back.
	 */
	private static function merge_flipbox_face_text_group_from_child_conversion( array &$attrs, array $converted, array $d4, string $prefix ): void {
		$text_key = "{$prefix}_text";
		$adv_text = null;
		if ( isset( $converted['module']['advanced']['text'] ) && is_array( $converted['module']['advanced']['text'] ) ) {
			$adv_text = $converted['module']['advanced']['text'];
		}
		if ( is_array( $adv_text ) && array() !== $adv_text ) {
			if ( ! isset( $attrs[ $text_key ] ) || ! is_array( $attrs[ $text_key ] ) ) {
				$attrs[ $text_key ] = array();
			}
			if ( ! isset( $attrs[ $text_key ]['advanced'] ) || ! is_array( $attrs[ $text_key ]['advanced'] ) ) {
				$attrs[ $text_key ]['advanced'] = array();
			}
			$existing = isset( $attrs[ $text_key ]['advanced']['text'] ) && is_array( $attrs[ $text_key ]['advanced']['text'] )
				? $attrs[ $text_key ]['advanced']['text']
				: array();
			$attrs[ $text_key ]['advanced']['text'] = array_replace_recursive( $existing, $adv_text );
			// Child conversion can emit a non-empty `advanced.text` shell without desktop color/orientation.
			self::merge_flipbox_face_text_group_from_flat_d4_attrs( $attrs, $d4, $prefix, true );
			return;
		}

		self::merge_flipbox_face_text_group_from_flat_d4_attrs( $attrs, $d4, $prefix, false );
	}

	/**
	 * @param array<string, mixed> $root `front_text` / `back_text` → `advanced.text`.
	 */
	private static function flipbox_text_group_breakpoint_has_valid_color( array $root, string $bp ): bool {
		if ( ! isset( $root['text'][ $bp ]['value'] ) || ! is_array( $root['text'][ $bp ]['value'] ) ) {
			return false;
		}
		$c = $root['text'][ $bp ]['value']['color'] ?? null;
		return is_string( $c ) && in_array( strtolower( $c ), array( 'light', 'dark' ), true );
	}

	/**
	 * @param array<string, mixed> $root `front_text` / `back_text` → `advanced.text`.
	 */
	private static function flipbox_text_group_breakpoint_has_valid_orientation( array $root, string $bp ): bool {
		if ( ! isset( $root['text'][ $bp ]['value'] ) || ! is_array( $root['text'][ $bp ]['value'] ) ) {
			return false;
		}
		$o = $root['text'][ $bp ]['value']['orientation'] ?? null;
		return is_string( $o ) && in_array( strtolower( $o ), array( 'left', 'center', 'right', 'justify' ), true );
	}

	/**
	 * Whether desktop TextGroup has light/dark + alignment (classnames from `TextClassnames::text_options_classnames`).
	 *
	 * @param array<string, mixed> $attrs  Parent attrs.
	 * @param string               $prefix front|back.
	 */
	private static function flipbox_face_text_group_desktop_ready_for_classnames( array $attrs, string $prefix ): bool {
		$text_key = "{$prefix}_text";
		$root     = $attrs[ $text_key ]['advanced']['text'] ?? null;
		if ( ! is_array( $root ) ) {
			return false;
		}
		return self::flipbox_text_group_breakpoint_has_valid_color( $root, 'desktop' )
			&& self::flipbox_text_group_breakpoint_has_valid_orientation( $root, 'desktop' );
	}

	/**
	 * Map D4 `background_layout` / `text_orientation` onto parent `front_text` / `back_text` TextGroup.
	 *
	 * @param array<string, mixed> $attrs               Parent attrs (by ref).
	 * @param array<string, mixed> $d4                  Parsed D4 attrs.
	 * @param string               $prefix              front|back.
	 * @param bool                 $fill_missing_only   When true, only set color/orientation if missing/invalid (after child conversion merge).
	 */
	private static function merge_flipbox_face_text_group_from_flat_d4_attrs( array &$attrs, array $d4, string $prefix, bool $fill_missing_only = false ): void {
		$text_key = "{$prefix}_text";
		if ( ! isset( $attrs[ $text_key ] ) || ! is_array( $attrs[ $text_key ] ) ) {
			$attrs[ $text_key ] = array();
		}
		if ( ! isset( $attrs[ $text_key ]['advanced'] ) || ! is_array( $attrs[ $text_key ]['advanced'] ) ) {
			$attrs[ $text_key ]['advanced'] = array();
		}
		if ( ! isset( $attrs[ $text_key ]['advanced']['text'] ) || ! is_array( $attrs[ $text_key ]['advanced']['text'] ) ) {
			$attrs[ $text_key ]['advanced']['text'] = array();
		}
		$root = &$attrs[ $text_key ]['advanced']['text'];
		if ( ! isset( $root['text'] ) || ! is_array( $root['text'] ) ) {
			$root['text'] = array();
		}

		$orientation_map = array(
			'left'      => 'left',
			'center'    => 'center',
			'right'     => 'right',
			'justify'   => 'justify',
			'justified' => 'justify',
		);

		if ( isset( $d4['background_layout'] ) && is_string( $d4['background_layout'] ) ) {
			$bl = strtolower( trim( $d4['background_layout'] ) );
			if ( in_array( $bl, array( 'light', 'dark' ), true ) ) {
				if ( ! $fill_missing_only || ! self::flipbox_text_group_breakpoint_has_valid_color( $root, 'desktop' ) ) {
					if ( ! isset( $root['text']['desktop'] ) || ! is_array( $root['text']['desktop'] ) ) {
						$root['text']['desktop'] = array();
					}
					if ( ! isset( $root['text']['desktop']['value'] ) || ! is_array( $root['text']['desktop']['value'] ) ) {
						$root['text']['desktop']['value'] = array();
					}
					$root['text']['desktop']['value']['color'] = $bl;
				}
			}
		}

		if ( isset( $d4['text_orientation'] ) && is_string( $d4['text_orientation'] ) ) {
			$o = strtolower( trim( $d4['text_orientation'] ) );
			if ( isset( $orientation_map[ $o ] ) ) {
				if ( ! $fill_missing_only || ! self::flipbox_text_group_breakpoint_has_valid_orientation( $root, 'desktop' ) ) {
					if ( ! isset( $root['text']['desktop'] ) || ! is_array( $root['text']['desktop'] ) ) {
						$root['text']['desktop'] = array();
					}
					if ( ! isset( $root['text']['desktop']['value'] ) || ! is_array( $root['text']['desktop']['value'] ) ) {
						$root['text']['desktop']['value'] = array();
					}
					$root['text']['desktop']['value']['orientation'] = $orientation_map[ $o ];
				}
			}
		}

		foreach ( array( 'tablet', 'phone' ) as $bp ) {
			$tos = 'text_orientation_' . $bp;
			if ( isset( $d4[ $tos ] ) && is_string( $d4[ $tos ] ) ) {
				$o = strtolower( trim( $d4[ $tos ] ) );
				if ( isset( $orientation_map[ $o ] ) ) {
					if ( ! $fill_missing_only || ! self::flipbox_text_group_breakpoint_has_valid_orientation( $root, $bp ) ) {
						if ( ! isset( $root['text'][ $bp ] ) || ! is_array( $root['text'][ $bp ] ) ) {
							$root['text'][ $bp ] = array();
						}
						if ( ! isset( $root['text'][ $bp ]['value'] ) || ! is_array( $root['text'][ $bp ]['value'] ) ) {
							$root['text'][ $bp ]['value'] = array();
						}
						$root['text'][ $bp ]['value']['orientation'] = $orientation_map[ $o ];
					}
				}
			}
			$bls = 'background_layout_' . $bp;
			if ( isset( $d4[ $bls ] ) && is_string( $d4[ $bls ] ) ) {
				$bl = strtolower( trim( $d4[ $bls ] ) );
				if ( in_array( $bl, array( 'light', 'dark' ), true ) ) {
					if ( ! $fill_missing_only || ! self::flipbox_text_group_breakpoint_has_valid_color( $root, $bp ) ) {
						if ( ! isset( $root['text'][ $bp ] ) || ! is_array( $root['text'][ $bp ] ) ) {
							$root['text'][ $bp ] = array();
						}
						if ( ! isset( $root['text'][ $bp ]['value'] ) || ! is_array( $root['text'][ $bp ]['value'] ) ) {
							$root['text'][ $bp ]['value'] = array();
						}
						$root['text'][ $bp ]['value']['color'] = $bl;
					}
				}
			}
		}
	}

	/**
	 * When the child is a `dsm/flipbox-child` block but attrs are incomplete, parse `[dsm_flipbox_child]` from
	 * block HTML and merge design from D4 (fonts, button, face background, etc.) using the same conversion as the shortcode path.
	 *
	 * @param array<string, mixed> $attrs  Parent attrs (by ref).
	 * @param array<string, mixed> $child  Inner block.
	 * @param string               $prefix front|back.
	 */
	private static function maybe_merge_flipbox_child_from_shortcode_fallback( array &$attrs, array $child, string $prefix ): void {
		$d4 = self::extract_d4_child_attrs_from_block( $child );
		if ( array() === $d4 ) {
			return;
		}

		$face_key     = 'front' === $prefix ? 'front_face' : 'back_face';
		$need_bg      = self::should_try_d4_background_fallback( $attrs, $face_key );
		$need_font    = self::flipbox_face_needs_typography_from_d4_conversion( $attrs, $prefix );
		$need_button  = self::flipbox_face_needs_button_from_d4_conversion( $attrs, $prefix );
		$need_icon    = self::flipbox_face_needs_font_icon_from_d4( $attrs, $prefix, $d4 );
		$need_text = ! self::flipbox_face_text_group_desktop_ready_for_classnames( $attrs, $prefix );
		if ( ! $need_bg && ! $need_font && ! $need_button && ! $need_icon && ! $need_text ) {
			return;
		}

		self::apply_flipbox_child_design_from_d4_shortcode_conversion( $attrs, $d4, $prefix );
	}

	/**
	 * True when `front_button` / `back_button` has no migrated decoration (D4 child button styles missing).
	 *
	 * @param array<string, mixed> $attrs  Parent attrs.
	 * @param string               $prefix front|back.
	 */
	private static function flipbox_face_needs_button_from_d4_conversion( array $attrs, string $prefix ): bool {
		$key = 'front' === $prefix ? 'front_button' : 'back_button';
		if ( ! isset( $attrs[ $key ] ) || ! is_array( $attrs[ $key ] ) ) {
			return true;
		}
		$dec = $attrs[ $key ]['decoration'] ?? null;
		if ( ! is_array( $dec ) || array() === $dec ) {
			return true;
		}
		foreach ( $dec as $dec_val ) {
			if ( is_array( $dec_val ) && array() !== $dec_val ) {
				return false;
			}
			if ( ! is_array( $dec_val ) && '' !== $dec_val && null !== $dec_val ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * D4 `button_use_icon` → D5 `decoration.button.*.value.icon.enable` (Show Button Icon). Runs even when `button_icon` is empty.
	 *
	 * D4 often omits `button_use_icon` from the shortcode when it matches the default (“on”), so missing key defaults to `on`.
	 *
	 * @param array<string, mixed> $button_attr Full `button` / `front_button` / `back_button` branch (by ref).
	 * @param array<string, mixed> $d4_attrs    Original D4 shortcode attrs.
	 */
	private static function apply_button_icon_enable_from_d4( array &$button_attr, array $d4_attrs ): void {
		if ( ! isset( $button_attr['decoration'] ) || ! is_array( $button_attr['decoration'] ) ) {
			$button_attr['decoration'] = array();
		}
		if ( ! isset( $button_attr['decoration']['button'] ) || ! is_array( $button_attr['decoration']['button'] ) ) {
			$button_attr['decoration']['button'] = array();
		}
		$use    = isset( $d4_attrs['button_use_icon'] ) ? strtolower( (string) $d4_attrs['button_use_icon'] ) : 'on';
		$enable = 'off' === $use ? 'off' : 'on';
		foreach ( array( 'desktop', 'tablet', 'phone' ) as $bp ) {
			if ( ! isset( $button_attr['decoration']['button'][ $bp ] ) || ! is_array( $button_attr['decoration']['button'][ $bp ] ) ) {
				$button_attr['decoration']['button'][ $bp ] = array();
			}
			if ( ! isset( $button_attr['decoration']['button'][ $bp ]['value'] ) || ! is_array( $button_attr['decoration']['button'][ $bp ]['value'] ) ) {
				$button_attr['decoration']['button'][ $bp ]['value'] = array();
			}
			$val = &$button_attr['decoration']['button'][ $bp ]['value'];
			if ( ! isset( $val['icon'] ) || ! is_array( $val['icon'] ) ) {
				$val['icon'] = array();
			}
			$val['icon']['enable'] = $enable;
		}
	}

	/**
	 * D4→D5 post-convert only: sync `button_use_icon` → `decoration.button.*.value.icon.enable`.
	 *
	 * Icon glyph (`icon.settings`) is left to Divi conversion — do not merge or override from PHP.
	 *
	 * @param array<string, mixed> $module_attrs    Full `dsm/flipbox-child` attrs (by ref).
	 * @param array<string, mixed> $d4_attrs        Original D4 shortcode attrs (`button_use_icon`).
	 */
	public static function sync_flipbox_child_button_icon_enable_from_d4_post_convert( array &$module_attrs, array $d4_attrs ): void {
		if ( ! isset( $module_attrs['button'] ) || ! is_array( $module_attrs['button'] ) ) {
			return;
		}
		self::apply_button_icon_enable_from_d4( $module_attrs['button'], $d4_attrs );
	}

	/**
	 * `dsm/flipbox-child` post-convert: ensure IconPicker has a glyph when D4 `font_icon` exists but conversion left a placeholder.
	 *
	 * @param array<string, mixed> $attrs    Converted child attrs (by ref).
	 * @param array<string, mixed> $d4_attrs Original D4 shortcode attributes.
	 */
	public static function hydrate_flipbox_child_font_icon_after_conversion( array &$attrs, array $d4_attrs ): void {
		if ( ! isset( $d4_attrs['font_icon'] ) || ! is_string( $d4_attrs['font_icon'] ) || '' === trim( $d4_attrs['font_icon'] ) ) {
			return;
		}
		if ( ! isset( $attrs['font_icon'] ) || ! is_array( $attrs['font_icon'] ) ) {
			return;
		}
		if ( self::flipbox_font_icon_inner_has_unicode( $attrs, 'font_icon' ) ) {
			return;
		}
		self::hydrate_font_icon_from_d4( $attrs, 'font_icon', trim( $d4_attrs['font_icon'] ) );
	}

	/**
	 * True when any of title / subtitle / content is missing a `decoration.font` branch (D4 header / subhead / body fonts).
	 *
	 * @param array<string, mixed> $attrs  Parent attrs.
	 * @param string               $prefix front|back.
	 */
	private static function flipbox_face_needs_typography_from_d4_conversion( array $attrs, string $prefix ): bool {
		foreach ( array( "{$prefix}_title", "{$prefix}_subtitle", "{$prefix}_content" ) as $key ) {
			if ( ! isset( $attrs[ $key ]['decoration']['font'] ) || ! is_array( $attrs[ $key ]['decoration']['font'] ) ) {
				return true;
			}
			if ( array() === $attrs[ $key ]['decoration']['font'] ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Whether `font_icon` innerContent already has a unicode glyph (IconPicker / migrated shapes).
	 *
	 * @param array<string, mixed> $attrs    Parent attrs.
	 * @param string               $attr_key front_font_icon|back_font_icon.
	 */
	private static function flipbox_font_icon_inner_has_unicode( array $attrs, string $attr_key ): bool {
		if ( ! isset( $attrs[ $attr_key ] ) || ! is_array( $attrs[ $attr_key ] ) ) {
			return false;
		}
		$ic = $attrs[ $attr_key ]['innerContent'] ?? null;
		if ( ! is_array( $ic ) ) {
			return false;
		}
		if ( isset( $ic['desktop']['value'] ) && is_array( $ic['desktop']['value'] ) ) {
			$u = $ic['desktop']['value']['unicode'] ?? '';
			return is_string( $u ) && '' !== trim( $u );
		}
		if ( isset( $ic['value'] ) && is_array( $ic['value'] ) ) {
			$u = $ic['value']['unicode'] ?? '';
			return is_string( $u ) && '' !== trim( $u );
		}
		if ( isset( $ic['unicode'] ) && is_string( $ic['unicode'] ) && '' !== trim( $ic['unicode'] ) ) {
			return true;
		}
		return false;
	}

	/**
	 * Run shortcode fallback when D4 used an icon but the merged parent attrs have no IconPicker value.
	 *
	 * @param array<string, mixed> $attrs  Parent attrs.
	 * @param string               $prefix front|back.
	 * @param array<string, mixed> $d4     Parsed `[dsm_flipbox_child]` attributes.
	 */
	private static function flipbox_face_needs_font_icon_from_d4( array $attrs, string $prefix, array $d4 ): bool {
		$use_d4 = isset( $d4['use_icon'] ) ? strtolower( (string) $d4['use_icon'] ) : '';
		$use_attrs = isset( $attrs[ "{$prefix}_use_icon" ]['innerContent']['desktop']['value'] )
			? strtolower( (string) $attrs[ "{$prefix}_use_icon" ]['innerContent']['desktop']['value'] )
			: '';
		$use_on = ( 'on' === $use_d4 || 'on' === $use_attrs );
		if ( ! $use_on ) {
			return false;
		}
		return ! self::flipbox_font_icon_inner_has_unicode( $attrs, "{$prefix}_font_icon" );
	}

	/**
	 * True when the face has no usable background branch yet (missing or empty array).
	 *
	 * @param array<string, mixed> $attrs         Parent attrs.
	 * @param string               $face_attr_key front_face|back_face.
	 */
	private static function should_try_d4_background_fallback( array $attrs, string $face_attr_key ): bool {
		if ( ! isset( $attrs[ $face_attr_key ]['decoration'] ) || ! is_array( $attrs[ $face_attr_key ]['decoration'] ) ) {
			return true;
		}
		if ( ! isset( $attrs[ $face_attr_key ]['decoration']['background'] ) ) {
			return true;
		}
		$bg = $attrs[ $face_attr_key ]['decoration']['background'];
		return is_array( $bg ) && array() === $bg;
	}

	/**
	 * @param array<string, mixed> $attrs    Attrs (by ref).
	 * @param string               $attr_key e.g. front_content.
	 * @param string               $html     HTML body.
	 */
	private static function set_inner_desktop_rich_text( array &$attrs, string $attr_key, string $html ): void
	{
		if ( ! isset( $attrs[ $attr_key ] ) || ! is_array( $attrs[ $attr_key ] ) ) {
			$attrs[ $attr_key ] = array();
		}
		if ( ! isset( $attrs[ $attr_key ]['innerContent'] ) || ! is_array( $attrs[ $attr_key ]['innerContent'] ) ) {
			$attrs[ $attr_key ]['innerContent'] = array();
		}
		if ( ! isset( $attrs[ $attr_key ]['innerContent']['desktop'] ) || ! is_array( $attrs[ $attr_key ]['innerContent']['desktop'] ) ) {
			$attrs[ $attr_key ]['innerContent']['desktop'] = array();
		}
		$attrs[ $attr_key ]['innerContent']['desktop']['value'] = array(
			'text' => $html,
		);
	}

	/**
	 * @param array<string, mixed> $attrs    Attrs (by ref).
	 * @param string               $attr_key Image attr key.
	 * @param string               $raw      URL or attachment ID string.
	 */
	private static function hydrate_upload_from_d4_string( array &$attrs, string $attr_key, string $raw ): void
	{
		$url = $raw;
		if ( ctype_digit( $raw ) ) {
			$attachment_url = wp_get_attachment_url( (int) $raw );
			if ( is_string( $attachment_url ) && '' !== $attachment_url ) {
				$url = $attachment_url;
			}
		}

		if ( ! isset( $attrs[ $attr_key ] ) || ! is_array( $attrs[ $attr_key ] ) ) {
			$attrs[ $attr_key ] = array();
		}
		$attrs[ $attr_key ]['innerContent'] = array(
			'desktop' => array(
				'value' => array(
					'src' => $url,
					'id'  => ctype_digit( $raw ) ? (string) (int) $raw : '0',
				),
			),
		);
	}

	/**
	 * Ensure `font_icon` innerContent uses `desktop.value` and default `type` to `divi` when unicode exists (migrated / D4 shapes).
	 *
	 * @param array<string, mixed> $attrs    Attrs (by ref).
	 * @param string               $attr_key front_font_icon|back_font_icon.
	 */
	private static function normalize_flipbox_font_icon_attr( array &$attrs, string $attr_key ): void {
		if ( ! isset( $attrs[ $attr_key ] ) || ! is_array( $attrs[ $attr_key ] ) ) {
			return;
		}
		$ic = $attrs[ $attr_key ]['innerContent'] ?? null;
		if ( ! is_array( $ic ) ) {
			return;
		}

		if ( isset( $ic['desktop']['value'] ) && is_array( $ic['desktop']['value'] ) ) {
			$dv = &$attrs[ $attr_key ]['innerContent']['desktop']['value'];
			if ( isset( $dv['unicode'] )
				&& is_string( $dv['unicode'] )
				&& '' !== trim( $dv['unicode'] )
				&& ( ! isset( $dv['type'] ) || '' === trim( (string) $dv['type'] ) ) ) {
				$dv['type'] = 'divi';
			}
			if ( ! isset( $dv['weight'] ) || '' === trim( (string) ( $dv['weight'] ?? '' ) ) ) {
				$dv['weight'] = '400';
			}
			return;
		}

		if ( isset( $ic['unicode'] ) || isset( $ic['type'] ) ) {
			$val = $ic;
			if ( isset( $val['unicode'] ) && is_string( $val['unicode'] ) && '' !== trim( $val['unicode'] ) && ( ! isset( $val['type'] ) || '' === trim( (string) $val['type'] ) ) ) {
				$val['type'] = 'divi';
			}
			if ( ! isset( $val['weight'] ) || '' === trim( (string) ( $val['weight'] ?? '' ) ) ) {
				$val['weight'] = '400';
			}
			$attrs[ $attr_key ]['innerContent'] = array(
				'desktop' => array(
					'value' => $val,
				),
			);
			return;
		}

		if ( isset( $ic['value'] ) && is_array( $ic['value'] ) && ( isset( $ic['value']['unicode'] ) || isset( $ic['value']['type'] ) ) ) {
			$val = $ic['value'];
			if ( isset( $val['unicode'] ) && is_string( $val['unicode'] ) && '' !== trim( $val['unicode'] ) && ( ! isset( $val['type'] ) || '' === trim( (string) $val['type'] ) ) ) {
				$val['type'] = 'divi';
			}
			if ( ! isset( $val['weight'] ) || '' === trim( (string) ( $val['weight'] ?? '' ) ) ) {
				$val['weight'] = '400';
			}
			$attrs[ $attr_key ]['innerContent'] = array(
				'desktop' => array(
					'value' => $val,
				),
			);
		}
	}

	/**
	 * Map D4 `font_icon` string → `front_font_icon` / `back_font_icon` IconPicker shape.
	 *
	 * Prefer `dsm/flipbox-child` conversion (attribute key `font_icon`); fall back to `divi/icon` (`icon`),
	 * then {@see ValueExpansion::convertIcon()}.
	 *
	 * @param array<string, mixed> $attrs    Attrs (by ref).
	 * @param string               $attr_key front_font_icon|back_font_icon.
	 * @param string               $raw      D4 font_icon string.
	 */
	private static function hydrate_font_icon_from_d4( array &$attrs, string $attr_key, string $raw ): void
	{
		$converted_child = null;
		if ( class_exists( Conversion::class ) ) {
			try {
				$converted_child = Conversion::convertAttrs(
					array( 'font_icon' => $raw ),
					'dsm/flipbox-child',
					null,
					false,
					false
				);
			} catch ( \Throwable $e ) {
				$converted_child = null;
			}
		}

		if ( is_array( $converted_child ) && isset( $converted_child['font_icon'] ) && is_array( $converted_child['font_icon'] ) ) {
			$attrs[ $attr_key ] = $converted_child['font_icon'];
		}

		self::normalize_flipbox_font_icon_attr( $attrs, $attr_key );

		if ( self::flipbox_font_icon_inner_has_unicode( $attrs, $attr_key ) ) {
			return;
		}

		$converted_divi_icon = null;
		if ( class_exists( Conversion::class ) ) {
			try {
				$converted_divi_icon = Conversion::convertAttrs(
					array( 'font_icon' => $raw ),
					'divi/icon',
					null,
					false,
					false
				);
			} catch ( \Throwable $e ) {
				$converted_divi_icon = null;
			}
		}

		if ( is_array( $converted_divi_icon ) && isset( $converted_divi_icon['icon'] ) && is_array( $converted_divi_icon['icon'] ) ) {
			$attrs[ $attr_key ] = array(
				'innerContent' => isset( $converted_divi_icon['icon']['innerContent'] ) && is_array( $converted_divi_icon['icon']['innerContent'] )
					? $converted_divi_icon['icon']['innerContent']
					: array(),
			);
		}

		self::normalize_flipbox_font_icon_attr( $attrs, $attr_key );

		if ( self::flipbox_font_icon_inner_has_unicode( $attrs, $attr_key ) ) {
			return;
		}

		if ( ! class_exists( ValueExpansion::class ) ) {
			return;
		}

		// convertIcon handles %%index%% and delegates to convertFontIcon for unicode||type||weight strings.
		$icon_flat = ValueExpansion::convertIcon( $raw );
		if ( false === $icon_flat || ! is_array( $icon_flat ) || array() === $icon_flat ) {
			return;
		}

		if ( ! isset( $icon_flat['type'] ) ) {
			$icon_flat['type'] = 'divi';
		}
		if ( ! isset( $icon_flat['weight'] ) || '' === trim( (string) ( $icon_flat['weight'] ?? '' ) ) ) {
			$icon_flat['weight'] = '400';
		}

		$attrs[ $attr_key ] = array(
			'innerContent' => array(
				'desktop' => array(
					'value' => $icon_flat,
				),
			),
		);
		self::normalize_flipbox_font_icon_attr( $attrs, $attr_key );
	}
}
