<?php

namespace WPSN\Modules\Diagnostics;

use WPSN\Core\CompatibilityDetector;
use WPSN\Core\Container;
use WPSN\Core\ErrorTracker;
use WPSN\Core\Http\HttpClient;
use WPSN\Core\Settings;
use WPSN\Core\Queue\QueuePause;
use WPSN\Modules\Stability\HeadSnapshotService;

defined('ABSPATH') || exit;

/**
 * Quality Gate service (runtime-safe).
 *
 * Used by:
 * - wp-admin Quality Gate screen
 * - scheduled cron runs (hourly tick decides when to run)
 * - Tasks Hub (last global run)
 */
final class QualityGateService
{
    /**
     * Option name that stores the last site-wide Quality Gate results.
     *
     * @var string
     */
    public const RESULTS_OPTION = 'wpsn_quality_gate_last_results';

    private Container $container;
    private Settings $settings;

    public function __construct(Container $container)
    {
        $this->container = $container;
        $this->settings = $container->get('settings');
    }

    /**
     * Run all checks and return a structured results payload.
     *
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    public function runAllChecks(array $context = []): array
    {
        $disableOutput = $this->disableOutputEffective();

        $checks = [
            'sitemap' => $this->checkSitemap($disableOutput),
            'robots' => $this->checkRobots($disableOutput),
            'head' => $this->checkHeadOutput($disableOutput),
            'frontend' => $this->checkFrontendContract(),
            'pages' => $this->checkPagesProbe(),
            'queue' => $this->checkQueue(),
            'fatals' => $this->checkFatalErrors(),
        ];

        // Overall: worst severity wins.
        $sevOrder = ['ok' => 0, 'info' => 1, 'warn' => 2, 'critical' => 3];
        $worst = 'ok';
        foreach ($checks as $c) {
            $s = is_array($c) && isset($c['severity']) ? (string) $c['severity'] : 'info';
            if (($sevOrder[$s] ?? 1) > ($sevOrder[$worst] ?? 0)) {
                $worst = $s;
            }
        }

        $ctx = [];
        if (is_array($context)) {
            foreach ($context as $k => $v) {
                $k = sanitize_key((string) $k);
                if ($k === '') {
                    continue;
                }
                if (is_scalar($v) || $v === null) {
                    $ctx[$k] = $v;
                }
            }
        }

        return [
            'created_at_utc' => gmdate('c'),
            'ts' => time(),
            'plugin_version' => (string) (defined('WPSN_VERSION') ? WPSN_VERSION : ''),
            'disable_output_effective' => $disableOutput,
            'checks' => $checks,
            'severity' => $worst,
            'context' => $ctx,
        ];
    }

    public function disableOutputEffective(): bool
    {
        $auto = (bool) $this->settings->get('compatibility.auto_disable_output_on_conflict', false);
        $stored = (bool) $this->settings->get('compatibility.disable_output', false);
        $const = defined('WPSN_DISABLE_OUTPUT') && (bool) WPSN_DISABLE_OUTPUT;
        return $const || $stored || ($auto && CompatibilityDetector::hasSeoConflict());
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkSitemap(bool $disableOutputEffective): array
    {
        $enabled = (bool) $this->settings->moduleEnabled('sitemap', true);

        $details = [
            'module_enabled' => $enabled,
            'rewrite_rules_present' => $this->sitemapRewriteRulesPresent(),
            'tests' => [],
        ];

        // IMPORTANT: Only test local URLs. Avoid SSRF.
        $tests = [
            ['label' => '/sitemap_index.xml', 'url' => home_url('/sitemap_index.xml')],
            ['label' => '/sitemap.xml', 'url' => home_url('/sitemap.xml')],
            ['label' => '/?wpsn_sitemap=index', 'url' => add_query_arg(['wpsn_sitemap' => 'index'], home_url('/'))],
        ];

        // Optional HTML sitemap (human-readable).
        $testsHtml = [
            ['label' => '/html-sitemap/', 'url' => home_url('/html-sitemap/')],
            ['label' => '/?wpsn_html_sitemap=1', 'url' => add_query_arg(['wpsn_html_sitemap' => '1'], home_url('/'))],
        ];

        $primaryOk = false;
        $fallbackOk = false;
        $htmlOk = false;

        foreach ($tests as $t) {
            $probe = $this->fetchProbe((string) $t['label'], (string) $t['url'], 'application/xml,text/xml;q=0.9,*/*;q=0.8');
            $details['tests'][] = $probe;
            if (!empty($probe['ok']) && (string) $t['label'] === '/sitemap_index.xml') {
                $primaryOk = true;
            }
            if (!empty($probe['ok']) && (string) $t['label'] === '/?wpsn_sitemap=index') {
                $fallbackOk = true;
            }
        }

        $details['tests_html'] = [];
        foreach ($testsHtml as $t) {
            $probe = $this->fetchProbe((string) $t['label'], (string) $t['url'], 'text/html,*/*;q=0.8');
            $details['tests_html'][] = $probe;
            if (!empty($probe['ok']) && (string) $t['label'] === '/html-sitemap/') {
                $htmlOk = true;
            }
        }

        if (!$enabled) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Sitemap module is disabled.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($disableOutputEffective) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Output is disabled (Compatibility mode). Sitemap endpoints may not be served by WP SEO Next.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($primaryOk) {
            return [
                'ok' => true,
                'severity' => $htmlOk ? 'ok' : 'warn',
                'summary' => $htmlOk
                    ? __('Sitemap is reachable.', 'wp-seo-next')
                    : __('XML sitemap is reachable, but HTML sitemap is not reachable (loopback).', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($fallbackOk) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Sitemap works via fallback, but /sitemap_index.xml is not reachable. Likely rewrite rules / cache.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => false,
            'severity' => 'critical',
            'summary' => __('Sitemap is not reachable (loopback).', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkRobots(bool $disableOutputEffective): array
    {
        $enabled = (bool) $this->settings->moduleEnabled('robots', true);
        $appendSitemap = (bool) $this->settings->get('robots_txt.append_sitemap', true);

        $probe = $this->fetchProbe('/robots.txt', home_url('/robots.txt'), 'text/plain,*/*;q=0.8');

        $details = [
            'module_enabled' => $enabled,
            'append_sitemap' => $appendSitemap,
            'probe' => $probe,
            'has_user_agent' => false,
            'has_sitemap' => false,
        ];

        $body = isset($probe['body_raw']) ? (string) $probe['body_raw'] : '';
        $ua = stripos($body, 'User-agent:') !== false;
        $sm = stripos($body, 'Sitemap:') !== false;
        $details['has_user_agent'] = $ua;
        $details['has_sitemap'] = $sm;

        if (empty($probe['ok'])) {
            return [
                'ok' => false,
                'severity' => 'critical',
                'summary' => __('robots.txt is not reachable (loopback).', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if (!$ua) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('robots.txt is reachable, but missing "User-agent" rules.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($disableOutputEffective || !$enabled) {
            return [
                'ok' => true,
                'severity' => 'warn',
                'summary' => __('robots.txt is reachable, but WP SEO Next output is disabled. Custom rules may not be appended.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($appendSitemap && !$sm) {
            return [
                'ok' => true,
                'severity' => 'warn',
                'summary' => __('robots.txt is reachable, but Sitemap directive is missing.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('robots.txt is reachable.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkHeadOutput(bool $disableOutputEffective): array
    {
        $svc = new HeadSnapshotService($this->container);
        $cases = $svc->buildDefaultCases();
        $payload = $svc->buildSnapshotPayload($cases);
        $siteHost = wp_parse_url(home_url('/'), PHP_URL_HOST);
        $siteHost = is_string($siteHost) ? $siteHost : '';

        $issues = [];
        $caseSummaries = [];
        $severity = 'ok';
        $ok = true;

        $payloadCases = isset($payload['cases']) && is_array($payload['cases']) ? $payload['cases'] : [];
        foreach ($payloadCases as $c) {
            if (!is_array($c)) {
                continue;
            }
            $label = isset($c['label']) ? (string) $c['label'] : '';
            $ctx = isset($c['context']) && is_array($c['context']) ? $c['context'] : [];
            $url = isset($ctx['url']) ? (string) $ctx['url'] : '';

            $struct = isset($c['head_struct']) && is_array($c['head_struct']) ? $c['head_struct'] : [];
            $title = isset($struct['title']) ? (string) $struct['title'] : '';
            $link = isset($struct['link']) && is_array($struct['link']) ? $struct['link'] : [];
            $meta = isset($struct['meta']) && is_array($struct['meta']) ? $struct['meta'] : [];

            $canon = '';
            if (isset($link['rel:canonical'])) {
                if (is_string($link['rel:canonical'])) {
                    $canon = $link['rel:canonical'];
                } elseif (is_array($link['rel:canonical']) && !empty($link['rel:canonical'][0])) {
                    $canon = (string) $link['rel:canonical'][0];
                }
            }

            $caseProblems = [];

            $missingTitle = trim($title) === '';
            $missingCanon = trim($canon) === '';
            $canonHostMismatch = false;

            if ($missingTitle) {
                $caseProblems[] = __('Missing <title>.', 'wp-seo-next');
            }

            if ($missingCanon) {
                $caseProblems[] = __('Missing canonical.', 'wp-seo-next');
            } else {
                $canonHost = wp_parse_url($canon, PHP_URL_HOST);
                $canonHost = is_string($canonHost) ? $canonHost : '';
                if ($siteHost !== '' && $canonHost !== '' && strtolower($canonHost) !== strtolower($siteHost)) {
                    $canonHostMismatch = true;
                    $caseProblems[] = sprintf(__('Canonical host mismatch: %s', 'wp-seo-next'), $canonHost);
                }
            }

            $robots = '';
            if (isset($meta['name:robots'])) {
                $rv = $meta['name:robots'];
                if (is_string($rv)) {
                    $robots = $rv;
                } elseif (is_array($rv) && !empty($rv[0])) {
                    $robots = (string) $rv[0];
                }
            }
            $missingRobots = $robots === '';
            $robotsNoindex = (!$missingRobots && stripos($robots, 'noindex') !== false);

            if ($missingRobots) {
                $caseProblems[] = __('No meta robots tag.', 'wp-seo-next');
            } elseif ($robotsNoindex) {
                $caseProblems[] = sprintf(__('Robots contains noindex (%s).', 'wp-seo-next'), $robots);
            }

            $caseSummaries[] = [
                'label' => $label !== '' ? $label : $url,
                'url' => $url,
                'title' => $title,
                'canonical' => $canon,
                'robots' => $robots,
                'problems' => $caseProblems,
            ];

            if (!empty($caseProblems)) {
                $ok = false;

                $caseSev = 'warn';
                if ($missingTitle || $missingCanon || $canonHostMismatch) {
                    $caseSev = 'critical';
                }

                if ($caseSev === 'critical') {
                    $severity = 'critical';
                } elseif ($severity !== 'critical') {
                    $severity = 'warn';
                }

                foreach ($caseProblems as $p) {
                    $p = (string) $p;
                    $issues[] = ($label !== '' ? $label : $url) . ': ' . $p;
                }
            }
        }

        $details = [
            'disable_output_effective' => $disableOutputEffective,
            'cases' => $caseSummaries,
            'issues' => $issues,
        ];

        if ($disableOutputEffective) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Head output is currently disabled (Compatibility mode).', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($ok) {
            return [
                'ok' => true,
                'severity' => 'ok',
                'summary' => __('Head output looks sane.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => false,
            'severity' => $severity,
            'summary' => $severity === 'critical'
                ? __('Head output has critical issues.', 'wp-seo-next')
                : __('Head output has warnings.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * Verify that no WPSN editor UI / assets leak into anonymous frontend pages.
     *
     * This is primarily a caching-safety check.
     *
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkFrontendContract(): array
    {
        $contractEnabled = (bool) $this->settings->get('performance.frontend.contract_enabled', true);
        $onDemand = (bool) $this->settings->get('performance.frontend.inspector_on_demand', true);

        $details = [
            'contract_enabled' => $contractEnabled,
            'inspector_on_demand' => $onDemand,
            'tests' => [],
            'markers_found' => [],
        ];

        if (!$contractEnabled) {
            return [
                'ok' => true,
                'severity' => 'info',
                'summary' => __('Frontend asset contract is disabled.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        // IMPORTANT: Only test local URLs. Avoid SSRF.
        $probe = $this->fetchProbe('/ (frontend)', home_url('/'), 'text/html,*/*;q=0.8');
        $details['tests'][] = $probe;

        if (empty($probe['ok'])) {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => __('Could not verify frontend output (loopback failed).', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        $body = isset($probe['body_raw']) ? (string) $probe['body_raw'] : '';

        // Detect WPSN editor UI markers that should never appear for anonymous visitors.
        $markers = [
            'id="wpsn-inspector"',
            'wpsn-frontend-inspector',
            'wpsn-universal-metabox',
            'wpsn-universal-metabox-root',
            '<!-- WP SEO Next: Frontend Inspector',
        ];

        $found = [];
        foreach ($markers as $m) {
            if ($m !== '' && strpos($body, $m) !== false) {
                $found[] = $m;
            }
        }

        $details['markers_found'] = $found;

        if (!empty($found)) {
            return [
                'ok' => false,
                'severity' => 'critical',
                'summary' => __('WPSN editor UI markers detected in anonymous HTML. Check page caching.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('Frontend output looks clean (no editor UI leaked).', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * Basic loopback probes for common public pages.
     *
     * This catches obvious runtime failures (5xx, fatal HTML) on:
     * - Home
     * - One published post/page (if available)
     * - One category term (if available)
     * - One WooCommerce product (if available)
     *
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkPagesProbe(): array
    {
        $tests = [];
        $tests[] = ['label' => 'home', 'url' => home_url('/')];

        // One published post/page.
        $ids = get_posts([
            'post_type' => ['post', 'page'],
            'post_status' => 'publish',
            'posts_per_page' => 1,
            'orderby' => 'date',
            'order' => 'DESC',
            'fields' => 'ids',
        ]);
        if (is_array($ids) && !empty($ids[0])) {
            $tests[] = ['label' => 'single', 'url' => get_permalink((int) $ids[0])];
        }

        // One category.
        $terms = get_terms([
            'taxonomy' => 'category',
            'hide_empty' => false,
            'number' => 1,
        ]);
        if (is_array($terms) && !empty($terms[0]) && $terms[0] instanceof \WP_Term) {
            $link = get_term_link($terms[0]);
            if (!is_wp_error($link)) {
                $tests[] = ['label' => 'category', 'url' => (string) $link];
            }
        }

        // One product (WooCommerce).
        if (function_exists('wc_get_products')) {
            try {
                $pids = wc_get_products([
                    'status' => 'publish',
                    'limit' => 1,
                    'return' => 'ids',
                ]);
                if (is_array($pids) && !empty($pids[0])) {
                    $tests[] = ['label' => 'product', 'url' => get_permalink((int) $pids[0])];
                }
            } catch (\Throwable $e) {
                // Ignore.
            }
        }

        $details = [
            'tests' => [],
            'count' => count($tests),
        ];

        $failed = 0;
        $hardFailed = 0;

        foreach ($tests as $t) {
            $probe = $this->fetchProbe('page:' . (string) $t['label'], (string) $t['url'], 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8');
            $details['tests'][] = $probe;

            if (empty($probe['ok'])) {
                $failed++;
                $code = isset($probe['code']) ? (int) $probe['code'] : 0;
                if ($code >= 500 || $code === 0) {
                    $hardFailed++;
                }
            }
        }

        if ($failed === 0) {
            return [
                'ok' => true,
                'severity' => 'ok',
                'summary' => sprintf(__('Public page probes OK (%d/%d).', 'wp-seo-next'), count($tests), count($tests)),
                'details' => $details,
            ];
        }

        $severity = $hardFailed > 0 ? 'critical' : 'warn';
        $summary = sprintf(
            /* translators: 1: failed count 2: total */
            __('Public page probes failed (%1$d/%2$d).', 'wp-seo-next'),
            $failed,
            max(1, count($tests))
        );

        return [
            'ok' => false,
            'severity' => $severity,
            'summary' => $summary,
            'details' => $details,
        ];
    }

    /**
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkQueue(): array
    {
        /** @var \WPSN\Core\Queue\QueueInterface $queue */
        $queue = $this->container->get('queue');

        $mode = (string) $this->settings->get('performance.queue.mode', 'manual');
        $counts = [];
        try {
            $counts = $queue->counts();
        } catch (\Throwable $e) {
            $counts = ['pending' => 0, 'running' => 0, 'done' => 0, 'failed' => 0];
        }

        $pending = (int) ($counts['pending'] ?? 0);
        $failed = (int) ($counts['failed'] ?? 0);
        $nextTick = wp_next_scheduled('wpsn_queue_tick');
        $wpCronDisabled = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;

        $paused = QueuePause::isPaused();
        $pauseUntil = QueuePause::pauseUntil();
        $pauseReason = QueuePause::reason();

        $details = [
            'mode' => $mode,
            'counts' => $counts,
            'next_tick_utc' => $nextTick ? gmdate('c', (int) $nextTick) : null,
            'wp_cron_disabled' => $wpCronDisabled,
            'paused' => $paused,
            'pause_until_utc' => ($pauseUntil > 0) ? gmdate('c', $pauseUntil) : null,
            'pause_reason' => $pauseReason !== '' ? $pauseReason : null,
        ];

        if ($paused) {
            $untilLabel = ($pauseUntil > 0) ? gmdate('Y-m-d H:i:s', $pauseUntil) . ' UTC' : __('unknown', 'wp-seo-next');

            $sev = 'info';
            if ($pending > 0 || $mode === 'auto') {
                $sev = 'warn';
            }

            $summary = $pending > 0
                ? sprintf(
                    /* translators: 1: pending jobs count, 2: UTC datetime */
                    __('Queue is paused (pending: %1$d) until %2$s.', 'wp-seo-next'),
                    $pending,
                    $untilLabel
                )
                : sprintf(
                    /* translators: %s: UTC datetime */
                    __('Queue is paused until %s.', 'wp-seo-next'),
                    $untilLabel
                );

            return [
                'ok' => $sev !== 'warn',
                'severity' => $sev,
                'summary' => $summary,
                'details' => $details,
            ];
        }

        if ($failed > 0) {
            return [
                'ok' => false,
                'severity' => $failed >= 20 ? 'critical' : 'warn',
                'summary' => sprintf(__('Queue has failed jobs: %d', 'wp-seo-next'), $failed),
                'details' => $details,
            ];
        }

        if ($pending > 0 && $mode === 'manual') {
            return [
                'ok' => false,
                'severity' => 'warn',
                'summary' => sprintf(__('Queue has pending jobs (%d) but mode is Manual.', 'wp-seo-next'), $pending),
                'details' => $details,
            ];
        }

        if ($mode === 'auto') {
            if ($wpCronDisabled) {
                return [
                    'ok' => false,
                    'severity' => 'warn',
                    'summary' => __('Queue is Auto, but DISABLE_WP_CRON is enabled. Ensure a real cron hits wp-cron.php.', 'wp-seo-next'),
                    'details' => $details,
                ];
            }
            if (!$nextTick) {
                return [
                    'ok' => false,
                    'severity' => 'warn',
                    'summary' => __('Queue is Auto, but no next tick is scheduled.', 'wp-seo-next'),
                    'details' => $details,
                ];
            }
        }

        return [
            'ok' => true,
            'severity' => 'ok',
            'summary' => __('Queue looks healthy.', 'wp-seo-next'),
            'details' => $details,
        ];
    }

    /**
     * Detect recent fatal errors originating from WP SEO Next.
     *
     * This uses the local ErrorTracker ring buffer (wp_options) and does not depend on server logs.
     *
     * @return array{ok:bool,severity:string,summary:string,details:array<string,mixed>}
     */
    public function checkFatalErrors(): array
    {
        $log = [];
        try {
            $log = ErrorTracker::getLog();
        } catch (\Throwable $e) {
            $log = [];
        }

        $now = time();
        $cutoff = $now - (7 * DAY_IN_SECONDS);

        $recent = [];
        foreach ($log as $row) {
            if (!is_array($row)) {
                continue;
            }

            $ts = isset($row['ts']) ? (int) $row['ts'] : 0;
            if ($ts <= 0 || $ts < $cutoff) {
                continue;
            }

            $recent[] = $row;
            if (count($recent) >= 5) {
                break;
            }
        }

        $currentVersion = defined('WPSN_VERSION') ? (string) WPSN_VERSION : '';
        $recentCurrentVersion = [];
        $recentHistorical = [];
        foreach ($recent as $row) {
            $rowVersion = isset($row['plugin_version']) ? trim((string) $row['plugin_version']) : '';
            if ($currentVersion !== '' && $rowVersion !== '' && $rowVersion === $currentVersion) {
                $recentCurrentVersion[] = $row;
            } else {
                $recentHistorical[] = $row;
            }
        }

        $count = count($recent);
        $currentCount = count($recentCurrentVersion);
        $historicalCount = count($recentHistorical);
        $historicalOnly = ($count > 0 && $currentCount === 0);
        $details = [
            'count_7d' => $count,
            'recent' => $recent,
            'recent_current' => $recentCurrentVersion,
            'recent_historical' => $recentHistorical,
            'current_version' => $currentVersion,
            'current_version_count' => $currentCount,
            'historical_count' => $historicalCount,
            'historical_only' => $historicalOnly,
        ];

        if ($count === 0) {
            return [
                'ok' => true,
                'severity' => 'ok',
                'summary' => __('No recent plugin fatal errors detected.', 'wp-seo-next'),
                'details' => $details,
            ];
        }

        if ($historicalOnly) {
            $summary = sprintf(
                /* translators: %d: count */
                __('Historical plugin fatal errors were recorded on an older WP SEO Next version (last 7 days): %d', 'wp-seo-next'),
                $historicalCount
            );

            return [
                'ok' => true,
                'severity' => 'info',
                'summary' => $summary,
                'details' => $details,
            ];
        }

        $last = $recentCurrentVersion[0];
        $lastTs = isset($last['ts']) ? (int) $last['ts'] : 0;
        $age = $lastTs > 0 ? max(0, $now - $lastTs) : 999999;

        $severity = $age <= DAY_IN_SECONDS ? 'critical' : 'warn';

        $summary = sprintf(
            /* translators: %d: count */
            __('Recent plugin fatal errors detected (last 7 days): %d', 'wp-seo-next'),
            $currentCount
        );

        return [
            'ok' => false,
            'severity' => $severity,
            'summary' => $summary,
            'details' => $details,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function fetchProbe(string $label, string $url, string $accept): array
    {
        $resp = HttpClient::get($url, [
            'timeout' => 8,
            'redirection' => 2,
            'headers' => [
                'Accept' => $accept,
                'X-WPSN-Debug' => 'quality_gate',
            ],
            'user-agent' => 'WPSN/' . (defined('WPSN_VERSION') ? WPSN_VERSION : 'dev') . '; ' . home_url('/'),
        ], [
            'service' => 'loopback',
            'context' => 'quality_gate_probe',
            'retries' => 0,
        ]);

        if (is_wp_error($resp)) {
            return [
                'label' => $label,
                'url' => $url,
                'ok' => false,
                'code' => 0,
                'content_type' => '',
                'len' => 0,
                'snip' => '',
                'error' => $resp->get_error_message(),
                'body_raw' => '',
            ];
        }

        $code = (int) wp_remote_retrieve_response_code($resp);
        $headers = wp_remote_retrieve_headers($resp);
        $ct = '';
        if (is_array($headers) && isset($headers['content-type'])) {
            $ct = (string) $headers['content-type'];
        } elseif (is_object($headers) && method_exists($headers, 'get')) {
            $ct = (string) $headers->get('content-type');
        }

        $body = (string) wp_remote_retrieve_body($resp);
        $len = strlen($body);
        $snip = substr($body, 0, 220);
        $snip = preg_replace('/\s+/', ' ', (string) $snip);

        $ok = $code >= 200 && $code < 300;

        return [
            'label' => $label,
            'url' => $url,
            'ok' => $ok,
            'code' => $code,
            'content_type' => $ct,
            'len' => $len,
            'snip' => $snip,
            'error' => '',
            'body_raw' => substr($body, 0, 4000),
        ];
    }

    private function sitemapRewriteRulesPresent(): bool
    {
        $rules = get_option('rewrite_rules');
        if (!is_array($rules) || $rules === []) {
            return false;
        }

        $needles = [
            '^sitemap\\.xml$',
            '^sitemap_index\\.xml$',
            '^([a-z0-9_-]+)-sitemap\\.xml$',
            '^([a-z0-9_-]+)-sitemap([0-9]+)\\.xml$',
        ];
        foreach ($needles as $n) {
            if (array_key_exists($n, $rules)) {
                return true;
            }
        }

        foreach ($rules as $regex => $_q) {
            $regex = (string) $regex;
            if (strpos($regex, 'sitemap\\.xml') !== false || strpos($regex, 'sitemap_index\\.xml') !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Persist the last global results.
     *
     * @param array<string,mixed> $results
     */
    public function saveGlobalResults(array $results): void
    {
        update_option(self::RESULTS_OPTION, $results, false);
    }

    /**
     * @return array<string,mixed>
     */
    public function getGlobalResults(): array
    {
        $res = get_option(self::RESULTS_OPTION, []);
        return is_array($res) ? $res : [];
    }

    public function clearGlobalResults(): void
    {
        delete_option(self::RESULTS_OPTION);
    }
}
