<?php
/**
 * CacheLoop – Admin Settings Controller
 *
 * Registers the settings page, handles option saving via Settings API,
 * handles the "Clear Cache" AJAX action, and enqueues admin assets.
 *
 * @package CacheLoop
 */

// Security: Prevent direct file access.
defined( 'ABSPATH' ) || exit;

/**
 * Class CACHELOOP_Admin
 *
 * Manages the CacheLoop admin settings UI.
 */
class CACHELOOP_Admin {

	/**
	 * Current plugin settings.
	 *
	 * @var array
	 */
	private $settings;

	/**
	 * The slug used for the settings page and menu.
	 *
	 * @var string
	 */
	const PAGE_SLUG = 'cacheloop';

	/**
	 * The option name stored in wp_options.
	 *
	 * @var string
	 */
	const OPTION_KEY = 'cacheloop_settings';

	/**
	 * Constructor.
	 *
	 * @param array $settings Current plugin settings.
	 */
	public function __construct( $settings ) {
		$this->settings = $settings;

		// Register admin menu page early (priority 5) so Freemius can attach its submenus to it.
		add_action( 'admin_menu', array( $this, 'cl_register_menu' ), 5 );

		// Register settings via Settings API.
		add_action( 'admin_init', array( $this, 'cl_register_settings' ) );

		// Enqueue admin assets only on our settings page.
		add_action( 'admin_enqueue_scripts', array( $this, 'cl_enqueue_assets' ) );

		// Admin POST actions for Import/Export settings.
		add_action( 'admin_post_cacheloop_export_settings', array( $this, 'cl_handle_export_settings' ) );
		add_action( 'admin_post_cacheloop_import_settings', array( $this, 'cl_handle_import_settings' ) );
		add_action( 'admin_post_cacheloop_reset_fs', array( $this, 'cl_handle_reset_fs' ) );
		add_action( 'admin_post_cacheloop_download_nginx_config', array( $this, 'cl_handle_download_nginx_config' ) );
		add_action( 'wp_ajax_cacheloop_save_pagespeed', array( $this, 'cl_save_pagespeed' ) );
		add_action( 'wp_ajax_cacheloop_run_pagespeed', array( $this, 'cl_run_pagespeed' ) );

		// AJAX: Clear all cache.
		add_action( 'wp_ajax_cacheloop_clear_cache', array( $this, 'cl_ajax_clear_cache' ) );

		// Admin toolbar "Clear All Cache" button — a direct link (not AJAX),
		// so it needs its own admin_post_ handler that redirects back instead
		// of returning JSON.
		add_action( 'admin_post_cacheloop_clear_cache', array( $this, 'cl_handle_clear_cache_toolbar' ) );

		// AJAX: View debug log.
		add_action( 'wp_ajax_cacheloop_view_debug_log', array( $this, 'cl_ajax_view_debug_log' ) );

		// AJAX: Clear debug log.
		add_action( 'wp_ajax_cacheloop_clear_debug_log', array( $this, 'cl_ajax_clear_debug_log' ) );

		// AJAX: Get cache metrics.
		add_action( 'wp_ajax_cacheloop_get_metrics', array( $this, 'cl_ajax_get_metrics' ) );

		// AJAX: Get preloader status (fallback when Pro is not loaded).
		// Note: CACHELOOP_Preloader registers its own wp_ajax_cacheloop_get_preload_status.
		// We register this only as a safety net if the Pro class isn't loaded.
		if ( ! function_exists( 'cacheloop_fs' ) || ! cacheloop_fs()->can_use_premium_code() ) {
			add_action( 'wp_ajax_cacheloop_get_preload_status', array( $this, 'cl_ajax_get_preload_status' ) );
		}

		// AJAX: Quick-enable feature from Pro Analysis.
		add_action( 'wp_ajax_cacheloop_quick_enable_feature', array( $this, 'cl_ajax_quick_enable_feature' ) );

		// AJAX: Get server info & system check.
		add_action( 'wp_ajax_cacheloop_get_server_info', array( $this, 'cl_ajax_get_server_info' ) );

		// Front-end: Purge single page from toolbar.
		add_action( 'init', array( $this, 'handle_purge_this_page' ) );

		// AJAX: Run DB optimizer manually (Pro). Pure dispatcher — the nonce
		// check, capability check, and license check all live in
		// CACHELOOP_DB_Optimizer::ajax_run_now() (includes/class-cl-db-optimizer__premium_only.php),
		// which is stripped from the Free build.
		if ( function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code() ) {
			add_action(
				'wp_ajax_cacheloop_run_db_optimizer',
				function () {
					require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-db-optimizer__premium_only.php';
					CACHELOOP_DB_Optimizer::ajax_run_now();
				}
			);
		}

		// Add "Settings" link to the Plugins list page.
		add_filter( 'plugin_action_links_' . plugin_basename( CACHELOOP_PLUGIN_DIR . 'cacheloop.php' ), array( $this, 'cl_add_action_links' ) );

		// Auto-flush cache when settings are saved.
		add_action( 'update_option_' . self::OPTION_KEY, array( $this, 'flush_cache_on_setting_update' ), 10, 3 );

		// Admin toolbar: "Clear Cache" and "Purge This Page" buttons.
		add_action( 'admin_bar_menu', array( $this, 'add_admin_toolbar_buttons' ), 999 );
	}

	/**
	 * Flushes the page cache automatically whenever the settings are updated.
	 *
	 * @param mixed  $old_value The old option value.
	 * @param mixed  $new_value The new option value.
	 * @param string $option_name Option name.
	 */
	public function handle_purge_this_page() {
		if ( ! isset( $_GET['cl_purge_page'] ) || ! isset( $_GET['cl_post_id'] ) ) {
			return;
		}

		check_admin_referer( 'cacheloop_admin_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$post_id = absint( $_GET['cl_post_id'] );

		// Purge this specific page.
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
		$page_cache = new CACHELOOP_Page_Cache( array() );

		$page_cache->purge_post_cache( $post_id );

		// Redirect back to the page with a notice.
		$redirect_url = remove_query_arg( array( 'cl_purge_page', 'cl_post_id', '_wpnonce' ) );
		wp_safe_redirect( add_query_arg( 'cl_purged', '1', $redirect_url ) );
		exit;
	}

	/**
	 * Flushes cache when settings are updated.
	 *
	 * @param mixed  $old_value The old option value.
	 * @param mixed  $new_value The new option value.
	 * @param string $option_name Option name.
	 */
	public function flush_cache_on_setting_update( $old_value, $new_value, $option_name ) {
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
		CACHELOOP_Page_Cache::flush_all();
	}

	// ---------------------------------------------------------------------------
	// Menu Registration
	// ---------------------------------------------------------------------------

	/**
	 * Registers the CacheLoop top-level menu page.
	 *
	 * @return void
	 */
	public function cl_register_menu() {
		// Premium and free ship the same menu strings, so without this the
		// sidebar is indistinguishable from the free plugin after an update
		// to Pro (see cacheloop_append_pro_suffix_to_plugin_name() for the
		// same fix applied to the Plugins list). Gated on can_use_premium_code()
		// (valid license/trial), matching $is_pro elsewhere in this class.
		$pro_suffix = ( function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code() ) ? ' Pro' : '';

		add_menu_page(
			__( 'CacheLoop Settings', 'cacheloop-simple-page-cache-speed-optimization' ) . $pro_suffix,
			__( 'CacheLoop', 'cacheloop-simple-page-cache-speed-optimization' ) . $pro_suffix,
			'manage_options',
			self::PAGE_SLUG,
			array( $this, 'cl_render_settings_page' ),
			'dashicons-performance',
			80
		);
	}

	// ---------------------------------------------------------------------------
	// Settings API
	// ---------------------------------------------------------------------------

	/**
	 * Registers the settings group and sanitization callback.
	 *
	 * @return void
	 */
	public function cl_register_settings() {
		register_setting(
			'cacheloop_settings_group',
			self::OPTION_KEY,
			array(
				'sanitize_callback' => array( $this, 'cl_sanitize_settings' ),
				'default'           => array(
					'page_cache'        => 1,
					'browser_cache'     => 1,
					'gzip'              => 1,
					'lazy_load'         => 1,
					'minify'            => 0,
					'delay_js'          => 0,
					'db_optimizer'       => 0,
					'preloader'          => 0,
					'heartbeat'          => 0,
					'pagespeed_api_key'  => '',
					'disable_emojis'     => 0,
					'local_google_fonts' => 0,
					'instant_page'       => 0,
					'preload_lcp_images' => 0,
					'preconnect_domains' => '',
					'object_cache_enabled'  => 0,
					'object_cache_driver'   => 'redis',
					'object_cache_host'     => '127.0.0.1',
					'object_cache_port'     => '6379',
					'object_cache_password' => '',
					'object_cache_ttl'      => 3600,
					'image_optimize'          => 0,
					'image_optimize_on_upload' => 0,
					'image_optimize_quality'  => 82,
					'image_optimize_method'   => 'auto',
					'bloat_disable_xmlrpc'       => 0,
					'bloat_disable_dashicons'    => 0,
					'bloat_disable_jquery_migrate' => 0,
					'bloat_disable_block_css'    => 0,
					'bloat_disable_cart_fragment' => 0,
					'bloat_disable_woo_assets'   => 0,
					'bloat_disable_rss'          => 0,
					'bloat_disable_oembeds'      => 0,
					'bloat_disable_gutenberg'    => 0,
					'bloat_limit_revisions'      => '',
					'combine_js_footer'          => 0,
				),
			)
		);
	}

	/**
	 * Reduces a CDN Integration input to a bare, valid hostname.
	 *
	 * Accepts either a plain domain ("cdn.example.com") or a full URL
	 * ("https://cdn.example.com/") and returns just the host — never a
	 * scheme, path, query string, or fragment. Anything that doesn't parse
	 * down to a valid hostname (including "javascript:", "data:", etc.) is
	 * rejected and treated as empty, since this value is later used to
	 * rewrite every asset URL on the front end.
	 *
	 * @param  string $raw Raw value from the settings form.
	 * @return string      A valid hostname, or '' if the input didn't parse to one.
	 */
	private function cl_sanitize_cdn_domain( $raw ) {
		$raw = trim( wp_unslash( (string) $raw ) );
		if ( '' === $raw ) {
			return '';
		}

		// Accept a bare hostname or a full URL and reduce to just the host.
		$host = $raw;
		if ( false !== strpos( $raw, '//' ) || preg_match( '#^[a-z][a-z0-9+.-]*:#i', $raw ) ) {
			$host = (string) wp_parse_url( $raw, PHP_URL_HOST );
		}
		$host = rtrim( $host, '/' );

		// Hostname-only allowlist (letters, digits, dots, hyphens, at least
		// one dot). Rejects query strings, spaces, and any non-hostname value.
		if ( '' === $host || ! preg_match( '/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i', $host ) ) {
			return '';
		}

		return $host;
	}

	/**
	 * Sanitizes and validates form input before saving to the database.
	 *
	 * @param  array $input Raw form input.
	 * @return array        Sanitized settings array.
	 */
	public function cl_sanitize_settings( $input ) {
		$clean = array();

		$boolean_keys = array(
			'page_cache',
			'browser_cache',
			'gzip',
			'lazy_load',
			'minify',
			'defer_css',
			'delay_js',
			'disable_animations',
			'db_optimizer',
			'preloader',
			'heartbeat',
			'disable_emojis',
			'local_google_fonts',
			'instant_page',
			'preload_lcp_images',
			'object_cache_enabled',
			'image_optimize',
			'image_optimize_on_upload',
			'bloat_disable_xmlrpc',
			'bloat_disable_dashicons',
			'bloat_disable_jquery_migrate',
			'bloat_disable_block_css',
			'bloat_disable_cart_fragment',
			'bloat_disable_woo_assets',
			'bloat_disable_rss',
			'bloat_disable_oembeds',
			'bloat_disable_gutenberg',
			'combine_js_footer',
			'varnish_enabled',
			'cf_enabled',
			'unused_css_enabled',
			'critical_css_enabled',
		);

		foreach ( $boolean_keys as $key ) {
			$clean[ $key ] = isset( $input[ $key ] ) ? 1 : 0;
		}

		// Varnish Cache Purge: host + port for the PURGE/BAN request.
		$clean['varnish_host'] = isset( $input['varnish_host'] )
			? sanitize_text_field( wp_unslash( $input['varnish_host'] ) )
			: '';
		$clean['varnish_port'] = isset( $input['varnish_port'] )
			? max( 1, min( 65535, (int) $input['varnish_port'] ) )
			: 6081;

		// Cloudflare Cache Purge: API token + zone ID.
		$clean['cf_api_token'] = isset( $input['cf_api_token'] )
			? sanitize_text_field( wp_unslash( $input['cf_api_token'] ) )
			: '';
		$clean['cf_zone_id'] = isset( $input['cf_zone_id'] )
			? sanitize_text_field( wp_unslash( $input['cf_zone_id'] ) )
			: '';

		// Unused CSS Removal: newline-separated list of selectors/classes to keep.
		$clean['unused_css_safelist'] = isset( $input['unused_css_safelist'] )
			? sanitize_textarea_field( $input['unused_css_safelist'] )
			: '';

		// Cache TTL: integer in seconds, min 3600 (1h), max 604800 (7 days).
		$clean['cache_ttl'] = isset( $input['cache_ttl'] )
			? max( 3600, min( 604800, (int) $input['cache_ttl'] ) )
			: DAY_IN_SECONDS;

		// Device split: boolean 0/1.
		$clean['cache_device_split'] = isset( $input['cache_device_split'] ) ? 1 : 0;

		// LCP image count: integer 1-10.
		$clean['lcp_image_count'] = isset( $input['lcp_image_count'] )
			? max( 1, min( 10, (int) $input['lcp_image_count'] ) )
			: 1;

		// CDN: boolean enabled + hostname-only domain (never a full URL —
		// no scheme, path, query, or fragment allowed).
		$clean['cdn_enabled'] = isset( $input['cdn_enabled'] ) ? 1 : 0;
		$clean['cdn_domain']  = $clean['cdn_enabled'] && ! empty( $input['cdn_domain'] )
			? $this->cl_sanitize_cdn_domain( $input['cdn_domain'] )
			: '';

		// Cache whitelist/blacklist: textarea fields (newline-separated patterns).
		$clean['cache_whitelist']         = isset( $input['cache_whitelist'] ) ? wp_strip_all_tags( wp_unslash( $input['cache_whitelist'] ) ) : '';
		$clean['cache_whitelist_enabled'] = isset( $input['cache_whitelist_enabled'] ) ? 1 : 0;
		$clean['cache_blacklist']         = isset( $input['cache_blacklist'] ) ? wp_strip_all_tags( wp_unslash( $input['cache_blacklist'] ) ) : '';

		// Generic cookie-based bypass: comma-separated cookie names.
		$clean['bypass_cookies'] = isset( $input['bypass_cookies'] )
			? sanitize_text_field( wp_unslash( $input['bypass_cookies'] ) )
			: '';

		// Delay JS exclusions: plain text, one entry per line.
		$clean['delay_js_exclusions'] = isset( $input['delay_js_exclusions'] )
			? sanitize_textarea_field( $input['delay_js_exclusions'] )
			: '';

		$clean['lcp_preload_urls'] = isset( $input['lcp_preload_urls'] )
			? sanitize_textarea_field( $input['lcp_preload_urls'] )
			: '';

		// Preconnect: user-supplied extra domains, one per line.
		$clean['preconnect_domains'] = isset( $input['preconnect_domains'] )
			? sanitize_textarea_field( $input['preconnect_domains'] )
			: '';

		// Object Cache: driver + connection details.
		$clean['object_cache_driver'] = ( isset( $input['object_cache_driver'] ) && 'memcached' === $input['object_cache_driver'] )
			? 'memcached'
			: 'redis';
		$clean['object_cache_host'] = isset( $input['object_cache_host'] ) && '' !== trim( $input['object_cache_host'] )
			? sanitize_text_field( wp_unslash( $input['object_cache_host'] ) )
			: '127.0.0.1';
		$clean['object_cache_port'] = isset( $input['object_cache_port'] ) && is_numeric( $input['object_cache_port'] )
			? (string) (int) $input['object_cache_port']
			: ( 'memcached' === $clean['object_cache_driver'] ? '11211' : '6379' );
		// A password/token field must be preserved as-is: sanitize_text_field()
		// strips tags and collapses whitespace, which can silently corrupt an
		// otherwise-valid password character-for-character. wp_unslash() alone
		// (undo magic-quotes escaping) is the correct sanitization here.
		$clean['object_cache_password'] = isset( $input['object_cache_password'] )
			? wp_unslash( $input['object_cache_password'] )
			: '';
		$clean['object_cache_ttl'] = isset( $input['object_cache_ttl'] )
			? max( 60, min( DAY_IN_SECONDS, (int) $input['object_cache_ttl'] ) )
			: 3600;

		// Image Optimization: WebP quality, 1-100.
		$clean['image_optimize_quality'] = isset( $input['image_optimize_quality'] )
			? max( 1, min( 100, (int) $input['image_optimize_quality'] ) )
			: 82;

		// Image Optimization: compression library — 'auto', 'gd', or 'imagick'.
		$clean['image_optimize_method'] = ( isset( $input['image_optimize_method'] ) && in_array( $input['image_optimize_method'], array( 'gd', 'imagick' ), true ) )
			? $input['image_optimize_method']
			: 'auto';

		// Bloat Remover: revision limit — empty string means "leave WP default".
		$clean['bloat_limit_revisions'] = ( isset( $input['bloat_limit_revisions'] ) && is_numeric( $input['bloat_limit_revisions'] ) )
			? (string) max( 0, (int) $input['bloat_limit_revisions'] )
			: '';

		// Object Cache drop-in lifecycle: install/remove wp-content/object-cache.php
		// to match the enabled toggle. Never overwrites a foreign drop-in.
		// The __premium_only class file is stripped from the Free build, so it
		// must never be required unconditionally — every Free user saving
		// settings would otherwise hit a fatal "failed to open" error.
		$object_cache_class_file = CACHELOOP_PLUGIN_DIR . 'includes/class-cl-object-cache__premium_only.php';
		if ( file_exists( $object_cache_class_file ) ) {
			require_once $object_cache_class_file;
			if ( ! empty( $clean['object_cache_enabled'] ) && function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code() ) {
				CACHELOOP_Object_Cache::install_dropin();
			} else {
				CACHELOOP_Object_Cache::remove_dropin();
			}
		} else {
			// Free build: the Pro class isn't shipped, so there's nothing to
			// install. Still clean up a drop-in left over from a previous Pro
			// install, the same safe way the Freemius after_uninstall hook
			// does — only ever touching a drop-in that is provably ours.
			$fs        = function_exists( 'cacheloop_get_filesystem' ) ? cacheloop_get_filesystem() : false;
			$oc_dropin = WP_CONTENT_DIR . '/object-cache.php';
			if ( $fs && $fs->exists( $oc_dropin ) ) {
				$oc_head = $fs->get_contents( $oc_dropin );
				if ( is_string( $oc_head ) && false !== strpos( substr( $oc_head, 0, 4096 ), 'CacheLoop Object Cache Drop-in' ) ) {
					$fs->delete( $oc_dropin );
				}
			}
		}

		$clean['pagespeed_api_key'] = isset( $input['pagespeed_api_key'] )
			? sanitize_text_field( $input['pagespeed_api_key'] )
			: '';

		// Asset Manager (Exclusions)
		$scanned_assets = get_option(
			'cacheloop_scanned_assets',
			array(
				'css' => array(),
				'js'  => array(),
			)
		);

		$included_js                 = isset( $input['included_assets_js'] ) ? array_map( 'sanitize_key', (array) $input['included_assets_js'] ) : array();
		$clean['excluded_assets_js'] = array();
		if ( ! empty( $scanned_assets['js'] ) ) {
			foreach ( $scanned_assets['js'] as $hash => $url ) {
				if ( ! in_array( $hash, $included_js, true ) ) {
					$clean['excluded_assets_js'][] = $hash;
				}
			}
		} else {
			// Keep existing if nothing was scanned yet
			$existing_settings           = get_option( self::OPTION_KEY, array() );
			$clean['excluded_assets_js'] = $existing_settings['excluded_assets_js'] ?? array();
		}

		$included_css                 = isset( $input['included_assets_css'] ) ? array_map( 'sanitize_key', (array) $input['included_assets_css'] ) : array();
		$clean['excluded_assets_css'] = array();
		if ( ! empty( $scanned_assets['css'] ) ) {
			foreach ( $scanned_assets['css'] as $hash => $url ) {
				if ( ! in_array( $hash, $included_css, true ) ) {
					$clean['excluded_assets_css'][] = $hash;
				}
			}
		} else {
			$existing_settings            = get_option( self::OPTION_KEY, array() );
			$clean['excluded_assets_css'] = $existing_settings['excluded_assets_css'] ?? array();
		}

		// When browser_cache or gzip toggle changes, rewrite .htaccess.
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-browser-cache.php';
		if ( ! empty( $clean['browser_cache'] ) || ! empty( $clean['gzip'] ) ) {
			CACHELOOP_Browser_Cache::write_htaccess_rules();
		} else {
			CACHELOOP_Browser_Cache::remove_htaccess_rules();
		}

		// Write runtime config file for the drop-in (pre-bootstrap TTL).
		$clean = $this->cl_write_runtime_config( $clean );

		return $clean;
	}

	/**
	 * Writes the runtime-config.json file that the drop-in reads pre-bootstrap.
	 *
	 * @param  array $settings Sanitized settings array.
	 * @return array           The same settings array (unchanged).
	 */
	private function cl_write_runtime_config( $settings ) {
		$cache_dir = WP_CONTENT_DIR . '/cache/cacheloop/';
		if ( ! is_dir( $cache_dir ) ) {
			wp_mkdir_p( $cache_dir );
		}

		// This file carries object_cache_password when Object Cache Pro is
		// configured — make sure the directory is never directly web-reachable
		// before writing it (also covers a dir that was recreated after a flush).
		if ( function_exists( 'cacheloop_protect_cache_dir' ) ) {
			cacheloop_protect_cache_dir();
		}

		// Data file (not executable PHP) read by the advanced-cache.php and
		// object-cache.php drop-ins pre-bootstrap, via json_decode().
		$config_path    = $cache_dir . 'runtime-config.json';
		$ttl            = isset( $settings['cache_ttl'] ) ? (int) $settings['cache_ttl'] : DAY_IN_SECONDS;
		$device_split   = isset( $settings['cache_device_split'] ) ? (int) $settings['cache_device_split'] : 0;
		$cdn_enabled    = isset( $settings['cdn_enabled'] ) ? (int) $settings['cdn_enabled'] : 0;
		$cdn_domain     = isset( $settings['cdn_domain'] ) ? sanitize_text_field( $settings['cdn_domain'] ) : '';
		$bypass_cookies = isset( $settings['bypass_cookies'] ) ? sanitize_text_field( $settings['bypass_cookies'] ) : '';
		$home_url       = home_url();

		$oc_is_pro   = function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code();
		$oc_enabled  = ( $oc_is_pro && ! empty( $settings['object_cache_enabled'] ) ) ? 1 : 0;
		$oc_driver   = isset( $settings['object_cache_driver'] ) ? sanitize_key( $settings['object_cache_driver'] ) : 'redis';
		$oc_host     = isset( $settings['object_cache_host'] ) ? sanitize_text_field( $settings['object_cache_host'] ) : '127.0.0.1';
		$oc_port     = isset( $settings['object_cache_port'] ) ? (int) $settings['object_cache_port'] : 6379;
		// Already sanitized (as-is, see cl_sanitize_settings()) — re-running
		// sanitize_text_field() here would strip the same valid password
		// characters a second time on every settings save.
		$oc_password = isset( $settings['object_cache_password'] ) ? (string) $settings['object_cache_password'] : '';
		$oc_ttl      = isset( $settings['object_cache_ttl'] ) ? (int) $settings['object_cache_ttl'] : 3600;

		$config = array(
			'version'                => CACHELOOP_VERSION,
			'ttl'                    => $ttl,
			'device_split'           => $device_split,
			'cdn_enabled'            => $cdn_enabled,
			'cdn_domain'             => $cdn_domain,
			'home_url'               => $home_url,
			'bypass_cookies'         => $bypass_cookies,
			'object_cache_enabled'   => $oc_enabled,
			'object_cache_driver'    => $oc_driver,
			'object_cache_host'      => $oc_host,
			'object_cache_port'      => $oc_port,
			'object_cache_password'  => $oc_password,
			'object_cache_ttl'       => $oc_ttl,
		);

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- writes a non-executable data file to wp-content/cache/, not the plugin's own directory.
		file_put_contents( $config_path, wp_json_encode( $config ) );

		return $settings;
	}

	// ---------------------------------------------------------------------------
	// Asset Enqueueing
	// ---------------------------------------------------------------------------

	/**
	 * Enqueues admin CSS and JS only on the CacheLoop settings page.
	 *
	 * @param  string $hook The current admin page hook.
	 * @return void
	 */
	public function cl_enqueue_assets( $hook ) {
		if ( 'toplevel_page_' . self::PAGE_SLUG !== $hook ) {
			$this->cl_maybe_enqueue_media_assets( $hook );
			return;
		}

		wp_enqueue_style(
			'cl-admin-css',
			CACHELOOP_PLUGIN_URL . 'admin/assets/css/cl-admin.css',
			array(),
			CACHELOOP_VERSION
		);

		// Inline CSS to position WordPress admin footer below all content.
		wp_add_inline_style(
			'cl-admin-css',
			'
            #wpfooter { position: relative !important; bottom: auto !important; clear: both !important; margin-top: 40px !important; padding-bottom: 20px !important; }
            #footer-left { float: left !important; clear: none !important; display: inline-block !important; width: auto !important; text-align: left !important; }
            #footer-upgrade { float: right !important; clear: none !important; display: inline-block !important; width: auto !important; text-align: right !important; margin-top: 0 !important; }
            #wpbody-content { padding-bottom: 0 !important; }
        '
		);

		wp_enqueue_script(
			'cl-admin-js',
			CACHELOOP_PLUGIN_URL . 'admin/assets/js/cl-admin.js',
			array( 'jquery' ),
			CACHELOOP_VERSION,
			true // Load in footer.
		);

		// Pass server-side data to the JS file.
		wp_localize_script(
			'cl-admin-js',
			'clAdminData',
			array(
				'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
				'nonceClearCache'  => wp_create_nonce( 'cacheloop_admin_nonce' ),
				'nonceDbOptimizer' => wp_create_nonce( 'cacheloop_admin_nonce' ),
				'isPro'            => function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code() ? '1' : '0',
				'i18n'             => array(
					'clearing'    => __( 'Clearing cache…', 'cacheloop-simple-page-cache-speed-optimization' ),
					'cleared'     => __( 'Cache cleared!', 'cacheloop-simple-page-cache-speed-optimization' ),
					'optimizing'  => __( 'Optimizing database…', 'cacheloop-simple-page-cache-speed-optimization' ),
					'optimized'   => __( 'Database optimized!', 'cacheloop-simple-page-cache-speed-optimization' ),
					'error'       => __( 'An error occurred. Please try again.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'proRequired' => __( 'This feature requires CacheLoop Pro.', 'cacheloop-simple-page-cache-speed-optimization' ),
				),
			)
		);

		// Tabs, Speedtest, Preloader polling, and Import form handling —
		// previously an inline <script> block in the settings-page view,
		// now a properly enqueued/localized script.
		wp_enqueue_script(
			'cl-settings-page-js',
			CACHELOOP_PLUGIN_URL . 'admin/assets/js/cl-settings-page.js',
			array( 'jquery' ),
			CACHELOOP_VERSION,
			true // Load in footer.
		);

		wp_localize_script(
			'cl-settings-page-js',
			'clSettingsData',
			array(
				'nonce'        => wp_create_nonce( 'cacheloop_admin_nonce' ),
				'adminPostUrl' => admin_url( 'admin-post.php' ),
				'i18n'         => array(
					'confirmResetExclusions' => __( 'Reset to default exclusions list? Your current list will be replaced.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'importedMsg'            => __( 'Settings imported successfully!', 'cacheloop-simple-page-cache-speed-optimization' ),
					'fsResetMsg'             => __( 'Freemius cache cleared successfully!', 'cacheloop-simple-page-cache-speed-optimization' ),
					'savedMsg'               => __( 'Settings saved successfully!', 'cacheloop-simple-page-cache-speed-optimization' ),
					'analyzingMobile'        => __( 'Analyzing mobile performance...', 'cacheloop-simple-page-cache-speed-optimization' ),
					'analyzingDesktop'       => __( 'Analyzing desktop performance...', 'cacheloop-simple-page-cache-speed-optimization' ),
					'suggestionsApplied'     => __( 'Suggested files checked — click Save Settings to apply.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'selectJsonFile'         => __( 'Please select a JSON file to import.', 'cacheloop-simple-page-cache-speed-optimization' ),
				),
			)
		);
	}

	/**
	 * Enqueues the "Optimize with CacheLoop" Media Library integration on
	 * the Media Library screens only (List/Grid view and the single
	 * attachment edit screen) — separate from cl-admin.js so the full
	 * settings-page bundle isn't loaded outside the CacheLoop page itself.
	 *
	 * @param  string $hook The current admin page hook.
	 * @return void
	 */
	private function cl_maybe_enqueue_media_assets( $hook ) {
		$is_attachment_edit = 'post.php' === $hook && isset( $_GET['post'] ) && 'attachment' === get_post_type( (int) $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change.

		if ( 'upload.php' !== $hook && ! $is_attachment_edit ) {
			return;
		}

		if ( ! function_exists( 'cacheloop_fs' ) || ! cacheloop_fs()->can_use_premium_code() ) {
			return; // Image Optimization is a Pro feature.
		}

		wp_enqueue_script(
			'cl-media-js',
			CACHELOOP_PLUGIN_URL . 'admin/assets/js/cl-media.js',
			array( 'jquery' ),
			CACHELOOP_VERSION,
			true
		);

		wp_localize_script(
			'cl-media-js',
			'clMediaData',
			array(
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'nonce'   => wp_create_nonce( 'cacheloop_admin_nonce' ),
			)
		);
	}

	// ---------------------------------------------------------------------------
	// Settings Page Render
	// ---------------------------------------------------------------------------

	/**
	 * Renders the main settings page HTML by loading the view template.
	 *
	 * @return void
	 */
	public function cl_render_settings_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		$settings            = $this->settings;
		$is_pro              = function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code();
		$show_nginx_download = ! self::cl_is_apache_or_litespeed();

		include CACHELOOP_PLUGIN_DIR . 'admin/views/settings-page.php';
	}

	// ---------------------------------------------------------------------------
	// AJAX Handlers
	// ---------------------------------------------------------------------------

	/**
	 * AJAX handler: clears all cached HTML files.
	 *
	 * @return void
	 */
	public function cl_ajax_clear_cache() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
		CACHELOOP_Page_Cache::flush_all();

		wp_send_json_success( array( 'message' => __( 'Cache cleared successfully!', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
	}

	/**
	 * Handles the admin toolbar "Clear All Cache" link (a plain navigation,
	 * not an AJAX request) — clears the cache, then redirects back to
	 * wherever the button was clicked from.
	 *
	 * @return void
	 */
	public function cl_handle_clear_cache_toolbar() {
		check_admin_referer( 'cacheloop_admin_nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
		CACHELOOP_Page_Cache::flush_all();

		$redirect = wp_get_referer();
		wp_safe_redirect( $redirect ? add_query_arg( 'cl_purged', '1', $redirect ) : admin_url() );
		exit;
	}

	/**
	 * AJAX handler: returns the last N lines of the debug log.
	 *
	 * @return void
	 */
	public function cl_ajax_view_debug_log() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-logger.php';
		$lines   = isset( $_REQUEST['lines'] ) ? absint( wp_unslash( $_REQUEST['lines'] ) ) : 50;
		$content = CACHELOOP_Logger::get_last_lines( $lines );

		wp_send_json_success( array( 'log' => $content ) );
	}

	/**
	 * AJAX handler: clears the debug log file.
	 *
	 * @return void
	 */
	public function cl_ajax_clear_debug_log() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-logger.php';
		CACHELOOP_Logger::clear_log();

		wp_send_json_success( array( 'message' => __( 'Debug log cleared.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
	}

	/**
	 * AJAX handler: returns cache metrics (file count, disk usage, etc.).
	 *
	 * @return void
	 */
	public function cl_ajax_get_metrics() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		$cache_dir = CACHELOOP_CACHE_DIR;
		$metrics   = array(
			'file_count' => 0,
			'disk_size'  => '0 B',
			'cache_dir'  => __( 'Not found', 'cacheloop-simple-page-cache-speed-optimization' ),
			'last_purge' => __( 'Unknown', 'cacheloop-simple-page-cache-speed-optimization' ),
		);

		if ( is_dir( $cache_dir ) ) {
			$metrics['file_count'] = $this->count_files_recursive( $cache_dir );
			$metrics['disk_size']  = $this->format_bytes( $this->dir_size( $cache_dir ) );
			$metrics['cache_dir']  = $cache_dir;

			// Find the oldest file as a proxy for "last purge".
			$oldest   = 0;
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $cache_dir, RecursiveDirectoryIterator::SKIP_DOTS )
			);
			foreach ( $iterator as $file ) {
				if ( $file->isFile() && $file->getMTime() < $oldest ) {
					$oldest = $file->getMTime();
				}
			}
			$metrics['last_purge'] = $oldest > 0 ? gmdate( 'Y-m-d H:i', $oldest ) : __( 'N/A', 'cacheloop-simple-page-cache-speed-optimization' );
		}

		wp_send_json_success( $metrics );
	}

	/**
	 * AJAX handler: get preloader status (Pro).
	 *
	 * @return void
	 */
	public function cl_ajax_get_preload_status() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		// Check if preloader is running (stored in transient).
		$running   = get_transient( 'cacheloop_preload_running' );
		$completed = get_transient( 'cacheloop_preload_completed' );
		$total     = get_transient( 'cacheloop_preload_total' );

		wp_send_json_success(
			array(
				'running'   => (bool) $running,
				'completed' => $completed ? intval( $completed ) : 0,
				'total'     => $total ? intval( $total ) : 0,
				'percent'   => $total ? min( 100, intval( $completed ) * 100 / intval( $total ) ) : 0,
			)
		);
	}

	/**
	 * AJAX handler: quick-enable a feature from the Pro Analysis panel.
	 *
	 * @return void
	 */
	public function cl_ajax_quick_enable_feature() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		$feature  = isset( $_POST['feature'] ) ? sanitize_text_field( wp_unslash( $_POST['feature'] ) ) : '';
		$settings = get_option( 'cacheloop_settings', array() );
		$updated  = false;
		$message  = '';

		// Check if this is a Pro feature and user doesn't have a license.
		$pro_features = array( 'enable_delay_js', 'enable_minify' );
		$is_pro       = function_exists( 'cacheloop_fs' ) && cacheloop_fs()->can_use_premium_code();
		$upgrade_url  = function_exists( 'cacheloop_fs' ) ? cacheloop_fs()->get_upgrade_url() : '';

		if ( in_array( $feature, $pro_features, true ) && ! $is_pro ) {
			wp_send_json_success(
				array(
					'updated'      => false,
					'message'      => __( 'This feature requires CacheLoop Pro.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'requires_pro' => true,
					'upgrade_url'  => $upgrade_url,
				)
			);
		}

		// Map of feature keys to settings keys and messages.
		$feature_map = array(
			'enable_page_cache'    => array(
				'key' => 'page_cache',
				'msg' => __( 'Page Cache enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'enable_lazy_load'     => array(
				'key' => 'lazy_load',
				'msg' => __( 'Lazy Load enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'enable_minify'        => array(
				'key' => 'minify',
				'msg' => __( 'CSS/JS Minification enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'enable_delay_js'      => array(
				'key' => 'delay_js',
				'msg' => __( 'Delay JS Execution enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'enable_gzip'          => array(
				'key' => 'gzip',
				'msg' => __( 'Gzip Compression enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'enable_browser_cache' => array(
				'key' => 'browser_cache',
				'msg' => __( 'Browser Caching enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
			'optimize_all'         => array(
				'key' => 'optimize_all',
				'msg' => __( 'All available optimizations enabled.', 'cacheloop-simple-page-cache-speed-optimization' ),
			),
		);

		// Actions that switch to a specific tab and scroll to the section.
		$tab_scroll_actions = array(
			'configure_delay_js_exclusions' => array(
				'tab'    => 'tab-assets',
				'scroll' => 'cl-card-delay-js',
			),
			'configure_lcp_preload'         => array(
				'tab'    => 'tab-optimization',
				'scroll' => 'cl-card-lcp-preload',
			),
			'configure_lcp_count'           => array(
				'tab'    => 'tab-optimization',
				'scroll' => 'cl-card-lcp-count',
			),
		);

		// If this is a tab-switch action, return the target tab and section.
		if ( isset( $tab_scroll_actions[ $feature ] ) ) {
			wp_send_json_success(
				array(
					'switch_tab' => $tab_scroll_actions[ $feature ]['tab'],
					'scroll_to'  => $tab_scroll_actions[ $feature ]['scroll'],
					'message'    => __( 'Switching to configuration tab.', 'cacheloop-simple-page-cache-speed-optimization' ),
				)
			);
		}

		// Actions that scroll within the current tab.
		$scroll_actions = array(
			'configure_delay_js_exclusions' => 'cl-card-delay-js',
			'configure_lcp_preload'         => 'cl-card-lcp-preload',
			'configure_lcp_count'           => 'cl-card-lcp-count',
		);

		if ( isset( $scroll_actions[ $feature ] ) ) {
			wp_send_json_success(
				array(
					'scroll_to' => $scroll_actions[ $feature ],
					'message'   => __( 'Scrolling to configuration section.', 'cacheloop-simple-page-cache-speed-optimization' ),
				)
			);
		}

		if ( ! isset( $feature_map[ $feature ] ) ) {
			wp_send_json_error( array( 'message' => __( 'Unknown feature.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		$map = $feature_map[ $feature ];

		if ( 'optimize_all' === $feature ) {
			// Enable ALL free features at once.
			$free_features = array( 'page_cache', 'browser_cache', 'gzip', 'lazy_load' );
			foreach ( $free_features as $key ) {
				if ( empty( $settings[ $key ] ) ) {
					$settings[ $key ] = 1;
					$updated          = true;
				}
			}
			$message = $map['msg'];
		} elseif ( empty( $settings[ $map['key'] ] ) ) {
				$settings[ $map['key'] ] = 1;
				$updated                 = true;
				$message                 = $map['msg'];
		} else {
			$message = __( 'Feature is already enabled.', 'cacheloop-simple-page-cache-speed-optimization' );
		}

		if ( $updated ) {
			update_option( 'cacheloop_settings', $settings );
			// Flush cache after enabling.
			if ( class_exists( 'CACHELOOP_Page_Cache' ) ) {
				CACHELOOP_Page_Cache::flush_all();
			}
		}

		// Check if any Pro features were requested (show upgrade prompt).
		// Already handled above — removed duplicate check.

		wp_send_json_success(
			array(
				'updated' => $updated,
				'message' => $message,
			)
		);
	}

	/**
	 * Detects the hosting control panel in use, if any, by checking for
	 * well-known filesystem signatures. Read-only, no user input involved.
	 *
	 * @return string Detected panel name, or 'Unknown / Unmanaged'.
	 */
	private function cl_detect_hosting_panel() {
		$signatures = array(
			'Virtualmin/Webmin' => '/etc/webmin',
			'cPanel'            => '/usr/local/cpanel',
			'Plesk'             => '/usr/local/psa',
			'CyberPanel'        => '/usr/local/CyberCP',
			'DirectAdmin'       => '/usr/local/directadmin',
			'ISPConfig'         => '/usr/local/ispconfig',
			'RunCloud'          => '/etc/rc.d/rc-agent',
		);

		foreach ( $signatures as $name => $path ) {
			if ( is_dir( $path ) ) {
				return $name;
			}
		}

		return 'Unknown / Unmanaged';
	}

	/**
	 * Detects whether the current web server is Apache or LiteSpeed (i.e. one
	 * that reads .htaccess) based on SERVER_SOFTWARE. Used to decide whether
	 * the .htaccess-based Browser Caching rules apply, or whether to offer
	 * the Nginx config download instead.
	 *
	 * @return bool True when SERVER_SOFTWARE identifies Apache or LiteSpeed.
	 */
	public static function cl_is_apache_or_litespeed() {
		$software = isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '';

		return (bool) preg_match( '/apache|litespeed/i', $software );
	}

	/**
	 * AJAX handler: get server info & system check.
	 *
	 * @return void
	 */
	public function cl_ajax_get_server_info() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		$info = array();

		// --- WordPress ---
		$info['WordPress'] = array(
			'Version'        => get_bloginfo( 'version' ),
			'Environment'    => defined( 'WP_DEBUG' ) && WP_DEBUG ? 'Development' : 'Production',
			'Memory Limit'   => WP_MEMORY_LIMIT,
			'Max Input Vars' => ini_get( 'max_input_vars' ),
			'Multisite'      => is_multisite() ? 'Yes' : 'No',
			'Site URL'       => get_site_url(),
			'Home URL'       => get_home_url(),
		);

		// --- PHP ---
		$info['PHP'] = array(
			'Version'        => phpversion(),
			'Engine'         => php_sapi_name(),
			'Max Execution'  => ini_get( 'max_execution_time' ) . 's',
			'Memory Limit'   => ini_get( 'memory_limit' ),
			'Post Max Size'  => ini_get( 'post_max_size' ),
			'Upload Max'     => ini_get( 'upload_max_filesize' ),
			'Display Errors' => ini_get( 'display_errors' ),
			'Error Logging'  => ini_get( 'log_errors' ),
			'Zend OPcache'   => function_exists( 'opcache_get_status' ) ? ( opcache_get_status() ? 'Enabled' : 'Disabled' ) : 'Not Available',
			'SSL'            => extension_loaded( 'openssl' ) ? 'Yes' : 'No',
			'cURL'           => function_exists( 'curl_init' ) ? 'Yes' : 'No',
			'GD'             => function_exists( 'gd_info' ) ? 'Yes (' . gd_info()['GD Version'] . ')' : 'No',
			'MB String'      => extension_loaded( 'mbstring' ) ? 'Yes' : 'No',
			'XML'            => extension_loaded( 'xml' ) ? 'Yes' : 'No',
		);

		// --- Server ---
		$info['Server'] = array(
			'Software'      => isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'Unknown',
			'Document Root' => isset( $_SERVER['DOCUMENT_ROOT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) : 'Unknown',
			'Protocol'      => isset( $_SERVER['SERVER_PROTOCOL'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ) : 'Unknown',
			'HTTPS'         => ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'Yes' : 'No',
			'Hosting Panel' => $this->cl_detect_hosting_panel(),
			'Behind Proxy'  => ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) || isset( $_SERVER['HTTP_CF_RAY'] ) ) ? 'Yes' : 'No',
			'Mod Rewrite'   => function_exists( 'apache_get_modules' ) ? ( in_array( 'mod_rewrite', apache_get_modules(), true ) ? 'Yes' : 'No' ) : 'Unknown (not an Apache module SAPI)',
			'Load Average'  => function_exists( 'sys_getloadavg' ) ? implode( ', ', array_map( 'strval', sys_getloadavg() ) ) : 'Not available',
		);

		// --- Disk ---
		$cache_dir    = defined( 'CACHELOOP_CACHE_DIR' ) ? CACHELOOP_CACHE_DIR : WP_CONTENT_DIR . '/cache/cacheloop/';
		$info['Disk'] = array(
			'Total Space'    => size_format( disk_total_space( '/' ) ),
			'Free Space'     => size_format( disk_free_space( '/' ) ),
			'Cache Dir'      => is_dir( $cache_dir ) ? $cache_dir : 'Not found',
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- read-only diagnostic display, not a file operation.
			'Cache Writable' => is_writable( $cache_dir ) || is_writable( WP_CONTENT_DIR ) ? 'Yes' : 'No',
		);

		// --- Plugins ---
		$active_plugins  = get_option( 'active_plugins', array() );
		$info['Plugins'] = array(
			'Active Count' => count( $active_plugins ),
			'CacheLoop'    => in_array( 'cacheloop/cacheloop.php', $active_plugins, true ) ? 'Active' : 'Inactive',
			'Freemius SDK' => file_exists( CACHELOOP_PLUGIN_DIR . 'vendor/freemius/wordpress-sdk/start.php' ) ? 'Present' : 'Missing',
		);

		// --- Active Plugins List (names + versions, helps spot conflicts) ---
		if ( ! function_exists( 'get_plugin_data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$plugin_list = array();
		foreach ( $active_plugins as $plugin_file ) {
			$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
			if ( ! file_exists( $plugin_path ) ) {
				continue;
			}
			$plugin_data                = get_plugin_data( $plugin_path, false, false );
			$plugin_list[ $plugin_file ] = ( $plugin_data['Name'] ? $plugin_data['Name'] : $plugin_file ) . ' v' . $plugin_data['Version'];
		}
		$info['Active Plugins'] = $plugin_list;

		// --- Active Theme ---
		$theme               = wp_get_theme();
		$info['Active Theme'] = array(
			'Name'    => $theme->get( 'Name' ),
			'Version' => $theme->get( 'Version' ),
		);

		// --- Freemius (if available) ---
		if ( function_exists( 'cacheloop_fs' ) ) {
			try {
				$fs               = cacheloop_fs();
				$info['Freemius'] = array(
					'Loaded'          => 'Yes',
					'Is Premium'      => $fs->is_premium() ? 'Yes' : 'No',
					'Is Registered'   => $fs->is_registered() ? 'Yes' : 'No',
					'Can Use Premium' => $fs->can_use_premium_code() ? 'Yes' : 'No',
					'Is Live'         => $fs->is_live() ? 'Live' : 'Test',
				);
			} catch ( Exception $e ) {
				$info['Freemius'] = array(
					'Loaded' => 'Yes',
					'Error'  => $e->getMessage(),
				);
			}
		} else {
			$info['Freemius'] = array(
				'Loaded' => 'No',
				'Note'   => 'SDK not loaded — check vendor/freemius directory.',
			);
		}

		// --- Page Cache Drop-in ---
		$dropin_path        = WP_CONTENT_DIR . '/advanced-cache.php';
		$info['Page Cache'] = array(
			'Drop-in Present' => file_exists( $dropin_path ) ? 'Yes' : 'No',
			'Drop-in Path'    => $dropin_path,
		);

		// --- WP Cron ---
		$info['WP Cron'] = array(
			'Disabled' => defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? 'Yes' : 'No',
			'Next Run' => wp_next_scheduled( 'cacheloop_preload_cron' ) ? gmdate( 'Y-m-d H:i:s', wp_next_scheduled( 'cacheloop_preload_cron' ) ) : 'Not scheduled',
		);

		wp_send_json_success( array( 'info' => $info ) );
	}

	/**
	 * Gets statistics about the cache directory.
	 *
	 * @return array Array with size and count.
	 */
	public function get_cache_stats() {
		$cache_dir = defined( 'CACHELOOP_CACHE_DIR' ) ? CACHELOOP_CACHE_DIR : WP_CONTENT_DIR . '/cache/cacheloop/';
		$size      = 0;
		$count     = 0;
		$oldest    = 0;

		if ( is_dir( $cache_dir ) ) {
			$this->count_dir_recursive( $cache_dir, $size, $count );

			// Find oldest file as proxy for "last purge".
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $cache_dir, RecursiveDirectoryIterator::SKIP_DOTS )
			);
			foreach ( $iterator as $file ) {
				if ( $file->isFile() && $file->getMTime() < $oldest ) {
					$oldest = $file->getMTime();
				}
			}
		}

		$last_purge = '—';
		if ( $oldest > 0 ) {
			$last_purge = gmdate( 'Y-m-d H:i', $oldest );
		}

		return array(
			'size'       => $size > 0 ? round( $size / 1024 / 1024, 2 ) . ' MB' : '0 MB',
			'count'      => $count,
			'last_purge' => $last_purge,
			'cache_dir'  => $cache_dir,
		);
	}

	/**
	 * Recursively counts files and sizes in a directory.
	 *
	 * @param string $dir
	 * @param int    $size
	 * @param int    $count
	 */
	private function count_dir_recursive( $dir, &$size, &$count ) {
		$files = @scandir( $dir );
		if ( ! $files ) {
			return;
		}
		foreach ( $files as $file ) {
			if ( $file === '.' || $file === '..' ) {
				continue;
			}
			$path = rtrim( $dir, '/' ) . '/' . $file;
			if ( is_dir( $path ) ) {
				// Count the directory itself as an item? No, user usually wants file count.
				// We will just recurse.
				$this->count_dir_recursive( $path, $size, $count );
			} elseif ( is_file( $path ) ) {
				$size += @filesize( $path );
				++$count;
			}
		}
	}

	/**
	 * Gets the latest PageSpeed statistics from the database.
	 *
	 * @return array
	 */
	public function get_pagespeed_stats() {
		$mobile  = get_option( 'cacheloop_pagespeed_mobile', array() );
		$desktop = get_option( 'cacheloop_pagespeed_desktop', array() );
		$empty   = array(
			'score'           => 0,
			'lcp'             => '-',
			'fcp'             => '-',
			'tbt'             => '-',
			'cls'             => '-',
			'audits'          => array(),
			'hero_image_hint' => array(),
			'timestamp'       => 0,
		);
		return array(
			'mobile'  => ! empty( $mobile ) ? $mobile : $empty,
			'desktop' => ! empty( $desktop ) ? $desktop : $empty,
		);
	}

	/**
	 * Generates Pro Analysis tips based on current PageSpeed data and settings.
	 *
	 * @return array
	 */
	public function get_pro_analysis_tips() {
		$all_stats = $this->get_pagespeed_stats();
		// Use mobile stats for recommendations (Google's primary standard)
		$stats = $all_stats['mobile'];
		$tips  = array();

		// Index the raw Lighthouse diagnostics/opportunities Google flagged on the last run,
		// keyed by audit id, so specific checks below can quote Google's own numbers.
		$google_audits = array();
		if ( ! empty( $stats['audits'] ) && is_array( $stats['audits'] ) ) {
			foreach ( $stats['audits'] as $audit ) {
				if ( ! empty( $audit['id'] ) ) {
					$google_audits[ $audit['id'] ] = $audit;
				}
			}
		}
		// Audit ids consumed by the metric-specific checks below, so the generic
		// "what else did Google find" pass further down doesn't repeat them.
		$consumed_audit_ids = array();

		// bootup-time and mainthread-work-breakdown both point at the same root
		// cause (heavy JS execution) — keep the more actionable one only.
		if ( isset( $google_audits['bootup-time'] ) && isset( $google_audits['mainthread-work-breakdown'] ) ) {
			unset( $google_audits['mainthread-work-breakdown'] );
		}

		if ( empty( $stats['score'] ) ) {
			$tips[] = array(
				'type'   => 'info',
				'msg'    => __( 'Run a Speedtest to collect initial performance data.', 'cacheloop-simple-page-cache-speed-optimization' ),
				'action' => '',
			);
			return $tips;
		}

		// Overall score warning (PageSpeed thresholds: 0-49 poor, 50-89 needs work, 90-100 good)
		if ( $stats['score'] < 50 ) {
			$tips[] = array(
				'type'   => 'error',
				'msg'    => sprintf(
					// translators: %d is the PageSpeed score.
					__( 'Your mobile score is %d/100 (Poor). Critical optimizations needed.', 'cacheloop-simple-page-cache-speed-optimization' ),
					$stats['score']
				),
				'action' => 'optimize_all',
			);
		} elseif ( $stats['score'] < 90 ) {
			$tips[] = array(
				'type'   => 'warning',
				'msg'    => sprintf(
					// translators: %d is the PageSpeed score.
					__( 'Your mobile score is %d/100 (Needs Improvement). Some optimizations can still help.', 'cacheloop-simple-page-cache-speed-optimization' ),
					$stats['score']
				),
				'action' => '',
			);
		}

		// TBT / Delay JS check (stored as display string e.g. "430 ms" or "1.2 s").
		// Name the actual offending script when Google's data has one, so the user
		// knows exactly what to add to the Delay JS exclusions instead of guessing.
		$tbt_ms = (float) $stats['tbt'];
		if ( $tbt_ms > 200 ) {
			$render_blocking = isset( $google_audits['render-blocking-insight'] ) ? $google_audits['render-blocking-insight'] : null;
			$js_execution     = isset( $google_audits['bootup-time'] ) ? $google_audits['bootup-time'] : null;
			if ( $render_blocking ) {
				$consumed_audit_ids['render-blocking-insight'] = true;
			}
			if ( $js_execution ) {
				$consumed_audit_ids['bootup-time'] = true;
			}
			$culprit = '';
			if ( $render_blocking && ! empty( $render_blocking['detail'] ) ) {
				$culprit = $render_blocking['detail'];
			} elseif ( $js_execution && ! empty( $js_execution['detail'] ) ) {
				$culprit = $js_execution['detail'];
			}
			if ( empty( $this->settings['delay_js'] ) ) {
				$tips[] = array(
					'type'   => 'error',
					'msg'    => $culprit
						? sprintf(
							/* translators: %s: the specific script and its cost, e.g. "jquery.min.js (1017ms)". */
							__( 'Your Total Blocking Time is high — the biggest offender Google found is %s. Turn on Delay JS Execution to defer it.', 'cacheloop-simple-page-cache-speed-optimization' ),
							$culprit
						)
						: __( 'Your Total Blocking Time is high. Turn on Delay JS Execution to eliminate render-blocking scripts.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_delay_js',
				);
			} else {
				$tips[] = array(
					'type'   => 'warning',
					'msg'    => $culprit
						? sprintf(
							/* translators: %s: the specific script and its cost, e.g. "recaptcha__it.js (555ms)". */
							__( 'Delay JS is enabled, but Google still finds %s costing you time. Add this file (or its handle/domain) to the Delay JS exclusions so it stops bypassing the delay — or if it\'s already excluded, that\'s likely why it\'s still slow (excluded scripts run immediately by design).', 'cacheloop-simple-page-cache-speed-optimization' ),
							$culprit
						)
						: __( 'Delay JS is enabled, but Total Blocking Time is still high. Check if you excluded too many heavy scripts, or if third-party trackers are bypassing the delay.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'configure_delay_js_exclusions',
				);
			}
		}

		// FCP check (stored as float seconds, good = < 1.8s).
		// document-latency-insight tells us whether Google itself measured the
		// server as fast or slow — that decides whether "upgrade hosting" is even
		// the right advice, or whether the real problem is a page-cache MISS.
		$fcp_s = (float) $stats['fcp'];
		if ( $fcp_s > 1.8 ) {
			$latency = isset( $google_audits['document-latency-insight'] ) ? $google_audits['document-latency-insight'] : null;
			if ( $latency ) {
				$consumed_audit_ids['document-latency-insight'] = true;
			}
			if ( empty( $this->settings['page_cache'] ) ) {
				$tips[] = array(
					'type'   => 'warning',
					'msg'    => __( 'Your First Contentful Paint is slow. Enabling Page Cache will improve server response instantly.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_page_cache',
				);
			} elseif ( $latency ) {
				// Google actually flagged the server response itself as slow —
				// with Page Cache on, that almost always means this URL wasn't
				// served from cache. Point the user at the one signal that proves it.
				$tips[] = array(
					'type'   => 'warning',
					'msg'    => sprintf(
						/* translators: %s: Google's measured server latency and failed checks, e.g. "TTFB 890ms | FAIL: Server responds quickly". */
						__( 'Page Cache is enabled but Google still measured a slow server response (%s). Check the response headers for this page (curl -I your-url, or browser DevTools → Network) — if "X-CacheLoop-Cache" says MISS instead of HIT, the page isn\'t actually being served from cache (common causes: it\'s excluded from caching, or the request had cookies/query strings that bypass it). If it does say HIT and is still this slow, the bottleneck is genuinely server-side — heavy database queries or PHP work on this page.', 'cacheloop-simple-page-cache-speed-optimization' ),
						$latency['detail'] ? $latency['detail'] : $latency['displayValue']
					),
					'action' => '',
				);
			} else {
				// Google did NOT flag document-latency-insight — server itself
				// responded fine, so a slow FCP here is coming from something
				// else (render-blocking resources, redirects), not the server.
				$tips[] = array(
					'type'   => 'info',
					'msg'    => __( 'Page Cache is enabled and Google confirms your server itself responds quickly — the slow First Contentful Paint is caused by something else blocking the render (see the render-blocking / JS execution findings below), not hosting.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => '',
				);
			}
		}

		// LCP check (good = < 2.5s). Google's node snapshot of the actual LCP
		// element (captured via lcp-discovery-insight) tells us the real image
		// path/selector — use it instead of the vague "your hero image".
		$lcp_s = (float) $stats['lcp'];
		if ( $lcp_s > 2.5 ) {
			$hero = ! empty( $stats['hero_image_hint'] ) && is_array( $stats['hero_image_hint'] ) ? $stats['hero_image_hint'] : array();

			if ( ! empty( $hero['src'] ) ) {
				$hero_desc = ! empty( $hero['truncated'] )
					? sprintf(
						/* translators: %s: partial image URL/path as reported by Google (may be cut off). */
						__( 'an image whose path starts with "%s" (Google truncated the full URL — open your page source and search for this to get the exact one)', 'cacheloop-simple-page-cache-speed-optimization' ),
						$hero['src']
					)
					/* translators: %s: the exact image URL Google identified as the LCP element. */
					: sprintf( __( 'this exact image: %s', 'cacheloop-simple-page-cache-speed-optimization' ), $hero['src'] );
			} elseif ( ! empty( $hero['selector'] ) ) {
				$hero_desc = sprintf(
					/* translators: %s: a CSS selector identifying the LCP element in the page source. */
					__( 'the image matching "%s" in your page source', 'cacheloop-simple-page-cache-speed-optimization' ),
					$hero['selector']
				);
			} else {
				$hero_desc = __( 'your hero image', 'cacheloop-simple-page-cache-speed-optimization' );
			}

			if ( empty( $this->settings['lcp_preload_urls'] ) ) {
				$tips[] = array(
					'type'   => 'warning',
					'msg'    => sprintf(
						/* translators: %s: description of the detected LCP image (URL, partial path, or selector). */
						__( 'Your Largest Contentful Paint is slow. Google identified %s as the LCP element — add its URL to the LCP Preload field.', 'cacheloop-simple-page-cache-speed-optimization' ),
						$hero_desc
					),
					'action' => 'configure_lcp_preload',
				);
			} else {
				$configured_matches = ! empty( $hero['src'] ) && false !== strpos( $this->settings['lcp_preload_urls'], $hero['src'] );
				$tips[]             = array(
					'type'   => 'warning',
					'msg'    => ( ! empty( $hero['src'] ) && ! $configured_matches )
						? sprintf(
							/* translators: %s: the image URL/path Google detected as the actual LCP element. */
							__( 'Your Largest Contentful Paint is still slow — and the URL you configured in LCP Preload doesn\'t match what Google detected as the LCP element (%s). Update the field to that image.', 'cacheloop-simple-page-cache-speed-optimization' ),
							$hero_desc
						)
						: __( 'Your Largest Contentful Paint is still slow even with LCP Preload active. Check if the preloaded URL is correct, or if the image size is too large (consider compressing it or converting to WebP).', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => '',
				);
			}

			$discovery = isset( $google_audits['lcp-discovery-insight'] ) ? $google_audits['lcp-discovery-insight'] : null;
			if ( $discovery ) {
				$consumed_audit_ids['lcp-discovery-insight'] = true;
			}
			$is_lazy_loaded = $discovery && false !== stripos( $discovery['detail'], 'loading=lazy' );

			if ( $is_lazy_loaded ) {
				$tips[] = array(
					'type'   => 'error',
					'msg'    => sprintf(
						/* translators: %s: description of the detected LCP image. */
						__( 'Google explicitly confirmed %s is being Lazy Loaded, which delays it. Increase the "Exclude First Images from Lazy Load" count by 1-2 and re-test — the hero image may not be the very first <img> tag in the page (e.g. a logo comes before it).', 'cacheloop-simple-page-cache-speed-optimization' ),
						$hero_desc
					),
					'action' => 'configure_lcp_count',
				);
			} elseif ( $discovery && ! empty( $this->settings['lazy_load'] ) ) {
				// $discovery here means Google flagged lcp-discovery-insight for a
				// reason OTHER than lazy-loading (e.g. missing fetchpriority or not
				// discoverable in the initial document) — still worth a nudge. If
				// $discovery is null, Google found no discovery issue at all, so
				// suggesting to touch the lazy-load count would be noise.
				if ( empty( $this->settings['lcp_image_count'] ) || (int) $this->settings['lcp_image_count'] < 1 ) {
					$tips[] = array(
						'type'   => 'info',
						'msg'    => __( 'Make sure your main hero image is NOT being Lazy Loaded by using the "Exclude First Images from Lazy Load" setting.', 'cacheloop-simple-page-cache-speed-optimization' ),
						'action' => 'configure_lcp_count',
					);
				} else {
					$tips[] = array(
						'type'   => 'info',
						'msg'    => __( 'You are excluding images from Lazy Load, but LCP is still high. Ensure the exclusion count covers your hero image.', 'cacheloop-simple-page-cache-speed-optimization' ),
						'action' => 'configure_lcp_count',
					);
				}
			}
		}

		// CLS check (good = < 0.1)
		$cls = (float) $stats['cls'];
		if ( $cls > 0.1 ) {
			$tips[] = array(
				'type'   => 'warning',
				'msg'    => __( 'Your Cumulative Layout Shift is high. Ensure all images have width/height attributes.', 'cacheloop-simple-page-cache-speed-optimization' ),
				'action' => '',
			);
		}

		// Everything else Google's audit flagged that isn't already covered above.
		// Some of these map to a CacheLoop setting; others are outside the plugin's
		// scope (images, unused code, DOM size, third-party scripts) but the user
		// still deserves to know exactly what Google is complaining about and why.
		foreach ( $google_audits as $id => $audit ) {
			if ( isset( $consumed_audit_ids[ $id ] ) ) {
				continue;
			}
			$val = $audit['displayValue'];

			switch ( $id ) {
				case 'unminified-css':
				case 'unminified-javascript':
					if ( empty( $this->settings['minify'] ) ) {
						$tips[] = array(
							'type'   => 'warning',
							/* translators: %s: Google's measured savings. */
							'msg'    => sprintf( __( 'Google found unminified CSS/JS (%s). Enable Minification to fix this.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
							'action' => 'enable_minify',
						);
					}
					break;

				case 'uses-text-compression':
					if ( empty( $this->settings['gzip'] ) ) {
						$tips[] = array(
							'type'   => 'warning',
							/* translators: %s: Google's measured savings. */
							'msg'    => sprintf( __( 'Google found uncompressed text assets (%s). Enable Gzip Compression to fix this.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
							'action' => 'enable_gzip',
						);
					}
					break;

				case 'offscreen-images':
					if ( empty( $this->settings['lazy_load'] ) ) {
						$tips[] = array(
							'type'   => 'warning',
							/* translators: %s: Google's measured savings. */
							'msg'    => sprintf( __( 'Google found offscreen images loading too early (%s). Enable Lazy Load to fix this.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
							'action' => 'enable_lazy_load',
						);
					}
					break;

				case 'uses-long-cache-ttl':
				case 'cache-insight':
					if ( empty( $this->settings['browser_cache'] ) ) {
						$tips[] = array(
							'type'   => 'warning',
							/* translators: %s: the specific asset and its cache TTL, e.g. "photo.jpg (TTL 0s)". */
							'msg'    => sprintf( __( 'Google found assets without an efficient cache policy — e.g. %s. Enable Browser Caching to fix this.', 'cacheloop-simple-page-cache-speed-optimization' ), $audit['detail'] ? $audit['detail'] : $val ),
							'action' => 'enable_browser_cache',
						);
					} else {
						$tips[] = array(
							'type'   => 'warning',
							/* translators: %s: the specific asset and its cache TTL, e.g. "photo.jpg (TTL 0s)". */
							'msg'    => sprintf( __( 'Browser Caching is on, but Google still finds assets with no cache lifetime — e.g. %s. If your server runs Nginx, this setting\'s rules may not apply: Nginx doesn\'t read .htaccess. Use the "Download Nginx Config" button in the Core Caching tab\'s Browser Caching section to get the equivalent rules for your Nginx server block.', 'cacheloop-simple-page-cache-speed-optimization' ), $audit['detail'] ? $audit['detail'] : $val ),
							'action' => '',
						);
					}
					break;

				case 'modern-image-formats':
				case 'uses-optimized-images':
				case 'uses-responsive-images':
				case 'image-delivery-insight':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: the specific image and its savings, e.g. "photo.jpg (420KiB)", or Google's overall savings if no single file stands out. */
						'msg'    => sprintf( __( 'Google found unoptimized images — e.g. %s. CacheLoop does not compress or convert images — use an image optimization plugin, or convert them to WebP/AVIF manually.', 'cacheloop-simple-page-cache-speed-optimization' ), $audit['detail'] ? $audit['detail'] : $val ),
						'action' => '',
					);
					break;

				case 'unused-css-rules':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: Google's measured savings. */
						'msg'    => sprintf( __( 'Google found unused CSS (%s), likely loaded by a theme/plugin style not needed on this page. Review your active plugins to see if one can be disabled here.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
						'action' => '',
					);
					break;

				case 'unused-javascript':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: Google's measured savings. */
						'msg'    => sprintf( __( 'Google found unused JavaScript (%s), likely loaded by a plugin feature not needed on this page. Review your active plugins to see if one can be disabled here.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
						'action' => '',
					);
					break;

				case 'bootup-time':
				case 'mainthread-work-breakdown':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: Google's measured JS execution / main-thread time. */
						'msg'    => sprintf( __( 'Google found heavy JavaScript execution (%s), usually caused by a specific plugin or theme script. Try disabling non-essential plugins on this page one at a time to spot the culprit, or add its script to the Delay JS exclusions.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
						'action' => 'configure_delay_js_exclusions',
					);
					break;

				case 'total-byte-weight':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: Google's measured total page weight. */
						'msg'    => sprintf( __( 'Google found a heavy page payload (%s), usually from large images or video. Compress your media or remove unused plugins that load extra assets.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
						'action' => '',
					);
					break;

				case 'dom-size':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: Google's measured DOM size. */
						'msg'    => sprintf( __( 'Google found an excessively large page structure (%s). Consider simplifying the layout, reducing widgets, or paginating long content.', 'cacheloop-simple-page-cache-speed-optimization' ), $val ),
						'action' => '',
					);
					break;

				case 'third-party-summary':
				case 'third-parties-insight':
					$tips[] = array(
						'type'   => 'info',
						/* translators: %s: the specific third-party script and its cost, e.g. "Google Tag Manager (48ms)". */
						'msg'    => sprintf( __( 'Google found third-party scripts blocking the page — e.g. %s. Add it to the Delay JS exclusions, or remove it if non-essential.', 'cacheloop-simple-page-cache-speed-optimization' ), $audit['detail'] ? $audit['detail'] : $val ),
						'action' => 'configure_delay_js_exclusions',
					);
					break;

				case 'lcp-breakdown-insight':
					// Only meaningful once LCP is already flagged slow above — explains
					// *where* the time goes (server, discovery delay, download, or render).
					if ( $lcp_s > 2.5 && ! empty( $audit['detail'] ) ) {
						$tips[] = array(
							'type'   => 'info',
							/* translators: %s: which LCP phase is the bottleneck, e.g. "Biggest delay: Resource load delay (1066ms)". */
							'msg'    => sprintf( __( 'LCP breakdown from Google: %s. That\'s the phase to focus on.', 'cacheloop-simple-page-cache-speed-optimization' ), $audit['detail'] ),
							'action' => '',
						);
					}
					break;

				default:
					// No CacheLoop-specific fix for this one — still show exactly what Google found,
					// preferring the specific file/entity we extracted over the bare headline value.
					$shown_value = $audit['detail'] ? $audit['detail'] : $val;
					if ( ! empty( $audit['title'] ) ) {
						$tips[] = array(
							'type'   => 'info',
							'msg'    => $shown_value
								? sprintf(
									/* translators: 1: Google's audit title, 2: Google's measured value or the specific file/entity found. */
									__( 'Google also flagged: "%1$s" (%2$s).', 'cacheloop-simple-page-cache-speed-optimization' ),
									$audit['title'],
									$shown_value
								)
								: sprintf(
									/* translators: %s: Google's audit title. */
									__( 'Google also flagged: "%s".', 'cacheloop-simple-page-cache-speed-optimization' ),
									$audit['title']
								),
							'action' => '',
						);
					}
					break;
			}
		}

		// Additional Settings Checks if Score < 90
		if ( $stats['score'] < 90 ) {
			if ( empty( $this->settings['minify'] ) ) {
				$tips[] = array(
					'type'   => 'info',
					'msg'    => __( 'Minification is disabled. Enable CSS/JS Minify to reduce file sizes and improve load times.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_minify',
				);
			}
			if ( empty( $this->settings['lazy_load'] ) ) {
				$tips[] = array(
					'type'   => 'info',
					'msg'    => __( 'Lazy Load is disabled. Enable it to defer offscreen images and boost your performance score.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_lazy_load',
				);
			}
			if ( empty( $this->settings['gzip'] ) ) {
				$tips[] = array(
					'type'   => 'info',
					'msg'    => __( 'Gzip Compression is disabled. Turn it on to drastically reduce the size of transferred data.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_gzip',
				);
			}
			if ( empty( $this->settings['browser_cache'] ) ) {
				$tips[] = array(
					'type'   => 'info',
					'msg'    => __( 'Browser Caching is disabled. Enable it to speed up load times for returning visitors.', 'cacheloop-simple-page-cache-speed-optimization' ),
					'action' => 'enable_browser_cache',
				);
			}
		}

		// All good only if score >= 90
		if ( empty( $tips ) && $stats['score'] >= 90 ) {
			$tips[] = array(
				'type'   => 'success',
				'msg'    => __( 'Excellent! Your mobile score is 90+ and your metrics are all green. Great job!', 'cacheloop-simple-page-cache-speed-optimization' ),
				'action' => '',
			);
		} elseif ( empty( $tips ) ) {
			$tips[] = array(
				'type'   => 'info',
				'msg'    => sprintf(
					// translators: %d is the PageSpeed score.
					__( 'Score: %d/100. Metrics look acceptable. Keep monitoring!', 'cacheloop-simple-page-cache-speed-optimization' ),
					$stats['score']
				),
				'action' => '',
			);
		}

		return $tips;
	}

	/**
	 * Extracts the actual filenames Google's last Speedtest flagged (render-blocking,
	 * unused, unminified, heavy JS execution), split by CSS/JS, so the "Select CSS to
	 * Combine/Defer" and "Select JS to Delay" lists can pre-suggest what to optimize
	 * instead of leaving the user to guess which scanned file to check.
	 *
	 * @return array{css: string[], js: string[]}
	 */
	public function get_google_flagged_assets() {
		$flagged = array(
			'css' => array(),
			'js'  => array(),
		);

		$stats  = $this->get_pagespeed_stats();
		$mobile = $stats['mobile'];

		if ( empty( $mobile['audits'] ) || ! is_array( $mobile['audits'] ) ) {
			return $flagged;
		}

		// Only audits whose top offender is a combinable/delayable asset — not
		// images, cache TTL, DOM size, third-parties, etc.
		$relevant_ids = array(
			'render-blocking-insight',
			'render-blocking-resources',
			'bootup-time',
			'mainthread-work-breakdown',
			'unused-javascript',
			'unminified-javascript',
			'unused-css-rules',
			'unminified-css',
		);

		foreach ( $mobile['audits'] as $audit ) {
			if ( empty( $audit['id'] ) || ! in_array( $audit['id'], $relevant_ids, true ) || empty( $audit['detail'] ) ) {
				continue;
			}
			// detail looks like "filename.ext (165ms)" — strip the trailing metric.
			$name = trim( preg_replace( '/\s*\([^)]*\)\s*$/', '', $audit['detail'] ) );
			if ( '' === $name ) {
				continue;
			}
			if ( preg_match( '/\.css$/i', $name ) && ! in_array( $name, $flagged['css'], true ) ) {
				$flagged['css'][] = $name;
			} elseif ( preg_match( '/\.js$/i', $name ) && ! in_array( $name, $flagged['js'], true ) ) {
				$flagged['js'][] = $name;
			}
		}

		return $flagged;
	}

	/**
	 * Handles the AJAX request to save PageSpeed data from JS.
	 */
	public function cl_save_pagespeed() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die();
		}

		$strategy   = isset( $_POST['strategy'] ) ? sanitize_text_field( wp_unslash( $_POST['strategy'] ) ) : 'mobile';
		$score      = isset( $_POST['score'] ) ? intval( wp_unslash( $_POST['score'] ) ) : 0;
		$lcp        = isset( $_POST['lcp'] ) ? sanitize_text_field( wp_unslash( $_POST['lcp'] ) ) : '-';
		$fcp        = isset( $_POST['fcp'] ) ? sanitize_text_field( wp_unslash( $_POST['fcp'] ) ) : '-';
		$tbt        = isset( $_POST['tbt'] ) ? sanitize_text_field( wp_unslash( $_POST['tbt'] ) ) : '-';
		$cls        = isset( $_POST['cls'] ) ? sanitize_text_field( wp_unslash( $_POST['cls'] ) ) : '-';
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON, only ever json_decode()'d below; every extracted field is sanitized individually before use.
		$audits_raw       = isset( $_POST['audits'] ) ? wp_unslash( $_POST['audits'] ) : '';
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON, only ever json_decode()'d below; every extracted field is sanitized individually before use.
		$hero_image_raw   = isset( $_POST['hero_image_hint'] ) ? wp_unslash( $_POST['hero_image_hint'] ) : '';

		// The raw Lighthouse diagnostics/insights Google flagged (see JS in settings-page.php).
		// Kept so the Pro Analysis panel can quote Google's own findings with real numbers
		// (which script, which file, which third-party) instead of generic advice.
		$audits = array();
		if ( ! empty( $audits_raw ) ) {
			$decoded = json_decode( $audits_raw, true );
			if ( is_array( $decoded ) ) {
				foreach ( $decoded as $item ) {
					if ( empty( $item['id'] ) ) {
						continue;
					}
					$audits[] = array(
						'id'           => sanitize_key( $item['id'] ),
						'title'        => isset( $item['title'] ) ? sanitize_text_field( $item['title'] ) : '',
						'displayValue' => isset( $item['displayValue'] ) ? sanitize_text_field( $item['displayValue'] ) : '',
						'savingsMs'    => isset( $item['savingsMs'] ) ? absint( $item['savingsMs'] ) : 0,
						'detail'       => isset( $item['detail'] ) ? sanitize_text_field( $item['detail'] ) : '',
					);
				}
			}
		}

		// Best-effort hint at the actual LCP element (src/selector), so the LCP
		// Preload / Lazy Load tips can point at a real image instead of "your hero image".
		$hero_image_hint = array();
		if ( ! empty( $hero_image_raw ) ) {
			$decoded_hero = json_decode( $hero_image_raw, true );
			if ( is_array( $decoded_hero ) ) {
				$hero_image_hint = array(
					'src'       => isset( $decoded_hero['src'] ) ? sanitize_text_field( $decoded_hero['src'] ) : '',
					'truncated' => ! empty( $decoded_hero['truncated'] ),
					'selector'  => isset( $decoded_hero['selector'] ) ? sanitize_text_field( $decoded_hero['selector'] ) : '',
					'label'     => isset( $decoded_hero['label'] ) ? sanitize_text_field( $decoded_hero['label'] ) : '',
				);
			}
		}

		if ( $score > 0 ) {
			$stats      = array(
				'score'           => $score,
				'lcp'             => $lcp,
				'fcp'             => $fcp,
				'tbt'             => $tbt,
				'cls'             => $cls,
				'audits'          => $audits,
				'hero_image_hint' => $hero_image_hint,
				'timestamp'       => time(),
			);
			$option_key = ( 'desktop' === $strategy ) ? 'cacheloop_pagespeed_desktop' : 'cacheloop_pagespeed_mobile';
			update_option( $option_key, $stats, false );
		}

		wp_send_json_success();
	}

	/**
	 * AJAX proxy: calls the Google PageSpeed API server-side.
	 * This allows API keys restricted to a specific domain or IP to work correctly,
	 * because the request originates from the server (not the user's browser admin page).
	 *
	 * @return void
	 */
	public function cl_run_pagespeed() {
		check_ajax_referer( 'cacheloop_admin_nonce', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cacheloop-simple-page-cache-speed-optimization' ) ) );
		}

		$settings = get_option( self::OPTION_KEY, array() );
		$home_url = get_home_url();

		// Prefer the key passed live from the JS input field (works before saving settings).
		// Fall back to the DB-saved key.
		$posted_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '';
		$saved_key  = isset( $settings['pagespeed_api_key'] ) ? sanitize_text_field( $settings['pagespeed_api_key'] ) : '';
		$api_key    = ! empty( $posted_key ) ? $posted_key : $saved_key;
		$strategy   = isset( $_POST['strategy'] ) ? sanitize_text_field( wp_unslash( $_POST['strategy'] ) ) : 'mobile';

		$api_url = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'
			. '?url=' . rawurlencode( $home_url )
			. '&strategy=' . rawurlencode( $strategy )
			. '&category=performance';

		if ( ! empty( $api_key ) ) {
			$api_url .= '&key=' . rawurlencode( $api_key );
		}

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$response = wp_remote_get(
			$api_url,
			array(
				'timeout' => 60,
				'headers' => array(
					// Send the site URL as Referer so domain-restricted keys pass Google's check.
					'Referer' => $home_url,
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			wp_send_json_error( array( 'message' => $response->get_error_message() ) );
		}

		$http_code = (int) wp_remote_retrieve_response_code( $response );
		$body      = wp_remote_retrieve_body( $response );
		$data      = json_decode( $body, true );

		if ( 429 === $http_code ) {
			wp_send_json_error(
				array(
					'code'    => 429,
					'message' => __( 'Errore 429: Quota Google superata. Inserisci o controlla la API Key nel tab Advanced.', 'cacheloop-simple-page-cache-speed-optimization' ),
				)
			);
		}

		if ( 403 === $http_code ) {
			wp_send_json_error(
				array(
					'code'    => 403,
					'message' => __( "Errore 403: Abilita la 'PageSpeed Insights API' su console.cloud.google.com → API e servizi → Libreria.", 'cacheloop-simple-page-cache-speed-optimization' ),
				)
			);
		}

		if ( 200 !== $http_code || empty( $data ) ) {
			wp_send_json_error(
				array(
					/* translators: %d: Google PageSpeed HTTP error code */
					'message' => sprintf( __( 'Errore API Google: HTTP %d', 'cacheloop-simple-page-cache-speed-optimization' ), $http_code ),
				)
			);
		}

		wp_send_json_success( $data );
	}

	// ---------------------------------------------------------------------------
	// Plugin Action Links
	// ---------------------------------------------------------------------------

	/**
	 * Adds a "Settings" quick-link to the Plugins list page row.
	 *
	 * @param  array $links Default plugin action links.
	 * @return array        Modified links with Settings added.
	 */
	public function cl_add_action_links( $links ) {
		$settings_link = '<a href="' . esc_url( admin_url( 'admin.php?page=' . self::PAGE_SLUG ) ) . '">' . esc_html__( 'Settings', 'cacheloop-simple-page-cache-speed-optimization' ) . '</a>';
		array_unshift( $links, $settings_link );
		return $links;
	}

	/**
	 * Handles the export of CacheLoop settings.
	 */
	public function cl_handle_export_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		check_admin_referer( 'cacheloop_export_settings_nonce' );

		$settings = get_option( self::OPTION_KEY, array() );

		// Force download of the settings as JSON.
		header( 'Content-Type: application/json; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="cacheloop-settings-export-' . gmdate( 'Y-m-d' ) . '.json"' );
		echo wp_json_encode( $settings, JSON_PRETTY_PRINT );
		exit;
	}

	/**
	 * Handles the import of CacheLoop settings.
	 */
	public function cl_handle_import_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		check_admin_referer( 'cacheloop_import_settings_nonce' );

		if ( empty( $_FILES['cl_import_file']['tmp_name'] ) ) {
			wp_die( esc_html__( 'Please upload a file to import.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- tmp_name is a system-generated temp file path.
		$file_contents     = file_get_contents( $_FILES['cl_import_file']['tmp_name'] );
		$imported_settings = json_decode( $file_contents, true );

		if ( ! is_array( $imported_settings ) ) {
			wp_die( esc_html__( 'Invalid settings file. Please upload a valid CacheLoop JSON export.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		// Pass imported data through our sanitizer to ensure safety.
		$clean_settings = $this->cl_sanitize_settings( $imported_settings );

		update_option( self::OPTION_KEY, $clean_settings );

		// Redirect back with a success parameter.
		wp_safe_redirect(
			add_query_arg(
				array(
					'page'        => self::PAGE_SLUG,
					'cl_imported' => '1',
				),
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Handles the download of the generated Nginx configuration snippet
	 * (Gzip + static asset browser-cache rules), the Nginx equivalent of the
	 * .htaccess rules CACHELOOP_Browser_Cache writes for Apache/LiteSpeed.
	 */
	public function cl_handle_download_nginx_config() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		check_admin_referer( 'cacheloop_download_nginx_config_nonce' );

		CACHELOOP_Nginx_Cache::download_rules( $this->settings );
	}

	/**
	 * Handles the emergency reset of Freemius's local option cache
	 * (used to recover from stuck activation/license states).
	 */
	public function cl_handle_reset_fs() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'cacheloop-simple-page-cache-speed-optimization' ) );
		}

		check_admin_referer( 'cacheloop_reset_fs_action' );

		global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'fs_api_cache_%'" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'fs_accounts_%'" );
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'fs_active_plugins_%'" );

		wp_safe_redirect(
			add_query_arg(
				array(
					'page'        => self::PAGE_SLUG,
					'cl_fs_reset' => '1',
				),
				admin_url( 'admin.php' )
			)
		);
		exit;
	}

	// ---------------------------------------------------------------------------
	// Admin Toolbar
	// ---------------------------------------------------------------------------

	/**
	 * AJAX handler: adds CacheLoop buttons to the WordPress admin toolbar.
	 *
	 * @param \WP_Admin_Bar $wp_admin_bar The admin bar instance.
	 * @return void
	 */
	public function add_admin_toolbar_buttons( $wp_admin_bar ) {
		if ( ! is_admin_bar_showing() || ! current_user_can( 'manage_options' ) ) {
			return;
		}

		// "Clear All Cache" button.
		$wp_admin_bar->add_node(
			array(
				'id'    => 'cl-clear-cache',
				'title' => '<span style="color:#fff;background:#2271b1;padding:3px 8px;border-radius:3px;">⚡ Clear All Cache</span>',
				'href'  => wp_nonce_url( admin_url( 'admin-post.php?action=cacheloop_clear_cache' ), 'cacheloop_admin_nonce' ),
				'meta'  => array( 'class' => 'cl-toolbar-clear' ),
			)
		);

		// "Purge This Page" button (only on front-end editable pages).
		if ( is_singular() ) {
			$post_id = get_queried_object_id();
			$wp_admin_bar->add_node(
				array(
					'id'    => 'cl-purge-this-page',
					'title' => '<span style="color:#fff;background:#d63638;padding:3px 8px;border-radius:3px;">🗑️ Purge This Page</span>',
					'href'  => wp_nonce_url(
						add_query_arg(
							array(
								'cl_purge_page' => 1,
								'cl_post_id'    => $post_id,
							),
							home_url( '/' )
						),
						'cacheloop_admin_nonce'
					),
					'meta'  => array( 'class' => 'cl-toolbar-purge' ),
				)
			);
		}
	}

	// ---------------------------------------------------------------------------
	// Metrics Helpers
	// ---------------------------------------------------------------------------

	/**
	 * Counts files recursively in a directory.
	 *
	 * @param string $dir Directory path.
	 * @return int File count.
	 */
	private function count_files_recursive( $dir ) {
		$count    = 0;
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS )
		);
		foreach ( $iterator as $file ) {
			if ( $file->isFile() ) {
				++$count;
			}
		}
		return $count;
	}

	/**
	 * Calculates directory size in bytes.
	 *
	 * @param string $dir Directory path.
	 * @return int Size in bytes.
	 */
	private function dir_size( $dir ) {
		$size     = 0;
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS )
		);
		foreach ( $iterator as $file ) {
			if ( $file->isFile() ) {
				$size += $file->getSize();
			}
		}
		return $size;
	}

	/**
	 * Formats bytes to human-readable string.
	 *
	 * @param int $bytes Number of bytes.
	 * @return string Formatted string.
	 */
	private function format_bytes( $bytes ) {
		$units = array( 'B', 'KB', 'MB', 'GB' );
		$i     = 0;
		while ( $bytes >= 1024 && $i < 3 ) {
			$bytes /= 1024;
			++$i;
		}
		return round( $bytes, 2 ) . ' ' . $units[ $i ];
	}
}
