<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'SftcyWordaiSeoScan' ) ) {
	/**
	 * Bulk SEO scanning and auto-fix workflow for WordAI.
	 */
	class SftcyWordaiSeoScan {
		const PAGE_SLUG        = 'sc-wordai-seo-scan';
		const NONCE_ACTION     = 'scwordai_wpnonce';
		const USAGE_PREFIX     = 'sftcy_wordai_seo_scan_usage_';
		const META_SCORE       = '_wordai_seo_scan_score';
		const META_GRADE       = '_wordai_seo_scan_grade';
		const META_ISSUES      = '_wordai_seo_scan_issues';
		const META_SUGGESTIONS = '_wordai_seo_scan_suggestions';
		const META_SCANNED_AT  = '_wordai_seo_scanned_at';
		const META_HASH        = '_wordai_seo_scan_content_hash';
		const JOB_OPTION       = 'sftcy_wordai_seo_scan_scheduled_job';
		const CRON_HOOK        = 'sftcy_wordai_seo_scan_process_job';
		const BATCH_SIZE       = 10;

		private static $initiated = false;

		public function __construct() {
			if ( ! self::$initiated ) {
				self::initiate_hooks();
			}
		}

		private static function initiate_hooks() {
			add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_list', array( __CLASS__, 'ajax_list_items' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_item', array( __CLASS__, 'ajax_scan_item' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_autofix', array( __CLASS__, 'ajax_autofix_item' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_undo', array( __CLASS__, 'ajax_undo_item' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_schedule_job', array( __CLASS__, 'ajax_schedule_job' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_cancel_job', array( __CLASS__, 'ajax_cancel_job' ) );
			add_action( 'wp_ajax_sc_wordai_seo_scan_job_status', array( __CLASS__, 'ajax_job_status' ) );
			add_action( self::CRON_HOOK, array( __CLASS__, 'process_scheduled_job' ), 10, 1 );

			self::$initiated = true;
		}

		public static function get_textdomain() {
			return class_exists( 'SftcyWordaiFeatures' ) ? SftcyWordaiFeatures::wordai_get_textdomain() : 'wordai-pro';
		}

		public static function get_screen_ids() {
			return array(
				'wordai-pro_page_' . self::PAGE_SLUG,
				'wordai_page_' . self::PAGE_SLUG,
			);
		}

		public static function is_current_page() {
			$screen = get_current_screen();
			return $screen && in_array( $screen->id, self::get_screen_ids(), true );
		}

		public static function enqueue_assets() {
			if ( ! self::is_current_page() ) {
				return;
			}

			wp_enqueue_script( 'jquery-ui-core' );
			wp_enqueue_script( 'jquery-ui-dialog' );
			wp_enqueue_style(
				'sftcy-wordai-google-fonts',
				'https://fonts.googleapis.com/css2?family=Poppins:wght@700;800&display=swap',
				array(),
				null
			);
			wp_enqueue_style(
				'sftcy-wordai-style',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/css/sc-wordai-misc-styles.css',
				array( 'sftcy-wordai-google-fonts' ),
				self::asset_version( 'admin/css/sc-wordai-misc-styles.css' ),
				'all'
			);
			wp_enqueue_style(
				'sftcy-wordai-jquery-ui-style',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/css/jquery-ui.css',
				array(),
				SFTCY_WORDAI_VERSION,
				'all'
			);
			wp_enqueue_style(
				'sftcy-wordai-fontawesome',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/css/fontawesome6-all.min.css',
				array(),
				SFTCY_WORDAI_VERSION,
				'all'
			);
			wp_enqueue_style(
				'sftcy-wordai-seo-scan',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/css/sc-wordai-seo-scan.css',
				array( 'sftcy-wordai-style' ),
				self::asset_version( 'admin/css/sc-wordai-seo-scan.css' ),
				'all'
			);
			wp_enqueue_script(
				'sftcy-wordai-provider-dialog',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/js/sc-wordai-provider-dialog.js',
				array( 'jquery', 'jquery-ui-dialog', 'jquery-ui-core' ),
				self::asset_version( 'admin/js/sc-wordai-provider-dialog.js' ),
				true
			);
			wp_enqueue_script(
				'sftcy-wordai-google-gemini',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/js/sc-wordai-google-gemini.js',
				array( 'jquery', 'jquery-ui-dialog', 'jquery-ui-core', 'sftcy-wordai-provider-dialog' ),
				SFTCY_WORDAI_VERSION,
				true
			);
			wp_enqueue_script(
				'sc-wordai-seo-scan',
				SFTCY_WORDAI_PLUGIN_URL . 'admin/js/sc-wordai-seo-scan.js',
				array( 'jquery', 'jquery-ui-dialog', 'jquery-ui-core' ),
				self::asset_version( 'admin/js/sc-wordai-seo-scan.js' ),
				true
			);

			$data = self::get_localized_data();
			wp_localize_script( 'sftcy-wordai-google-gemini', 'sftcy_wordai_metabox_script_obj', $data );
			wp_localize_script( 'sftcy-wordai-google-gemini', 'sftcy_wordai_google_gemini', $data );
			wp_localize_script( 'sc-wordai-seo-scan', 'scwordai_seo_scan', $data );
		}

		private static function asset_version( $relative_path ) {
			$path = SFTCY_WORDAI_PLUGIN_DIR . ltrim( $relative_path, '/\\' );
			return file_exists( $path ) ? (string) filemtime( $path ) : SFTCY_WORDAI_VERSION;
		}

		public static function get_localized_data() {
			$provider = SftcyWordaiBase::sc_wordai_get_current_ai_provider();

			return array(
				'adminajax_url'            => admin_url( 'admin-ajax.php' ),
				'ajax_url'                 => admin_url( 'admin-ajax.php' ),
				'nonce'                    => wp_create_nonce( self::NONCE_ACTION ),
				'active_provider'          => $provider,
				'provider_labels'          => array_map(
					static function( $meta ) {
						return isset( $meta['label'] ) ? $meta['label'] : '';
					},
					SftcyWordaiBase::sc_wordai_get_ai_providers()
				),
				'wordai_provider_settings' => array(
					'provider' => $provider,
				),
				'post_types'               => self::get_supported_post_types(),
				'usage'                    => self::get_usage_summary(),
				'is_basic_tier'            => class_exists( 'SftcyWordaiFeatures' ) && SftcyWordaiFeatures::wordai_is_basic_tier(),
				'upgrade_url'              => class_exists( 'SftcyWordaiFeatures' ) ? SftcyWordaiFeatures::wordai_get_upgrade_url() : 'https://softcoy.com/products/wordai-pro/checkout',
				'seo_integration'          => SftcyWordaiSeoIntegrationManager::status(),
				'scheduled_job'            => self::get_public_job_summary(),
				'initial_filters'          => self::get_initial_filters(),
				'strings'                  => array(
					'loading'        => __( 'Loading SEO items...', self::get_textdomain() ),
					'empty'          => __( 'No content found for this tab.', self::get_textdomain() ),
					'scan_failed'    => __( 'SEO scan failed. Please try again.', self::get_textdomain() ),
					'autofix_failed' => __( 'AI auto-fix failed. Please try again.', self::get_textdomain() ),
					'confirm_fix'    => __( 'Run AI auto-fix for this item? WordAI will update SEO title, meta description, focus keyword, canonical, robots, and schema fields.', self::get_textdomain() ),
				),
			);
		}

		private static function get_initial_filters() {
			$post_types = self::get_supported_post_types();
			$post_type  = isset( $_GET['wordai_post_type'] ) ? sanitize_key( wp_unslash( $_GET['wordai_post_type'] ) ) : '';
			$status     = isset( $_GET['wordai_status'] ) ? sanitize_key( wp_unslash( $_GET['wordai_status'] ) ) : '';
			$search     = isset( $_GET['wordai_search'] ) ? sanitize_text_field( wp_unslash( $_GET['wordai_search'] ) ) : '';
			$focus_post = isset( $_GET['wordai_focus_post'] ) ? absint( $_GET['wordai_focus_post'] ) : 0;

			if ( empty( $post_types[ $post_type ] ) ) {
				$post_type = ! empty( $post_types ) ? array_key_first( $post_types ) : 'post';
			}

			if ( ! in_array( $status, array( 'publish', 'draft', 'pending', 'private', 'future' ), true ) ) {
				$status = 'publish';
			}

			return array(
				'post_type'  => $post_type,
				'post_status'=> $status,
				'search'     => $search,
				'focus_post' => $focus_post,
			);
		}

		public static function get_supported_post_types() {
			$post_types = array(
				'post' => array(
					'label' => __( 'Posts', self::get_textdomain() ),
					'name'  => 'post',
				),
				'page' => array(
					'label' => __( 'Pages', self::get_textdomain() ),
					'name'  => 'page',
				),
			);

			if ( post_type_exists( 'product' ) ) {
				$post_types['product'] = array(
					'label' => __( 'Products', self::get_textdomain() ),
					'name'  => 'product',
				);
			}

			return $post_types;
		}

		public static function render_page() {
			if ( ! current_user_can( 'manage_options' ) ) {
				return;
			}

			$post_types = self::get_supported_post_types();
			$usage      = self::get_usage_summary();

			include SFTCY_WORDAI_PLUGIN_DIR . 'admin/views/submenus-seo-scan.php';
		}

		public static function ajax_list_items() {
			self::verify_ajax_request();

			$post_type  = self::get_requested_post_type();
			$page       = isset( $_POST['page_num'] ) ? max( 1, absint( $_POST['page_num'] ) ) : 1;
			$per_page   = isset( $_POST['per_page'] ) ? min( 50, max( 10, absint( $_POST['per_page'] ) ) ) : 20;
			$search     = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
			$seo_filter = isset( $_POST['seo_filter'] ) ? sanitize_key( wp_unslash( $_POST['seo_filter'] ) ) : 'all';
			$status     = isset( $_POST['post_status'] ) ? sanitize_key( wp_unslash( $_POST['post_status'] ) ) : 'publish';

			$allowed_statuses = array( 'publish', 'draft', 'pending', 'private', 'future' );
			if ( ! in_array( $status, $allowed_statuses, true ) ) {
				$status = 'publish';
			}

			$query = new WP_Query(
				array(
					'post_type'              => $post_type,
					'post_status'            => $status,
					's'                      => $search,
					'posts_per_page'         => $per_page,
					'paged'                  => $page,
					'orderby'                => 'modified',
					'order'                  => 'DESC',
					'no_found_rows'          => false,
					'update_post_meta_cache' => true,
					'update_post_term_cache' => false,
				)
			);

			$items = array();
			foreach ( $query->posts as $post ) {
				$row = self::build_row_data( $post );
				if ( self::row_matches_filter( $row, $seo_filter ) ) {
					$items[] = $row;
				}
			}

			wp_send_json_success(
				array(
					'items'       => $items,
					'total'       => (int) $query->found_posts,
					'total_pages' => (int) $query->max_num_pages,
					'page'        => $page,
					'per_page'    => $per_page,
					'usage'       => self::get_usage_summary(),
				)
			);
		}

		public static function ajax_scan_item() {
			self::verify_ajax_request();

			$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
			self::validate_post_id( $post_id );

			$limit = self::maybe_consume_usage_slot( $post_id );
			if ( is_wp_error( $limit ) ) {
				wp_send_json_error( self::limit_error_payload( $limit ) );
			}

			$analysis = self::analyze_post( $post_id, true );

			wp_send_json_success(
				array(
					'row'      => self::build_row_data( get_post( $post_id ) ),
					'analysis' => $analysis,
					'usage'    => self::get_usage_summary(),
				)
			);
		}

		public static function ajax_autofix_item() {
			self::verify_ajax_request();

			$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
			self::validate_post_id( $post_id );
			$approval_token = isset( $_POST['approval_token'] ) ? sanitize_text_field( wp_unslash( $_POST['approval_token'] ) ) : '';

			if ( '' === $approval_token ) {
				$post = get_post( $post_id );
				$values = self::generate_ai_autofix_values( $post );
				$before = SftcyWordaiSeoIntegrationManager::read( $post_id );
				if ( is_wp_error( $before ) ) {
					wp_send_json_error( array( 'message' => $before->get_error_message() ) );
				}

				$after = SftcyWordaiSeoIntegrationManager::normalize_document(
					array_merge(
						$before,
						array(
							'title'         => $values['seo_title'],
							'description'   => $values['meta_description'],
							'focus_keyword' => $values['focus_keyword'],
							'canonical'     => get_permalink( $post_id ),
							'robots'        => 'index,follow',
							'schema_type'   => self::get_default_schema_type( $post->post_type ),
						)
					)
				);
				$approval_token = wp_generate_uuid4();
				set_transient(
					'sftcy_wordai_seo_proposal_' . get_current_user_id() . '_' . md5( $approval_token ),
					array( 'post_id' => $post_id, 'values' => $values ),
					15 * MINUTE_IN_SECONDS
				);

				wp_send_json_success(
					array(
						'preview'        => true,
						'approval_token' => $approval_token,
						'provider'       => $before['provider_name'],
						'supported_fields'=> SftcyWordaiSeoIntegrationManager::supported_fields(),
						'before'         => $before,
						'after'          => $after,
					)
				);
			}

			$proposal_key = 'sftcy_wordai_seo_proposal_' . get_current_user_id() . '_' . md5( $approval_token );
			$proposal = get_transient( $proposal_key );
			if ( ! is_array( $proposal ) || (int) ( $proposal['post_id'] ?? 0 ) !== $post_id || ! is_array( $proposal['values'] ?? null ) ) {
				wp_send_json_error( array( 'message' => __( 'This SEO proposal expired. Generate a new preview and try again.', self::get_textdomain() ) ) );
			}

			$limit = self::maybe_consume_usage_slot( $post_id );
			if ( is_wp_error( $limit ) ) {
				wp_send_json_error( self::limit_error_payload( $limit ) );
			}

			$write_result = self::apply_autofix_to_post( $post_id, $proposal['values'] );
			if ( is_wp_error( $write_result ) ) {
				wp_send_json_error( array( 'message' => $write_result->get_error_message() ) );
			}
			delete_transient( $proposal_key );

			$analysis = self::analyze_post( $post_id, true );

			wp_send_json_success(
				array(
					'row'      => self::build_row_data( get_post( $post_id ) ),
					'analysis' => $analysis,
					'usage'    => self::get_usage_summary(),
					'message'  => __( 'SEO fields were updated and rescanned.', self::get_textdomain() ),
				)
			);
		}

		public static function ajax_undo_item() {
			self::verify_ajax_request();
			$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
			self::validate_post_id( $post_id );
			$change_id = SftcyWordaiSeoIntegrationManager::latest_change_id( $post_id );
			$result = SftcyWordaiSeoIntegrationManager::undo( $change_id );
			if ( is_wp_error( $result ) ) {
				wp_send_json_error( array( 'message' => $result->get_error_message() ) );
			}

			$analysis = self::analyze_post( $post_id, true );
			wp_send_json_success(
				array(
					'row'      => self::build_row_data( get_post( $post_id ) ),
					'analysis' => $analysis,
					'usage'    => self::get_usage_summary(),
					'message'  => __( 'The previous SEO values were restored and rescanned.', self::get_textdomain() ),
				)
			);
		}

		public static function ajax_schedule_job() {
			self::verify_ajax_request();

			if ( class_exists( 'SftcyWordaiFeatures' ) && SftcyWordaiFeatures::wordai_is_basic_tier() ) {
				wp_send_json_error(
					array(
						'message' => __( 'Scheduled bulk SEO scanning is available in WordAI Pro.', self::get_textdomain() ),
						'job'     => self::get_public_job_summary(),
					),
					403
				);
			}

			$existing = self::get_job();
			if ( $existing && in_array( $existing['status'], array( 'queued', 'running' ), true ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'A scheduled SEO job is already queued or running. Cancel it before creating another one.', self::get_textdomain() ),
						'job'     => self::get_public_job_summary( $existing ),
					),
					409
				);
			}

			$task = isset( $_POST['task'] ) ? sanitize_key( wp_unslash( $_POST['task'] ) ) : 'scan';
			if ( ! in_array( $task, array( 'scan', 'scan_autofix' ), true ) ) {
				$task = 'scan';
			}

			$scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : 'unscanned_recheck';
			if ( ! in_array( $scope, array( 'unscanned_recheck', 'needs_work', 'all' ), true ) ) {
				$scope = 'unscanned_recheck';
			}

			$allowed_types = array_keys( self::get_supported_post_types() );
			$post_types    = isset( $_POST['post_types'] ) ? (array) wp_unslash( $_POST['post_types'] ) : array();
			$post_types    = array_values( array_intersect( array_map( 'sanitize_key', $post_types ), $allowed_types ) );
			if ( empty( $post_types ) ) {
				$post_types = $allowed_types;
			}

			$timestamp = self::parse_schedule_timestamp( isset( $_POST['scheduled_at'] ) ? wp_unslash( $_POST['scheduled_at'] ) : '' );
			if ( is_wp_error( $timestamp ) ) {
				wp_send_json_error(
					array(
						'message' => $timestamp->get_error_message(),
						'job'     => self::get_public_job_summary(),
					),
					400
				);
			}

			$job = array(
				'id'           => 'seo_' . wp_generate_uuid4(),
				'status'       => 'queued',
				'task'         => $task,
				'scope'        => $scope,
				'post_types'   => $post_types,
				'post_status'  => 'publish',
				'user_id'      => get_current_user_id(),
				'scheduled_at' => (int) $timestamp,
				'created_at'   => time(),
				'started_at'   => 0,
				'finished_at'  => 0,
				'processed'    => 0,
				'total'        => 0,
				'fixed'        => 0,
				'failed'       => 0,
				'failures'     => array(),
				'post_ids'     => array(),
				'message'      => '',
			);

			update_option( self::JOB_OPTION, $job, false );
			wp_clear_scheduled_hook( self::CRON_HOOK, array( $job['id'] ) );
			wp_schedule_single_event( (int) $timestamp, self::CRON_HOOK, array( $job['id'] ) );

			wp_send_json_success(
				array(
					'message' => __( 'Bulk SEO job scheduled.', self::get_textdomain() ),
					'job'     => self::get_public_job_summary( $job ),
				)
			);
		}

		public static function ajax_cancel_job() {
			self::verify_ajax_request();

			$job = self::get_job();
			if ( ! $job || ! in_array( $job['status'], array( 'queued', 'running' ), true ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'No active scheduled SEO job was found.', self::get_textdomain() ),
						'job'     => self::get_public_job_summary( $job ),
					),
					404
				);
			}

			wp_clear_scheduled_hook( self::CRON_HOOK, array( $job['id'] ) );
			$job['status']      = 'cancelled';
			$job['finished_at'] = time();
			$job['message']     = __( 'Scheduled job was cancelled.', self::get_textdomain() );
			update_option( self::JOB_OPTION, $job, false );

			wp_send_json_success(
				array(
					'message' => $job['message'],
					'job'     => self::get_public_job_summary( $job ),
				)
			);
		}

		public static function ajax_job_status() {
			self::verify_ajax_request();

			wp_send_json_success(
				array(
					'job'   => self::get_public_job_summary(),
					'usage' => self::get_usage_summary(),
				)
			);
		}

		public static function process_scheduled_job( $job_id = '' ) {
			$job = self::get_job();
			if ( ! $job || empty( $job['id'] ) || $job['id'] !== $job_id || ! in_array( $job['status'], array( 'queued', 'running' ), true ) ) {
				return;
			}

			$lock_key = 'sftcy_wordai_seo_scan_job_lock_' . md5( $job['id'] );
			if ( get_transient( $lock_key ) ) {
				wp_schedule_single_event( time() + MINUTE_IN_SECONDS, self::CRON_HOOK, array( $job['id'] ) );
				return;
			}
			set_transient( $lock_key, 1, 5 * MINUTE_IN_SECONDS );
			$previous_user_id = get_current_user_id();
			$job_user_id = absint( $job['user_id'] ?? 0 );
			if ( $job_user_id > 0 ) {
				wp_set_current_user( $job_user_id );
			}

			if ( empty( $job['post_ids'] ) ) {
				$job['post_ids'] = self::get_job_post_ids( $job );
				$job['total']    = count( $job['post_ids'] );
			}

			$job['status']     = 'running';
			$job['started_at'] = ! empty( $job['started_at'] ) ? (int) $job['started_at'] : time();
			$remaining         = array_slice( $job['post_ids'], (int) $job['processed'], self::BATCH_SIZE );

			if ( empty( $remaining ) ) {
				$job['status']      = 'finished';
				$job['finished_at'] = time();
				$job['message']     = __( 'Scheduled SEO job finished.', self::get_textdomain() );
				update_option( self::JOB_OPTION, $job, false );
				delete_transient( $lock_key );
				wp_set_current_user( $previous_user_id );
				return;
			}

			foreach ( $remaining as $post_id ) {
				$post_id = absint( $post_id );
				try {
					$limit = self::maybe_consume_usage_slot( $post_id );
					if ( is_wp_error( $limit ) ) {
						throw new Exception( $limit->get_error_message() );
					}

					if ( 'scan_autofix' === $job['task'] ) {
						$post = get_post( $post_id );
						if ( ! $post ) {
							throw new Exception( __( 'Content item no longer exists.', self::get_textdomain() ) );
						}
						$write_result = self::apply_autofix_to_post( $post_id, self::generate_ai_autofix_values( $post ) );
						if ( is_wp_error( $write_result ) ) {
							throw new Exception( $write_result->get_error_message() );
						}
						$job['fixed']++;
					}

					self::analyze_post( $post_id, true );
				} catch ( Exception $exception ) {
					$job['failed']++;
					if ( count( $job['failures'] ) < 10 ) {
						$job['failures'][] = array(
							'post_id' => $post_id,
							'message' => $exception->getMessage(),
						);
					}
				}

				$job['processed']++;
			}

			if ( (int) $job['processed'] >= (int) $job['total'] ) {
				$job['status']      = 'finished';
				$job['finished_at'] = time();
				$job['message']     = __( 'Scheduled SEO job finished.', self::get_textdomain() );
			} else {
				$job['status']  = 'running';
				$job['message'] = __( 'Scheduled SEO job is processing in the background.', self::get_textdomain() );
				wp_schedule_single_event( time() + MINUTE_IN_SECONDS, self::CRON_HOOK, array( $job['id'] ) );
			}

			update_option( self::JOB_OPTION, $job, false );
			delete_transient( $lock_key );
			wp_set_current_user( $previous_user_id );
		}

		private static function apply_autofix_to_post( $post_id, $values ) {
			$post = get_post( $post_id );
			if ( ! $post ) {
				return new WP_Error( 'wordai_seo_post_missing', __( 'The selected content item no longer exists.', self::get_textdomain() ) );
			}

			return SftcyWordaiSeoIntegrationManager::write(
				$post_id,
				array(
					'title'         => $values['seo_title'],
					'description'   => $values['meta_description'],
					'focus_keyword' => $values['focus_keyword'],
					'canonical'     => get_permalink( $post_id ),
					'robots'        => 'index,follow',
					'schema_type'   => self::get_default_schema_type( $post->post_type ),
				)
			);
		}

		private static function get_job() {
			$job = get_option( self::JOB_OPTION, array() );
			return is_array( $job ) && ! empty( $job['id'] ) ? $job : null;
		}

		private static function parse_schedule_timestamp( $value ) {
			$value = sanitize_text_field( (string) $value );
			if ( '' === $value ) {
				return new WP_Error( 'wordai_seo_schedule_missing_time', __( 'Choose a scheduled date and time.', self::get_textdomain() ) );
			}

			$date = DateTime::createFromFormat( 'Y-m-d\TH:i', $value, wp_timezone() );
			if ( ! $date ) {
				return new WP_Error( 'wordai_seo_schedule_invalid_time', __( 'Schedule date and time is invalid.', self::get_textdomain() ) );
			}

			return max( time() + MINUTE_IN_SECONDS, $date->getTimestamp() );
		}

		private static function get_job_post_ids( $job ) {
			$query = new WP_Query(
				array(
					'post_type'              => ! empty( $job['post_types'] ) ? $job['post_types'] : array_keys( self::get_supported_post_types() ),
					'post_status'            => ! empty( $job['post_status'] ) ? $job['post_status'] : 'publish',
					'posts_per_page'         => -1,
					'fields'                 => 'ids',
					'orderby'                => 'modified',
					'order'                  => 'DESC',
					'no_found_rows'          => true,
					'update_post_meta_cache' => true,
					'update_post_term_cache' => false,
				)
			);

			$scope = isset( $job['scope'] ) ? $job['scope'] : 'unscanned_recheck';
			$ids   = array();
			foreach ( $query->posts as $post_id ) {
				$row = self::build_row_data( get_post( $post_id ) );
				if ( 'all' === $scope ) {
					$ids[] = (int) $post_id;
				} elseif ( 'needs_work' === $scope && ! empty( $row['score'] ) && (int) $row['score'] < 75 ) {
					$ids[] = (int) $post_id;
				} elseif ( 'unscanned_recheck' === $scope && in_array( $row['status'], array( 'unscanned', 'recheck' ), true ) ) {
					$ids[] = (int) $post_id;
				}
			}

			return $ids;
		}

		private static function get_public_job_summary( $job = null ) {
			$job = $job ?: self::get_job();
			if ( ! $job ) {
				return null;
			}

			$supported = self::get_supported_post_types();
			$labels    = array();
			foreach ( (array) ( $job['post_types'] ?? array() ) as $type ) {
				if ( isset( $supported[ $type ]['label'] ) ) {
					$labels[] = $supported[ $type ]['label'];
				}
			}

			$total     = max( 0, (int) ( $job['total'] ?? 0 ) );
			$processed = max( 0, (int) ( $job['processed'] ?? 0 ) );

			return array(
				'id'                 => (string) ( $job['id'] ?? '' ),
				'status'             => (string) ( $job['status'] ?? 'queued' ),
				'status_label'       => self::get_job_status_label( $job['status'] ?? 'queued' ),
				'task'               => (string) ( $job['task'] ?? 'scan' ),
				'task_label'         => self::get_job_task_label( $job['task'] ?? 'scan' ),
				'scope'              => (string) ( $job['scope'] ?? 'unscanned_recheck' ),
				'scope_label'        => self::get_job_scope_label( $job['scope'] ?? 'unscanned_recheck' ),
				'post_types'         => array_values( (array) ( $job['post_types'] ?? array() ) ),
				'post_type_labels'   => $labels,
				'scheduled_at'       => (int) ( $job['scheduled_at'] ?? 0 ),
				'scheduled_at_human' => ! empty( $job['scheduled_at'] ) ? wp_date( 'M j, Y g:i A', (int) $job['scheduled_at'] ) : '',
				'started_at_human'   => ! empty( $job['started_at'] ) ? wp_date( 'M j, Y g:i A', (int) $job['started_at'] ) : '',
				'finished_at_human'  => ! empty( $job['finished_at'] ) ? wp_date( 'M j, Y g:i A', (int) $job['finished_at'] ) : '',
				'processed'          => $processed,
				'total'              => $total,
				'fixed'              => max( 0, (int) ( $job['fixed'] ?? 0 ) ),
				'failed'             => max( 0, (int) ( $job['failed'] ?? 0 ) ),
				'message'            => (string) ( $job['message'] ?? '' ),
				'can_cancel'         => in_array( $job['status'] ?? 'queued', array( 'queued', 'running' ), true ),
				'progress'           => $total > 0 ? min( 100, round( ( $processed / $total ) * 100 ) ) : 0,
			);
		}

		private static function get_job_status_label( $status ) {
			$labels = array(
				'queued'    => __( 'Queued', self::get_textdomain() ),
				'running'   => __( 'Running', self::get_textdomain() ),
				'finished'  => __( 'Finished', self::get_textdomain() ),
				'cancelled' => __( 'Cancelled', self::get_textdomain() ),
				'failed'    => __( 'Failed', self::get_textdomain() ),
			);

			return $labels[ $status ] ?? $labels['queued'];
		}

		private static function get_job_task_label( $task ) {
			return 'scan_autofix' === $task ? __( 'Scan and AI Auto Fix', self::get_textdomain() ) : __( 'Scan', self::get_textdomain() );
		}

		private static function get_job_scope_label( $scope ) {
			$labels = array(
				'unscanned_recheck' => __( 'Scan required and re-scan required items', self::get_textdomain() ),
				'needs_work'        => __( 'Items with SEO score below 75', self::get_textdomain() ),
				'all'               => __( 'All published items in selected types', self::get_textdomain() ),
			);

			return $labels[ $scope ] ?? $labels['unscanned_recheck'];
		}

		private static function verify_ajax_request() {
			check_ajax_referer( self::NONCE_ACTION, 'security' );

			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'You do not have permission to use SEO Scan.', self::get_textdomain() ),
					),
					403
				);
			}
		}

		private static function get_requested_post_type() {
			$post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : 'post';
			$allowed   = array_keys( self::get_supported_post_types() );

			if ( ! in_array( $post_type, $allowed, true ) ) {
				$post_type = 'post';
			}

			return $post_type;
		}

		private static function validate_post_id( $post_id ) {
			$post = $post_id ? get_post( $post_id ) : null;
			if ( ! $post || ! in_array( $post->post_type, array_keys( self::get_supported_post_types() ), true ) ) {
				wp_send_json_error(
					array(
						'message' => __( 'Invalid content item selected.', self::get_textdomain() ),
					),
					400
				);
			}
		}

		private static function row_matches_filter( $row, $filter ) {
			if ( 'all' === $filter ) {
				return true;
			}
			if ( 'unscanned' === $filter ) {
				return 'unscanned' === $row['status'];
			}
			if ( 'recheck' === $filter ) {
				return 'recheck' === $row['status'];
			}
			if ( 'needs-work' === $filter ) {
				return ! empty( $row['score'] ) && (int) $row['score'] < 75;
			}

			return true;
		}

		private static function build_row_data( $post ) {
			$post_id     = (int) $post->ID;
			$score       = get_post_meta( $post_id, self::META_SCORE, true );
			$grade       = get_post_meta( $post_id, self::META_GRADE, true );
			$issues      = self::get_array_meta( $post_id, self::META_ISSUES );
			$suggestions = self::get_array_meta( $post_id, self::META_SUGGESTIONS );
			$scanned_at  = (string) get_post_meta( $post_id, self::META_SCANNED_AT, true );
			$saved_hash  = (string) get_post_meta( $post_id, self::META_HASH, true );
			$current_hash = self::get_content_hash( $post_id );
			$status      = 'unscanned';
			$status_label = __( 'Scan required', self::get_textdomain() );

			if ( '' !== $scanned_at && '' !== $saved_hash ) {
				if ( hash_equals( $saved_hash, $current_hash ) ) {
					$status       = 'scanned';
					$status_label = __( 'Scanned', self::get_textdomain() );
				} else {
					$status       = 'recheck';
					$status_label = __( 'Re-scan required', self::get_textdomain() );
				}
			}

			return array(
				'id'            => $post_id,
				'title'         => self::normalize_title( get_the_title( $post_id ) ?: __( '(no title)', self::get_textdomain() ) ),
				'type'          => $post->post_type,
				'status'        => $status,
				'status_label'  => $status_label,
				'post_status'   => $post->post_status,
				'score'         => '' === $score ? null : (int) $score,
				'grade'         => $grade ? sanitize_key( $grade ) : '',
				'issues'        => $issues,
				'suggestions'   => $suggestions,
				'issue_count'   => count( $issues ),
				'focus_keyword' => self::get_seo_document( $post_id )['focus_keyword'],
				'modified'      => get_the_modified_date( 'M j, Y g:i A', $post_id ),
				'scanned_at'    => $scanned_at,
				'edit_url'      => get_edit_post_link( $post_id, 'raw' ),
				'view_url'      => get_permalink( $post_id ),
				'change_id'     => SftcyWordaiSeoIntegrationManager::latest_change_id( $post_id ),
			);
		}

		private static function analyze_post( $post_id, $save = false ) {
			$post = get_post( $post_id );
			if ( ! $post ) {
				return array();
			}

			$seo_document = self::get_seo_document( $post_id );
			$title       = $seo_document['title'];
			$description = $seo_document['description'];
			$keyword     = $seo_document['focus_keyword'];
			$canonical   = $seo_document['canonical'];
			$robots      = $seo_document['robots'];
			$schema      = $seo_document['schema_type'];
			$content     = self::plain_text_with_spacing( strip_shortcodes( $post->post_content ) );
			$content_lc  = strtolower( $content );
			$keyword_lc  = strtolower( $keyword );
			$word_count  = str_word_count( $content );
			$score       = 0;
			$issues      = array();
			$suggestions = array();

			self::score_check( $score, $issues, $suggestions, '' !== trim( $title ), 6, __( 'SEO title is missing.', self::get_textdomain() ), __( 'Add a clear SEO title that includes the main topic.', self::get_textdomain() ) );
			$rules = class_exists( 'SftcyWordaiSeoStandards' ) ? SftcyWordaiSeoStandards::get_rules( $post->post_type ) : array(
				'meta_title_min'       => 35,
				'meta_title_max'       => 60,
				'meta_description_min' => 120,
				'meta_description_max' => 160,
				'minimum_words'        => 'product' === $post->post_type ? 120 : 500,
			);

			self::score_check( $score, $issues, $suggestions, self::string_length( $title ) >= $rules['meta_title_min'] && self::string_length( $title ) <= $rules['meta_title_max'], 7, __( 'SEO title length is outside the 35-60 character target.', self::get_textdomain() ), __( 'Keep the SEO title concise, searchable, and under 60 characters.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' !== $keyword && false !== strpos( strtolower( $title ), $keyword_lc ), 7, __( 'Focus keyword is not used in the SEO title.', self::get_textdomain() ), __( 'Use the focus keyword naturally inside the SEO title.', self::get_textdomain() ) );

			self::score_check( $score, $issues, $suggestions, '' !== trim( $description ), 6, __( 'Meta description is missing.', self::get_textdomain() ), __( 'Write a persuasive meta description for search results.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, self::string_length( $description ) >= $rules['meta_description_min'] && self::string_length( $description ) <= $rules['meta_description_max'], 7, __( 'Meta description should be about 120-160 characters.', self::get_textdomain() ), __( 'Tune the meta description length so it fits search snippets.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' !== $keyword && false !== strpos( strtolower( $description ), $keyword_lc ), 7, __( 'Focus keyword is not used in the meta description.', self::get_textdomain() ), __( 'Mention the focus keyword once in the meta description.', self::get_textdomain() ) );

			self::score_check( $score, $issues, $suggestions, '' !== trim( $keyword ), 8, __( 'Focus keyword is missing.', self::get_textdomain() ), __( 'Choose one primary focus keyword for this content.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' !== $keyword && false !== strpos( $content_lc, $keyword_lc ), 7, __( 'Focus keyword is not found in the content.', self::get_textdomain() ), __( 'Use the focus keyword naturally in the introduction or body.', self::get_textdomain() ) );

			$minimum_words = (int) $rules['minimum_words'];
			self::score_check( $score, $issues, $suggestions, $word_count >= $minimum_words, 10, __( 'Content is shorter than the recommended SEO depth.', self::get_textdomain() ), __( 'Expand the content with helpful details, examples, and clear sections.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, (bool) preg_match( '/<h[2-3][^>]*>/i', $post->post_content ), 8, __( 'Content does not use H2 or H3 headings.', self::get_textdomain() ), __( 'Add scannable H2/H3 headings to organize the content.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, (bool) preg_match( '/href=["\']' . preg_quote( home_url(), '/' ) . '/i', $post->post_content ), 5, __( 'No internal link was detected.', self::get_textdomain() ), __( 'Add at least one useful internal link.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, (bool) preg_match( '/href=["\']https?:\/\/(?!' . preg_quote( wp_parse_url( home_url(), PHP_URL_HOST ), '/' ) . ')/i', $post->post_content ), 5, __( 'No external reference link was detected.', self::get_textdomain() ), __( 'Add an authoritative external reference when relevant.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, has_post_thumbnail( $post_id ), 5, __( 'Featured image is missing.', self::get_textdomain() ), __( 'Add a relevant featured image.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, self::has_image_alt_text( $post_id, $post->post_content ), 5, __( 'Image alt text is missing.', self::get_textdomain() ), __( 'Add descriptive alt text to images.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' !== trim( $canonical ), 4, __( 'Canonical URL is missing.', self::get_textdomain() ), __( 'Set a canonical URL to avoid duplicate-content confusion.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' === $robots || false === stripos( $robots, 'noindex' ), 4, __( 'Robots meta is set to noindex.', self::get_textdomain() ), __( 'Use index,follow when this page should appear in search results.', self::get_textdomain() ) );
			self::score_check( $score, $issues, $suggestions, '' !== trim( $schema ), 4, __( 'Schema type is missing.', self::get_textdomain() ), __( 'Select the most accurate schema type for this content.', self::get_textdomain() ) );

			if ( class_exists( 'SftcyWordaiFeatures' ) && SftcyWordaiFeatures::wordai_is_basic_tier() && ( '' === trim( $title ) || '' === trim( $description ) || '' === trim( $keyword ) ) ) {
				$suggestions[] = class_exists( 'SftcyWordaiSeoStandards' ) ? SftcyWordaiSeoStandards::get_basic_upgrade_suggestion() : __( 'Upgrade to WordAI Pro to generate SEO title, meta description, and focus keyword automatically.', self::get_textdomain() );
			}

			$score = min( 100, max( 0, (int) $score ) );
			$grade = self::get_grade( $score );
			$result = array(
				'score'       => $score,
				'grade'       => $grade,
				'issues'      => $issues,
				'suggestions' => $suggestions,
				'hash'        => self::get_content_hash( $post_id ),
			);

			if ( $save ) {
				update_post_meta( $post_id, self::META_SCORE, $score );
				update_post_meta( $post_id, self::META_GRADE, $grade );
				update_post_meta( $post_id, self::META_ISSUES, $issues );
				update_post_meta( $post_id, self::META_SUGGESTIONS, $suggestions );
				update_post_meta( $post_id, self::META_HASH, $result['hash'] );
				update_post_meta( $post_id, self::META_SCANNED_AT, current_time( 'mysql' ) );
			}

			return $result;
		}

		private static function score_check( &$score, &$issues, &$suggestions, $passed, $points, $issue, $suggestion ) {
			if ( $passed ) {
				$score += (int) $points;
				return;
			}

			$issues[]      = $issue;
			$suggestions[] = $suggestion;
		}

		private static function generate_ai_autofix_values( $post ) {
			return self::generate_local_autofix_values( $post );
		}

		private static function generate_local_autofix_values( $post ) {
			$title   = self::normalize_title( $post->post_title );
			$content = self::plain_text_with_spacing( strip_shortcodes( $post->post_excerpt ?: $post->post_content ) );
			$keyword = self::derive_focus_keyword( $title, $content );

			$seo_title = $title;
			if ( $keyword && false === stripos( $seo_title, $keyword ) ) {
				$seo_title = $keyword . ': ' . $seo_title;
			}
			if ( self::string_length( $seo_title ) < 35 ) {
				$seo_title = trim( $seo_title . ' - Practical Guide' );
			}

			$description = $content ?: $title;
			if ( $keyword && false === stripos( $description, $keyword ) ) {
				$description = $keyword . ' - ' . $description;
			}

			return array(
				'seo_title'        => self::sanitize_limited_text( $seo_title, 60 ),
				'meta_description' => self::sanitize_limited_text( $description, 160 ),
				'focus_keyword'    => self::sanitize_limited_text( $keyword ?: $title, 80 ),
			);
		}

		private static function derive_focus_keyword( $title, $content ) {
			$content_keyword = self::derive_keyword_from_text( $content );
			if ( '' !== $content_keyword ) {
				return $content_keyword;
			}

			return self::derive_keyword_from_text( $title );
		}

		private static function derive_keyword_from_text( $text ) {
			$source = strtolower( remove_accents( wp_strip_all_tags( (string) $text ) ) );
			$source = preg_replace( '/[^a-z0-9\s-]/', ' ', $source );
			$words  = preg_split( '/\s+/', $source );
			$stop   = array( 'the', 'and', 'for', 'with', 'that', 'this', 'from', 'your', 'you', 'are', 'was', 'were', 'into', 'about', 'have', 'has', 'will', 'can', 'its', 'our', 'their', 'a', 'an', 'of', 'to', 'in', 'on', 'by', 'is', 'as', 'at', 'but', 'not', 'all', 'new', 'more', 'than', 'then', 'when', 'where', 'what', 'how', 'why' );
			$terms  = array();

			foreach ( $words as $word ) {
				$word = trim( $word );
				if ( strlen( $word ) < 4 || in_array( $word, $stop, true ) ) {
					continue;
				}
				$terms[ $word ] = isset( $terms[ $word ] ) ? $terms[ $word ] + 1 : 1;
			}

			arsort( $terms );
			$top = array_slice( array_keys( $terms ), 0, 1 );

			return ucwords( implode( ' ', $top ) );
		}

		private static function normalize_title( $title ) {
			$title = wp_strip_all_tags( (string) $title );
			$title = html_entity_decode( $title, ENT_QUOTES | ENT_HTML5, get_bloginfo( 'charset' ) ?: 'UTF-8' );
			$title = wp_specialchars_decode( $title, ENT_QUOTES );
			$title = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $title );
			$title = preg_replace( '/\s+/', ' ', $title );
			return trim( $title );
		}

		private static function plain_text_with_spacing( $html ) {
			$text = preg_replace( '/<\s*\/?(h[1-6]|p|div|li|br|ul|ol|blockquote)[^>]*>/i', ' ', (string) $html );
			$text = wp_strip_all_tags( $text );
			$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, get_bloginfo( 'charset' ) ?: 'UTF-8' );
			$text = preg_replace( '/\s+/', ' ', $text );
			return trim( $text );
		}

		private static function get_content_hash( $post_id ) {
			$post = get_post( $post_id );
			if ( ! $post ) {
				return '';
			}

			$seo_document = self::get_seo_document( $post_id );
			$parts = array(
				$post->post_title,
				$post->post_excerpt,
				$post->post_content,
				$seo_document['provider'],
				$seo_document['title'],
				$seo_document['description'],
				$seo_document['focus_keyword'],
				$seo_document['canonical'],
				$seo_document['robots'],
				$seo_document['schema_type'],
			);

			return sha1( implode( '|', array_map( 'strval', $parts ) ) );
		}

		private static function get_seo_document( $post_id ) {
			$document = SftcyWordaiSeoIntegrationManager::read( $post_id );
			if ( is_wp_error( $document ) ) {
				$document = SftcyWordaiSeoIntegrationManager::providers()['native']->read( $post_id );
			}

			return $document;
		}

		private static function has_image_alt_text( $post_id, $content ) {
			$thumbnail_id = get_post_thumbnail_id( $post_id );
			if ( $thumbnail_id && trim( (string) get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true ) ) !== '' ) {
				return true;
			}

			if ( preg_match_all( '/<img[^>]+>/i', $content, $matches ) ) {
				foreach ( $matches[0] as $img ) {
					if ( preg_match( '/alt=["\']([^"\']+)["\']/i', $img, $alt ) && trim( $alt[1] ) !== '' ) {
						return true;
					}
				}
			}

			return false;
		}

		private static function get_default_schema_type( $post_type ) {
			if ( 'product' === $post_type ) {
				return 'Product';
			}

			return 'Article';
		}

		private static function get_grade( $score ) {
			if ( $score >= 90 ) {
				return 'excellent';
			}
			if ( $score >= 75 ) {
				return 'good';
			}
			if ( $score >= 50 ) {
				return 'needs_improvement';
			}

			return 'poor';
		}

		private static function get_array_meta( $post_id, $key ) {
			$value = get_post_meta( $post_id, $key, true );
			return is_array( $value ) ? array_values( $value ) : array();
		}

		public static function get_usage_summary() {
			$limit = self::get_monthly_limit();
			$items = self::get_usage_items();
			$used  = count( $items );

			return array(
				'limited'     => $limit > 0,
				'limit'       => $limit,
				'used'        => $used,
				'remaining'   => $limit > 0 ? max( 0, $limit - $used ) : 0,
				'month_label' => date_i18n( 'F Y', current_time( 'timestamp' ) ),
				'upgrade_url' => class_exists( 'SftcyWordaiFeatures' ) ? SftcyWordaiFeatures::wordai_get_upgrade_url() : 'https://softcoy.com/products/wordai-pro',
			);
		}

		private static function get_monthly_limit() {
			if ( ! class_exists( 'SftcyWordaiFeatures' ) ) {
				return 0;
			}

			if ( ! SftcyWordaiFeatures::wordai_is_feature_limited( 'seo_scan' ) ) {
				return 0;
			}

			if ( method_exists( 'SftcyWordaiFeatures', 'wordai_get_seo_scan_monthly_limit' ) ) {
				return (int) SftcyWordaiFeatures::wordai_get_seo_scan_monthly_limit();
			}

			$limits = SftcyWordaiFeatures::wordai_get_feature_limits( 'seo_scan' );
			return ! empty( $limits['monthlyItems'] ) ? max( 1, (int) $limits['monthlyItems'] ) : 3;
		}

		private static function get_usage_key() {
			return self::USAGE_PREFIX . date_i18n( 'Ym', current_time( 'timestamp' ) );
		}

		private static function get_usage_items() {
			$items = get_option( self::get_usage_key(), array() );
			if ( ! is_array( $items ) ) {
				return array();
			}

			return array_values( array_unique( array_map( 'absint', $items ) ) );
		}

		private static function maybe_consume_usage_slot( $post_id ) {
			$limit = self::get_monthly_limit();
			if ( $limit <= 0 ) {
				return true;
			}

			$items = self::get_usage_items();
			if ( in_array( $post_id, $items, true ) ) {
				return true;
			}

			if ( count( $items ) >= $limit ) {
				return new WP_Error(
					'wordai_seo_scan_limit_reached',
					sprintf(
						/* translators: %d: monthly scan limit. */
						__( 'You have used your %d free SEO scans with AI fixes for this month. Upgrade to WordAI Pro for unlimited SEO scans and auto fixes.', self::get_textdomain() ),
						$limit
					)
				);
			}

			$items[] = $post_id;
			update_option( self::get_usage_key(), array_values( array_unique( $items ) ), false );

			return true;
		}

		private static function limit_error_payload( WP_Error $error ) {
			return array(
				'message' => $error->get_error_message(),
				'usage'   => self::get_usage_summary(),
			);
		}

		private static function extract_json_object( $text ) {
			$text = trim( preg_replace( '/```(?:json)?|```/i', '', $text ) );
			if ( preg_match( '/\{[\s\S]*\}/', $text, $matches ) ) {
				$decoded = json_decode( $matches[0], true );
				if ( is_array( $decoded ) ) {
					return $decoded;
				}
			}

			$decoded = json_decode( $text, true );
			return is_array( $decoded ) ? $decoded : array();
		}

		private static function sanitize_limited_text( $text, $limit ) {
			$text = sanitize_text_field( wp_strip_all_tags( (string) $text ) );
			$text = preg_replace( '/\s+/', ' ', $text );
			return self::truncate( trim( $text ), $limit );
		}

		private static function truncate( $text, $limit ) {
			$text = trim( (string) $text );
			if ( self::string_length( $text ) <= $limit ) {
				return $text;
			}

			if ( function_exists( 'mb_substr' ) ) {
				$text = mb_substr( $text, 0, $limit - 1 );
			} else {
				$text = substr( $text, 0, $limit - 1 );
			}

			return rtrim( $text, " \t\n\r\0\x0B.,;:-" );
		}

		private static function string_length( $text ) {
			$text = (string) $text;
			return function_exists( 'mb_strlen' ) ? mb_strlen( $text ) : strlen( $text );
		}
	}
}
