<?php
/**
 * Database Cleanup module.
 *
 * @package DatabaseCleanup
 */

namespace DatabaseCleanup;

use DatabaseCleanup\Includes\Cleanup_History_List_Table;
use DatabaseCleanup\Includes\Cleanup_Logger;
use DatabaseCleanup\Includes\Cleanup_Task_Registry;
use DatabaseCleanup\Includes\Database_Cleaner;
use DatabaseCleanup\Includes\Database_Scanner;
use DatabaseCleanup\Includes\Entity_Browser;
use DatabaseCleanup\Includes\Item_Detail;
use DatabaseCleanup\Includes\Item_Ownership;
use DatabaseCleanup\Includes\Ownership_Store;
use DatabaseCleanup\Includes\Scheduler;
use DatabaseCleanup\Includes\Source_Scanner;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\Contracts\DataLocationsInterface;
use WP_PowerSuite\Core\NetworkScope;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;

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

require_once __DIR__ . '/includes/interface-cleanup-task.php';
require_once __DIR__ . '/includes/class-cleanup-task-registry.php';
require_once __DIR__ . '/includes/class-database-scanner.php';
require_once __DIR__ . '/includes/class-database-cleaner.php';
require_once __DIR__ . '/includes/class-cleanup-logger.php';
require_once __DIR__ . '/includes/class-cleanup-history-list-table.php';
require_once __DIR__ . '/includes/class-scheduler.php';
require_once __DIR__ . '/includes/class-protected-items.php';
require_once __DIR__ . '/includes/class-ownership-store.php';
require_once __DIR__ . '/includes/class-source-scanner.php';
require_once __DIR__ . '/includes/class-entity-browser.php';
require_once __DIR__ . '/includes/class-item-ownership.php';
require_once __DIR__ . '/includes/class-item-detail.php';

class Module extends AbstractModule implements DataLocationsInterface {

	public const MODULE_ID = 'database-cleanup';

	public const VERSION = '2.11.0';

	/** @var array<string, mixed>|null */
	private ?array $settings_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Database Cleanup', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::VERSION;
	}

	public function getDescription(): string {
		return __( 'Ownership-aware cleanup for revisions, transients, and orphans. Browse tables and options safely—templates, builder data, and active plugin tables stay protected.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'ajax', 'cron' ];
	}

	public function getDataLocations(): array {
		return [
			'options'           => [
				Cleanup_Logger::OPTION_KEY,
				'wpps_dc_last_scan',
				'wpps_dc_last_cleanup',
				'wpps_dc_size_history',
				Item_Ownership::OVERRIDE_OPTION,
				Ownership_Store::META_OPTION,
				Ownership_Store::OPTION_PREFIX . 'option',
				Ownership_Store::OPTION_PREFIX . 'table',
				Ownership_Store::OPTION_PREFIX . 'cron',
				Ownership_Store::OPTION_PREFIX . 'meta',
			],
			'option_store_keys' => [ self::MODULE_ID ],
		];
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function getDefaultSettings(): array {
		return [
			'revision_keep_count'  => 5,
			'trash_post_days'      => 30,
			'trash_comment_days'   => 30,
			'draft_post_days'         => 30,
			'auto_draft_days'         => 1,
			'unapproved_comment_days' => 30,
			'excluded_post_types'  => [],
			'optimize_tables'      => false,
			'batch_size'           => 300,
			'backup_warn_days'     => 7,
			'schedule_enabled'     => false,
			'schedule_frequency'   => 'weekly',
			'schedule_tasks'       => [],
			'email_report'         => false,
			'report_email'         => '',
		];
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public static function sanitizeSettings( array $settings ): array {
		$defaults = self::getDefaultSettings();

		$excluded = [];
		if ( isset( $settings['excluded_post_types'] ) ) {
			if ( is_array( $settings['excluded_post_types'] ) ) {
				foreach ( $settings['excluded_post_types'] as $type ) {
					$key = sanitize_key( (string) $type );
					if ( $key !== '' ) {
						$excluded[] = $key;
					}
				}
			} elseif ( is_string( $settings['excluded_post_types'] ) ) {
				$lines = preg_split( '/\r\n|\r|\n|,/', $settings['excluded_post_types'] ) ?: [];
				foreach ( $lines as $line ) {
					$key = sanitize_key( trim( (string) $line ) );
					if ( $key !== '' ) {
						$excluded[] = $key;
					}
				}
			}
		}

		$schedule_tasks = [];
		if ( ! empty( $settings['schedule_tasks'] ) && is_array( $settings['schedule_tasks'] ) ) {
			foreach ( $settings['schedule_tasks'] as $task_id ) {
				$key  = sanitize_key( (string) $task_id );
				$task = $key !== '' ? Cleanup_Task_Registry::get( $key ) : null;
				if ( $task && $task->is_safe() ) {
					$schedule_tasks[] = $key;
				}
			}
		}

		$frequency = isset( $settings['schedule_frequency'] ) ? sanitize_key( (string) $settings['schedule_frequency'] ) : 'weekly';
		if ( ! in_array( $frequency, [ 'hourly', 'twicedaily', 'daily', 'weekly', 'monthly' ], true ) ) {
			$frequency = 'weekly';
		}

		$sanitized = [
			'revision_keep_count' => max( 1, min( 50, absint( $settings['revision_keep_count'] ?? $defaults['revision_keep_count'] ) ) ),
			'trash_post_days'     => max( 1, min( 365, absint( $settings['trash_post_days'] ?? $defaults['trash_post_days'] ) ) ),
			'trash_comment_days'  => max( 1, min( 365, absint( $settings['trash_comment_days'] ?? $defaults['trash_comment_days'] ) ) ),
			'draft_post_days'         => max( 1, min( 365, absint( $settings['draft_post_days'] ?? $defaults['draft_post_days'] ) ) ),
			'auto_draft_days'         => max( 1, min( 30, absint( $settings['auto_draft_days'] ?? $defaults['auto_draft_days'] ) ) ),
			'unapproved_comment_days' => max( 30, min( 365, absint( $settings['unapproved_comment_days'] ?? $defaults['unapproved_comment_days'] ) ) ),
			'excluded_post_types'     => array_values( array_unique( $excluded ) ),
			'optimize_tables'     => ! empty( $settings['optimize_tables'] ),
			'batch_size'          => max( 200, min( 500, absint( $settings['batch_size'] ?? $defaults['batch_size'] ) ) ),
			'backup_warn_days'    => max( 0, min( 90, absint( $settings['backup_warn_days'] ?? $defaults['backup_warn_days'] ) ) ),
			'schedule_enabled'    => ! empty( $settings['schedule_enabled'] ),
			'schedule_frequency'  => $frequency,
			'schedule_tasks'      => array_values( array_unique( $schedule_tasks ) ),
			'email_report'        => ! empty( $settings['email_report'] ),
			'report_email'        => sanitize_email( (string) ( $settings['report_email'] ?? '' ) ),
		];

		return $sanitized;
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function getStoredSettings(): array {
		$stored = OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->all( self::getDefaultSettings() );

		return self::sanitizeSettings( is_array( $stored ) ? $stored : [] );
	}

	/**
	 * @return array<string, mixed>
	 */
	public function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$stored = $this->moduleSettings()->all( self::getDefaultSettings() );
		$this->settings_cache = self::sanitizeSettings( is_array( $stored ) ? $stored : [] );

		return $this->settings_cache;
	}

	public static function is_schedule_allowed(): bool {
		return true;
	}

	public static function cleanupOnDisable(): void {
		Scheduler::clear_schedule();
		Database_Cleaner::clear_all_runtime_state();
		Ownership_Store::clear_all();
		Source_Scanner::cleanup_all();
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		Cleanup_Task_Registry::boot();
		Scheduler::register_hooks();
		Source_Scanner::register_hooks();

		add_action(
			'wp_powersuite_module_settings_saved',
			static function ( string $module_id, array $settings ): void {
				if ( $module_id !== self::MODULE_ID ) {
					return;
				}
				Scheduler::sync_schedule( self::sanitizeSettings( $settings ) );
			},
			10,
			2
		);

		add_action( 'wp_ajax_wpps_dc_scan_init', [ $this, 'ajaxScanInit' ] );
		add_action( 'wp_ajax_wpps_dc_scan_step', [ $this, 'ajaxScanStep' ] );
		add_action( 'wp_ajax_wpps_dc_scan_abort', [ $this, 'ajaxScanAbort' ] );
		add_action( 'wp_ajax_wpps_dc_scan', [ $this, 'ajaxScanInit' ] );
		add_action( 'wp_ajax_wpps_dc_clean_init', [ $this, 'ajaxCleanInit' ] );
		add_action( 'wp_ajax_wpps_dc_clean_step', [ $this, 'ajaxCleanStep' ] );
		add_action( 'wp_ajax_wpps_dc_clean_abort', [ $this, 'ajaxCleanAbort' ] );
		add_action( 'wp_ajax_wpps_dc_save_settings', [ $this, 'ajaxSaveSettings' ] );
		add_action( 'wp_ajax_wpps_dc_history_detail', [ $this, 'ajaxHistoryDetail' ] );
		add_action( 'wp_ajax_wpps_dc_browse', [ $this, 'ajaxBrowse' ] );
		add_action( 'wp_ajax_wpps_dc_orphan_preview', [ $this, 'ajaxOrphanPreview' ] );
		add_action( 'wp_ajax_wpps_dc_browse_action', [ $this, 'ajaxBrowseAction' ] );
		add_action( 'wp_ajax_wpps_dc_item_detail', [ $this, 'ajaxItemDetail' ] );
		add_action( 'wp_ajax_wpps_dc_save_ownership', [ $this, 'ajaxSaveOwnership' ] );
		add_action( 'wp_ajax_wpps_dc_owner_scan_init', [ $this, 'ajaxOwnerScanInit' ] );
		add_action( 'wp_ajax_wpps_dc_owner_scan_step', [ $this, 'ajaxOwnerScanStep' ] );
		add_action( 'wp_ajax_wpps_dc_owner_scan_abort', [ $this, 'ajaxOwnerScanAbort' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( NetworkScope::adminMenuHook(), [ $this, 'registerMenu' ], 25 );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
	}

	public function registerMenu(): void {
		if ( ! NetworkScope::canManagePlugin() ) {
			return;
		}

		add_submenu_page(
			'powersuite',
			__( 'Database Cleanup', 'wp-powersuite' ),
			__( 'Database Cleanup', 'wp-powersuite' ),
			NetworkScope::adminCapability(),
			'powersuite-database-cleanup',
			[ $this, 'renderPage' ]
		);
	}

	public function renderPage(): void {
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-powersuite' ) );
		}
		if ( ! $this->isLicenseActive() ) {
			?>
			<div class="wrap">
				<h1><?php esc_html_e( 'Database Cleanup', 'wp-powersuite' ); ?></h1>
				<div class="notice notice-warning inline" style="margin:0;">
					<p>
						<?php esc_html_e( 'Database Cleanup requires an active license. Activate your license to scan and clean your database.', 'wp-powersuite' ); ?>
						<a href="<?php echo esc_url( admin_url( 'admin.php?page=powersuite-license' ) ); ?>"><?php esc_html_e( 'Activate license', 'wp-powersuite' ); ?></a>
					</p>
				</div>
			</div>
			<?php
			return;
		}

		Database_Cleaner::release_stale_active_job( get_current_user_id() );

		$view = self::resolveView(
			isset( $_GET['view'] ) ? (string) wp_unslash( $_GET['view'] ) : 'general'
		);

		if ( $view === 'settings' ) {
			$this->processHistoryActions();
		}

		$settings           = $this->getSettings();
		$last_scan          = Database_Scanner::get_last_scan();
		$size_history       = Database_Scanner::get_size_history();
		$last_clean         = get_option( 'wpps_dc_last_cleanup', [] );
		$backup             = self::getBackupStatus( $settings );
		$overview           = Database_Scanner::get_overview_metrics();
		$screen_url         = admin_url( 'admin.php?page=powersuite-database-cleanup' );
		$history_updated    = isset( $_GET['history_updated'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['history_updated'] ) ) : '';
		$history_list_table = null;

		if ( $view === 'settings' ) {
			$history_list_table = new Cleanup_History_List_Table();
			$history_list_table->prepare_items();
		}

		include __DIR__ . '/admin/page-database-cleanup.php';
	}

	private static function resolveView( string $view ): string {
		$view = sanitize_key( $view );

		return match ( $view ) {
			'cleanup' => 'general',
			'history', 'info', 'analytics', 'activity' => 'settings',
			'general', 'tables', 'options', 'post_meta', 'user_meta', 'comment_meta', 'term_meta', 'transients', 'cron', 'post_types', 'settings' => $view,
			default => 'general',
		};
	}

	private function processHistoryActions(): void {
		if ( ! NetworkScope::canManagePlugin() ) {
			return;
		}

		$redirect_base = add_query_arg(
			[
				'page' => 'powersuite-database-cleanup',
				'view' => 'settings',
			],
			admin_url( 'admin.php' )
		);

		if ( isset( $_GET['source'] ) ) {
			$source = sanitize_key( wp_unslash( (string) $_GET['source'] ) );
			if ( in_array( $source, [ 'manual', 'scheduled' ], true ) ) {
				$redirect_base = add_query_arg( 'source', $source, $redirect_base );
			}
		}

		if ( isset( $_GET['clear_history'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( (string) ( $_GET['_wpnonce'] ?? '' ) ) ), 'wpps_dc_clear_history' ) ) {
			Cleanup_Logger::clear();
			wp_safe_redirect( add_query_arg( 'history_updated', 'cleared', $redirect_base ) );
			exit;
		}

		if ( isset( $_GET['delete_history'] ) ) {
			$id = sanitize_text_field( wp_unslash( (string) $_GET['delete_history'] ) );
			if ( $id !== '' && wp_verify_nonce( sanitize_text_field( wp_unslash( (string) ( $_GET['_wpnonce'] ?? '' ) ) ), 'wpps_dc_delete_history_' . $id ) ) {
				Cleanup_Logger::delete_by_ids( [ $id ] );
				wp_safe_redirect( add_query_arg( 'history_updated', 'deleted_1', $redirect_base ) );
				exit;
			}
		}

		$bulk_action = isset( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1'
			? sanitize_text_field( wp_unslash( (string) $_REQUEST['action'] ) )
			: ( isset( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1'
				? sanitize_text_field( wp_unslash( (string) $_REQUEST['action2'] ) )
				: '' );
		$bulk_ids    = isset( $_REQUEST['history'] ) && is_array( $_REQUEST['history'] )
			? array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['history'] ) )
			: [];
		$bulk_ids    = array_values( array_filter( $bulk_ids ) );
		$bulk_nonce  = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['_wpnonce'] ) ) : '';

		if ( $bulk_action === 'delete' && $bulk_ids !== [] && wp_verify_nonce( $bulk_nonce, 'bulk-cleanup_runs' ) ) {
			$removed = Cleanup_Logger::delete_by_ids( $bulk_ids );
			wp_safe_redirect(
				add_query_arg(
					'history_updated',
					'deleted_' . max( 0, $removed ),
					$redirect_base
				)
			);
			exit;
		}
	}

	public function enqueueAssets( string $hook ): void {
		$hooks = [
			'wp-powersuite_page_powersuite-database-cleanup',
			'powersuite_page_powersuite-database-cleanup',
		];

		if ( ! in_array( $hook, $hooks, true ) ) {
			return;
		}

		wp_enqueue_style( 'dashicons' );

		$version = self::VERSION;

		wp_enqueue_style(
			'wpps-database-cleanup',
			wp_powersuite_module_asset_url( __FILE__, 'assets/database-cleanup.css' ),
			[ 'wp-powersuite-admin' ],
			$version
		);

		wp_enqueue_script(
			'wpps-database-cleanup',
			wp_powersuite_module_asset_url( __FILE__, 'assets/database-cleanup.js' ),
			[],
			$version,
			true
		);

		wp_localize_script(
			'wpps-database-cleanup',
			'wppsDatabaseCleanup',
			[
				'ajaxUrl'        => admin_url( 'admin-ajax.php' ),
				'nonceScan'      => wp_create_nonce( 'wpps_dc_scan' ),
				'nonceClean'     => wp_create_nonce( 'wpps_dc_clean' ),
				'nonceSettings'  => wp_create_nonce( 'wpps_dc_settings' ),
				'nonceHistory'   => wp_create_nonce( 'wpps_dc_history' ),
				'nonceBrowse'    => wp_create_nonce( 'wpps_dc_browse' ),
				'nonceDetail'    => wp_create_nonce( 'wpps_dc_detail' ),
				'nonceOwnership' => wp_create_nonce( 'wpps_dc_ownership' ),
				'currentView'    => self::resolveView(
					isset( $_GET['view'] ) ? (string) wp_unslash( $_GET['view'] ) : 'general'
				),
				'safeTaskIds'    => Cleanup_Task_Registry::safe_task_ids(),
				'taskCatalog'    => Cleanup_Task_Registry::catalog(),
				'categoryLabels' => Cleanup_Task_Registry::category_labels(),
				'scheduleAllowed'=> self::is_schedule_allowed(),
				'initialScan'    => Database_Scanner::get_last_scan(),
				'backupStatus'   => self::getBackupStatus( $this->getSettings() ),
				'optimizeTables' => ! empty( $this->getSettings()['optimize_tables'] ),
				'perPageDefault' => Entity_Browser::DEFAULT_PER_PAGE,
				'perPageOptions' => Entity_Browser::per_page_options(),
				'browserConfigs' => [
					'tables'       => Entity_Browser::browser_config( 'tables' ),
					'options'      => Entity_Browser::browser_config( 'options' ),
					'post_meta'    => Entity_Browser::browser_config( 'post_meta' ),
					'user_meta'    => Entity_Browser::browser_config( 'user_meta' ),
					'comment_meta' => Entity_Browser::browser_config( 'comment_meta' ),
					'term_meta'    => Entity_Browser::browser_config( 'term_meta' ),
					'transients'   => Entity_Browser::browser_config( 'transients' ),
					'cron'         => Entity_Browser::browser_config( 'cron' ),
					'post_types'   => Entity_Browser::browser_config( 'post_types' ),
				],
				'ownerScan'      => [
					'hasData'   => Ownership_Store::has_data(),
					'scannedAt' => Ownership_Store::scanned_at(),
					'stale'     => Ownership_Store::is_stale(),
				],
				'strings'        => [
					'detectOwners'        => __( 'Detect owners', 'wp-powersuite' ),
					'redetectOwners'      => __( 'Re-detect owners', 'wp-powersuite' ),
					/* translators: %d: number value. */
					'ownerScanning'       => __( 'Scanning source files… %d%%', 'wp-powersuite' ),
					/* translators: 1: string, 2: string. */
					'ownerScanDone'       => __( 'Owner detection complete: %1$s of %2$s items attributed.', 'wp-powersuite' ),
					'ownerScanNever'      => __( 'Owners not yet detected. Run detection to scan installed plugin/theme code for accurate attribution.', 'wp-powersuite' ),
					'ownerScanStale'      => __( 'Plugins or themes changed since the last detection. Re-detect owners for accurate results.', 'wp-powersuite' ),
					/* translators: %s: string value. */
					'ownerScanAt'         => __( 'Owners last detected %s ago.', 'wp-powersuite' ),
					'ownerScanFailed'     => __( 'Owner detection failed. Please try again.', 'wp-powersuite' ),
					'ownerScanStarting'   => __( 'Preparing source scan…', 'wp-powersuite' ),
					'ownerScanProgress'   => __( 'Scanning source files…', 'wp-powersuite' ),
					/* translators: 1: string, 2: string. */
					'ownerScanFiles'      => __( '%1$s of %2$s files', 'wp-powersuite' ),
					'ownerScanCancel'     => __( 'Cancel', 'wp-powersuite' ),
					'ownerScanCanceled'   => __( 'Owner detection canceled.', 'wp-powersuite' ),
					'ownerScanEmpty'      => __( 'Nothing to scan — no unattributed items were found.', 'wp-powersuite' ),
					'candidatesTitle'     => __( 'Ownership candidates', 'wp-powersuite' ),
					'ownerMore'           => __( 'more', 'wp-powersuite' ),
					'candidateHits'       => __( 'references', 'wp-powersuite' ),
					'confidenceLabel'     => __( 'Confidence', 'wp-powersuite' ),
					'sourceScan'          => __( 'Source scan', 'wp-powersuite' ),
					'sourceHeuristic'     => __( 'Name match', 'wp-powersuite' ),
					'sourceManual'        => __( 'Manual', 'wp-powersuite' ),
					'statusActive'        => __( 'Active', 'wp-powersuite' ),
					'statusInactive'      => __( 'Inactive', 'wp-powersuite' ),
					'statusNotInstalled'  => __( 'Not installed', 'wp-powersuite' ),
					'cronCallbacks'       => __( 'Registered callbacks', 'wp-powersuite' ),
					'cronCallback'        => __( 'Callback', 'wp-powersuite' ),
					'cronLocation'        => __( 'Location', 'wp-powersuite' ),
					'scanning'            => __( 'Scanning database…', 'wp-powersuite' ),
					/* translators: 1: string, 1: number, 2: number. */
					'scanningStep'        => __( 'Scanning: %s (%1$d of %2$d)', 'wp-powersuite' ),
					/* translators: 1: string, 2: string. */
					'scanningFound'       => __( '%1$s items · %2$s found', 'wp-powersuite' ),
					'badgeSafe'           => __( 'Safe', 'wp-powersuite' ),
					'badgeReview'         => __( 'Review', 'wp-powersuite' ),
					'badgeAdvanced'       => __( 'Advanced', 'wp-powersuite' ),
					'statusCleaned'       => __( 'Cleaned', 'wp-powersuite' ),
					'statusCleaning'      => __( 'Cleaning…', 'wp-powersuite' ),
					'statusError'         => __( 'Failed', 'wp-powersuite' ),
					'actionClean'         => __( 'Clean', 'wp-powersuite' ),
					'healthGood'          => __( 'Healthy', 'wp-powersuite' ),
					'healthAttention'     => __( 'Needs attention', 'wp-powersuite' ),
					'healthUnknown'       => __( 'Not scanned', 'wp-powersuite' ),
					'confirmTitleSelected'=> __( 'Clean selected tasks', 'wp-powersuite' ),
					'confirmTitleAllSafe' => __( 'Clean all safe tasks', 'wp-powersuite' ),
					'confirmTitleSingle'  => __( 'Confirm cleanup', 'wp-powersuite' ),
					'confirmText'         => __( 'This permanently deletes the rows listed below. This action cannot be undone.', 'wp-powersuite' ),
					'confirmTextCaution'  => __( 'You are about to clean tasks marked Review or Advanced. These permanently delete data that may still be in use. Proceed only if you understand the impact.', 'wp-powersuite' ),
					'confirmTotal'        => __( 'Total to remove', 'wp-powersuite' ),
					'confirmBackupWarn'   => __( 'No recent backup was found. We strongly recommend creating a full backup before cleaning.', 'wp-powersuite' ),
					'confirmBackupHint'   => __( 'For safety, make sure you have a recent backup before deleting data.', 'wp-powersuite' ),
					'cleaning'            => __( 'Cleaning…', 'wp-powersuite' ),
					/* translators: 1: number, 2: number. */
					'cleaningStep'        => __( 'Cleaning… (%1$d of %2$d)', 'wp-powersuite' ),
					'actionDone'          => __( 'Done', 'wp-powersuite' ),
					'reportBreakdown'     => __( 'What was removed', 'wp-powersuite' ),
					'recommendHint'       => __( 'Recommended maintenance for this table', 'wp-powersuite' ),
					'scanComplete'        => __( 'Scan complete.', 'wp-powersuite' ),
					'scanCached'          => __( 'Showing recent scan results. Hold Shift and click Scan to run a full rescan.', 'wp-powersuite' ),
					'scanRestartHint'     => __( 'Hold Shift and click Scan to restart if a previous scan was interrupted.', 'wp-powersuite' ),
					'cleanComplete'       => __( 'Cleanup complete.', 'wp-powersuite' ),
					'errorGeneric'        => __( 'Something went wrong. Please try again.', 'wp-powersuite' ),
					'errorServerHtml'     => __( 'The server returned an unexpected response. Refresh the page or check the PHP error log.', 'wp-powersuite' ),
					'errorSessionExpired' => __( 'Request failed. Refresh the page and try again.', 'wp-powersuite' ),
					'noTasksSelected'     => __( 'Select at least one cleanup task.', 'wp-powersuite' ),
					'cleanSafeZeroHint'   => __( 'No safe rows were removed. Caution tasks (for example old drafts or completed Action Scheduler actions) are not included in Clean All Safe — select those tasks individually or use Clean Selected.', 'wp-powersuite' ),
					'settingsSaved'       => __( 'Settings saved.', 'wp-powersuite' ),
					'rowsRemoved'         => __( 'Rows removed', 'wp-powersuite' ),
					'spaceSaved'          => __( 'Space saved (est.)', 'wp-powersuite' ),
					'noneFound'           => __( 'None found', 'wp-powersuite' ),
					'notScanned'          => __( 'Not scanned', 'wp-powersuite' ),
					'notScannedMaintenance' => __( 'Not scanned (enable in Settings)', 'wp-powersuite' ),
					/* translators: 1: number, 2: string, 3: string. */
					'scanSummary'         => __( '%1$d tasks with bloat · %2$s items · %3$s removable', 'wp-powersuite' ),
					'savingSettings'      => __( 'Saving settings…', 'wp-powersuite' ),
					'historyLoading'      => __( 'Loading…', 'wp-powersuite' ),
					'browserLoading'      => __( 'Loading items…', 'wp-powersuite' ),
					'browserEmpty'          => __( 'No items found.', 'wp-powersuite' ),
					'browserSelectItems'    => __( 'Select at least one item.', 'wp-powersuite' ),
					/* translators: 1: number, 2: number. */
					'browserPage'           => __( 'Page %1$d of %2$d', 'wp-powersuite' ),
					/* translators: %s: string value. */
					'browserTotal'          => __( '%s items', 'wp-powersuite' ),
					/* translators: %s: string value. */
					'browserSelected'       => __( '%s selected', 'wp-powersuite' ),
					'browserTruncated'      => __( 'Showing the first sample of matching rows. Refine search or filters to narrow results.', 'wp-powersuite' ),
					'browserPerPage'        => __( 'Items per page', 'wp-powersuite' ),
					'protectedItem'         => __( 'Protected', 'wp-powersuite' ),
					'detailLoading'         => __( 'Loading details…', 'wp-powersuite' ),
					'viewItem'              => __( 'View', 'wp-powersuite' ),
					'originalValue'         => __( 'Original', 'wp-powersuite' ),
					'formattedValue'        => __( 'Formatted', 'wp-powersuite' ),
					'belongsTo'             => __( 'Belongs to', 'wp-powersuite' ),
					'itemSize'              => __( 'Size', 'wp-powersuite' ),
					'autoload'              => __( 'Autoload', 'wp-powersuite' ),
					'tableRows'             => __( 'Rows', 'wp-powersuite' ),
					'tableData'             => __( 'Data size', 'wp-powersuite' ),
					'tableIndex'            => __( 'Index size', 'wp-powersuite' ),
					'tableEngine'           => __( 'Engine', 'wp-powersuite' ),
					'tableColumns'          => __( 'Columns', 'wp-powersuite' ),
					'columnField'           => __( 'Field', 'wp-powersuite' ),
					'columnType'            => __( 'Type', 'wp-powersuite' ),
					'columnNull'            => __( 'Null', 'wp-powersuite' ),
					'columnKey'             => __( 'Key', 'wp-powersuite' ),
					'createTable'           => __( 'CREATE TABLE', 'wp-powersuite' ),
					'recentPosts'           => __( 'Recent posts', 'wp-powersuite' ),
					'totalPosts'            => __( 'Total posts', 'wp-powersuite' ),
					'postTitle'             => __( 'Title', 'wp-powersuite' ),
					'postStatus'            => __( 'Status', 'wp-powersuite' ),
					'postDate'              => __( 'Date', 'wp-powersuite' ),
					'noDetails'             => __( 'No details available.', 'wp-powersuite' ),
					'saveOwnership'         => __( 'Save ownership', 'wp-powersuite' ),
					'ownershipSaved'        => __( 'Ownership override saved.', 'wp-powersuite' ),
					'orphanWarning'         => __( 'Orphaned items may still be used by code or add-ons that are temporarily inactive. Create a full backup before deleting anything.', 'wp-powersuite' ),
					'orphanReviewText'      => __( 'Review the items below. Only the items you keep checked will be deleted.', 'wp-powersuite' ),
					'orphanLoadingPreview'  => __( 'Loading items…', 'wp-powersuite' ),
					'orphanNoneSelected'    => __( 'Select at least one item to delete.', 'wp-powersuite' ),
					/* translators: %s: string value. */
					'orphanSelectedCount'   => __( '%s selected', 'wp-powersuite' ),
					'orphanSelectAll'       => __( 'Select all', 'wp-powersuite' ),
					'orphanDeselectAll'     => __( 'Deselect all', 'wp-powersuite' ),
					/* translators: 1: string, 2: string. */
					'orphanMore'            => __( 'Showing the first %1$s of %2$s items. Run the cleanup again to remove the rest.', 'wp-powersuite' ),
					'browserNoOrphans'      => __( 'No orphaned items were found to clean.', 'wp-powersuite' ),
					'protectedLabel'        => __( 'Protected', 'wp-powersuite' ),
				],
			]
		);
	}

	public function ajaxBrowse(): void {
		$this->assertAjaxRequest( 'wpps_dc_browse' );

		$type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( (string) $_POST['type'] ) ) : '';
		$args = [
			'page'       => isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1,
			'per_page'   => isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : Entity_Browser::DEFAULT_PER_PAGE,
			'search'     => isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['search'] ) ) : '',
			'filter'     => isset( $_POST['filter'] ) ? sanitize_key( wp_unslash( (string) $_POST['filter'] ) ) : 'all',
			'belongs_to' => isset( $_POST['belongs_to'] ) ? sanitize_key( wp_unslash( (string) $_POST['belongs_to'] ) ) : 'all',
			'sort_by'    => isset( $_POST['sort_by'] ) ? sanitize_key( wp_unslash( (string) $_POST['sort_by'] ) ) : '',
			'sort_dir'   => isset( $_POST['sort_dir'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['sort_dir'] ) ) : 'DESC',
		];

		wp_send_json_success( Entity_Browser::list_items( $type, $args ) );
	}

	public function ajaxOrphanPreview(): void {
		$this->assertAjaxRequest( 'wpps_dc_browse' );

		$type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( (string) $_POST['type'] ) ) : '';

		wp_send_json_success(
			Entity_Browser::orphan_preview( $type, [ 'settings' => $this->getSettings() ] )
		);
	}

	public function ajaxBrowseAction(): void {
		$this->assertAjaxRequest( 'wpps_dc_browse' );

		$type   = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( (string) $_POST['type'] ) ) : '';
		$action = isset( $_POST['browser_action'] ) ? sanitize_key( wp_unslash( (string) $_POST['browser_action'] ) ) : '';
		$items  = isset( $_POST['items'] ) && is_array( $_POST['items'] )
			? array_map( static fn( $item ) => is_scalar( $item ) ? sanitize_text_field( (string) $item ) : '', wp_unslash( $_POST['items'] ) )
			: [];
		$items  = array_values( array_filter( $items, static fn( string $item ): bool => $item !== '' ) );

		// clean_orphans discovers its own items server-side, so an empty selection is expected.
		if ( $items === [] && $action !== 'clean_orphans' ) {
			wp_send_json_error( [ 'message' => __( 'No items selected.', 'wp-powersuite' ) ] );
		}

		if ( Entity_Browser::action_requires_typed_confirm( $type, $action ) ) {
			$confirm_error = self::validateTypedConfirm();
			if ( $confirm_error !== null ) {
				wp_send_json_error( $confirm_error );
			}
		}

		$result = Entity_Browser::run_action(
			$type,
			$action,
			$items,
			[
				'settings'             => $this->getSettings(),
				'typed_confirm_valid'  => true,
			]
		);

		wp_send_json_success( $result );
	}

	public function ajaxItemDetail(): void {
		$this->assertAjaxRequest( 'wpps_dc_detail' );

		$type = isset( $_POST['detail_type'] ) ? sanitize_key( wp_unslash( (string) $_POST['detail_type'] ) ) : '';
		$id   = isset( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['item_id'] ) ) : '';
		$ctx  = [];

		if ( isset( $_POST['context'] ) && is_array( $_POST['context'] ) ) {
			$ctx = array_map(
				static fn( $value ) => is_scalar( $value ) ? sanitize_text_field( (string) $value ) : $value,
				wp_unslash( $_POST['context'] )
			);
		}

		if ( $type === 'cron' && isset( $_POST['context_json'] ) ) {
			$decoded = json_decode( wp_unslash( (string) $_POST['context_json'] ), true );
			if ( is_array( $decoded ) ) {
				$ctx = $decoded;
			}
		}

		if ( $type === 'cron' ) {
			$decoded = json_decode( base64_decode( $id, true ) ?: '', true );
			if ( is_array( $decoded ) ) {
				$ctx = array_merge( $ctx, $decoded );
			}
		}

		if ( $type === '' || $id === '' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid item.', 'wp-powersuite' ) ] );
		}

		$detail = Item_Detail::get( $type, $id, $ctx );
		if ( isset( $detail['error'] ) ) {
			wp_send_json_error( [ 'message' => (string) $detail['error'] ] );
		}

		wp_send_json_success( $detail );
	}

	public function ajaxSaveOwnership(): void {
		$this->assertAjaxRequest( 'wpps_dc_ownership' );

		$item_type  = isset( $_POST['item_type'] ) ? sanitize_key( wp_unslash( (string) $_POST['item_type'] ) ) : '';
		$identifier = isset( $_POST['identifier'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['identifier'] ) ) : '';
		$ownership  = isset( $_POST['ownership'] ) && is_array( $_POST['ownership'] ) ? wp_unslash( $_POST['ownership'] ) : [];

		if ( $item_type === '' || $identifier === '' || $ownership === [] ) {
			wp_send_json_error( [ 'message' => __( 'Invalid ownership payload.', 'wp-powersuite' ) ] );
		}

		$saved = Item_Ownership::save_override( $item_type, $identifier, $ownership );

		if ( ! $saved ) {
			wp_send_json_error( [ 'message' => __( 'Could not save ownership override.', 'wp-powersuite' ) ] );
		}

		wp_send_json_success(
			[
				'ownership' => Item_Ownership::resolve( $item_type, $identifier ),
				'label'     => Item_Ownership::format_belongs_to( Item_Ownership::resolve( $item_type, $identifier ) ),
			]
		);
	}

	public function ajaxOwnerScanInit(): void {
		$user_id = get_current_user_id();
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );

			$rate_key = 'wpps_dc_owner_rl_' . $user_id;
			if ( get_transient( $rate_key ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'Owner detection was just started. Please wait a moment before retrying.', 'wp-powersuite' ) ] );
			}
			set_transient( $rate_key, 1, 5 );

			$scanner = new Source_Scanner();
			$scanner->abort( $user_id );
			$result = $scanner->create_job( $user_id );

			$this->cleanAjaxOutput();
			wp_send_json_success( $result );
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: owner scan init failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to start owner detection. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxOwnerScanStep(): void {
		$user_id = get_current_user_id();
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );

			$job_id = isset( $_POST['job_id'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['job_id'] ) ) : '';
			if ( $job_id === '' || ! Source_Scanner::is_valid_job_id( $job_id ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'Invalid owner scan job.', 'wp-powersuite' ) ] );
			}

			$step = ( new Source_Scanner() )->run_step( $job_id, $user_id );

			if ( ! empty( $step['error'] ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => (string) $step['error'] ] );
			}

			$this->cleanAjaxOutput();
			wp_send_json_success( $step );
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: owner scan step failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Owner detection failed. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxOwnerScanAbort(): void {
		$user_id = get_current_user_id();
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );
			( new Source_Scanner() )->abort( $user_id );
			$this->cleanAjaxOutput();
			wp_send_json_success( [ 'aborted' => true ] );
		} catch ( \Throwable $e ) {
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to cancel owner detection.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxHistoryDetail(): void {
		if ( ! check_ajax_referer( 'wpps_dc_history', 'nonce', false ) ) {
			wp_send_json_error(
				[ 'message' => __( 'Security check failed. Refresh the page and try again.', 'wp-powersuite' ) ],
				403
			);
		}

		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ], 403 );
		}

		$id = isset( $_POST['history_id'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['history_id'] ) ) : '';
		if ( $id === '' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid history entry.', 'wp-powersuite' ) ] );
		}

		$entry = Cleanup_Logger::get_by_id( $id );
		if ( $entry === null ) {
			wp_send_json_error( [ 'message' => __( 'History entry not found.', 'wp-powersuite' ) ] );
		}

		$ts      = (int) ( $entry['timestamp'] ?? 0 );
		$user_id = (int) ( $entry['user_id'] ?? 0 );
		$user    = $user_id > 0 ? get_userdata( $user_id ) : null;
		$tasks   = is_array( $entry['tasks'] ?? null ) ? $entry['tasks'] : [];

		ob_start();
		?>
		<dl class="wpps-dc-detail-meta">
			<div>
				<dt><?php esc_html_e( 'Date', 'wp-powersuite' ); ?></dt>
				<dd><?php echo esc_html( $ts > 0 ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts ) : '—' ); ?></dd>
			</div>
			<div>
				<dt><?php esc_html_e( 'User', 'wp-powersuite' ); ?></dt>
				<dd>
					<?php
					if ( ! empty( $entry['scheduled'] ) ) {
						esc_html_e( 'Scheduled', 'wp-powersuite' );
					} elseif ( $user ) {
						echo esc_html( $user->display_name );
					} else {
						esc_html_e( 'System', 'wp-powersuite' );
					}
					?>
				</dd>
			</div>
			<div>
				<dt><?php esc_html_e( 'Duration', 'wp-powersuite' ); ?></dt>
				<?php /* translators: %d: number value. */ ?>
				<dd><?php echo esc_html( sprintf( __( '%ds', 'wp-powersuite' ), (int) ( $entry['duration'] ?? 0 ) ) ); ?></dd>
			</div>
			<div>
				<dt><?php esc_html_e( 'Totals', 'wp-powersuite' ); ?></dt>
				<dd>
					<?php
					printf(
						/* translators: 1: string, 2: string. */
						esc_html__( '%1$s rows · %2$s saved', 'wp-powersuite' ),
						esc_html( number_format_i18n( (int) ( $entry['totals']['deleted'] ?? 0 ) ) ),
						esc_html( self::formatBytes( (int) ( $entry['totals']['bytes'] ?? 0 ) ) )
					);
					?>
				</dd>
			</div>
		</dl>
		<?php if ( $tasks !== [] ) : ?>
			<table class="wp-list-table widefat fixed striped">
				<thead>
					<tr>
						<th><?php esc_html_e( 'Task', 'wp-powersuite' ); ?></th>
						<th><?php esc_html_e( 'Rows removed', 'wp-powersuite' ); ?></th>
						<th><?php esc_html_e( 'Space saved (est.)', 'wp-powersuite' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ( $tasks as $task ) : ?>
						<?php if ( ! is_array( $task ) ) { continue; } ?>
						<tr>
							<td><?php echo esc_html( (string) ( $task['label'] ?? __( 'Task', 'wp-powersuite' ) ) ); ?></td>
							<td><?php echo esc_html( number_format_i18n( (int) ( $task['deleted'] ?? 0 ) ) ); ?></td>
							<td><?php echo esc_html( self::formatBytes( (int) ( $task['bytes'] ?? 0 ) ) ); ?></td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>
		<?php else : ?>
			<p><?php esc_html_e( 'No per-task details recorded for this run.', 'wp-powersuite' ); ?></p>
		<?php endif; ?>
		<?php
		wp_send_json_success( [ 'html' => (string) ob_get_clean() ] );
	}

	public function ajaxScanInit(): void {
		$user_id  = get_current_user_id();
		$lock_key = $this->scanLockKey( $user_id );
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );

			$force = ! empty( $_POST['force'] );

			if ( $force ) {
				$this->releaseScanLock( $user_id );
			} else {
				$this->assertScanAvailable( $user_id );
			}

			$scanner = new Database_Scanner();
			$init    = $scanner->create_scan_job( $this->getSettings(), $force );

			if ( ! empty( $init['cached'] ) && is_array( $init['results'] ?? null ) ) {
				$this->releaseScanLock( $user_id );
				$this->cleanAjaxOutput();
				wp_send_json_success(
					[
						'done'    => true,
						'results' => $init['results'],
						'cached'  => true,
					]
				);
			}

			if ( empty( $init['job_id'] ) ) {
				$this->releaseScanLock( $user_id );
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'Unable to start scan.', 'wp-powersuite' ) ] );
			}

			set_transient(
				$lock_key,
				[
					'started_at' => time(),
					'job_id'     => (string) $init['job_id'],
				],
				300
			);

			$this->cleanAjaxOutput();
			wp_send_json_success(
				[
					'done'        => false,
					'job_id'      => (string) $init['job_id'],
					'total_tasks' => (int) ( $init['total_tasks'] ?? 0 ),
					'cached'      => false,
				]
			);
		} catch ( \Throwable $e ) {
			$this->releaseScanLock( $user_id );
			error_log( 'WP PowerSuite Database Cleanup: scan init failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to start scan. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxScanStep(): void {
		$user_id  = get_current_user_id();
		$lock_key = $this->scanLockKey( $user_id );
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );

			$job_id = isset( $_POST['job_id'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['job_id'] ) ) : '';

			if ( $job_id === '' || ! Database_Cleaner::is_valid_job_id( $job_id ) ) {
				$this->releaseScanLock( $user_id );
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'Invalid scan job.', 'wp-powersuite' ) ] );
			}

			set_transient(
				$lock_key,
				[
					'started_at' => time(),
					'job_id'     => $job_id,
				],
				300
			);

			$scanner = new Database_Scanner();
			$step    = $scanner->run_scan_step( $job_id, $this->getSettings(), get_current_user_id() );

			if ( ! empty( $step['error'] ) ) {
				$this->releaseScanLock( $user_id );
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => (string) $step['error'] ] );
			}

			if ( ! empty( $step['done'] ) ) {
				$this->releaseScanLock( $user_id );
			}

			$this->cleanAjaxOutput();
			wp_send_json_success( $step );
		} catch ( \Throwable $e ) {
			$this->releaseScanLock( $user_id );
			error_log( 'WP PowerSuite Database Cleanup: scan step failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Scan failed. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxScanAbort(): void {
		$user_id = get_current_user_id();
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_scan' );
			$this->releaseScanLock( $user_id );
			$this->cleanAjaxOutput();
			wp_send_json_success( [ 'aborted' => true ] );
		} catch ( \Throwable $e ) {
			$this->releaseScanLock( $user_id );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to cancel scan.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxCleanInit(): void {
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_clean' );

			$user_id = get_current_user_id();
			$force   = ! empty( $_POST['force'] );

			if ( $force ) {
				Database_Cleaner::abort_active_job_for_user( $user_id );
			} else {
				Database_Cleaner::release_stale_active_job( $user_id );
			}

			$settings = $this->getSettings();
			$backup   = self::getBackupStatus( $settings );
			$task_ids = isset( $_POST['tasks'] ) ? (array) wp_unslash( $_POST['tasks'] ) : [];
			$task_ids = array_slice( array_values( array_unique( array_map( 'sanitize_key', $task_ids ) ) ), 0, 50 );

			if ( $task_ids === [] ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'No valid cleanup tasks selected.', 'wp-powersuite' ) ] );
			}

			$has_caution   = Cleanup_Task_Registry::includes_caution_tasks( $task_ids );
			$needs_confirm = $has_caution || ! empty( $backup['show_warning'] );

			if ( $needs_confirm ) {
				$confirm_error = self::validateTypedConfirm();
				if ( $confirm_error !== null ) {
					$this->cleanAjaxOutput();
					wp_send_json_error(
						array_merge(
							$confirm_error,
							[
								'requires_backup_ack'  => ! empty( $backup['show_warning'] ),
								'requires_caution_ack' => $has_caution,
							]
						)
					);
				}
			}

			if ( ! empty( $backup['show_warning'] ) && empty( $_POST['backup_ack'] ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error(
					[
						'message'              => __( 'Create a backup before running cleanup, or confirm that you accept the risk.', 'wp-powersuite' ),
						'requires_backup_ack'  => true,
						'requires_caution_ack' => $has_caution,
					]
				);
			}

			if ( $has_caution && empty( $_POST['caution_ack'] ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error(
					[
						'message'              => __( 'Caution tasks require explicit confirmation before they can run.', 'wp-powersuite' ),
						'requires_caution_ack' => true,
					]
				);
			}

			$cleaner = new Database_Cleaner();
			$job_id  = $cleaner->create_job( $task_ids, $settings, false, $has_caution );

			if ( $job_id === '' ) {
				$active  = get_transient( Database_Cleaner::ACTIVE_JOB_TRANSIENT . get_current_user_id() );
				$message = is_string( $active ) && $active !== ''
					? __( 'A cleanup is already running. Wait a few minutes, refresh the page, or hold Shift and click Clean again to cancel the stuck job.', 'wp-powersuite' )
					: __( 'No valid cleanup tasks selected.', 'wp-powersuite' );
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => $message, 'in_progress' => is_string( $active ) && $active !== '' ] );
			}

			$first_step = $cleaner->run_step( $job_id, $settings, $user_id );

			if ( ! empty( $first_step['error'] ) ) {
				Database_Cleaner::abort_job( $job_id, $user_id );
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => (string) $first_step['error'] ] );
			}

			$this->cleanAjaxOutput();
			wp_send_json_success(
				[
					'job_id' => $job_id,
					'step'   => $first_step,
				]
			);
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: clean init failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to start cleanup. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxCleanStep(): void {
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_clean' );

			$job_id = isset( $_POST['job_id'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['job_id'] ) ) : '';

			if ( $job_id === '' || ! Database_Cleaner::is_valid_job_id( $job_id ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => __( 'Invalid cleanup job.', 'wp-powersuite' ) ] );
			}

			$cleaner = new Database_Cleaner();
			$step    = $cleaner->run_step( $job_id, $this->getSettings(), get_current_user_id() );

			if ( ! empty( $step['error'] ) ) {
				$this->cleanAjaxOutput();
				wp_send_json_error( [ 'message' => (string) $step['error'] ] );
			}

			$this->cleanAjaxOutput();
			wp_send_json_success( $step );
		} catch ( \Throwable $e ) {
			error_log( 'WP PowerSuite Database Cleanup: clean step failed: ' . $e->getMessage() );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Cleanup failed. Please refresh and try again.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxCleanAbort(): void {
		$user_id = get_current_user_id();
		$this->beginAjaxBuffer();

		try {
			$this->assertAjaxRequest( 'wpps_dc_clean' );
			Database_Cleaner::abort_active_job_for_user( $user_id );
			$this->cleanAjaxOutput();
			wp_send_json_success( [ 'aborted' => true ] );
		} catch ( \Throwable $e ) {
			Database_Cleaner::abort_active_job_for_user( $user_id );
			$this->cleanAjaxOutput();
			wp_send_json_error( [ 'message' => __( 'Unable to cancel cleanup.', 'wp-powersuite' ) ] );
		}
	}

	public function ajaxSaveSettings(): void {
		$this->assertAjaxRequest( 'wpps_dc_settings' );

		$raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : [];
		if ( ! is_array( $raw ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid settings payload.', 'wp-powersuite' ) ] );
		}

		$sanitized = self::sanitizeSettings( $raw );
		$this->moduleSettings()->save( $sanitized );
		$this->settings_cache = $sanitized;
		Database_Scanner::clear_cache( $sanitized );
		Scheduler::sync_schedule( $sanitized );

		wp_send_json_success(
			[
				'settings' => $sanitized,
				'next_run' => Scheduler::next_run(),
			]
		);
	}

	private function assertAjaxRequest( string $nonce_action ): void {
		if ( ! check_ajax_referer( $nonce_action, 'nonce', false ) ) {
			wp_send_json_error(
				[ 'message' => __( 'Security check failed. Refresh the page and try again.', 'wp-powersuite' ) ],
				403
			);
		}

		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'wp-powersuite' ) ], 403 );
		}
	}

	/**
	 * @return array<string, mixed>|null
	 */
	private static function validateTypedConfirm(): ?array {
		$typed = isset( $_POST['typed_confirm'] )
			? sanitize_text_field( wp_unslash( (string) $_POST['typed_confirm'] ) )
			: '';

		if ( $typed !== 'CLEAN' ) {
			return [
				'message'                => __( 'Type CLEAN to confirm this action.', 'wp-powersuite' ),
				'requires_typed_confirm' => true,
			];
		}

		return null;
	}

	private function beginAjaxBuffer(): void {
		if ( function_exists( 'ini_set' ) ) {
			@ini_set( 'zlib.output_compression', 'Off' );
		}

		if ( ob_get_level() === 0 ) {
			ob_start();
		}
	}

	private function cleanAjaxOutput(): void {
		while ( ob_get_level() > 0 ) {
			$chunk = ob_get_contents();
			if ( is_string( $chunk ) && trim( $chunk ) !== '' ) {
				error_log( 'WP PowerSuite Database Cleanup AJAX noise: ' . substr( wp_strip_all_tags( $chunk ), 0, 500 ) );
			}
			ob_end_clean();
		}
	}

	private function scanLockKey( int $user_id ): string {
		return 'wpps_dc_scan_lock_' . max( 0, $user_id );
	}

	private function releaseScanLock( int $user_id ): void {
		$lock = get_transient( $this->scanLockKey( $user_id ) );

		if ( is_array( $lock ) && ! empty( $lock['job_id'] ) ) {
			$job_id = (string) $lock['job_id'];
			if ( Database_Cleaner::is_valid_job_id( $job_id ) ) {
				delete_transient( Database_Scanner::SCAN_JOB_PREFIX . $job_id );
			}
		}

		delete_transient( $this->scanLockKey( $user_id ) );
	}

	private function assertScanAvailable( int $user_id ): void {
		$lock = get_transient( $this->scanLockKey( $user_id ) );

		if ( ! $lock ) {
			return;
		}

		$started = is_array( $lock ) ? (int) ( $lock['started_at'] ?? 0 ) : 0;
		$job_id  = is_array( $lock ) ? (string) ( $lock['job_id'] ?? '' ) : '';
		$active  = false;

		if ( $job_id !== '' && Database_Cleaner::is_valid_job_id( $job_id ) ) {
			$job = get_transient( Database_Scanner::SCAN_JOB_PREFIX . $job_id );
			$active = is_array( $job );
		}

		if ( $active && $started > 0 && ( time() - $started ) < 300 ) {
			wp_send_json_error(
				[
					'message'     => __( 'A scan is already in progress. Hold Shift and click Scan to restart.', 'wp-powersuite' ),
					'in_progress' => true,
					'job_id'      => $job_id,
				]
			);
		}

		$this->releaseScanLock( $user_id );
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public static function getBackupStatus( array $settings ): array {
		$warn_days = max( 0, absint( $settings['backup_warn_days'] ?? 7 ) );
		$enabled   = false;
		$url       = '';
		$last_ts   = null;
		$show_warn = false;

		$option_store = get_option( 'wp_powersuite_settings', [] );
		$enabled_list = is_array( $option_store ) && isset( $option_store['enabled_modules'] ) && is_array( $option_store['enabled_modules'] )
			? $option_store['enabled_modules']
			: [];
		$enabled      = in_array( 'backup-restore', $enabled_list, true );

		if ( $enabled ) {
			$url = admin_url( 'admin.php?page=powersuite-backup-restore' );
			$history = get_option( 'wp_powersuite_backup_restore_history', [] );

			if ( is_array( $history ) && $history !== [] ) {
				$first = reset( $history );
				if ( is_array( $first ) && ! empty( $first['created'] ) ) {
					$last_ts = (int) $first['created'];
				}
			}

			if ( $warn_days > 0 ) {
				if ( $last_ts === null ) {
					$show_warn = true;
				} else {
					$show_warn = ( time() - $last_ts ) > ( $warn_days * DAY_IN_SECONDS );
				}
			}
		}

		return [
			'module_enabled' => $enabled,
			'url'            => $url,
			'last_backup_ts' => $last_ts,
			'show_warning'   => $show_warn,
			'warn_days'      => $warn_days,
		];
	}

	/**
	 * @param int $bytes
	 */
	public static function formatBytes( int $bytes ): string {
		if ( $bytes < 1024 ) {
			return sprintf( '%d B', $bytes );
		}

		if ( $bytes < 1048576 ) {
			return sprintf( '%.1f KB', $bytes / 1024 );
		}

		if ( $bytes < 1073741824 ) {
			return sprintf( '%.1f MB', $bytes / 1048576 );
		}

		return sprintf( '%.2f GB', $bytes / 1073741824 );
	}
}

