<?php

if ( ! defined( 'ABSPATH' ) ) { exit; }

class AIEO_DMM_Media_Cleanup extends AIEO_DMM_Module_Base {

	const QUARANTINE_DIR   = 'aieo-quarantine';
	const STATE_TRANSIENT  = 'aieo_media_cleanup_scan';
	const CAND_FILE        = '.aieo-scan-candidates.jsonl';
	const REF_IDS_FILE     = '.aieo-ref-ids.bin';
	const REF_FILES_FILE   = '.aieo-ref-files.bin';
	const QUAR_TRANSIENT   = 'aieo_media_cleanup_quar';
	const PURGE_TRANSIENT  = 'aieo_media_cleanup_purge';
	const OP_BUDGET_SEC    = 15;    
	const OP_FILE_BUDGET   = 8000;  
	const MAX_CANDIDATES   = 3000000; 
	const STEP_FILE_BUDGET = 4000;
	const EDIT_HASH_RE     = '/-e\d{13}\./';
	const EXTS             = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif' ); 

	public function get_id()       { return 'media_cleanup'; }
	public function get_label()    { return __( 'Media Cleanup', 'ai-eshop-optimizer' ); }
	public function get_priority() { return 40; }
	public function get_group()    { return 'tools'; }

	public function register_hooks() {
		add_action( 'wp_ajax_aieo_media_scan_start', array( $this, 'ajax_scan_start' ) );
		add_action( 'wp_ajax_aieo_media_scan_step',  array( $this, 'ajax_scan_step' ) );
		add_action( 'wp_ajax_aieo_media_quarantine', array( $this, 'ajax_quarantine' ) );
		add_action( 'wp_ajax_aieo_media_purge',      array( $this, 'ajax_purge' ) );
	}

	
	
	

	private static function lc( $s ) {
		return function_exists( 'mb_strtolower' ) ? mb_strtolower( $s, 'UTF-8' ) : strtolower( $s );
	}

	
	public static function name_referenced( $bl, array $ref_files ) {
		if ( isset( $ref_files[ $bl ] ) ) { return true; }
		if ( preg_match( '/^(.+?)(?:-scaled)?-\d+x\d+(\.[a-z0-9]+)$/i', $bl, $m ) ) {
			if ( isset( $ref_files[ $m[1] . $m[2] ] ) ) { return true; }
		}
		return false;
	}

	
	public static function classify_file( $rel, $aid, array $bases, array $ref, array $reff, array $opts ) {
		$rel  = ltrim( str_replace( '\\', '/', $rel ), '/' );
		$base = basename( $rel );
		$dot  = strrpos( $base, '.' );
		if ( $dot === false ) { return array( 'cat' => 'keep' ); }
		$ext = strtolower( substr( $base, $dot + 1 ) );
		if ( ! in_array( $ext, self::EXTS, true ) ) { return array( 'cat' => 'keep' ); }
		if ( strpos( $rel, self::QUARANTINE_DIR . '/' ) === 0 || strpos( $rel, '/' . self::QUARANTINE_DIR . '/' ) !== false ) { return array( 'cat' => 'keep' ); }

		$bl = self::lc( $base );

		if ( $aid > 0 ) {
			
			if ( isset( $ref[ $aid ] ) ) { return array( 'cat' => 'keep' ); }        
			if ( self::name_referenced( $bl, $reff ) ) { return array( 'cat' => 'keep' ); } 
			if ( empty( $opts['want_unused'] ) ) { return array( 'cat' => 'keep' ); }  
			if ( strpos( $bl, 'woocommerce-placeholder' ) === 0 ) { return array( 'cat' => 'keep' ); } 
			return array( 'cat' => 'unused_attachment', 'aid' => $aid );
		}

		
		if ( preg_match( self::EDIT_HASH_RE, $base ) ) { return array( 'cat' => 'keep' ); } 
		if ( self::name_referenced( $bl, $reff ) ) { return array( 'cat' => 'keep' ); }

		$name = substr( $base, 0, $dot );
		if ( preg_match( '/^(.+)-\d+x\d+$/', $name, $m ) ) {
			$stem = $m[1];
			$dir  = ( strpos( $rel, '/' ) !== false ) ? substr( $rel, 0, strrpos( $rel, '/' ) ) : '';
			$c1   = ( $dir !== '' ? $dir . '/' : '' ) . $stem . '.' . $ext;
			$c2   = ( $dir !== '' ? $dir . '/' : '' ) . $stem . '-scaled.' . $ext;
			if ( isset( $bases[ $c1 ] ) || isset( $bases[ $c2 ] ) ) {
				return array( 'cat' => empty( $opts['want_stale'] ) ? 'keep' : 'stale_subsize' );
			}
			return array( 'cat' => empty( $opts['want_orphan'] ) ? 'keep' : 'orphan' );
		}
		return array( 'cat' => empty( $opts['want_orphan'] ) ? 'keep' : 'orphan' );
	}

	
	public static function harvest_ints( $val, array &$ref ) {
		$v = maybe_unserialize( $val );
		for ( $i = 0; $i < 3 && is_string( $v ) && preg_match( '/^(a|s|O|i|b|d):/', $v ); $i++ ) { $v = maybe_unserialize( $v ); }
		if ( is_string( $v ) ) { $j = json_decode( $v, true ); if ( is_array( $j ) ) { $v = $j; } }
		$stack = array( $v );
		while ( $stack ) {
			$x = array_pop( $stack );
			if ( is_array( $x ) ) { foreach ( $x as $e ) { $stack[] = $e; } continue; }
			if ( is_scalar( $x ) && is_numeric( $x ) ) { $id = (int) $x; if ( $id > 0 ) { $ref[ $id ] = true; } } 
		}
	}

	
	public static function raw_fallback( $raw, array &$ref, array &$reff ) {
		$s = html_entity_decode( str_replace( '\\/', '/', (string) $raw ), ENT_QUOTES );
		$s = rawurldecode( $s );
		if ( preg_match_all( '/[is]:\d+:?"?(\d+)/', $s, $m ) ) {
			foreach ( $m[1] as $d ) { $id = (int) $d; if ( $id > 0 ) { $ref[ $id ] = true; } }
		}
		self::scan_filenames( $raw, $reff );
	}

	
	public static function scan_filenames( $raw, array &$reff ) {
		$s = str_replace( '\\/', '/', (string) $raw );
		$s = html_entity_decode( $s, ENT_QUOTES );
		if ( ! preg_match_all( '#([^/\\\\\s"\'<>?]+\.(?:jpe?g|png|gif|webp|avif))#i', $s, $m ) ) { return; }
		foreach ( $m[1] as $fn ) {
			foreach ( array( $fn, rawurldecode( $fn ) ) as $cand ) {
				$b = self::lc( basename( $cand ) );
				$reff[ $b ] = true;
				if ( preg_match( '/^(.+?)(?:-scaled)?-\d+x\d+(\.[a-z0-9]+)$/i', $b, $mm ) ) { $reff[ $mm[1] . $mm[2] ] = true; }
			}
		}
	}

	
	public static function walk_options( $v, array &$ref, array &$reff, $key = '' ) {
		if ( is_string( $v ) ) {
			$u = maybe_unserialize( $v );
			if ( $u !== $v && ( is_array( $u ) || is_object( $u ) ) ) { self::walk_options( $u, $ref, $reff, $key ); return; }
			$j = json_decode( $v, true );
			if ( is_array( $j ) ) { self::walk_options( $j, $ref, $reff, $key ); }
			self::scan_filenames( $v, $reff );
			if ( is_numeric( $v ) && preg_match( '/image|logo|icon|thumb|favicon|attachment|media|header_image|background/i', (string) $key ) ) {
				$id = (int) $v; if ( $id > 0 ) { $ref[ $id ] = true; }
			}
			return;
		}
		if ( is_array( $v ) || is_object( $v ) ) {
			foreach ( (array) $v as $k => $e ) {
				if ( is_scalar( $e ) && is_numeric( $e ) && preg_match( '/image|logo|icon|thumb|favicon|attachment|media|header_image|background/i', (string) $k ) ) {
					$id = (int) $e; if ( $id > 0 ) { $ref[ $id ] = true; }
				}
				self::walk_options( $e, $ref, $reff, (string) $k );
			}
		}
	}

	
	
	

	public function build_reference_sets() {
		global $wpdb;
		$ref = array(); $reff = array();
		$PM = $wpdb->postmeta; $TM = $wpdb->termmeta; $P = $wpdb->posts; $OPT = $wpdb->options;

		$int_keys  = apply_filters( 'aieo_media_ref_int_postmeta_keys',  array( '_thumbnail_id', '_rpwc2', '_used_by', '_botiga_brand_image_id' ) );
		$csv_keys  = apply_filters( 'aieo_media_ref_csv_postmeta_keys',  array( '_product_image_gallery' ) );
		$ser_keys  = apply_filters( 'aieo_media_ref_serialized_postmeta_keys', array( 'botiga_variation_gallery', '_botiga_videos_in_gallery', '_merchant_videos_in_gallery', '_aieo_variation_gallery_videos', 'selected_images_categories', '_megamenu', 'woocommerce_shop_look_products', 'related_brands', '_pods_related_brands', 'related_product_categories', '_pods_related_product_categories', 'iconic-cffv-field-data' ) );
		$term_keys = apply_filters( 'aieo_media_ref_int_termmeta_keys', array( '_aieo_swatch_image_id', 'product_attribute_image', 'thumbnail_id' ) );

		
		$pfld = $wpdb->prefix . 'pods_fields';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pfld ) ) === $pfld ) {
			foreach ( (array) $wpdb->get_col( "SELECT name FROM {$pfld} WHERE type='pick' AND (pick_object='media' OR pick_object LIKE 'post_type-attachment')" ) as $nm ) {
				$nm = (string) $nm; if ( $nm !== '' ) { $ser_keys[] = $nm; $ser_keys[] = '_pods_' . $nm; }
			}
			$ser_keys = array_values( array_unique( $ser_keys ) );
		}

		$maxpm = (int) $wpdb->get_var( "SELECT MAX(meta_id) FROM {$PM}" );

		
		if ( $int_keys ) {
			$in = implode( ',', array_fill( 0, count( $int_keys ), '%s' ) );
			for ( $lo = 0; $lo < $maxpm; $lo += 50000 ) {
				$sql = $wpdb->prepare( "SELECT meta_value FROM {$PM} WHERE meta_id>%d AND meta_id<=%d AND meta_key IN($in)", array_merge( array( $lo, $lo + 50000 ), $int_keys ) );
				foreach ( (array) $wpdb->get_col( $sql ) as $v ) { if ( ctype_digit( (string) $v ) ) { $id = (int) $v; if ( $id > 0 ) { $ref[ $id ] = true; } } }
			}
		}
		
		foreach ( $csv_keys as $k ) {
			for ( $lo = 0; $lo < $maxpm; $lo += 50000 ) {
				foreach ( (array) $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$PM} WHERE meta_id>%d AND meta_id<=%d AND meta_key=%s AND meta_value<>''", $lo, $lo + 50000, $k ) ) as $csv ) {
					foreach ( explode( ',', $csv ) as $t ) { $id = (int) trim( $t ); if ( $id > 0 ) { $ref[ $id ] = true; } }
				}
			}
		}
		
		if ( $ser_keys ) {
			$sin = implode( ',', array_fill( 0, count( $ser_keys ), '%s' ) );
			for ( $lo = 0; $lo < $maxpm; $lo += 50000 ) {
				$sql = $wpdb->prepare( "SELECT meta_value FROM {$PM} WHERE meta_id>%d AND meta_id<=%d AND meta_key IN($sin)", array_merge( array( $lo, $lo + 50000 ), $ser_keys ) );
				foreach ( (array) $wpdb->get_col( $sql ) as $blob ) {
					if ( $blob === '' || strpbrk( $blob, '0123456789' ) === false ) { continue; }
					$u = maybe_unserialize( $blob );
					if ( $u === false && $blob !== 'b:0;' ) { self::raw_fallback( $blob, $ref, $reff ); continue; }
					self::harvest_ints( $blob, $ref );
				}
			}
		}
		
		if ( $term_keys ) {
			$tin = implode( ',', array_fill( 0, count( $term_keys ), '%s' ) );
			foreach ( (array) $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$TM} WHERE meta_key IN($tin)", $term_keys ) ) as $v ) {
				if ( ctype_digit( (string) $v ) ) { $id = (int) $v; if ( $id > 0 ) { $ref[ $id ] = true; } }
			}
		}
		
		foreach ( (array) $wpdb->get_col( "SELECT meta_value FROM {$TM} WHERE meta_key LIKE 'woocommerce_advanced_categories%image'" ) as $blob ) {
			$a = maybe_unserialize( $blob );
			if ( is_array( $a ) ) {
				if ( ! empty( $a['id'] ) ) { $id = (int) $a['id']; if ( $id > 0 ) { $ref[ $id ] = true; } }
				if ( ! empty( $a['url'] ) ) { self::scan_filenames( $a['url'], $reff ); }
			} else { self::raw_fallback( $blob, $ref, $reff ); }
		}
		
		foreach ( array( 'site_icon', 'site_logo', 'woocommerce_placeholder_image' ) as $o ) { $id = (int) get_option( $o ); if ( $id > 0 ) { $ref[ $id ] = true; } }
		$atd = maybe_unserialize( get_option( 'woocommerce_attribute_image_term_data' ) );
		if ( is_array( $atd ) ) { foreach ( $atd as $e ) { if ( is_array( $e ) && isset( $e['thumbnail'] ) ) { $id = (int) $e['thumbnail']; if ( $id > 0 ) { $ref[ $id ] = true; } } } }
		foreach ( (array) $wpdb->get_results( "SELECT option_name,option_value FROM {$OPT} WHERE option_name LIKE 'theme_mods_%' OR option_name LIKE 'widget_%' OR option_name='woocommerce_email_header_image' OR option_name LIKE '%botiga%' OR option_name='nav_menu_options'" ) as $r ) {
			self::walk_options( $r->option_value, $ref, $reff, $r->option_name );
		}
		
		$be = $wpdb->prefix . 'aieo_dmm_brand_extras';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $be ) ) === $be ) {
			$cols = (array) $wpdb->get_col( "SHOW COLUMNS FROM {$be}" );
			if ( in_array( 'logo_image_id', $cols, true ) ) {
				foreach ( (array) $wpdb->get_results( "SELECT * FROM {$be}" ) as $r ) {
					foreach ( array( 'logo_image_id', 'hero_image_id' ) as $c ) { if ( isset( $r->$c ) ) { $id = (int) $r->$c; if ( $id > 0 ) { $ref[ $id ] = true; } } }
					if ( ! empty( $r->gallery_image_ids ) ) {
						$g = json_decode( $r->gallery_image_ids, true );
						if ( ! is_array( $g ) ) { $g = preg_split( '/\D+/', $r->gallery_image_ids ); }
						foreach ( (array) $g as $x ) { $id = (int) $x; if ( $id > 0 ) { $ref[ $id ] = true; } }
					}
				}
			}
		}
		
		$pr = $wpdb->prefix . 'podsrel';
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $pr ) ) === $pr ) {
			foreach ( (array) $wpdb->get_col( "SELECT DISTINCT r.related_item_id FROM {$pr} r JOIN {$P} p ON p.ID=r.related_item_id AND p.post_type='attachment' WHERE r.related_item_id>0" ) as $v ) { $id = (int) $v; if ( $id > 0 ) { $ref[ $id ] = true; } }
		}
		
		$idres = array( '/wp-image-(\d+)/', '/"id"\s*:\s*(\d+)/', '/"ids"\s*:\s*\[([0-9,\s]+)\]/', '/ids="([0-9,]+)"/', '/\[caption[^\]]*attachment_(\d+)/' );
		$maxp = (int) $wpdb->get_var( "SELECT MAX(ID) FROM {$P}" );
		for ( $lo = 0; $lo < $maxp; $lo += 2000 ) {
			foreach ( (array) $wpdb->get_results( $wpdb->prepare( "SELECT post_content,post_excerpt FROM {$P} WHERE ID>%d AND ID<=%d AND post_status<>'auto-draft'", $lo, $lo + 2000 ) ) as $p ) {
				foreach ( array( $p->post_content, $p->post_excerpt ) as $blob ) {
					$blob = str_replace( '\\/', '/', (string) $blob );
					foreach ( $idres as $re ) { if ( preg_match_all( $re, $blob, $m ) ) { foreach ( $m[1] as $g ) { foreach ( preg_split( '/[,\s]+/', $g ) as $d ) { $id = (int) $d; if ( $id > 0 ) { $ref[ $id ] = true; } } } } }
				}
			}
		}
		
		$this->scan_col_filenames( $P,             'ID',       'post_content', 2000, $reff );
		$this->scan_col_filenames( $P,             'ID',       'post_excerpt', 2000, $reff );
		$this->scan_col_filenames( $PM,            'meta_id',  'meta_value',   5000, $reff );
		$this->scan_col_filenames( $OPT,           'option_id','option_value', 2000, $reff );
		$this->scan_col_filenames( $wpdb->usermeta,'umeta_id', 'meta_value',   5000, $reff );

		
		foreach ( (array) $wpdb->get_col( "SELECT a.ID FROM {$P} a JOIN {$P} p ON p.ID=a.post_parent WHERE a.post_type='attachment' AND a.post_parent>0" ) as $v ) { $id = (int) $v; if ( $id > 0 ) { $ref[ $id ] = true; } }

		return array( $ref, $reff );
	}

	private function scan_col_filenames( $table, $idc, $vc, $chunk, array &$reff ) {
		global $wpdb;
		$mx = (int) $wpdb->get_var( "SELECT MAX({$idc}) FROM {$table}" );
		$like = " AND ({$vc} LIKE '%uploads%' OR {$vc} LIKE '%wp-image-%' OR {$vc} LIKE '%wp-content%')";
		for ( $lo = 0; $lo < $mx; $lo += $chunk ) {
			$sql = $wpdb->prepare( "SELECT {$vc} FROM {$table} WHERE {$idc}>%d AND {$idc}<=%d", $lo, $lo + $chunk ) . $like;
			foreach ( (array) $wpdb->get_col( $sql ) as $v ) { self::scan_filenames( $v, $reff ); }
		}
	}

	
	
	

	private function index_rows( $rows, array &$protected, array &$bases, array &$path_aid ) {
		foreach ( (array) $rows as $row ) {
			$aid = (int) $row['id'];
			$af  = ltrim( str_replace( '\\', '/', (string) $row['af'] ), '/' );
			if ( $af === '' ) { continue; }
			$protected[ $af ] = true; $bases[ $af ] = true; if ( $aid ) { $path_aid[ $af ] = $aid; }
			$dir = ( strpos( $af, '/' ) !== false ) ? substr( $af, 0, strrpos( $af, '/' ) ) : '';

			$md = $row['md'] ? maybe_unserialize( $row['md'] ) : null;
			if ( ! is_array( $md ) ) { continue; }
			if ( ! empty( $md['file'] ) ) {
				$mf = str_replace( '\\', '/', (string) $md['file'] );
				$mainrel = ( strpos( $mf, '/' ) !== false ) ? ltrim( $mf, '/' ) : ( $dir !== '' ? $dir . '/' . $mf : $mf );
				$protected[ $mainrel ] = true; $bases[ $mainrel ] = true; if ( $aid ) { $path_aid[ $mainrel ] = $aid; }
				$dir = ( strpos( $mainrel, '/' ) !== false ) ? substr( $mainrel, 0, strrpos( $mainrel, '/' ) ) : '';
			}
			if ( ! empty( $md['original_image'] ) ) {
				$orel = ( $dir !== '' ? $dir . '/' : '' ) . basename( (string) $md['original_image'] );
				$protected[ $orel ] = true; $bases[ $orel ] = true; if ( $aid ) { $path_aid[ $orel ] = $aid; }
			}
			if ( ! empty( $md['sizes'] ) && is_array( $md['sizes'] ) ) {
				foreach ( $md['sizes'] as $size ) {
					if ( empty( $size['file'] ) ) { continue; }
					$srel = ( $dir !== '' ? $dir . '/' : '' ) . basename( (string) $size['file'] );
					$protected[ $srel ] = true; if ( $aid ) { $path_aid[ $srel ] = $aid; }
				}
			}
		}
	}

	private function build_index() {
		global $wpdb;
		$protected = array(); $bases = array(); $path_aid = array();
		$rows = $wpdb->get_results(
			"SELECT p.ID AS id, mf.meta_value AS af, mm.meta_value AS md
			 FROM {$wpdb->posts} p
			 JOIN {$wpdb->postmeta} mf ON mf.post_id = p.ID AND mf.meta_key = '_wp_attached_file'
			 LEFT JOIN {$wpdb->postmeta} mm ON mm.post_id = p.ID AND mm.meta_key = '_wp_attachment_metadata'
			 WHERE p.post_type = 'attachment'", ARRAY_A );
		$this->index_rows( $rows, $protected, $bases, $path_aid );
		return array( $protected, $bases, $path_aid );
	}

	private function build_bucket_index( $bucket ) {
		global $wpdb;
		$protected = array(); $bases = array(); $path_aid = array();
		$sql = "SELECT p.ID AS id, mf.meta_value AS af, mm.meta_value AS md
			 FROM {$wpdb->posts} p
			 JOIN {$wpdb->postmeta} mf ON mf.post_id = p.ID AND mf.meta_key = '_wp_attached_file'
			 LEFT JOIN {$wpdb->postmeta} mm ON mm.post_id = p.ID AND mm.meta_key = '_wp_attachment_metadata'
			 WHERE p.post_type = 'attachment' AND ";
		if ( $bucket === '' ) {
			$rows = $wpdb->get_results( $sql . "mf.meta_value NOT LIKE '%/%'", ARRAY_A );
		} else {
			$rows = $wpdb->get_results( $wpdb->prepare( $sql . 'mf.meta_value LIKE %s AND mf.meta_value NOT LIKE %s', $wpdb->esc_like( $bucket ) . '/%', $wpdb->esc_like( $bucket ) . '/%/%' ), ARRAY_A );
		}
		$this->index_rows( $rows, $protected, $bases, $path_aid );
		return array( $protected, $bases, $path_aid );
	}

	
	private function enumerate_buckets() {
		global $wpdb;
		$basedir = self::basedir();
		$set = array( '' => true );
		foreach ( (array) $wpdb->get_col( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE meta_key='_wp_attached_file'" ) as $af ) {
			$af = str_replace( '\\', '/', (string) $af );
			$d  = ( strpos( $af, '/' ) !== false ) ? substr( $af, 0, strrpos( $af, '/' ) ) : '';
			$set[ $d ] = true;
		}
		if ( is_dir( $basedir ) ) {
			$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $basedir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST );
			foreach ( $it as $f ) {
				if ( ! $f->isDir() ) { continue; }
				$rel = substr( str_replace( '\\', '/', $f->getPathname() ), strlen( $basedir ) + 1 );
				if ( strpos( $rel, self::QUARANTINE_DIR ) === 0 ) { continue; }
				$set[ $rel ] = true;
			}
		}
		return array_keys( $set );
	}

	
	
	

	private static function basedir() {
		$u = wp_upload_dir();
		return rtrim( str_replace( '\\', '/', $u['basedir'] ), '/' );
	}
	private static function cand_path()     { return self::basedir() . '/' . self::QUARANTINE_DIR . '/' . self::CAND_FILE; }
	private static function ref_ids_path()  { return self::basedir() . '/' . self::QUARANTINE_DIR . '/' . self::REF_IDS_FILE; }
	private static function ref_files_path(){ return self::basedir() . '/' . self::QUARANTINE_DIR . '/' . self::REF_FILES_FILE; }

	private function persist_reference_sets( $ref, $reff ) {
		wp_mkdir_p( self::basedir() . '/' . self::QUARANTINE_DIR );
		@file_put_contents( self::ref_ids_path(), serialize( $ref ) );
		@file_put_contents( self::ref_files_path(), serialize( $reff ) );
	}
	private function load_reference_sets() {
		$r = @file_get_contents( self::ref_ids_path() ); $rf = @file_get_contents( self::ref_files_path() );
		$ref = ( $r !== false ) ? @unserialize( $r ) : array(); $reff = ( $rf !== false ) ? @unserialize( $rf ) : array();
		return array( is_array( $ref ) ? $ref : array(), is_array( $reff ) ? $reff : array() );
	}

	
	
	

	private function scan_slice( $bucket, $offset, $budget, array $opts, array &$state, $fh, array &$ref, array &$reff ) {
		$basedir = self::basedir();
		$dir = ( $bucket === '' ) ? $basedir : $basedir . '/' . $bucket;
		if ( ! is_dir( $dir ) ) { return array( 0, 0, 0, true ); }
		$entries = @scandir( $dir );
		if ( ! is_array( $entries ) ) { return array( 0, 0, 0, true ); }
		$entries = array_values( array_filter( $entries, function ( $e ) { return $e !== '.' && $e !== '..'; } ) );
		$total = count( $entries );

		list( $protected, $bases, $path_aid ) = $this->build_bucket_index( $bucket );

		$i = (int) $offset; $processed = 0;
		for ( ; $i < $total && $processed < $budget; $i++ ) {
			$e = $entries[ $i ]; $abs = $dir . '/' . $e; $processed++;
			if ( ! is_file( $abs ) ) { continue; }
			$rel = ( $bucket === '' ) ? $e : $bucket . '/' . $e;
			if ( strpos( $rel, self::QUARANTINE_DIR . '/' ) === 0 ) { continue; }

			$state['scanned']++;
			$aid = isset( $path_aid[ $rel ] ) ? (int) $path_aid[ $rel ] : 0;
			$res = self::classify_file( $rel, $aid, $bases, $ref, $reff, $opts );
			$cat = $res['cat'];
			if ( $cat === 'keep' ) { continue; }
			if ( ( $state['orphan_count'] + $state['stale_count'] + $state['unused_count'] ) >= self::MAX_CANDIDATES ) { $state['truncated'] = true; break; }

			$sz = (int) @filesize( $abs );
			$rec = array( 'p' => $rel, 'b' => $sz, 'c' => $cat );
			if ( $cat === 'unused_attachment' ) { $rec['aid'] = isset( $res['aid'] ) ? (int) $res['aid'] : 0; }
			if ( $fh ) { fwrite( $fh, wp_json_encode( $rec ) . "\n" ); }

			if ( $cat === 'orphan' ) { $state['orphan_count']++; $state['orphan_bytes'] += $sz; }
			elseif ( $cat === 'stale_subsize' ) { $state['stale_count']++; $state['stale_bytes'] += $sz; }
			else { $state['unused_count']++; $state['unused_bytes'] += $sz; }
		}
		return array( $processed, $total, $i, ( $i >= $total ) );
	}

	public function ajax_scan_start() {
		$this->guard();
		@set_time_limit( 0 );
		$opts = array(
			'want_orphan' => ! empty( $_POST['want_orphan'] ),
			'want_stale'  => ! empty( $_POST['want_stale'] ),
			'want_unused' => ! empty( $_POST['want_unused'] ),
		);
		wp_mkdir_p( self::basedir() . '/' . self::QUARANTINE_DIR );
		@file_put_contents( self::cand_path(), '' );

		
		list( $ref, $reff ) = $this->build_reference_sets();
		$this->persist_reference_sets( $ref, $reff );

		$buckets = $this->enumerate_buckets();
		$state = array(
			'buckets' => $buckets, 'cursor' => 0, 'file_offset' => 0, 'opts' => $opts,
			'scanned' => 0, 'orphan_count' => 0, 'stale_count' => 0, 'unused_count' => 0,
			'orphan_bytes' => 0, 'stale_bytes' => 0, 'unused_bytes' => 0, 'truncated' => false,
		);
		set_transient( self::STATE_TRANSIENT, $state, 2 * HOUR_IN_SECONDS );
		wp_send_json_success( array( 'total' => count( $buckets ), 'ref_ids' => count( $ref ), 'ref_files' => count( $reff ) ) );
	}

	public function ajax_scan_step() {
		$this->guard();
		$state = get_transient( self::STATE_TRANSIENT );
		if ( ! is_array( $state ) || ! isset( $state['buckets'] ) ) {
			wp_send_json_error( array( 'message' => __( 'Scan not started — press Scan again.', 'ai-eshop-optimizer' ) ) );
		}
		@set_time_limit( 0 );
		list( $ref, $reff ) = $this->load_reference_sets();

		$total = count( $state['buckets'] );
		$fh = fopen( self::cand_path(), 'a' );
		$done_files = 0; $cur_count = 0;
		while ( $state['cursor'] < $total && $done_files < self::STEP_FILE_BUDGET && empty( $state['truncated'] ) ) {
			list( $proc, $entry_count, $next_off, $exhausted ) = $this->scan_slice( $state['buckets'][ $state['cursor'] ], $state['file_offset'], self::STEP_FILE_BUDGET - $done_files, $state['opts'], $state, $fh, $ref, $reff );
			$done_files += $proc; $cur_count = $entry_count;
			if ( $exhausted ) { $state['cursor']++; $state['file_offset'] = 0; }
			else { $state['file_offset'] = $next_off; break; }
		}
		if ( $fh ) { fclose( $fh ); }

		$done = ( $state['cursor'] >= $total ) || ! empty( $state['truncated'] );
		$position = $done ? $total : ( $state['cursor'] + ( $cur_count > 0 ? ( $state['file_offset'] / $cur_count ) : 0 ) );
		set_transient( self::STATE_TRANSIENT, $state, 2 * HOUR_IN_SECONDS );

		$resp = array(
			'done' => $done, 'position' => round( $position, 3 ), 'total' => $total, 'folder' => (int) $state['cursor'],
			'scanned' => (int) $state['scanned'],
			'orphan_count' => (int) $state['orphan_count'], 'stale_count' => (int) $state['stale_count'], 'unused_count' => (int) $state['unused_count'],
			'orphan_mb' => round( $state['orphan_bytes'] / 1048576, 1 ), 'stale_mb' => round( $state['stale_bytes'] / 1048576, 1 ), 'unused_mb' => round( $state['unused_bytes'] / 1048576, 1 ),
			'truncated' => ! empty( $state['truncated'] ),
		);
		if ( $done ) {
			$resp['sample'] = $this->read_sample( 120 );
			$resp['unused_attachments'] = $this->count_distinct_unused_aids();
		}
		wp_send_json_success( $resp );
	}

	private function read_sample( $n ) {
		$out = array(); $path = self::cand_path();
		if ( ! is_readable( $path ) ) { return $out; }
		$fh = fopen( $path, 'r' );
		while ( count( $out ) < $n && ( $line = fgets( $fh ) ) !== false ) {
			$c = json_decode( trim( $line ), true );
			if ( is_array( $c ) && ! empty( $c['p'] ) ) { $out[] = array( 'path' => $c['p'], 'cat' => isset( $c['c'] ) ? $c['c'] : 'orphan' ); }
		}
		fclose( $fh );
		return $out;
	}

	private function count_distinct_unused_aids() {
		$path = self::cand_path(); if ( ! is_readable( $path ) ) { return 0; }
		$seen = array(); $fh = fopen( $path, 'r' );
		while ( ( $line = fgets( $fh ) ) !== false ) {
			$c = json_decode( trim( $line ), true );
			if ( is_array( $c ) && isset( $c['aid'] ) && (int) $c['aid'] > 0 ) { $seen[ (int) $c['aid'] ] = true; }
		}
		fclose( $fh );
		return count( $seen );
	}

	
	
	

	private function guard() {
		AIEO_DMM::verify_ajax_request();
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Site administrators only.', 'ai-eshop-optimizer' ) ) );
		}
	}

	
	public function ajax_quarantine() {
		$this->guard();
		@set_time_limit( 0 );
		$candpath = self::cand_path();
		$st = get_transient( self::QUAR_TRANSIENT );

		if ( ! is_array( $st ) ) {
			if ( ! is_readable( $candpath ) ) {
				wp_send_json_error( array( 'message' => __( 'No scan result — run a scan first.', 'ai-eshop-optimizer' ) ) );
			}
			list( $ref, $reff ) = $this->build_reference_sets();
			$this->persist_reference_sets( $ref, $reff );
			$stamp = gmdate( 'Ymd-His', time() );
			wp_mkdir_p( self::basedir() . '/' . self::QUARANTINE_DIR . '/' . $stamp );
			$st = array( 'stamp' => $stamp, 'offset' => 0, 'moved' => 0, 'skipped' => 0, 'freed' => 0, 'total' => (int) @filesize( $candpath ) );
		}

		list( $ref, $reff ) = $this->load_reference_sets();
		$basedir = self::basedir();
		$qroot   = $basedir . '/' . self::QUARANTINE_DIR . '/' . $st['stamp'];
		$fh = fopen( $candpath, 'r' );
		if ( (int) $st['offset'] > 0 ) { fseek( $fh, (int) $st['offset'] ); }

		$made = array(); $mfh = null; $n = 0; $start = microtime( true );
		while ( $n < self::OP_FILE_BUDGET && ( microtime( true ) - $start ) < self::OP_BUDGET_SEC && ( $line = fgets( $fh ) ) !== false ) {
			$c = json_decode( trim( $line ), true );
			if ( ! is_array( $c ) || empty( $c['p'] ) ) { continue; }
			$n++;
			$rel = ltrim( str_replace( '\\', '/', (string) $c['p'] ), '/' );
			$cat = isset( $c['c'] ) ? $c['c'] : 'orphan';
			$aid = isset( $c['aid'] ) ? (int) $c['aid'] : 0;
			$src = $basedir . '/' . $rel;
			$real = realpath( $src );
			if ( ! $real || strpos( str_replace( '\\', '/', $real ), $basedir . '/' ) !== 0 ) { $st['skipped']++; continue; }

			
			$bl = self::lc( basename( $rel ) );
			if ( self::name_referenced( $bl, $reff ) || ( $cat === 'unused_attachment' && isset( $ref[ $aid ] ) ) ) { $st['skipped']++; continue; }

			$dest = $qroot . '/' . $rel;
			$ddir = dirname( $dest );
			if ( ! isset( $made[ $ddir ] ) ) { wp_mkdir_p( $ddir ); $made[ $ddir ] = true; }
			$size = (int) @filesize( $src );
			if ( @rename( $src, $dest ) ) {
				$st['moved']++; $st['freed'] += $size;
				if ( $cat === 'unused_attachment' && $aid > 0 ) {
					if ( ! $mfh ) { $mfh = fopen( $qroot . '/manifest.jsonl', 'a' ); }
					fwrite( $mfh, $aid . "\n" );
				}
			} else { $st['skipped']++; }
		}
		$st['offset'] = ftell( $fh );
		$eof = feof( $fh );
		fclose( $fh );
		if ( $mfh ) { fclose( $mfh ); }

		if ( $eof ) {
			@unlink( $candpath ); @unlink( self::ref_ids_path() ); @unlink( self::ref_files_path() );
			delete_transient( self::STATE_TRANSIENT ); delete_transient( self::QUAR_TRANSIENT );
			wp_send_json_success( array( 'done' => true, 'moved' => $st['moved'], 'skipped' => $st['skipped'], 'freed_mb' => round( $st['freed'] / 1048576, 1 ), 'records' => $this->count_manifest_records( $qroot . '/manifest.jsonl' ), 'folder' => self::QUARANTINE_DIR . '/' . $st['stamp'] ) );
		}
		set_transient( self::QUAR_TRANSIENT, $st, 2 * HOUR_IN_SECONDS );
		wp_send_json_success( array( 'done' => false, 'progress' => $st['total'] ? min( 99, (int) round( $st['offset'] / $st['total'] * 100 ) ) : 0, 'moved' => $st['moved'], 'freed_mb' => round( $st['freed'] / 1048576, 1 ) ) );
	}

	private function count_manifest_records( $path ) {
		if ( ! is_readable( $path ) ) { return 0; }
		$seen = array(); $fh = fopen( $path, 'r' );
		while ( ( $l = fgets( $fh ) ) !== false ) { $a = (int) trim( $l ); if ( $a > 0 ) { $seen[ $a ] = true; } }
		fclose( $fh );
		return count( $seen );
	}

	
	public function ajax_purge() {
		$this->guard();
		@set_time_limit( 0 );
		$basedir = self::basedir();
		$qdir = $basedir . '/' . self::QUARANTINE_DIR;
		$real = realpath( $qdir );
		if ( ! $real || str_replace( '\\', '/', $real ) !== $qdir ) {
			wp_send_json_error( array( 'message' => __( 'Quarantine folder not found.', 'ai-eshop-optimizer' ) ) );
		}
		$st = get_transient( self::PURGE_TRANSIENT );
		if ( ! is_array( $st ) ) {
			$adb = ! empty( $_POST['also_db'] );
			$st = array( 'phase' => $adb ? 'db' : 'files', 'aids' => null, 'db_off' => 0, 'records' => 0, 'freed' => 0 );
		}
		$start = microtime( true );

		global $wpdb;

		
		
		
		
		
		if ( $st['phase'] === 'db' ) {
			if ( $st['aids'] === null ) {
				$aids = array();
				foreach ( (array) glob( $qdir . '/*/manifest.jsonl' ) as $mf ) {
					$mh = fopen( $mf, 'r' );
					while ( ( $l = fgets( $mh ) ) !== false ) { $a = (int) trim( $l ); if ( $a > 0 ) { $aids[ $a ] = true; } }
					fclose( $mh );
				}
				$st['aids'] = array_keys( $aids );
				list( $ref, $reff ) = $this->build_reference_sets();
				$this->persist_reference_sets( $ref, $reff );
			} else {
				list( $ref, $reff ) = $this->load_reference_sets();
				if ( empty( $ref ) ) { list( $ref, $reff ) = $this->build_reference_sets(); $this->persist_reference_sets( $ref, $reff ); } 
			}
			$aids = $st['aids']; $n = count( $aids );
			while ( $st['db_off'] < $n && ( microtime( true ) - $start ) < self::OP_BUDGET_SEC ) {
				$slice = array_slice( $aids, $st['db_off'], 3000 );
				$st['db_off'] += count( $slice );
				$safe = array();
				foreach ( $slice as $a ) { $a = (int) $a; if ( $a > 0 && ! isset( $ref[ $a ] ) ) { $safe[] = $a; } }
				if ( ! $safe ) { continue; }
				$in = implode( ',', $safe ); 
				
				$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($in)" );
				$st['records'] += (int) $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type='attachment' AND ID IN ($in)" );
				$wpdb->query( "DELETE FROM {$wpdb->term_relationships} WHERE object_id IN ($in)" );
				
			}
			if ( $st['db_off'] < $n ) {
				set_transient( self::PURGE_TRANSIENT, $st, 2 * HOUR_IN_SECONDS );
				wp_send_json_success( array( 'done' => false, 'phase' => 'records', 'progress' => (int) round( $st['db_off'] / max( 1, $n ) * 100 ), 'records' => $st['records'] ) );
			}
			$st['phase'] = 'files'; @unlink( self::ref_ids_path() ); @unlink( self::ref_files_path() );
		}

		
		$skip = array( self::CAND_FILE, self::REF_IDS_FILE, self::REF_FILES_FILE );
		$exhausted = true;
		if ( is_dir( $qdir ) ) {
			$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $qdir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
			foreach ( $it as $f ) {
				if ( ( microtime( true ) - $start ) > self::OP_BUDGET_SEC ) { $exhausted = false; break; }
				if ( $f->isDir() ) { @rmdir( $f->getPathname() ); continue; }
				if ( $f->getPath() === $qdir && in_array( $f->getFilename(), $skip, true ) ) { continue; }
				$st['freed'] += (int) $f->getSize(); 
				@unlink( $f->getPathname() );
			}
		}
		$freed_mb = round( $st['freed'] / 1048576, 1 );
		if ( $exhausted ) {
			delete_transient( self::PURGE_TRANSIENT );
			wp_send_json_success( array( 'done' => true, 'freed_mb' => $freed_mb, 'records' => $st['records'] ) );
		}
		set_transient( self::PURGE_TRANSIENT, $st, 2 * HOUR_IN_SECONDS );
		wp_send_json_success( array( 'done' => false, 'phase' => 'files', 'freed_mb' => $freed_mb, 'records' => $st['records'] ) );
	}

	private static function rmtree( $dir, array $except = array() ) {
		$freed = 0;
		if ( ! is_dir( $dir ) ) { return 0; }
		$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
		foreach ( $it as $f ) {
			if ( $f->isDir() ) { @rmdir( $f->getPathname() ); }
			else {
				if ( $except && $f->getPath() === $dir && in_array( $f->getFilename(), $except, true ) ) { continue; }
				$freed += (int) $f->getSize(); @unlink( $f->getPathname() );
			}
		}
		return $freed;
	}

	private function quarantine_bytes() {
		$qdir = self::basedir() . '/' . self::QUARANTINE_DIR;
		if ( ! is_dir( $qdir ) ) { return 0; }
		$bytes = 0; $skip = array( self::CAND_FILE, self::REF_IDS_FILE, self::REF_FILES_FILE );
		$it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $qdir, FilesystemIterator::SKIP_DOTS ) );
		foreach ( $it as $f ) { if ( $f->isFile() && ! in_array( $f->getFilename(), $skip, true ) ) { $bytes += (int) $f->getSize(); } }
		return $bytes;
	}

	
	
	

	public function render_subtab() {
		if ( ! current_user_can( 'manage_options' ) ) {
			echo '<div class="aieo-dmm-section"><div class="aieo-dmm-card"><h3 style="margin-top:0;">'
				. esc_html__( 'Media cleanup', 'ai-eshop-optimizer' ) . '</h3><p>'
				. esc_html__( 'This tool is available to site administrators only.', 'ai-eshop-optimizer' )
				. '</p></div></div>';
			return;
		}
		$nonce = wp_create_nonce( AIEO_DMM::NONCE_ACTION );
		$ajax  = admin_url( 'admin-ajax.php' );
		$qmb   = round( $this->quarantine_bytes() / 1048576, 1 );
		?>
		<div class="aieo-dmm-section">
			<div class="aieo-dmm-card" id="aieo-card-media-cleanup">
				<h3 style="margin-top:0;"><?php esc_html_e( 'Media cleanup', 'ai-eshop-optimizer' ); ?></h3>
				<p class="description" style="max-width:840px;">
					<?php esc_html_e( 'Reclaim image files left on disk with no live use — orphaned files, stale thumbnails, and (deleted-product) attachments referenced nowhere. Nothing is deleted outright: flagged files move to a quarantine folder you review and then purge.', 'ai-eshop-optimizer' ); ?>
				</p>

				<div style="background:#fcf9e8;border:1px solid #f0e6a6;border-radius:8px;padding:12px 14px;margin:12px 0;max-width:840px;">
					<strong><?php esc_html_e( 'Before you run:', 'ai-eshop-optimizer' ); ?></strong>
					<?php esc_html_e( 'take a backup. An image is kept if it is referenced anywhere — featured/gallery/variation/swatch/category images, content embeds, page-builder & serialized metadata, or a live product of any status (incl. draft, private, trash). But a backup is the right safety net for a filesystem operation.', 'ai-eshop-optimizer' ); ?>
				</div>

				<p style="margin:10px 0 4px;font-weight:600;"><?php esc_html_e( 'What to look for', 'ai-eshop-optimizer' ); ?></p>
				<p style="margin:2px 0;"><label><input type="checkbox" id="mc-orphan" checked> <?php esc_html_e( 'Orphaned image files (no attachment owns them)', 'ai-eshop-optimizer' ); ?></label></p>
				<p style="margin:2px 0;"><label><input type="checkbox" id="mc-stale" checked> <?php esc_html_e( 'Stale thumbnails (sizes no longer registered)', 'ai-eshop-optimizer' ); ?></label></p>
				<p style="margin:2px 0;"><label><input type="checkbox" id="mc-unused"> <strong><?php esc_html_e( 'Unused attachment records (deleted-product images)', 'ai-eshop-optimizer' ); ?></strong> — <?php esc_html_e( 'the big reclaim; each unused record moves its whole set of files. Review one quarantine cycle before purging.', 'ai-eshop-optimizer' ); ?></label></p>

				<p class="submit" style="margin-top:14px;">
					<button type="button" class="button button-secondary" id="mc-scan"><?php esc_html_e( 'Scan (dry run)', 'ai-eshop-optimizer' ); ?></button>
					<button type="button" class="button" id="mc-cancel" style="display:none;"><?php esc_html_e( 'Stop', 'ai-eshop-optimizer' ); ?></button>
				</p>

				<div id="mc-progress" style="display:none;margin:10px 0 4px;max-width:840px;">
					<div style="background:#e2e4e7;border-radius:6px;height:10px;overflow:hidden;"><div id="mc-bar" style="background:#2271b1;height:100%;width:0;transition:width .2s;"></div></div>
					<div id="mc-prog" style="font-size:12px;color:#646970;margin-top:6px;font-variant-numeric:tabular-nums;"></div>
				</div>

				<div id="mc-results" style="display:none;margin-top:10px;">
					<div id="mc-summary" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px;"></div>
					<div id="mc-sample" style="max-height:280px;overflow:auto;border:1px solid #e2e4e7;border-radius:6px;padding:8px 10px;font-family:monospace;font-size:12px;color:#3c434a;background:#fff;"></div>
					<p class="submit" style="margin-top:12px;">
						<button type="button" class="button button-primary" id="mc-quarantine"><?php esc_html_e( 'Move flagged files to quarantine', 'ai-eshop-optimizer' ); ?></button>
						<span id="mc-q-status" style="margin-left:10px;font-size:13px;color:#646970;"></span>
					</p>
				</div>

				<hr style="margin:20px 0;">
				<p style="margin:0 0 6px;font-weight:600;"><?php esc_html_e( 'Quarantine', 'ai-eshop-optimizer' ); ?>
					<span id="mc-qmb" style="font-weight:400;color:#646970;">(<?php echo esc_html( $qmb ); ?> MB)</span></p>
				<p class="description" style="margin:2px 0 8px;"><?php esc_html_e( 'Flagged files are held in wp-content/uploads/aieo-quarantine/. Review your storefront, then purge to reclaim the space permanently.', 'ai-eshop-optimizer' ); ?></p>
				<p style="margin:2px 0 8px;"><label><input type="checkbox" id="mc-purge-db"> <?php esc_html_e( 'Also remove the now-empty attachment DB records (recommended only after reviewing the storefront)', 'ai-eshop-optimizer' ); ?></label></p>
				<button type="button" class="button button-link-delete" id="mc-purge" style="color:#b32d2e;"><?php esc_html_e( 'Empty quarantine (permanent)', 'ai-eshop-optimizer' ); ?></button>
				<span id="mc-purge-status" style="margin-left:10px;font-size:13px;color:#646970;"></span>
			</div>
		</div>
		<script>
		(function () {
			var AJAX = <?php echo wp_json_encode( $ajax ); ?>, NONCE = <?php echo wp_json_encode( $nonce ); ?>;
			var T = {
				folders: <?php echo wp_json_encode( __( 'folders', 'ai-eshop-optimizer' ) ); ?>, scanned: <?php echo wp_json_encode( __( 'scanned', 'ai-eshop-optimizer' ) ); ?>,
				orphans: <?php echo wp_json_encode( __( 'orphaned', 'ai-eshop-optimizer' ) ); ?>, stale: <?php echo wp_json_encode( __( 'stale', 'ai-eshop-optimizer' ) ); ?>, unused: <?php echo wp_json_encode( __( 'unused', 'ai-eshop-optimizer' ) ); ?>,
				oFiles: <?php echo wp_json_encode( __( 'Orphaned files', 'ai-eshop-optimizer' ) ); ?>, sThumbs: <?php echo wp_json_encode( __( 'Stale thumbnails', 'ai-eshop-optimizer' ) ); ?>, uRec: <?php echo wp_json_encode( __( 'Unused records', 'ai-eshop-optimizer' ) ); ?>,
				done: <?php echo wp_json_encode( __( 'Scan complete', 'ai-eshop-optimizer' ) ); ?>, stopped: <?php echo wp_json_encode( __( 'Stopped', 'ai-eshop-optimizer' ) ); ?>,
				clean: <?php echo wp_json_encode( __( 'Nothing flagged — your uploads are clean.', 'ai-eshop-optimizer' ) ); ?>, more: <?php echo wp_json_encode( __( 'more', 'ai-eshop-optimizer' ) ); ?>, capped: <?php echo wp_json_encode( __( 'result capped — quarantine then scan again', 'ai-eshop-optimizer' ) ); ?>
			};
			var $ = function (id) { return document.getElementById(id); };
			function post(a, x) { var fd = new FormData(); fd.set('action', a); fd.set('nonce', NONCE); Object.keys(x || {}).forEach(function (k) { fd.set(k, x[k]); }); return fetch(AJAX, { method: 'POST', credentials: 'same-origin', body: fd }).then(function (r) { return r.json(); }); }
			function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]; }); }
			function n(x) { return Number(x).toLocaleString(); }
			var stopped = false;
			function setProg(d) {
				var pct = d.total ? Math.min(100, Math.round((d.position / d.total) * 100)) : 0;
				$('mc-bar').style.width = pct + '%';
				$('mc-prog').textContent = pct + '%  ·  ' + d.folder + '/' + d.total + ' ' + T.folders + '  ·  ' + n(d.scanned) + ' ' + T.scanned
					+ '  ·  ' + n(d.orphan_count) + ' ' + T.orphans + '  ·  ' + n(d.stale_count) + ' ' + T.stale + '  ·  ' + n(d.unused_count) + ' ' + T.unused;
			}
			function card(l, v) { return '<div style="flex:1 1 170px;min-width:170px;background:#f6f7f7;border:1px solid #e2e4e7;border-radius:8px;padding:12px 14px;"><div style="font-size:18px;font-weight:600;color:#1d2327;">' + esc(v) + '</div><div style="font-size:12px;color:#646970;margin-top:3px;">' + esc(l) + '</div></div>'; }
			function finish(d, was) {
				$('mc-cancel').style.display = 'none'; $('mc-scan').disabled = false;
				$('mc-prog').textContent = (was ? T.stopped : T.done) + '  ·  ' + $('mc-prog').textContent;
				$('mc-results').style.display = 'block';
				$('mc-summary').innerHTML = card(T.oFiles, n(d.orphan_count) + ' · ' + d.orphan_mb + ' MB') + card(T.sThumbs, n(d.stale_count) + ' · ' + d.stale_mb + ' MB')
					+ card(T.uRec, n(d.unused_attachments || 0) + ' recs · ' + n(d.unused_count) + ' files · ' + d.unused_mb + ' MB') + (d.truncated ? card('!', T.capped) : '');
				var s = $('mc-sample');
				if (!d.sample || !d.sample.length) { s.textContent = T.clean; }
				else { var tot = d.orphan_count + d.stale_count + d.unused_count; s.innerHTML = d.sample.map(function (c) { var t = c.cat === 'orphan' ? 'orphan' : (c.cat === 'stale_subsize' ? 'stale' : 'UNUSED'); return '<div>[' + t + '] ' + esc(c.path) + '</div>'; }).join('') + (tot > d.sample.length ? '<div style="color:#888;">… ' + n(tot - d.sample.length) + ' ' + T.more + '</div>' : ''); }
			}
			function loop() { post('aieo_media_scan_step', {}).then(function (j) {
				if (!j || !j.success) { $('mc-cancel').style.display = 'none'; $('mc-scan').disabled = false; $('mc-prog').textContent = (j && j.data && j.data.message) || 'Scan failed.'; return; }
				setProg(j.data); if (j.data.done) { finish(j.data, false); } else if (stopped) { finish(j.data, true); } else { loop(); }
			}).catch(function () { $('mc-cancel').style.display = 'none'; $('mc-scan').disabled = false; $('mc-prog').textContent = 'Scan failed (network).'; }); }
			$('mc-scan').addEventListener('click', function () {
				stopped = false; $('mc-scan').disabled = true; $('mc-cancel').disabled = false; $('mc-cancel').style.display = 'inline-block';
				$('mc-results').style.display = 'none'; $('mc-progress').style.display = 'block'; $('mc-bar').style.width = '0'; $('mc-prog').textContent = <?php echo wp_json_encode( __( 'Building reference index…', 'ai-eshop-optimizer' ) ); ?>;
				post('aieo_media_scan_start', { want_orphan: $('mc-orphan').checked ? 1 : '', want_stale: $('mc-stale').checked ? 1 : '', want_unused: $('mc-unused').checked ? 1 : '' }).then(function (j) {
					if (!j || !j.success) { $('mc-scan').disabled = false; $('mc-cancel').style.display = 'none'; $('mc-prog').textContent = (j && j.data && j.data.message) || 'Failed.'; return; } loop();
				});
			});
			$('mc-cancel').addEventListener('click', function () { stopped = true; $('mc-cancel').disabled = true; });
			$('mc-quarantine').addEventListener('click', function () {
				if (!confirm(<?php echo wp_json_encode( __( 'Move all flagged files to the quarantine folder?', 'ai-eshop-optimizer' ) ); ?>)) { return; }
				var st = $('mc-q-status'); $('mc-quarantine').disabled = true; st.style.color = '#646970';
				st.textContent = <?php echo wp_json_encode( __( 'Building a fresh reference index…', 'ai-eshop-optimizer' ) ); ?>;
				var MV = <?php echo wp_json_encode( ' ' . __( 'moved', 'ai-eshop-optimizer' ) ); ?>, RC = <?php echo wp_json_encode( ' ' . __( 'records', 'ai-eshop-optimizer' ) ); ?>;
				function qstep() {
					post('aieo_media_quarantine', {}).then(function (j) {
						if (!j || !j.success) { $('mc-quarantine').disabled = false; st.style.color = '#b91c1c'; st.textContent = (j && j.data && j.data.message) || 'Failed.'; return; }
						var d = j.data;
						if (d.done) { $('mc-quarantine').disabled = false; st.style.color = '#15803d'; st.textContent = n(d.moved) + MV + ' · ' + d.freed_mb + ' MB' + (d.records ? (' · ' + n(d.records) + RC) : ''); $('mc-results').style.display = 'none'; $('mc-qmb').textContent = '(' + d.freed_mb + ' MB — reload to refresh)'; }
						else { st.textContent = (d.progress || 0) + '% · ' + n(d.moved) + MV + ' · ' + d.freed_mb + ' MB'; qstep(); }
					}).catch(function () { $('mc-quarantine').disabled = false; st.style.color = '#b91c1c'; st.textContent = 'Failed (network).'; });
				}
				qstep();
			});
			$('mc-purge').addEventListener('click', function () {
				var db = $('mc-purge-db').checked;
				if (!confirm(db ? <?php echo wp_json_encode( __( 'Permanently delete the quarantined files AND remove their now-empty attachment records? This cannot be undone.', 'ai-eshop-optimizer' ) ); ?> : <?php echo wp_json_encode( __( 'Permanently delete everything in the quarantine folder? This cannot be undone.', 'ai-eshop-optimizer' ) ); ?>)) { return; }
				var st = $('mc-purge-status'); $('mc-purge').disabled = true; st.style.color = '#646970'; st.textContent = <?php echo wp_json_encode( __( 'Purging…', 'ai-eshop-optimizer' ) ); ?>;
				var RR = <?php echo wp_json_encode( ' ' . __( 'records removed', 'ai-eshop-optimizer' ) ); ?>, first = true;
				function pstep() {
					post('aieo_media_purge', first ? { also_db: db ? 1 : '' } : {}).then(function (j) { first = false;
						if (!j || !j.success) { $('mc-purge').disabled = false; st.style.color = '#b91c1c'; st.textContent = (j && j.data && j.data.message) || 'Failed.'; return; }
						var d = j.data;
						if (d.done) { $('mc-purge').disabled = false; st.style.color = '#15803d'; st.textContent = <?php echo wp_json_encode( __( 'freed', 'ai-eshop-optimizer' ) ); ?> + ' ' + d.freed_mb + ' MB' + (d.records ? (' · ' + n(d.records) + RR) : ''); $('mc-qmb').textContent = '(0 MB)'; }
						else { st.textContent = ( d.phase === 'records' ? ( <?php echo wp_json_encode( __( 'removing records', 'ai-eshop-optimizer' ) ); ?> + ' ' + (d.progress || 0) + '% · ' + n(d.records) ) : ( <?php echo wp_json_encode( __( 'deleting files', 'ai-eshop-optimizer' ) ); ?> + ' · ' + d.freed_mb + ' MB' ) ); pstep(); }
					}).catch(function () { $('mc-purge').disabled = false; st.style.color = '#b91c1c'; st.textContent = 'Failed.'; });
				}
				pstep();
			});
		})();
		</script>
		<?php
	}
}
