<?php
/**
 * AVI Health Check — diagnostic dashboard + backend.
 *
 * Règles:
 * - Arrays PHP uniquement via array() (jamais []).
 * - Checks isolés (try/catch) : une erreur n'arrête pas les autres.
 * - Jamais afficher les valeurs complètes des secrets (len= uniquement).
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'Avision_Health_Check' ) ) {
	class Avision_Health_Check {
		/**
		 * Token bypass stable (24 chars).
		 *
		 * @return string
		 */
		public static function get_token() {
			if ( ! defined( 'AVISION_HC_TOKEN' ) ) {
				define( 'AVISION_HC_TOKEN', substr( md5( AUTH_KEY . 'avi_hc_2026' ), 0, 24 ) );
			}
			return AVISION_HC_TOKEN;
		}

		/**
		 * Lance tous les checks.
		 *
		 * @return array
		 */
		public static function run_full_check() {
			$results = self::base_results( 'full' );

			// Ordre strict.
			try {
				$results['categories']['premium_dom'] = self::checks_premium_dom();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['premium_dom'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Premium Dashboard',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['premium_dom'] = $cat;
			}

			try {
				$results['categories']['agency_dom'] = self::checks_agency_dom();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['agency_dom'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Agency Dashboard',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['agency_dom'] = $cat;
			}

			try {
				$results['categories']['backend_infra'] = self::checks_backend_infra();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['backend_infra'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Backend Infra',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['backend_infra'] = $cat;
			}

			try {
				$results['categories']['backend_api'] = self::checks_backend_api();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['backend_api'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Backend API',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['backend_api'] = $cat;
			}

			try {
				$results['categories']['db_keys'] = self::checks_db_keys();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['db_keys'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur DB/Clés',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['db_keys'] = $cat;
			}

			try {
				$results['categories']['cron_capacity'] = self::checks_cron_capacity();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['cron_capacity'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Capacité Crons',
						array(
							self::item_fail( 'Exception', $e->getMessage(), '' ),
						)
					)
				);
				$results['categories']['cron_capacity'] = $cat;
			}

			$results = self::finalize_scores( $results );

			if ( function_exists( 'update_option' ) ) {
				update_option( 'avision_hc_full_last', $results, false );
				$hist = get_option( 'avision_hc_history', array() );
				if ( ! is_array( $hist ) ) {
					$hist = array();
				}
				array_unshift( $hist, $results );
				$hist = array_slice( $hist, 0, 14 );
				update_option( 'avision_hc_history', $hist, false );
			}

			// --- Alerte email automatique Health Check ---
			// Items sont dans categories.*.sections[].items avec status ok|warn|fail.
			$hc_has_failure = false;
			if ( ! empty( $results['categories'] ) && is_array( $results['categories'] ) ) {
				foreach ( $results['categories'] as $hc_cat ) {
					if ( ! is_array( $hc_cat ) || empty( $hc_cat['sections'] ) ) {
						continue;
					}
					foreach ( $hc_cat['sections'] as $hc_section ) {
						if ( ! is_array( $hc_section ) || empty( $hc_section['items'] ) ) {
							continue;
						}
						foreach ( $hc_section['items'] as $hc_item ) {
							if ( is_array( $hc_item ) && isset( $hc_item['status'] ) && $hc_item['status'] === 'fail' ) {
								$hc_has_failure = true;
								break 3;
							}
						}
					}
				}
			}

			if ( $hc_has_failure ) {
				// Collecter les items fail pour le corps de l'email
				$hc_fail_lines = array();
				foreach ( $results['categories'] as $hc_cat ) {
					if ( ! is_array( $hc_cat ) || empty( $hc_cat['sections'] ) ) continue;
					$hc_cat_label = isset( $hc_cat['label'] ) ? $hc_cat['label'] : '';
					foreach ( $hc_cat['sections'] as $hc_section ) {
						if ( ! is_array( $hc_section ) || empty( $hc_section['items'] ) ) continue;
						foreach ( $hc_section['items'] as $hc_item ) {
							if ( is_array( $hc_item ) && isset( $hc_item['status'] ) && $hc_item['status'] === 'fail' ) {
								$hc_fail_lines[] = '❌ [' . $hc_cat_label . '] ' . ( $hc_item['label'] ?? '' ) . ' — ' . ( $hc_item['detail'] ?? $hc_item['value'] ?? '' );
							}
						}
					}
				}
				$hc_recipients = array( 'contact@avision.org', 'toniosbh@gmail.com' );
				$hc_subject    = '[AVI Health Check] ❌ ' . count( $hc_fail_lines ) . ' anomalie(s) — ' . current_time( 'mysql' );
				$hc_body       = "Le Health Check a détecté " . count( $hc_fail_lines ) . " anomalie(s) sur avision.org.\n\n"
							   . 'Date : ' . current_time( 'mysql' ) . "\n\n"
							   . "Anomalies détectées :\n"
							   . implode( "\n", $hc_fail_lines ) . "\n\n"
							   . "Vérifiez l'onglet Health Check dans AVI Control Center :\n"
							   . admin_url( 'admin.php?page=avi-avi-control-center&tab=health' );
				foreach ( $hc_recipients as $hc_to ) {
					wp_mail( $hc_to, $hc_subject, $hc_body );
				}
			}
			// --- Fin alerte Health Check ---

			return $results;
		}

		/**
		 * Lance checks rapides.
		 *
		 * @return array
		 */
		public static function run_fast_check() {
			$results = self::base_results( 'fast' );

			try {
				$results['categories']['backend_infra'] = self::checks_backend_infra();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['backend_infra'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Backend Infra',
						array( self::item_fail( 'Exception', $e->getMessage(), '' ) )
					)
				);
				$results['categories']['backend_infra'] = $cat;
			}

			try {
				$results['categories']['db_keys'] = self::checks_db_keys();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['db_keys'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur DB/Clés',
						array( self::item_fail( 'Exception', $e->getMessage(), '' ) )
					)
				);
				$results['categories']['db_keys'] = $cat;
			}

			try {
				$results['categories']['cron_capacity'] = self::checks_cron_capacity();
			} catch ( \Throwable $e ) {
				$cat = $results['categories']['cron_capacity'];
				self::add_section(
					$cat,
					self::make_section(
						'Erreur Capacité Crons',
						array( self::item_fail( 'Exception', $e->getMessage(), '' ) )
					)
				);
				$results['categories']['cron_capacity'] = $cat;
			}

			$results = self::finalize_scores( $results );

			if ( function_exists( 'update_option' ) ) {
				update_option( 'avision_hc_fast_last', $results, false );
			}

			return $results;
		}

		/**
		 * AJAX (admin).
		 *
		 * @return void
		 */
		public static function ajax_run_full_check() {
			check_ajax_referer( 'avision_cc_hc', 'nonce' );
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( 'Unauthorized' );
			}
			$results = self::run_full_check();
			wp_send_json_success( $results );
		}

		/**
		 * AJAX (admin).
		 *
		 * @return void
		 */
		public static function ajax_run_fast_check() {
			check_ajax_referer( 'avision_cc_hc', 'nonce' );
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( 'Unauthorized' );
			}
			$results = self::run_fast_check();
			wp_send_json_success( $results );
		}

		/**
		 * Lance smoke_test.sh et stocke le résultat en WP options.
		 *
		 * @return array<string, mixed>
		 */
		public static function run_smoke_test() {
			$script = '/root/scripts/smoke_test.sh';
			if ( ! file_exists( $script ) ) {
				return array( 'error' => 'Script absent' );
			}

			$raw   = shell_exec( 'sudo bash ' . escapeshellarg( $script ) . ' 2>&1' );
			$lines = $raw ? explode( "\n", trim( (string) $raw ) ) : array();
			$checks = array();
			$pass   = 0;
			$fail   = 0;

			foreach ( $lines as $line ) {
				$line = trim( $line );
				if ( $line === '' ) {
					continue;
				}
				if ( strpos( $line, '✅' ) !== false ) {
					$checks[] = array(
						'ok'   => true,
						'text' => $line,
					);
					$pass++;
				} elseif ( strpos( $line, '❌' ) !== false ) {
					$checks[] = array(
						'ok'   => false,
						'text' => $line,
					);
					$fail++;
				}
			}

			$result = array(
				'timestamp' => current_time( 'mysql' ),
				'pass'      => $pass,
				'fail'      => $fail,
				'checks'    => $checks,
				'trigger'   => 'auto',
				'raw'       => substr( (string) $raw, 0, 5000 ),
			);

			update_option( 'avi_smoke_last_result', $result, false );

			$history = get_option( 'avi_smoke_history', array() );
			if ( ! is_array( $history ) ) {
				$history = array();
			}
			$summary = array(
				'timestamp' => $result['timestamp'],
				'pass'      => $pass,
				'fail'      => $fail,
				'trigger'   => $result['trigger'],
			);
			array_unshift( $history, $summary );
			$history = array_slice( $history, 0, 10 );
			update_option( 'avi_smoke_history', $history, false );

			if ( $fail > 0 ) {
				$email      = get_option( 'avi_code_guard_alert_email', 'contact@avision.org' );
				$recipients = array_unique( array( $email, 'toniosbh@gmail.com' ) );
				$subject    = '[AVI Smoke Test] ❌ ' . $fail . ' check(s) en échec — ' . current_time( 'mysql' );
				$body       = "Le Smoke Test automatique a détecté " . $fail . " erreur(s).\n\n"
					. implode(
						"\n",
						array_map(
							function ( $c ) {
								return isset( $c['text'] ) ? (string) $c['text'] : '';
							},
							$checks
						)
					) . "\n\n"
					. 'Code Guard : ' . admin_url( 'admin.php?page=avi-avi-control-center&tab=code_guard' );
				foreach ( $recipients as $to ) {
					wp_mail( $to, $subject, $body );
				}
			}

			return $result;
		}

		/**
		 * Handler AJAX manuel Smoke Test.
		 *
		 * @return void
		 */
		public static function ajax_run_smoke_test() {
			check_ajax_referer( 'avision_cc_nonce', 'nonce' );
			if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( 'Unauthorized' );
			}
			$result            = self::run_smoke_test();
			$result['trigger'] = 'manual';
			update_option( 'avi_smoke_last_result', $result, false );
			wp_send_json_success( $result );
		}

		/**
		 * @param string $type full|fast
		 * @return array|null
		 */
		public static function get_last_result( $type = 'full' ) {
			$type = is_string( $type ) ? $type : 'full';
			return get_option( 'avision_hc_' . $type . '_last', null );
		}

		/**
		 * Crons.
		 *
		 * @return void
		 */
		public static function init_crons() {
			add_action( 'avision_hc_fast_cron', array( __CLASS__, 'run_fast_check' ) );
			add_action( 'avision_hc_full_cron', array( __CLASS__, 'run_full_check' ) );
			// Smoke Test automatique
			add_action( 'avision_smoke_test_cron', array( __CLASS__, 'run_smoke_test' ) );
			add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) );
			self::schedule_events();
		}

		/**
		 * Exposé pour le bypass dev (autologin).
		 *
		 * @return int
		 */
		public static function get_or_create_hc_user_public() {
			return self::get_or_create_hc_user();
		}

		/**
		 * Test WP-CLI / eval : charge le HTML du template premium (lecture fichier).
		 *
		 * @return string
		 */
		public static function fetch_template_html_test() {
			return self::fetch_template_html( 'premium' );
		}

		// ----------------------------
		// Checks (privés)
		// ----------------------------

		/**
		 * @return array
		 */
		private static function checks_premium_dom() {
			$cat = self::base_category( 'Premium Dashboard', 'layout-dashboard' );
			$html = self::fetch_template_html( 'premium' );

			// Section 1 — Structure DOM (8).
			$items = array();
			$needles_a = array(
				'id="avi-premium-score-card"'  => 'Score card (#avi-premium-score-card)',
				'avi-kpi-tile-rank'            => 'KPI Top 3 (.avi-kpi-tile-rank)',
				'avi-kpi-tile-leaders'         => 'KPI Concurrents (.avi-kpi-tile-leaders)',
				'id="avi-kpi-reputation"'      => 'KPI Réputation (#avi-kpi-reputation)',
			);
			foreach ( $needles_a as $needle => $label ) {
				try {
					$ok = ( $html !== '' && strpos( $html, (string) $needle ) !== false );
					$items[] = $ok ? self::item_ok( $label, 'présent', '' ) : self::item_fail( $label, 'absent du HTML', '' );
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
				}
			}
			try {
				$found_estimation = (
					strpos( $html, 'avi-kpi-tile-estimation' ) !== false
					|| strpos( $html, 'kpi-estimation' ) !== false
					|| strpos( $html, 'avi-kpi-tile-impact' ) !== false
				);
				$items[] = $found_estimation
					? self::item_ok( 'KPI Valeur estimée', 'présent', '' )
					: self::item_fail( 'KPI Valeur estimée', 'absent du HTML', '' );
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'KPI Valeur estimée', 'Exception: ' . $e->getMessage(), '' );
			}
			$needles_b = array(
				'id="avi-kpi-guardian-wrap"' => 'KPI Guardian (#avi-kpi-guardian-wrap)',
				'id="avi-radar-section"'     => 'Growth Radar (#avi-radar-section)',
				'id="avi-mastery-bar-wrap"'  => 'Progression bar (#avi-mastery-bar-wrap)',
			);
			foreach ( $needles_b as $needle => $label ) {
				try {
					$ok = ( $html !== '' && strpos( $html, (string) $needle ) !== false );
					$items[] = $ok ? self::item_ok( $label, 'présent', '' ) : self::item_fail( $label, 'absent du HTML', '' );
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
				}
			}
			self::add_section( $cat, self::make_section( 'Structure DOM', $items ) );

			// Section 2 — Modals (6).
			$items = array();
			$modals = array(
				array( 'id' => 'onboardingModalOverlay',       'trigger' => 'id="btnGoogleConnectOnboarding"', 'label' => 'Onboarding modal' ),
				array( 'id' => 'premiumGoogleConsentModal',    'trigger' => 'id="premiumGoogleAuthModalConnect"', 'label' => 'Google consent modal' ),
				array( 'id' => 'estimationHowCalculatedModal', 'trigger' => 'estimationHowCalculatedModal', 'label' => 'Estimation modal' ),
				array( 'id' => 'aviPedagoModal',               'trigger' => 'aria-controls="aviPedagoModal"', 'label' => 'Pedago modal' ),
				array( 'id' => 'aviVerticalLockModal',         'trigger' => 'aviVerticalLockModalOk', 'label' => 'Vertical lock modal' ),
				array( 'id' => 'radarConfirmStrategyModal',    'trigger' => 'radarConfirmStrategyModal', 'label' => 'Radar confirm modal' ),
			);
			foreach ( $modals as $m ) {
				try {
					$id = (string) $m['id'];
					$trigger = (string) $m['trigger'];
					$label = (string) $m['label'];
					$has_modal = ( $html !== '' && strpos( $html, 'id="' . $id . '"' ) !== false );
					if ( ! $has_modal ) {
						$items[] = self::item_fail( $label . ' (#' . $id . ')', 'modal absente du HTML', '' );
						continue;
					}
					$has_trigger = ( $html !== '' && strpos( $html, $trigger ) !== false );
					if ( $has_trigger ) {
						$items[] = self::item_ok( $label . ' (#' . $id . ')', 'modal + trigger', '' );
					} else {
						$items[] = self::item_warn( $label . ' (#' . $id . ')', 'modal présente mais trigger introuvable', '' );
					}
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( 'Modal check', 'Exception: ' . $e->getMessage(), '' );
				}
			}
			self::add_section( $cat, self::make_section( 'Modals (existence + câblage)', $items ) );

			// Section 3 — Modals + boutons Stripe (6).
			$items_stripe = array();

			// 1. salesBtn B2C — bouton checkout principal (demo dashboard)
			$demo_html = '';
			foreach ( array(
				get_template_directory() . '/page-demo-dashboard.php',
				WP_PLUGIN_DIR . '/avision-dashboard/templates/pages/demo-dashboard.php',
			) as $p ) {
				if ( file_exists( $p ) ) {
					$demo_html = (string) file_get_contents( $p );
					break;
				}
			}
			$sales_pos = $demo_html !== '' ? strpos( $demo_html, 'id="salesBtn"' ) : false;
			$sales_ok  = $sales_pos !== false;
			$sales_disabled = $sales_ok && strpos( substr( $demo_html, max( 0, $sales_pos - 50 ), 200 ), 'disabled' ) !== false;
			$items_stripe[] = ! $sales_ok
				? self::item_fail( 'salesBtn B2C (Stripe)', 'MANQUANT dans demo-dashboard — règle #80' )
				: ( $sales_disabled
					? self::item_warn( 'salesBtn B2C (Stripe)', 'présent mais disabled — règle #80' )
					: self::item_ok( 'salesBtn B2C (Stripe)', 'modal + bouton OK' ) );

			// 2. Nonce Stripe premium
			$nonce_ok = strpos( $html, 'avision_premium_nonce' ) !== false;
			$items_stripe[] = $nonce_ok
				? self::item_ok( 'Nonce Stripe premium', 'modal + nonce OK' )
				: self::item_fail( 'Nonce Stripe premium', 'avision_premium_nonce MANQUANT' );

			// 3. Google consent modal premium
			$items_stripe[] = self::agency_modal_button_check(
				$html, 'premiumGoogleConsentModal', 'premiumGoogleAuthModalConnect', 'Google consent modal (Stripe OAuth)'
			);

			// 4. Demo modal trigger
			$trigger_ok = strpos( $html, 'id="aviDemoModalTrigger"' ) !== false
				|| strpos( $html, 'aviDemoModalTrigger' ) !== false;
			$items_stripe[] = $trigger_ok
				? self::item_ok( 'Demo modal trigger', 'modal + trigger' )
				: self::item_warn( 'Demo modal trigger', 'absent (masqué si non admin)' );

			// 5. AppSumo upgrade modal (agency dashboard)
			$agency_html = '';
			foreach ( array(
				get_template_directory() . '/page-agency-dashboard.php',
				WP_PLUGIN_DIR . '/avision-dashboard/templates/pages/agency-dashboard.php',
			) as $p ) {
				if ( file_exists( $p ) ) {
					$agency_html = (string) file_get_contents( $p );
					break;
				}
			}
			$items_stripe[] = self::modal_exists_item( $agency_html, 'appsumoUpgradeModal', 'AppSumo upgrade modal' );

			// 6. Webhook secret Stripe
			$wh_ok = defined( 'AVISION_STRIPE_WEBHOOK_SECRET' ) && strlen( AVISION_STRIPE_WEBHOOK_SECRET ) > 10;
			$items_stripe[] = $wh_ok
				? self::item_ok( 'Webhook secret Stripe', 'modal + bouton OK' )
				: self::item_fail( 'Webhook secret Stripe', 'AVISION_STRIPE_WEBHOOK_SECRET manquant' );

			self::add_section( $cat, self::make_section( 'Modals + boutons Stripe', $items_stripe ) );

			// Section 4 — Interactivité (5).
			$items = array();
			$needles = array(
				'aria-controls="radarContentWrap"' => 'Radar toggle câblé',
				'data-lang="en"'                   => 'Flag langue EN (data-lang)',
				'data-lang="fr"'                   => 'Flag langue FR (data-lang)',
				'data-lang="es"'                   => 'Flag langue ES (data-lang)',
				'id="notifBell"'                   => 'Notification bell (#notifBell)',
			);
			foreach ( $needles as $needle => $label ) {
				try {
					$ok = ( $html !== '' && strpos( $html, (string) $needle ) !== false );
					$items[] = $ok ? self::item_ok( $label, 'présent', '' ) : self::item_fail( $label, 'absent du HTML', '' );
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
				}
			}
			self::add_section( $cat, self::make_section( 'Interactivité JS + câblage', $items ) );

			// Section 5 — AJAX simulés (2).
			$items = array();
			// Item 1 — AVI Chat.
			try {
				$t0 = microtime( true );
				$resp = wp_remote_post(
					'http://localhost:8001/api/v1/chat',
					array(
						'timeout' => 25,
						'headers' => array(
							'Content-Type'  => 'application/json',
							'X-WP-User-ID'  => '1',
							'X-Business-ID' => '43',
						),
						'body'    => wp_json_encode(
							array(
								'user_input'       => 'Hello test health check',
								'user_type'        => 'b2c',
								'language'         => 'en',
								'dashboard_type'   => 'b2c',
								'business_name'    => 'HC Test',
								'city'             => 'London',
								'current_score'    => 50,
								'active_level_id'  => 2,
							),
							JSON_UNESCAPED_UNICODE
						),
					)
				);
				$elapsed = round( microtime( true ) - $t0, 2 );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'AVI Chat', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = (string) wp_remote_retrieve_body( $resp );
					$data = json_decode( $body, true );
					$reply = ( is_array( $data ) && isset( $data['reply'] ) && is_string( $data['reply'] ) ) ? $data['reply'] : '';
					if ( $code === 200 && strlen( $reply ) > 5 ) {
						$items[] = self::item_ok( 'AVI Chat', 'reply OK', 'reply reçu (' . $elapsed . 's)' );
					} else {
						$items[] = self::item_fail( 'AVI Chat', 'HTTP ' . $code, 'reply len=' . strlen( $reply ) );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVI Chat', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 2 — avision_get_agency_alerts.
			try {
				$resp = wp_remote_post(
					admin_url( 'admin-ajax.php' ),
					array(
						'timeout' => 8,
						'body'    => array(
							'action' => 'avision_get_agency_alerts',
							'nonce'  => wp_create_nonce( 'avision_agency_nonce' ),
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'AJAX avision_get_agency_alerts', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 200 || $code === 400 ) {
						$items[] = self::item_ok(
							'AJAX avision_get_agency_alerts',
							$code === 400 ? 'Endpoint actif · rejet contexte admin = attendu' : 'HTTP 200',
							'HTTP ' . $code
						);
					} elseif ( $code === 403 ) {
						$items[] = self::item_warn( 'AJAX avision_get_agency_alerts', 'HTTP 403', 'HTTP 403' );
					} elseif ( $code >= 500 ) {
						$items[] = self::item_fail( 'AJAX avision_get_agency_alerts', 'HTTP ' . $code, 'HTTP ' . $code );
					} else {
						$items[] = self::item_warn( 'AJAX avision_get_agency_alerts', 'HTTP ' . $code, 'HTTP ' . $code );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AJAX avision_get_agency_alerts', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'AJAX simulés', $items ) );

			return $cat;
		}

		/**
		 * @return array
		 */
		private static function checks_agency_dom() {
			$cat = self::base_category( 'Agency Dashboard', 'building-skyscraper' );
			$html = self::fetch_template_html( 'agency' );

			// Section 1 — Structure DOM (5).
			$items = array();
			try {
				$has_trigger = ( $html !== '' && strpos( $html, 'id="agencyFleetTierBadgeTrigger"' ) !== false );
				$has_aria = ( $html !== '' && strpos( $html, 'aria-controls="' ) !== false );
				$status = ( $has_trigger && $has_aria ) ? 'ok' : ( $has_trigger ? 'warn' : 'fail' );
				if ( $status === 'ok' ) {
					$items[] = self::item_ok( 'Fleet tier badge trigger', 'id + aria-controls', '' );
				} elseif ( $status === 'warn' ) {
					$items[] = self::item_warn( 'Fleet tier badge trigger', 'id présent mais aria-controls introuvable', '' );
				} else {
					$items[] = self::item_fail( 'Fleet tier badge trigger', 'absent du HTML', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Fleet tier badge trigger', 'Exception: ' . $e->getMessage(), '' );
			}

			$needles = array(
				'id="avi-banners-wrap"' => 'Banners wrap (#avi-banners-wrap)',
				'id="aviGuAgency"'      => 'Google Updates section (#aviGuAgency)',
				'id="userProfileTrigger"' => 'User profile trigger (#userProfileTrigger)',
			);
			foreach ( $needles as $needle => $label ) {
				try {
					$ok = ( $html !== '' && strpos( $html, (string) $needle ) !== false );
					$items[] = $ok ? self::item_ok( $label, 'présent', '' ) : self::item_fail( $label, 'absent du HTML', '' );
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
				}
			}

			// data-report-id count (depuis template Premium Report Builder).
			try {
				$premium_html = self::fetch_template_html( 'premium' );
				$report_count = substr_count( $premium_html, 'data-report-id' );
				$val            = $report_count . ' éléments';
				if ( $report_count >= 20 ) {
					$items[] = self::item_ok( 'Report Builder (data-report-id ≥ 20)', 'Attributs dans page-premium-dashboard.php', $val );
				} elseif ( $report_count >= 10 ) {
					$items[] = self::item_warn( 'Report Builder (data-report-id ≥ 20)', 'Attributs dans page-premium-dashboard.php', $val );
				} else {
					$items[] = self::item_fail( 'Report Builder (data-report-id ≥ 20)', 'Attributs dans page-premium-dashboard.php', $val );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Report Builder (data-report-id ≥ 20)', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'Structure DOM', $items ) );

			// Section 2 — Drawers (3).
			$items = array();
			$drawers = array(
				array( 'id' => 'agencyMarketplaceDrawer', 'trigger' => 'aria-controls="agencyMarketplaceDrawer"', 'label' => 'Marketplace drawer' ),
				array( 'id' => 'agencyBulkDrawer',        'trigger' => 'aria-controls="agencyBulkDrawer"', 'label' => 'Bulk drawer' ),
				array( 'id' => 'agencyStrategyNotePanel', 'trigger' => 'agencyStrategyNotePanel', 'label' => 'Strategy note panel' ),
			);
			foreach ( $drawers as $d ) {
				try {
					$id = (string) $d['id'];
					$trigger = (string) $d['trigger'];
					$label = (string) $d['label'];
					$has = ( $html !== '' && strpos( $html, 'id="' . $id . '"' ) !== false );
					if ( ! $has ) {
						$items[] = self::item_fail( $label . ' (#' . $id . ')', 'drawer absent', '' );
						continue;
					}
					$has_trigger = ( $html !== '' && strpos( $html, $trigger ) !== false );
					if ( $has_trigger ) {
						$items[] = self::item_ok( $label . ' (#' . $id . ')', 'drawer + trigger', '' );
					} else {
						$items[] = self::item_warn( $label . ' (#' . $id . ')', 'drawer présent mais trigger introuvable', '' );
					}
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( 'Drawer check', 'Exception: ' . $e->getMessage(), '' );
				}
			}
			self::add_section( $cat, self::make_section( 'Drawers (existence + aria-controls)', $items ) );

			// Section 3 — Modals + boutons Stripe (6).
			$items = array();
			// Guardian modal + Stripe button enabled.
			$items[] = self::agency_modal_button_check( $html, 'agencyGuardianModal', 'btnActivateGuardianAgency', 'Agency Guardian (Stripe)' );
			$items[] = self::agency_modal_button_check( $html, 'agencyTop3Modal', 'btnActivateTop3Agency', 'Agency Top3 (Stripe)' );
			$items[] = self::agency_modal_trigger_check( $html, 'agencyBundleModal', 'openBundleModal', 'Agency Bundle modal' );
			$items[] = self::modal_exists_item( $html, 'agencyImportModal', 'Agency Import modal' );
			$items[] = self::modal_exists_item( $html, 'avi-bulk-post-modal', 'Bulk post modal' );
			$items[] = self::agency_modal_button_check( $html, 'avi-clean-modal', 'btn-google-final-v4', 'Clean modal (Google final)' );
			self::add_section( $cat, self::make_section( 'Modals + boutons Stripe', $items ) );

			// Section 4 — AJAX Agency simulés (3).
			$items = array();
			$ajax_url = admin_url( 'admin-ajax.php' );

			// 1) save_report_visibility.
			$items[] = self::ajax_admin_context_check(
				'AVISION save_report_visibility',
				$ajax_url,
				array(
					'action'     => 'avision_save_report_visibility',
					'nonce'      => wp_create_nonce( 'avision_agency_nonce' ),
					'profile_id' => 0,
					'visibility' => wp_json_encode( array( 'score-card' => true ), JSON_UNESCAPED_UNICODE ),
				),
				'Endpoint actif · nonce admin invalide = attendu'
			);
			// 2) get_report_visibility.
			$items[] = self::ajax_code_check(
				'AVISION get_report_visibility',
				$ajax_url,
				array(
					'action'     => 'avision_get_report_visibility',
					'nonce'      => wp_create_nonce( 'avision_agency_nonce' ),
					'profile_id' => 0,
				),
				false
			);
			// 3) bulk_wholesale_checkout (nonce only).
			try {
				$resp = wp_remote_post(
					$ajax_url,
					array(
						'timeout' => 8,
						'body'    => array(
							'action'      => 'avision_bulk_wholesale_checkout',
							'nonce'       => wp_create_nonce( 'avision_agency_nonce' ),
							'profile_ids' => wp_json_encode( array(), JSON_UNESCAPED_UNICODE ),
							'product_key' => 'ws_guardian_t1',
							'price_id'    => '',
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'AVISION bulk_wholesale_checkout', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 403 ) {
						$items[] = self::item_fail( 'AVISION bulk_wholesale_checkout', 'HTTP 403', 'HTTP 403' );
					} else {
						$items[] = self::item_ok( 'AVISION bulk_wholesale_checkout', 'Nonce valide · session test rejetée proprement', 'HTTP ' . $code );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVISION bulk_wholesale_checkout', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'AJAX Agency simulés', $items ) );
			return $cat;
		}

		/**
		 * @return array
		 */
		private static function checks_backend_infra() {
			$cat = self::base_category( 'Backend — Infra & Process', 'server' );

			// Section 1 — Serveur & Process (4).
			$items = array();
			// Item 1 — service /health.
			try {
				$resp = wp_remote_get( 'http://localhost:8001/health', array( 'timeout' => 8 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'avision-backend.service', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 200 ) {
						$items[] = self::item_ok( 'avision-backend.service', 'actif', 'actif · port 8001 OK' );
					} else {
						$items[] = self::item_fail( 'avision-backend.service', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'avision-backend.service', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 2 — Port 8001 (même /health).
			try {
				$resp = wp_remote_get( 'http://localhost:8001/health', array( 'timeout' => 8 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'Port 8001', 'Port 8001 inaccessible', $resp->get_error_message() );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 200 ) {
						$items[] = self::item_ok( 'Port 8001', 'Port ouvert et écoute', '' );
					} else {
						$items[] = self::item_fail( 'Port 8001', 'Port 8001 inaccessible', 'HTTP ' . $code );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Port 8001', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 3 — Crontab serveur.
			$cron_out       = null;
			$shell_disabled = array_map( 'trim', explode( ',', (string) ini_get( 'disable_functions' ) ) );
			if ( function_exists( 'shell_exec' ) && ! in_array( 'shell_exec', $shell_disabled, true ) ) {
				$cron_out = shell_exec( '/usr/bin/sudo -n /root/scripts/read_crontab_www.sh 2>/dev/null' );
			}
			$cron_guardian_found = is_string( $cron_out ) && strpos( $cron_out, 'cron_guardian' ) !== false;
			// Fallback : dernière exécution enregistrée en DB si shell_exec indisponible.
			if ( ! $cron_guardian_found ) {
				$last_run = get_option( 'avi_last_guardian_cron_run', '' );
				if ( $last_run && ( time() - (int) strtotime( $last_run ) ) < 7200 ) {
					$cron_guardian_found = true;
					$cron_out            = 'fallback: last run ' . $last_run;
				}
			}
			if ( $cron_guardian_found ) {
				$items[] = self::item_ok( 'Crontab serveur', 'cron_guardian actif', trim( (string) $cron_out ) );
			} else {
				$items[] = self::item_warn( 'Crontab serveur', 'cron_guardian non trouvé', '' );
			}

			// Item 4 — DISABLE_WP_CRON.
			try {
				if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON === true ) {
					$items[] = self::item_ok( 'DISABLE_WP_CRON', 'OK', 'true' );
				} else {
					$items[] = self::item_warn( 'DISABLE_WP_CRON', 'WP-CRON interne actif — risque de doublons', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'DISABLE_WP_CRON', 'Exception: ' . $e->getMessage(), '' );
			}
			self::add_section( $cat, self::make_section( 'Serveur & Process', $items ) );

			// Section 2 — Qdrant RAG (3).
			$items = array();
			$key = defined( 'AVISION_ADMIN_API_KEY' ) ? (string) AVISION_ADMIN_API_KEY : '';

			// Item 1 — Qdrant healthz.
			try {
				$resp = wp_remote_get( 'http://localhost:6333/healthz', array( 'timeout' => 8 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'Qdrant connexion', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 200 || $code === 204 ) {
						$items[] = self::item_ok( 'Qdrant connexion', 'OK', 'HTTP ' . $code );
					} else {
						$items[] = self::item_fail( 'Qdrant connexion', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Qdrant connexion', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 2 — points (via /api/v1/knowledge/stats).
			try {
				$resp = wp_remote_get(
					'http://localhost:8001/api/v1/knowledge/stats',
					array(
						'timeout' => 8,
						'headers' => array(
							'X-Admin-Key'   => defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '',
							'Authorization' => 'Bearer ' . ( defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '' ),
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'Qdrant points', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = json_decode( (string) wp_remote_retrieve_body( $resp ), true );
					$count = isset( $body['total_points'] ) ? (int) $body['total_points'] : 0;
					if ( $code !== 200 ) {
						$items[] = self::item_fail( 'Qdrant points', 'HTTP ' . $code, '' );
					} elseif ( $count >= 900 ) {
						$items[] = self::item_ok( 'Qdrant points', 'OK', $count . ' points' );
					} elseif ( $count >= 500 ) {
						$items[] = self::item_warn( 'Qdrant points', 'points insuffisants', $count . ' points' );
					} else {
						$items[] = self::item_fail( 'Qdrant points', 'trop peu de points', $count . ' points' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Qdrant points', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 3 — search test (GET query + top_k).
			try {
				$resp = wp_remote_get(
					'http://localhost:8001/admin/admin/qdrant/search?query=guardian+local+seo&top_k=3',
					array(
						'timeout' => 10,
						'headers' => array(
							'X-Admin-Key' => defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '',
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'Qdrant search test', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body_raw = (string) wp_remote_retrieve_body( $resp );
					if ( $code !== 200 ) {
						$items[] = self::item_fail( 'Qdrant search test', 'HTTP ' . $code, '' );
					} elseif ( trim( $body_raw ) === '' ) {
						$items[] = self::item_warn( 'Qdrant search test', 'réponse vide', 'HTTP 200' );
					} else {
						$data = json_decode( $body_raw, true );
						$has  = self::body_has_results( $data );
						if ( $has === true ) {
							$items[] = self::item_ok( 'Qdrant search test', 'résultats non vides', 'HTTP 200' );
						} elseif ( $has === false ) {
							$items[] = self::item_warn( 'Qdrant search test', 'tableau vide', 'HTTP 200' );
						} else {
							$items[] = self::item_fail( 'Qdrant search test', 'réponse illisible', '' );
						}
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'Qdrant search test', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'Qdrant RAG', $items ) );
			return $cat;
		}

		/**
		 * @return array
		 */
		private static function checks_backend_api() {
			$cat = self::base_category( 'Backend — Routes FastAPI', 'api' );

			$key = defined( 'AVISION_ADMIN_API_KEY' ) ? (string) AVISION_ADMIN_API_KEY : '';
			$headers = array(
				'Content-Type' => 'application/json',
				'X-Admin-Key'  => $key,
			);

			// Section 1 — Routes FastAPI critiques (6).
			$items = array();

			// Item 1 — GET /health.
			try {
				$t0 = microtime( true );
				$resp = wp_remote_get( 'http://localhost:8001/health', array( 'timeout' => 8 ) );
				$ms = (int) round( ( microtime( true ) - $t0 ) * 1000 );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'GET /health', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					if ( $code === 200 ) {
						$items[] = self::item_ok( 'GET /health', 'OK', '200 (' . $ms . 'ms)' );
					} else {
						$items[] = self::item_fail( 'GET /health', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'GET /health', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 2 — GET /api/v1/knowledge/stats (points).
			try {
				$ka = defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '';
				$resp = wp_remote_get(
					'http://localhost:8001/api/v1/knowledge/stats',
					array(
						'timeout' => 8,
						'headers' => array(
							'X-Admin-Key'   => $ka,
							'Authorization' => 'Bearer ' . $ka,
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = json_decode( (string) wp_remote_retrieve_body( $resp ), true );
					$count = isset( $body['total_points'] ) ? (int) $body['total_points'] : 0;
					if ( $code === 200 ) {
						$items[] = self::item_ok( 'GET /api/v1/knowledge/stats', 'OK', 'pts=' . $count );
					} else {
						$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 3 — POST /api/v1/chat.
			try {
				$t0 = microtime( true );
				$resp = wp_remote_post(
					'http://localhost:8001/api/v1/chat',
					array(
						'timeout' => 25,
						'headers' => array(
							'Content-Type'  => 'application/json',
							'X-WP-User-ID'  => '1',
							'X-Business-ID' => '43',
						),
						'body'    => wp_json_encode(
							array(
								'user_input'       => 'Hello test health check',
								'user_type'        => 'b2c',
								'language'         => 'en',
								'dashboard_type'   => 'b2c',
								'business_name'    => 'HC Test',
								'city'             => 'London',
								'current_score'    => 50,
								'active_level_id'  => 2,
							),
							JSON_UNESCAPED_UNICODE
						),
					)
				);
				$elapsed = (int) round( ( microtime( true ) - $t0 ) * 1000 );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'POST /api/v1/chat', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = (string) wp_remote_retrieve_body( $resp );
					$data = json_decode( $body, true );
					$reply = ( is_array( $data ) && isset( $data['reply'] ) && is_string( $data['reply'] ) ) ? $data['reply'] : '';
					if ( $code === 200 && strlen( $reply ) > 5 ) {
						$items[] = self::item_ok( 'POST /api/v1/chat', 'reply OK', $elapsed . 'ms' );
					} else {
						$items[] = self::item_fail( 'POST /api/v1/chat', 'HTTP ' . $code, 'reply len=' . strlen( $reply ) );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'POST /api/v1/chat', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 4 — GET /api/v1/google-updates.
			try {
				$resp = wp_remote_get( 'http://localhost:8001/api/v1/google-updates?lang=en&audience=b2c&limit=5', array( 'timeout' => 8 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'GET /api/v1/google-updates', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = (string) wp_remote_retrieve_body( $resp );
					if ( $code === 200 && trim( $body ) !== '' ) {
						$items[] = self::item_ok( 'GET /api/v1/google-updates', 'OK', 'HTTP 200' );
					} else {
						$items[] = self::item_fail( 'GET /api/v1/google-updates', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'GET /api/v1/google-updates', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 5 — QI check désactivé (manuel uniquement)
			// Remplacé par ping /health pour éviter
			// les appels OpenRouter automatiques
			// Lancer manuellement depuis AVI CC si besoin
			$items[] = self::item_ok(
				'QI Score',
				'Manuel uniquement — lancer depuis AVI CC',
				'désactivé auto'
			);

			// Item 6 — GET /api/v1/knowledge/stats.
			try {
				$ka = defined( 'AVISION_ADMIN_API_KEY' ) ? AVISION_ADMIN_API_KEY : '';
				$resp = wp_remote_get(
					'http://localhost:8001/api/v1/knowledge/stats',
					array(
						'timeout' => 8,
						'headers' => array(
							'X-Admin-Key'   => $ka,
							'Authorization' => 'Bearer ' . $ka,
						),
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$body = (string) wp_remote_retrieve_body( $resp );
					$data = json_decode( $body, true );
					$n = self::extract_points_count( $data );
					if ( $code === 200 ) {
						$items[] = self::item_ok( 'GET /api/v1/knowledge/stats', 'OK', ( $n !== null ? (string) (int) $n : '' ) );
					} else {
						$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', 'HTTP ' . $code, '' );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'GET /api/v1/knowledge/stats', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'Routes FastAPI critiques', $items ) );

			// Section 2 — REST WordPress (3).
			$items = array();
			try {
				$resp = wp_remote_get( rest_url( 'avision/v1/google-updates-proxy' ) . '?audience=b2c&lang=en&limit=1', array( 'timeout' => 15 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'REST avision/v1/google-updates-proxy', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$items[] = ( $code === 200 ) ? self::item_ok( 'REST avision/v1/google-updates-proxy', 'OK', 'HTTP 200' ) : self::item_fail( 'REST avision/v1/google-updates-proxy', 'HTTP ' . $code, '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'REST avision/v1/google-updates-proxy', 'Exception: ' . $e->getMessage(), '' );
			}

			try {
				$resp = wp_remote_get( rest_url( 'avision/v1/banners' ), array( 'timeout' => 8 ) );
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'REST avision/v1/banners', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$items[] = ( $code === 200 ) ? self::item_ok( 'REST avision/v1/banners', 'OK', 'HTTP 200' ) : self::item_fail( 'REST avision/v1/banners', 'HTTP ' . $code, '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'REST avision/v1/banners', 'Exception: ' . $e->getMessage(), '' );
			}

			try {
				$resp = wp_remote_post(
					rest_url( 'avision/v1/stripe-webhook' ),
					array(
						'timeout' => 15,
						'body'    => '',
					)
				);
				if ( is_wp_error( $resp ) ) {
					$items[] = self::item_fail( 'REST avision/v1/stripe-webhook', $resp->get_error_message(), '' );
				} else {
					$code = (int) wp_remote_retrieve_response_code( $resp );
					$detail = 'POST only · rejet sans signature Stripe = attendu';
					if ( $code === 200 || $code === 400 || $code === 401 ) {
						$items[] = self::item_ok( 'REST avision/v1/stripe-webhook', $detail, 'HTTP ' . $code );
					} elseif ( $code === 405 ) {
						$items[] = self::item_warn( 'REST avision/v1/stripe-webhook', $detail, 'HTTP 405' );
					} elseif ( $code === 404 ) {
						$items[] = self::item_fail( 'REST avision/v1/stripe-webhook', 'endpoint non enregistré', '404' );
					} else {
						$items[] = self::item_fail( 'REST avision/v1/stripe-webhook', 'HTTP ' . $code, 'HTTP ' . $code );
					}
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'REST avision/v1/stripe-webhook', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'REST WordPress', $items ) );
			return $cat;
		}

		/**
		 * @return array
		 */
		private static function checks_db_keys() {
			$cat = self::base_category( 'DB, Clés & Cohérence', 'database' );

			// Section 1 — Base de données (7 tables).
			$items = array();
			global $wpdb;
			$tables = array(
				array( 'name' => $wpdb->prefix . 'avi_saas_profiles', 'col' => 'report_visibility', 'need_count' => false ),
				array( 'name' => $wpdb->prefix . 'avi_stripe_sync', 'col' => '', 'need_count' => false ),
				array( 'name' => $wpdb->prefix . 'avi_appsumo_codes', 'col' => '', 'need_count' => false ),
				array( 'name' => $wpdb->prefix . 'avi_saas_agencies', 'col' => '', 'need_count' => true ),
				array( 'name' => $wpdb->prefix . 'avi_google_updates', 'col' => 'audience', 'need_count' => false ),
				array( 'name' => $wpdb->prefix . 'avi_unsubscribe_feedback', 'col' => '', 'need_count' => false ),
				array( 'name' => $wpdb->prefix . 'avi_banners', 'col' => '', 'need_count' => false ),
			);

			foreach ( $tables as $t ) {
				try {
					$table = (string) $t['name'];
					$exists = ( $wpdb && $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table );
					if ( ! $exists ) {
						$items[] = self::item_fail( $table, 'table absente', '' );
						continue;
					}

					$col = (string) $t['col'];
					$need_count = (bool) $t['need_count'];
					if ( $col !== '' ) {
						$c = $wpdb->get_var( $wpdb->prepare( 'SHOW COLUMNS FROM ' . $table . ' LIKE %s', $col ) );
						if ( empty( $c ) ) {
							$items[] = self::item_warn( $table, 'colonne manquante: ' . $col, '' );
							continue;
						}
					}

					if ( $need_count ) {
						$count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $table );
						if ( $count >= 1 ) {
							$items[] = self::item_ok( $table, 'OK', $count . ' ligne(s)' );
						} else {
							$items[] = self::item_warn( $table, 'table vide', '0' );
						}
					} else {
						$items[] = self::item_ok( $table, 'OK', '' );
					}
				} catch ( \Throwable $e ) {
					$items[] = self::item_fail( (string) $t['name'], 'Exception: ' . $e->getMessage(), '' );
				}
			}
			self::add_section( $cat, self::make_section( 'Base de données (7 tables)', $items ) );

			// Section 2 — Clés & Secrets (6).
			$items = array();
			// 1) Stripe secret key.
			try {
				if ( defined( 'AVISION_STRIPE_SECRET_KEY' ) && is_string( AVISION_STRIPE_SECRET_KEY ) && strlen( AVISION_STRIPE_SECRET_KEY ) > 20 ) {
					$k = (string) AVISION_STRIPE_SECRET_KEY;
					if ( substr( $k, 0, 8 ) === 'sk_live_' ) {
						$items[] = self::item_ok( 'AVISION_STRIPE_SECRET_KEY', 'OK', 'sk_live_*** · len=' . strlen( $k ) );
					} elseif ( substr( $k, 0, 8 ) === 'sk_test_' ) {
						$items[] = self::item_warn( 'AVISION_STRIPE_SECRET_KEY', 'mode test détecté (sk_test_)', 'sk_test_*** · len=' . strlen( $k ) );
					} else {
						$items[] = self::item_warn( 'AVISION_STRIPE_SECRET_KEY', 'format inattendu', 'len=' . strlen( $k ) );
					}
				} else {
					$items[] = self::item_fail( 'AVISION_STRIPE_SECRET_KEY', 'absente', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVISION_STRIPE_SECRET_KEY', 'Exception: ' . $e->getMessage(), '' );
			}

			// 2) AVI_STRIPE_LIVE.
			try {
				if ( defined( 'AVI_STRIPE_LIVE' ) && AVI_STRIPE_LIVE === true ) {
					$items[] = self::item_ok( 'AVI_STRIPE_LIVE', 'OK', 'true' );
				} else {
					$items[] = self::item_fail( 'AVI_STRIPE_LIVE', 'AVI_STRIPE_LIVE absent ou false', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVI_STRIPE_LIVE', 'Exception: ' . $e->getMessage(), '' );
			}

			// 3) Brevo.
			try {
				if ( defined( 'AVISION_BREVO_API_KEY' ) && is_string( AVISION_BREVO_API_KEY ) && strlen( AVISION_BREVO_API_KEY ) > 10 ) {
					$items[] = self::item_ok( 'AVISION_BREVO_API_KEY', 'OK', 'len=' . strlen( (string) AVISION_BREVO_API_KEY ) );
				} else {
					$items[] = self::item_fail( 'AVISION_BREVO_API_KEY', 'absente ou trop courte', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVISION_BREVO_API_KEY', 'Exception: ' . $e->getMessage(), '' );
			}

			// 4) OpenRouter.
			try {
				if ( defined( 'OPENROUTER_API_KEY' ) && is_string( OPENROUTER_API_KEY ) && strlen( OPENROUTER_API_KEY ) > 10 ) {
					$k = (string) OPENROUTER_API_KEY;
					if ( substr( $k, 0, 6 ) === 'sk-or-' ) {
						$items[] = self::item_ok( 'OPENROUTER_API_KEY', 'OK', 'sk-or-*** · len=' . strlen( $k ) );
					} else {
						$items[] = self::item_warn( 'OPENROUTER_API_KEY', 'format inattendu', 'len=' . strlen( $k ) );
					}
				} else {
					$items[] = self::item_fail( 'OPENROUTER_API_KEY', 'absente', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'OPENROUTER_API_KEY', 'Exception: ' . $e->getMessage(), '' );
			}

			// 5) sk-or-* absent de proxy-avi.php.
			try {
				$proxy_path = get_template_directory() . '/proxy-avi.php';
				$content = ( file_exists( $proxy_path ) ) ? (string) file_get_contents( $proxy_path ) : '';
				$count = substr_count( $content, 'sk-or-' );
				if ( $count === 0 ) {
					$items[] = self::item_ok( 'sk-or-* absent de proxy-avi.php', 'OK', '0 occurrences hardcodées' );
				} else {
					$items[] = self::item_fail( 'sk-or-* absent de proxy-avi.php', $count . ' occurrence(s) hardcodée(s) !', (string) $count );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'sk-or-* absent de proxy-avi.php', 'Exception: ' . $e->getMessage(), '' );
			}

			// 6) AVISION_VERSION cohérence.
			try {
				$plugin_file = WP_PLUGIN_DIR . '/avision-dashboard/avision-dashboard.php';
				if ( ! function_exists( 'get_plugin_data' ) ) {
					require_once ABSPATH . 'wp-admin/includes/plugin.php';
				}
				$data            = function_exists( 'get_plugin_data' ) ? get_plugin_data( $plugin_file, false, false ) : array();
				$header_version  = trim( (string) ( $data['Version'] ?? 'unknown' ) );
				if ( $header_version === '' ) {
					$header_version = 'unknown';
				}
				$define_defined  = defined( 'AVISION_VERSION' );
				$define_v        = $define_defined ? (string) AVISION_VERSION : '';
				if ( $define_defined && $header_version !== 'unknown' && $header_version === $define_v ) {
					$items[] = self::item_ok( 'AVISION_VERSION cohérence', 'OK', $define_v );
				} elseif ( $define_defined && $header_version !== 'unknown' && $header_version !== $define_v ) {
					$items[] = self::item_warn( 'AVISION_VERSION cohérence', 'Header: ' . $header_version . ' ≠ define: ' . $define_v, '' );
				} else {
					$items[] = self::item_warn( 'AVISION_VERSION cohérence', 'Version non détectable (header/define manquants)', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVISION_VERSION cohérence', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'Clés & Secrets', $items ) );

			// Section 3 — Cohérence plugin (3).
			$items = array();
                        // Item 1 — LiteSpeed cache-exc.
                        $htaccess = file_get_contents( ABSPATH . '.htaccess' );
                        if ( is_string( $htaccess ) && strpos( $htaccess, 'premium-dashboard' ) !== false && strpos( $htaccess, 'no-cache' ) !== false ) {
                            $items[] = self::item_ok( 'LiteSpeed cache-exc', 'OK — pages app exclues du cache', '.htaccess configuré' );
                        } elseif ( is_string( $htaccess ) && strpos( $htaccess, 'Cache-Control' ) !== false ) {
                            $items[] = self::item_ok( 'LiteSpeed cache-exc', 'OK — Cache-Control présent', '.htaccess configuré' );
                        } else {
                            $items[] = self::item_warn( 'LiteSpeed cache-exc', 'vérifier .htaccess manuellement', 'non détecté' );
                        }

			// Item 2 — avi_saas_agencies non vide.
			try {
				$count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM ' . $wpdb->prefix . 'avi_saas_agencies' );
				if ( $count >= 1 ) {
					$items[] = self::item_ok( 'avi_saas_agencies non vide', 'OK', $count . ' agence(s)' );
				} else {
					$items[] = self::item_fail( 'avi_saas_agencies non vide', 'Table vide — INSERT dans avi_saas_agencies requis', '0' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'avi_saas_agencies non vide', 'Exception: ' . $e->getMessage(), '' );
			}

			// Item 3 — AVISION_ADMIN_API_KEY.
			try {
				if ( defined( 'AVISION_ADMIN_API_KEY' ) && is_string( AVISION_ADMIN_API_KEY ) && strlen( AVISION_ADMIN_API_KEY ) > 5 ) {
					$items[] = self::item_ok( 'AVISION_ADMIN_API_KEY', 'OK', 'définie · len=' . strlen( (string) AVISION_ADMIN_API_KEY ) );
				} else {
					$items[] = self::item_fail( 'AVISION_ADMIN_API_KEY', 'absente', '' );
				}
			} catch ( \Throwable $e ) {
				$items[] = self::item_fail( 'AVISION_ADMIN_API_KEY', 'Exception: ' . $e->getMessage(), '' );
			}

			self::add_section( $cat, self::make_section( 'Cohérence plugin', $items ) );
			return $cat;
		}

		/**
		 * Capacité crons / scaling Guardian (agrégats DB).
		 *
		 * @return array
		 */
		private static function checks_cron_capacity() {
			$scan_time_per_profile   = 45;
			$cron_interval_seconds   = 3600;
			$threshold_warn          = 80;
			$limit_single_cron       = (int) floor( $cron_interval_seconds / $scan_time_per_profile );
			$limit_multi_cron        = $limit_single_cron * 4;
			$limit_queue_worker      = $limit_multi_cron * 5;

			global $wpdb;
			$t = $wpdb->prefix;

			$guardian_active   = 0;
			$total_profiles    = 0;
			$active_agencies   = 0;
			$new_profiles_24h  = 0;
			$last_scan_ts      = null;

			try {
				$profiles_table = $t . 'avi_saas_profiles';
				if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $profiles_table ) ) === $profiles_table ) {
					$guardian_active = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$profiles_table} WHERE guardian_active = 1" );
					$total_profiles  = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$profiles_table}" );

					$has_agency_col = (string) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM {$profiles_table} LIKE %s", 'agency_id' ) );
					if ( $has_agency_col !== '' ) {
						$active_agencies = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT agency_id) FROM {$profiles_table} WHERE guardian_active = 1 AND agency_id IS NOT NULL" );
					}

					$has_ga_col = (string) $wpdb->get_var( $wpdb->prepare( "SHOW COLUMNS FROM {$profiles_table} LIKE %s", 'guardian_activated_at' ) );
					if ( $has_ga_col !== '' ) {
						$new_profiles_24h = (int) $wpdb->get_var(
							"SELECT COUNT(*) FROM {$profiles_table} WHERE guardian_active = 1 AND guardian_activated_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)"
						);
					}
				}
			} catch ( \Throwable $e ) {
				$guardian_active  = 0;
				$total_profiles   = 0;
				$active_agencies  = 0;
				$new_profiles_24h = 0;
			}

			try {
				$sec_table = $t . 'avi_security_logs';
				if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $sec_table ) ) === $sec_table ) {
					$last_scan_ts = $wpdb->get_var( "SELECT MAX(created_at) FROM {$sec_table}" );
				}
				if ( ! $last_scan_ts ) {
					$posts_table = $t . 'avi_saas_posts';
					if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $posts_table ) ) === $posts_table ) {
						$last_scan_ts = $wpdb->get_var( "SELECT MAX(created_at) FROM {$posts_table}" );
					}
				}
			} catch ( \Throwable $e ) {
				$last_scan_ts = null;
			}

			$guardian_worker_count = 0;
			if ( function_exists( 'shell_exec' ) ) {
				$disabled = array_map( 'trim', explode( ',', (string) ini_get( 'disable_functions' ) ) );
				if ( ! in_array( 'shell_exec', $disabled, true ) ) {
					$timer_raw = @shell_exec( 'systemctl list-timers avision-guardian-worker-*.timer --no-pager 2>/dev/null | grep -c avision-guardian-worker' );
					if ( is_string( $timer_raw ) ) {
						$guardian_worker_count = max( 0, (int) trim( $timer_raw ) );
					}
				}
			}

			if ( $guardian_worker_count >= 5 ) {
				$arch_level          = 'Multi-Cron (5 workers × 12 min)';
				$capacity_per_hour   = 1000;
				$next_level          = '2ème serveur CCX23 requis à 1000 profils';
				$capacity_tier_label = '5 workers';
			} elseif ( $guardian_worker_count >= 3 ) {
				$arch_level          = 'Multi-Cron (3 workers × 20 min)';
				$capacity_per_hour   = 240;
				$next_level          = '5 workers requis à 240 profils';
				$capacity_tier_label = '3 workers';
			} else {
				$arch_level          = 'Single Cron (1 job/heure)';
				$capacity_per_hour   = 80;
				$next_level          = 'Multi-Cron (3 workers) requis à 80 profils';
				$capacity_tier_label = 'single cron';
			}

			$utilization_pct   = $capacity_per_hour > 0
				? (int) round( ( $guardian_active / $capacity_per_hour ) * 100 )
				: 0;
			$profiles_to_next = max( 0, $capacity_per_hour - $guardian_active );

			if ( $guardian_active > $capacity_per_hour ) {
				$arch_status = 'fail';
			} elseif ( $utilization_pct >= $threshold_warn ) {
				$arch_status = 'warn';
			} else {
				$arch_status = 'ok';
			}

			$estimated_cycle_minutes = $capacity_per_hour > 0
				? (int) ceil( ( $guardian_active * $scan_time_per_profile ) / 60 )
				: 0;

			$marge_status = ( $profiles_to_next <= 0 )
				? 'fail'
				: ( ( $profiles_to_next <= 10 ) ? 'warn' : 'ok' );

			$cat = self::base_category( 'Capacité Crons & Scaling', 'timeline' );

			$sec1 = array();
			$sec1[] = self::item_custom(
				'Architecture actuelle',
				$arch_status,
				$arch_level,
				$utilization_pct . '% utilisé'
			);
			$sec1[] = self::item_custom(
				'Profils Guardian actifs',
				$arch_status,
				$guardian_active . ' / ' . $capacity_per_hour . ' max (' . $capacity_tier_label . ')',
				$guardian_active . ' profils'
			);
			$sec1[] = self::item_custom(
				'Marge avant scaling',
				$marge_status,
				'Prochain palier : ' . $next_level,
				$profiles_to_next . ' profils libres'
			);
			$sec1[] = self::item_custom(
				'Durée cycle complet estimée',
				( $estimated_cycle_minutes > 55 ) ? 'warn' : 'ok',
				$guardian_active . ' profils × ' . $scan_time_per_profile . 's/profil',
				$estimated_cycle_minutes . ' min/heure'
			);

			$scan_detail = 'Aucun log trouvé';
			$scan_val    = '—';
			$scan_status = 'warn';
			if ( $last_scan_ts ) {
				$ts = strtotime( (string) $last_scan_ts );
				if ( $ts ) {
					$scan_val    = date_i18n( 'd/m H:i', $ts );
					$scan_detail = human_time_diff( $ts, current_time( 'timestamp' ) );
					$age = time() - $ts;
					$scan_status = ( $age >= 0 && $age < 7200 ) ? 'ok' : 'warn';
				}
			}
			$sec1[] = self::item_custom(
				'Dernier scan enregistré',
				$scan_status,
				$scan_detail,
				$scan_val
			);
			self::add_section( $cat, self::make_section( 'Capacité actuelle', $sec1 ) );

			$sec2 = array(
				self::item_ok( 'Profils totaux en base', '', $total_profiles . ' profils' ),
				self::item_ok(
					'Guardian actif',
					round( ( $guardian_active / max( 1, $total_profiles ) ) * 100 ) . '% du parc',
					$guardian_active . ' profils'
				),
				self::item_ok( 'Agences avec Guardian actif', '', $active_agencies . ' agence(s)' ),
				self::item_ok( 'Nouveaux Guardian (24h)', '', $new_profiles_24h . ' profil(s)' ),
			);
			self::add_section( $cat, self::make_section( 'Répartition des profils', $sec2 ) );

			$p1_ok = ( $guardian_active <= $limit_single_cron );
			$sec3  = array(
				self::item_custom(
					'Palier 1 — Single Cron',
					$p1_ok ? 'ok' : 'fail',
					'1 job/heure · max ' . $limit_single_cron . ' profils Guardian',
					$p1_ok ? 'Actif ✓' : 'Dépassé'
				),
				self::item_custom(
					'Palier 2 — Multi-Cron (4×15min)',
					( $guardian_active > $limit_single_cron && $guardian_active <= $limit_multi_cron )
						? 'warn'
						: ( ( $guardian_active <= $limit_single_cron ) ? 'ok' : 'fail' ),
					'4 crons décalés · max ' . $limit_multi_cron . ' profils',
					( $guardian_active > $limit_single_cron )
						? 'Requis maintenant'
						: ( 'Déclencher à ' . $limit_single_cron . ' profils' )
				),
				self::item_custom(
					'Palier 3 — Queue Workers',
					( $guardian_active > $limit_multi_cron ) ? 'fail' : 'ok',
					'Workers parallèles + DB queue · max ' . $limit_queue_worker . ' profils',
					'Déclencher à ' . $limit_multi_cron . ' profils'
				),
			);
			self::add_section( $cat, self::make_section( 'Roadmap scaling', $sec3 ) );

			$cat['hc_gauge'] = array(
				'utilization_pct'     => (int) $utilization_pct,
				'guardian_active'    => (int) $guardian_active,
				'capacity_per_hour'  => (int) $capacity_per_hour,
			);

			return $cat;
		}

		/**
		 * Item avec statut arbitraire (ok|warn|fail).
		 *
		 * @param string $label
		 * @param string $status
		 * @param string $detail
		 * @param string $val
		 * @return array
		 */
		private static function item_custom( $label, $status, $detail = '', $val = '' ) {
			$status = in_array( $status, array( 'ok', 'warn', 'fail' ), true ) ? $status : 'warn';
			return array(
				'label'  => (string) $label,
				'status' => $status,
				'detail' => (string) $detail,
				'val'    => (string) $val,
			);
		}

		// ----------------------------
		// Utilitaires (privés)
		// ----------------------------

		/**
		 * @param string $type
		 * @return array
		 */
		private static function base_results( $type ) {
			return array(
				'timestamp'  => time(),
				'type'       => $type === 'fast' ? 'fast' : 'full',
				'score'      => 0,
				'max_score'  => 0,
				'categories' => array(
					'premium_dom'     => array( 'label' => 'Premium Dashboard', 'icon' => 'layout-dashboard', 'score' => 0, 'max' => 0, 'sections' => array() ),
					'agency_dom'      => array( 'label' => 'Agency Dashboard', 'icon' => 'building-skyscraper', 'score' => 0, 'max' => 0, 'sections' => array() ),
					'backend_infra'   => array( 'label' => 'Backend — Infra & Process', 'icon' => 'server', 'score' => 0, 'max' => 0, 'sections' => array() ),
					'backend_api'     => array( 'label' => 'Backend — Routes FastAPI', 'icon' => 'api', 'score' => 0, 'max' => 0, 'sections' => array() ),
					'db_keys'         => array( 'label' => 'DB, Clés & Cohérence', 'icon' => 'database', 'score' => 0, 'max' => 0, 'sections' => array() ),
					'cron_capacity'   => array( 'label' => 'Capacité Crons & Scaling', 'icon' => 'timeline', 'score' => 0, 'max' => 0, 'sections' => array() ),
				),
			);
		}

		/**
		 * @param array $results
		 * @return array
		 */
		private static function finalize_scores( $results ) {
			$score = 0;
			$max = 0;
			if ( isset( $results['categories'] ) && is_array( $results['categories'] ) ) {
				foreach ( $results['categories'] as $c ) {
					if ( is_array( $c ) && isset( $c['score'] ) && isset( $c['max'] ) ) {
						$score += (int) $c['score'];
						$max += (int) $c['max'];
					}
				}
			}
			$results['score'] = $score;
			$results['max_score'] = $max;
			return $results;
		}

		/**
		 * @param string $label
		 * @param string $icon
		 * @return array
		 */
		private static function base_category( $label, $icon ) {
			return array(
				'label'    => (string) $label,
				'icon'     => (string) $icon,
				'score'    => 0,
				'max'      => 0,
				'sections' => array(),
			);
		}

		/**
		 * Lit le fichier PHP du dashboard sans exécution HTTP (Premium / Agency).
		 *
		 * @param string $template_slug premium|agency
		 * @return string
		 */
		private static function fetch_template_html( string $template_slug ): string {
			if ( $template_slug === 'premium' ) {
				$paths = array(
					get_template_directory() . '/page-premium-dashboard.php',
					WP_PLUGIN_DIR . '/avision-dashboard/templates/pages/premium-dashboard.php',
				);
			} elseif ( $template_slug === 'agency' ) {
				$paths = array(
					get_template_directory() . '/page-agency-dashboard.php',
					WP_PLUGIN_DIR . '/avision-dashboard/templates/pages/agency-dashboard.php',
				);
			} else {
				return '';
			}
			foreach ( $paths as $path ) {
				if ( file_exists( $path ) ) {
					return (string) file_get_contents( $path );
				}
			}
			return '';
		}

		/**
		 * Requête HTTP distante (marketing, REST, localhost backend).
		 *
		 * @param string $url
		 * @return string
		 */
		private static function fetch_url( $url ) {
			$resp = wp_remote_get(
				$url,
				array(
					'timeout'     => 12,
					'sslverify'   => false,
					'redirection' => 3,
				)
			);
			if ( is_wp_error( $resp ) ) {
				return '';
			}
			return (string) wp_remote_retrieve_body( $resp );
		}

		/**
		 * @return int
		 */
		private static function get_or_create_hc_user() {
			$user = get_user_by( 'login', 'avi-hc-internal' );
			if ( $user ) {
				return (int) $user->ID;
			}
			$uid = wp_create_user( 'avi-hc-internal', substr( md5( AUTH_KEY . 'hc_user' ), 0, 16 ), 'hc@avision.internal' );
			if ( is_wp_error( $uid ) ) {
				return 1;
			}
			wp_update_user( array( 'ID' => $uid, 'display_name' => 'AVI Health Check', 'role' => 'subscriber' ) );
			return (int) $uid;
		}

		/**
		 * @param float $t0
		 * @return string
		 */
		private static function ms_since( $t0 ) {
			return (string) round( ( microtime( true ) - (float) $t0 ) * 1000 ) . 'ms';
		}

		/**
		 * @param string $label
		 * @param string $detail
		 * @param string $val
		 * @return array
		 */
		private static function item_ok( $label, $detail = '', $val = '' ) {
			return array( 'label' => (string) $label, 'status' => 'ok', 'detail' => (string) $detail, 'val' => (string) $val );
		}

		/**
		 * @param string $label
		 * @param string $detail
		 * @param string $val
		 * @return array
		 */
		private static function item_warn( $label, $detail = '', $val = '' ) {
			return array( 'label' => (string) $label, 'status' => 'warn', 'detail' => (string) $detail, 'val' => (string) $val );
		}

		/**
		 * @param string $label
		 * @param string $detail
		 * @param string $val
		 * @return array
		 */
		private static function item_fail( $label, $detail = '', $val = '' ) {
			return array( 'label' => (string) $label, 'status' => 'fail', 'detail' => (string) $detail, 'val' => (string) $val );
		}

		/**
		 * @param string $label
		 * @param array  $items
		 * @return array
		 */
		private static function make_section( $label, $items ) {
			$score = 0;
			if ( is_array( $items ) ) {
				foreach ( $items as $i ) {
					if ( is_array( $i ) && isset( $i['status'] ) && $i['status'] === 'ok' ) {
						$score++;
					}
				}
			}
			return array(
				'label' => (string) $label,
				'score' => (int) $score,
				'max'   => is_array( $items ) ? count( $items ) : 0,
				'items' => is_array( $items ) ? $items : array(),
			);
		}

		/**
		 * @param array $cat
		 * @param array $section
		 * @return void
		 */
		private static function add_section( &$cat, $section ) {
			if ( ! is_array( $cat ) || ! is_array( $section ) ) {
				return;
			}
			if ( ! isset( $cat['sections'] ) || ! is_array( $cat['sections'] ) ) {
				$cat['sections'] = array();
			}
			$cat['sections'][] = $section;
			$cat['score'] += isset( $section['score'] ) ? (int) $section['score'] : 0;
			$cat['max'] += isset( $section['max'] ) ? (int) $section['max'] : 0;
		}

		/**
		 * @return array
		 */
		public static function cron_schedules( $schedules ) {
			if ( ! is_array( $schedules ) ) {
				$schedules = array();
			}
			$schedules['thirtyminutes'] = array( 'interval' => 1800, 'display' => 'Every 30 minutes' );
			return $schedules;
		}

		/**
		 * @return void
		 */
		private static function schedule_events() {
			if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) {
				return;
			}

			if ( ! wp_next_scheduled( 'avision_hc_fast_cron' ) ) {
				wp_schedule_event( time(), 'thirtyminutes', 'avision_hc_fast_cron' );
			}

			if ( ! wp_next_scheduled( 'avision_hc_full_cron' ) ) {
				$ts = strtotime( 'today 7am' );
				if ( ! is_int( $ts ) || $ts <= 0 ) {
					$ts = time();
				}
				wp_schedule_event( $ts, 'hourly', 'avision_hc_full_cron' );
			}

			if ( ! wp_next_scheduled( 'avision_smoke_test_cron' ) ) {
				wp_schedule_event( time(), 'hourly', 'avision_smoke_test_cron' );
			}
		}

		/**
		 * @param mixed $data
		 * @return int|null
		 */
		private static function extract_points_count( $data ) {
			if ( is_array( $data ) ) {
				foreach ( array( 'total_points', 'total', 'count' ) as $k ) {
					if ( isset( $data[ $k ] ) && is_numeric( $data[ $k ] ) ) {
						return (int) $data[ $k ];
					}
				}
				if ( isset( $data['data'] ) && is_array( $data['data'] ) ) {
					return self::extract_points_count( $data['data'] );
				}
			}
			return null;
		}

		/**
		 * @param mixed $data
		 * @return bool|null true=résultats, false=vide, null=inconnu
		 */
		private static function body_has_results( $data ) {
			if ( is_array( $data ) ) {
				if ( isset( $data['result'] ) && is_array( $data['result'] ) ) {
					return count( $data['result'] ) > 0;
				}
				if ( isset( $data['results'] ) && is_array( $data['results'] ) ) {
					return count( $data['results'] ) > 0;
				}
				if ( isset( $data[0] ) ) {
					return count( $data ) > 0;
				}
			}
			return null;
		}

		/**
		 * @param string $html
		 * @param string $modal_id
		 * @param string $button_id
		 * @param string $label
		 * @return array
		 */
		private static function agency_modal_button_check( $html, $modal_id, $button_id, $label ) {
			try {
				$has_modal = ( $html !== '' && strpos( $html, 'id="' . $modal_id . '"' ) !== false );
				if ( ! $has_modal ) {
					return self::item_fail( $label, 'modal absente', '' );
				}
				$pos = ( $html !== '' ) ? strpos( $html, 'id="' . $button_id . '"' ) : false;
				if ( $pos === false ) {
					return self::item_warn( $label, 'modal présente mais bouton absent', '' );
				}
				$line = self::extract_line_around_pos( $html, (int) $pos );
				if ( strpos( $line, 'disabled' ) !== false ) {
					return self::item_warn( $label, 'bouton disabled', '' );
				}
				return self::item_ok( $label, 'modal + bouton OK', '' );
			} catch ( \Throwable $e ) {
				return self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
			}
		}

		/**
		 * @param string $html
		 * @param string $modal_id
		 * @param string $trigger
		 * @param string $label
		 * @return array
		 */
		private static function agency_modal_trigger_check( $html, $modal_id, $trigger, $label ) {
			try {
				$has_modal = ( $html !== '' && strpos( $html, 'id="' . $modal_id . '"' ) !== false );
				if ( ! $has_modal ) {
					return self::item_fail( $label, 'modal absente', '' );
				}
				$has_trigger = ( $html !== '' && strpos( $html, $trigger ) !== false );
				return $has_trigger ? self::item_ok( $label, 'modal + trigger', '' ) : self::item_warn( $label, 'modal présente mais trigger introuvable', '' );
			} catch ( \Throwable $e ) {
				return self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
			}
		}

		/**
		 * @param string $html
		 * @param string $modal_id
		 * @param string $label
		 * @return array
		 */
		private static function modal_exists_item( $html, $modal_id, $label ) {
			try {
				$has = ( $html !== '' && strpos( $html, 'id="' . $modal_id . '"' ) !== false );
				return $has ? self::item_ok( $label, 'présent', '' ) : self::item_fail( $label, 'absent du HTML', '' );
			} catch ( \Throwable $e ) {
				return self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
			}
		}

		/**
		 * @param string $label
		 * @param string $ajax_url
		 * @param array  $body
		 * @param bool   $warn_if_error_non403
		 * @return array
		 */
		/**
		 * AJAX admin : 200/400 = OK, 403 = warn, ≥500 = fail (nonce / contexte).
		 *
		 * @param string $label
		 * @param string $ajax_url
		 * @param array  $body
		 * @param string $detail_400
		 * @return array
		 */
		private static function ajax_admin_context_check( $label, $ajax_url, $body, $detail_400 ) {
			try {
				$resp = wp_remote_post(
					$ajax_url,
					array(
						'timeout' => 8,
						'body'    => $body,
					)
				);
				if ( is_wp_error( $resp ) ) {
					return self::item_fail( $label, $resp->get_error_message(), '' );
				}
				$code = (int) wp_remote_retrieve_response_code( $resp );
				if ( $code >= 500 ) {
					return self::item_fail( $label, 'HTTP ' . $code, 'HTTP ' . $code );
				}
				if ( $code === 403 ) {
					return self::item_warn( $label, 'HTTP 403', 'HTTP 403' );
				}
				if ( $code === 200 || $code === 400 ) {
					return self::item_ok( $label, $code === 400 ? $detail_400 : 'HTTP 200', 'HTTP ' . $code );
				}
				return self::item_warn( $label, 'HTTP ' . $code, 'HTTP ' . $code );
			} catch ( \Throwable $e ) {
				return self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
			}
		}

		/**
		 * @param string $label
		 * @param string $ajax_url
		 * @param array  $body
		 * @param bool   $warn_if_error_non403
		 * @return array
		 */
		private static function ajax_code_check( $label, $ajax_url, $body, $warn_if_error_non403 ) {
			try {
				$resp = wp_remote_post(
					$ajax_url,
					array(
						'timeout' => 8,
						'body'    => $body,
					)
				);
				if ( is_wp_error( $resp ) ) {
					return self::item_fail( $label, $resp->get_error_message(), '' );
				}
				$code = (int) wp_remote_retrieve_response_code( $resp );
				if ( $code === 403 ) {
					return self::item_fail( $label, 'HTTP 403', 'HTTP 403' );
				}
				if ( $code === 200 ) {
					return self::item_ok( $label, 'HTTP 200', 'HTTP 200' );
				}
				if ( $warn_if_error_non403 ) {
					return self::item_warn( $label, 'HTTP ' . $code, 'HTTP ' . $code );
				}
				return self::item_ok( $label, 'HTTP ' . $code, 'HTTP ' . $code );
			} catch ( \Throwable $e ) {
				return self::item_fail( $label, 'Exception: ' . $e->getMessage(), '' );
			}
		}

		/**
		 * @param string $html
		 * @param int    $pos
		 * @return string
		 */
		private static function extract_line_around_pos( $html, $pos ) {
			$start = strrpos( substr( $html, 0, $pos ), "\n" );
			$end = strpos( $html, "\n", $pos );
			if ( $start === false ) {
				$start = 0;
			} else {
				$start = (int) $start + 1;
			}
			if ( $end === false ) {
				$end = strlen( $html );
			}
			return (string) substr( $html, $start, $end - $start );
		}
	}
}

