<?php
/**
 * Batch Processing
 *
 * @package ST Importer
 * @since 1.2.14
 */

namespace STImporter\Importer\Batch;

use STImporter\Importer\Batch\ST_Batch_Processing;
use STImporter\Importer\ST_Importer_File_System;
use STImporter\Importer\Helpers\ST_Image_Importer;
use STImporter\Importer\ST_Importer_Helper;
use STImporter\Importer\ST_Importer_Log;

if ( ! class_exists( 'ST_Batch_Processing_Gutenberg' ) ) :

	/**
	 * Astra Sites Batch Processing Brizy
	 *
	 * @since 1.2.14
	 */
	class ST_Batch_Processing_Gutenberg {

		/**
		 * Instance
		 *
		 * @since 1.2.14
		 * @access private
		 * @var object Class object.
		 */
		private static $instance = null;

		/**
		 * Initiator
		 *
		 * @since 1.2.14
		 * @return object initialized object of class.
		 */
		public static function get_instance() {

			if ( null === self::$instance ) {
				self::$instance = new self();
			}
			return self::$instance;
		}

		/**
		 * Constructor
		 *
		 * @since 1.2.14
		 */
		public function __construct() {}

		/**
		 * Allowed tags for the batch update process.
		 *
		 * @param  array<string, array<string, bool>> $allowedposttags   Array of default allowable HTML tags.
		 * @param  string|array<int, string>          $context    The context for which to retrieve tags. Allowed values are 'post',
		 *                                           'strip', 'data', 'entities', or the name of a field filter such as
		 *                                           'pre_user_description'.
		 * @return array<string, array<string, bool>> Array of allowed HTML tags and their allowed attributes.
		 */
		public function allowed_tags_and_attributes( $allowedposttags, $context ) {

			// Keep only for 'post' contenxt.
			if ( 'post' === $context ) {

				// <svg> tag and attributes.
				$allowedposttags['svg'] = array(
					'xmlns'   => true,
					'viewbox' => true,
				);

				// <path> tag and attributes.
				$allowedposttags['path'] = array(
					'd' => true,
				);
			}

			return $allowedposttags;
		}

		/**
		 * Import
		 *
		 * @since 1.2.14
		 * @return array<string, mixed>
		 */
		public function import() {
			ST_Importer_Log::add( 'Gutenberg batch processing started' );

			// Allow the SVG tags in batch update process.
			add_filter( 'wp_kses_allowed_html', array( $this, 'allowed_tags_and_attributes' ), 10, 2 );

			if ( defined( 'WP_CLI' ) ) {
				\WP_CLI::line( 'Processing "Gutenberg" Batch Import' );
			}

			$post_types = apply_filters( 'astra_sites_gutenberg_batch_process_post_types', array( 'page', 'post', 'wp_block', 'wp_template', 'wp_navigation', 'wp_template_part', 'wp_global_styles', 'sc_form', 'spectra-popup' ) );
			if ( defined( 'WP_CLI' ) ) {
				\WP_CLI::line( 'For post types: ' . implode( ', ', $post_types ) );
			}

			$post_ids = St_Batch_Processing::get_pages( $post_types );

			ST_Importer_Log::add(
				'Gutenberg posts retrieved for processing',
				'info',
				array(
					'post_count' => count( $post_ids ),
					'post_types' => implode( ', ', $post_types ),
				)
			);

			if ( ! is_array( $post_ids ) ) {
				ST_Importer_Log::add( 'Gutenberg batch processing failed: Post IDs are empty', 'error' );
				return array(
					'success' => false,
					'msg'     => __( 'Post ids are empty', 'astra-sites' ),
				);
			}

			foreach ( $post_ids as $post_id ) {
				$this->import_single_post( $post_id );
			}

			ST_Importer_Log::add( 'Gutenberg batch processing completed successfully', 'success', array( 'posts_processed' => count( $post_ids ) ) );

			return array(
				'success' => true,
				'msg'     => __( 'Gutenberg batch completed.', 'astra-sites' ),
			);
		}

		/**
		 * Update post meta.
		 *
		 * @param int $post_id Post ID.
		 * @return void
		 */
		public function import_single_post( $post_id = 0 ) {
			ST_Importer_Log::add(
				'Processing Gutenberg post',
				'info',
				array(
					'post_id' => $post_id,
				)
			);

			if ( defined( 'WP_CLI' ) ) {
				\WP_CLI::line( 'Gutenberg - Processing page: ' . $post_id );
			}

			// Is page imported with Starter Sites?
			// If not then skip batch process.
			$imported_from_demo_site = get_post_meta( $post_id, '_astra_sites_enable_for_batch', true );
			if ( ! $imported_from_demo_site ) {
				ST_Importer_Log::add(
					'Skipping post - not imported from demo site',
					'info',
					array(
						'post_id' => $post_id,
					)
				);
				return;
			}

			$required_plugins = (array) astra_get_site_data( 'required-plugins' );
			$plugins_slug     = array_column( $required_plugins, 'slug' );

			$is_elementor_page      = in_array( 'elementor', $plugins_slug, true ) && get_post_meta( $post_id, '_elementor_version', true );
			$is_beaver_builder_page = get_post_meta( $post_id, '_fl_builder_enabled', true );
			$is_brizy_page          = get_post_meta( $post_id, 'brizy_post_uid', true );

			// If page contain Elementor, Brizy or Beaver Builder meta then skip this page.
			if ( $is_elementor_page || $is_beaver_builder_page || $is_brizy_page ) {
				ST_Importer_Log::add(
					'Skipping post - uses other page builder',
					'info',
					array(
						'post_id'   => $post_id,
						'elementor' => (bool) $is_elementor_page,
						'beaver'    => (bool) $is_beaver_builder_page,
						'brizy'     => (bool) $is_brizy_page,
					)
				);
				return;
			}

			$ids_mapping = get_option( 'astra_sites_wpforms_ids_mapping', array() );

			// Post content.
			$content = get_post_field( 'post_content', $post_id );

			// Empty mapping? Then return.
			if ( ! empty( $ids_mapping ) ) {
				ST_Importer_Log::add(
					'Replacing WPForms IDs in post content',
					'info',
					array(
						'post_id'        => $post_id,
						'mappings_count' => count( $ids_mapping ),
					)
				);
				// Replace ID's.
				foreach ( $ids_mapping as $old_id => $new_id ) {
					$content = str_replace( '[wpforms id=\"' . $old_id, '[wpforms id=\"' . $new_id, $content );
					$content = str_replace( '{\"formId\":\"' . $old_id . '\"}', '{\"formId\":\"' . $new_id . '\"}', $content );
				}
			}

			// This replaces the category ID in UAG Post blocks.
			$site_options = ST_Importer_File_System::get_instance()->get_demo_content();

			if ( isset( $site_options['astra-site-taxonomy-mapping'] ) ) {

				$tax_mapping = $site_options['astra-site-taxonomy-mapping'];

				if ( isset( $tax_mapping['post'] ) ) {

					$catogory_mapping = ( isset( $tax_mapping['post']['category'] ) ) ? $tax_mapping['post']['category'] : array();

					if ( is_array( $catogory_mapping ) && ! empty( $catogory_mapping ) ) {
						ST_Importer_Log::add(
							'Replacing taxonomy category IDs in post content',
							'info',
							array(
								'post_id'        => $post_id,
								'mappings_count' => count( $catogory_mapping ),
							)
						);

						foreach ( $catogory_mapping as $key => $value ) {

							$this_site_term = get_term_by( 'slug', $value['slug'], 'category' );
							if ( $this_site_term ) {
								$content = str_replace( '"categories":"' . $value['id'], '"categories":"' . $this_site_term->term_id, $content );
								$content = str_replace( '\"categories\":\"' . $value['id'], '"categories":"' . $this_site_term->term_id, $content );
								$content = str_replace( '{"categories":[{"id":' . $value['id'], '{"categories":[{"id":' . $this_site_term->term_id, $content );
								$content = str_replace( 'categories/' . $value['id'], 'categories/' . $this_site_term->term_id, $content );
								$content = str_replace( 'categories=' . $value['id'], 'categories=' . $this_site_term->term_id, $content );
							}
						}
					}
				}
			}

			// Replace SureForm ID's.
			$content = $this->replace_sureforms_ids( $content );
			$content = $this->replace_surecart_forms_ids( $content );
			$content = $this->replace_suredonation_ids( $content );
			$content = $this->replace_suremembers_ids( $content );

			ST_Importer_Log::add(
				'Updating post content (first pass)',
				'info',
				array(
					'post_id' => $post_id,
				)
			);

			// Preserve \uXXXX JSON unicode escapes so they survive stripslashes() inside wp_update_post().
			$content = ST_Importer_Helper::preserve_block_unicode_escapes( $content );

			wp_update_post(
				array(
					'ID'           => $post_id,
					'post_content' => $content,
					'post_excerpt' => '',
				)
			);

			// # Tweak
			// Gutenberg break block markup from render. Because the '&' is updated in database with '&amp;' and it
			// expects as 'u0026amp;'. So, Converted '&amp;' with 'u0026amp;'.
			//
			// @todo This affect for normal page content too. Detect only Gutenberg pages and process only on it.
			// $content = str_replace( '&amp;', "\u0026amp;", $content );
			$content = $this->get_content( $content );

			ST_Importer_Log::add(
				'Updating post content (second pass with link replacements)',
				'info',
				array(
					'post_id' => $post_id,
				)
			);

			// Update content.
			// Preserve \uXXXX JSON unicode escapes so they survive stripslashes() inside wp_update_post().
			$content = ST_Importer_Helper::preserve_block_unicode_escapes( $content );

			wp_update_post(
				array(
					'ID'           => $post_id,
					'post_content' => $content,
					'post_excerpt' => '',
				)
			);

			ST_Importer_Log::add(
				'Successfully completed processing Gutenberg post',
				'success',
				array(
					'post_id' => $post_id,
				)
			);
		}

		/**
		 * Replace SureCart Forms IDs in content.
		 *
		 * @since 1.1.9
		 *
		 * @param string $content Post content.
		 * @return string
		 */
		public function replace_surecart_forms_ids( $content ) {
			$surecart_id_map = get_option( 'astra_sites_surecart_forms_id_map', array() );

			if ( empty( $surecart_id_map ) ) {
				return $content;
			}

			foreach ( $surecart_id_map as $old_id => $new_id ) {
				// Replace shortcode format.
				$content = str_replace( '[sc_form id="' . $old_id . '"]', '[sc_form id="' . $new_id . '"]', $content );

				// Replace Gutenberg checkout form block format (handles both {"id":X} and {"id":X,).
				$content = str_replace( '<!-- wp:surecart/checkout-form {"id":' . $old_id, '<!-- wp:surecart/checkout-form {"id":' . $new_id, $content );
			}

			return $content;
		}

		/**
		 * Replace SureForm IDs in content.
		 *
		 * @since 1.1.9
		 *
		 * @param string $content Post content.
		 * @return string
		 */
		public function replace_sureforms_ids( $content ) {

			$sureform_id_map = get_option( 'astra_sites_sureforms_id_map', array() );

			if ( empty( $sureform_id_map ) ) {
				return $content;
			}

			foreach ( $sureform_id_map as $old_id => $new_id ) {
				$content = str_replace( '[sureforms id="' . $old_id . '"]', '[sureforms id="' . $new_id . '"]', $content );
			}

			return $content;
		}

		/**
		 * Replace SureDonation campaign and form IDs in content.
		 *
		 * SureDonation campaign blocks store the source campaign post ID in the
		 * `campaignId` attribute and form blocks store the source form post ID in
		 * the `formId` attribute (both serialized as unquoted numbers). The
		 * [suredonation_form] shortcode references the form ID as a quoted
		 * attribute. All are remapped to the newly imported post IDs.
		 *
		 * The `campaignId` / `formId` remap is scoped to `wp:suredonation/*`
		 * block markup so identically named attributes belonging to other plugins'
		 * blocks are never touched.
		 *
		 * @since 1.1.35
		 *
		 * @param string $content Post content.
		 * @return string
		 */
		public function replace_suredonation_ids( $content ) {

			$campaign_id_map = get_option( 'astra_sites_suredonation_campaign_id_map', array() );
			$form_id_map     = get_option( 'astra_sites_suredonation_form_id_map', array() );

			$campaign_id_map = is_array( $campaign_id_map ) ? $campaign_id_map : array();
			$form_id_map     = is_array( $form_id_map ) ? $form_id_map : array();

			// Remap campaignId/formId only inside SureDonation block comments so
			// other plugins' blocks that happen to use the same attribute names
			// are left untouched.
			if ( ! empty( $campaign_id_map ) || ! empty( $form_id_map ) ) {
				$replaced = preg_replace_callback(
					'/<!-- wp:suredonation\/.*?-->/s',
					function ( $matches ) use ( $campaign_id_map, $form_id_map ) {
						$block = $matches[0];

						foreach ( $campaign_id_map as $old_id => $new_id ) {
							$block = $this->replace_block_numeric_attribute( $block, 'campaignId', (int) $old_id, (int) $new_id );
						}

						foreach ( $form_id_map as $old_id => $new_id ) {
							$block = $this->replace_block_numeric_attribute( $block, 'formId', (int) $old_id, (int) $new_id );
						}

						return $block;
					},
					$content
				);

				$content = null !== $replaced ? $replaced : $content;
			}

			// The [suredonation_form] shortcode is uniquely named, so replace it
			// across the whole content.
			foreach ( $form_id_map as $old_id => $new_id ) {
				$content = str_replace( '[suredonation_form id="' . $old_id . '"]', '[suredonation_form id="' . $new_id . '"]', $content );
			}

			return $content;
		}

		/**
		 * Replace SureMembers access group IDs in content.
		 *
		 * Access groups are `wsm_access_group` posts, re-created with new IDs on
		 * import. Content references them in two places:
		 * - the `[suremembers_restrict access_group_ids="1,2"]` shortcode, and
		 * - the `"sureMemberRestrictions":[1,2]` attribute SureMembers registers
		 *   on every block type.
		 * Both are remapped to the newly imported access group IDs.
		 *
		 * @since 1.1.37
		 *
		 * @param string $content Post content.
		 * @return string
		 */
		public function replace_suremembers_ids( $content ) {

			$access_group_id_map = get_option( 'astra_sites_suremembers_access_group_id_map', array() );

			if ( empty( $access_group_id_map ) || ! is_array( $access_group_id_map ) ) {
				return $content;
			}

			$map_id = function ( $id ) use ( $access_group_id_map ) {
				$id = (int) trim( $id );
				return isset( $access_group_id_map[ $id ] ) ? (int) $access_group_id_map[ $id ] : $id;
			};

			// The [suremembers_restrict] shortcode (quotes may be slash-escaped
			// when the shortcode sits inside serialized block attributes).
			$replaced = preg_replace_callback(
				'/(\[suremembers_restrict\s[^\]]*access_group_ids=\\\\?")([\d,\s]+)(\\\\?")/',
				function ( $matches ) use ( $map_id ) {
					$new_ids = array_map( $map_id, explode( ',', $matches[2] ) );
					return $matches[1] . implode( ',', $new_ids ) . $matches[3];
				},
				$content
			);

			$content = null !== $replaced ? $replaced : $content;

			// The per-block restriction attribute inside block comments. Quotes
			// may be slash-escaped (the WXR importer stores content that way)
			// and IDs may be serialized as numbers or numeric strings; both
			// formats are preserved.
			$replaced = preg_replace_callback(
				'/(\\\\?"sureMemberRestrictions\\\\?":\[)([^\]]*)(\])/',
				function ( $matches ) use ( $map_id ) {
					$ids_raw = str_replace( array( '\\', '"' ), '', $matches[2] );

					if ( '' === trim( $ids_raw ) || ! preg_match( '/^[\d,\s]+$/', $ids_raw ) ) {
						return $matches[0];
					}

					$new_ids = array_map( $map_id, explode( ',', $ids_raw ) );

					if ( false !== strpos( $matches[2], '"' ) ) {
						$quote   = false !== strpos( $matches[2], '\\"' ) ? '\\"' : '"';
						$new_ids = array_map(
							function ( $id ) use ( $quote ) {
								return $quote . $id . $quote;
							},
							$new_ids
						);
					}

					return $matches[1] . implode( ',', $new_ids ) . $matches[3];
				},
				$content
			);

			return null !== $replaced ? $replaced : $content;
		}

		/**
		 * Replace a numeric block attribute value within block markup.
		 *
		 * Gutenberg serializes number attributes unquoted, so the value is always
		 * terminated by a comma (another attribute follows) or a closing brace
		 * (last attribute). Matching both terminators keeps "attr":12 from being
		 * corrupted when a longer id such as "attr":123 is also present.
		 *
		 * @since 1.1.35
		 *
		 * @param string $block     Block markup to operate on.
		 * @param string $attribute Attribute name, e.g. campaignId.
		 * @param int    $old_id    Source post ID.
		 * @param int    $new_id    Imported post ID.
		 * @return string
		 */
		public function replace_block_numeric_attribute( $block, $attribute, $old_id, $new_id ) {
			$search  = array(
				'"' . $attribute . '":' . $old_id . ',',
				'"' . $attribute . '":' . $old_id . '}',
			);
			$replace = array(
				'"' . $attribute . '":' . $new_id . ',',
				'"' . $attribute . '":' . $new_id . '}',
			);

			return str_replace( $search, $replace, $block );
		}

		/**
		 * Download and Replace hotlink images
		 *
		 * @since 2.0.0
		 *
		 * @param  string $content Mixed post content.
		 * @return string           Hotlink image array.
		 */
		public function get_content( $content = '' ) {

			ST_Importer_Log::add( 'Starting link extraction and replacement process' );

			// Extract all links.
			preg_match_all( '#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#', $content, $match );

			$all_links = array_unique( $match[0] );

			ST_Importer_Log::add(
				'Links discovered in content',
				'info',
				array(
					'total_links' => count( $all_links ),
				)
			);

			// Not have any link.
			if ( empty( $all_links ) ) {
				ST_Importer_Log::add( 'No links found in content, skipping replacement' );
				return $content;
			}

			$link_mapping = array();
			$image_links  = array();
			$other_links  = array();

			// Extract normal and image links.
			foreach ( $all_links as $key => $link ) {
				if ( function_exists( 'astra_sites_is_valid_image' ) && astra_sites_is_valid_image( $link ) ) {
					$image_links[] = $link;
				} else {

					// Collect other links.
					$other_links[] = $link;
				}
			}

			// Step 1: Download images.
			if ( is_array( $image_links ) && ! empty( $image_links ) ) {
				ST_Importer_Log::add(
					'Downloading images through ST_Image_Importer',
					'info',
					array(
						'image_count' => count( $image_links ),
					)
				);

				foreach ( $image_links as $key => $image_url ) {
					// Download remote image.
					$image = array(
						'url' => $image_url,
						'id'  => 0,
					);

					if ( method_exists( ST_Image_Importer::get_instance(), 'import' ) ) {
						$downloaded_image = ST_Image_Importer::get_instance()->import( $image );

						// Old and New image mapping links.
						$link_mapping[ $image_url ] = $downloaded_image['url'];
					}
				}
			}

			// Step 2: Replace the demo site URL with live site URL.
			if ( is_array( $other_links ) ) {
				$demo_data = ST_Importer_File_System::get_instance()->get_demo_content();
				if ( isset( $demo_data['astra-site-url'] ) ) {
					$site_url = get_site_url();
					foreach ( $other_links as $key => $link ) {
						$link_mapping[ $link ] = str_replace( 'https:' . $demo_data['astra-site-url'], $site_url, $link );
					}
				}
			}

			ST_Importer_Log::add(
				'Starting link replacements in content',
				'info',
				array(
					'replacements_count' => count( $link_mapping ),
				)
			);

			// Step 3: Replace mapping links.
			foreach ( $link_mapping as $old_url => $new_url ) {
				if ( ! is_string( $old_url ) ) {
					continue;
				}
				$content = str_replace( $old_url, $new_url, $content );

				// Replace the slashed URLs if any exist.
				$old_url = str_replace( '/', '/\\', (string) $old_url );
				$new_url = str_replace( '/', '/\\', $new_url );
				$content = str_replace( $old_url, $new_url, $content );
			}

			ST_Importer_Log::add(
				'Link replacement process completed',
				'success',
				array(
					'total_replacements' => count( $link_mapping ),
				)
			);

			return $content;
		}

	}

	/**
	 * Kicking this off by calling 'get_instance()' method
	 */
	ST_Batch_Processing_Gutenberg::get_instance();

endif;
