<?php
/**
 * Cache module — Health Check (embedded diagnostic)
 *
 * Self-test suite that runs from the Advanced tab and produces a copyable
 * report (text + JSON) so the user can validate a remote install without SSH
 * access. Every check is best-effort and never throws — a check that cannot
 * be evaluated returns status "warn" with a reason instead of breaking the
 * whole report.
 *
 * Result shape:
 *   array{
 *     summary: array{pass:int, fail:int, warn:int, info:int, total:int, ok:bool},
 *     checks:  array<int, array{id:string, group:string, label:string,
 *                               status:'pass'|'fail'|'warn'|'info',
 *                               message:string, details:string}>,
 *     env:     array{wp:string, php:string, plugin:string, server:string,
 *                    site_url:string, multisite:bool, generated_at:string}
 *   }
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Health_Check {

	/** @var Blaminhor_Essentials_Module_Cache */
	private $parent;

	/** @var array<int, array<string, mixed>> */
	private $checks = array();

	public function __construct( Blaminhor_Essentials_Module_Cache $parent ) {
		$this->parent = $parent;
	}

	/**
	 * Run every check and return the report.
	 *
	 * @return array<string, mixed>
	 */
	public function run() {
		$this->checks = array();

		// A. Drop-in / WP_CACHE.
		$this->check_wp_cache_constant();
		$this->check_dropin_exists();
		$this->check_dropin_is_ours();
		$this->check_dropin_version();

		// B. Filesystem.
		$this->check_cache_dir_exists();
		$this->check_pages_dir_writable();
		$this->check_critical_min_logs_writable();
		$this->check_wp_content_writable();

		// C. .htaccess.
		$this->check_htaccess_supported();
		$this->check_htaccess_block_browser_cache();
		$this->check_htaccess_block_gzip();

		// D. Lifecycle.
		$this->check_lifecycle_miss_hit_etag_purge();

		// E. REST RUM.
		$this->check_rest_route_registered();
		$this->check_rest_route_responds();

		// F. Crons.
		$this->check_cron_aggregator();
		$this->check_cron_preloader();

		// G. DB schema.
		$this->check_table_web_vitals();
		$this->check_table_web_vitals_p75();

		// H. Conflict detector.
		$this->check_conflict_detector_callable();
		$this->check_no_conflicts_active();

		// I. Deactivation safety.
		$this->check_deactivation_dry_run();
		$this->check_uninstall_php_present();

		// J. Capabilities.
		$this->check_php_capabilities();
		$this->check_safe_mode_state();
		$this->check_disk_usage_reasonable();

		return array(
			'summary' => $this->summary(),
			'checks'  => $this->checks,
			'env'     => $this->env(),
		);
	}

	/* ------------------------------------------------------------------ */
	/*  Aggregation                                                        */
	/* ------------------------------------------------------------------ */

	private function summary() {
		$pass = $fail = $warn = $info = 0;
		foreach ( $this->checks as $c ) {
			switch ( $c['status'] ) {
				case 'pass': $pass++; break;
				case 'fail': $fail++; break;
				case 'warn': $warn++; break;
				default:     $info++; break;
			}
		}
		return array(
			'pass'  => $pass,
			'fail'  => $fail,
			'warn'  => $warn,
			'info'  => $info,
			'total' => count( $this->checks ),
			'ok'    => 0 === $fail,
		);
	}

	private function env() {
		global $wp_version;
		return array(
			'wp'           => isset( $wp_version ) ? (string) $wp_version : 'unknown',
			'php'          => PHP_VERSION,
			'plugin'       => defined( 'BLAMINHOR_ESSENTIALS_VERSION' ) ? BLAMINHOR_ESSENTIALS_VERSION : 'unknown',
			'server'       => $this->parent->environment->get_server_software(),
			'site_url'     => home_url( '/' ),
			'multisite'    => is_multisite(),
			'generated_at' => gmdate( 'Y-m-d H:i:s' ) . ' UTC',
		);
	}

	/**
	 * Append a check result.
	 */
	private function add( $id, $group, $label, $status, $message, $details = '' ) {
		$this->checks[] = array(
			'id'      => (string) $id,
			'group'   => (string) $group,
			'label'   => (string) $label,
			'status'  => in_array( $status, array( 'pass', 'fail', 'warn', 'info' ), true ) ? $status : 'info',
			'message' => (string) $message,
			'details' => (string) $details,
		);
	}

	/* ------------------------------------------------------------------ */
	/*  A. Drop-in / WP_CACHE                                              */
	/* ------------------------------------------------------------------ */

	private function check_wp_cache_constant() {
		$enabled = (bool) $this->parent->get_setting( 'page_cache_enabled', true );
		if ( ! $enabled ) {
			$this->add(
				'wp_cache_constant', 'dropin',
				__( 'WP_CACHE constant', 'blaminhor-essentials' ),
				'info',
				__( 'Page cache disabled in settings — WP_CACHE not required.', 'blaminhor-essentials' )
			);
			return;
		}
		$defined = defined( 'WP_CACHE' ) && WP_CACHE;
		$this->add(
			'wp_cache_constant', 'dropin',
			__( 'WP_CACHE constant', 'blaminhor-essentials' ),
			$defined ? 'pass' : 'fail',
			$defined
				? __( 'WP_CACHE is defined and true in wp-config.php.', 'blaminhor-essentials' )
				: __( 'WP_CACHE is not enabled — the drop-in will not run, page cache served via PHP fallback.', 'blaminhor-essentials' )
		);
	}

	private function check_dropin_exists() {
		$enabled = (bool) $this->parent->get_setting( 'page_cache_enabled', true );
		$exists  = file_exists( WP_CONTENT_DIR . '/advanced-cache.php' );
		if ( ! $enabled ) {
			$this->add(
				'dropin_exists', 'dropin',
				__( 'advanced-cache.php drop-in', 'blaminhor-essentials' ),
				$exists ? 'warn' : 'info',
				$exists
					? __( 'Drop-in is installed but page cache is disabled — consider re-enabling page cache or removing the drop-in.', 'blaminhor-essentials' )
					: __( 'Drop-in not installed (page cache disabled).', 'blaminhor-essentials' )
			);
			return;
		}
		$this->add(
			'dropin_exists', 'dropin',
			__( 'advanced-cache.php drop-in', 'blaminhor-essentials' ),
			$exists ? 'pass' : 'fail',
			$exists
				? __( 'Drop-in file present in wp-content/.', 'blaminhor-essentials' )
				: __( 'Drop-in is missing — early-phase serving is unavailable. Use "Reinstall now" in the Advanced tab.', 'blaminhor-essentials' )
		);
	}

	private function check_dropin_is_ours() {
		$path = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( ! file_exists( $path ) ) {
			$this->add(
				'dropin_is_ours', 'dropin',
				__( 'Drop-in ownership marker', 'blaminhor-essentials' ),
				'info',
				__( 'No drop-in present.', 'blaminhor-essentials' )
			);
			return;
		}
		$ours = $this->parent->dropin_installer->is_installed();
		$this->add(
			'dropin_is_ours', 'dropin',
			__( 'Drop-in ownership marker', 'blaminhor-essentials' ),
			$ours ? 'pass' : 'fail',
			$ours
				? __( 'BLAMINHOR_ESSENTIALS_DROPIN marker found.', 'blaminhor-essentials' )
				: __( 'A foreign advanced-cache.php is installed — another caching plugin owns it. Remove it before activating ours.', 'blaminhor-essentials' )
		);
	}

	private function check_dropin_version() {
		$path = $this->parent->storage->dropin_version_path();
		if ( ! file_exists( $path ) ) {
			$this->add(
				'dropin_version', 'dropin',
				__( 'Drop-in version marker', 'blaminhor-essentials' ),
				'info',
				__( 'No version marker on disk yet (drop-in may not have been installed since this version).', 'blaminhor-essentials' )
			);
			return;
		}
		$installed = trim( (string) @file_get_contents( $path ) );
		$current   = Blaminhor_Essentials_Cache_Dropin_Installer::DROPIN_VERSION;
		$ok        = $installed === $current;
		$this->add(
			'dropin_version', 'dropin',
			__( 'Drop-in version marker', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'warn',
			$ok
				? sprintf( /* translators: %s: drop-in version */ __( 'Installed drop-in version %s matches current.', 'blaminhor-essentials' ), $current )
				: sprintf( /* translators: 1: installed version, 2: current version */ __( 'Installed drop-in version (%1$s) differs from current (%2$s) — reinstall to refresh.', 'blaminhor-essentials' ), $installed, $current )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  B. Filesystem                                                      */
	/* ------------------------------------------------------------------ */

	private function check_cache_dir_exists() {
		$dir = untrailingslashit( $this->parent->storage->base() );
		$ok  = is_dir( $dir );
		$this->add(
			'cache_dir_exists', 'filesystem',
			__( 'Cache base directory', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? sprintf( /* translators: %s: directory path */ __( 'Base directory exists at %s.', 'blaminhor-essentials' ), $dir )
				: __( 'Base directory missing — file writes will fail.', 'blaminhor-essentials' )
		);
	}

	private function check_pages_dir_writable() {
		$dir = untrailingslashit( $this->parent->storage->pages_dir() );
		if ( ! is_dir( $dir ) ) {
			$this->add(
				'pages_dir_writable', 'filesystem',
				__( 'Pages cache directory writable', 'blaminhor-essentials' ),
				'fail',
				__( 'pages/ directory does not exist.', 'blaminhor-essentials' )
			);
			return;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only health probe; no write performed.
		$ok = is_writable( $dir );
		$this->add(
			'pages_dir_writable', 'filesystem',
			__( 'Pages cache directory writable', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? __( 'pages/ is writable.', 'blaminhor-essentials' )
				: sprintf( /* translators: %s: directory path */ __( 'Directory %s is not writable — page cache will not persist.', 'blaminhor-essentials' ), $dir )
		);
	}

	private function check_critical_min_logs_writable() {
		$paths = array(
			'critical/' => $this->parent->storage->critical_dir(),
			'min/'      => $this->parent->storage->min_dir(),
			'logs/'     => $this->parent->storage->logs_dir(),
		);
		$failures = array();
		foreach ( $paths as $label => $dir ) {
			$d = untrailingslashit( $dir );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only health probe; no write performed.
			if ( ! is_dir( $d ) || ! is_writable( $d ) ) {
				$failures[] = $label;
			}
		}
		if ( empty( $failures ) ) {
			$this->add(
				'aux_dirs_writable', 'filesystem',
				__( 'Critical / min / logs directories writable', 'blaminhor-essentials' ),
				'pass',
				__( 'All auxiliary cache directories exist and are writable.', 'blaminhor-essentials' )
			);
		} else {
			$this->add(
				'aux_dirs_writable', 'filesystem',
				__( 'Critical / min / logs directories writable', 'blaminhor-essentials' ),
				'warn',
				sprintf(
					/* translators: %s: comma-separated list of subdirectories */
					__( 'Some auxiliary directories are missing or not writable: %s.', 'blaminhor-essentials' ),
					implode( ', ', $failures )
				)
			);
		}
	}

	private function check_wp_content_writable() {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only health probe; no write performed.
		$ok = is_writable( WP_CONTENT_DIR );
		$this->add(
			'wp_content_writable', 'filesystem',
			__( 'wp-content/ writable', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? __( 'wp-content/ is writable — drop-in install/remove possible.', 'blaminhor-essentials' )
				: __( 'wp-content/ is not writable — cannot install or update advanced-cache.php.', 'blaminhor-essentials' )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  C. .htaccess                                                       */
	/* ------------------------------------------------------------------ */

	private function check_htaccess_supported() {
		$server    = $this->parent->environment->get_server_software();
		$supported = $this->parent->environment->supports_htaccess();
		$this->add(
			'htaccess_supported', 'htaccess',
			__( 'Server supports .htaccess', 'blaminhor-essentials' ),
			$supported ? 'pass' : 'info',
			$supported
				? sprintf( /* translators: %s: server software */ __( 'Detected %s — .htaccess directives are honoured.', 'blaminhor-essentials' ), ucfirst( $server ) )
				: sprintf( /* translators: %s: server software */ __( 'Detected %s — browser cache and GZIP must be configured server-side, not via .htaccess.', 'blaminhor-essentials' ), ucfirst( $server ) )
		);
	}

	private function check_htaccess_block_browser_cache() {
		if ( ! $this->parent->environment->supports_htaccess() ) {
			return;
		}
		$enabled = (bool) $this->parent->get_setting( 'browser_cache_enabled', true );
		$exists  = $this->parent->htaccess_writer->block_exists( 'browser-cache' );
		if ( ! $enabled ) {
			$this->add(
				'htaccess_browser_cache', 'htaccess',
				__( 'Browser cache .htaccess block', 'blaminhor-essentials' ),
				$exists ? 'warn' : 'info',
				$exists
					? __( 'Block present in .htaccess but the setting is disabled — toggle the setting to re-sync.', 'blaminhor-essentials' )
					: __( 'Browser cache disabled in settings — block absent (expected).', 'blaminhor-essentials' )
			);
			return;
		}
		$this->add(
			'htaccess_browser_cache', 'htaccess',
			__( 'Browser cache .htaccess block', 'blaminhor-essentials' ),
			$exists ? 'pass' : 'fail',
			$exists
				? __( '"# BEGIN Blaminhor Essentials Cache: browser-cache" block found.', 'blaminhor-essentials' )
				: __( 'Browser cache enabled but no .htaccess block written — check that .htaccess is writable.', 'blaminhor-essentials' )
		);
	}

	private function check_htaccess_block_gzip() {
		if ( ! $this->parent->environment->supports_htaccess() ) {
			return;
		}
		$enabled = (bool) $this->parent->get_setting( 'gzip_enabled', true );
		$exists  = $this->parent->htaccess_writer->block_exists( 'gzip' );
		if ( ! $enabled ) {
			$this->add(
				'htaccess_gzip', 'htaccess',
				__( 'GZIP .htaccess block', 'blaminhor-essentials' ),
				$exists ? 'warn' : 'info',
				$exists
					? __( 'Block present in .htaccess but the setting is disabled — toggle the setting to re-sync.', 'blaminhor-essentials' )
					: __( 'GZIP disabled in settings — block absent (expected).', 'blaminhor-essentials' )
			);
			return;
		}
		$this->add(
			'htaccess_gzip', 'htaccess',
			__( 'GZIP .htaccess block', 'blaminhor-essentials' ),
			$exists ? 'pass' : 'fail',
			$exists
				? __( '"# BEGIN Blaminhor Essentials Cache: gzip" block found.', 'blaminhor-essentials' )
				: __( 'GZIP enabled but no .htaccess block written — check that .htaccess is writable.', 'blaminhor-essentials' )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  D. Lifecycle                                                       */
	/* ------------------------------------------------------------------ */

	/**
	 * Combined MISS → HIT → ETag → Purge probe.
	 *
	 * Performed via wp_remote_get against home_url('/') so we exercise the
	 * exact same code path a public visitor hits. The test is skipped if the
	 * page cache is disabled or if safe-mode is active (both are legitimate
	 * configurations under which a HIT could not happen).
	 */
	private function check_lifecycle_miss_hit_etag_purge() {
		if ( ! $this->parent->get_setting( 'page_cache_enabled', true ) ) {
			$this->add( 'lifecycle_miss_hit', 'lifecycle', __( 'MISS → HIT cycle', 'blaminhor-essentials' ), 'info', __( 'Page cache disabled — skipped.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_etag', 'lifecycle', __( 'ETag stable across requests', 'blaminhor-essentials' ), 'info', __( 'Page cache disabled — skipped.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_purge', 'lifecycle', __( 'Purge clears cached files', 'blaminhor-essentials' ), 'info', __( 'Page cache disabled — skipped.', 'blaminhor-essentials' ) );
			return;
		}
		if ( $this->parent->safe_mode->is_active() ) {
			$this->add( 'lifecycle_miss_hit', 'lifecycle', __( 'MISS → HIT cycle', 'blaminhor-essentials' ), 'warn', __( 'Safe mode is on — cache is intentionally bypassed, MISS/HIT not testable.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_etag', 'lifecycle', __( 'ETag stable across requests', 'blaminhor-essentials' ), 'warn', __( 'Safe mode is on — skipped.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_purge', 'lifecycle', __( 'Purge clears cached files', 'blaminhor-essentials' ), 'warn', __( 'Safe mode is on — skipped.', 'blaminhor-essentials' ) );
			return;
		}

		// Pre-flight: clear the home URL so we know the first request is a MISS.
		$this->parent->purge->purge_url( home_url( '/' ) );

		// Force one full render to seed the cache. The "miss" hit returns
		// either MISS (drop-in not installed → PHP path) or no header (when
		// served by the drop-in for a brand-new visitor with no cache file
		// the drop-in falls through to WP and PHP path runs). Either way the
		// shutdown handler writes the file.
		$first = $this->probe_request();

		if ( null === $first ) {
			$this->add( 'lifecycle_miss_hit', 'lifecycle', __( 'MISS → HIT cycle', 'blaminhor-essentials' ), 'warn', __( 'Could not reach the home URL via wp_remote_get — server may block loopback HTTP requests.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_etag', 'lifecycle', __( 'ETag stable across requests', 'blaminhor-essentials' ), 'warn', __( 'Skipped because the seeding request failed.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_purge', 'lifecycle', __( 'Purge clears cached files', 'blaminhor-essentials' ), 'warn', __( 'Skipped because the seeding request failed.', 'blaminhor-essentials' ) );
			return;
		}

		// Tiny pause so the shutdown write completes on slow filesystems.
		usleep( 200000 );

		$second = $this->probe_request();
		if ( null === $second ) {
			$this->add( 'lifecycle_miss_hit', 'lifecycle', __( 'MISS → HIT cycle', 'blaminhor-essentials' ), 'warn', __( 'Could not reach the home URL on second request.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_etag', 'lifecycle', __( 'ETag stable across requests', 'blaminhor-essentials' ), 'warn', __( 'Skipped because the second request failed.', 'blaminhor-essentials' ) );
			$this->add( 'lifecycle_purge', 'lifecycle', __( 'Purge clears cached files', 'blaminhor-essentials' ), 'warn', __( 'Skipped because the second request failed.', 'blaminhor-essentials' ) );
			return;
		}

		$be_cache    = isset( $second['headers']['x-be-cache'] ) ? $second['headers']['x-be-cache'] : '';
		$is_hit      = ( 'HIT' === strtoupper( $be_cache ) ) || 0 === strpos( strtoupper( (string) $be_cache ), 'HIT' );
		// File must also exist on disk (corroboration when no header was emitted).
		$expected_file = trailingslashit( $this->parent->storage->url_to_cache_dir( home_url( '/' ) ) ) . $this->parent->page_cache->variant_key() . '.html';
		$file_exists   = is_file( $expected_file );
		$disk_ok       = $file_exists;

		$details_lines = array(
			'1st X-BE-Cache: ' . ( isset( $first['headers']['x-be-cache'] ) ? $first['headers']['x-be-cache'] : '(none)' ),
			'2nd X-BE-Cache: ' . ( '' === $be_cache ? '(none)' : $be_cache ),
			'cache file: ' . ( $disk_ok ? 'present' : 'missing' ),
		);
		$this->add(
			'lifecycle_miss_hit', 'lifecycle',
			__( 'MISS → HIT cycle', 'blaminhor-essentials' ),
			( $is_hit || $disk_ok ) ? 'pass' : 'fail',
			( $is_hit || $disk_ok )
				? __( 'Cache is being served on the second hit (or persisted to disk).', 'blaminhor-essentials' )
				: __( 'No HIT header on second hit and no cache file on disk — page cache is not capturing output.', 'blaminhor-essentials' ),
			implode( "\n", $details_lines )
		);

		// ETag stability: 2nd and (synthetic) 3rd request should return the same ETag.
		$third = $this->probe_request();
		$etag2 = isset( $second['headers']['etag'] ) ? trim( $second['headers']['etag'] ) : '';
		$etag3 = is_array( $third ) && isset( $third['headers']['etag'] ) ? trim( $third['headers']['etag'] ) : '';
		if ( '' === $etag2 && '' === $etag3 ) {
			$this->add(
				'lifecycle_etag', 'lifecycle',
				__( 'ETag stable across requests', 'blaminhor-essentials' ),
				'info',
				__( 'No ETag emitted (drop-in only emits ETag for HIT responses; if you see HIT-304 in your browser, the ETag is working).', 'blaminhor-essentials' )
			);
		} else {
			$stable = ( '' !== $etag2 ) && ( $etag2 === $etag3 );
			$this->add(
				'lifecycle_etag', 'lifecycle',
				__( 'ETag stable across requests', 'blaminhor-essentials' ),
				$stable ? 'pass' : 'warn',
				$stable
					? __( 'ETag is identical on consecutive HIT responses.', 'blaminhor-essentials' )
					: __( 'ETag changed between two HIT responses — investigate cache file mtime/size churn.', 'blaminhor-essentials' ),
				'etag #2: ' . $etag2 . "\netag #3: " . $etag3
			);
		}

		// Purge clears the file we just created.
		$this->parent->purge->purge_url( home_url( '/' ) );
		$still_there = is_file( $expected_file );
		$this->add(
			'lifecycle_purge', 'lifecycle',
			__( 'Purge clears cached files', 'blaminhor-essentials' ),
			$still_there ? 'fail' : 'pass',
			$still_there
				? __( 'After calling purge_url(home), the cache file is still on disk — purge is not effective.', 'blaminhor-essentials' )
				: __( 'After calling purge_url(home), the cache file was removed.', 'blaminhor-essentials' )
		);
	}

	/**
	 * Issue an HTTP request to home_url() and normalise the response.
	 * Returns null on failure.
	 *
	 * @return array{status:int, headers:array<string,string>, body_len:int}|null
	 */
	private function probe_request() {
		$url      = home_url( '/' );
		$response = wp_remote_get(
			$url,
			array(
				'timeout'    => 8,
				'redirection'=> 2,
				'sslverify'  => false,
				'user-agent' => 'Blaminhor-Essentials-HealthCheck/1.0',
				'cookies'    => array(),
				'headers'    => array(
					'Cache-Control' => 'no-cache',
				),
			)
		);
		if ( is_wp_error( $response ) ) {
			return null;
		}
		$headers = array();
		$raw     = wp_remote_retrieve_headers( $response );
		if ( is_object( $raw ) && method_exists( $raw, 'getAll' ) ) {
			foreach ( $raw->getAll() as $k => $v ) {
				$headers[ strtolower( $k ) ] = is_array( $v ) ? implode( ', ', $v ) : (string) $v;
			}
		} elseif ( is_array( $raw ) ) {
			foreach ( $raw as $k => $v ) {
				$headers[ strtolower( $k ) ] = is_array( $v ) ? implode( ', ', $v ) : (string) $v;
			}
		}
		return array(
			'status'   => (int) wp_remote_retrieve_response_code( $response ),
			'headers'  => $headers,
			'body_len' => strlen( (string) wp_remote_retrieve_body( $response ) ),
		);
	}

	/* ------------------------------------------------------------------ */
	/*  E. REST RUM                                                        */
	/* ------------------------------------------------------------------ */

	private function check_rest_route_registered() {
		$server = function_exists( 'rest_get_server' ) ? rest_get_server() : null;
		if ( ! $server ) {
			$this->add(
				'rest_route_registered', 'rest',
				__( 'REST route /web-vitals registered', 'blaminhor-essentials' ),
				'warn',
				__( 'REST server not available in this context.', 'blaminhor-essentials' )
			);
			return;
		}
		$routes     = $server->get_routes();
		$expected   = '/' . Blaminhor_Essentials_Cache_Webvitals_Collector::NAMESPACE_REST . Blaminhor_Essentials_Cache_Webvitals_Collector::ENDPOINT;
		$registered = isset( $routes[ $expected ] );
		$this->add(
			'rest_route_registered', 'rest',
			__( 'REST route /web-vitals registered', 'blaminhor-essentials' ),
			$registered ? 'pass' : 'fail',
			$registered
				? sprintf( /* translators: %s: REST route */ __( 'Route %s is registered.', 'blaminhor-essentials' ), $expected )
				: sprintf( /* translators: %s: REST route */ __( 'Route %s is NOT registered — RUM samples will not be collected.', 'blaminhor-essentials' ), $expected )
		);
	}

	private function check_rest_route_responds() {
		if ( ! $this->parent->get_setting( 'webvitals_enabled', false ) ) {
			$this->add(
				'rest_route_responds', 'rest',
				__( 'REST route /web-vitals responds', 'blaminhor-essentials' ),
				'info',
				__( 'Web Vitals RUM disabled — endpoint will be a no-op (200), not exercised.', 'blaminhor-essentials' )
			);
			return;
		}
		// Do an internal REST dispatch (no HTTP loopback) so we exercise the
		// callback even on hosts that block self-loopback.
		$request = new WP_REST_Request(
			'POST',
			'/' . Blaminhor_Essentials_Cache_Webvitals_Collector::NAMESPACE_REST . Blaminhor_Essentials_Cache_Webvitals_Collector::ENDPOINT
		);
		$request->set_param( 'metric', 'LCP' );
		$request->set_param( 'value', 1234 );
		$request->set_param( 'url', home_url( '/' ) );
		$request->set_param( 'device', 'desktop' );
		$request->set_param( 'rating', 'good' );
		$response = rest_do_request( $request );
		$code     = $response instanceof WP_REST_Response ? $response->get_status() : 0;
		$ok       = in_array( $code, array( 200, 204 ), true );
		$this->add(
			'rest_route_responds', 'rest',
			__( 'REST route /web-vitals responds', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? sprintf( /* translators: %d: HTTP status code */ __( 'Endpoint responded with %d.', 'blaminhor-essentials' ), $code )
				: sprintf( /* translators: %d: HTTP status code */ __( 'Endpoint responded with %d — collector is not accepting samples.', 'blaminhor-essentials' ), $code )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  F. Crons                                                           */
	/* ------------------------------------------------------------------ */

	private function check_cron_aggregator() {
		$enabled   = (bool) $this->parent->get_setting( 'webvitals_enabled', false );
		$next      = wp_next_scheduled( Blaminhor_Essentials_Cache_Webvitals_Aggregator::CRON_HOOK );
		if ( ! $enabled ) {
			$this->add(
				'cron_aggregator', 'crons',
				__( 'Web Vitals aggregator cron', 'blaminhor-essentials' ),
				$next ? 'warn' : 'info',
				$next
					? __( 'Cron is scheduled but Web Vitals is disabled — it will be unscheduled on next page load.', 'blaminhor-essentials' )
					: __( 'Web Vitals disabled — aggregator cron not needed.', 'blaminhor-essentials' )
			);
			return;
		}
		$this->add(
			'cron_aggregator', 'crons',
			__( 'Web Vitals aggregator cron', 'blaminhor-essentials' ),
			$next ? 'pass' : 'fail',
			$next
				? sprintf( /* translators: %s: human-readable time */ __( 'Next run scheduled in %s.', 'blaminhor-essentials' ), human_time_diff( time(), (int) $next ) )
				: __( 'Cron not scheduled — daily aggregation will not run.', 'blaminhor-essentials' )
		);
	}

	private function check_cron_preloader() {
		$enabled = (bool) $this->parent->get_setting( 'preload_sitemap_enabled', false );
		$next    = wp_next_scheduled( Blaminhor_Essentials_Cache_Preloader::CRON_HOOK );
		if ( ! $enabled ) {
			$this->add(
				'cron_preloader', 'crons',
				__( 'Sitemap preloader cron', 'blaminhor-essentials' ),
				$next ? 'warn' : 'info',
				$next
					? __( 'Cron is scheduled but sitemap preload is disabled — it will be unscheduled on next page load.', 'blaminhor-essentials' )
					: __( 'Sitemap preload disabled — cron not needed.', 'blaminhor-essentials' )
			);
			return;
		}
		$this->add(
			'cron_preloader', 'crons',
			__( 'Sitemap preloader cron', 'blaminhor-essentials' ),
			$next ? 'pass' : 'fail',
			$next
				? sprintf( /* translators: %s: human-readable time */ __( 'Next run scheduled in %s.', 'blaminhor-essentials' ), human_time_diff( time(), (int) $next ) )
				: __( 'Cron not scheduled — sitemap warmup will not run.', 'blaminhor-essentials' )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  G. DB schema                                                       */
	/* ------------------------------------------------------------------ */

	private function check_table_web_vitals() {
		$this->check_table_columns(
			'be_web_vitals',
			array( 'id', 'blog_id', 'url_hash', 'url', 'device', 'metric', 'value', 'rating', 'navigation_type', 'user_agent_hash', 'recorded_at' ),
			__( 'Table be_web_vitals schema', 'blaminhor-essentials' ),
			'table_web_vitals'
		);
	}

	private function check_table_web_vitals_p75() {
		$this->check_table_columns(
			'be_web_vitals_p75',
			array( 'id', 'blog_id', 'period_date', 'url_hash', 'url', 'device', 'metric', 'p75_value', 'sample_count' ),
			__( 'Table be_web_vitals_p75 schema', 'blaminhor-essentials' ),
			'table_web_vitals_p75'
		);
	}

	private function check_table_columns( $suffix, array $expected_columns, $label, $id ) {
		global $wpdb;
		$table = $wpdb->prefix . $suffix;
		if ( ! blaminhor_essentials_validate_table_name( $table ) ) {
			$this->add( $id, 'database', $label, 'fail', __( 'Invalid table name.', 'blaminhor-essentials' ) );
			return;
		}
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
		if ( $exists !== $table ) {
			$this->add( $id, 'database', $label, 'fail', sprintf( /* translators: %s: table name */ __( 'Table %s does not exist.', 'blaminhor-essentials' ), $table ) );
			return;
		}
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,PluginCheck.Security.DirectDB.UnescapedDBParameter
		$rows    = $wpdb->get_results( "SHOW COLUMNS FROM {$table}", ARRAY_A );
		$columns = is_array( $rows ) ? array_map( static function ( $r ) { return $r['Field']; }, $rows ) : array();
		$missing = array_diff( $expected_columns, $columns );
		if ( empty( $missing ) ) {
			$this->add( $id, 'database', $label, 'pass', sprintf( /* translators: %d: column count */ __( 'Table exists with all %d expected columns.', 'blaminhor-essentials' ), count( $expected_columns ) ) );
		} else {
			$this->add(
				$id, 'database', $label, 'fail',
				sprintf(
					/* translators: %s: comma-separated list of missing column names */
					__( 'Table is present but missing columns: %s. Re-activate the module to run dbDelta.', 'blaminhor-essentials' ),
					implode( ', ', $missing )
				)
			);
		}
	}

	/* ------------------------------------------------------------------ */
	/*  H. Conflict detector                                               */
	/* ------------------------------------------------------------------ */

	private function check_conflict_detector_callable() {
		$ok = is_object( $this->parent->conflict_detector ) && method_exists( $this->parent->conflict_detector, 'detect_active_conflicts' );
		$this->add(
			'conflict_detector_callable', 'conflicts',
			__( 'Conflict detector ready', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? sprintf(
					/* translators: %d: number of plugins */
					__( 'Detector knows %d cache plugins.', 'blaminhor-essentials' ),
					count( $this->parent->conflict_detector->get_known_cache_plugins() )
				)
				: __( 'Conflict detector instance missing.', 'blaminhor-essentials' )
		);
	}

	private function check_no_conflicts_active() {
		$conflicts = $this->parent->conflict_detector->detect_active_conflicts();
		if ( empty( $conflicts ) ) {
			$this->add(
				'no_conflicts_active', 'conflicts',
				__( 'No competing cache plugin active', 'blaminhor-essentials' ),
				'pass',
				__( 'No conflicting cache plugin detected.', 'blaminhor-essentials' )
			);
			return;
		}
		$names = $this->parent->conflict_detector->format_conflict_list( $conflicts );
		$this->add(
			'no_conflicts_active', 'conflicts',
			__( 'No competing cache plugin active', 'blaminhor-essentials' ),
			'fail',
			sprintf( /* translators: %s: list of conflicting plugin names */ __( 'Active cache plugin(s) detected: %s. Disable them to avoid double-caching.', 'blaminhor-essentials' ), $names )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  I. Deactivation safety                                             */
	/* ------------------------------------------------------------------ */

	private function check_deactivation_dry_run() {
		$callable_on_deactivate = method_exists( $this->parent, 'on_deactivate' );
		$has_remove_all         = is_object( $this->parent->htaccess_writer ) && method_exists( $this->parent->htaccess_writer, 'remove_all' );
		$dropin_uninstall       = is_object( $this->parent->dropin_installer ) && method_exists( $this->parent->dropin_installer, 'uninstall' );
		$ok = $callable_on_deactivate && $has_remove_all && $dropin_uninstall;
		$this->add(
			'deactivation_dry_run', 'safety',
			__( 'Deactivation routines available', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? __( 'on_deactivate, drop-in uninstall and .htaccess cleanup are all callable.', 'blaminhor-essentials' )
				: __( 'One of the deactivation routines is missing — module would leave artefacts behind.', 'blaminhor-essentials' )
		);
	}

	private function check_uninstall_php_present() {
		$path = defined( 'BLAMINHOR_ESSENTIALS_PLUGIN_DIR' ) ? BLAMINHOR_ESSENTIALS_PLUGIN_DIR . 'uninstall.php' : '';
		$ok   = '' !== $path && is_readable( $path );
		$this->add(
			'uninstall_php_present', 'safety',
			__( 'uninstall.php present', 'blaminhor-essentials' ),
			$ok ? 'pass' : 'fail',
			$ok
				? __( 'uninstall.php is in place — full uninstall path is honoured by WordPress.', 'blaminhor-essentials' )
				: __( 'uninstall.php is missing — deletion via the WP Plugins screen will leave options behind.', 'blaminhor-essentials' )
		);
	}

	/* ------------------------------------------------------------------ */
	/*  J. Capabilities                                                    */
	/* ------------------------------------------------------------------ */

	private function check_php_capabilities() {
		$caps    = $this->parent->environment->get_capabilities();
		$missing = array();
		foreach ( array( 'gzencode', 'mbstring', 'dom_document' ) as $required ) {
			if ( empty( $caps[ $required ] ) ) {
				$missing[] = $required;
			}
		}
		if ( empty( $missing ) ) {
			$this->add(
				'php_capabilities', 'environment',
				__( 'PHP capabilities sufficient', 'blaminhor-essentials' ),
				'pass',
				__( 'gzencode, mbstring and DOMDocument are all available.', 'blaminhor-essentials' )
			);
		} else {
			$this->add(
				'php_capabilities', 'environment',
				__( 'PHP capabilities sufficient', 'blaminhor-essentials' ),
				'warn',
				sprintf( /* translators: %s: comma-separated list of missing PHP extensions */ __( 'Missing PHP capabilities: %s. Some optimizations will degrade gracefully.', 'blaminhor-essentials' ), implode( ', ', $missing ) )
			);
		}
	}

	private function check_safe_mode_state() {
		$active = $this->parent->safe_mode->is_active();
		$this->add(
			'safe_mode_state', 'environment',
			__( 'Safe mode state', 'blaminhor-essentials' ),
			$active ? 'warn' : 'pass',
			$active
				? __( 'Safe mode IS active — cache is fully bypassed. Disable it once debugging is finished.', 'blaminhor-essentials' )
				: __( 'Safe mode is off — cache serves normally.', 'blaminhor-essentials' )
		);
	}

	private function check_disk_usage_reasonable() {
		$bytes = $this->parent->storage->disk_usage();
		// 2 GB hard ceiling: above this we suspect runaway state.
		$warn_threshold = 2 * 1024 * 1024 * 1024;
		$this->add(
			'disk_usage_reasonable', 'environment',
			__( 'Cache disk usage', 'blaminhor-essentials' ),
			$bytes > $warn_threshold ? 'warn' : 'info',
			sprintf(
				/* translators: %s: human-readable file size */
				__( 'Total cache footprint: %s.', 'blaminhor-essentials' ),
				blaminhor_essentials_format_size( $bytes )
			)
		);
	}
}
