HEX
Server: Apache/2.4.63 (Unix)
System: Linux TOMS-220NAS 4.4.302+ #86009 SMP Wed Nov 26 18:19:17 CST 2025 x86_64
User: flavio87 (1026)
PHP: 8.3.27
Disabled: NONE
Upload Files
File: /volume1/web/tom-marchisa/wp-content/plugins/media-file-renamer/classes/rest.php
<?php

class Meow_MFRH_Rest
{
	private $core = null;
	private $admin = null;
	private $namespace = 'media-file-renamer/v1';
	private $allow_usage = false;
	private $allow_setup = false;

	public function __construct( $core, $admin) {
		$this->core = $core;
		$this->admin = $admin;

		// FOR DEBUG
		// For experiencing the UI behavior on a slower install.
		// sleep(1);
		// For experiencing the UI behavior on a buggy install.
		// trigger_error( "Error", E_USER_ERROR);
		// trigger_error( "Warning", E_USER_WARNING);
		// trigger_error( "Notice", E_USER_NOTICE);
		// trigger_error( "Deprecated", E_USER_DEPRECATED);

		add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
	}

	function rest_api_init() {
		$this->allow_usage = apply_filters( 'mfrh_allow_usage', current_user_can( 'administrator' ) );
		$this->allow_setup = apply_filters( 'mfrh_allow_setup', current_user_can( 'manage_options' ) );

		// SETTINGS
		if ( $this->allow_setup ) {
			register_rest_route( $this->namespace, '/update_option', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_settings' ),
				'callback' => array( $this, 'rest_update_option' )
			) );
			register_rest_route( $this->namespace, '/all_settings', array(
				'methods' => 'GET',
				'permission_callback' => array( $this->core, 'can_access_settings' ),
				'callback' => array( $this, 'rest_all_settings' )
			) );
			register_rest_route( $this->namespace, '/reset_options', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_reset_options' )
			) );
			register_rest_route( $this->namespace, '/reset_metadata', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_reset_metadata' )
			) );
			register_rest_route( $this->namespace, '/toggle_parser', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_toggle_parser' )
			) );
			register_rest_route( $this->namespace, '/test_rules', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_test_rules' )
			) );


		}

		// STATS & LISTING
		if ( $this->allow_usage ) {
			register_rest_route( $this->namespace, '/stats', array(
				'methods' => 'GET',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_get_stats' ),
				'args' => array(
					'search' => array( 'required' => false ),
					'postType' => array( 'required' => false ),
				)
			) );
			register_rest_route( $this->namespace, '/media', array(
				'methods' => 'GET',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_media' ),
				'args' => array(
					'limit' => array( 'required' => false, 'default' => 10 ),
					'skip' => array( 'required' => false, 'default' => 20 ),
					'filterBy' => array( 'required' => false, 'default' => 'all' ),
					'orderBy' => array( 'required' => false, 'default' => 'id' ),
					'order' => array( 'required' => false, 'default' => 'desc' ),
					'search' => array( 'required' => false ),
					'postType' => array( 'required' => false ),
					'offset' => array( 'required' => false ),
					'order' => array( 'required' => false ),
				)
			) );

			register_rest_route( $this->namespace, '/media/id', array(
				'methods' => 'GET',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_media_id' ),
				'args' => array(
					'filterBy' => array( 'required' => false, 'default' => 'all' ),
				)
			) );

			register_rest_route( $this->namespace, '/uploads_directory_hierarchy', array(
				'methods' => 'GET',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_uploads_directory_hierarchy' ),
				'args' => array(
					'force' => array( 'required' => false, 'default' => false ),
				)
			) );
			register_rest_route( $this->namespace, '/analyze', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_analyze' )
			) );
			register_rest_route( $this->namespace, '/reanalyze_pending', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_reanalyze_pending' )
			) );
			register_rest_route( $this->namespace, '/auto_attach', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_auto_attach' )
			) );
			register_rest_route( $this->namespace, '/get_all_ids', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_get_all_ids' )
			) );
			register_rest_route( $this->namespace, '/get_all_post_ids', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_get_all_post_ids' )
			) );

			// ACTIONS
			register_rest_route( $this->namespace, '/set_lock', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_set_lock' )
			) );
			register_rest_route( $this->namespace, '/clear_pending', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_clear_pending' )
			) );
			register_rest_route( $this->namespace, '/rename', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_rename' )
			) );
			register_rest_route( $this->namespace, '/move', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_move' )
			) );
			register_rest_route( $this->namespace, '/undo', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_undo' )
			) );
			register_rest_route( $this->namespace, '/status', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_status' )
			) );
			register_rest_route( $this->namespace, '/update_media', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_update_media' )
			) );
			register_rest_route( $this->namespace, '/sync_fields', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_sync_fields' )
			) );
			register_rest_route( $this->namespace, '/ai_suggest', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_ai_suggest' )
			) );
			register_rest_route( $this->namespace, '/create_folder', array(
				'methods' => 'POST',
				'permission_callback' => array( $this->core, 'can_access_features' ),
				'callback' => array( $this, 'rest_create_folder' )
			) );
		}

		// LOGS
		register_rest_route( $this->namespace, '/refresh_logs', array(
			'methods' => 'POST',
			'permission_callback' => array( $this->core, 'can_access_features' ),
			'callback' => array( $this, 'rest_refresh_logs' )
		) );
		register_rest_route( $this->namespace, '/clear_logs', array(
			'methods' => 'POST',
			'permission_callback' => array( $this->core, 'can_access_features' ),
			'callback' => array( $this, 'rest_clear_logs' )
		) );
	}

	function rest_refresh_logs() {
		return new WP_REST_Response( [ 'success' => true, 'data' => $this->core->get_logs() ], 200 );
	}

	function rest_clear_logs() {
		$this->core->clear_logs();
		return new WP_REST_Response( [ 'success' => true ], 200 );
	}

	function rest_analyze( $request ) {
		$params = $request->get_json_params();
		$mediaIds = isset( $params['mediaIds'] ) ? (array)$params['mediaIds'] : null;
		$mediaId = isset( $params['mediaId'] ) ? (int)$params['mediaId'] : null;
		$data = array();
		if ( !empty( $mediaIds ) ) {
			foreach ( $mediaIds as $mediaId ) {
				$entry = $this->core->get_media_status_one( $mediaId );
				array_push( $data, $entry );
			}
		}
		else if ( !empty( $mediaId ) ) {
			$data = $this->core->get_media_status_one( $mediaId );
		}
		return new WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 );
	}

	function rest_reanalyze_pending( $request ) {
		global $wpdb;
		$params = $request->get_json_params();
		$offset = isset( $params['offset'] ) ? (int)$params['offset'] : 0;
		$limit = isset( $params['limit'] ) ? (int)$params['limit'] : 10;

		// Build query to get all unlocked media
		$whereClauses = [ "p.post_type = 'attachment'", "p.post_status = 'inherit'" ];
		$joins = [];

		if ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$whereClauses[] = "p.post_mime_type IN ( '$images_mime_types' )";
		}
		if ( $this->core->featured_only ) {
			$joins[] = "INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}

		// Exclude locked items
		$lock_enabled = $this->core->get_option( 'lock' );
		if ( $lock_enabled ) {
			$joins[] = "LEFT JOIN $wpdb->postmeta pm_lock ON pm_lock.post_id = p.ID AND pm_lock.meta_key = '_manual_file_renaming'";
			$whereClauses[] = "pm_lock.meta_value IS NULL";
		}

		$joinsSql = implode( ' ', $joins );
		$whereSql = implode( ' AND ', $whereClauses );

		// Get total count
		$total = (int)$wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p $joinsSql WHERE $whereSql" );

		// Get batch of IDs
		$ids = $wpdb->get_col( $wpdb->prepare(
			"SELECT DISTINCT p.ID FROM $wpdb->posts p $joinsSql WHERE $whereSql ORDER BY p.ID ASC LIMIT %d, %d",
			$offset,
			$limit
		) );

		$processed = 0;
		$marked_pending = 0;
		$skipped_kept = 0;
		$force_rename = $this->core->get_option( 'force_rename' );
		$current_method = $this->core->get_option( 'auto_rename' );

		foreach ( $ids as $id ) {
			$post = get_post( $id, ARRAY_A );
			if ( !$post ) {
				continue;
			}

			// Check if user previously decided to keep this filename
			$keep_method = get_post_meta( $id, '_mfrh_keep_filename', true );
			if ( $keep_method ) {
				// If the method is the same, respect their decision and skip
				if ( $keep_method === $current_method ) {
					// Make sure it's not marked as pending
					delete_post_meta( $id, '_require_file_renaming' );
					$skipped_kept++;
					$processed++;
					continue;
				}
				// Method changed, clear the keep flag and re-evaluate
				delete_post_meta( $id, '_mfrh_keep_filename' );
			}

			$output = [];
			$result = $this->core->check_attachment( $post, $output, null, $force_rename, true );

			// If check_attachment returns a proposed filename, mark as pending
			if ( $result !== false && isset( $output['proposed_filename'] ) ) {
				if ( !get_post_meta( $id, '_require_file_renaming', true ) ) {
					add_post_meta( $id, '_require_file_renaming', true, true );
				}
				// Store computed values to avoid recalculating during listing
				update_post_meta( $id, '_mfrh_proposed_filename', $output['proposed_filename'] );
				update_post_meta( $id, '_mfrh_used_method', $output['used_method'] ?? null );
				update_post_meta( $id, '_mfrh_ideal_filename', $output['ideal_filename'] ?? null );
				update_post_meta( $id, '_mfrh_proposed_filename_exists', $output['proposed_filename_exists'] ? '1' : '0' );
				$marked_pending++;
			} else {
				// Not pending, remove the flag and cached data if they exist
				delete_post_meta( $id, '_require_file_renaming' );
				delete_post_meta( $id, '_mfrh_proposed_filename' );
				delete_post_meta( $id, '_mfrh_used_method' );
				delete_post_meta( $id, '_mfrh_ideal_filename' );
				delete_post_meta( $id, '_mfrh_proposed_filename_exists' );
			}
			$processed++;
		}

		$done = ( $offset + $processed ) >= $total;

		// If done, update the analyzed method
		if ( $done ) {
			$options = $this->core->get_all_options();
			$options['pending_analyzed_method'] = $this->core->get_option( 'auto_rename' );
			$this->core->update_options( $options );
		}

		return new WP_REST_Response( [
			'success' => true,
			'processed' => $processed,
			'marked_pending' => $marked_pending,
			'offset' => $offset,
			'total' => $total,
			'done' => $done,
		], 200 );
	}

	function rest_auto_attach( $request ) {
		$params = $request->get_json_params();
		$post_ids = isset( $params['postIds'] ) ? (array)$params['postIds'] : null;
		$post_id = isset( $params['postId'] ) ? (int)$params['postId'] : null;
		$media_ids = isset( $params['mediaIds'] ) ? (array)$params['mediaIds'] : [];
		if ( !empty( $post_ids ) ) {
			foreach ( $post_ids as $post_id ) {
				$this->do_auto_attach( $post_id, $media_ids );
			}
		}
		else if ( !empty( $post_id ) ) {
			$this->do_auto_attach( $post_id, $media_ids );
		}
		return new WP_REST_Response( [ 'success' => true ], 200 );
	}

	/**
	 * TODO: delete if not used anywhere
	 */
	function rest_get_all_ids( $request ) {
		global $wpdb;
		$params = $request->get_json_params();
		$unlockedOnly = isset( $params['unlockedOnly'] ) ? (bool)$params['unlockedOnly'] : false;

		$innerJoinCondition = '';
		if ( $this->core->featured_only ) {
			$innerJoinCondition = "INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}

		if ( $unlockedOnly ) {
			$ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts p 
				$innerJoinCondition
				LEFT JOIN $wpdb->postmeta pm ON p.ID = pm.post_id 
				AND pm.meta_key='_manual_file_renaming'
				WHERE post_type='attachment'
				AND post_status='inherit'
				AND pm.meta_value IS NULL"
			);
		}
		else {
			$ids = $wpdb->get_col( "SELECT ID FROM $wpdb->posts p 
				WHERE post_type='attachment'
				AND post_status='inherit'"
			);
		}
		return new WP_REST_Response( [ 'success' => true, 'data' => $ids ], 200 );
	}

	function rest_get_all_post_ids(  ) {
		global $wpdb;
		$ids = $wpdb->get_col( "SELECT p.ID FROM $wpdb->posts p
			WHERE p.post_status NOT IN ('inherit', 'trash', 'auto-draft')
			AND p.post_type NOT IN ('attachment', 'shop_order', 'shop_order_refund', 'nav_menu_item', 'revision', 'auto-draft', 'wphb_minify_group', 'customize_changeset', 'oembed_cache', 'nf_sub')
			AND p.post_type NOT LIKE 'dlssus%'
			AND p.post_type NOT LIKE 'ml-slide%'
			AND p.post_type NOT LIKE '%acf-%'
			AND p.post_type NOT LIKE '%edd%'"
		);
		return new WP_REST_Response( [ 'success' => true, 'data' => $ids ], 200 );
	}

	function rest_uploads_directory_hierarchy( $request ) {
		if ( !$this->admin->is_pro_user() ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => __( 'This feature for Pro users.', 'media-file-renamer' ) ], 200 );
		}

		$force = trim( $request->get_param('force') ) === 'true';
		$transientKey = 'uploads_directory_hierarchy';
		if ( $force ) {
			delete_transient( $transientKey );
		}

		$data = get_transient( $transientKey );
		if ( !$data ) {
			$data = $this->core->get_uploads_directory_hierarchy();
			set_transient( $transientKey, $data );
		}
		return new WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 );
	}

	function rest_create_folder( $request ) {
		if ( !$this->admin->is_pro_user() ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => __( 'This feature for Pro users.', 'media-file-renamer' ) ], 200 );
		}

		try {
			$params = $request->get_json_params();
			$folder = isset( $params['folder'] ) ? $params['folder'] : null;
			if ( !$folder ) {
				throw new Exception( __( 'The folder name is missing.', 'media-file-renamer' ) );
			}
			$this->core->create_folder( $folder );
			return new WP_REST_Response( [ 'success' => true ], 200 );
		} catch ( Exception $e ) {
			return new WP_REST_Response([
				'success' => false,
				'message' => $e->getMessage(),
			], 200 );
		}
	}

	function rest_status( $request ) {
		$params = $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$entry = $this->core->get_media_status_one( $mediaId );
		return new WP_REST_Response( [ 'success' => true, 'data' => $entry ], 200 );
	}

	function rest_update_media( $request ) {
		$params = is_array( $request ) ? $request : $request->get_json_params();
		$id = isset( $params['id'] ) ? $params['id'] : '';
		$postTitle = isset( $params['post_title'] ) ? $params['post_title'] : '';
		$imageAlt = isset( $params['image_alt'] ) ? $params['image_alt'] : '';
		$imageDescription = isset( $params['image_description'] ) ? $params['image_description'] : '';
		$imageCaption = isset( $params['image_caption'] ) ? $params['image_caption'] : '';
		$method = isset( $params['method'] ) ? $params['method'] : 'manual';
		$sync = isset( $params['sync'] ) ? $params['sync'] : false;

		$result = $this->core->update_media($id, $postTitle, $imageAlt, $imageDescription, $imageCaption, $method, $sync);

		if (!empty($result['errors'])) {
			return new WP_REST_Response([
				'success' => false,
				'message' => implode(',', $result['errors']),
			], 200 );
		}

		return new WP_REST_Response( [ 'success' => true ], 200 );
	}

	function rest_sync_fields( $request ) {
		$params = $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$useVision = isset( $params['ai'] ) ? (bool)$params['ai'] : false;
		$timeFilter = isset( $params['syncFieldsTimeFilter'] ) ? (int)$params['syncFieldsTimeFilter'] : 0;

		$post = get_post( $mediaId, ARRAY_A );
		if ( !$post ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'The media ID is invalid.' ], 200 );
		}

		do_action( 'mfrh_media_resync', $post, $useVision, $timeFilter );
		return new WP_REST_Response( [ 'success' => true, 'data' => [] ], 200 );
	}

	function rest_ai_suggest ( $request ) {
		$params = is_array( $request ) ? $request : $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$metadataType = isset( $params['type'] ) ? (string)$params['type'] : null;

		if ( !$mediaId || !$metadataType ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => 'The media ID or the metadata type is missing.' ], 200 );
		}

		try {
			$newMetadata = $this->core->ai_suggestion( $mediaId, $metadataType );

			if ( empty( $newMetadata ) ) {
				return new WP_REST_Response( [ 'success' => true, 'message' => 'No suggestion.' ], 200 );
			}

			return new WP_REST_Response( [ 'success' => true, 'data' => $newMetadata ], 200 );
		}
		catch ( Exception $e ) {
			return new WP_REST_Response( [ 'success' => true, 'message' => $e->getMessage() ], 200 );
		}
	}

	function rest_rename( $request ) {
		$params = $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$filename = isset( $params['filename'] ) ? (string)$params['filename'] : null;
		$renameMethod = isset( $params['renameMethod'] ) ? (string)$params['renameMethod'] : null;
		
		if ( $filename && $renameMethod === 'auto' ) {
			$this->core->log( 'The rename method is set to Auto but a filename is provided. The filename will be ignored.' );
			$filename = null;
		}

		try {
			$res = $this->core->engine->rename( $mediaId, $filename, false, $renameMethod );
			$entry = $this->core->get_media_status_one( $mediaId );
			$response = [ 'success' => !!$res, 'data' => $entry ];

			if ( array_key_exists( 'warning', $res ) ) {
				$response['warning'] = $res['warning'];
			}

			return $this->create_rest_response( $response, 200 );
		}
		catch ( Exception $e ) {
			return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 200 );
		}
	}

	function rest_move( $request ) {
		$params = $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$newPath = isset( $params['newPath'] ) ? (string)$params['newPath'] : null;
		$res = $this->core->move( $mediaId, $newPath );
		$entry = $this->core->get_media_status_one( $mediaId );
		return new WP_REST_Response( [ 'success' => !!$res, 'data' => $entry ], 200 );
	}

	function rest_undo( $request ) {
		$params = $request->get_json_params();
		$mediaId = (int)$params['mediaId'];
		$res = $this->core->undo( $mediaId );
		$entry = $this->core->get_media_status_one( $mediaId );
		return new WP_REST_Response( [ 'success' => !!$res, 'data' => $entry ], 200 );
	}

	function rest_set_lock( $request ) {
		$params = $request->get_json_params();
		$lock = (boolean)$params['lock'];
		$mediaIds = isset( $params['mediaIds'] ) ? (array)$params['mediaIds'] : null;
		$mediaId = isset( $params['mediaId'] ) ? (int)$params['mediaId'] : null;
		$data = null;
		if ( !empty( $mediaIds ) ) {
			foreach ( $mediaIds as $mediaId ) {
				$lock ? $this->core->lock( $mediaId ) : $this->core->unlock( $mediaId );
			}
			$data = 'N/A';
		}
		else if ( !empty( $mediaId ) ) {
			$lock ? $this->core->lock( $mediaId ) : $this->core->unlock( $mediaId );
			$data = $this->core->get_media_status_one( $mediaId );
		}
		return new WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 );
	}

	function rest_clear_pending( $request ) {
		global $wpdb;
		$params = $request->get_json_params();
		$mediaIds = isset( $params['mediaIds'] ) ? (array)$params['mediaIds'] : null;
		$mediaId = isset( $params['mediaId'] ) ? (int)$params['mediaId'] : null;
		$data = null;

		// Store the current rename method so we know when this decision was made
		// If the method changes later, the file should be re-evaluated
		$current_method = $this->core->get_option( 'auto_rename' );

		if ( !empty( $mediaIds ) ) {
			foreach ( $mediaIds as $id ) {
				$id = (int)$id;
				delete_post_meta( $id, '_require_file_renaming' );
				update_post_meta( $id, '_mfrh_keep_filename', $current_method );
				// Clear cached rename data
				delete_post_meta( $id, '_mfrh_proposed_filename' );
				delete_post_meta( $id, '_mfrh_used_method' );
				delete_post_meta( $id, '_mfrh_ideal_filename' );
				delete_post_meta( $id, '_mfrh_proposed_filename_exists' );
				clean_post_cache( $id );
			}
			$data = 'N/A';
		}
		else if ( !empty( $mediaId ) ) {
			delete_post_meta( $mediaId, '_require_file_renaming' );
			update_post_meta( $mediaId, '_mfrh_keep_filename', $current_method );
			// Clear cached rename data
			delete_post_meta( $mediaId, '_mfrh_proposed_filename' );
			delete_post_meta( $mediaId, '_mfrh_used_method' );
			delete_post_meta( $mediaId, '_mfrh_ideal_filename' );
			delete_post_meta( $mediaId, '_mfrh_proposed_filename_exists' );
			clean_post_cache( $mediaId );
			// Don't call get_media_status_one here as it will re-add the pending meta via check_attachment
			$data = [ 'mediaId' => $mediaId, 'pending' => false ];
		}
		return new WP_REST_Response( [ 'success' => true, 'data' => $data ], 200 );
	}

	/**
	 * Get all stats counts in a single query for better performance.
	 * Returns: total, pending, locked, renamed counts.
	 */
	function count_all_stats( $search, $postType = null ) {
		global $wpdb;

		$joinSql = '';
		$whereClauses = [];

		// Images only filter
		if ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$whereClauses[] = "p.post_mime_type IN ( '$images_mime_types' )";
		}

		// Search filter
		if ( $search ) {
			$joinSql .= " LEFT JOIN $wpdb->postmeta pm_file ON pm_file.post_id = p.ID AND pm_file.meta_key = '_wp_attached_file'";
			$searchValue = '%' . $wpdb->esc_like( $search ) . '%';
			$whereClauses[] = $wpdb->prepare( "(p.post_title LIKE %s OR pm_file.meta_value LIKE %s)", $searchValue, $searchValue );
		}

		// Featured only filter
		if ( $this->core->featured_only ) {
			$joinSql .= " INNER JOIN $wpdb->postmeta pm_featured ON pm_featured.meta_value = p.ID AND pm_featured.meta_key = '_thumbnail_id'";
		}

		// Post type filter - join with parent post to filter by its type
		if ( !empty( $postType ) ) {
			$joinSql .= " INNER JOIN $wpdb->posts parent_post ON parent_post.ID = p.post_parent";
			$whereClauses[] = $wpdb->prepare( "parent_post.post_type = %s", $postType );
		}

		// LEFT JOINs for counting specific meta keys
		$joinSql .= " LEFT JOIN $wpdb->postmeta pm_pending ON pm_pending.post_id = p.ID AND pm_pending.meta_key = '_require_file_renaming'";
		$joinSql .= " LEFT JOIN $wpdb->postmeta pm_locked ON pm_locked.post_id = p.ID AND pm_locked.meta_key = '_manual_file_renaming'";
		$joinSql .= " LEFT JOIN $wpdb->postmeta pm_renamed ON pm_renamed.post_id = p.ID AND pm_renamed.meta_key = '_original_filename'";

		$whereSql = count( $whereClauses ) > 0 ? "AND " . implode( " AND ", $whereClauses ) : "";

		$sql = "SELECT
			COUNT(DISTINCT p.ID) as total,
			COUNT(DISTINCT CASE WHEN pm_pending.post_id IS NOT NULL THEN p.ID END) as pending,
			COUNT(DISTINCT CASE WHEN pm_pending.post_id IS NOT NULL AND pm_locked.post_id IS NULL THEN p.ID END) as pending_unlocked,
			COUNT(DISTINCT CASE WHEN pm_locked.post_id IS NOT NULL THEN p.ID END) as locked,
			COUNT(DISTINCT CASE WHEN pm_renamed.post_id IS NOT NULL THEN p.ID END) as renamed
			FROM $wpdb->posts p
			$joinSql
			WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' $whereSql";

		$result = $wpdb->get_row( $sql );

		return [
			'all' => (int) $result->total,
			'pending' => (int) $result->pending,
			'pending_unlocked' => (int) $result->pending_unlocked,
			'locked' => (int) $result->locked,
			'renamed' => (int) $result->renamed,
		];
	}

	function count_locked( $search ) {
		global $wpdb;
		$innerJoinSql = '';
		$whereSql = '';
		if ( $search ) {
			$innerJoinSql = "INNER JOIN $wpdb->postmeta pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_wp_attached_file'";
			$searchValue = '%' . $wpdb->esc_like( $search ) . '%';
			$whereSql = $wpdb->prepare( "AND (p.post_title LIKE %s OR pm2.meta_value LIKE %s)", $searchValue, $searchValue );
		}
		if ( $this->core->featured_only ) {
			$innerJoinSql .= " INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}
		return (int)$wpdb->get_var( "SELECT COUNT( DISTINCT p.ID) FROM $wpdb->posts p
			INNER JOIN $wpdb->postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_manual_file_renaming'
			$innerJoinSql
			WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' $whereSql"
		);
	}

	function count_pending( $search, $hide_locked = true ) {
		global $wpdb;
		$whereClauses = [];
		if ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$whereClauses[] = "p.post_mime_type IN ( '$images_mime_types' )";
		}
		$innerJoinSql = '';
		$leftJoinSql = '';
		if ( $search ) {
			$innerJoinSql = "INNER JOIN $wpdb->postmeta pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_wp_attached_file'";
			$searchValue = '%' . $wpdb->esc_like($search) . '%';
			$whereClauses[] = $wpdb->prepare("(p.post_title LIKE %s OR pm2.meta_value LIKE %s)", $searchValue, $searchValue);
		}
		if ( $this->core->featured_only ) {
			$innerJoinSql .= " INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}
		if ( $hide_locked ) {
			$leftJoinSql = "LEFT JOIN $wpdb->postmeta pm_lock ON pm_lock.post_id = p.ID AND pm_lock.meta_key = '_manual_file_renaming'";
			$whereClauses[] = "pm_lock.meta_value IS NULL";
		}
		$whereSql = count( $whereClauses ) > 0 ? "AND " . implode( " AND ", $whereClauses ) : "";
		return (int)$wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p
			INNER JOIN $wpdb->postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_require_file_renaming'
			$innerJoinSql
			$leftJoinSql
			WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' $whereSql"
		);
	}

	function count_renamed($search) {
		global $wpdb;
		$whereCaluses = [];
		if ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$whereCaluses[] = "p.post_mime_type IN ( '$images_mime_types' )";
		}
		$innerJoinSql = '';
		if ($search) {
			$innerJoinSql = "INNER JOIN $wpdb->postmeta pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_wp_attached_file'";
			$searchValue = '%' . $wpdb->esc_like($search) . '%';
			$whereCaluses[] = $wpdb->prepare("(p.post_title LIKE %s OR pm2.meta_value LIKE %s)", $searchValue, $searchValue);
		}
		if ( $this->core->featured_only ) {
			$innerJoinSql .= " INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}
		$whereSql = count($whereCaluses) > 0 ? "AND " . implode("AND ", $whereCaluses) : "";
		return (int)$wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p 
			INNER JOIN $wpdb->postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_original_filename'
			$innerJoinSql 
			WHERE p.post_type = 'attachment' AND p.post_status = 'inherit' $whereSql"
		);
	}

	function count_all( $search ) {
		global $wpdb;
		$whereCaluses = [];
		if ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$whereCaluses[] = "p.post_mime_type IN ( '$images_mime_types' )";
		}
		$innerJoinSql = '';
		if ( $search ) {
			$innerJoinSql = "INNER JOIN $wpdb->postmeta pm ON pm.post_id = p.ID";
			$searchValue = '%' . $wpdb->esc_like($search) . '%';
			$whereCaluses[] = $wpdb->prepare("( p.post_title LIKE %s OR pm.meta_value LIKE %s )", $searchValue, $searchValue);
		}
		if ( $this->core->featured_only ) {
			$innerJoinSql .= " INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		}
		$whereSql = count($whereCaluses) > 0 ? "AND " . implode("AND ", $whereCaluses) : "";
		return (int)$wpdb->get_var( "SELECT COUNT(DISTINCT p.ID) FROM $wpdb->posts p 
			$innerJoinSql 
			WHERE post_type='attachment' AND post_status='inherit' $whereSql"
		);
	}



	function count_by_filter( $filterBy, $search, $hide_locked = true, $postType = null ) {
		// Use the consolidated stats query for better performance
		$stats = $this->count_all_stats( $search, $postType );
		
		if ( $filterBy === 'pending' ) {
			return $hide_locked ? $stats['pending_unlocked'] : $stats['pending'];
		} else if ( $filterBy === 'renamed' ) {
			return $stats['renamed'];
		} else if ( $filterBy === 'locked' ) {
			return $stats['locked'];
		} else if ( $filterBy === 'unlocked' ) {
			return $stats['all'] - $stats['locked'];
		} else if ( $filterBy === 'unrenamed' ) {
			$unrenamed = $stats['all'] - $stats['renamed'];
			// If hide_locked is true, only count unlocked unrenamed items
			return $hide_locked ? ($unrenamed - $stats['locked']) : $unrenamed;
		} else { // 'all'
			return $stats['all'];
		}
	}

	function rest_get_stats($request) {
		$search = trim( $request->get_param( 'search' ) );
		$postType = trim( $request->get_param( 'postType' ) );
		$hide_locked = $this->core->get_option( 'hide_locked', true );

		// Single query for all stats instead of 4 separate queries
		$stats = $this->count_all_stats( $search, $postType );

		$all = $stats['all'];
		$pending = $hide_locked ? $stats['pending_unlocked'] : $stats['pending'];
		$locked = $stats['locked'];
		$renamed = $stats['renamed'];
		$unlocked = $all - $locked;
		$unrenamed = $all - $renamed;

		return new WP_REST_Response( [ 'success' => true, 'data' => array(
			'all' => $all,
			'locked' => $locked,
			'unlocked' => $unlocked,
			'renamed' => $renamed,
			'pending' => $pending,
			'unrenamed' => $unrenamed,
		) ], 200 );
	}

	/**
	 * Simply get only the media IDs with a simple and fast query.
	 *
	 * @param integer      $skip
	 * @param integer      $limit
	 * @param string       $filterBy
	 * @param string|null  $search
	 * @param boolean      $hide_locked
	 * @return array
	 */
	function get_media_ids( $skip, $limit, $filterBy, $search = null, $hide_locked = true )
	{
		global $wpdb;

		// Initialize query parts
		$joins   = array();
		$wheres  = array();

		// Base WHERE conditions
		$wheres[] = "p.post_type = 'attachment'";
		$wheres[] = "p.post_status = 'inherit'";

		// Handle featured_only and images_only options
		if ( $this->core->featured_only ) {
			$joins[] = "INNER JOIN {$wpdb->postmeta} pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
		} elseif ( $this->core->images_only ) {
			$images_mime_types = implode( "','", $this->core->images_mime_types );
			$wheres[] = "p.post_mime_type IN ('$images_mime_types')";
		}

		// Depending on filterBy, add necessary joins and conditions
		if ( $filterBy === 'pending' ) {
			$joins[] = "INNER JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_require_file_renaming' AND pm1.meta_value IS NOT NULL";
		} elseif ( $filterBy === 'renamed' ) {
			$joins[] = "INNER JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_original_filename' AND pm1.meta_value IS NOT NULL";
		} elseif ( $filterBy === 'unrenamed' ) {
			$joins[]  = "LEFT JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_original_filename'";
			$wheres[] = "pm1.meta_value IS NULL";
		} elseif ( $filterBy === 'locked' ) {
			$joins[] = "INNER JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_manual_file_renaming' AND pm1.meta_value IS NOT NULL";
		} elseif ( $filterBy === 'unlocked' ) {
			$joins[]  = "LEFT JOIN {$wpdb->postmeta} pm1 ON pm1.post_id = p.ID AND pm1.meta_key = '_manual_file_renaming'";
			$wheres[] = "pm1.meta_value IS NULL";
		}

		// Hide locked files if hide_locked is true
		if ( $hide_locked && $filterBy !== 'locked' && $filterBy !== 'unlocked' ) {
			$joins[]  = "LEFT JOIN {$wpdb->postmeta} pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_manual_file_renaming'";
			$wheres[] = "pm2.meta_value IS NULL";
		}

		// Handle search functionality
		if ( $search ) {
			$search_value = '%' . $wpdb->esc_like( $search ) . '%';
			$joins[]      = "LEFT JOIN {$wpdb->postmeta} pm_search ON pm_search.post_id = p.ID";
			$wheres[]     = $wpdb->prepare( "(p.post_title LIKE %s OR pm_search.meta_value LIKE %s)", $search_value, $search_value );
		}

		// Build the query
		$query = "SELECT DISTINCT p.ID FROM {$wpdb->posts} p";
		if ( ! empty( $joins ) ) {
			$query .= ' ' . implode( ' ', $joins );
		}
		if ( ! empty( $wheres ) ) {
			$query .= ' WHERE ' . implode( ' AND ', $wheres );
		}
		$query .= $wpdb->prepare( " LIMIT %d, %d", $skip, $limit );

		// Retrieve and return the IDs
		$ids = $wpdb->get_col( $query );

		return $ids;
	}

	/**
	 * Get the status for many Media IDs.
	 *
	 * @param integer $skip
	 * @param integer $limit
	 * @param string $filterBy
	 * @param string $orderBy
	 * @param string $order
	 * @param string|null $search
	 * @param string|null $postType Filter by attached post type
	 * @return array
	 */
	function get_media_status(
        $skip = 0,
        $limit = 10,
        $filterBy = 'pending',
        $orderBy = 'post_title',
        $order = 'asc',
        $search = null,
        $hide_locked = true,
        $postType = null
    ) {
        global $wpdb;

        // Initialize an array to hold HAVING conditions
        $havingConditions = array();

        // Add conditions based on the filter
        if ( $filterBy === 'pending' ) {
            $havingConditions[] = 'pending IS NOT NULL';
        } else if ( $filterBy === 'renamed' ) {
            $havingConditions[] = 'original_filename IS NOT NULL';
        } else if ( $filterBy === 'unrenamed' ) {
            $havingConditions[] = 'original_filename IS NULL';
        } else if ( $filterBy === 'locked' ) {
            $havingConditions[] = 'locked IS NOT NULL';
        } else if ( $filterBy === 'unlocked' ) {
            $havingConditions[] = 'locked IS NULL';
        }

        // Hide locked files in other filters if hide_locked is true
        if ( $hide_locked && $filterBy !== 'locked' && $filterBy !== 'unlocked' ) {
            $havingConditions[] = 'locked IS NULL';
        }

        if ( count( $havingConditions ) > 0 ) {
            $havingSql = 'HAVING ' . implode( ' AND ', $havingConditions );
        } else {
            $havingSql = '';
        }

        $orderSql = 'ORDER BY p.ID DESC';
        if ( $orderBy === 'post_title' ) {
            $orderSql = 'ORDER BY post_title ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
        } else if ( $orderBy === 'post_parent' ) {
            $orderSql = 'ORDER BY post_parent ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
        } else if ( $orderBy === 'current_filename' ) {
            $orderSql = 'ORDER BY current_filename ' . ( $order === 'asc' ? 'ASC' : 'DESC' );
        }

        $whereSql = '';
        if ( $search ) {
            $searchValue = '%' . $wpdb->esc_like( $search ) . '%';
            $whereSql = $wpdb->prepare( "AND (p.post_title LIKE %s OR pm.meta_value LIKE %s)", $searchValue, $searchValue );
        }

        $innerJoinCondition = '';
        if ( $this->core->featured_only ) {
            $innerJoinCondition = "INNER JOIN $wpdb->postmeta pmm ON pmm.meta_value = p.ID AND pmm.meta_key = '_thumbnail_id'";
        } else {
            if ( $this->core->images_only ) {
                $images_mime_types = implode( "','", $this->core->images_mime_types );
                $whereSql .= "$whereSql AND p.post_mime_type IN ('$images_mime_types')";
            }
        }

        // Post type filter - join with parent post to filter by its type
        $postTypeJoinSql = '';
        $postTypeWhereSql = '';
        if ( !empty( $postType ) ) {
            $postTypeJoinSql = "INNER JOIN $wpdb->posts parent_post ON parent_post.ID = p.post_parent";
            $postTypeWhereSql = $wpdb->prepare( "AND parent_post.post_type = %s", $postType );
        }

        $request = $wpdb->prepare( "
            SELECT p.ID, p.post_title, p.post_parent, p.post_content AS image_description, p.post_excerpt AS image_caption,
                MAX(CASE WHEN pm.meta_key = '_wp_attached_file' THEN pm.meta_value END) AS current_filename,
                MAX(CASE WHEN pm.meta_key = '_original_filename' THEN pm.meta_value END) AS original_filename,
                MAX(CASE WHEN pm.meta_key = '_wp_attachment_metadata' THEN pm.meta_value END) AS metadata,
                MAX(CASE WHEN pm.meta_key = '_wp_attachment_image_alt' THEN pm.meta_value END) AS image_alt,
                MAX(CASE WHEN pm.meta_key = '_require_file_renaming' THEN pm.meta_value END) AS pending,
                MAX(CASE WHEN pm.meta_key = '_manual_file_renaming' THEN pm.meta_value END) AS locked,
                MAX(CASE WHEN pm.meta_key = '_mfrh_history' THEN pm.meta_value END) AS history,
                MAX(CASE WHEN pm.meta_key = '_mfrh_proposed_filename' THEN pm.meta_value END) AS cached_proposed_filename,
                MAX(CASE WHEN pm.meta_key = '_mfrh_used_method' THEN pm.meta_value END) AS cached_used_method,
                MAX(CASE WHEN pm.meta_key = '_mfrh_ideal_filename' THEN pm.meta_value END) AS cached_ideal_filename,
                MAX(CASE WHEN pm.meta_key = '_mfrh_proposed_filename_exists' THEN pm.meta_value END) AS cached_proposed_filename_exists
            FROM (
                SELECT p.ID,
                    MAX(CASE WHEN pm.meta_key = '_original_filename' THEN pm.meta_value END) AS original_filename,
                    MAX(CASE WHEN pm.meta_key = '_require_file_renaming' THEN pm.meta_value END) AS pending,
                    MAX(CASE WHEN pm.meta_key = '_manual_file_renaming' THEN pm.meta_value END) AS locked
                FROM $wpdb->posts p
                $innerJoinCondition
                $postTypeJoinSql
                JOIN $wpdb->postmeta pm ON pm.post_id = p.ID
                WHERE p.post_type = 'attachment'
                    AND p.post_status = 'inherit'
                    $whereSql
                    $postTypeWhereSql
                GROUP BY p.ID
                $havingSql
            ) AS filtered_posts
            JOIN $wpdb->posts p ON p.ID = filtered_posts.ID
            JOIN $wpdb->postmeta pm ON pm.post_id = p.ID
            WHERE (pm.meta_key = '_wp_attached_file'
                    OR pm.meta_key = '_original_filename'
                    OR pm.meta_key = '_wp_attachment_metadata'
                    OR pm.meta_key = '_wp_attachment_image_alt'
                    OR pm.meta_key = '_require_file_renaming'
                    OR pm.meta_key = '_manual_file_renaming'
                    OR pm.meta_key = '_mfrh_history'
                    OR pm.meta_key = '_mfrh_proposed_filename'
                    OR pm.meta_key = '_mfrh_used_method'
                    OR pm.meta_key = '_mfrh_ideal_filename'
                    OR pm.meta_key = '_mfrh_proposed_filename_exists')
            GROUP BY p.ID
            $orderSql
            LIMIT %d, %d
        ", $skip, $limit );

        $entries = $wpdb->get_results( $request );

        foreach ( $entries as $entry ) {
            $this->core->consolidate_media_status( $entry );
            if ( !is_null( $entry->history ) ) {
                $entry->history = unserialize( $entry->history );
            }
        }

        return $entries;
    }

	function rest_media_id( $request ) {
		$filterBy = trim( $request->get_param( 'filterBy' ) );
		$skip     = trim( $request->get_param( 'skip' ) );
		$limit    = trim( $request->get_param( 'limit' ) );
		$search   = trim( $request->get_param( 'search' ) );
	
		$hide_locked = $this->core->get_option( 'hide_locked', true );
	
		$entries = $this->get_media_ids( $skip, $limit, $filterBy, $search, $hide_locked );
		return new WP_REST_Response( [ 'success' => true, 'data' => $entries ], 200 );
	}

	function rest_media( $request ) {
		$limit = trim( $request->get_param('limit') );
		$skip = trim( $request->get_param('skip') );
		$filterBy = trim( $request->get_param('filterBy') );
		$orderBy = trim( $request->get_param('orderBy') );
		$order = trim( $request->get_param('order') );
		$search = trim( $request->get_param('search') );
		$postType = trim( $request->get_param('postType') );

		$hide_locked = $this->core->get_option( 'hide_locked', true );

		$entries = $this->get_media_status( $skip, $limit, $filterBy, $orderBy, $order, $search, $hide_locked, $postType );
		$total = $this->count_by_filter( $filterBy, $search, $hide_locked, $postType );

		return new WP_REST_Response( [ 'success' => true, 'data' => $entries, 'total' => $total ], 200 );
	}

	function rest_all_settings() {
		return new WP_REST_Response( [ 'success' => true, 'data' => $this->core->get_all_options() ], 200 );
	}

	function rest_reset_options() {
		$this->core->reset_options();
		return new WP_REST_Response( [ 'success' => true, 'options' => $this->core->get_all_options() ], 200 );
	}

	function rest_reset_metadata() {
		$this->core->reset_metadata();
		return new WP_REST_Response( [ 'success' => true ], 200 );
	}

	function rest_update_option( $request ) {
		try {
			$params = $request->get_json_params();
			$value = $params['options'];
			list( $options, $success, $message ) = $this->core->update_options( $value );
			return new WP_REST_Response([ 'success' => $success, 'message' => $message, 'options' => $options ], 200 );
		}
		catch ( Exception $e ) {
			return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
		}
	}

	function rest_toggle_parser( $request ) {
		try {
			$params = $request->get_json_params();
			$parser = $params['parser'];

			$res = $this->core->toggle_parser( $parser );
			if ( !$res ) {
				throw new Exception( __( 'The parser could not be toggled.', 'media-file-renamer' ) );
			}

			return new WP_REST_Response([ 'success' => true, 'options' => $res[0] ], 200 );
		}
		catch ( Exception $e ) {
			return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
		}
	}

	function rest_test_rules( $request ) {
		try {
			$params = $request->get_json_params();
			$filename = $params['filename'];

			$new_filename = $this->core->engine->new_filename( $filename , $filename );

			if ( !$new_filename ) {
				throw new Exception( __( 'The test could not be performed.', 'media-file-renamer' ) );
			}

			return new WP_REST_Response([ 'success' => true, 'filename' => $new_filename ], 200 );
		}
		catch ( Exception $e ) {
			return new WP_REST_Response([ 'success' => false, 'message' => $e->getMessage() ], 500 );
		}
	}

	function validate_updated_option( $option_name ) {
		$needsCheckingOptions = [
			'mfrh_auto_rename',
			'mfrh_sync_alt',
			'mfrh_sync_media_title',
			'mfrh_force_rename',
			'mfrh_unique_files'
		];
		if ( !in_array( $option_name, $needsCheckingOptions ) ) {
			return $this->createValidationResult();
		}

		if ( $option_name === 'mfrh_force_rename' || $option_name === 'mfrh_unique_files' ) {
			$force_rename = $this->core->get_option( 'force_rename', false );
			$unique_files = $this->core->get_option( 'unique_files', 'none' );

			if ( !$force_rename || !$unique_files || $unique_files === 'none' ) {
				return $this->createValidationResult();
			}

			update_option( 'mfrh_force_rename', false, false );
			return $this->createValidationResult( false, __( 'Force Rename and Numbered Files cannot be used at the same time. Please use Force Rename only when you are trying to repair a broken install. For now, Force Rename has been disabled.', 'media-file-renamer' ));

		} 
		else if ( $option_name === 'mfrh_auto_rename' || $option_name === 'mfrh_sync_alt' || 
			$option_name ==='mfrh_sync_media_title' ) {
			if ( $this->core->method !== 'alt_text' && $this->core->method !== 'media_title' ) {
				return $this->createValidationResult();
			}

			$sync_alt = $this->core->get_option( 'sync_alt' );
			if ( $sync_alt && $this->core->method === 'alt_text' ) {
				update_option( 'mfrh_sync_alt', false, false );
				return $this->createValidationResult( false, __( 'The option Sync ALT was turned off since it does not make sense to have it with this Auto-Rename mode.', 'media-file-renamer' ));
			}

			$sync_meta_title = $this->core->get_option( 'sync_media_title' );
			if ( $sync_meta_title && $this->core->method === 'media_title' ) {
				update_option( 'mfrh_sync_media_title', false, false );
				return $this->createValidationResult( false, __( 'The option Sync Media Title was turned off since it does not make sense to have it with this Auto-Rename mode.', 'media-file-renamer' ));
			}
		}
		return $this->createValidationResult();
	}

	function createValidationResult( $result = true, $message = null) {
		$message = $message ? $message : __( 'Option updated.', 'media-file-renamer' );
		return ['result' => $result, 'message' => $message];
	}

	function do_auto_attach( $post_id, $selected_media_ids ) {
		$media_ids = [];

		// If the global variable $wpmc exists, it means Media Cleaner functions are accessible
		// and we can use them to attach the media to its post.
		global $wpmc;
		if ( !empty( $wpmc ) ) {
			$references = $wpmc->get_references_for_post_id( $post_id );
			if ( !empty( $references ) ) {
				foreach ( $references as $reference ) {
					$media_ids[] = $reference['mediaId'];
				}
			}
		}

		// Otherwise, let's go through simple WordPress functions.
		if ( empty( $media_ids ) ) {
			// Featured Image
			$mediaId = get_post_thumbnail_id( $post_id );
			if ( $mediaId !== false ) {
				$media_ids[] = $mediaId;
			}
			// WooCommerce Product Gallery
			if ( get_post_type( $post_id ) === 'product' && class_exists( 'WC_product' ) ) {
				$product = new WC_product( $post_id );
				$mediaIds = $product->get_gallery_image_ids();
				if ( !empty( $mediaIds ) ) {
					foreach ( $mediaIds as $mediaId ) {
						$media_ids[] = $mediaId;
					}
				}
			}
		}

		if ( count( $selected_media_ids ) > 0 ) {
			$media_ids = array_unique( array_intersect( $media_ids, $selected_media_ids ) );
		}

		if ( empty( $media_ids ) ) {
			return;
		}

		// Attach all the Media IDs to this post.
		foreach ( $media_ids as $mediaId ) {
			$attachment = array( 'ID' => (int)$mediaId, 'post_parent' => (int)$post_id );
			wp_update_post( $attachment );
		}
	}


	/**
	 * Helper method to create REST responses with automatic token refresh
	 *
	 * @param array $data The response data
	 * @param int $status HTTP status code
	 * @return WP_REST_Response
	 */
	protected function create_rest_response($data, $status = 200)
	{
		// Always check if we need to provide a new nonce
		$current_nonce = $this->core->get_nonce( true );
		$request_nonce = isset($_SERVER['HTTP_X_WP_NONCE']) ? $_SERVER['HTTP_X_WP_NONCE'] : null;

		// Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
		// We'll refresh if the nonce is older than 10 hours to be safe
		$should_refresh = false;

		if ($request_nonce) {
			// Try to determine the age of the nonce
			// WordPress uses a tick system where each tick is 12 hours
			// If we're in the second half of the nonce's life, refresh it
			$time = time();
			$nonce_tick = wp_nonce_tick();

			// Verify if the nonce is still valid but getting old
			$verify = wp_verify_nonce($request_nonce, 'wp_rest');
			if ( $verify === 2 || $verify === false ) {
				// Nonce is valid but was generated 12-24 hours ago
				$should_refresh = true;
				// Log will be written when token is included in response
			}
		}

		// If the nonce has changed or should be refreshed, include the new one
		if ($should_refresh || ($request_nonce && $current_nonce !== $request_nonce)) {
			$data['new_token'] = $current_nonce;

			// Log if server debug mode is enabled
			$this->core->log('🔐 Provided new REST token in response.');
		}

		return new WP_REST_Response($data, $status);
	}
}

?>