<?php

/**
 * SystemHandler — Domain handler for WordPress system/settings actions.
 *
 * Owns: get_site_status, get_discovered_abilities, get_options,
 *       get_database_status, get_site_settings, get_theme_info,
 *       optimize_database, update_option, purge_cache, cleanup_cron_jobs
 *
 * Note: diagnose_seo and audit_security are complex enough to merit their own
 * handlers in a future phase. They remain in ActionExecutor for now.
 *
 * @package LolaCore\Actions\Handlers
 * @license GPL-2.0-or-later
 */

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Required for system-level queries with no WP API equivalent.

declare( strict_types=1 );

namespace LolaCore\Actions\Handlers;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Diagnostic exception messages; escaped at REST/chat presentation layer.

use LolaCore\Actions\Contracts\ActionHandlerInterface;

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

/**
 * Handles WordPress system/settings inspection and maintenance actions.
 */
class SystemHandler implements ActionHandlerInterface {

	/**
	 * WordPress options that are PHP constants in wp-config.php, not DB options.
	 * update_option() has zero effect on these.
	 *
	 * @var array<string>
	 */
	private const PHP_CONSTANTS = array(
		'WP_DEBUG', 'WP_DEBUG_LOG', 'WP_DEBUG_DISPLAY',
		'DISALLOW_FILE_EDIT', 'DISALLOW_FILE_MODS',
		'WP_POST_REVISIONS', 'AUTOSAVE_INTERVAL',
		'WP_MEMORY_LIMIT', 'WP_MAX_MEMORY_LIMIT',
		'FORCE_SSL_ADMIN', 'WP_CACHE',
	);

	/**
	 * WordPress options that must never be updated by the AI.
	 *
	 * Beyond the obvious site-identity keys, this blocks options that are
	 * privilege-escalation or takeover primitives: default_role (new users as
	 * admin), the role/capability map, plugin/theme activation (dedicated
	 * lifecycle actions exist for those), mail server (email interception), and
	 * upload paths (path manipulation). update_option remains available for
	 * ordinary, non-security-sensitive options.
	 *
	 * @var array<string>
	 */
	private const OPTION_BLOCKLIST = array(
		// Site identity.
		'siteurl',
		'home',
		'admin_email',
		// User registration / privilege escalation.
		'users_can_register',
		'default_role',
		// Plugin & theme activation — use the dedicated lifecycle actions instead.
		'active_plugins',
		'active_sitewide_plugins',
		'template',
		'stylesheet',
		'current_theme',
		// Mail server — prevents email interception via SMTP redirection.
		'mailserver_url',
		'mailserver_login',
		'mailserver_pass',
		'mailserver_port',
		// Upload paths — prevents arbitrary write-location manipulation.
		'upload_path',
		'upload_url_path',
	);

	/** @inheritDoc */
	public function getSupportedActions(): array {
		return array(
			'get_site_status',
			'get_discovered_abilities',
			'get_options',
			'get_database_status',
			'get_site_settings',
			'get_theme_info',
			'optimize_database',
			'update_option',
			'purge_cache',
			'cleanup_cron_jobs',
		);
	}

	/** @inheritDoc */
	public function handle( string $actionId, array $params, bool $dryRun ): array {
		return match ( $actionId ) {
			'get_site_status'          => $this->getSiteStatus( $params ),
			'get_discovered_abilities' => $this->getDiscoveredAbilities( $params ),
			'get_options'              => $this->getOptions( $params ),
			'get_database_status'      => $this->getDatabaseStatus( $params ),
			'get_site_settings'        => $this->getSiteSettings( $params ),
			'get_theme_info'           => $this->getThemeInfo( $params ),
			'optimize_database'        => $this->optimizeDatabase( $params, $dryRun ),
			'update_option'            => $this->updateOption( $params, $dryRun ),
			'purge_cache'              => $this->purgeCache( $params, $dryRun ),
			'cleanup_cron_jobs'        => $this->cleanupCronJobs( $params, $dryRun ),
			default                    => throw new \Exception( esc_html( "SystemHandler: unknown action '{$actionId}'." ) ),
		};
	}

	// --- READ ACTIONS ---

	private function getSiteStatus( array $params ): array {
		return array(
			'wp_version'     => get_bloginfo( 'version' ),
			'php_version'    => phpversion(),
			'ssl_active'     => is_ssl(),
			'active_theme'   => wp_get_theme()->get( 'Name' ),
			'active_plugins' => count( get_option( 'active_plugins', array() ) ),
			'site_health'    => 'good',
			'db_size_mb'     => $this->getDbSize(),
			'multisite'      => is_multisite(),
		);
	}

	/**
	 * Lists all abilities registered in the WordPress Abilities API.
	 *
	 * Supports optional filters so chat agents can discover tools efficiently
	 * without dumping the full registry into context.
	 *
	 * @param array $params Optional query, category, limit, names_only.
	 * @return array Map with total count, per-category breakdown, and tool list.
	 */
	private function getDiscoveredAbilities( array $params ): array {
		$registry  = new \LolaCore\Actions\AbilitiesRegistry();
		$abilities = $registry->getRegisteredAbilities();

		$query      = isset( $params['query'] ) ? strtolower( trim( (string) $params['query'] ) ) : '';
		$category   = isset( $params['category'] ) ? strtolower( trim( (string) $params['category'] ) ) : '';
		$limit      = isset( $params['limit'] ) ? (int) $params['limit'] : 20;
		$names_only = ! empty( $params['names_only'] );

		if ( $limit <= 0 ) {
			$limit = 20;
		}
		$limit = min( $limit, 50 );

		$by_category = array();
		$tool_list   = array();

		foreach ( $abilities as $name => $schema ) {
			$cat = $schema['category'] ?? 'uncategorized';

			if ( $category !== '' && $cat !== $category ) {
				continue;
			}

			$description = (string) ( $schema['description'] ?? '' );
			if ( $query !== '' ) {
				$haystack = strtolower( $name . ' ' . $description );
				if ( strpos( $haystack, $query ) === false ) {
					continue;
				}
			}

			if ( ! isset( $by_category[ $cat ] ) ) {
				$by_category[ $cat ] = array();
			}
			$by_category[ $cat ][] = $name;

			if ( $names_only ) {
				$tool_list[] = array(
					'name'     => $name,
					'category' => $cat,
				);
			} else {
				$tool_list[] = array(
					'name'        => $name,
					'category'    => $cat,
					'description' => $description,
				);
			}

			if ( count( $tool_list ) >= $limit ) {
				break;
			}
		}

		ksort( $by_category );

		$result = array(
			'total_abilities' => count( $abilities ),
			'categories'      => array_map( 'count', $by_category ),
			'tools'           => $tool_list,
			'filtered_count'  => count( $tool_list ),
		);

		if ( $query !== '' || $category !== '' ) {
			$result['filters'] = array_filter(
				array(
					'query'    => $query !== '' ? $query : null,
					'category' => $category !== '' ? $category : null,
					'limit'    => $limit,
				)
			);
			$result['hint'] = 'Call requestAdditionalCategories with the category slug to add those tools to your session.';
		}

		return $result;
	}

	private function getOptions( array $params ): array {
		$keys = $params['keys'] ?? array();
		$data = array();
		foreach ( $keys as $k ) {
			// Skip sensitive keys.
			if ( strpos( $k, 'password' ) !== false || strpos( $k, 'key' ) !== false || strpos( $k, 'secret' ) !== false ) {
				continue;
			}
			$data[ $k ] = get_option( $k );
		}
		return $data;
	}

	private function getDatabaseStatus( array $params ): array {
		global $wpdb;
		$revisions  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'revision'" );
		$transients = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->options WHERE option_name LIKE '_transient_%'" );
		return array(
			'size_mb'    => $this->getDbSize(),
			'revisions'  => $revisions,
			'transients' => $transients,
		);
	}

	private function getSiteSettings( array $params ): array {
		$group    = $params['group'] ?? 'all';
		$settings = array();

		if ( $group === 'all' || $group === 'general' ) {
			$settings['general'] = array(
				'blogname'        => get_option( 'blogname' ),
				'blogdescription' => get_option( 'blogdescription' ),
				'siteurl'         => get_option( 'siteurl' ),
				'home'            => get_option( 'home' ),
				'admin_email'     => get_option( 'admin_email' ),
				'timezone_string' => get_option( 'timezone_string' ),
				'gmt_offset'      => get_option( 'gmt_offset' ),
				'date_format'     => get_option( 'date_format' ),
				'time_format'     => get_option( 'time_format' ),
				'start_of_week'   => get_option( 'start_of_week' ),
				'WPLANG'          => get_option( 'WPLANG' ),
			);
		}

		if ( $group === 'all' || $group === 'reading' ) {
			$settings['reading'] = array(
				'posts_per_page'  => get_option( 'posts_per_page' ),
				'posts_per_rss'   => get_option( 'posts_per_rss' ),
				'rss_use_excerpt' => get_option( 'rss_use_excerpt' ),
				'show_on_front'   => get_option( 'show_on_front' ),
				'page_on_front'   => get_option( 'page_on_front' ),
				'page_for_posts'  => get_option( 'page_for_posts' ),
				'blog_public'     => get_option( 'blog_public' ),
			);
		}

		if ( $group === 'all' || $group === 'discussion' ) {
			$settings['discussion'] = array(
				'default_comment_status'      => get_option( 'default_comment_status' ),
				'require_name_email'          => get_option( 'require_name_email' ),
				'comment_moderation'          => get_option( 'comment_moderation' ),
				'comment_previously_approved' => get_option( 'comment_previously_approved', 1 ),
				'moderation_keys'             => ! empty( get_option( 'moderation_keys' ) ) ? '(configured)' : '(empty)',
				'disallowed_keys'             => ! empty( get_option( 'disallowed_keys' ) ) ? '(configured)' : '(empty)',
				'comments_per_page'           => get_option( 'comments_per_page' ),
				'thread_comments'             => get_option( 'thread_comments' ),
				'thread_comments_depth'       => get_option( 'thread_comments_depth' ),
			);
		}

		if ( $group === 'all' || $group === 'permalinks' ) {
			$settings['permalinks'] = array(
				'permalink_structure' => get_option( 'permalink_structure' ),
				'category_base'       => get_option( 'category_base' ),
				'tag_base'            => get_option( 'tag_base' ),
			);
		}

		return $settings;
	}

	private function getThemeInfo( array $params ): array {
		$active  = wp_get_theme();
		$themes  = wp_get_themes();
		$updates = get_site_transient( 'update_themes' );

		$active_data = array(
			'name'       => $active->get( 'Name' ),
			'stylesheet' => $active->get_stylesheet(),
			'template'   => $active->get_template(),
			'version'    => $active->get( 'Version' ),
			'author'     => $active->get( 'Author' ),
			'is_child'   => $active->get_stylesheet() !== $active->get_template(),
			'parent'     => $active->get_stylesheet() !== $active->get_template() ? $active->get_template() : null,
		);

		$installed = array();
		foreach ( $themes as $stylesheet => $theme ) {
			$has_update  = isset( $updates->response[ $stylesheet ] );
			$installed[] = array(
				'name'        => $theme->get( 'Name' ),
				'stylesheet'  => $stylesheet,
				'version'     => $theme->get( 'Version' ),
				'is_active'   => $stylesheet === $active->get_stylesheet(),
				'has_update'  => $has_update,
				'new_version' => $has_update ? ( $updates->response[ $stylesheet ]['new_version'] ?? null ) : null,
				'is_child'    => $stylesheet !== $theme->get_template(),
				'parent'      => $stylesheet !== $theme->get_template() ? $theme->get_template() : null,
			);
		}

		return array(
			'active_theme'    => $active_data,
			'installed_count' => count( $installed ),
			'themes'          => $installed,
		);
	}

	// --- WRITE ACTIONS ---

	/**
	 * Batch size for DELETE operations.
	 *
	 * 1000 strikes a balance between speed and table-lock duration
	 * on shared hosting environments.
	 */
	private const BATCH_SIZE = 1000;

	/**
	 * Fraction of max_execution_time to use before aborting safely.
	 *
	 * At 0.80 we leave a 20% buffer for the response serialization
	 * and any remaining PHP shutdown work.
	 */
	private const TIME_LIMIT_RATIO = 0.80;

	/**
	 * Cleans the WordPress database: revisions, spam comments,
	 * orphaned postmeta, and expired transients — all in safe batches.
	 *
	 * @param array $params Unused (tool takes no parameters).
	 * @param bool  $dryRun When true, returns counts without deleting.
	 * @return array Result payload with cleaned counts and space delta.
	 */
	private function optimizeDatabase( array $params, bool $dryRun ): array {
		global $wpdb;

		$revisions_count  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'revision'" );
		$spam_count       = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_approved = 'spam'" );
		$transients_count = (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d",
				'_transient_timeout_%',
				time()
			)
		);

		if ( $dryRun ) {
			return array(
				'will_clean' => array(
					'revisions'          => $revisions_count,
					'spam'               => $spam_count,
					'expired_transients' => $transients_count,
				),
				'db_size_mb' => $this->getDbSize(),
			);
		}

		$db_size_before    = $this->getDbSize();
		$errors            = array();
		$batches_executed  = 0;
		$completed         = true;
		$time_guard        = $this->getTimeGuard();

		// --- 1. Delete revisions in batches ---
		$revisions_deleted = 0;
		while ( $revisions_deleted < $revisions_count ) {
			if ( $time_guard > 0 && microtime( true ) >= $time_guard ) {
				$completed = false;
				$errors[]  = sprintf(
					/* translators: 1: number */
					__( 'Time limit approaching — stopped safely after %d revisions. Run again to continue.', 'lolacore' ),
					$revisions_deleted
				);
				break;
			}

			$result = $this->deleteBatched(
				"DELETE FROM $wpdb->posts WHERE post_type = 'revision' LIMIT %d",
				self::BATCH_SIZE
			);

			if ( $result['error'] !== '' ) {
				/* translators: 1: value */
				$errors[] = sprintf( __( 'Revision batch error: %s', 'lolacore' ), $result['error'] );
				$completed = false;
				break;
			}

			$revisions_deleted += $result['affected'];
			++$batches_executed;

			// No more rows to delete — stop early even if count was higher.
			if ( $result['affected'] < self::BATCH_SIZE ) {
				break;
			}
		}

		// --- 2. Clean orphaned postmeta (revisions leave ghost rows) ---
		$orphaned_meta_deleted = 0;
		if ( $completed && $revisions_deleted > 0 ) {
			while ( true ) {
				if ( $time_guard > 0 && microtime( true ) >= $time_guard ) {
					$completed = false;
					$errors[]  = __( 'Time limit approaching during orphaned meta cleanup. Run again to continue.', 'lolacore' );
					break;
				}

				$result = $this->deleteBatched(
					"DELETE pm FROM $wpdb->postmeta pm LEFT JOIN $wpdb->posts p ON pm.post_id = p.ID WHERE p.ID IS NULL LIMIT %d",
					self::BATCH_SIZE
				);

				if ( $result['error'] !== '' ) {
					/* translators: 1: value */
					$errors[] = sprintf( __( 'Orphaned meta error: %s', 'lolacore' ), $result['error'] );
					break;
				}

				$orphaned_meta_deleted += $result['affected'];
				++$batches_executed;

				if ( $result['affected'] < self::BATCH_SIZE ) {
					break;
				}
			}
		}

		// --- 3. Delete spam comments in batches ---
		$spam_deleted = 0;
		if ( $completed ) {
			while ( $spam_deleted < $spam_count ) {
				if ( $time_guard > 0 && microtime( true ) >= $time_guard ) {
					$completed = false;
					$errors[]  = sprintf(
						/* translators: 1: number */
						__( 'Time limit approaching — stopped safely after %d spam comments. Run again to continue.', 'lolacore' ),
						$spam_deleted
					);
					break;
				}

				$result = $this->deleteBatched(
					"DELETE FROM $wpdb->comments WHERE comment_approved = 'spam' LIMIT %d",
					self::BATCH_SIZE
				);

				if ( $result['error'] !== '' ) {
					/* translators: 1: value */
					$errors[] = sprintf( __( 'Spam batch error: %s', 'lolacore' ), $result['error'] );
					$completed = false;
					break;
				}

				$spam_deleted += $result['affected'];
				++$batches_executed;

				if ( $result['affected'] < self::BATCH_SIZE ) {
					break;
				}
			}
		}

		// --- 4. Clean expired transients ---
		$transients_deleted = 0;
		if ( $completed ) {
			$now = time();
			while ( true ) {
				if ( $time_guard > 0 && microtime( true ) >= $time_guard ) {
					$completed = false;
					$errors[]  = __( 'Time limit approaching during transient cleanup. Run again to continue.', 'lolacore' );
					break;
				}

				// Find expired timeout keys, then delete both the timeout and the value.
				$expired_keys = $wpdb->get_col(
					$wpdb->prepare(
						"SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d LIMIT %d",
						$wpdb->esc_like( '_transient_timeout_' ) . '%',
						$now,
						self::BATCH_SIZE
					)
				);

				if ( empty( $expired_keys ) ) {
					break;
				}

				foreach ( $expired_keys as $timeout_key ) {
					$transient_key = str_replace( '_transient_timeout_', '_transient_', $timeout_key );
					$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name IN (%s, %s)", $timeout_key, $transient_key ) );
				}

				$transients_deleted += count( $expired_keys );
				++$batches_executed;

				if ( count( $expired_keys ) < self::BATCH_SIZE ) {
					break;
				}
			}
		}

		$db_size_after      = $this->getDbSize();
		$space_recovered_mb = round( $db_size_before - $db_size_after, 2 );

		return array(
			'cleaned'            => array(
				'revisions'          => $revisions_deleted,
				'spam'               => $spam_deleted,
				'orphaned_meta'      => $orphaned_meta_deleted,
				'expired_transients' => $transients_deleted,
			),
			'space_recovered_mb' => max( 0.0, $space_recovered_mb ),
			'batches_executed'   => $batches_executed,
			'completed'          => $completed,
			'errors'             => $errors,
		);
	}

	/**
	 * Executes a DELETE query with a LIMIT placeholder and returns
	 * the number of affected rows plus any error message.
	 *
	 * @param string $sql_template SQL with a single %d placeholder for LIMIT.
	 * @param int    $batch_size   Number of rows to delete per batch.
	 * @return array{affected: int, error: string}
	 */
	private function deleteBatched( string $sql_template, int $batch_size ): array {
		global $wpdb;

		$wpdb->last_error = '';

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql_template is built internally with table constants.
		$affected = $wpdb->query( $wpdb->prepare( $sql_template, $batch_size ) );

		return array(
			'affected' => $affected !== false ? (int) $affected : 0,
			'error'    => $wpdb->last_error,
		);
	}

	/**
	 * Returns the absolute timestamp at which we should stop working,
	 * or 0 if max_execution_time is unlimited.
	 *
	 * @return float Unix timestamp with microseconds, or 0.0 for no limit.
	 */
	private function getTimeGuard(): float {
		$max_time = (int) ini_get( 'max_execution_time' );
		if ( $max_time <= 0 ) {
			return 0.0;
		}
		// REQUEST_TIME_FLOAT is set at the very start of the PHP process.
		$request_start = filter_input( INPUT_SERVER, 'REQUEST_TIME_FLOAT', FILTER_VALIDATE_FLOAT );
		$start         = is_float( $request_start ) ? $request_start : microtime( true );
		return (float) $start + ( $max_time * self::TIME_LIMIT_RATIO );
	}

	private function updateOption( array $params, bool $dryRun ): array {
		$key   = $params['key'] ?? '';
		$value = $params['value'] ?? '';

		if ( in_array( $key, self::PHP_CONSTANTS, true ) ) {
			throw new \Exception(
				esc_html(
					sprintf(
						/* translators: %s: PHP constant name from wp-config.php */
						__( "'%s' is a PHP constant defined in wp-config.php, not a WordPress option. update_option() cannot change it. The site owner must edit wp-config.php manually via FTP, cPanel File Manager, or SSH.", 'lolacore' ),
						$key
					)
				)
			);
		}

		// Block the role/capability map ({$prefix}user_roles) regardless of prefix:
		// writing it could grant arbitrary capabilities to any role.
		$isRoleMap = stripos( $key, 'user_roles' ) !== false;

		if ( in_array( $key, self::OPTION_BLOCKLIST, true ) || strpos( $key, 'db_' ) === 0 || $isRoleMap ) {
			/* translators: 1: value */
			throw new \Exception( sprintf( __( "Updating option '%s' is blocked for security reasons.", 'lolacore' ), $key ) );
		}

		$current = get_option( $key );

		if ( $dryRun ) {
			return array(
				'key'            => $key,
				'current_value'  => $current,
				'proposed_value' => $value,
			);
		}

		update_option( $key, $value );
		return array(
			'success' => true,
			'key'     => $key,
			'updated' => true,
		);
	}

	private function purgeCache( array $params, bool $dryRun ): array {
		$cache_detected = function_exists( 'w3tc_flush_all' )
			|| function_exists( 'rocket_clean_domain' )
			|| function_exists( 'wp_cache_clear_cache' );

		if ( $dryRun ) {
			return array(
				'cache_detected' => $cache_detected,
				'estimated_size' => 'Unknown',
			);
		}

		if ( ! $cache_detected ) {
			return array( 'success' => false, 'message' => __( 'No recognized caching plugin detected.', 'lolacore' ) );
		}

		if ( function_exists( 'w3tc_flush_all' ) ) {
			w3tc_flush_all();
		} elseif ( function_exists( 'rocket_clean_domain' ) ) {
			rocket_clean_domain();
		} elseif ( function_exists( 'wp_cache_clear_cache' ) ) {
			wp_cache_clear_cache();
		}

		return array( 'success' => true, 'message' => __( 'Cache purged successfully.', 'lolacore' ) );
	}

	private function cleanupCronJobs( array $params, bool $dryRun ): array {
		$action = $params['action'] ?? 'audit';
		$hooks  = $params['hooks'] ?? array();

		$crons    = _get_cron_array();
		$all_jobs = array();

		if ( ! empty( $crons ) ) {
			foreach ( $crons as $timestamp => $cron_hooks ) {
				foreach ( $cron_hooks as $hook => $events ) {
					foreach ( $events as $event ) {
						$all_jobs[] = array(
							'hook'         => $hook,
							'timestamp'    => $timestamp,
							'next_run'     => wp_date( 'Y-m-d H:i:s', $timestamp ),
							'schedule'     => $event['schedule'] ?? 'single',
							'has_callback' => (bool) has_action( $hook ),
						);
					}
				}
			}
		}

		if ( $action === 'audit' ) {
			$orphaned = array_filter( $all_jobs, fn( $j ) => ! $j['has_callback'] );
			return array(
				'total_jobs'    => count( $all_jobs ),
				'orphaned_jobs' => count( $orphaned ),
				'jobs'          => $all_jobs,
				'orphaned'      => array_values( $orphaned ),
			);
		}

		// clean_orphaned action.
		$orphaned = array();
		foreach ( $all_jobs as $job ) {
			if ( ! $job['has_callback'] ) {
				if ( ! empty( $hooks ) && ! in_array( $job['hook'], $hooks, true ) ) {
					continue;
				}
				$orphaned[] = $job;
			}
		}

		if ( empty( $orphaned ) ) {
			return array( 'success' => true, 'message' => __( 'No orphaned cron jobs found.', 'lolacore' ), 'cleaned' => 0 );
		}

		if ( $dryRun ) {
			return array(
				'action'   => __( 'Will remove orphaned cron hooks', 'lolacore' ),
				'count'    => count( $orphaned ),
				'orphaned' => $orphaned,
			);
		}

		$cleaned = 0;
		foreach ( $orphaned as $job ) {
			wp_unschedule_event( $job['timestamp'], $job['hook'] );
			$cleaned++;
		}

		return array(
			'success' => true,
			/* translators: 1: number */
			'message' => sprintf( __( '%d orphaned cron job(s) removed.', 'lolacore' ), $cleaned ),
			'cleaned' => $cleaned,
		);
	}

	// --- HELPERS ---

	/**
	 * Returns the total database size in megabytes.
	 */
	private function getDbSize(): float {
		global $wpdb;
		$size = $wpdb->get_var( $wpdb->prepare( "SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = %s", DB_NAME ) );
		return round( ( (int) $size ) / 1024 / 1024, 2 );
	}
}

