<?php
/**
 * Helper
 *
 * @package surerank
 * @since 0.0.1
 */

namespace SureRank\Inc\Functions;

use SureRank\Inc\Admin\Helper as AdminHelper;
use SureRank\Inc\Frontend\Robots;
use SureRank\Inc\Meta_Variables\Post;
use SureRank\Inc\Meta_Variables\Site;
use SureRank\Inc\Meta_Variables\Term;
use SureRank\Inc\ThirdPartyIntegrations\Multilingual\Rewrite_Shim;
use WP_Post;
use WP_Query;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Helper
 * This class will handle all helper functions.
 *
 * @since 1.0.0
 */
class Helper {

	/**
	 * Check if classic editor is enabled.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function is_classic_editor_active() {
		// Check if classic editor is active.
		if ( ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
			return false;
		}

		return 'classic' === Get::option( 'classic-editor-replace' );
	}

	/**
	 * Get role names.
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function get_role_names() {

		global $wp_roles;

		if ( empty( $wp_roles ) ) {
			$wp_roles = new \WP_Roles(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		}

		return $wp_roles->get_names();
	}

	/**
	 * Update flush rules.
	 *
	 * @since 0.0.1
	 * @param array<string, mixed>|array<int, string> $updated_options Array of updated option keys and values.
	 * @return void
	 */
	public static function update_flush_rules( $updated_options ) {
		$flush_settings = apply_filters( 'surerank_flush_rewrite_settings', [] );
		$updated_keys   = array_values( $updated_options ); // Get keys from $updated_options.
		$flush_required = false;

		foreach ( $flush_settings as $setting ) {
			if ( in_array( $setting, $updated_keys, true ) ) {
				$flush_required = true;
				break; // Exit the loop as soon as we find a match.
			}
		}
		if ( $flush_required ) {
			Update::option( 'surerank_flush_required', 1 );
		}
	}

	/**
	 * Flush rewrite rules.
	 *
	 * Routes through the multilingual Rewrite_Shim which temporarily detaches
	 * Polylang and WPML term-query filters during the flush, preventing
	 * per-language CPT and taxonomy slugs from being collapsed to the default
	 * language when the flush is triggered from admin context.
	 *
	 * On non-multilingual sites the shim is a pure passthrough.
	 *
	 * @since 0.0.1
	 * @return void
	 */
	public static function flush() {
		Rewrite_Shim::safe_flush();
	}

	/**
	 * Check if woocommerce is active
	 *
	 * @since 0.0.1
	 * @return bool
	 */
	public static function wc_status() {
		return class_exists( 'WooCommerce' );
	}

	/**
	 * Check if the WooCommerce integration is active.
	 *
	 * True only when WooCommerce is active AND the user has not disabled the
	 * SureRank WooCommerce integration via Tools > Integrations. Use this for
	 * WooCommerce-specific SEO behavior; use wc_status() only when you need to
	 * know whether the WooCommerce plugin itself is present.
	 *
	 * @since 1.7.5
	 * @return bool
	 */
	public static function woocommerce_enabled() {
		return self::wc_status() && wp_validate_boolean( Settings::get( 'enable_woocommerce_integration' ) );
	}

	/**
	 * Check if surecart is active
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function sc_status() {
		return class_exists( 'SureCart' );
	}

	/**
	 * Get the correct page ID, handling WooCommerce shop page as front page.
	 *
	 * When WooCommerce shop page is set as the front page in Settings > Reading,
	 * get_the_ID() may not return the correct shop page ID. This function handles
	 * that case by explicitly checking for the shop page and returning its ID.
	 *
	 * @since 1.6.3
	 * @return int Page ID.
	 */
	public static function get_page_id() {
		// Handle WooCommerce shop page as front page.
		if ( function_exists( 'is_shop' ) && is_shop() && is_front_page() ) {
			$shop_page_id = get_option( 'woocommerce_shop_page_id', '' );
			if ( $shop_page_id ) {
				return absint( $shop_page_id );
			}
		}
		return (int) get_the_ID();
	}

	/**
	 * Get public custom post types.
	 *
	 * @since 0.0.1
	 * @return array<string, mixed>
	 */
	public static function get_public_cpts() {
		$args = [
			'public'  => true,
			'show_ui' => true,
		];

		$output   = 'objects';
		$operator = 'and';

		return get_post_types( $args, $output, $operator );
	}

	/**
	 * Get public taxonomies.
	 *
	 * @since 1.2.0
	 * @return array<string, mixed>
	 */
	public static function get_public_taxonomies() {
		$args = [
			'public'  => true,
			'show_ui' => true,
		];

		$output   = 'objects';
		$operator = 'and';

		$taxonomies = get_taxonomies( $args, $output, $operator );

		$unsupported = [
			'wp_theme',
			'wp_template_part_area',
			'link_category',
			'nav_menu',
			'post_format',
			'mb-views-category',
		];

		return array_diff_key( $taxonomies, array_flip( $unsupported ) );
	}

	/**
	 * Check if the current page is a WooCommerce product.
	 *
	 * @since 1.0.0
	 * @return bool
	 */
	public static function is_product() {
		return function_exists( 'is_product' ) && is_product();
	}

	/**
	 * Get website details
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function website_details() {
		return [
			'website_name'         => self::get_sitename(),
			'website_owner_name'   => self::get_admin_name(),
			'website_owner_email'  => self::get_admin_email(),
			'business_description' => AdminHelper::get_saved_business_details( 'business_description' ),
			'website_represents'   => Helper::website_represents(),
			'website_logo'         => self::get_logo(),
			'website_lead_details' => self::get_website_lead_details(),
			'website_about_us'     => self::get_website_about_us(),
			'website_contact_us'   => self::get_website_contact_us(),
		];
	}

	/**
	 * Check if the current page is a WooCommerce shop page.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function website_represents() {
		if ( class_exists( 'WooCommerce' ) || class_exists( 'Easy_Digital_Downloads' ) || class_exists( 'SureCart' ) ) {
			return 'ecommerce';
		}
		return '';
	}

	/**
	 * Get sitename
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function get_sitename() {
		// getting title and if it is not set then we can get URL.
		$title = get_bloginfo( 'title' );
		$url   = get_bloginfo( 'url' );
		return $title ? $title : $url;
	}

	/**
	 * Get sitename
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function get_admin_name() {
		return get_bloginfo( 'name' );
	}

	/**
	 * Get admin email
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function get_admin_email() {
		return get_bloginfo( 'admin_email' );
	}

	/**
	 * Get current page number
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function get_paged_info() {

		global $wp_query;
		if ( ! is_paged() ) {
			return '';
		}

		$page_num  = $wp_query->get( 'paged' );
		$max_pages = $wp_query->max_num_pages;

		if ( empty( $page_num ) || empty( $max_pages ) ) {
			return '';
		}

		return self::format_paged_info( $page_num, $max_pages );
	}

	/**
	 * Format paged info
	 *
	 * @param int $page_num The current page number.
	 * @param int $max_pages The total number of pages.
	 * @since 1.0.0
	 * @return string
	 */
	public static function format_paged_info( $page_num, $max_pages ) {
		if ( empty( $page_num ) || empty( $max_pages ) ) {
			return '';
		}

		/* translators: %s: page number, %s: total pages */
		$formatted_string = sprintf( __( ' - Page %1$s of %2$s', 'surerank' ), $page_num, $max_pages );

		/**
		 * Filter the page number format.
		 *
		 * @since 1.0.0
		 * @param string $formatted_string The formatted string.
		 * @param int $page_num The current page number.
		 * @param int $max_pages The total number of pages.
		 */
		return apply_filters( 'surerank_homepage_pagination_format', $formatted_string, $page_num, $max_pages );
	}

	/**
	 * Get logo
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function get_logo() {
		$logo     = get_theme_mod( 'custom_logo' );
		$logo_url = '';

		if ( $logo ) {
			$logo_url = wp_get_attachment_url( $logo );
		}

		$attachment = get_post( $logo );
		$metadata   = wp_get_attachment_metadata( $logo );
		$filesize   = $metadata['filesize'] ?? 0;

		if ( ! $attachment ) {
			return [
				'attachment_id' => ! empty( $logo ) ? $logo : 0,
				'name'          => '',
				'size'          => '',
				'type'          => '',
				'url'           => '',
			];
		}

		$name = $attachment->post_title;
		$type = $attachment->post_mime_type;
		$url  = $logo_url;

		return [
			'attachment_id' => $attachment->ID,
			'name'          => $name,
			'size'          => $filesize,
			'type'          => $type,
			'url'           => $url,
		];
	}

	/**
	 * Get logo
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function logo_uri() {
		return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjU1MzcgMS41QzE3Ljg0NTMgMS41IDIxLjMyNTEgNC45Nzg5NSAyMS4zMjUyIDkuMjcwNTFDMjEuMzI1MiAxMi4zNDcgMTkuNTM2OCAxNS4wMDU2IDE2Ljk0MzQgMTYuMjY0NkgyMS4zMjUyVjIyLjVIMTguMDg4OUMxNC45MDg2IDIyLjUgMTIuMjg2MSAyMC4xMTg2IDExLjkwMzMgMTcuMDQySDExLjkwMTRMMTEuOTAzMyAxMy43ODUyQzE0LjgyODMgMTMuNzY2MSAxNy4wMzQyIDExLjM4OTQgMTcuMDM0MiA4LjQ1OTk2VjYuMDI5M0MxNC4xMzcgNi4wMjk0NyAxMS42OTQ4IDcuOTc2ODIgMTAuOTQ0MyAxMC42MzM4QzEwLjE2MDUgOS41MzM0NSA4Ljg3MzgzIDguODE2NSA3LjQxOTkyIDguODE2NDFINi4zODA4NlY5Ljg1MzUySDYuMzgzNzlDNi40NDUxNSAxMi4wMzU2IDguMjMzNzUgMTMuNzg2IDEwLjQzMDcgMTMuNzg2MUgxMC43MDYxTDEwLjY5MzQgMTcuMDQySDEwLjY4NjVDMTAuMjk0MyAyMC4xMDgyIDcuNjc2NzggMjIuNDc4NSA0LjUwMzkxIDIyLjQ3ODVIMi42NzQ4VjEuNUgxMy41NTM3WiIgZmlsbD0iIzQzMzhDQSIvPgo8L3N2Zz4K';
	}

	/**
	 * Get website lead details
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function get_website_lead_details() {
		$user = wp_get_current_user();
		return [
			'first_name' => $user->first_name ?? '',
			'last_name'  => $user->last_name ?? '',
			'email'      => $user->user_email ?? '',
		];
	}

	/**
	 * Replace variables in a string.
	 *
	 * @param string               $key       The key for the value.
	 * @param mixed                $value     The value to process.
	 * @param array<string, mixed> $variables The variables for replacement.
	 * @return mixed
	 */
	public static function replacement( $key, $value, $variables ) {
		if ( ! self::is_replaceable_value( $value ) ) {
			return $value;
		}

		$chunks = self::extract_variables( $value );
		if ( empty( $chunks ) ) {
			return $value;
		}

		$replacement_array = self::build_replacement_array( $chunks, $variables );
		return self::apply_replacements( $value, $replacement_array );
	}

	/**
	 * Extract variables from a string.
	 *
	 * @param string $str The input string.
	 * @return array<int, string>
	 */
	public static function extract_variables( $str ) {
		preg_match_all( '/%([^%\s]+)%/', $str, $matches );
		return ! empty( $matches[1] ) ? $matches[1] : [];
	}

	/**
	 * Retrieves a WordPress page's ID and title based on a partial title search.
	 *
	 * @param string $search_term The term to search for in page titles.
	 * @return array<string, mixed>|false Array with 'value' (page ID) and 'label' (page title) if found, false otherwise.
	 */
	public static function get_page_id_by_title( $search_term ) {
		if ( empty( $search_term ) ) {
			return false;
		}

		$screen = get_current_screen();
		if ( $screen && $screen->base !== 'surerank_page_surerank_onboarding' ) {
			return false;
		}

		$search_term = sanitize_text_field( $search_term );
		$args        = [
			'post_type'      => 'page',
			'post_status'    => 'publish',
			'posts_per_page' => 1,
			'sort_order'     => 'ASC',
			'sort_column'    => 'post_title',
			's'              => $search_term,
		];

		$pages = new WP_Query( $args );

		if ( ! empty( $pages->posts[0] ) && $pages->posts[0] instanceof WP_Post ) {
			return [
				'value' => $pages->posts[0]->ID,
				'label' => $pages->posts[0]->post_title,
			];
		}

		return false;
	}

	/**
	 * Get website About Us page ID.
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>|false
	 */
	public static function get_website_about_us() {
		return self::get_page_id_by_title( 'about' );
	}

	/**
	 * Get website Contact Us page ID.
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>|false
	 */
	public static function get_website_contact_us() {
		return self::get_page_id_by_title( 'contact' );
	}

	/**
	 * Retrieves robots.txt data and search engine visibility settings.
	 *
	 * This method fetches the robots.txt content stored in the plugin option,
	 * retrieves the current "Search engine visibility" setting from WordPress,
	 * checks if a physical robots.txt file exists in the site root, and returns
	 * its contents if present.
	 *
	 * @since 1.2.0
	 *
	 * @return array<string, mixed> {
	 *     @type string $robots_txt_content       The robots.txt content saved in the plugin option.
	 *     @type int    $search_engine_visibility The 'blog_public' option value (1 = visible to search engines, 0 = hidden).
	 *     @type bool   $robots_file_exists       Whether a physical robots.txt file exists in the site root.
	 *     @type string $robot_file_content       The content of the physical robots.txt file, if it exists.
	 * }
	 */
	public static function get_robots_data() {
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}

		$public = get_option( 'blog_public' );

		$robots_file_exists = false;
		$robot_file_content = '';

		if ( $wp_filesystem->exists( ABSPATH . 'robots.txt' ) ) {
			$robots_file_exists = true;
			$robot_file_content = $wp_filesystem->get_contents( ABSPATH . 'robots.txt' );
		}

		return [
			'default_robots_txt'       => Robots::get_instance()->get_default_robots_txt(),
			'robots_txt_content'       => Get::option( SURERANK_ROBOTS_TXT_CONTENT, '' ),
			'search_engine_visibility' => $public,
			'robots_file_exists'       => $robots_file_exists,
			'robot_file_content'       => $robot_file_content,
		];
	}

	/**
	 * Get formatted post types
	 *
	 * @return array<string, string>
	 */
	public static function get_formatted_post_types() {
		$post_types = \get_post_types( [ 'public' => true ], 'objects' );
		if ( empty( $post_types ) || ! is_array( $post_types ) ) {
			return [];
		}

		return array_map(
			static function ( $post_type ) {
				return ! empty( $post_type->label ) ? $post_type->label : '';
			},
			$post_types
		);
	}

	/**
	 * Get formatted taxonomies
	 *
	 * @return array<string, string>
	 */
	public static function get_formatted_taxonomies() {
		$taxonomies = \get_taxonomies( [ 'public' => true ], 'objects' );
		if ( empty( $taxonomies ) || ! is_array( $taxonomies ) ) {
			return [];
		}

		return array_map(
			static function ( $taxonomy ) {
				return $taxonomy->label;
			},
			$taxonomies
		);
	}

	/**
	 * Check if crons are available and working.
	 *
	 * @since 1.4.3
	 * @param bool $force Bypass the cached HTTP self-test result. Has no effect
	 *                    when DISABLE_WP_CRON / ALTERNATE_WP_CRON is set
	 *                    (those rely on the recorded server-side cron timestamp).
	 * @return bool True if cron is available, false if not.
	 */
	public static function are_crons_available( $force = false ) {
		global $wp_version;

		// Escape hatch: filter must return an explicit bool to override.
		$override = apply_filters( 'surerank_are_crons_available', null );
		if ( is_bool( $override ) ) {
			return $override;
		}

		// If we're currently running within a cron, it's obviously working.
		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return true;
		}

		// WP-Cron off: treat server-side cron as available if a recent run was recorded.
		if ( ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ||
			( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) ) {
			$last_run = (int) get_option( 'surerank_last_cron_run', 0 );
			return $last_run > 0 && ( time() - $last_run ) < HOUR_IN_SECONDS;
		}

		if ( ! $force ) {
			// Check cached status.
			$cached_status = get_option( 'surerank_cron_test_ok' );

			if ( $cached_status ) {
				return 'yes' === $cached_status;
			}
		}

		// Prepare cron test request.
		$sslverify     = version_compare( $wp_version, '4.0', '<' );
		$doing_wp_cron = sprintf( '%.22F', microtime( true ) );

		$cron_request = apply_filters(
			'surerank_cron_request',
			[
				'url'  => site_url( 'wp-cron.php?doing_wp_cron=' . $doing_wp_cron ),
				'key'  => $doing_wp_cron,
				'args' => [
					'timeout'   => 5, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
					'blocking'  => true,
					'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ),
				],
			]
		);

		// Ensure blocking is set to true.
		$cron_request['args']['blocking'] = true;

		// Test cron spawn by sending a request to wp-cron.php.
		$result = wp_safe_remote_post( $cron_request['url'], $cron_request['args'] );

		// Handle errors - cache failures for 5 minutes to prevent performance issues.
		if ( is_wp_error( $result ) ) {
			update_option( 'surerank_cron_test_ok', 'no' );
			return false;
		}

		$response_code = wp_remote_retrieve_response_code( $result );

		// Check for non-success HTTP response codes or empty response.
		if ( empty( $response_code ) || $response_code < 200 || $response_code >= 300 ) {
			update_option( 'surerank_cron_test_ok', 'no' );
			return false;
		}

		// Success - cache the result.
		update_option( 'surerank_cron_test_ok', 'yes' );
		return true;
	}

	/**
	 * Check if WP Schema Pro is active.
	 *
	 * @since 1.5.0
	 * @return bool
	 */
	public static function is_wp_schema_pro_active() {
		return defined( 'BSF_AIOSRS_PRO_VER' );
	}

	/**
	 * Encode a value using base64.
	 *
	 * @since 1.6.0
	 * @param string $value Value to encode.
	 * @return string Encoded value.
	 */
	public static function encode( $value ) {
		return base64_encode( $value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
	}

	/**
	 * Decode a value using base64.
	 *
	 * @since 1.6.0
	 * @param string $value Value to decode.
	 * @return string|false Decoded value or false on failure.
	 */
	public static function decode( $value ) {
		return base64_decode( $value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
	}

	/**
	 * Get site title and tagline.
	 *
	 * @since 1.7.2
	 * @return array<string, string> Array containing site_title and site_tagline.
	 */
	public static function site_details() {
		return [
			'site_title'   => get_bloginfo( 'name' ),
			'site_tagline' => get_bloginfo( 'description' ),
		];
	}

	/**
	 * Build a UTM-tagged marketing link with stable defaults.
	 *
	 * Uses deterministic UTM values for SureRank outbound marketing links and
	 * tracks placement via `utm_content`.
	 *
	 * @since 1.7.4
	 * @param string                $path        Optional path appended to the SureRank website.
	 * @param string                $utm_content Placement identifier for attribution.
	 * @param array<string, string> $extra       Additional query args merged last.
	 * @return string Sanitized URL with UTM parameters.
	 */
	public static function get_marketing_link( string $path = '', string $utm_content = '', array $extra = [] ): string {
		$base = 'https://surerank.com/';

		if ( '' !== $path ) {
			$base .= ltrim( $path, '/' );
		}

		$utm_args = array_merge(
			[
				'utm_source'   => 'surerank_plugin',
				'utm_medium'   => 'wordpress_plugin',
				'utm_campaign' => 'core_plugin',
			],
			'' !== $utm_content ? [ 'utm_content' => $utm_content ] : [],
			$extra
		);

		return esc_url_raw( add_query_arg( $utm_args, $base ) );
	}

	/**
	 * Check if a value can be replaced.
	 *
	 * @param mixed $value The value to check.
	 * @return bool True if replaceable.
	 */
	private static function is_replaceable_value( $value ): bool {
		return ! empty( $value ) && is_string( $value );
	}

	/**
	 * Build replacement array for chunks.
	 *
	 * @param array<string>        $chunks    Variable chunks.
	 * @param array<string, mixed> $variables Available variables.
	 * @return array<string, string> Replacement array.
	 */
	private static function build_replacement_array( array $chunks, array $variables ): array {
		$dynamic_keys      = self::get_dynamic_keys();
		$replacement_array = [];

		foreach ( $chunks as $chunk ) {
			if ( ! isset( $replacement_array[ $chunk ] ) ) {
				$replacement_array[ $chunk ] = self::get_replacement_value( $chunk, $variables, $dynamic_keys );
			}
		}

		return $replacement_array;
	}

	/**
	 * Get all dynamic keys.
	 *
	 * @return array<string> Dynamic keys.
	 */
	private static function get_dynamic_keys(): array {
		$post_keys = Post::get_instance()->variables;
		$site_keys = Site::get_instance()->variables;
		$term_keys = Term::get_instance()->variables;

		return array_keys( array_merge( $post_keys, $site_keys, $term_keys ) );
	}

	/**
	 * Get replacement value for a chunk.
	 *
	 * @param string               $chunk        The chunk to replace.
	 * @param array<string, mixed> $variables    Available variables.
	 * @param array<string>        $dynamic_keys Dynamic keys list.
	 * @return string Replacement value.
	 */
	private static function get_replacement_value( string $chunk, array $variables, array $dynamic_keys ): string {
		if ( ! in_array( $chunk, $dynamic_keys, true ) ) {
			return '';
		}

		$sources = [ 'post', 'term', 'site' ];
		foreach ( $sources as $source ) {
			$value = self::get_variable_value( $variables, $source, $chunk );
			if ( '' !== $value ) {
				return $value;
			}
		}

		return '';
	}

	/**
	 * Get variable value from source.
	 *
	 * @param array<string, mixed> $variables Variables array.
	 * @param string               $source    Source type.
	 * @param string               $chunk     Variable chunk.
	 * @return string Variable value or empty string.
	 */
	private static function get_variable_value( array $variables, string $source, string $chunk ): string {
		return $variables[ $source ][ $chunk ]['value'] ?? '';
	}

	/**
	 * Apply replacements to value.
	 *
	 * @param string                $value             Original value.
	 * @param array<string, string> $replacement_array Replacements.
	 * @return string Replaced value.
	 */
	private static function apply_replacements( string $value, array $replacement_array ): string {
		return preg_replace_callback(
			'/%([^%\s]+)%/',
			static function ( $matches ) use ( $replacement_array ) {
				return $replacement_array[ $matches[1] ] ?? '';
			},
			$value
		) ?? $value;
	}
}
