<?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 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'];
		}

		$inner_html = trim( (string) ( $block['innerHTML'] ?? '' ) );

		if ( preg_match( '/^\[([^\s\]]+)/', $inner_html, $matches ) ) {
			return (string) $matches[1];
		}

		return '';
	}

	/**
	 * @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
	{
		if ( ! self::content_needs_legacy_conversion( $content_raw ) ) {
			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;

			// Divi's fast path converts every shortcode-module on the page; skip when third-party/core blocks are mixed in.
			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 );

		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 );
			}

			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' );
		}
	}
}
