<?php
/**
 * BaseCloud Boost — Admin Controller
 *
 * @package BaseCloud\Boost\Admin
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Admin;

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

use BaseCloud\Boost\Core\Cache_Purge;
use BaseCloud\Boost\Core\Cache_Preloader;
use BaseCloud\Boost\Core\Page_Cache;
use BaseCloud\Boost\Core\Metrics_Collector;
use BaseCloud\Boost\Core\Webhook_Sender;
use BaseCloud\Boost\Core\Lighthouse_Sender;
use BaseCloud\Boost\Database\DB_Optimizer;
use BaseCloud\Boost\Optimizer\Image_Compressor;
/**
 * Registers and manages the plugin admin panel.
 */
class Admin_Controller {

	/** @var string[] Known caching plugins that conflict. Public: Fleet_Ops reuses this list for /health conflict detection. */
	public const CONFLICTING_PLUGINS = [
		'w3-total-cache/w3-total-cache.php',
		'wp-super-cache/wp-cache.php',
		'wp-rocket/wp-rocket.php',
		'litespeed-cache/litespeed-cache.php',
		'sg-cachepress/sg-cachepress.php',
		'hummingbird-performance/wp-hummingbird.php',
		'cache-enabler/cache-enabler.php',
		'comet-cache/comet-cache.php',
	];

	/**
	 * Register all admin hooks.
	 *
	 * @return void
	 */
	public function boot(): void {
		add_action( 'admin_menu',            [ $this, 'register_menu' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
		add_filter( 'admin_body_class',      [ $this, 'add_mode_body_class' ] );
		add_action( 'admin_notices',         [ $this, 'show_admin_notices' ] );
		add_action( 'admin_post_BCBOOST_save_settings', [ $this, 'handle_save_settings' ] );
		add_action( 'admin_post_BCBOOST_apply_preset',  [ $this, 'handle_apply_preset' ] );
		add_action( 'admin_init', [ $this, 'handle_admin_bar_action' ] );

		// After a save/preset/import redirect, guarantee the settings page renders
		// values FRESH from the database — never a stale object-cache copy. This is
		// the user's trustable "what you see is what is saved" confirmation.
		add_action( 'admin_init', [ $this, 'refresh_options_after_save' ], 1 );

		// AJAX handlers.
		// NOTE: Cache_Engine also registers wp_ajax_BCBOOST_clear_cache and, being
		// booted earlier, would answer first at equal priority (wp_send_json exits,
		// so only one handler ever responds). Priority 5 makes THIS handler the
		// responder so the toast reports an honest, itemised purge summary. It
		// replicates the legacy handler's behaviour (last_purge metric + purge_all),
		// so nothing is lost and nothing double-purges.
		add_action( 'wp_ajax_BCBOOST_clear_cache',      [ $this, 'ajax_clear_cache' ], 5 );
		add_action( 'wp_ajax_BCBOOST_db_cleanup',       [ $this, 'ajax_db_cleanup' ] );
		add_action( 'wp_ajax_BCBOOST_preloader_start',  [ $this, 'ajax_preloader_start' ] );
		add_action( 'wp_ajax_BCBOOST_preloader_stop',   [ $this, 'ajax_preloader_stop' ] );
		add_action( 'wp_ajax_BCBOOST_preloader_status', [ $this, 'ajax_preloader_status' ] );
		add_action( 'wp_ajax_BCBOOST_test_cloudflare',  [ $this, 'ajax_test_cloudflare' ] );
		add_action( 'wp_ajax_BCBOOST_test_bunnycdn',    [ $this, 'ajax_test_bunnycdn' ] );
		add_action( 'wp_ajax_BCBOOST_webhook_test',           [ $this, 'ajax_webhook_test' ] );
		add_action( 'wp_ajax_BCBOOST_send_metrics_now',       [ $this, 'ajax_send_metrics_now' ] );
		add_action( 'wp_ajax_BCBOOST_get_metrics',            [ $this, 'ajax_get_metrics' ] );
		add_action( 'wp_ajax_BCBOOST_lighthouse_send',         [ $this, 'ajax_lighthouse_send' ] );
		add_action( 'wp_ajax_BCBOOST_lighthouse_webhook_test', [ $this, 'ajax_lighthouse_webhook_test' ] );
		add_action( 'wp_ajax_BCBOOST_lighthouse_send_now',     [ $this, 'ajax_lighthouse_send_now' ] );
		add_action( 'wp_ajax_BCBOOST_run_psi',                 [ $this, 'ajax_run_psi' ] );

		// Image compression (Bulk Compress tool).
		add_action( 'wp_ajax_BCBOOST_compress_start',  [ $this, 'ajax_compress_start' ] );
		add_action( 'wp_ajax_BCBOOST_compress_batch',  [ $this, 'ajax_compress_batch' ] );
		add_action( 'wp_ajax_BCBOOST_compress_status', [ $this, 'ajax_compress_status' ] );
		add_action( 'wp_ajax_BCBOOST_compress_stop',   [ $this, 'ajax_compress_stop' ] );

		// Settings import/export.
		add_action( 'wp_ajax_BCBOOST_export_settings', [ $this, 'ajax_export_settings' ] );
		add_action( 'admin_post_BCBOOST_import_settings', [ $this, 'handle_import_settings' ] );

		// Fleet Ops pairing (Advanced tab).
		add_action( 'admin_post_BCBOOST_fleet_ops_generate_key', [ $this, 'handle_fleet_ops_generate_key' ] );
		add_action( 'admin_post_BCBOOST_fleet_ops_revoke_key',   [ $this, 'handle_fleet_ops_revoke_key' ] );
	}

	/**
	 * Register top-level admin menu and submenus.
	 *
	 * @return void
	 */
	public function register_menu(): void {
		add_menu_page(
			__( 'BaseCloud Boost', 'basecloud-boost' ),
			__( 'BaseCloud Boost', 'basecloud-boost' ),
			'manage_options',
			'basecloud-boost',
			[ $this, 'render_dashboard' ],
			'dashicons-performance',
			80
		);

		$tabs = [
			'dashboard'         => __( 'Dashboard', 'basecloud-boost' ),
			'page-cache'        => __( 'Page Cache', 'basecloud-boost' ),
			'file-optimization' => __( 'File Optimization', 'basecloud-boost' ),
			'media'             => __( 'Media', 'basecloud-boost' ),
			'preload'           => __( 'Preload', 'basecloud-boost' ),
			'cdn'               => __( 'CDN', 'basecloud-boost' ),
			'database'          => __( 'Database', 'basecloud-boost' ),
			'advanced'          => __( 'Advanced', 'basecloud-boost' ),
			'appearance'        => __( 'Appearance', 'basecloud-boost' ),
			'security'          => __( 'Security', 'basecloud-boost' ),
			'webhook'           => __( 'Webhook', 'basecloud-boost' ),
			'lighthouse'        => __( 'Lighthouse', 'basecloud-boost' ),
			'tools'             => __( 'Tools', 'basecloud-boost' ),
		];

		foreach ( $tabs as $slug => $title ) {
			add_submenu_page(
				'basecloud-boost',
				$title . ' — BaseCloud Boost',
				$title,
				'manage_options',
				'basecloud-' . $slug,
				[ $this, 'render_tab_' . str_replace( '-', '_', $slug ) ]
			);
		}
	}

	/**
	 * Valid admin themes: the two modes (light/dark), plus dark-based ambient
	 * backdrops (classic particles + shipped videos). The old Goop WebGL theme
	 * is retired — normalize_theme() migrates saved legacy values.
	 */
	public const ADMIN_THEMES = [
		'dark', 'light', 'classic',
		'video-boost', 'video-colours', 'video-fish-tank',
		'video-goop', 'video-lava-lamp', 'video-zombies',
	];

	/**
	 * Map any saved theme value (including retired ones) to a valid theme.
	 * 'goop' and 'minimal' collapse into 'dark' so upgrades never render an
	 * unknown backdrop.
	 *
	 * @param string $theme Saved theme value.
	 * @return string Valid ADMIN_THEMES member.
	 */
	public static function normalize_theme( string $theme ): string {
		if ( in_array( $theme, self::ADMIN_THEMES, true ) ) {
			return $theme;
		}
		return 'dark';
	}

	/**
	 * Whether a theme renders on the light UI mode. Everything except 'light'
	 * (classic particles, ambient videos) sits on the dark glass UI.
	 *
	 * @param string $theme Valid theme.
	 * @return bool
	 */
	public static function is_light_theme( string $theme ): bool {
		return 'light' === $theme;
	}

	/** Shipped accent colours — single source for defaults, reset, and sanitize fallback. */
	public const APPEARANCE_DEFAULTS = [
		'primary'   => '#4ade80',
		'secondary' => '#8b5cf6',
		'info'      => '#3b82f6',
		'highlight' => '#22d3ee',
	];

	/**
	 * The four saved accent colours (validated hex, defaults on anything invalid).
	 *
	 * @return array<string,string> primary|secondary|info|highlight => #rrggbb.
	 */
	public static function appearance_colors(): array {
		$out = [];
		foreach ( self::APPEARANCE_DEFAULTS as $slot => $default ) {
			$hex          = sanitize_hex_color( (string) BCBOOST_get_option( 'admin_color_' . $slot, $default ) );
			$out[ $slot ] = ( is_string( $hex ) && '' !== $hex ) ? $hex : $default;
		}
		return $out;
	}

	/**
	 * Re-declare the admin design tokens from the chosen accents. Derived shades
	 * (dark/light/glow/glass/border) are computed so ONE picked colour re-themes
	 * every component consistently.
	 *
	 * @param array<string,string> $c Accent colours from appearance_colors().
	 * @return string CSS.
	 */
	private static function build_accent_css( array $c ): string {
		$p = $c['primary'];
		$s = $c['secondary'];
		$i = $c['info'];
		$h = $c['highlight'];

		return ':root{'
			. '--bcboost-green:' . $p . ';'
			. '--bcboost-green-dark:' . self::shade( $p, -0.18 ) . ';'
			. '--bcboost-green-dim:' . self::shade( $p, -0.55 ) . ';'
			. '--bcboost-green-glow:' . self::rgba( $p, 0.25 ) . ';'
			. '--bcboost-green-glow-lg:' . self::rgba( $p, 0.5 ) . ';'
			. '--bcboost-glass-green:' . self::rgba( $p, 0.08 ) . ';'
			. '--bcboost-border-green:' . self::rgba( $p, 0.30 ) . ';'
			. '--bcboost-success-light:' . self::rgba( $p, 0.10 ) . ';'
			. '--bcboost-purple:' . $s . ';'
			. '--bcboost-purple-dark:' . self::shade( $s, -0.15 ) . ';'
			. '--bcboost-purple-light:' . self::shade( $s, 0.20 ) . ';'
			. '--bcboost-glass-purple:' . self::rgba( $s, 0.08 ) . ';'
			. '--bcboost-border-purple:' . self::rgba( $s, 0.30 ) . ';'
			. '--bcboost-blue:' . $i . ';'
			. '--bcboost-blue-light:' . self::shade( $i, 0.20 ) . ';'
			. '--bcboost-glass-blue:' . self::rgba( $i, 0.08 ) . ';'
			. '--bcboost-border-blue:' . self::rgba( $i, 0.30 ) . ';'
			. '--bcboost-cyan:' . $h . ';'
			. '--bcboost-shadow-glow-g:0 0 32px ' . self::rgba( $p, 0.15 ) . ',0 4px 24px rgba(0,0,0,0.5);'
			. '--bcboost-shadow-glow-p:0 0 32px ' . self::rgba( $s, 0.15 ) . ',0 4px 24px rgba(0,0,0,0.5);'
			. '--bcboost-shadow-glow-b:0 0 32px ' . self::rgba( $i, 0.15 ) . ',0 4px 24px rgba(0,0,0,0.5);'
			. '}';
	}

	/**
	 * Lighten (amount > 0, toward white) or darken (amount < 0, toward black) a hex colour.
	 *
	 * @param string $hex    #rgb or #rrggbb.
	 * @param float  $amount -1.0 … 1.0.
	 * @return string #rrggbb.
	 */
	private static function shade( string $hex, float $amount ): string {
		[ $r, $g, $b ] = self::rgb( $hex );
		$mix = static function ( int $ch ) use ( $amount ): int {
			$to = $amount >= 0 ? 255 : 0;
			return (int) round( $ch + ( $to - $ch ) * abs( $amount ) );
		};
		return sprintf( '#%02x%02x%02x', $mix( $r ), $mix( $g ), $mix( $b ) );
	}

	/**
	 * rgba() string from a hex colour.
	 *
	 * @param string $hex   Hex colour.
	 * @param float  $alpha 0–1.
	 * @return string
	 */
	private static function rgba( string $hex, float $alpha ): string {
		[ $r, $g, $b ] = self::rgb( $hex );
		// number_format: float→string casting is locale-sensitive on PHP < 8.0 —
		// a comma-decimal locale would emit rgba(…,0,25) and break every token.
		return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . number_format( $alpha, 2, '.', '' ) . ')';
	}

	/**
	 * Hex → [r,g,b] ints. Accepts #rgb and #rrggbb; anything else = mid grey.
	 *
	 * @param string $hex Hex colour.
	 * @return int[]
	 */
	private static function rgb( string $hex ): array {
		$hex = ltrim( trim( $hex ), '#' );
		if ( strlen( $hex ) === 3 ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}
		if ( ! preg_match( '/^[0-9a-f]{6}$/i', $hex ) ) {
			return [ 128, 128, 128 ];
		}
		return [ (int) hexdec( substr( $hex, 0, 2 ) ), (int) hexdec( substr( $hex, 2, 2 ) ), (int) hexdec( substr( $hex, 4, 2 ) ) ];
	}

	/**
	 * Enqueue admin CSS and JS only on BaseCloud Boost pages.
	 *
	 * @param string $hook Current admin page hook.
	 * @return void
	 */
	public function enqueue_admin_assets( string $hook ): void {
		if ( strpos( $hook, 'basecloud' ) === false ) {
			return;
		}

		wp_enqueue_style(
			'bcboost-admin',
			BCBOOST_URL . 'admin/css/admin.css',
			[],
			BCBOOST_VERSION
		);

		// ── Appearance: user-chosen accents override the CSS design tokens ──
		// Every component styles itself through the :root custom properties, so
		// re-declaring them re-themes the whole admin (token-driven theming).
		$colors = self::appearance_colors();
		wp_add_inline_style( 'bcboost-admin', self::build_accent_css( $colors ) );

		$theme = self::normalize_theme( (string) BCBOOST_get_option( 'admin_theme', 'dark' ) );

		wp_enqueue_script(
			'bcboost-admin',
			BCBOOST_URL . 'admin/js/admin.js',
			[],
			BCBOOST_VERSION,
			true
		);

		wp_localize_script( 'bcboost-admin', 'bcbAdmin', [
			'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
			'theme'         => $theme,
			'colors'        => $colors,
			'colorDefaults' => self::APPEARANCE_DEFAULTS,
			'nonce'         => wp_create_nonce( 'BCBOOST_admin' ),
			'exportNonce'   => wp_create_nonce( 'BCBOOST_export_settings' ),
			'siteUrl'       => get_site_url(),
			// The PSI API key is intentionally NOT exposed here. Audits run through
			// the server-side BCBOOST_run_psi proxy so the key never reaches the browser.
			'lighthouseUrl' => (string) BCBOOST_get_option( 'lighthouse_default_url', '' ),
			'webpSupported' => \BaseCloud\Boost\Optimizer\Image_Compressor::webp_supported(),
			'webpSupportReason' => \BaseCloud\Boost\Optimizer\Image_Compressor::webp_supported()
				? ''
				: \BaseCloud\Boost\Optimizer\Image_Compressor::unsupported_reason( 'webp' ),
			'strings'       => [
				'compressing'       => __( 'Compressing…', 'basecloud-boost' ),
				'compressDone'      => __( 'Compression complete!', 'basecloud-boost' ),
				'compressRetrying'  => __( 'Connection hiccup — retrying…', 'basecloud-boost' ),
				'imagesFailed'      => __( 'image(s) could not be converted — click for details', 'basecloud-boost' ),
				'clearing'          => __( 'Boosting…', 'basecloud-boost' ),
				'cleared'           => __( 'Cache Boosted!', 'basecloud-boost' ),
				'error'             => __( 'An error occurred. Please try again.', 'basecloud-boost' ),
				'confirmClear'      => __( 'Clear all cache and boost performance?', 'basecloud-boost' ),
				'confirmCleanup'    => __( 'Run database cleanup?', 'basecloud-boost' ),
				'preloaderStarted'  => __( 'Preloader launched.', 'basecloud-boost' ),
				'preloaderStopped'  => __( 'Preloader stopped.', 'basecloud-boost' ),
				'cdnConnected'      => __( 'Connected!', 'basecloud-boost' ),
				'cdnFailed'         => __( 'Failed.', 'basecloud-boost' ),
				'cdnRequestFailed'  => __( 'Request failed.', 'basecloud-boost' ),
				'exportFailed'      => __( 'Export failed. Please try again.', 'basecloud-boost' ),
			],
		] );

		// Lighthouse page — inline CSS for score ring + spinner components.
		$lighthouse_css = '
.bcboost-lh-score-card{display:flex;flex-direction:column;align-items:center;gap:6px}
.bcboost-lh-score-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;opacity:.75}
.bcboost-lh-ring-bg{stroke:rgba(255,255,255,.08)}
.bcboost-lh-ring-fill{stroke:#4ade80;transition:stroke-dashoffset .9s cubic-bezier(.34,1.56,.64,1),stroke .3s}
.bcboost-lh-ring-fill--warn{stroke:#facc15}
.bcboost-lh-ring-fill--fail{stroke:#f87171}
.bcboost-lh-ring-text{fill:#f0f0f0;font-family:inherit}
.bcboost-lh-spinner{display:inline-flex;align-items:center;gap:6px;font-size:13px;opacity:.8}
@keyframes bcboost-spin{to{transform:rotate(360deg)}}
.bcboost-spin{animation:bcboost-spin 1s linear infinite}
.bcboost-lighthouse-controls{display:flex;gap:16px;flex-wrap:wrap;align-items:flex-end}
';
		wp_add_inline_style( 'bcboost-admin', $lighthouse_css );

		// CDN settings page — test connection buttons.
		$cdn_js = '( function () {
	function testCdn( btnId, resultId, action ) {
		var btn    = document.getElementById( btnId );
		var result = document.getElementById( resultId );
		if ( ! btn ) return;
		btn.addEventListener( "click", function () {
			btn.disabled = true;
			btn.classList.add( "bcboost-btn--loading" );
			result.textContent = "";
			result.className   = "bcboost-cdn-test-result";
			fetch( bcbAdmin.ajaxUrl, {
				method:  "POST",
				headers: { "Content-Type": "application/x-www-form-urlencoded" },
				body:    new URLSearchParams({ action: action, nonce: bcbAdmin.nonce }),
			} )
			.then( function(r){ return r.json(); } )
			.then( function(data) {
				result.textContent = ( data.data && data.data.message ) ? data.data.message : ( data.success ? bcbAdmin.strings.cdnConnected : bcbAdmin.strings.cdnFailed );
				result.classList.add( data.success ? "bcboost-cdn-test-result--ok" : "bcboost-cdn-test-result--err" );
			} )
			.catch( function() {
				result.textContent = bcbAdmin.strings.cdnRequestFailed;
				result.classList.add( "bcboost-cdn-test-result--err" );
			} )
			.finally( function() {
				btn.disabled = false;
				btn.classList.remove( "bcboost-btn--loading" );
			} );
		} );
	}
	document.addEventListener( "DOMContentLoaded", function () {
		testCdn( "bcboost-test-cloudflare", "bcboost-cf-test-result",    "BCBOOST_test_cloudflare" );
		testCdn( "bcboost-test-bunnycdn",   "bcboost-bunny-test-result", "BCBOOST_test_bunnycdn" );
	} );
} )();';
		wp_add_inline_script( 'bcboost-admin', $cdn_js );

		// Tools page — export settings button.
		$tools_js = '( function($) {
	$( "#bcboost-export-settings" ).on( "click", function() {
		var $btn = $( this ).prop( "disabled", true );
		$.post( ajaxurl, {
			action:      "BCBOOST_export_settings",
			_ajax_nonce: bcbAdmin.exportNonce,
		}, function( res ) {
			$btn.prop( "disabled", false );
			if ( res.success && res.data ) {
				var blob = new Blob( [ JSON.stringify( res.data, null, 2 ) ], { type: "application/json" } );
				var a    = document.createElement( "a" );
				a.href   = URL.createObjectURL( blob );
				a.download = "bcboost-settings-" + new Date().toISOString().slice( 0, 10 ) + ".json";
				document.body.appendChild( a );
				a.click();
				document.body.removeChild( a );
			} else {
				$( "#bcboost-export-feedback" ).show().text( bcbAdmin.strings.exportFailed );
			}
		} ).fail( function() {
			$btn.prop( "disabled", false );
			$( "#bcboost-export-feedback" ).show().text( bcbAdmin.strings.exportFailed );
		} );
	} );
} )( jQuery );';
		wp_add_inline_script( 'bcboost-admin', $tools_js );
	}

	/**
	 * Stamp the chosen UI mode on <body> (Boost pages only) so admin.css can
	 * theme every token from one class: bcboost-mode-light or bcboost-mode-dark.
	 *
	 * @param string $classes Space-separated admin body classes.
	 * @return string
	 */
	public function add_mode_body_class( string $classes ): string {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page detection.
		$page = (string) ( $_GET['page'] ?? '' );
		if ( 0 !== strpos( $page, 'basecloud' ) ) {
			return $classes;
		}
		$theme = self::normalize_theme( (string) BCBOOST_get_option( 'admin_theme', 'dark' ) );
		return $classes . ( self::is_light_theme( $theme ) ? ' bcboost-mode-light' : ' bcboost-mode-dark' );
	}

	/**
	 * Evict cached copies of the Boost options when re-rendering a settings page
	 * straight after a save/preset/import redirect, so the toggles displayed are
	 * read from the database — the source of truth — not from a possibly-stale
	 * persistent object cache (see Settings_API::persist()).
	 *
	 * @return void
	 */
	public function refresh_options_after_save(): void {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only cache eviction, no state change.
		if (
			isset( $_GET['BCBOOST_saved'] )
			|| isset( $_GET['BCBOOST_preset'] )
			|| isset( $_GET['BCBOOST_import_success'] )
		) {
			wp_cache_delete( BCBOOST_OPTIONS_KEY, 'options' );
			wp_cache_delete( 'alloptions', 'options' );
		}
		// phpcs:enable
	}

	/**
	 * Display admin notices.
	 *
	 * @return void
	 */
	public function show_admin_notices(): void {
		if ( isset( $_GET['BCBOOST_saved'] ) && current_user_can( 'manage_options' ) ) {
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'BaseCloud Boost settings saved.', 'basecloud-boost' ) . '</p></div>';
		}

		if ( isset( $_GET['BCBOOST_cleared'] ) && current_user_can( 'manage_options' ) ) {
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Cache cleared successfully.', 'basecloud-boost' ) . '</p></div>';
		}

		if ( isset( $_GET['BCBOOST_preset'] ) && current_user_can( 'manage_options' ) ) {
			echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Recommended PageSpeed settings applied and cache cleared. Re-run PageSpeed Insights in a minute to see the difference.', 'basecloud-boost' ) . '</p></div>';
		}

		$conflicts = $this->detect_conflicts();
		if ( ! empty( $conflicts ) ) {
			echo '<div class="notice notice-warning is-dismissible"><p>';
			printf(
				/* translators: %s comma-separated plugin list */
				esc_html__( 'BaseCloud Boost detected other caching plugins: %s. Running multiple caching plugins can cause conflicts.', 'basecloud-boost' ),
				'<strong>' . esc_html( implode( ', ', $conflicts ) ) . '</strong>'
			);
			echo '</p></div>';
		}
	}

	/**
	 * Handle settings form submission.
	 *
	 * @return void
	 */
	public function handle_save_settings(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'basecloud-boost' ) );
		}

		check_admin_referer( 'BCBOOST_save_settings' );

		// WordPress slashes $_POST (wp_magic_quotes), so quotes/backslashes arrive
		// escaped. Strip that one slashing layer before saving — otherwise CSS values
		// such as background-image:url("…") are stored as url(\"…\"), which is invalid
		// and silently breaks (e.g. hero backgrounds in Critical CSS). The per-field
		// sanitizers in Settings_API run on the unslashed value, as WordPress intends.
		Settings_API::save_settings( wp_unslash( $_POST ) );

		// Reschedule crons whenever settings are saved — picks up any schedule change.
		$this->reschedule_crons();

		wp_safe_redirect( add_query_arg( 'BCBOOST_saved', '1', wp_get_referer() ) );
		exit;
	}

	/**
	 * Apply the recommended "Optimize for PageSpeed" profile, then purge the cache
	 * so pages rebuild with the new optimisations.
	 *
	 * @return void
	 */
	public function handle_apply_preset(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'basecloud-boost' ) );
		}

		check_admin_referer( 'BCBOOST_apply_preset' );

		Settings_API::apply_performance_preset();
		( new Cache_Purge() )->purge_all();
		$this->reschedule_crons();

		$back = wp_get_referer() ?: admin_url( 'admin.php?page=basecloud-tools' );
		wp_safe_redirect( add_query_arg( 'BCBOOST_preset', '1', $back ) );
		exit;
	}

	/**
	 * Handle admin bar "Clear Cache" action.
	 *
	 * @return void
	 */
	public function handle_admin_bar_action(): void {
		if ( ! isset( $_GET['BCBOOST_action'] ) || $_GET['BCBOOST_action'] !== 'clear_all' ) {
			return;
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		check_admin_referer( 'BCBOOST_clear_all' );

		( new Cache_Purge() )->purge_all();

		// No-JS fallback: return to wherever the user clicked from, not the dashboard.
		// (With JavaScript enabled the toolbar button clears the cache in place via AJAX.)
		$back = wp_get_referer();
		if ( ! $back ) {
			$back = admin_url();
		}
		wp_safe_redirect( add_query_arg( 'BCBOOST_cleared', '1', $back ) );
		exit;
	}

	// ─── AJAX Handlers ────────────────────────────────────────────────────────

	/**
	 * AJAX: clear all cache.
	 *
	 * @return void
	 */
	public function ajax_clear_cache(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		( new Metrics_Collector() )->set( 'last_purge', time() );
		$summary = ( new Cache_Purge() )->purge_all();

		wp_send_json_success( [
			'message' => $this->purge_summary_message( $summary ),
			'summary' => $summary,
		] );
	}

	/**
	 * Build an honest purge-feedback message enumerating what was actually
	 * cleared, instead of the old blanket "All pages cleared."
	 *
	 * @param array{pages?:int, minified?:int} $summary Counts from Cache_Purge::purge_all().
	 * @return string
	 */
	private function purge_summary_message( array $summary ): string {
		$pages    = (int) ( $summary['pages'] ?? 0 );
		$minified = (int) ( $summary['minified'] ?? 0 );

		$parts = [];

		/* translators: %s: number of cached pages deleted */
		$parts[] = sprintf( _n( '%s cached page', '%s cached pages', $pages, 'basecloud-boost' ), number_format_i18n( $pages ) );

		/* translators: %s: number of minified asset files deleted */
		$parts[] = sprintf( _n( '%s minified asset', '%s minified assets', $minified, 'basecloud-boost' ), number_format_i18n( $minified ) );

		// Edge layers are purged via the BCBOOST_after_purge_all fan-out — only
		// claim them when the integration is actually active.
		if ( BCBOOST_get_option( 'varnish_enabled', false )
			|| \BaseCloud\Boost\Compatibility\Host_Detector::is_cloudways() ) {
			$parts[] = __( 'Varnish purge sent', 'basecloud-boost' );
		}
		if ( BCBOOST_get_option( 'cdn_enabled', false ) ) {
			$parts[] = __( 'Cloudflare purge sent', 'basecloud-boost' );
		}
		if ( BCBOOST_get_option( 'bunnycdn_enabled', false ) ) {
			$parts[] = __( 'BunnyCDN purge sent', 'basecloud-boost' );
		}

		/* translators: %s: comma-separated list of what was purged */
		return sprintf( __( 'Cache cleared: %s.', 'basecloud-boost' ), implode( ', ', $parts ) );
	}

	/**
	 * AJAX: run database cleanup.
	 *
	 * @return void
	 */
	public function ajax_db_cleanup(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		$action = sanitize_key( $_POST['cleanup_action'] ?? 'all' );
		$db     = new DB_Optimizer();

		switch ( $action ) {
			case 'revisions':
				$result = [ 'deleted' => $db->clean_post_revisions() ];
				break;
			case 'auto_drafts':
				$result = [ 'deleted' => $db->clean_auto_drafts() ];
				break;
			case 'trash':
				$result = [ 'deleted' => $db->clean_trashed_posts() ];
				break;
			case 'spam':
				$result = [ 'deleted' => $db->clean_spam_comments() ];
				break;
			case 'transients':
				$result = [ 'deleted' => $db->clean_expired_transients() ];
				break;
			case 'postmeta':
				$result = [ 'deleted' => $db->clean_orphaned_postmeta() ];
				break;
			case 'optimize':
				$result = [ 'tables' => $db->optimize_tables() ];
				break;
			default:
				$result = $db->run_all_cleanup();
				break;
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: start cache preloader.
	 *
	 * @return void
	 */
	public function ajax_preloader_start(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		$preloader = new Cache_Preloader();
		$preloader->start_preload();

		$progress = $preloader->get_preload_progress();
		if ( ( $progress['status'] ?? '' ) === 'skipped' ) {
			wp_send_json_success( [
				'message'  => __( 'Preloading skipped — Boost\'s page cache is inactive on this host, so there is nothing to warm.', 'basecloud-boost' ),
				'progress' => $progress,
			] );
		}

		wp_send_json_success( [
			'message'  => ( $progress['mode'] ?? '' ) === 'cron'
				? __( 'Preloader launched (WP-Cron mode — this host blocks loopback requests).', 'basecloud-boost' )
				: __( 'Preloader launched — warming in the background.', 'basecloud-boost' ),
			'progress' => $progress,
		] );
	}

	/**
	 * AJAX: stop cache preloader.
	 *
	 * @return void
	 */
	public function ajax_preloader_stop(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		( new Cache_Preloader() )->stop_preload();
		wp_send_json_success( [ 'message' => __( 'Preloader stopped.', 'basecloud-boost' ) ] );
	}

	/**
	 * AJAX: get preloader progress.
	 *
	 * @return void
	 */
	public function ajax_preloader_status(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		wp_send_json_success( ( new Cache_Preloader() )->get_preload_progress() );
	}

	/**
	 * AJAX: test Cloudflare credentials.
	 *
	 * @return void
	 */
	public function ajax_test_cloudflare(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		$result = ( new \BaseCloud\Boost\CDN\CDN_Integration() )->test_connection();
		if ( $result['success'] ) {
			wp_send_json_success( [ 'message' => $result['message'] ] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ] );
		}
	}

	/**
	 * AJAX: test BunnyCDN credentials.
	 *
	 * @return void
	 */
	public function ajax_test_bunnycdn(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		$result = ( new \BaseCloud\Boost\CDN\BunnyCDN_Integration() )->test_connection();
		if ( $result['success'] ) {
			wp_send_json_success( [ 'message' => $result['message'] ] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ] );
		}
	}

	/**
	 * AJAX: test webhook with a live send.
	 *
	 * @return void
	 */
	public function ajax_webhook_test(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		$url    = sanitize_text_field( $_POST['webhook_url'] ?? '' );
		$result = ( new Webhook_Sender() )->send( $url ?: null );
		if ( $result['success'] ) {
			wp_send_json_success( [ 'message' => $result['message'], 'payload' => $result['payload'] ?? [] ] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ] );
		}
	}

	/**
	 * AJAX: send metrics now via webhook.
	 *
	 * @return void
	 */
	public function ajax_send_metrics_now(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		$result = ( new Webhook_Sender() )->send();
		if ( $result['success'] ) {
			wp_send_json_success( [ 'message' => $result['message'] ] );
		} else {
			wp_send_json_error( [ 'message' => $result['message'] ] );
		}
	}

	/**
	 * AJAX: return current metrics for dashboard refresh.
	 *
	 * @return void
	 */
	public function ajax_get_metrics(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error();
		}
		$metrics    = new Metrics_Collector();
		$page_cache = new Page_Cache();
		$stats      = $metrics->get_all();
		$cache      = $page_cache->get_cache_stats();

		wp_send_json_success( [
			'hits'         => (int) $stats['hits'],
			'misses'       => (int) $stats['misses'],
			'hit_rate'     => $metrics->get_hit_rate(),
			'html_saved'   => (int) $stats['html_saved'],
			'css_saved'    => (int) $stats['css_saved'],
			'js_saved'     => (int) $stats['js_saved'],
			'total_saved'  => $metrics->get_total_saved_bytes(),
			'pages_cached' => (int) $cache['count'],
			'cache_size'   => (int) $cache['size'],
			'last_purge'   => $stats['last_purge'] ? human_time_diff( (int) $stats['last_purge'] ) . ' ago' : '—',
			'last_webhook' => $stats['last_webhook_sent'] ? human_time_diff( (int) $stats['last_webhook_sent'] ) . ' ago' : '—',
		] );
	}

	/**
	 * AJAX: receive Lighthouse results from the browser and send to webhook.
	 *
	 * @return void
	 */
	public function ajax_lighthouse_send(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON payload sanitized after decode.
		$raw = isset( $_POST['result'] ) ? wp_unslash( $_POST['result'] ) : '';
		if ( empty( $raw ) ) {
			wp_send_json_error( [ 'message' => __( 'No Lighthouse result provided.', 'basecloud-boost' ) ] );
		}

		// Validate it is a JSON string before decoding; individual fields are
		// sanitised inside Lighthouse_Sender::build_payload().
		if ( ! is_string( $raw ) || strlen( $raw ) > 5242880 ) { // 5 MB cap.
			wp_send_json_error( [ 'message' => __( 'Invalid Lighthouse result.', 'basecloud-boost' ) ] );
		}

		$result = json_decode( sanitize_text_field( $raw ), true );
		if ( ! is_array( $result ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid Lighthouse result JSON.', 'basecloud-boost' ) ] );
		}

		$sender  = new Lighthouse_Sender();
		$outcome = $sender->send( $result );

		if ( $outcome['success'] ) {
			wp_send_json_success( [
				'message' => $outcome['message'],
				'payload' => $sender->build_payload( $result ),
			] );
		} else {
			wp_send_json_error( [ 'message' => $outcome['message'] ] );
		}
	}

	/**
	 * AJAX: test Lighthouse webhook with a real server-side PSI audit (mobile).
	 *
	 * @return void
	 */
	public function ajax_lighthouse_webhook_test(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		$url_override = sanitize_text_field( $_POST['webhook_url'] ?? '' );
		$sender       = new Lighthouse_Sender();
		$test_url     = trim( (string) BCBOOST_get_option( 'lighthouse_default_url', '' ) ) ?: get_site_url();

		$result = $sender->fetch_psi( $test_url, 'mobile' );
		if ( null === $result ) {
			wp_send_json_error( [ 'message' => __( 'PageSpeed Insights audit failed. Check your API key or try again.', 'basecloud-boost' ) ] );
		}

		$outcome = $sender->send( $result, $url_override ?: null );

		if ( $outcome['success'] ) {
			wp_send_json_success( [
				'message' => $outcome['message'],
				'payload' => $sender->build_payload( $result ),
			] );
		} else {
			wp_send_json_error( [ 'message' => $outcome['message'] ] );
		}
	}

	/**
	 * AJAX: run server-side PSI audit for mobile + desktop and send both to webhook.
	 *
	 * @return void
	 */
	public function ajax_lighthouse_send_now(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		$sender = new Lighthouse_Sender();
		$sender->run_and_send_daily();

		wp_send_json_success( [ 'message' => __( 'Lighthouse audits dispatched for mobile & desktop.', 'basecloud-boost' ) ] );
	}

	/**
	 * AJAX: run a PageSpeed Insights audit server-side.
	 *
	 * Keeps the PSI API key on the server — it is never exposed to the browser.
	 *
	 * @return void
	 */
	public function ajax_run_psi(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		$url      = esc_url_raw( wp_unslash( $_POST['url'] ?? '' ) );
		$strategy = sanitize_key( $_POST['strategy'] ?? 'mobile' );
		if ( ! in_array( $strategy, [ 'mobile', 'desktop' ], true ) ) {
			$strategy = 'mobile';
		}
		if ( empty( $url ) ) {
			$url = (string) ( BCBOOST_get_option( 'lighthouse_default_url', '' ) ?: get_site_url() );
		}

		$result = ( new Lighthouse_Sender() )->fetch_psi( $url, $strategy );
		if ( null === $result ) {
			wp_send_json_error( [ 'message' => __( 'PageSpeed Insights audit failed. Check the URL or your API key and try again.', 'basecloud-boost' ) ] );
		}

		wp_send_json_success( $result );
	}

	/**
	 * AJAX: begin a bulk image-compression run.
	 *
	 * @return void
	 */
	public function ajax_compress_start(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		if ( ! Image_Compressor::webp_supported() ) {
			wp_send_json_error( [ 'message' => Image_Compressor::unsupported_reason( 'webp' ) ] );
		}
		wp_send_json_success( ( new Image_Compressor() )->start_bulk() );
	}

	/**
	 * AJAX: process the next batch of the bulk compression run.
	 *
	 * @return void
	 */
	public function ajax_compress_batch(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		wp_send_json_success( ( new Image_Compressor() )->process_bulk_batch( 5 ) );
	}

	/**
	 * AJAX: current bulk compression progress.
	 *
	 * @return void
	 */
	public function ajax_compress_status(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		wp_send_json_success( ( new Image_Compressor() )->get_bulk_progress() );
	}

	/**
	 * AJAX: stop a bulk compression run.
	 *
	 * @return void
	 */
	public function ajax_compress_stop(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}
		wp_send_json_success( ( new Image_Compressor() )->stop_bulk() );
	}

	// ─── Render Methods ───────────────────────────────────────────────────────

	/** @return void */
	public function render_dashboard(): void { $this->load_view( 'dashboard' ); }
	/** @return void */
	public function render_tab_dashboard(): void { $this->load_view( 'dashboard' ); }
	/** @return void */
	public function render_tab_page_cache(): void { $this->load_view( 'page-cache' ); }
	/** @return void */
	public function render_tab_file_optimization(): void { $this->load_view( 'file-optimization' ); }
	/** @return void */
	public function render_tab_media(): void { $this->load_view( 'media' ); }
	/** @return void */
	public function render_tab_preload(): void { $this->load_view( 'preload' ); }
	/** @return void */
	public function render_tab_database(): void { $this->load_view( 'database' ); }
	/** @return void */
	public function render_tab_cdn(): void { $this->load_view( 'cdn' ); }
	/** @return void */
	public function render_tab_advanced(): void { $this->load_view( 'advanced' ); }
	/** @return void */
	public function render_tab_appearance(): void { $this->load_view( 'appearance' ); }
	/** @return void */
	public function render_tab_security(): void { $this->load_view( 'security' ); }
	/** @return void */
	public function render_tab_webhook(): void { $this->load_view( 'webhook' ); }
	/** @return void */
	public function render_tab_lighthouse(): void { $this->load_view( 'lighthouse' ); }
	/** @return void */
	public function render_tab_tools(): void { $this->load_view( 'tools' ); }

	// ─── Import / Export ──────────────────────────────────────────────────────

	/**
	 * AJAX: export settings as JSON download.
	 *
	 * @return void
	 */
	public function ajax_export_settings(): void {
		check_ajax_referer( 'BCBOOST_admin', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Insufficient permissions.', 'basecloud-boost' ) ] );
		}

		$options = (array) get_option( BCBOOST_OPTIONS_KEY, [] );

		// The Fleet Ops pairing key is strictly site-local: even its encrypted
		// blob must never travel in an export (imports on another site would
		// carry an undecryptable — or on a cloned site, a LIVE — API credential).
		unset( $options['fleet_ops_key'] );

		wp_send_json_success( [ 'settings' => $options ] );
	}

	/**
	 * Handle settings import from uploaded JSON file.
	 *
	 * @return void
	 */
	public function handle_import_settings(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'basecloud-boost' ) );
		}

		check_admin_referer( 'BCBOOST_import_settings' );

		if ( empty( $_FILES['BCBOOST_settings_file']['tmp_name'] ) ) {
			wp_safe_redirect( add_query_arg( 'BCBOOST_import_error', 'no_file', admin_url( 'admin.php?page=basecloud-tools' ) ) );
			exit;
		}

		$file    = $_FILES['BCBOOST_settings_file']['tmp_name'];
		$json    = file_get_contents( $file );
		$options = json_decode( (string) $json, true );

		if ( ! is_array( $options ) ) {
			wp_safe_redirect( add_query_arg( 'BCBOOST_import_error', 'invalid_json', admin_url( 'admin.php?page=basecloud-tools' ) ) );
			exit;
		}

		$defaults  = Settings_API::get_defaults();
		$sanitized = [];
		foreach ( $defaults as $key => $default ) {
			if ( array_key_exists( $key, $options ) ) {
				$sanitized[ $key ] = Settings_API::sanitize_option( $key, $options[ $key ] );
			} else {
				$sanitized[ $key ] = $default;
			}
		}

		Settings_API::persist( $sanitized );

		wp_safe_redirect( add_query_arg( 'BCBOOST_import_success', '1', admin_url( 'admin.php?page=basecloud-tools' ) ) );
		exit;
	}

	// ─── Fleet Ops pairing ────────────────────────────────────────────────────

	/**
	 * Generate a new Fleet Ops pairing key: 32 random bytes as hex, stored
	 * encrypted (Crypto) via the single Settings_API::persist() write path.
	 * The plaintext is shown ONCE — parked in a short-lived encrypted
	 * transient that the Advanced view reads and deletes on next render.
	 *
	 * @return void
	 */
	public function handle_fleet_ops_generate_key(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'basecloud-boost' ) );
		}

		check_admin_referer( 'BCBOOST_fleet_ops' );

		try {
			$key = bin2hex( random_bytes( 32 ) );
		} catch ( \Throwable $e ) {
			// CSPRNG unavailable (exotic hosts): wp_generate_password() still
			// yields a high-entropy 64-char alphanumeric key.
			$key = wp_generate_password( 64, false );
		}

		$this->persist_fleet_ops_key( \BaseCloud\Boost\Core\Crypto::encrypt( $key ) );

		// Show-once handoff: encrypted at rest, 5-minute lifetime, deleted on display.
		set_transient( 'bcboost_fleet_ops_new_key', \BaseCloud\Boost\Core\Crypto::encrypt( $key ), 5 * MINUTE_IN_SECONDS );

		wp_safe_redirect( add_query_arg( 'BCBOOST_fleet_ops', 'paired', admin_url( 'admin.php?page=basecloud-advanced' ) ) );
		exit;
	}

	/**
	 * Revoke the Fleet Ops pairing key. Keyless = every bcboost/v1 request is
	 * refused (403) regardless of the enable toggle — this is the emergency cutoff.
	 *
	 * @return void
	 */
	public function handle_fleet_ops_revoke_key(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Insufficient permissions.', 'basecloud-boost' ) );
		}

		check_admin_referer( 'BCBOOST_fleet_ops' );

		$this->persist_fleet_ops_key( '' );
		delete_transient( 'bcboost_fleet_ops_new_key' );

		wp_safe_redirect( add_query_arg( 'BCBOOST_fleet_ops', 'revoked', admin_url( 'admin.php?page=basecloud-advanced' ) ) );
		exit;
	}

	/**
	 * Store a (pre-encrypted or empty) fleet_ops_key value, preserving every
	 * other saved option. Fresh-from-DB read — see Settings_API::persist() for
	 * why the cached copy can be stale on Redis hosts.
	 *
	 * @param string $stored_value Encrypted key blob, or '' to revoke.
	 * @return void
	 */
	private function persist_fleet_ops_key( string $stored_value ): void {
		wp_cache_delete( BCBOOST_OPTIONS_KEY, 'options' );
		wp_cache_delete( 'alloptions', 'options' );
		$existing = (array) get_option( BCBOOST_OPTIONS_KEY, [] );

		$merged                  = array_merge( Settings_API::get_defaults(), $existing );
		$merged['fleet_ops_key'] = $stored_value;

		Settings_API::persist( $merged );
	}

	// ─── Helpers ──────────────────────────────────────────────────────────────

	/**
	 * Reschedule both webhook crons to reflect the currently saved schedule options.
	 * Called after settings are saved so changes take effect immediately.
	 *
	 * @return void
	 */
	private function reschedule_crons(): void {
		$allowed = [ 'hourly', 'twicedaily', 'daily', 'weekly' ];

		$webhook_schedule     = (string) BCBOOST_get_option( 'webhook_schedule', 'daily' );
		$lighthouse_schedule  = (string) BCBOOST_get_option( 'lighthouse_schedule', 'daily' );

		if ( ! in_array( $webhook_schedule, $allowed, true ) ) {
			$webhook_schedule = 'daily';
		}
		if ( ! in_array( $lighthouse_schedule, $allowed, true ) ) {
			$lighthouse_schedule = 'daily';
		}

		wp_clear_scheduled_hook( 'BCBOOST_webhook_daily' );
		wp_schedule_event( time(), $webhook_schedule, 'BCBOOST_webhook_daily' );

		wp_clear_scheduled_hook( 'BCBOOST_lighthouse_daily' );
		wp_schedule_event( time(), $lighthouse_schedule, 'BCBOOST_lighthouse_daily' );
	}

	/**
	 * Detect active conflicting caching plugins.
	 *
	 * @return string[] Human-readable names.
	 */
	public function detect_conflicts(): array {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$conflicts = [];
		foreach ( self::CONFLICTING_PLUGINS as $plugin_file ) {
			if ( is_plugin_active( $plugin_file ) ) {
				$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file, false, false );
				$conflicts[] = $plugin_data['Name'] ?? $plugin_file;
			}
		}
		return $conflicts;
	}

	/**
	 * Load a view file from admin/views/.
	 *
	 * @param string $view View slug.
	 * @return void
	 */
	private function load_view( string $view ): void {
		$file = BCBOOST_DIR . 'admin/views/' . sanitize_file_name( $view ) . '.php';
		if ( file_exists( $file ) ) {
			include $file;
		}
	}
}
