<?php
/**
 * Converts remaining `divi/shortcode-module` blocks to native `dsm/*` (and other D5) blocks.
 *
 * Single source of truth for Visual Builder save persistence and bulk migration.
 *
 * @package DiviSupreme
 */

declare(strict_types=1);

namespace DiviSupreme\Includes\Divi5Conversion;

use ET\Builder\Migration\Migration;
use ET\Builder\Packages\Conversion\Conversion;
use ET\Builder\Packages\Conversion\ShortcodeMigration;
use ET\Builder\Packages\GlobalData\GlobalData;
use ET\Builder\VisualBuilder\Saving\SavingUtility;
use Throwable;

/**
 * Fault-tolerant legacy shortcode-module conversion.
 */
final class LegacyShortcodeConversionService
{

	private const LEGACY_BLOCK_MARKER = 'divi/shortcode-module';

	/**
	 * Divi 4 Supreme module shortcode prefix (e.g. `dsm_card`, not `et_pb_dsm_*`).
	 */
	private const SUPREME_D4_SHORTCODE_PREFIX = 'dsm_';

	/**
	 * Minimum seconds to allow for conversion during a single HTTP request.
	 */
	private const CONVERSION_TIME_BUDGET_SECONDS = 120;

	/**
	 * @return void
	 */
	public static function register_hooks(): void
	{
		add_action(
			'divi_visual_builder_rest_before_save_post',
			array( self::class, 'extend_time_limit_for_vb_save' ),
			2,
			5
		);
	}

	/**
	 * Extend PHP time limit when a VB save still contains legacy shortcode-module blocks.
	 *
	 * @param mixed  $abort_save_response Unused filter value.
	 * @param int    $post_id             Post ID.
	 * @param string $post_content        Content being saved.
	 * @param mixed  ...$rest             Additional hook arguments (status, options, page settings).
	 * @return mixed
	 */
	public static function extend_time_limit_for_vb_save( $abort_save_response, int $post_id, string $post_content, ...$rest )
	{
		unset( $abort_save_response, $post_id, $rest );

		if ( self::content_needs_legacy_conversion( $post_content ) ) {
			self::raise_time_limit();
		}

		return null;
	}

	/**
	 * Whether content is a D5 layout that still wraps Divi Supreme D4 shortcodes (`dsm_*`) in shortcode-module blocks.
	 */
	public static function content_needs_legacy_conversion( string $content ): bool
	{
		return str_contains( $content, self::LEGACY_BLOCK_MARKER )
			&& str_contains( $content, '<!-- wp:divi/' )
			&& self::content_contains_supreme_legacy_shortcode( $content );
	}

	/**
	 * Whether stored layout content contains native Divi 5 `dsm/*` blocks (`<!-- wp:dsm/foo -->`).
	 *
	 * These layouts are already native in the database but may still show D4 shortcodes on the
	 * frontend until Divi's builder save pipeline runs (cache / static resources refresh).
	 */
	public static function content_contains_native_supreme_blocks( string $content ): bool
	{
		if ( ! str_contains( $content, 'wp:dsm/' ) ) {
			return false;
		}

		$offset = 0;
		$needle = 'wp:dsm/';

		while ( true ) {
			$pos = strpos( $content, $needle, $offset );

			if ( false === $pos ) {
				return false;
			}

			$after = $content[ $pos + strlen( $needle ) ] ?? '';

			if ( '' === $after || ! ctype_alnum( (string) $after ) ) {
				return true;
			}

			$offset = $pos + 1;
		}
	}

	/**
	 * Whether this layout should be included in bulk scan / re-save (legacy or native Supreme modules).
	 */
	public static function content_has_supreme_stored_modules( string $content ): bool
	{
		return null !== self::get_resave_reason( $content );
	}

	/**
	 * Classify why a layout appears in the bulk tool.
	 *
	 * @return 'legacy_conversion'|'builder_resave'|null
	 */
	public static function get_resave_reason( string $content ): ?string
	{
		if ( self::content_needs_legacy_conversion( $content ) ) {
			return 'legacy_conversion';
		}

		// Pure D4 trees that still contain Supreme `dsm_*` shortcodes — convert via outlines
		// (Divi maps surrounding `et_pb_*` structure so native `dsm/*` blocks can exist).
		if ( self::content_is_pure_d4_shortcode_layout( $content ) ) {
			return 'legacy_conversion';
		}

		if ( self::content_contains_native_supreme_blocks( $content ) ) {
			return 'builder_resave';
		}

		// D5 layouts that still wrap Supreme D4 shortcodes.
		if (
			self::content_contains_supreme_legacy_shortcode( $content )
			&& (
				str_contains( $content, '[_et_pb_' )
				|| str_contains( $content, '<!-- wp:' )
			)
			&& ! self::content_is_pure_d4_shortcode_layout( $content )
		) {
			return 'legacy_conversion';
		}

		/**
		 * Allow sites to flag additional stored shapes for builder re-save.
		 *
		 * @param string|null $reason   `legacy_conversion`, `builder_resave`, or null.
		 * @param string      $content  Post content.
		 */
		$filtered = apply_filters( 'dsm_legacy_conversion_resave_reason', null, $content );

		if ( is_string( $filtered ) && in_array( $filtered, array( 'legacy_conversion', 'builder_resave' ), true ) ) {
			return $filtered;
		}

		return null;
	}

	/**
	 * Whether content is a pure Divi 4 shortcode layout (no Gutenberg / D5 block markers).
	 */
	public static function content_is_pure_d4_shortcode_layout( string $content ): bool
	{
		$content = trim( $content );

		if ( '' === $content ) {
			return false;
		}

		if ( str_contains( $content, '<!-- wp:' ) ) {
			return false;
		}

		return self::content_contains_supreme_legacy_shortcode( $content )
			&& str_contains( $content, '[et_pb_' );
	}

	/**
	 * Inspect one post's stored layout for troubleshooting (read-only; no serialize_blocks).
	 *
	 * @return array<string, mixed>
	 */
	public static function diagnose_post( int $post_id ): array
	{
		$post = get_post( $post_id );

		if ( ! $post instanceof \WP_Post ) {
			return array(
				'post_id' => $post_id,
				'error'   => __( 'Post not found.', 'dsm-supreme-modules-pro-for-divi' ),
			);
		}

		$content = wp_unslash( (string) $post->post_content );

		return array(
			'post_id'                    => $post_id,
			'post_type'                  => (string) $post->post_type,
			'title'                      => (string) $post->post_title,
			'resave_reason'              => self::get_resave_reason( $content ),
			'needs_legacy_conversion'    => self::content_needs_legacy_conversion( $content ),
			'has_native_supreme_blocks'  => self::content_contains_native_supreme_blocks( $content ),
			'has_supreme_legacy_shortcode' => self::content_contains_supreme_legacy_shortcode( $content ),
			'is_pure_d4_shortcode_layout'  => self::content_is_pure_d4_shortcode_layout( $content ),
			'legacy_shortcode_module_count' => self::count_legacy_blocks( $content ),
			'has_divi_shortcode_module'  => str_contains( $content, self::LEGACY_BLOCK_MARKER ),
			'has_d5_divi_layout'         => str_contains( $content, '<!-- wp:divi/' ),
			'edit_url'                   => (string) get_edit_post_link( $post_id, 'raw' ),
		);
	}

	/**
	 * Whether stored content still references a Divi Supreme D4 shortcode (`dsm_*`).
	 */
	public static function content_contains_supreme_legacy_shortcode( string $content ): bool
	{
		if ( str_contains( $content, '[' . self::SUPREME_D4_SHORTCODE_PREFIX ) ) {
			return true;
		}

		if ( str_contains( $content, '"shortcodeName":"' . self::SUPREME_D4_SHORTCODE_PREFIX ) ) {
			return true;
		}

		/**
		 * Allow extensions to refine Supreme-only detection (e.g. custom storage shapes).
		 *
		 * @param bool   $contains Whether Supreme legacy shortcodes were detected.
		 * @param string $content  Post content.
		 */
		return (bool) apply_filters(
			'dsm_legacy_conversion_content_has_supreme_legacy',
			(bool) preg_match( '/shortcodeName["\']?\s*:\s*["\']' . preg_quote( self::SUPREME_D4_SHORTCODE_PREFIX, '/' ) . '/', $content ),
			$content
		);
	}

	/**
	 * Count `divi/shortcode-module` blocks whose inner shortcode is a Divi Supreme `dsm_*` slug.
	 */
	public static function count_legacy_blocks( string $content ): int
	{
		if ( ! self::content_contains_supreme_legacy_shortcode( $content ) ) {
			return 0;
		}

		return self::count_supreme_shortcode_module_blocks( $content );
	}

	/**
	 * Whether the layout still has non-Supreme legacy `divi/shortcode-module` blocks (third-party / core).
	 */
	public static function content_has_non_supreme_legacy_shortcode_module( string $content ): bool
	{
		if ( ! str_contains( $content, self::LEGACY_BLOCK_MARKER ) ) {
			return false;
		}

		foreach ( self::get_parsed_layout_blocks( $content ) as $block ) {
			if ( 'divi/shortcode-module' === ( $block['blockName'] ?? null ) && ! self::shortcode_module_block_is_supreme( $block ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<string, mixed> $block Parsed block.
	 */
	public static function shortcode_module_block_is_supreme( array $block ): bool
	{
		if ( 'divi/shortcode-module' !== ( $block['blockName'] ?? null ) ) {
			return false;
		}

		$shortcode_name = self::extract_shortcode_name_from_shortcode_module_block( $block );

		return '' !== $shortcode_name && str_starts_with( $shortcode_name, self::SUPREME_D4_SHORTCODE_PREFIX );
	}

	/**
	 * @param array<string, mixed> $block Parsed `divi/shortcode-module` block.
	 */
	public static function extract_shortcode_name_from_shortcode_module_block( array $block ): string
	{
		$attrs = $block['attrs'] ?? array();

		if ( is_array( $attrs ) && ! empty( $attrs['shortcodeName'] ) && is_string( $attrs['shortcodeName'] ) ) {
			return $attrs['shortcodeName'];
		}

		foreach ( self::shortcode_module_inner_html_candidates( $block ) as $inner_html ) {
			// Divi import / wpautop often wraps shortcodes in `<p>…</p>`; do not require `[` at offset 0.
			if ( preg_match( '/\[([a-z0-9_]+)/i', $inner_html, $matches ) ) {
				return (string) $matches[1];
			}
		}

		return '';
	}

	/**
	 * Inner HTML sources for a shortcode-module (body + attrs.innerHTML).
	 *
	 * @param array<string, mixed> $block Parsed block.
	 * @return array<int, string>
	 */
	private static function shortcode_module_inner_html_candidates( array $block ): array
	{
		$candidates = array();
		$attrs      = $block['attrs'] ?? array();

		if ( is_array( $attrs ) && isset( $attrs['innerHTML'] ) && is_string( $attrs['innerHTML'] ) && '' !== $attrs['innerHTML'] ) {
			$candidates[] = $attrs['innerHTML'];
		}

		if ( isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) && '' !== $block['innerHTML'] ) {
			$candidates[] = $block['innerHTML'];
		}

		return $candidates;
	}

	/**
	 * Strip wpautop `<p>` wrappers so Divi's shortcode-module converter sees a leading `[slug`.
	 *
	 * Divi {@see Conversion::processShortcodeModuleBlock()} only treats content as convertible when
	 * `trim(innerHTML)` matches `/^\[…/`. Library imports often store `<p>[dsm_advanced_tabs…]</p>`,
	 * which leaves the block as `divi/shortcode-module` and crashes the VB (`moduleDefinition.asMutable`).
	 *
	 * @param string $html Shortcode-module inner HTML.
	 */
	public static function unwrap_shortcode_autop( string $html ): string
	{
		$html = trim( $html );

		if ( '' === $html || ! str_contains( $html, '[' ) ) {
			return $html;
		}

		if ( function_exists( 'shortcode_unautop' ) ) {
			$html = shortcode_unautop( $html );
		}

		$html = preg_replace( '/<p>\s*(\[)/i', '$1', $html );
		$html = preg_replace( '/(\])\s*<\/p>/i', '$1', $html );

		return trim( (string) $html );
	}

	/**
	 * Normalize Supreme shortcode-module HTML so Divi can convert it.
	 *
	 * @param array<string, mixed> $block Parsed `divi/shortcode-module` block.
	 * @return array<string, mixed>
	 */
	public static function normalize_supreme_shortcode_module_block( array $block ): array
	{
		if ( ! self::shortcode_module_block_is_supreme( $block ) ) {
			return $block;
		}

		if ( isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) {
			$block['innerHTML'] = self::unwrap_shortcode_autop( $block['innerHTML'] );
		}

		if ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) && isset( $block['attrs']['innerHTML'] ) && is_string( $block['attrs']['innerHTML'] ) ) {
			$block['attrs']['innerHTML'] = self::unwrap_shortcode_autop( $block['attrs']['innerHTML'] );
		}

		if ( isset( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) {
			foreach ( $block['innerContent'] as $index => $piece ) {
				if ( is_string( $piece ) && str_contains( $piece, '[dsm_' ) ) {
					$block['innerContent'][ $index ] = self::unwrap_shortcode_autop( $piece );
				}
			}
		}

		return $block;
	}

	/**
	 * Unwrap autop wrappers on Supreme shortcode-modules before Divi conversion.
	 *
	 * @param string $content Layout content.
	 */
	public static function normalize_supreme_shortcode_modules_in_content( string $content ): string
	{
		if ( ! str_contains( $content, self::LEGACY_BLOCK_MARKER ) || ! str_contains( $content, 'dsm_' ) ) {
			return $content;
		}

		$blocks  = parse_blocks( $content );
		$changed = false;
		$blocks  = self::normalize_supreme_shortcode_modules_in_blocks( $blocks, $changed );

		return $changed ? serialize_blocks( $blocks ) : $content;
	}

	/**
	 * @param array<int, array<string, mixed>> $blocks  Parsed blocks.
	 * @param bool                             $changed Set true when a block is mutated.
	 * @return array<int, array<string, mixed>>
	 */
	private static function normalize_supreme_shortcode_modules_in_blocks( array $blocks, bool &$changed ): array
	{
		foreach ( $blocks as $index => $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}

			if ( 'divi/shortcode-module' === ( $block['blockName'] ?? null ) && self::shortcode_module_block_is_supreme( $block ) ) {
				$normalized = self::normalize_supreme_shortcode_module_block( $block );
				if ( $normalized !== $block ) {
					$blocks[ $index ] = $normalized;
					$changed          = true;
				}
			}

			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$inner_changed = false;
				$inner         = self::normalize_supreme_shortcode_modules_in_blocks( $block['innerBlocks'], $inner_changed );
				if ( $inner_changed ) {
					$blocks[ $index ]['innerBlocks'] = $inner;
					$changed                         = true;
				}
			}
		}

		return $blocks;
	}

	/**
	 * @return array<int, array<string, mixed>>
	 */
	private static function get_parsed_layout_blocks( string $content ): array
	{
		$content_to_process = $content;
		$placeholder_pattern = '/<!-- wp:divi\/placeholder[^>]*? -->(.*?)<!-- \/wp:divi\/placeholder -->/s';

		if ( preg_match( $placeholder_pattern, $content, $matches ) ) {
			$content_to_process = $matches[1];
		}

		$blocks = parse_blocks( $content_to_process );

		return is_array( $blocks ) ? self::flatten_blocks( $blocks ) : array();
	}

	/**
	 * @param array<int, array<string, mixed>> $blocks Parsed blocks.
	 * @return array<int, array<string, mixed>>
	 */
	private static function flatten_blocks( array $blocks ): array
	{
		$flat = array();

		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}

			$flat[] = $block;

			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				$flat = array_merge( $flat, self::flatten_blocks( $block['innerBlocks'] ) );
			}
		}

		return $flat;
	}

	/**
	 * Count Supreme `divi/shortcode-module` blocks only.
	 */
	private static function count_supreme_shortcode_module_blocks( string $content ): int
	{
		$count = 0;

		foreach ( self::get_parsed_layout_blocks( $content ) as $block ) {
			if ( self::shortcode_module_block_is_supreme( $block ) ) {
				++$count;
			}
		}

		return $count;
	}

	/**
	 * Load Divi shortcode framework + Supreme conversion maps (required for admin-ajax bulk runs).
	 *
	 * @throws \RuntimeException When Divi builder / conversion is unavailable.
	 */
	public static function bootstrap_divi_for_conversion(): void
	{
		// Divi skips the builder + shortcode framework on most admin-ajax requests; bulk conversion needs both.
		add_filter( 'et_builder_should_load_framework', '__return_true', 99999 );
		add_filter( 'et_should_load_shortcode_framework', '__return_true', 99999 );

		if ( function_exists( 'et_builder_load_framework' ) && ! did_action( 'et_builder_framework_loaded' ) ) {
			et_builder_load_framework();
		}

		if ( ! function_exists( 'et_load_shortcode_framework' ) ) {
			throw new \RuntimeException(
				__( 'Divi Builder is not loaded. Ensure Divi 5 is active before converting legacy modules.', 'dsm-supreme-modules-pro-for-divi' )
			);
		}

		if ( ! class_exists( Conversion::class ) || ! class_exists( Migration::class ) ) {
			throw new \RuntimeException(
				__( 'Divi 5 conversion is not available.', 'dsm-supreme-modules-pro-for-divi' )
			);
		}

		ConversionOutlineBootstrap::ensure_conversion_maps();
		Conversion::initialize_shortcode_framework();

		if ( did_action( 'wp_loaded' ) && function_exists( 'et_builder_add_main_elements' ) && ! did_action( 'et_builder_ready' ) ) {
			if ( function_exists( 'et_builder_init_global_settings' ) ) {
				et_builder_init_global_settings();
			}

			et_builder_add_main_elements();
		}

		/**
		 * Mirrors Divi Visual Builder / REST conversion bootstrap.
		 *
		 * @see \ET\Builder\VisualBuilder\REST\ContentConversion\ContentConversionController::convert_content()
		 */
		do_action( 'divi_visual_builder_before_d4_conversion' );
	}

	/**
	 * Convert legacy shortcode-module blocks (VB save + bulk tool entry point).
	 *
	 * @param string   $content_raw Raw post content.
	 * @param int|null $post_id     Optional post ID for AB testing / global template context.
	 */
	public static function convert_content( string $content_raw, ?int $post_id = null ): LegacyShortcodeConversionResult
	{
		$is_pure_d4_supreme = self::content_is_pure_d4_shortcode_layout( $content_raw );

		if ( ! self::content_needs_legacy_conversion( $content_raw ) && ! $is_pure_d4_supreme ) {
			return new LegacyShortcodeConversionResult(
				$content_raw,
				false,
				0,
				0
			);
		}

		$legacy_before = self::count_legacy_blocks( $content_raw );

		try {
			self::bootstrap_divi_for_conversion();
		} catch ( Throwable $bootstrap_error ) {
			return new LegacyShortcodeConversionResult(
				$content_raw,
				false,
				$legacy_before,
				$legacy_before,
				array(),
				$bootstrap_error->getMessage()
			);
		}

		self::raise_time_limit();

		try {
			$prepared      = self::prepare_content_migrations( $content_raw, $post_id );
			$fatal_error   = null;
			$module_errors = array();
			$converted     = null;
			$is_pure_d4_supreme = self::content_is_pure_d4_shortcode_layout( $prepared );

			// Pure D4 + Supreme: run Divi's full shortcode→block converter. Outlines map `dsm_*` →
			// `dsm/*`; Divi maps `et_pb_*` → `divi/*` (required host structure for native modules).
			if ( $is_pure_d4_supreme ) {
				try {
					$converted = Conversion::maybeConvertContent( $prepared, true, $post_id );
				} catch ( Throwable $exception ) {
					$fatal_error = $exception->getMessage();
				}

				if (
					is_string( $converted )
					&& $converted !== $prepared
					&& (
						self::content_contains_native_supreme_blocks( $converted )
						|| ! self::content_contains_supreme_legacy_shortcode( $converted )
					)
				) {
					$converted = self::apply_d5_portability_migrations( $converted );

					return new LegacyShortcodeConversionResult(
						$converted,
						true,
						$legacy_before,
						self::count_legacy_blocks( $converted ),
						$module_errors,
						$fatal_error
					);
				}

				return new LegacyShortcodeConversionResult(
					$content_raw,
					false,
					$legacy_before,
					$legacy_before,
					$module_errors,
					$fatal_error ?? __(
						'Pure Divi 4 layout with Supreme modules could not be converted.',
						'dsm-supreme-modules-pro-for-divi'
					)
				);
			}

			// D5 layouts with Supreme shortcode-modules (skip when third-party shortcode-modules remain).
			if ( ! self::content_has_non_supreme_legacy_shortcode_module( $prepared ) ) {
				try {
					$converted = Conversion::maybeConvertContent( $prepared, false, $post_id );
				} catch ( Throwable $exception ) {
					$fatal_error = $exception->getMessage();
				}
			}

			if ( is_string( $converted ) && $converted !== $prepared ) {
				$legacy_after = self::count_legacy_blocks( $converted );

				if ( $legacy_after < $legacy_before ) {
					$converted = self::apply_d5_portability_migrations( $converted );

					return new LegacyShortcodeConversionResult(
						$converted,
						true,
						$legacy_before,
						$legacy_after,
						$module_errors,
						$fatal_error
					);
				}
			}

			$fault_tolerant = self::convert_shortcode_modules_fault_tolerant( $prepared, $post_id );
			$converted_ft   = $fault_tolerant['content'];
			$module_errors  = array_merge( $module_errors, $fault_tolerant['module_errors'] );
			$legacy_after   = self::count_legacy_blocks( $converted_ft );
			$changed        = $converted_ft !== $content_raw;

			if ( $changed && $legacy_after < $legacy_before ) {
				$converted_ft   = self::apply_d5_portability_migrations( $converted_ft );
				$legacy_after = self::count_legacy_blocks( $converted_ft );
			}

			return new LegacyShortcodeConversionResult(
				$converted_ft,
				$changed,
				$legacy_before,
				$legacy_after,
				$module_errors,
				$fatal_error
			);
		} catch ( Throwable $exception ) {
			return new LegacyShortcodeConversionResult(
				$content_raw,
				false,
				$legacy_before,
				$legacy_before,
				array(),
				$exception->getMessage()
			);
		}
	}

	/**
	 * Persist converted content for a post (bulk tool + optional revision snapshot).
	 *
	 * @param int    $post_id Post ID.
	 * @param string $converted_content Converted post content (unslashed).
	 * @param bool   $dry_run When true, do not write to the database.
	 * @return array<string, mixed>
	 */
	public static function persist_converted_post( int $post_id, string $converted_content, bool $dry_run = false ): array
	{
		try {
			$post = get_post( $post_id );

			if ( ! $post instanceof \WP_Post ) {
				return array(
					'success' => false,
					'message' => __( 'Post not found.', 'dsm-supreme-modules-pro-for-divi' ),
				);
			}

			if ( ! current_user_can( 'edit_post', $post_id ) ) {
				return array(
					'success' => false,
					'message' => __( 'You do not have permission to edit this item.', 'dsm-supreme-modules-pro-for-divi' ),
				);
			}

			if ( $dry_run ) {
				return array(
					'success' => true,
					'dry_run' => true,
				);
			}

			$revision_id = wp_save_post_revision( $post );

			$update = wp_update_post(
				array(
					'ID'           => $post_id,
					'post_content' => wp_slash( $converted_content ),
				),
				true
			);

			if ( is_wp_error( $update ) ) {
				return array(
					'success' => false,
					'message' => $update->get_error_message(),
				);
			}

			update_post_meta( $post_id, '_et_pb_use_divi_5', 'on' );
			update_post_meta( $post_id, '_et_pb_use_builder', 'on' );

			if ( function_exists( 'et_divi_save_post' ) ) {
				et_divi_save_post( $post_id );
			}

			if ( class_exists( SavingUtility::class ) && defined( 'ET_BUILDER_LAYOUT_POST_TYPE' ) ) {
				SavingUtility::prime_page_cache_on_save( $post_id );
			}

			if ( function_exists( 'et_theme_builder_clear_wp_post_cache' ) ) {
				et_theme_builder_clear_wp_post_cache( $post_id );
			}

			clean_post_cache( $post_id );

			return array(
				'success'     => true,
				'revision_id' => $revision_id ? (int) $revision_id : null,
			);
		} catch ( Throwable $exception ) {
			return array(
				'success' => false,
				'message' => $exception->getMessage(),
			);
		}
	}

	/**
	 * Run migrations that {@see Conversion::maybeConvertContent()} applies before block conversion.
	 */
	private static function prepare_content_migrations( string $content_raw, ?int $post_id ): string
	{
		GlobalData::maybe_convert_global_colors_data();

		$content = $content_raw;

		if ( str_contains( $content, '<!-- wp:divi/placeholder -->' ) && str_contains( $content, '[et_pb_' ) ) {
			$placeholder_pattern = '/^<!-- wp:divi\/placeholder[^>]*? -->(.*?)<!-- \/wp:divi\/placeholder -->$/s';

			if ( preg_match( $placeholder_pattern, $content, $matches ) ) {
				$content = trim( $matches[1] );
			}
		}

		$content = trim( $content );
		$content = ShortcodeMigration::maybe_migrate_legacy_shortcode( $content );
		$content = Migration::get_instance()->migrate_content_shortcode( $content );
		// Before Divi processShortcodeModuleBlock: strip `<p>[dsm_…]</p>` wrappers from imports.
		$content = self::normalize_supreme_shortcode_modules_in_content( $content );

		return Conversion::normalizeAbSubjectId( $content );
	}

	/**
	 * Per shortcode-module fault isolation (mirrors Divi {@see Conversion::convertShortcodeModulesInD5Content()}).
	 *
	 * @return array{content: string, module_errors: array<int, array<string, mixed>>}
	 */
	private static function convert_shortcode_modules_fault_tolerant( string $content, ?int $post_id ): array
	{
		unset( $post_id );

		$module_collections = Conversion::getModuleCollections();
		$module_errors      = array();

		$placeholder_pattern = '/<!-- wp:divi\/placeholder[^>]*? -->(.*?)<!-- \/wp:divi\/placeholder -->/s';
		$content_to_process  = $content;
		$has_placeholder     = false;

		if ( preg_match( $placeholder_pattern, $content, $matches ) ) {
			$has_placeholder    = true;
			$content_to_process = $matches[1];
		}

		$block_objects = parse_blocks( $content_to_process );
		$converted       = '';

		foreach ( $block_objects as $block ) {
			$converted .= self::process_block_recursively_safe(
				$block,
				$module_collections,
				$module_errors
			);
		}

		if ( $has_placeholder ) {
			$converted = '<!-- wp:divi/placeholder -->' . $converted . '<!-- /wp:divi/placeholder -->';
		}

		return array(
			'content'        => $converted,
			'module_errors'  => $module_errors,
		);
	}

	/**
	 * @param array<string, mixed>              $block
	 * @param array<int, array<string, mixed>>  $module_collections
	 * @param array<int, array<string, mixed>>  $module_errors
	 */
	private static function process_block_recursively_safe( array $block, array $module_collections, array &$module_errors ): string
	{
		$block_name = $block['blockName'] ?? null;

		if ( 'divi/shortcode-module' === $block_name ) {
			if ( ! self::shortcode_module_block_is_supreme( $block ) ) {
				return self::serialize_shortcode_module_block( $block );
			}

			$block = self::normalize_supreme_shortcode_module_block( $block );

			try {
				$shortcode_module_count = 0;

				return Conversion::processShortcodeModuleBlock(
					$block,
					$module_collections,
					$shortcode_module_count
				);
			} catch ( Throwable $exception ) {
				$snippet = is_string( $block['innerHTML'] ?? null )
					? substr( trim( $block['innerHTML'] ), 0, 120 )
					: '';

				$module_errors[] = array(
					'message'   => $exception->getMessage(),
					'shortcode' => $snippet,
				);

				return self::serialize_shortcode_module_block( $block );
			}
		}

		if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
			$processed_inner = '';

			foreach ( $block['innerBlocks'] as $inner_block ) {
				if ( ! is_array( $inner_block ) ) {
					continue;
				}

				$processed_inner .= self::process_block_recursively_safe(
					$inner_block,
					$module_collections,
					$module_errors
				);
			}

			$attrs           = ! empty( $block['attrs'] ) ? ' ' . serialize_block_attributes( $block['attrs'] ) : '';
			$parent_name     = str_replace( 'core/', '', (string) $block_name );

			return "<!-- wp:{$parent_name}{$attrs} -->{$processed_inner}<!-- /wp:{$parent_name} -->";
		}

		if ( null === $block_name ) {
			return (string) ( $block['innerHTML'] ?? '' );
		}

		$leaf_name    = str_replace( 'core/', '', (string) $block_name );
		$attrs        = ! empty( $block['attrs'] ) ? ' ' . serialize_block_attributes( $block['attrs'] ) : '';
		$inner_html   = (string) ( $block['innerHTML'] ?? '' );

		return "<!-- wp:{$leaf_name}{$attrs} -->{$inner_html}<!-- /wp:{$leaf_name} -->";
	}

	/**
	 * @param array<string, mixed> $block
	 */
	private static function serialize_shortcode_module_block( array $block ): string
	{
		$attrs      = ! empty( $block['attrs'] ) ? ' ' . serialize_block_attributes( $block['attrs'] ) : '';
		$inner_html = (string) ( $block['innerHTML'] ?? '' );
		$name       = (string) ( $block['blockName'] ?? 'divi/shortcode-module' );

		return "<!-- wp:{$name}{$attrs} -->{$inner_html}<!-- /wp:{$name} -->";
	}

	/**
	 * D5-to-D5 migrations applied after D4→D5 conversion on VB REST endpoints.
	 */
	private static function apply_d5_portability_migrations( string $content ): string
	{
		$migrated = apply_filters( 'divi_framework_portability_import_migrated_post_content', $content );

		return is_string( $migrated ) ? $migrated : $content;
	}

	/**
	 * @return void
	 */
	private static function raise_time_limit(): void
	{
		if ( function_exists( 'set_time_limit' ) ) {
			@set_time_limit( self::CONVERSION_TIME_BUDGET_SECONDS );
		}

		if ( function_exists( 'wp_raise_memory_limit' ) ) {
			wp_raise_memory_limit( 'admin' );
		}
	}
}
