<?php
/**
 * Class MG_Diagnostic
 * Moteur de diagnostic « Health Check » — v2.9.116
 *
 * Vérifie que l'environnement du site permet à Maintenance GENIUS de tenir
 * ses promesses (envoi de courriels, écriture DB, 503 réellement servi, etc.).
 *
 * Principes :
 * - Chargée et exécutée UNIQUEMENT côté admin (zéro impact frontend / page 503).
 * - Checks à la demande (bouton), jamais en tâche de fond.
 * - Résultats persistés dans une option non-autoloadée (mg_diagnostic_results)
 *   pour que les badges contextuels / pastille / bandeau LISENT sans re-scanner.
 * - Chaque check retourne : status (ok|warn|fail|skip), label néophyte,
 *   détail humain, fix en une phrase, tooltip technique.
 * - PHP 7.3-safe. Aucune requête sortante hors self-request (timeout 5 s)
 *   et test Turnstile (timeout 5 s), tous deux à la demande.
 */

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

class MG_Diagnostic {

    const RESULTS_OPTION = 'mg_diagnostic_results';
    const EMAIL_THROTTLE_TRANSIENT = 'mg_diag_email_throttle';

    public static function init() {
        add_action('wp_ajax_mg_run_diagnostic', [__CLASS__, 'ajax_run_diagnostic']);
        add_action('wp_ajax_mg_diag_send_test_email', [__CLASS__, 'ajax_send_test_email']);
        add_action('wp_ajax_mg_diag_dismiss_banner', [__CLASS__, 'ajax_dismiss_banner']);
    }

    /**
     * AJAX : masque le bandeau critique 24 h (il revient si un nouveau scan
     * re-détecte le problème — le transient est purgé à chaque scan).
     */
    public static function ajax_dismiss_banner() {
        header('Content-Type: application/json; charset=utf-8');
        check_ajax_referer('mg_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'maintenance-genius')]);
        }

        $check_id = isset($_POST['check_id']) ? sanitize_key( wp_unslash( $_POST['check_id'] ) ) : '';
        if (in_array($check_id, ['email', 'http503'], true)) {
            set_transient('mg_diag_banner_dismissed_' . $check_id, 1, DAY_IN_SECONDS);
        }
        wp_send_json_success();
    }

    // PERSISTANCE

    /**
     * Dernier résultat persisté (ou null si jamais scanné).
     * @return array|null {timestamp:int, checks:array, fails:int, warns:int}
     */
    public static function get_results() {
        $results = get_option(self::RESULTS_OPTION, null);
        return is_array($results) ? $results : null;
    }

    /**
     * Statut d'un check individuel depuis le dernier scan.
     * @param string $check_id
     * @return string 'ok' | 'warn' | 'fail' | 'skip' | 'never'
     */
    public static function get_check_status($check_id) {
        $results = self::get_results();
        if (!$results || empty($results['checks'][$check_id]['status'])) {
            return 'never';
        }
        return $results['checks'][$check_id]['status'];
    }

    /**
     * Compteurs pour la pastille d'onglet. {fails:int, warns:int, never:bool}
     * @return array
     */
    public static function get_badge_counts() {
        $results = self::get_results();
        if (!$results) {
            return ['fails' => 0, 'warns' => 0, 'never' => true];
        }
        return [
            'fails' => (int) ($results['fails'] ?? 0),
            'warns' => (int) ($results['warns'] ?? 0),
            'never' => false,
        ];
    }

    /**
     * Le bandeau critique doit-il s'afficher ? Retourne le message ou null.
     * Règle : uniquement les échecs qui trahissent la promesse du produit,
     * max 1 bandeau (le plus grave), pages MG seulement (géré par l'appelant).
     * @return array|null {message:string, check_id:string}
     */
    public static function get_critical_banner() {
        $results = self::get_results();
        if (!$results) {
            return null;
        }
        $settings = MG_Settings::load();
        $checks = $results['checks'] ?? [];

        // Critique #1 : collecte d'emails active + envoi courriel cassé.
        if (
            !empty($settings['notification_enabled'])
            && isset($checks['email']['status']) && 'fail' === $checks['email']['status']
        ) {
            return [
                'check_id' => 'email',
                'message'  => __('Email collection is enabled but your site cannot send emails — subscribers will never be notified.', 'maintenance-genius'),
            ];
        }

        // Critique #2 : maintenance ON + 503 pas réellement servi.
        if (
            !empty($settings['maintenance_enabled'])
            && isset($checks['http503']['status']) && 'fail' === $checks['http503']['status']
        ) {
            return [
                'check_id' => 'http503',
                'message'  => __('Maintenance mode is ON but your server is not actually serving the maintenance page — a cache layer is probably bypassing it.', 'maintenance-genius'),
            ];
        }

        return null;
    }

    // AJAX

    /**
     * AJAX : lance l'analyse complète et persiste le résultat.
     */
    public static function ajax_run_diagnostic() {
        header('Content-Type: application/json; charset=utf-8');
        check_ajax_referer('mg_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'maintenance-genius')]);
        }

        $checks = [
            'email'      => self::check_email(),
            'db_write'   => self::check_db_write(),
            'http503'    => self::check_http503(),
            'mu_plugin'  => self::check_mu_plugin(),
            'cache'      => self::check_cache_conflicts(),
            'cron'       => self::check_cron(),
            'turnstile'  => self::check_turnstile(),
            'env'        => self::check_environment(),
        ];

        $fails = 0;
        $warns = 0;
        foreach ($checks as $check) {
            if ('fail' === $check['status']) { $fails++; }
            if ('warn' === $check['status']) { $warns++; }
        }

        $results = [
            'timestamp' => time(),
            'checks'    => $checks,
            'fails'     => $fails,
            'warns'     => $warns,
        ];

        update_option(self::RESULTS_OPTION, $results, false);

        // Un nouveau scan ré-arme les bandeaux : si le problème persiste, il réapparaît.
        delete_transient('mg_diag_banner_dismissed_email');
        delete_transient('mg_diag_banner_dismissed_http503');

        wp_send_json_success([
            'results'   => $results,
            'date_human'=> date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $results['timestamp']),
        ]);
    }

    /**
     * AJAX : envoi d'un courriel test (throttlé 1/minute).
     */
    public static function ajax_send_test_email() {
        header('Content-Type: application/json; charset=utf-8');
        check_ajax_referer('mg_nonce', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'maintenance-genius')]);
        }

        if (get_transient(self::EMAIL_THROTTLE_TRANSIENT)) {
            wp_send_json_error(['message' => __('Please wait one minute between test emails.', 'maintenance-genius')]);
        }
        set_transient(self::EMAIL_THROTTLE_TRANSIENT, 1, MINUTE_IN_SECONDS);

        $to = isset($_POST['email']) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
        if (empty($to) || !is_email($to)) {
            $to = get_option('admin_email');
        }

        $sent = self::do_send_test_email($to);

        if ($sent) {
            wp_send_json_success([
                /* translators: %s: email address */
                'message' => sprintf(__('Test email sent to %s — check your inbox (and spam folder).', 'maintenance-genius'), $to),
            ]);
        }

        wp_send_json_error([
            'message' => __('Sending failed — your site cannot send emails. Configure an SMTP service or contact your host.', 'maintenance-genius'),
        ]);
    }

    // CHECKS — chacun retourne le même contrat :
    // ['status','label','detail','fix','tooltip']

    /**
     * #1 — Envoi de courriel via wp_mail (test réel vers l'admin).
     * Throttlé 1/minute : un re-scan rapproché réutilise le dernier résultat
     * (évite de spammer l'admin si l'utilisateur relance l'analyse en boucle).
     */
    private static function check_email() {
        $to = get_option('admin_email');

        if (get_transient(self::EMAIL_THROTTLE_TRANSIENT)) {
            $previous = self::get_results();
            if ($previous && isset($previous['checks']['email'])) {
                $cached = $previous['checks']['email'];
                $cached['detail'] = __('(Result from the previous scan — email tests are limited to one per minute.)', 'maintenance-genius') . ' ' . $cached['detail'];
                return $cached;
            }
            return self::result('warn',
                __('Email test skipped (rate limit)', 'maintenance-genius'),
                __('An email test was already sent less than a minute ago. Wait a minute, then run the analysis again.', 'maintenance-genius'),
                '',
                __('Test emails are limited to one per minute to protect your mail relay reputation.', 'maintenance-genius')
            );
        }
        set_transient(self::EMAIL_THROTTLE_TRANSIENT, 1, MINUTE_IN_SECONDS);

        $sent = self::do_send_test_email($to);

        if ($sent) {
            return self::result('ok',
                __('Your site can send emails', 'maintenance-genius'),
                /* translators: %s: email address */
                sprintf(__('A test email was sent to %s. Check the inbox (and spam folder) to confirm delivery.', 'maintenance-genius'), $to),
                '',
                __('wp_mail() returned true. Delivery to the inbox still depends on your SPF/DKIM alignment.', 'maintenance-genius')
            );
        }

        return self::result('fail',
            __('Your site cannot send emails', 'maintenance-genius'),
            __('The test email could not be sent. Subscriber notifications and admin alerts will silently fail.', 'maintenance-genius'),
            __('Install an SMTP plugin (e.g. WP Mail SMTP) or ask your host to enable outgoing mail.', 'maintenance-genius'),
            __('wp_mail() returned false — PHP mail()/SMTP is not configured on this server.', 'maintenance-genius')
        );
    }

    /**
     * #2 — Écriture DB vérifiée (roundtrip write/read, cache purgé).
     * Détecte le cas Redis/object-cache où update_option échoue en silence.
     */
    private static function check_db_write() {
        global $wpdb;

        $probe_key   = 'mg_diag_probe';
        $probe_value = 'probe_' . time() . '_' . wp_rand(1000, 9999);

        // Écriture SQL directe (comme le stockage emails S2).
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe
        $exists = $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $probe_key ) );
        if (null === $exists) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe
            $write = $wpdb->insert($wpdb->options, ['option_name' => $probe_key, 'option_value' => $probe_value, 'autoload' => 'no'], ['%s', '%s', '%s']);
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe
            $write = $wpdb->update($wpdb->options, ['option_value' => $probe_value], ['option_name' => $probe_key], ['%s'], ['%s']);
        }
        wp_cache_delete($probe_key, 'options');
        wp_cache_delete('alloptions', 'options');
        wp_cache_delete('notoptions', 'options');

        // Relecture via l'API standard : c'est CE chemin que le plugin utilise partout.
        $read_api = get_option($probe_key, null);

        // Relecture SQL directe pour distinguer « écriture perdue » de « cache stale ».
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe
        $read_sql = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", $probe_key ) );

        // Nettoyage.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Diagnostic probe cleanup
        $wpdb->delete($wpdb->options, ['option_name' => $probe_key], ['%s']);
        wp_cache_delete($probe_key, 'options');

        if (false === $write || $read_sql !== $probe_value) {
            return self::result('fail',
                __('Database writes are failing', 'maintenance-genius'),
                __('A test value could not be written to the database. Settings and subscriber emails may be silently lost.', 'maintenance-genius'),
                __('Contact your host — the database user may lack write permissions, or the disk may be full.', 'maintenance-genius'),
                __('Direct SQL write to wp_options failed or the value did not persist.', 'maintenance-genius')
            );
        }

        if ($read_api !== $probe_value) {
            return self::result('warn',
                __('Object cache is serving stale data', 'maintenance-genius'),
                __('The database write succeeded but WordPress read back an outdated value. A persistent object cache (Redis/Memcached) is not being invalidated correctly.', 'maintenance-genius'),
                __('Flush the object cache (e.g. « wp cache flush ») or check your Redis drop-in configuration.', 'maintenance-genius'),
                __('SQL read returned the fresh value but get_option() served a stale one despite wp_cache_delete().', 'maintenance-genius')
            );
        }

        return self::result('ok',
            __('Database writes work correctly', 'maintenance-genius'),
            __('A test value was written, read back and deleted successfully — including through the object cache.', 'maintenance-genius'),
            '',
            __('Write/read roundtrip verified via both direct SQL and get_option() after cache purge.', 'maintenance-genius')
        );
    }

    /**
     * #3 — La page maintenance est-elle RÉELLEMENT servie (503/200) ?
     * Self-request avec timeout court. « Non concluant » plutôt que faux échec :
     * loopback bloqué = courant sur mutualisé, ce n'est PAS une erreur du site.
     */
    private static function check_http503() {
        $settings = MG_Settings::load();

        if (empty($settings['maintenance_enabled'])) {
            return self::result('skip',
                __('Maintenance page delivery', 'maintenance-genius'),
                __('Maintenance mode is currently OFF — this check runs only while it is ON.', 'maintenance-genius'),
                '',
                __('Enable maintenance mode, then run the diagnostic again to verify the HTTP 503 response.', 'maintenance-genius')
            );
        }

        $response = wp_remote_get(home_url('/'), [
            'timeout'     => 5,
            'redirection' => 2,
            'sslverify'   => false, // Self-request : certains hébergements ont un SSL interne invalide.
            'headers'     => ['Cache-Control' => 'no-cache'],
            'cookies'     => [], // Requête anonyme : simule un vrai visiteur, pas l'admin connecté.
        ]);

        if (is_wp_error($response)) {
            return self::result('warn',
                __('Maintenance page delivery could not be verified', 'maintenance-genius'),
                __('Your server blocked the self-request (loopback) — common on shared hosting. This does not mean the page is broken.', 'maintenance-genius'),
                __('Open your site in a private/incognito window to confirm visitors see the maintenance page.', 'maintenance-genius'),
                /* translators: %s: technical error message */
                sprintf(__('wp_remote_get() to home_url failed: %s', 'maintenance-genius'), $response->get_error_message())
            );
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $body = wp_remote_retrieve_body($response);
        $is_mg_page = (false !== strpos($body, 'maintenance-container'));
        $expects_503 = !empty($settings['seo_503_enabled']);

        if ($is_mg_page && (!$expects_503 || 503 === $code)) {
            return self::result('ok',
                __('The maintenance page is being served correctly', 'maintenance-genius'),
                $expects_503
                    ? __('Visitors receive the maintenance page with the proper HTTP 503 status — search engines will not index it as your real site.', 'maintenance-genius')
                    : __('Visitors receive the maintenance page.', 'maintenance-genius'),
                '',
                /* translators: %d: HTTP status code */
                sprintf(__('Self-request returned HTTP %d and the response body contains the maintenance markup.', 'maintenance-genius'), $code)
            );
        }

        if ($is_mg_page && $expects_503 && 503 !== $code) {
            return self::result('warn',
                __('Maintenance page served, but with the wrong HTTP status', 'maintenance-genius'),
                /* translators: %d: HTTP status code */
                sprintf(__('Visitors see the maintenance page but the server returns HTTP %d instead of 503. Search engines may index the maintenance content.', 'maintenance-genius'), $code),
                __('A cache or proxy layer (Cloudflare, server cache) is rewriting the status code — exclude your site URL from full-page caching during maintenance.', 'maintenance-genius'),
                __('Body contains maintenance markup but status differs from the configured 503.', 'maintenance-genius')
            );
        }

        return self::result('fail',
            __('Visitors are NOT seeing the maintenance page', 'maintenance-genius'),
            /* translators: %d: HTTP status code */
            sprintf(__('The self-request received your normal site (HTTP %d) instead of the maintenance page. A cache layer is bypassing the plugin.', 'maintenance-genius'), $code),
            __('Purge all caches (plugin, server, Cloudflare) and exclude your homepage from caching while maintenance is active.', 'maintenance-genius'),
            __('Response body does not contain the maintenance page markup.', 'maintenance-genius')
        );
    }

    /**
     * #4 — mu-plugin Cache Guard installé (quand maintenance ON).
     */
    private static function check_mu_plugin() {
        $settings = MG_Settings::load();
        $mu_file = WPMU_PLUGIN_DIR . '/mg-cache-guard.php';
        $installed = file_exists($mu_file);

        if (empty($settings['maintenance_enabled'])) {
            return self::result('skip',
                __('Cache Guard (mu-plugin)', 'maintenance-genius'),
                __('Maintenance mode is OFF — the Cache Guard is only required while it is ON.', 'maintenance-genius'),
                '',
                __('The mu-plugin intercepts requests before cache plugins to guarantee the maintenance page displays.', 'maintenance-genius')
            );
        }

        if ($installed) {
            return self::result('ok',
                __('Cache Guard is installed', 'maintenance-genius'),
                __('The protective mu-plugin is in place — cache plugins cannot bypass the maintenance page.', 'maintenance-genius'),
                '',
                /* translators: %s: file path */
                sprintf(__('File present: %s', 'maintenance-genius'), $mu_file)
            );
        }

        return self::result('fail',
            __('Cache Guard is missing', 'maintenance-genius'),
            __('Maintenance mode is ON but the protective mu-plugin is not installed. Cached pages may be served to visitors instead of the maintenance page.', 'maintenance-genius'),
            __('Toggle maintenance mode OFF then ON again to reinstall it, or check that wp-content/mu-plugins is writable.', 'maintenance-genius'),
            /* translators: %s: file path */
            sprintf(__('Expected file not found: %s', 'maintenance-genius'), $mu_file)
        );
    }

    /**
     * #5 — Plugins de cache actifs (information, pas un échec).
     */
    private static function check_cache_conflicts() {
        $detected = [];

        if (get_option('WpFastestCache'))                       { $detected[] = 'WP Fastest Cache'; }
        if (defined('WPCACHEHOME'))                             { $detected[] = 'WP Super Cache'; }
        if (get_option('litespeed.conf.cache'))                 { $detected[] = 'LiteSpeed Cache'; }
        if (get_option('siteground_optimizer_enable_cache'))    { $detected[] = 'SiteGround Optimizer'; }
        if (defined('WP_ROCKET_VERSION'))                       { $detected[] = 'WP Rocket'; }
        if (defined('W3TC'))                                    { $detected[] = 'W3 Total Cache'; }
        if (defined('WPO_VERSION'))                             { $detected[] = 'WP-Optimize'; }

        if (empty($detected)) {
            return self::result('ok',
                __('No cache plugin conflicts', 'maintenance-genius'),
                __('No full-page cache plugin was detected — the maintenance page will always be served fresh.', 'maintenance-genius'),
                '',
                __('Checked: WP Rocket, W3TC, LiteSpeed, WP Super Cache, WP Fastest Cache, SiteGround Optimizer, WP-Optimize.', 'maintenance-genius')
            );
        }

        return self::result('warn',
            __('Cache plugin detected', 'maintenance-genius'),
            /* translators: %s: comma-separated plugin names */
            sprintf(__('Detected: %s. The Cache Guard normally handles this, but if visitors report seeing the old site, purge this cache.', 'maintenance-genius'), implode(', ', $detected)),
            __('After enabling maintenance, purge the cache once. Run the « Maintenance page delivery » check to confirm.', 'maintenance-genius'),
            __('Full-page caches can serve stale copies of your site during the first minutes of maintenance.', 'maintenance-genius')
        );
    }

    /**
     * #6 — WP-Cron fonctionnel (requis pour la planification ; le countdown
     * auto-disable se déclenche à la visite, mais le scheduling passe par cron).
     */
    private static function check_cron() {
        if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
            // Cron système possible — on ne peut pas le prouver d'ici : warn informatif.
            return self::result('warn',
                __('WP-Cron is disabled in wp-config.php', 'maintenance-genius'),
                __('Scheduled tasks rely on a server-side cron job. If none is configured, scheduled maintenance activation/deactivation will not run on time.', 'maintenance-genius'),
                __('Confirm a real cron job calls wp-cron.php every few minutes, or remove DISABLE_WP_CRON.', 'maintenance-genius'),
                __('DISABLE_WP_CRON is true — WordPress will not trigger cron on page loads.', 'maintenance-genius')
            );
        }

        $crons = _get_cron_array();
        if (empty($crons)) {
            return self::result('warn',
                __('No scheduled tasks found', 'maintenance-genius'),
                __('The WordPress cron queue is empty, which is unusual and may indicate cron is not running.', 'maintenance-genius'),
                __('Install a cron-checking plugin (WP Crontrol) to inspect, or contact your host.', 'maintenance-genius'),
                __('_get_cron_array() returned empty.', 'maintenance-genius')
            );
        }

        return self::result('ok',
            __('Scheduled tasks are operational', 'maintenance-genius'),
            __('WP-Cron is enabled and the task queue is populated — scheduling features will run.', 'maintenance-genius'),
            '',
            __('Note: the countdown auto-disable triggers on visitor requests, independent of cron.', 'maintenance-genius')
        );
    }

    /**
     * #7 — Turnstile : clés présentes + endpoint joignable (si activé).
     */
    private static function check_turnstile() {
        $settings = MG_Settings::load();

        if (empty($settings['turnstile_enabled'])) {
            return self::result('skip',
                __('Cloudflare Turnstile', 'maintenance-genius'),
                __('Turnstile is disabled — nothing to verify.', 'maintenance-genius'),
                '',
                __('Enable Turnstile in the Access tab to protect your forms against bots.', 'maintenance-genius')
            );
        }

        $site_key   = $settings['turnstile_site_key'] ?? '';
        $secret_key = $settings['turnstile_secret_key'] ?? '';

        if (empty($site_key) || empty($secret_key)) {
            return self::result('fail',
                __('Turnstile keys are missing', 'maintenance-genius'),
                __('Turnstile is enabled but the site key and/or secret key is empty — protected forms will reject every submission.', 'maintenance-genius'),
                __('Paste both keys from your Cloudflare dashboard (Turnstile section), or disable Turnstile.', 'maintenance-genius'),
                __('Both turnstile_site_key and turnstile_secret_key must be set for verification to work.', 'maintenance-genius')
            );
        }

        $response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
            'timeout' => 5,
            'body'    => ['secret' => $secret_key, 'response' => 'mg-diagnostic-probe'],
        ]);

        if (is_wp_error($response)) {
            return self::result('warn',
                __('Turnstile endpoint unreachable', 'maintenance-genius'),
                __('Your server could not reach the Cloudflare verification endpoint. Form submissions may fail while this persists.', 'maintenance-genius'),
                __('Check that your host allows outgoing HTTPS requests to challenges.cloudflare.com.', 'maintenance-genius'),
                /* translators: %s: technical error message */
                sprintf(__('wp_remote_post() failed: %s', 'maintenance-genius'), $response->get_error_message())
            );
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);
        $error_codes = isset($body['error-codes']) && is_array($body['error-codes']) ? $body['error-codes'] : [];

        // Une clé secrète invalide est signalée explicitement par l'API.
        if (in_array('invalid-input-secret', $error_codes, true)) {
            return self::result('fail',
                __('Turnstile secret key is invalid', 'maintenance-genius'),
                __('Cloudflare rejected your secret key — every protected form submission will fail verification.', 'maintenance-genius'),
                __('Re-copy the secret key from the Cloudflare dashboard (it differs from the site key).', 'maintenance-genius'),
                __('siteverify returned invalid-input-secret.', 'maintenance-genius')
            );
        }

        // invalid-input-response attendu (probe factice) → la clé secrète est acceptée.
        return self::result('ok',
            __('Turnstile is configured correctly', 'maintenance-genius'),
            __('Keys are present and the Cloudflare endpoint accepted your secret key.', 'maintenance-genius'),
            '',
            __('The probe intentionally sends a dummy token; only the secret-key validity is asserted.', 'maintenance-genius')
        );
    }

    /**
     * #8 — Environnement : PHP/WP, HTTPS, salts.
     */
    private static function check_environment() {
        global $wp_version;
        $issues = [];

        if (version_compare(PHP_VERSION, '7.4', '<')) {
            /* translators: %s: PHP version */
            $issues[] = sprintf(__('PHP %s is end-of-life — upgrade to PHP 8.x for security and speed.', 'maintenance-genius'), PHP_VERSION);
        }
        if (version_compare($wp_version, '6.0', '<')) {
            /* translators: %s: WordPress version */
            $issues[] = sprintf(__('WordPress %s is outdated — update to the latest version.', 'maintenance-genius'), $wp_version);
        }
        if (!is_ssl()) {
            $issues[] = __('The site is not served over HTTPS — remote control tokens travel unencrypted.', 'maintenance-genius');
        }
        if (defined('AUTH_KEY') && ('put your unique phrase here' === AUTH_KEY || strlen(AUTH_KEY) < 32)) {
            $issues[] = __('Security salts in wp-config.php are default or too short — regenerate them.', 'maintenance-genius');
        }

        if (empty($issues)) {
            return self::result('ok',
                __('Server environment is healthy', 'maintenance-genius'),
                /* translators: 1: PHP version, 2: WordPress version */
                sprintf(__('PHP %1$s · WordPress %2$s · HTTPS active · security salts set.', 'maintenance-genius'), PHP_VERSION, $wp_version),
                '',
                __('Checked: PHP version, WordPress version, SSL, wp-config.php salts.', 'maintenance-genius')
            );
        }

        return self::result('warn',
            __('Server environment needs attention', 'maintenance-genius'),
            implode(' ', $issues),
            __('Address the points above via your hosting panel or wp-config.php.', 'maintenance-genius'),
            /* translators: 1: PHP version, 2: WordPress version */
            sprintf(__('PHP %1$s · WordPress %2$s', 'maintenance-genius'), PHP_VERSION, $wp_version)
        );
    }

    /**
     * Badge contextuel de section (échappé, prêt à imprimer).
     * Lit UNIQUEMENT le dernier résultat persisté — zéro check live.
     * @param string $check_id
     * @return string HTML sûr ('' si le check est ok ou skip)
     */
    public static function section_badge($check_id) {
        $status = self::get_check_status($check_id);

        if ('never' === $status) {
            return '<span class="mg-diag-badge mg-diag-badge-never" data-diag-goto="' . esc_attr($check_id) . '" title="' . esc_attr__('Not verified yet — run the diagnostic', 'maintenance-genius') . '">🩺</span>';
        }
        if ('fail' === $status) {
            $results = self::get_results();
            $fix = $results['checks'][$check_id]['fix'] ?? '';
            return '<span class="mg-diag-badge mg-diag-badge-fail" data-diag-goto="' . esc_attr($check_id) . '" title="' . esc_attr($fix ? $fix : __('Issue detected — see the Diagnostic tab', 'maintenance-genius')) . '">⚠️</span>';
        }
        if ('warn' === $status) {
            return '<span class="mg-diag-badge mg-diag-badge-warn" data-diag-goto="' . esc_attr($check_id) . '" title="' . esc_attr__('Warning — see the Diagnostic tab', 'maintenance-genius') . '">⚠️</span>';
        }
        return '';
    }

    // HELPERS

    /**
     * Contrat de résultat uniforme.
     * @param string $status ok|warn|fail|skip
     * @param string $label Titre néophyte
     * @param string $detail Explication humaine
     * @param string $fix Correctif en une phrase (vide si ok/skip)
     * @param string $tooltip Détail technique (tooltip H2)
     * @return array
     */
    private static function result($status, $label, $detail, $fix, $tooltip) {
        return [
            'status'  => $status,
            'label'   => $label,
            'detail'  => $detail,
            'fix'     => $fix,
            'tooltip' => $tooltip,
        ];
    }

    /**
     * Envoi réel du courriel test.
     * @param string $to
     * @return bool
     */
    private static function do_send_test_email($to) {
        $site_name = get_bloginfo('name');
        /* translators: %s: site name */
        $subject = sprintf(__('[%s] Maintenance GENIUS — test email', 'maintenance-genius'), $site_name);
        $message = __("This is a test email sent by the Maintenance GENIUS diagnostic.\n\nIf you received it, your site can deliver subscriber notifications.\n\n— Maintenance GENIUS", 'maintenance-genius');
        $headers = ['Content-Type: text/plain; charset=UTF-8'];

        return (bool) wp_mail($to, $subject, $message, $headers);
    }
}
