<?php

namespace WPSN\Admin;

use WPSN\Core\Container;
use WPSN\Core\Capabilities;
use WPSN\Core\CompatibilityDetector;
use WPSN\Core\UI;
use WPSN\Modules\Diagnostics\PlatformOpsAutomation;
use WPSN\Modules\Diagnostics\PlatformOpsCompactSummaryBuilder;
use WPSN\Modules\Diagnostics\ReleaseSmokeBadgeHtml;
use WPSN\Modules\Diagnostics\ReleaseSmokeCompactSummaryBuilder;
use WPSN\Modules\Licensing\PlanMatrix;
use WPSN\Modules\TasksHub\PinnedTaskQueuesRepository;
use WPSN\Modules\TasksHub\SetupTasksProvider;
use WPSN\Modules\TasksHub\TaskQueuesCatalog;
use WPSN\Modules\TasksHub\TaskViewsRepository;

defined('ABSPATH') || exit;

final class AdminMenu
{
    /**
     * Top-level admin menu slug (our own section in wp-admin).
     */
    private const TOP_MENU_SLUG = 'wpsn';

    private Container $container;

    /**
     * Map of current ?page=... to submenu slugs used under our top menu.
     *
     * @var array<string,string>
     */
    private array $submenuHighlight = [];

    /**
     * Cached list of WPSN Tools pages collected before we hide them from WP's Tools menu.
     *
     * We must cache because cleanupLegacyMenus() removes our pages from tools.php,
     * but the Hub still needs a complete list for navigation/search/quick actions.
     *
     * @var array<string,array<string,mixed>>
     */
    private array $cachedToolsPages = [];

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

    public function register(): void
    {
        // Register our own top-level menu AFTER modules register their Tools pages (we mirror them).
        // NOTE: Use a late priority to ensure all modules (even ones hooking admin_menu later)
        // have already registered their Tools pages.
        add_action('admin_menu', [$this, 'addMenu'], 900);

        // Hide WP SEO Next pages from WP's Tools/Settings menus to avoid clutter.
        // Cleanup should run very late to reliably remove any legacy Tools pages
        // registered by modules/plugins with unusual priorities.
        add_action('admin_menu', [$this, 'cleanupLegacyMenus'], 9999);

        // Keep our top-level menu highlighted when viewing legacy tools.php?page=wpsn-* screens.
        // Make highlighting robust even on legacy tools.php?page=wpsn-* pages.
        // Use a high priority to win against other plugins that may tweak parent/submenu.
        add_filter('parent_file', [$this, 'filterParentFile'], 9999);
        add_filter('submenu_file', [$this, 'filterSubmenuFile'], 9999, 2);
        // UI mode: hide advanced Tools pages in Simple mode (menu only; pages still exist).
        add_action('admin_menu', [$this, 'applyUiModeMenu'], 950);
        // Non-clickable group headings styling.
        add_action('admin_head', [$this, 'outputMenuHeadingStyles']);
        add_action('admin_notices', [$this, 'uiModeNotices']);
        // Global AJAX mode: hide optional live-mode blocks when disabled.
        add_action('admin_notices', [$this, 'ajaxModeNotices']);
        add_filter('admin_body_class', [$this, 'filterAjaxBodyClass'], 9999);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAjaxUiAssets'], 20);
        add_action('admin_post_wpsn_save_settings', [$this, 'handleSave']);
        add_action('admin_post_wpsn_switch_ui_mode', [$this, 'handleSwitchUiMode']);
        add_action('admin_post_wpsn_toggle_pin', [$this, 'handleTogglePin']);
        add_action('admin_post_wpsn_clear_ai_user_override', [$this, 'handleClearAIUserOverride']);
        add_action('admin_post_wpsn_clear_ai_overrides_all', [$this, 'handleClearAIOverridesAll']);
        add_action('admin_post_wpsn_enable_compatibility_mode', [$this, 'handleEnableCompatibilityMode']);
        add_action('wp_ajax_wpsn_cf_token_test', ['\\WPSN\\Admin\\CustomFieldsTokenTester', 'handle']);
        add_action('admin_post_wpsn_cf_token_test_post', ['\WPSN\Admin\CustomFieldsTokenTester', 'handlePost']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);

        // Tools → Tokens & Variables (TemplateEngine helper).
        (new TokensVariablesAdmin($this->container))->register();

        // Tools → Sitemaps (help users quickly find /sitemap.xml and fix 404s by flushing rewrites).
        (new \WPSN\Modules\Sitemap\Admin\SitemapAdmin($this->container))->register();

        // Tools → Robots (central place to manage /robots.txt + presets).
        (new \WPSN\Modules\Robots\Admin\RobotsAdmin($this->container))->register();

        // Tools → URL Rules (noindex rules applied via wp_robots).
        (new \WPSN\Modules\Robots\Admin\UrlRulesAdmin($this->container))->register();

        // Tools → Connections (setup hub for integrations).
        (new ConnectionsAdmin($this->container))->register();
    }

    public function addMenu(): void
    {
        // White label support (agency mode).
        $settings = $this->container->get('settings');

        $wlEnabled = (bool) $settings->get('white_label.enabled', false);
        $hideBranding = (bool) $settings->get('white_label.hide_branding', false);

        $brand = $wlEnabled ? trim((string) $settings->get('white_label.brand_name', '')) : '';
        if ($brand === '') {
            // If white-label is enabled but brand_name is empty,
            // avoid leaking vendor name when hide_branding is on.
            $brand = ($wlEnabled && $hideBranding) ? __('SEO', 'wp-seo-next') : 'WP SEO Next';
        }

        $menuTitle = $wlEnabled ? trim((string) $settings->get('white_label.menu_title', '')) : '';
        if ($menuTitle === '') {
            $menuTitle = $brand;
        }

        $pageTitle = $menuTitle;

        if (!$wlEnabled) {
            $pageTitle = __('WP SEO Next', 'wp-seo-next');
            $menuTitle = __('WP SEO Next', 'wp-seo-next');
        }

        // Top-level WP SEO Next menu (plugin section).
        // We mirror all our legacy Tools pages under this menu to keep wp-admin clean.
        $this->submenuHighlight = [];

        // Default position: show WP SEO Next higher than Tools/Settings.
        // (58 is a common slot used by many plugins to appear before Appearance.)
        // You can override via the `wpsn/admin_menu/position` filter.
        $position = (int) apply_filters('wpsn/admin_menu/position', 58);

        add_menu_page(
            $pageTitle,
            $menuTitle,
            Capabilities::CAP_TOOLS,
            self::TOP_MENU_SLUG,
            [$this, 'renderHub'],
            'dashicons-chart-area',
            $position
        );

        // Settings (inside our own section).
        add_submenu_page(
            self::TOP_MENU_SLUG,
            __('Settings', 'wp-seo-next'),
            __('Settings', 'wp-seo-next'),
            Capabilities::CAP_SETTINGS,
            'wp-seo-next',
            [$this, 'renderPage']
        );

        // Quick UI-mode switch (Simple <-> Pro). This prevents the common confusion
        // when advanced tools are hidden in Simple mode.
        $uiMode = UI::mode($settings);
        $switchTo = ($uiMode === UI::MODE_SIMPLE) ? UI::MODE_PRO : UI::MODE_SIMPLE;
        $switchLabel = ($switchTo === UI::MODE_PRO)
            ? __('Switch to Pro UI', 'wp-seo-next')
            : __('Switch to Simple UI', 'wp-seo-next');

        $switchUrl = wp_nonce_url(
            admin_url('admin-post.php?action=wpsn_switch_ui_mode&mode=' . rawurlencode($switchTo)),
            'wpsn_switch_ui_mode'
        );
        global $submenu;
        if (!isset($submenu[self::TOP_MENU_SLUG]) || !is_array($submenu[self::TOP_MENU_SLUG])) {
            $submenu[self::TOP_MENU_SLUG] = [];
        }
        $submenu[self::TOP_MENU_SLUG][] = [
            $switchLabel,
            Capabilities::CAP_SETTINGS,
            $switchUrl,
        ];

        $this->submenuHighlight[self::TOP_MENU_SLUG] = self::TOP_MENU_SLUG;
        $this->submenuHighlight['wp-seo-next'] = 'wp-seo-next';

        // Group tools pages under our menu to avoid a flat "kasha-malasha" list.
        $toolsPages = $this->collectToolsPages();
        $toolsPages = $this->injectDisabledPlaceholders($toolsPages);
        // Cache for the Hub page (renderHub) because we hide these pages from Tools later.
        $this->cachedToolsPages = $toolsPages;
        $grouped = $this->groupToolsPages($toolsPages);

        foreach ($grouped as $groupId => $pages) {
            if (empty($pages)) {
                continue;
            }

            $label = $this->groupLabel($groupId);
            // Group heading (non-clickable label). If somehow clicked (e.g. CSS off),
            // it will redirect to the hub anchor.
            $headingSlug = 'wpsn-group-' . $groupId;
            add_submenu_page(
                self::TOP_MENU_SLUG,
                $label,
                '— ' . $label . ' —',
                Capabilities::CAP_TOOLS,
                $headingSlug,
                [$this, 'renderGroupHeading']
            );
            $this->submenuHighlight[$headingSlug] = $headingSlug;

            foreach ($pages as $slug => $meta) {
                $isDisabled = !empty($meta['disabled']);
                if ($isDisabled) {
                    $rawTitle = wp_strip_all_tags((string) ($meta['title'] ?? $slug));
                    $menuTitle = '<span class="wpsn-menu-disabled">' . esc_html($rawTitle) . '</span>'
                        . ' <span class="wpsn-menu-badge">' . esc_html__('disabled', 'wp-seo-next') . '</span>';

                    $moduleId = isset($meta['module']) ? sanitize_key((string) $meta['module']) : '';
                    $targetUrl = 'admin.php?page=wp-seo-next';
                    if ($moduleId !== '') {
                        $targetUrl .= '&wpsn_focus_module=' . rawurlencode($moduleId);
                    }

                    add_submenu_page(
                        self::TOP_MENU_SLUG,
                        $rawTitle,
                        $menuTitle,
                        Capabilities::CAP_SETTINGS,
                        $targetUrl,
                        '__return_null'
                    );
                    $this->submenuHighlight[$slug] = $targetUrl;
                    continue;
                }

                $menuSlug = 'tools.php?page=' . $slug;
                add_submenu_page(
                    self::TOP_MENU_SLUG,
                    $meta['title'],
                    $meta['title'],
                    $meta['cap'],
                    $menuSlug,
                    null
                );
                $this->submenuHighlight[$slug] = $menuSlug;
            }
        }

        // Legacy Settings → WP SEO Next (back-compat direct URL).
        add_options_page(
            $pageTitle,
            $menuTitle,
            Capabilities::CAP_SETTINGS,
            'wp-seo-next',
            [$this, 'renderPage']
        );
    }

    /**
     * Render handler for group heading pseudo-pages.
     *
     * These should be non-clickable in the menu. If clicked anyway,
     * redirect to the Hub section.
     */
    public function renderGroupHeading(): void
    {
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        $groupId = '';
        if (strpos($page, 'wpsn-group-') === 0) {
            $groupId = substr($page, strlen('wpsn-group-'));
            $groupId = sanitize_key($groupId);
        }

        $url = admin_url('admin.php?page=' . self::TOP_MENU_SLUG);
        if ($groupId !== '') {
            $url .= '#' . rawurlencode($groupId);
        }

        wp_safe_redirect($url);
        exit;
    }

    /**
     * Add "disabled" placeholders for important tools when their modules are disabled.
     *
     * This is intentionally minimal: we only list core plugin tools (no ads/upsell pages).
     *
     * @param array<string, array{title:string,cap:string}> $toolsPages
     * @return array<string, array<string,mixed>>
     */
    private function injectDisabledPlaceholders(array $toolsPages): array
    {
        $showPlaceholders = (bool) apply_filters('wpsn/admin/show_locked_module_placeholders', false, PlanMatrix::buildTier());
        if (!$showPlaceholders) {
            return $toolsPages;
        }

        /** @var \WPSN\Core\ModuleRegistry $modules */
        $modules = $this->container->get('modules');
        $buildTier = PlanMatrix::buildTier();

        // Core-focused catalog of tool slugs -> modules.
        $catalog = [
            // Monitoring
            'wpsn-search-console' => ['title' => __('Search Console', 'wp-seo-next'), 'module' => 'search_console'],
            'wpsn-drift-monitor' => ['title' => __('Drift Monitor', 'wp-seo-next'), 'module' => 'drift_monitor'],
            'wpsn-indexation-risk' => ['title' => __('Indexation Risk', 'wp-seo-next'), 'module' => 'indexation_risk'],
            // Unique crawl/content features
            'wpsn-content-clusters' => ['title' => __('Content Clusters', 'wp-seo-next'), 'module' => 'content_clusters'],
            'wpsn-crawl-budget' => ['title' => __('Crawl Budget', 'wp-seo-next'), 'module' => 'crawl_budget'],
            'wpsn-site-crawl' => ['title' => __('Site Crawl', 'wp-seo-next'), 'module' => 'site_crawl'],
            // Automation / analysis
            'wpsn-auto-pilot' => ['title' => __('SEO Auto-Pilot', 'wp-seo-next'), 'module' => 'autopilot'],
            'wpsn-serp-gap' => ['title' => __('SERP Gap', 'wp-seo-next'), 'module' => 'serp_gap'],
            // Trust / debugging
            'wpsn-activity-log' => ['title' => __('Activity Log', 'wp-seo-next'), 'module' => 'activity_log'],
            'wpsn-revisions' => ['title' => __('Revisions', 'wp-seo-next'), 'module' => 'revisions'],
            'wpsn-quality-gate' => ['title' => __('Quality Gate', 'wp-seo-next'), 'module' => 'diagnostics'],
        ];

        foreach ($catalog as $slug => $meta) {
            if (isset($toolsPages[$slug])) {
                continue;
            }
            $moduleId = isset($meta['module']) ? sanitize_key((string) $meta['module']) : '';
            if ($moduleId === '') {
                continue;
            }
            if (!PlanMatrix::buildAllowsModule($moduleId, $buildTier)) {
                continue;
            }
            if ($modules->isEnabled($moduleId)) {
                continue;
            }

            $toolsPages[$slug] = [
                'title' => (string) $meta['title'],
                'cap' => Capabilities::CAP_SETTINGS,
                'disabled' => true,
                'module' => $moduleId,
            ];
        }

        return $toolsPages;
    }

    /**
     * Make our group headings look like headings and not links.
     */
    public function outputMenuHeadingStyles(): void
    {
        // Admin only.
        if (!is_admin()) {
            return;
        }

        // Only on users who can see our menu.
        if (!Capabilities::canManageTools()) {
            return;
        }

        echo '<style>';
        // Disable clicks on heading rows.
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a[href*="page=wpsn-group-"]{pointer-events:none;cursor:default;color:#a7aaad;font-weight:600;}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a[href*="page=wpsn-group-"]:hover{background:transparent;}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a[href*="page=wpsn-group-"]:focus{box-shadow:none;}';
        // Disabled placeholders.
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu .wpsn-menu-disabled{opacity:.65;}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu .wpsn-menu-badge{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:10px;font-size:11px;line-height:1.6;opacity:.75;border:1px solid rgba(255,255,255,.18);}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a.wpsn-current-tool{position:relative;overflow:hidden;background:#2271b1;color:#fff;font-weight:600;box-shadow:inset 4px 0 0 #72aee6;}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a.wpsn-current-tool:hover{background:#2271b1;color:#fff;}';
        echo '#adminmenu .toplevel_page_' . esc_html(self::TOP_MENU_SLUG) . ' .wp-submenu a.wpsn-current-tool:after{content:"";position:absolute;top:0;bottom:0;left:-70%;width:48%;pointer-events:none;background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.42),rgba(255,255,255,0));transform:skewX(-18deg);animation:wpsnCurrentMenuSheen 2.8s ease-in-out infinite;}';
        echo '@keyframes wpsnCurrentMenuSheen{0%{left:-70%;}42%,100%{left:125%;}}';
        echo '#adminmenuwrap.wpsn-menu-scroll-managed{max-height:calc(100vh - 32px);overflow-y:auto;overscroll-behavior:contain;}';
        echo '</style>';

        // Fallback: on legacy tools.php?page=wpsn-* screens some environments may still highlight Tools.
        // Force WP SEO Next menu highlight to reduce confusion.
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if ($page === '' || ($page !== 'wp-seo-next' && $page !== self::TOP_MENU_SLUG && strpos($page, 'wpsn-') !== 0)) {
            return;
        }

        echo '<script>window.WPSN_ADMIN_MENU_CURRENT=' . wp_json_encode($page) . ';</script>';
        echo <<<'HTML'
<script>
(function(){
    function pageFromHref(href) {
        if (!href) {
            return "";
        }
        try {
            return (new URL(href, window.location.href)).searchParams.get("page") || "";
        } catch (e) {
            var match = href.match(/[?&]page=([^&#]+)/);
            return match ? decodeURIComponent(match[1]) : "";
        }
    }

    function isWpsnPage(page) {
        return page === "wp-seo-next" || page.indexOf("wpsn-") === 0;
    }

    function findCurrentLink(links, page) {
        for (var i = 0; i < links.length; i++) {
            if (pageFromHref(links[i].getAttribute("href") || "") === page) {
                return links[i];
            }
        }
        return null;
    }

    function centerMenuItem(match) {
        var menuWrap = document.getElementById("adminmenuwrap");
        if (!menuWrap) {
            return;
        }
        if (menuWrap.classList) {
            menuWrap.classList.add("wpsn-menu-scroll-managed");
        }
        var matchRect = match.getBoundingClientRect();
        var menuRect = menuWrap.getBoundingClientRect();
        var above = matchRect.top < menuRect.top + 24;
        var below = matchRect.bottom > menuRect.bottom - 24;
        if (!above && !below) {
            return;
        }
        var delta = matchRect.top - menuRect.top - (menuWrap.clientHeight / 2) + (matchRect.height / 2);
        menuWrap.scrollTop += delta;
    }

    document.addEventListener("DOMContentLoaded", function(){
        try {
            var page = window.WPSN_ADMIN_MENU_CURRENT || "";
            var top = document.getElementById("toplevel_page_wpsn");
            if (!top || !isWpsnPage(page)) {
                return;
            }

            var current = document.querySelector("#adminmenu li.wp-has-current-submenu");
            if (current && current !== top) {
                current.classList.remove("wp-has-current-submenu", "wp-menu-open", "current");
            }
            top.classList.add("wp-has-current-submenu", "wp-menu-open", "current");

            var match = findCurrentLink(top.querySelectorAll(".wp-submenu a"), page);
            if (!match) {
                return;
            }
            match.classList.add("current", "wpsn-current-tool");
            centerMenuItem(match);
        } catch (e) {}
    });
})();
</script>
HTML;
    }



    public function renderPage(): void
    {
        (new SettingsPage($this->container))->render();
    }

    public function renderHub(): void
    {
        if (!Capabilities::canManageTools()) {
            wp_die(esc_html__('You do not have permission to access this page.', 'wp-seo-next'));
        }

        $settingsUrl = admin_url('admin.php?page=wp-seo-next');

        $settings = $this->container->get('settings');

        // Compatibility snapshot (shown only inside our Dashboard; no global nags).
        $seoPlugins = CompatibilityDetector::detectSeoPlugins();
        $detectedNames = [];
        $hasConflict = false;
        foreach ($seoPlugins as $row) {
            if (!is_array($row) || empty($row['detected'])) {
                continue;
            }
            $hasConflict = true;
            $detectedNames[] = isset($row['name']) ? (string) $row['name'] : '';
        }
        $detectedNames = array_values(array_filter(array_map('trim', $detectedNames)));

        $disableOutputStored = (bool) $settings->get('compatibility.disable_output', false);
        $autoDisableOnConflict = (bool) $settings->get('compatibility.auto_disable_output_on_conflict', false);
        $disableOutputConst = defined('WPSN_DISABLE_OUTPUT') && (bool) WPSN_DISABLE_OUTPUT;
        $disableOutputEffective = $disableOutputConst || $disableOutputStored || ($autoDisableOnConflict && $hasConflict);

        $toolsPages = !empty($this->cachedToolsPages) ? $this->cachedToolsPages : $this->collectToolsPages();

        // Respect UI mode on the Hub page too (not only the left menu).
        // This keeps the Dashboard clean and avoids extra clutter for most users.
        if (UI::isSimple($settings)) {
            $hide = array_fill_keys(UI::proOnlyToolsPages(), true);
            $toolsPages = array_filter(
                $toolsPages,
                static function ($meta, $slug) use ($hide) {
                    return !isset($hide[(string) $slug]);
                },
                ARRAY_FILTER_USE_BOTH
            );
        }

        $grouped = $this->groupToolsPages($toolsPages);
        $pinned = $this->getPinnedPages();

        // Ensure Settings can be pinned too.
        $pinCandidates = array_merge(['wp-seo-next'], array_keys($toolsPages));

        $search = isset($_GET['wpsn_search']) ? sanitize_text_field((string) wp_unslash($_GET['wpsn_search'])) : '';

        echo '<div class="wrap">';
        echo '<h1>' . esc_html__('WP SEO Next', 'wp-seo-next') . '</h1>';
        echo '<p class="description">' . esc_html__('Dashboard + grouped navigation for all WP SEO Next features. Nothing is scattered under Tools.', 'wp-seo-next') . '</p>';
        echo '<style>.wpsn-release-badges{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 0 0}.wpsn-release-badge{display:inline-block;padding:3px 10px;border-radius:999px;background:#f0f0f1;font-size:12px;line-height:1.2}.wpsn-release-badge.ok{background:#d1e7dd}.wpsn-release-badge.warn{background:#fff3cd}.wpsn-release-badge.bad{background:#f8d7da}.wpsn-release-badge.info{background:#cff4fc}</style>';

        // Lightweight notices (only on our hub).
        $notice = isset($_GET['wpsn_notice']) ? sanitize_key((string) wp_unslash($_GET['wpsn_notice'])) : '';
        if ($notice === 'compat_enabled') {
            echo '<div class="notice notice-success"><p>' . esc_html__('Compatibility mode enabled: WP SEO Next frontend output modules are now disabled.', 'wp-seo-next') . '</p></div>';
        }

        if ($notice === 'setup_task_done') {
            echo '<div class="notice notice-success"><p>' . esc_html__('Setup task marked as done.', 'wp-seo-next') . '</p></div>';
        } elseif ($notice === 'setup_task_hidden') {
            echo '<div class="notice notice-warning"><p>' . esc_html__('Setup task hidden from the open list.', 'wp-seo-next') . '</p></div>';
        } elseif ($notice === 'setup_task_reopened') {
            echo '<div class="notice notice-info"><p>' . esc_html__('Setup task reopened.', 'wp-seo-next') . '</p></div>';
        }

        if ($hasConflict && !$disableOutputEffective) {
            $names = empty($detectedNames) ? __('another SEO plugin', 'wp-seo-next') : implode(', ', $detectedNames);
            $enableUrl = add_query_arg([
                'action' => 'wpsn_enable_compatibility_mode',
            ], admin_url('admin-post.php'));
            $enableUrl = wp_nonce_url($enableUrl, 'wpsn_enable_compatibility_mode');

            echo '<div class="notice notice-warning">';
            echo '<p><strong>' . esc_html__('Potential SEO conflict detected.', 'wp-seo-next') . '</strong> ' . esc_html(sprintf(__('Detected: %s. To avoid duplicate title/meta/schema output, enable Compatibility mode.', 'wp-seo-next'), $names)) . '</p>';
            echo '<p style="margin:8px 0 0 0;">';
            echo '<a class="button button-primary" href="' . esc_url($enableUrl) . '">' . esc_html__('Enable Compatibility mode', 'wp-seo-next') . '</a> ';
            echo '<a class="button button-secondary" href="' . esc_url(admin_url('tools.php?page=wpsn-diagnostics')) . '">' . esc_html__('Open Compatibility & Diagnostics', 'wp-seo-next') . '</a>';
            echo '</p>';
            echo '</div>';
        }

        // Top bar: version + search.
        echo '<div style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin:10px 0 14px 0;">';
        if (defined('WPSN_VERSION')) {
            echo '<div><code>' . esc_html__('Version:', 'wp-seo-next') . ' ' . esc_html((string) WPSN_VERSION) . '</code></div>';
        }
        echo '<form method="get" action="" style="margin:0;">';
        echo '<input type="hidden" name="page" value="' . esc_attr(self::TOP_MENU_SLUG) . '" />';
        echo '<input type="search" name="wpsn_search" value="' . esc_attr($search) . '" placeholder="' . esc_attr__('Search tools…', 'wp-seo-next') . '" style="min-width:260px;" />';
        echo '<button class="button button-secondary" type="submit" style="margin-left:6px;">' . esc_html__('Search', 'wp-seo-next') . '</button>';
        if ($search !== '') {
            echo '<a class="button button-link" href="' . esc_url(admin_url('admin.php?page=' . self::TOP_MENU_SLUG)) . '">' . esc_html__('Clear', 'wp-seo-next') . '</a>';
        }
        echo '</form>';
        echo '</div>';

        // Quick actions.
        echo '<h2 style="margin-top:10px;">' . esc_html__('Quick actions', 'wp-seo-next') . '</h2>';
        echo '<div style="display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 0 0;">';
        echo '<a class="button button-primary" href="' . esc_url($settingsUrl) . '">' . esc_html__('Settings', 'wp-seo-next') . '</a>';
        $quick = [
            'wpsn-setup-wizard' => __('Setup Wizard', 'wp-seo-next'),
            'wpsn-tasks' => __('Tasks', 'wp-seo-next'),
            'wpsn-notifications' => __('Notifications', 'wp-seo-next'),
            'wpsn-site-crawl' => __('Run Site Crawl', 'wp-seo-next'),
            'wpsn-issues' => __('Issues', 'wp-seo-next'),
            'wpsn-audit' => __('Run Audit', 'wp-seo-next'),
            'wpsn-auto-pilot' => __('AutoPilot', 'wp-seo-next'),
            'wpsn-search-console' => __('Search Console', 'wp-seo-next'),
            'wpsn-robots' => __('Robots', 'wp-seo-next'),
            'wpsn-sitemaps' => __('Sitemaps', 'wp-seo-next'),
            'wpsn-quality-gate' => __('Quality Gate', 'wp-seo-next'),
            'wpsn-redirects' => __('Redirects', 'wp-seo-next'),
            'wpsn-import-export' => __('Import/Export', 'wp-seo-next'),
            'wpsn-diagnostics' => __('Compatibility', 'wp-seo-next'),
        ];
        foreach ($quick as $slug => $label) {
            if (!isset($toolsPages[$slug])) {
                continue;
            }
            echo '<a class="button button-secondary" href="' . esc_url(admin_url('tools.php?page=' . $slug)) . '">' . esc_html($label) . '</a>';
        }
        echo '</div>';

        $this->renderDashboardPinnedTaskQueues($toolsPages);
        $this->renderDashboardSavedTaskViews($toolsPages);

        // Overview cards.
        $overview = $this->getDashboardOverview($toolsPages);
        echo '<h2 style="margin-top:18px;">' . esc_html__('Overview', 'wp-seo-next') . '</h2>';
        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:12px; max-width:1100px; margin-top:10px;">';
        foreach ($overview as $card) {
            $title = isset($card['title']) ? (string) $card['title'] : '';
            $value = isset($card['value']) ? (string) $card['value'] : '';
            $hint = isset($card['hint']) ? (string) $card['hint'] : '';
            $url = isset($card['url']) ? (string) $card['url'] : '';

            echo '<div class="card" style="margin:0;">';
            echo '<h3 style="margin:0 0 6px 0;">' . esc_html($title) . '</h3>';
            echo '<div style="font-size:22px; font-weight:600; line-height:1.2;">' . esc_html($value) . '</div>';
            if ($hint !== '') {
                echo '<p class="description" style="margin:6px 0 0 0;">' . esc_html($hint) . '</p>';
            }
            if (!empty($card['badges']) && is_array($card['badges'])) {
                echo ReleaseSmokeBadgeHtml::render($card['badges']);
            }
            if ($url !== '') {
                echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url($url) . '">' . esc_html__('Open', 'wp-seo-next') . '</a></p>';
            }
            echo '</div>';
        }
        echo '</div>';

        // Tasks preview.
        echo '<h2 style="margin-top:22px;">' . esc_html__('Open items', 'wp-seo-next') . '</h2>';
        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(360px, 1fr)); gap:12px; max-width:1200px;">';
        $this->renderDashboardSetupPreview($toolsPages);
        $this->renderDashboardNotificationsPreview($toolsPages);
        if (isset($toolsPages['wpsn-auto-pilot'])) {
            $this->renderDashboardAutoPilotPreview($toolsPages);
        }
        echo '</div>';

        // Pinned / Favorites.
        echo '<h2 style="margin-top:22px;">' . esc_html__('Pinned', 'wp-seo-next') . '</h2>';
        if (empty($pinned)) {
            echo '<p class="description">' . esc_html__('No pinned pages yet. Use the ★ icon next to a page to pin it here.', 'wp-seo-next') . '</p>';
        } else {
            echo '<div style="display:flex; flex-wrap:wrap; gap:10px; margin:10px 0 0 0;">';
            foreach ($pinned as $slug) {
                $info = $this->pageInfo($slug, $toolsPages);
                if ($info === null) {
                    continue;
                }
                echo '<a class="button button-secondary" style="margin:0;" href="' . esc_url($info['url']) . '">' . esc_html($info['label']) . '</a>';
            }
            echo '</div>';
        }

        echo '<hr style="margin:22px 0;" />';

        // Grouped navigation.
        foreach ($grouped as $groupId => $pages) {
            if (empty($pages)) {
                continue;
            }
            $label = $this->groupLabel($groupId);
            echo '<h2 id="' . esc_attr($groupId) . '" style="margin-top:22px;">' . esc_html($label) . '</h2>';
            echo '<table class="widefat striped" style="max-width:980px;">';
            echo '<tbody>';

            foreach ($pages as $slug => $meta) {
                if ($search !== '') {
                    $hay = strtolower($slug . ' ' . (string) ($meta['title'] ?? ''));
                    if (strpos($hay, strtolower($search)) === false) {
                        continue;
                    }
                }

                $info = $this->pageInfo($slug, $toolsPages);
                if ($info === null) {
                    continue;
                }

                $isPinned = in_array($slug, $pinned, true);
                $pinUrl = $this->pinUrl($slug);
                $pinLabel = $isPinned ? '★' : '☆';
                $pinTitle = $isPinned ? __('Unpin', 'wp-seo-next') : __('Pin', 'wp-seo-next');

                echo '<tr>';
                echo '<td style="width:32px; text-align:center;">';
                echo '<a href="' . esc_url($pinUrl) . '" title="' . esc_attr($pinTitle) . '" style="text-decoration:none; font-size:16px;">' . esc_html($pinLabel) . '</a>';
                echo '</td>';
                echo '<td>';
                echo '<a href="' . esc_url($info['url']) . '"><strong>' . esc_html($info['label']) . '</strong></a>';
                echo '</td>';
                echo '</tr>';
            }

            echo '</tbody>';
            echo '</table>';
        }

        echo '<hr style="margin:18px 0;" />';

        echo '<p class="description">' . esc_html__('Old bookmarks like tools.php?page=wpsn-… still work. The left menu is now grouped under WP SEO Next.', 'wp-seo-next') . '</p>';
        echo '</div>';
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardPinnedTaskQueues(array $toolsPages): void
    {
        if (!isset($toolsPages['wpsn-tasks'])) {
            return;
        }

        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $catalog = new TaskQueuesCatalog(new TaskViewsRepository(), $userId);
        $queues = (new PinnedTaskQueuesRepository($catalog, $userId))->all();
        if (empty($queues)) {
            return;
        }

        echo '<h2 style="margin-top:18px;">' . esc_html__('Pinned task queues', 'wp-seo-next') . '</h2>';
        echo '<div style="display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 0 0; align-items:center;">';
        foreach (array_slice($queues, 0, 6) as $queue) {
            if (!is_array($queue)) {
                continue;
            }
            $label = isset($queue['label']) ? (string) $queue['label'] : '';
            $url = isset($queue['url']) ? (string) $queue['url'] : '';
            if ($label === '' || $url === '') {
                continue;
            }
            echo '<a class="button button-secondary" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
        }
        echo '<a class="button button-link" href="' . esc_url(admin_url('tools.php?page=wpsn-tasks&tab=all')) . '">' . esc_html__('Manage pinned queues', 'wp-seo-next') . '</a>';
        echo '</div>';
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardSavedTaskViews(array $toolsPages): void
    {
        if (!isset($toolsPages['wpsn-tasks'])) {
            return;
        }

        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $catalog = new TaskQueuesCatalog(new TaskViewsRepository(), $userId);
        $queues = $catalog->savedQueues();
        if (empty($queues)) {
            return;
        }

        echo '<h2 style="margin-top:18px;">' . esc_html__('Saved task views', 'wp-seo-next') . '</h2>';
        echo '<div style="display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 0 0;">';
        foreach (array_slice($queues, 0, 6) as $queue) {
            if (!is_array($queue)) {
                continue;
            }
            $label = isset($queue['label']) ? (string) $queue['label'] : '';
            $url = isset($queue['url']) ? (string) $queue['url'] : '';
            if ($label === '' || $url === '') {
                continue;
            }
            echo '<a class="button button-secondary" href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
        }
        if (count($queues) > 6) {
            echo '<a class="button button-link" href="' . esc_url(admin_url('tools.php?page=wpsn-tasks&tab=all')) . '">' . esc_html__('Open all task views', 'wp-seo-next') . '</a>';
        }
        echo '</div>';
    }

    /**
     * @return array<int,array{title:string,value:string,hint:string,url:string}>
     */
    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     * @return array<int,array{title:string,value:string,hint:string,url:string}>
     */
    private function getDashboardOverview(array $toolsPages): array
    {
        $cards = [];

        if (isset($toolsPages['wpsn-setup-wizard']) || isset($toolsPages['wpsn-tasks'])) {
            $setup = $this->setupSummaryCard();
            if (!empty($setup)) {
                $cards[] = $setup;
            }
        }

        // Notifications.
        if (isset($toolsPages['wpsn-notifications'])) {
            $notifOpen = $this->countNotificationsOpen();
            $notifCrit = $this->countNotificationsOpenBySeverity('critical');
            $cards[] = [
                'title' => __('Notifications', 'wp-seo-next'),
                'value' => sprintf('%d open', $notifOpen),
                'hint' => $notifCrit > 0 ? sprintf(__('%d critical', 'wp-seo-next'), $notifCrit) : __('No critical alerts.', 'wp-seo-next'),
                'url' => admin_url('tools.php?page=wpsn-notifications'),
            ];
        }

        // AutoPilot.
        if (isset($toolsPages['wpsn-auto-pilot'])) {
            $apOpen = $this->countAutoPilotOpen();
            $cards[] = [
                'title' => __('AutoPilot tasks', 'wp-seo-next'),
                'value' => sprintf('%d open', $apOpen),
                'hint' => __('AI + rules suggestions you can review and apply.', 'wp-seo-next'),
                'url' => admin_url('tools.php?page=wpsn-auto-pilot'),
            ];
        }

        // Site crawl.
        if (isset($toolsPages['wpsn-site-crawl'])) {
            $crawl = $this->siteCrawlSummaryCard();
            if (!empty($crawl)) {
                $cards[] = $crawl;
            }
        }

        // Audit.
        if (isset($toolsPages['wpsn-audit'])) {
            $audit = $this->auditSummaryCard();
            if (!empty($audit)) {
                $cards[] = $audit;
            }
        }

        // Release smoke regression snapshot.
        if (isset($toolsPages['wpsn-quality-gate'])) {
            $release = $this->releaseSmokeSummaryCard();
            if (!empty($release)) {
                $cards[] = $release;
            }
        }

        if (isset($toolsPages['wpsn-platform-ops']) || isset($toolsPages['wpsn-diagnostics'])) {
            $platform = $this->platformOpsSummaryCard();
            if (!empty($platform)) {
                $cards[] = $platform;
            }
        }

        // Compatibility / conflicts.
        if (isset($toolsPages['wpsn-diagnostics'])) {
            $compat = $this->compatibilitySummaryCard();
            if (!empty($compat)) {
                $cards[] = $compat;
            }
        }

        return $cards;
    }

    /**
     * @return array{title:string,value:string,hint:string,url:string}
     */
    private function setupSummaryCard(): array
    {
        try {
            /** @var \WPSN\Core\Settings $settings */
            $settings = $this->container->get('settings');
            $provider = new SetupTasksProvider($settings);
            $progress = $provider->progress();

            $hint = sprintf(
                __('Done: %1$d/%2$d · Open: %3$d', 'wp-seo-next'),
                (int) ($progress['all_done'] ?? 0),
                (int) ($progress['all_total'] ?? 0),
                (int) ($progress['open_total'] ?? 0)
            );

            if ((int) ($progress['acknowledged_total'] ?? 0) > 0 || (int) ($progress['hidden_total'] ?? 0) > 0) {
                $hint .= ' · ' . sprintf(
                    __('Suppressed: %1$d done / %2$d hidden', 'wp-seo-next'),
                    (int) ($progress['acknowledged_total'] ?? 0),
                    (int) ($progress['hidden_total'] ?? 0)
                );
            }

            return [
                'title' => __('Setup checklist', 'wp-seo-next'),
                'value' => sprintf('%d open', (int) ($progress['open_total'] ?? 0)),
                'hint' => $hint,
                'url' => admin_url('tools.php?page=wpsn-tasks&tab=setup'),
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function countNotificationsOpen(): int
    {
        global $wpdb;
        $table = $wpdb->prefix . 'wpsn_notifications';
        $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if (!is_string($exists) || $exists !== $table) {
            return 0;
        }
        $now = gmdate('Y-m-d H:i:s');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $c = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE acknowledged_at IS NULL AND (muted_until IS NULL OR muted_until < %s)",
            $now
        ));
        return (int) $c;
    }

    private function countNotificationsOpenBySeverity(string $severity): int
    {
        global $wpdb;
        $table = $wpdb->prefix . 'wpsn_notifications';
        $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if (!is_string($exists) || $exists !== $table) {
            return 0;
        }
        $now = gmdate('Y-m-d H:i:s');
        $severity = sanitize_key($severity);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $c = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE severity = %s AND acknowledged_at IS NULL AND (muted_until IS NULL OR muted_until < %s)",
            $severity,
            $now
        ));
        return (int) $c;
    }

    private function countAutoPilotOpen(): int
    {
        global $wpdb;
        $table = $wpdb->prefix . 'wpsn_autopilot_tasks';
        $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if (!is_string($exists) || $exists !== $table) {
            return 0;
        }
        $now = time();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $c = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE status = 'open' AND (muted_until = 0 OR muted_until < %d)",
            $now
        ));
        return (int) $c;
    }

    /**
     * @return array{title:string,value:string,hint:string,url:string}
     */
    private function siteCrawlSummaryCard(): array
    {
        try {
            /** @var \WPSN\Core\ModuleRegistry $modules */
            $modules = $this->container->get('modules');
            if (!$modules->isEnabled('site_crawl')) {
                return [];
            }
        } catch (\Throwable $e) {
            // ignore
        }

        try {
            /** @var \WPSN\Core\Logger $logger */
            $logger = $this->container->get('logger');
            $repo = new \WPSN\Modules\SiteCrawl\SiteCrawlRepository($logger);
            $run = $repo->latestRun();
            if (!is_array($run) || empty($run['id'])) {
                return [
                    'title' => __('Site Crawl', 'wp-seo-next'),
                    'value' => __('No runs yet', 'wp-seo-next'),
                    'hint' => __('Run your first crawl to see errors, duplicates and URL patterns.', 'wp-seo-next'),
                    'url' => admin_url('tools.php?page=wpsn-site-crawl'),
                ];
            }

            $runId = (int) $run['id'];
            $status = isset($run['status']) ? (string) $run['status'] : '';
            $errors = $repo->countUrls($runId, ['filter' => 'errors']);
            $server = $repo->countUrls($runId, ['filter' => 'server_errors']);
            $cm = $repo->countUrls($runId, ['filter' => 'canonical_mismatch']);

            $hintParts = [];
            if ($server > 0) {
                $hintParts[] = sprintf(__('5xx: %d', 'wp-seo-next'), $server);
            }
            if ($errors > 0) {
                $hintParts[] = sprintf(__('Errors: %d', 'wp-seo-next'), $errors);
            }
            if ($cm > 0) {
                $hintParts[] = sprintf(__('Canonical mismatch: %d', 'wp-seo-next'), $cm);
            }

            $hint = !empty($hintParts) ? implode(' · ', $hintParts) : __('No major issues detected in the latest run.', 'wp-seo-next');
            $value = sprintf('#%d · %s', $runId, $status);

            return [
                'title' => __('Site Crawl', 'wp-seo-next'),
                'value' => $value,
                'hint' => $hint,
                'url' => admin_url('tools.php?page=wpsn-site-crawl&run_id=' . $runId),
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * @return array{title:string,value:string,hint:string,url:string}
     */
    private function auditSummaryCard(): array
    {
        try {
            /** @var \WPSN\Core\ModuleRegistry $modules */
            $modules = $this->container->get('modules');
            if (!$modules->isEnabled('audit')) {
                return [];
            }
        } catch (\Throwable $e) {
            // ignore
        }

        try {
            /** @var \WPSN\Core\Logger $logger */
            $logger = $this->container->get('logger');
            $repo = new \WPSN\Modules\Audit\AuditScanRepository($logger);
            $list = $repo->list(1, 1, []);
            $items = $list['items'] ?? [];
            $latest = !empty($items[0]) && is_array($items[0]) ? $items[0] : null;
            if (!$latest) {
                return [
                    'title' => __('Audit', 'wp-seo-next'),
                    'value' => __('No scans yet', 'wp-seo-next'),
                    'hint' => __('Run an audit to find missing meta, duplicates and indexation risks.', 'wp-seo-next'),
                    'url' => admin_url('tools.php?page=wpsn-audit'),
                ];
            }

            $scanId = isset($latest['id']) ? (int) $latest['id'] : 0;
            $status = isset($latest['status']) ? (string) $latest['status'] : '';
            $issues = isset($latest['issues_total']) ? (int) $latest['issues_total'] : 0;
            $value = $scanId > 0 ? sprintf('#%d · %s', $scanId, $status) : $status;
            $hint = $issues > 0 ? sprintf(__('Issues found: %d', 'wp-seo-next'), $issues) : __('No issues recorded in the latest scan.', 'wp-seo-next');

            return [
                'title' => __('Audit', 'wp-seo-next'),
                'value' => $value,
                'hint' => $hint,
                'url' => admin_url('tools.php?page=wpsn-audit'),
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function releaseSmokeSummaryCard(): array
    {
        try {
            $summary = (new ReleaseSmokeCompactSummaryBuilder())->build();
            $url = admin_url('tools.php?page=wpsn-quality-gate#wpsn-release-smoke');

            return [
                'title' => __('Release smoke', 'wp-seo-next'),
                'value' => isset($summary['value']) ? (string) $summary['value'] : __('Not run', 'wp-seo-next'),
                'hint' => isset($summary['hint']) ? (string) $summary['hint'] : '',
                'url' => $url,
                'badges' => isset($summary['badges']) && is_array($summary['badges']) ? $summary['badges'] : [],
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function platformOpsSummaryCard(): array
    {
        try {
            $summary = (new PlatformOpsCompactSummaryBuilder())->build();
            $automation = new PlatformOpsAutomation($this->container);
            $autoBadge = PlatformOpsAutomation::compactBadgeFromState([
                'mode' => $automation->automationMode(),
                'legacy_fallback' => $automation->legacyFallbackActive(),
                'wp_cron_disabled' => defined('DISABLE_WP_CRON') && DISABLE_WP_CRON,
            ]);
            $badges = isset($summary['badges']) && is_array($summary['badges']) ? $summary['badges'] : [];
            $badges[] = $autoBadge;
            $url = admin_url('tools.php?page=wpsn-platform-ops');

            return [
                'title' => __('Platform Ops', 'wp-seo-next'),
                'value' => isset($summary['value']) ? (string) $summary['value'] : __('Not run', 'wp-seo-next'),
                'hint' => isset($summary['hint']) ? (string) $summary['hint'] : '',
                'url' => $url,
                'badges' => $badges,
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * @return array{title:string,value:string,hint:string,url:string}
     */
    private function compatibilitySummaryCard(): array
    {
        try {
            $settings = $this->container->get('settings');

            $seoPlugins = CompatibilityDetector::detectSeoPlugins();
            $detectedNames = [];
            foreach ($seoPlugins as $row) {
                if (!is_array($row) || empty($row['detected'])) {
                    continue;
                }
                $name = isset($row['name']) ? (string) $row['name'] : '';
                if ($name !== '') {
                    $detectedNames[] = $name;
                }
            }

            $hasConflict = !empty($detectedNames);
            $autoDisableOnConflict = (bool) $settings->get('compatibility.auto_disable_output_on_conflict', false);
            $disableOutputStored = (bool) $settings->get('compatibility.disable_output', false);
            $disableOutputConst = defined('WPSN_DISABLE_OUTPUT') && (bool) WPSN_DISABLE_OUTPUT;
            $disableOutputEffective = $disableOutputConst || $disableOutputStored || ($autoDisableOnConflict && $hasConflict);

            $value = $disableOutputEffective
                ? __('Output disabled', 'wp-seo-next')
                : ($hasConflict ? __('Conflict detected', 'wp-seo-next') : __('Normal', 'wp-seo-next'));

            $hintParts = [];
            if ($hasConflict) {
                $hintParts[] = sprintf(__('Detected: %s', 'wp-seo-next'), implode(', ', $detectedNames));
            } else {
                $hintParts[] = __('No known SEO plugin conflicts detected.', 'wp-seo-next');
            }
            if ($disableOutputEffective) {
                if ($disableOutputConst) {
                    $hintParts[] = __('Forced by wp-config.', 'wp-seo-next');
                } elseif ($autoDisableOnConflict && $hasConflict && !$disableOutputStored) {
                    $hintParts[] = __('Auto safeguard (conflict).', 'wp-seo-next');
                } else {
                    $hintParts[] = __('Compatibility mode enabled.', 'wp-seo-next');
                }
            }

            return [
                'title' => __('Compatibility', 'wp-seo-next'),
                'value' => $value,
                'hint' => implode(' · ', $hintParts),
                'url' => admin_url('tools.php?page=wpsn-diagnostics'),
            ];
        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardNotificationsPreview(array $toolsPages): void
    {
        echo '<div class="card" style="margin:0;">';
        echo '<h3 style="margin:0 0 6px 0;">' . esc_html__('Notifications (open)', 'wp-seo-next') . '</h3>';

        $openPageUrl = admin_url('tools.php?page=wpsn-notifications');

        try {
            /** @var \WPSN\Core\ModuleRegistry $modules */
            $modules = $this->container->get('modules');
            if (!$modules->isEnabled('notifications')) {
                echo '<p class="description">' . esc_html__('Notifications module is disabled.', 'wp-seo-next') . '</p>';
                echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url(admin_url('admin.php?page=wp-seo-next#wpsn-modules')) . '">' . esc_html__('Open Settings', 'wp-seo-next') . '</a></p>';
                echo '</div>';
                return;
            }
        } catch (\Throwable $e) {
            // If modules registry is unavailable for any reason, still render the preview below.
        }

        try {
            $repo = new \WPSN\Modules\Notifications\NotificationsRepository();
            $rows = $repo->list(['view' => 'open'], 10, 0);
            if (empty($rows)) {
                echo '<p class="description">' . esc_html__('No open notifications.', 'wp-seo-next') . '</p>';
            } else {
                echo '<table class="widefat striped">';
                echo '<tbody>';
                foreach ($rows as $r) {
                    if (!is_array($r)) {
                        continue;
                    }
                    $title = isset($r['title']) ? (string) $r['title'] : '';
                    $sev = isset($r['severity']) ? strtoupper((string) $r['severity']) : '';
                    $url = isset($r['url']) ? (string) $r['url'] : '';
                    $created = isset($r['created_at']) ? (string) $r['created_at'] : '';
                    $openUrl = $url !== '' ? $url : admin_url('tools.php?page=wpsn-notifications');

                    echo '<tr>';
                    echo '<td style="width:70px;"><code>' . esc_html($sev) . '</code></td>';
                    echo '<td><a href="' . esc_url($openUrl) . '">' . esc_html($title !== '' ? $title : __('(no title)', 'wp-seo-next')) . '</a>';
                    if ($created !== '') {
                        echo '<div class="description" style="margin-top:4px;">' . esc_html($created) . '</div>';
                    }
                    echo '</td>';
                    echo '</tr>';
                }
                echo '</tbody>';
                echo '</table>';
            }

            echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url($openPageUrl) . '">' . esc_html__('Open Notifications', 'wp-seo-next') . '</a></p>';
        } catch (\Throwable $e) {
            echo '<p class="description">' . esc_html__('Notifications preview unavailable.', 'wp-seo-next') . '</p>';
        }

        echo '</div>';
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardSetupPreview(array $toolsPages): void
    {
        echo '<div class="card" style="margin:0;">';
        echo '<h3 style="margin:0 0 6px 0;">' . esc_html__('Setup checklist', 'wp-seo-next') . '</h3>';

        try {
            /** @var \WPSN\Core\Settings $settings */
            $settings = $this->container->get('settings');
            $provider = new SetupTasksProvider($settings);
            $progress = $provider->progress();
            $tasks = $provider->buildTasks(4);

            echo '<p class="description" style="margin-top:0;">' . esc_html(sprintf(
                __('Done: %1$d/%2$d · Essentials left: %3$d', 'wp-seo-next'),
                (int) ($progress['all_done'] ?? 0),
                (int) ($progress['all_total'] ?? 0),
                (int) ($progress['essentials_open'] ?? 0)
            )) . '</p>';

            if ((int) ($progress['acknowledged_total'] ?? 0) > 0 || (int) ($progress['hidden_total'] ?? 0) > 0) {
                echo '<p class="description" style="margin-top:6px;">' . esc_html(sprintf(
                    __('Suppressed reminders: %1$d marked done · %2$d hidden', 'wp-seo-next'),
                    (int) ($progress['acknowledged_total'] ?? 0),
                    (int) ($progress['hidden_total'] ?? 0)
                )) . '</p>';
            }

            if (empty($tasks)) {
                echo '<p class="description">' . esc_html__('No open setup tasks right now.', 'wp-seo-next') . '</p>';
            } else {
                echo '<table class="widefat striped">';
                echo '<tbody>';
                foreach ($tasks as $task) {
                    if (!is_array($task)) {
                        continue;
                    }

                    $title = isset($task['title']) ? (string) $task['title'] : '';
                    $summary = isset($task['summary']) ? (string) $task['summary'] : '';
                    $actions = isset($task['actions']) && is_array($task['actions']) ? $task['actions'] : [];

                    echo '<tr>';
                    echo '<td>';
                    echo '<strong>' . esc_html($title !== '' ? $title : __('Setup task', 'wp-seo-next')) . '</strong>';
                    if ($summary !== '') {
                        echo '<div class="description" style="margin-top:4px;">' . esc_html($summary) . '</div>';
                    }
                    if (!empty($actions)) {
                        echo '<div style="margin-top:6px;">';
                        foreach ($actions as $a) {
                            if (!is_array($a)) {
                                continue;
                            }
                            $label = isset($a['label']) ? (string) $a['label'] : '';
                            $url = isset($a['url']) ? (string) $a['url'] : '';
                            if ($label === '' || $url === '') {
                                continue;
                            }
                            echo '<a class="button button-small" href="' . esc_url($url) . '">' . esc_html($label) . '</a> ';
                        }
                        echo '</div>';
                    }
                    echo '</td>';
                    echo '</tr>';
                }
                echo '</tbody>';
                echo '</table>';
            }

            echo '<p style="margin:10px 0 0 0;">';
            echo '<a class="button button-secondary" href="' . esc_url(admin_url('tools.php?page=wpsn-tasks&tab=setup')) . '">' . esc_html__('Open Setup tasks', 'wp-seo-next') . '</a> ';
            echo '<a class="button button-secondary" href="' . esc_url(admin_url('tools.php?page=wpsn-setup-wizard&step=3')) . '">' . esc_html__('Open Setup Wizard', 'wp-seo-next') . '</a>';
            echo '</p>';
        } catch (\Throwable $e) {
            echo '<p class="description">' . esc_html__('Setup preview unavailable.', 'wp-seo-next') . '</p>';
        }

        echo '</div>';
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardAutoPilotPreview(array $toolsPages): void
    {
        echo '<div class="card" style="margin:0;">';
        echo '<h3 style="margin:0 0 6px 0;">' . esc_html__('AutoPilot tasks (open)', 'wp-seo-next') . '</h3>';

        if (!isset($toolsPages['wpsn-auto-pilot'])) {
            echo '<p class="description">' . esc_html__('AutoPilot module/page is not available.', 'wp-seo-next') . '</p>';
            echo '</div>';
            return;
        }

        try {
            /** @var \WPSN\Core\Logger $logger */
            $logger = $this->container->get('logger');
            $repo = new \WPSN\Modules\AutoPilot\AutoPilotRepository($logger);
            $list = $repo->list('open', '', '', 10, 1, false);
            $rows = isset($list['rows']) && is_array($list['rows']) ? $list['rows'] : [];

            if (empty($rows)) {
                echo '<p class="description">' . esc_html__('No open AutoPilot tasks.', 'wp-seo-next') . '</p>';
            } else {
                echo '<table class="widefat striped">';
                echo '<tbody>';
                foreach ($rows as $r) {
                    if (!is_array($r)) {
                        continue;
                    }
                    $sev = isset($r['severity']) ? strtoupper((string) $r['severity']) : '';
                    $summary = isset($r['summary']) ? (string) $r['summary'] : '';
                    $url = isset($r['object_url']) ? (string) $r['object_url'] : '';
                    $link = $url !== '' ? $url : admin_url('tools.php?page=wpsn-auto-pilot');
                    echo '<tr>';
                    echo '<td style="width:70px;"><code>' . esc_html($sev) . '</code></td>';
                    echo '<td><a href="' . esc_url($link) . '">' . esc_html($summary !== '' ? $summary : __('(no summary)', 'wp-seo-next')) . '</a></td>';
                    echo '</tr>';
                }
                echo '</tbody>';
                echo '</table>';
            }

            echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url(admin_url('tools.php?page=wpsn-auto-pilot')) . '">' . esc_html__('Open AutoPilot', 'wp-seo-next') . '</a></p>';
        } catch (\Throwable $e) {
            echo '<p class="description">' . esc_html__('AutoPilot preview unavailable.', 'wp-seo-next') . '</p>';
        }

        echo '</div>';
    }

    /**
     * Cached Search Console insights (via Google connector) (fast; no remote calls).
     *
     * @param array<string,array{title:string,cap:string}> $toolsPages
     */
    private function renderDashboardSearchInsights(array $toolsPages): void
    {
        if (!isset($toolsPages['wpsn-site-kit'])) {
            return;
        }

        // Avoid heavy operations on the Hub.
        $installed = false;
        $active = false;
        $consent = (bool) get_option('wpsn_sitekit_consent', false);

        $siteKitPlugin = 'google-site-kit/google-site-kit.php';
        $installed = defined('WP_PLUGIN_DIR') && file_exists(WP_PLUGIN_DIR . '/' . $siteKitPlugin);

        if ($installed) {
            if (!function_exists('is_plugin_active')) {
                require_once ABSPATH . 'wp-admin/includes/plugin.php';
            }
            $active = is_plugin_active($siteKitPlugin);
        }

        $openUrl = admin_url('tools.php?page=wpsn-site-kit');

        echo '<h2 style="margin-top:22px;">' . esc_html__('Search insights', 'wp-seo-next') . '</h2>';
        echo '<p class="description" style="margin-top:6px;">' . esc_html__('Fast, cached Search Console metrics. No remote calls are made on this dashboard.', 'wp-seo-next') . '</p>';

        // Status / CTA.
        if (!$installed) {
            echo '<div class="notice notice-info" style="margin:10px 0 0 0;"><p>' . esc_html__('Search insights are not available yet. Install the Google connector to view Search Console metrics.', 'wp-seo-next') . '</p>';
            echo '<p style="margin:8px 0 0 0;"><a class="button button-primary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p></div>';
            return;
        }
        if (!$active) {
            echo '<div class="notice notice-info" style="margin:10px 0 0 0;"><p>' . esc_html__('Search insights are not available yet. Activate the Google connector to continue.', 'wp-seo-next') . '</p>';
            echo '<p style="margin:8px 0 0 0;"><a class="button button-primary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p></div>';
            return;
        }
        if (!$consent) {
            echo '<div class="notice notice-info" style="margin:10px 0 0 0;"><p>' . esc_html__('Search insights are not connected yet (consent not granted).', 'wp-seo-next') . '</p>';
            echo '<p style="margin:8px 0 0 0;"><a class="button button-primary" href="' . esc_url($openUrl) . '">' . esc_html__('Connect Google', 'wp-seo-next') . '</a></p></div>';
            return;
        }

        // Read cached insights (set by the Search Insights page). If missing, keep the Hub fast.
        $summary = get_transient($this->siteKitTransientKey('sc_summary'));
        $topQueries = get_transient($this->siteKitTransientKey('sc_top_queries'));
        $topPages = get_transient($this->siteKitTransientKey('sc_top_pages'));

        if (!is_array($summary) || (!isset($summary['current']) && !isset($summary['error']))) {
            echo '<div class="notice notice-warning" style="margin:10px 0 0 0;"><p>' . esc_html__('No cached Search Console metrics yet. Open Search Insights and click “Refresh data”, then return here.', 'wp-seo-next') . '</p>';
            echo '<p style="margin:8px 0 0 0;"><a class="button button-secondary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p></div>';
            return;
        }

        if (isset($summary['error']) && is_wp_error($summary['error'])) {
            echo '<div class="notice notice-warning" style="margin:10px 0 0 0;"><p>' . esc_html__('Search Console data is not available yet.', 'wp-seo-next') . '</p>';
            echo '<p class="description" style="margin:6px 0 0 0;">' . esc_html($summary['error']->get_error_message()) . '</p>';
            echo '<p style="margin:8px 0 0 0;"><a class="button button-secondary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p></div>';
            return;
        }

        $cur = isset($summary['current']) && is_array($summary['current']) ? $summary['current'] : [];
        $delta = isset($summary['delta']) && is_array($summary['delta']) ? $summary['delta'] : [];

        $impr = isset($cur['impressions']) ? (int) $cur['impressions'] : 0;
        $clicks = isset($cur['clicks']) ? (int) $cur['clicks'] : 0;
        $ctr = isset($cur['ctr']) ? (float) $cur['ctr'] : 0.0;
        $pos = isset($cur['position']) ? (float) $cur['position'] : 0.0;

        $dImpr = isset($delta['impressions']) ? (float) $delta['impressions'] : 0.0;
        $dClicks = isset($delta['clicks']) ? (float) $delta['clicks'] : 0.0;
        $dCtr = isset($delta['ctr']) ? (float) $delta['ctr'] : 0.0;
        $dPos = isset($delta['position']) ? (float) $delta['position'] : 0.0;

        $fmtDelta = static function (float $v): string {
            if (!is_finite($v) || $v === 0.0) {
                return '0%';
            }
            $sign = $v > 0 ? '+' : '';
            return $sign . number_format_i18n($v, 1) . '%';
        };

        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:12px; max-width:980px; margin-top:12px;">';
        $cards = [
            [__('Impressions', 'wp-seo-next'), number_format_i18n($impr), __('Δ', 'wp-seo-next') . ' ' . $fmtDelta($dImpr)],
            [__('Clicks', 'wp-seo-next'), number_format_i18n($clicks), __('Δ', 'wp-seo-next') . ' ' . $fmtDelta($dClicks)],
            [__('CTR', 'wp-seo-next'), $this->formatPercent($ctr), __('Δ', 'wp-seo-next') . ' ' . $fmtDelta($dCtr)],
            [__('Avg position', 'wp-seo-next'), number_format_i18n($pos, 2), __('Δ', 'wp-seo-next') . ' ' . $fmtDelta($dPos)],
        ];
        foreach ($cards as $c) {
            echo '<div class="card" style="margin:0;">';
            echo '<h3 style="margin:0 0 6px 0;">' . esc_html((string) $c[0]) . '</h3>';
            echo '<div style="font-size:22px; font-weight:600; line-height:1.2;">' . esc_html((string) $c[1]) . '</div>';
            echo '<p class="description" style="margin:6px 0 0 0;">' . esc_html((string) $c[2]) . '</p>';
            echo '</div>';
        }
        echo '</div>';

        // Tables.
        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(420px, 1fr)); gap:12px; max-width:1200px; margin-top:14px;">';

        // Top queries.
        echo '<div class="card" style="margin:0;">';
        echo '<h3 style="margin:0 0 10px 0;">' . esc_html__('Top queries (last 28 days)', 'wp-seo-next') . '</h3>';
        if (is_array($topQueries) && isset($topQueries['rows']) && is_array($topQueries['rows']) && !empty($topQueries['rows'])) {
            echo '<table class="widefat striped">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__('Query', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Clicks', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Impr.', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('CTR', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Pos', 'wp-seo-next') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($topQueries['rows'] as $r) {
                if (!is_array($r)) {
                    continue;
                }
                $q = isset($r['query']) ? (string) $r['query'] : '';
                echo '<tr>';
                echo '<td>' . esc_html($q) . '</td>';
                echo '<td>' . esc_html(number_format_i18n((int) ($r['clicks'] ?? 0))) . '</td>';
                echo '<td>' . esc_html(number_format_i18n((int) ($r['impressions'] ?? 0))) . '</td>';
                echo '<td>' . esc_html($this->formatPercent((float) ($r['ctr'] ?? 0.0))) . '</td>';
                echo '<td>' . esc_html(number_format_i18n((float) ($r['position'] ?? 0.0), 2)) . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        } else {
            echo '<p class="description">' . esc_html__('No query data cached yet (or not enough data).', 'wp-seo-next') . '</p>';
        }
        echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p>';
        echo '</div>';

        // Top pages.
        echo '<div class="card" style="margin:0;">';
        echo '<h3 style="margin:0 0 10px 0;">' . esc_html__('Top content (last 28 days)', 'wp-seo-next') . '</h3>';
        if (is_array($topPages) && isset($topPages['rows']) && is_array($topPages['rows']) && !empty($topPages['rows'])) {
            echo '<table class="widefat striped">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__('Page', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Clicks', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Impr.', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('CTR', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Pos', 'wp-seo-next') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($topPages['rows'] as $r) {
                if (!is_array($r)) {
                    continue;
                }
                $u = isset($r['url']) ? (string) $r['url'] : '';
                $label = isset($r['label']) ? (string) $r['label'] : $u;
                echo '<tr>';
                echo '<td>';
                if ($u !== '') {
                    echo '<a href="' . esc_url($u) . '" target="_blank" rel="noopener">' . esc_html($label) . '</a>';
                } else {
                    echo esc_html($label);
                }
                echo '</td>';
                echo '<td>' . esc_html(number_format_i18n((int) ($r['clicks'] ?? 0))) . '</td>';
                echo '<td>' . esc_html(number_format_i18n((int) ($r['impressions'] ?? 0))) . '</td>';
                echo '<td>' . esc_html($this->formatPercent((float) ($r['ctr'] ?? 0.0))) . '</td>';
                echo '<td>' . esc_html(number_format_i18n((float) ($r['position'] ?? 0.0), 2)) . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        } else {
            echo '<p class="description">' . esc_html__('No page data cached yet (or not enough data).', 'wp-seo-next') . '</p>';
        }
        echo '<p style="margin:10px 0 0 0;"><a class="button button-secondary" href="' . esc_url($openUrl) . '">' . esc_html__('Open Search Insights', 'wp-seo-next') . '</a></p>';
        echo '</div>';

        echo '</div>';
    }

    private function siteKitTransientKey(string $suffix): string
    {
        $blogId = function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0;
        $userId = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $suffix = sanitize_key($suffix);
        return 'wpsn_sitekit_' . $blogId . '_' . $userId . '_' . $suffix;
    }

    private function formatPercent(float $ratio): string
    {
        if (!is_finite($ratio) || $ratio <= 0) {
            return '0.0%';
        }
        return number_format_i18n($ratio * 100, 1) . '%';
    }

    public function handleTogglePin(): void
    {
        if (!Capabilities::canManageTools()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_toggle_pin');

        $slug = isset($_GET['slug']) ? sanitize_key((string) wp_unslash($_GET['slug'])) : '';
        if ($slug === '') {
            wp_safe_redirect(admin_url('admin.php?page=' . self::TOP_MENU_SLUG));
            exit;
        }

        // Only allow pinning of our pages.
        if ($slug !== 'wp-seo-next' && strpos($slug, 'wpsn-') !== 0) {
            wp_safe_redirect(admin_url('admin.php?page=' . self::TOP_MENU_SLUG));
            exit;
        }

        $userId = get_current_user_id();
        $current = get_user_meta($userId, 'wpsn_pinned_pages', true);
        $pins = is_array($current) ? array_values(array_filter(array_map('sanitize_key', $current))) : [];

        if (in_array($slug, $pins, true)) {
            $pins = array_values(array_diff($pins, [$slug]));
        } else {
            $pins[] = $slug;
            $pins = array_values(array_unique($pins));
        }

        update_user_meta($userId, 'wpsn_pinned_pages', $pins);

        $redirect = wp_get_referer();
        if (!is_string($redirect) || $redirect === '') {
            $redirect = admin_url('admin.php?page=' . self::TOP_MENU_SLUG);
        }
        wp_safe_redirect($redirect);
        exit;
    }

    /**
     * Collect our Tools pages registered under Tools → ... to mirror them under our own top menu.
     *
     * @return array<string,array{title:string,cap:string}>
     */
    private function collectToolsPages(): array
    {
        $out = [];

        global $submenu;
        if (!isset($submenu['tools.php']) || !is_array($submenu['tools.php'])) {
            return $out;
        }

        foreach ($submenu['tools.php'] as $row) {
            if (!is_array($row) || count($row) < 3) {
                continue;
            }

            $menuTitle = isset($row[0]) ? (string) $row[0] : '';
            $cap = isset($row[1]) ? (string) $row[1] : Capabilities::CAP_TOOLS;
            $slug = isset($row[2]) ? (string) $row[2] : '';

            if ($slug === '' || strpos($slug, 'wpsn-') !== 0) {
                continue;
            }

            // Remove any tags/markup added by WP (like counts).
            $menuTitle = wp_strip_all_tags($menuTitle);

            $out[$slug] = [
                'title' => $menuTitle !== '' ? $menuTitle : $slug,
                'cap' => $cap !== '' ? $cap : Capabilities::CAP_TOOLS,
            ];
        }

        // Prefer a stable, curated order for the most important pages.
        $preferred = [
            'wpsn-tasks',
            'wpsn-issues',
            'wpsn-notifications',
            'wpsn-connections',
            'wpsn-search-console',
            'wpsn-site-kit',
            'wpsn-drift-monitor',
            'wpsn-site-crawl',
            'wpsn-serp-gap',
            'wpsn-content-clusters',
            'wpsn-crawl-budget',
            'wpsn-audit',
            'wpsn-seo-bulk-table',
            'wpsn-seo-bulk',
            'wpsn-redirects',
            'wpsn-404-monitor',
            'wpsn-indexation-risk',
            'wpsn-sitemaps',
            'wpsn-robots',
            'wpsn-indexnow',
            'wpsn-config-sync',
            'wpsn-revisions',
            'wpsn-snapshots',
            'wpsn-schema-validator',
            'wpsn-seo-media',
            'wpsn-activity-log',
            'wpsn-diagnostics',
        ];

        $sorted = [];
        foreach ($preferred as $slug) {
            if (isset($out[$slug])) {
                $sorted[$slug] = $out[$slug];
                unset($out[$slug]);
            }
        }

        // Append any remaining pages (alphabetical by title).
        if (!empty($out)) {
            uasort($out, function ($a, $b) {
                $ta = is_array($a) && isset($a['title']) ? (string) $a['title'] : '';
                $tb = is_array($b) && isset($b['title']) ? (string) $b['title'] : '';
                return strcasecmp($ta, $tb);
            });
            foreach ($out as $slug => $meta) {
                $sorted[$slug] = $meta;
            }
        }

        return $sorted;
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     * @return array<string,array<string,array{title:string,cap:string}>>
     */
    private function groupToolsPages(array $toolsPages): array
    {
        $groups = [
            'monitoring' => [],
            'crawl' => [],
            'content' => [],
            'technical' => [],
            'admin' => [],
            'other' => [],
        ];

        foreach ($toolsPages as $slug => $meta) {
            $group = $this->groupForSlug($slug);
            if (!isset($groups[$group])) {
                $group = 'other';
            }
            $groups[$group][$slug] = $meta;
        }

        return $groups;
    }

    private function groupLabel(string $groupId): string
    {
        switch ($groupId) {
            case 'monitoring':
                return __('Monitoring', 'wp-seo-next');
            case 'crawl':
                return __('Crawl', 'wp-seo-next');
            case 'content':
                return __('Content', 'wp-seo-next');
            case 'technical':
                return __('Technical', 'wp-seo-next');
            case 'admin':
                return __('Admin', 'wp-seo-next');
            case 'other':
            default:
                return __('Other', 'wp-seo-next');
        }
    }

    private function groupForSlug(string $slug): string
    {
        // Monitoring.
        if (in_array($slug, [
            'wpsn-tasks',
            'wpsn-issues',
            'wpsn-audit',
            'wpsn-notifications',
            'wpsn-connections',
            'wpsn-search-console',
            'wpsn-site-kit',
            'wpsn-drift-monitor',
            'wpsn-indexation-risk',
            'wpsn-analysis-report',
        ], true)) {
            return 'monitoring';
        }

        // Crawl.
        if (in_array($slug, [
            'wpsn-site-crawl',
            'wpsn-crawl-budget',
            'wpsn-canonical-matrix',
        ], true)) {
            return 'crawl';
        }

        // Content.
        if (in_array($slug, [
            'wpsn-serp-gap',
            'wpsn-content-clusters',
            'wpsn-auto-pilot',
            'wpsn-seo-media',
            'wpsn-seo-bulk',
            'wpsn-seo-bulk-table',
            'wpsn-bulk',
            'wpsn-bulk-table',
        ], true)) {
            return 'content';
        }

        // Technical.
        if (in_array($slug, [
            'wpsn-redirects',
            'wpsn-404-monitor',
            'wpsn-robots',
            'wpsn-url-rules',
            'wpsn-sitemaps',
            'wpsn-quality-gate',
            'wpsn-indexnow',
            'wpsn-config-sync',
            'wpsn-revisions',
            'wpsn-snapshots',
            'wpsn-schema-validator',
        ], true)) {
            return 'technical';
        }

        // Admin / system.
        if (in_array($slug, [
            'wpsn-diagnostics',
            'wpsn-queue',
            'wpsn-import-export',
            'wpsn-undo',
            'wpsn-activity-log',
            'wpsn-saas-sync',
            'wpsn-saas-dashboard',
            'wpsn-license',
            'wpsn-tokens-variables',
        ], true)) {
            return 'admin';
        }

        return 'other';
    }

    /**
     * @return string[]
     */
    private function getPinnedPages(): array
    {
        $userId = get_current_user_id();
        if ($userId <= 0) {
            return [];
        }
        $raw = get_user_meta($userId, 'wpsn_pinned_pages', true);
        $pins = is_array($raw) ? array_values(array_filter(array_map('sanitize_key', $raw))) : [];

        // Only keep our pages.
        $out = [];
        foreach ($pins as $slug) {
            if ($slug === 'wp-seo-next' || strpos($slug, 'wpsn-') === 0) {
                $out[] = $slug;
            }
        }
        return array_values(array_unique($out));
    }

    /**
     * @param array<string,array{title:string,cap:string}> $toolsPages
     * @return array{label:string,url:string}|null
     */
    private function pageInfo(string $slug, array $toolsPages): ?array
    {
        if ($slug === 'wp-seo-next') {
            return [
                'label' => __('Settings', 'wp-seo-next'),
                'url' => admin_url('admin.php?page=wp-seo-next'),
            ];
        }

        if (!isset($toolsPages[$slug])) {
            return null;
        }

        return [
            'label' => (string) $toolsPages[$slug]['title'],
            'url' => admin_url('tools.php?page=' . $slug),
        ];
    }

    private function pinUrl(string $slug): string
    {
        $url = add_query_arg([
            'action' => 'wpsn_toggle_pin',
            'slug' => $slug,
        ], admin_url('admin-post.php'));

        return wp_nonce_url($url, 'wpsn_toggle_pin');
    }

    /**
     * Hide our pages from WordPress core menus (Tools/Settings) to reduce clutter.
     * Pages remain accessible directly by URL.
     */
    public function cleanupLegacyMenus(): void
    {
        // Hide from Settings.
        remove_submenu_page('options-general.php', 'wp-seo-next');

        // Hide from Tools.
        global $submenu;
        if (!isset($submenu['tools.php']) || !is_array($submenu['tools.php'])) {
            return;
        }

        foreach ($submenu['tools.php'] as $row) {
            if (!is_array($row) || count($row) < 3) {
                continue;
            }
            $slug = isset($row[2]) ? (string) $row[2] : '';
            // Remove all WP SEO Next legacy entries (including Settings, if present)
            // so users only see our dedicated top-level menu.
            if ($slug !== '' && (strpos($slug, 'wpsn') === 0 || $slug === 'wp-seo-next')) {
                // Remove from Tools menu to avoid clutter. Pages remain accessible by URL.
                remove_submenu_page('tools.php', $slug);
            }
        }
    }

    /**
     * Make sure our top menu stays highlighted when visiting legacy URLs.
     *
     * WordPress core can pass null into these filters depending on screen.
     * We accept mixed to avoid PHP 8+ TypeErrors.
     *
     * @param mixed $parentFile
     */
    public function filterParentFile($parentFile): string
    {
        $parentFile = is_string($parentFile) ? $parentFile : '';
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if ($page === '') {
            return $parentFile;
        }

        if ($page === 'wp-seo-next' || strpos($page, 'wpsn-') === 0 || $page === self::TOP_MENU_SLUG) {
            return $this->resolvedTopMenuSlug();
        }

        return $parentFile;
    }

    /**
     * @param mixed $submenuFile
     * @param mixed $parentFile
     */
    public function filterSubmenuFile($submenuFile, $parentFile): string
    {
        $submenuFile = is_string($submenuFile) ? $submenuFile : '';
        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if ($page === '') {
            return $submenuFile;
        }

        if (isset($this->submenuHighlight[$page])) {
            return (string) $this->submenuHighlight[$page];
        }

        // Non-clickable headings live as their own pseudo-pages.
        if (strpos($page, 'wpsn-group-') === 0) {
            return $page;
        }

        // Default mapping for our legacy tools pages.
        if (strpos($page, 'wpsn-') === 0) {
            return 'tools.php?page=' . $page;
        }

        return $submenuFile;
    }

    /**
     * WordPress stores the “file” value for top-level menu items either as the plain slug
     * (e.g. `wpsn`) or as a fully qualified `admin.php?page=wpsn` depending on how the
     * menu was registered and on WP internals.
     *
     * To avoid highlight regressions, detect what exists in global $menu and return
     * the matching value.
     */
    private function resolvedTopMenuSlug(): string
    {
        $plain = self::TOP_MENU_SLUG;
        $full = 'admin.php?page=' . self::TOP_MENU_SLUG;

        global $menu;
        if (isset($menu) && is_array($menu)) {
            foreach ($menu as $row) {
                if (!is_array($row) || !isset($row[2])) {
                    continue;
                }
                $file = (string) $row[2];
                if ($file === $plain) {
                    return $plain;
                }
                if ($file === $full) {
                    return $full;
                }
            }
        }

        return $plain;
    }

    public function handleSave(): void
    {
        // Even though SettingsPage::handleSave() performs the same checks,
        // keep them here too so the admin_post handler is always protected.
        if (!Capabilities::canManageSettings()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_save_settings', 'wpsn_nonce');

        (new SettingsPage($this->container))->handleSave();
    }

    /**
     * One-click UI mode switch (Simple <-> Pro).
     */
    public function handleSwitchUiMode(): void
    {
        if (!Capabilities::canManageSettings()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_switch_ui_mode');

        $mode = isset($_GET['mode']) ? sanitize_key((string) wp_unslash($_GET['mode'])) : UI::MODE_SIMPLE;
        if (!in_array($mode, [UI::MODE_SIMPLE, UI::MODE_PRO], true)) {
            $mode = UI::MODE_SIMPLE;
        }

        /** @var \WPSN\Admin\SettingsRepository $repo */
        $repo = $this->container->get('settings_repo');
        $current = $repo->getAll();
        if (!isset($current['ui']) || !is_array($current['ui'])) {
            $current['ui'] = [];
        }
        $current['ui']['mode'] = $mode;

        $repo->update($current);
        $this->container->get('settings')->flush();

        $redirect = wp_get_referer();
        if (!is_string($redirect) || $redirect === '') {
            $redirect = admin_url('admin.php?page=' . self::TOP_MENU_SLUG);
        }
        $redirect = add_query_arg(['wpsn_ui_mode' => $mode], $redirect);
        wp_safe_redirect($redirect);
        exit;
    }

    /**
     * One-click action from the Dashboard/Hub to enable Compatibility mode.
     *
     * We keep it lightweight: it only flips compatibility.disable_output = true.
     */
    public function handleEnableCompatibilityMode(): void
    {
        if (!Capabilities::canManageSettings()) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_enable_compatibility_mode');

        /** @var \WPSN\Admin\SettingsRepository $repo */
        $repo = $this->container->get('settings_repo');
        $current = $repo->getAll();
        if (!isset($current['compatibility']) || !is_array($current['compatibility'])) {
            $current['compatibility'] = [];
        }
        $current['compatibility']['disable_output'] = true;

        $repo->update($current);
        $this->container->get('settings')->flush();

        $url = admin_url('admin.php?page=' . self::TOP_MENU_SLUG . '&wpsn_notice=compat_enabled');
        wp_safe_redirect($url);
        exit;
    }

    /**
     * Clear per-user AI overrides (use/manage) from Settings UI.
     */
    public function handleClearAIUserOverride(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_clear_ai_user_override');

        $userId = isset($_GET['user_id']) ? (int) $_GET['user_id'] : 0;
        $which = isset($_GET['which']) ? sanitize_key((string) wp_unslash($_GET['which'])) : 'both';
        if ($userId <= 0) {
            wp_die(esc_html__('Invalid user.', 'wp-seo-next'));
        }

        if (!current_user_can('edit_user', $userId)) {
            wp_die(esc_html__('You do not have permission to edit this user.', 'wp-seo-next'));
        }

        if (!in_array($which, ['use', 'manage', 'both'], true)) {
            $which = 'both';
        }

        $actor = (int) get_current_user_id();
        $changedAny = false;

        if ($which === 'use' || $which === 'both') {
            $old = Capabilities::getUserAIUseOverride($userId);
            Capabilities::setUserAIUseOverride($userId, 'inherit');
            $new = Capabilities::getUserAIUseOverride($userId);
            if ($new !== $old) {
                do_action('wpsn/ai/user_override_updated', $actor, $userId, $old, $new, 'use');
                $changedAny = true;
            }
        }

        if ($which === 'manage' || $which === 'both') {
            $old = Capabilities::getUserAIManageOverride($userId);
            Capabilities::setUserAIManageOverride($userId, 'inherit');
            $new = Capabilities::getUserAIManageOverride($userId);
            if ($new !== $old) {
                do_action('wpsn/ai/user_override_updated', $actor, $userId, $old, $new, 'manage');
                $changedAny = true;
            }
        }

        $redirect = wp_get_referer();
        if (!is_string($redirect) || $redirect === '') {
            $redirect = admin_url('admin.php?page=wp-seo-next');
        }

        $redirect = add_query_arg([
            'wpsn_ai_override_cleared' => $changedAny ? '1' : '0',
            'wpsn_ai_override_user' => (string) $userId,
        ], $redirect);

        wp_safe_redirect($redirect);
        exit;
    }

    /**
     * Clear ALL per-user AI overrides (use/manage) across the site.
     *
     * This is an admin-only safety valve (useful for agencies when handing over a site).
     */
    public function handleClearAIOverridesAll(): void
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_clear_ai_overrides_all');

        $which = isset($_POST['which']) ? sanitize_key((string) wp_unslash($_POST['which'])) : 'both';
        if (!in_array($which, ['use', 'manage', 'both'], true)) {
            $which = 'both';
        }

        global $wpdb;
        $useKey = Capabilities::USERMETA_AI_USE_OVERRIDE;
        $manageKey = Capabilities::USERMETA_AI_MANAGE_OVERRIDE;

        // Select user IDs first (do not paginate while deleting).
        $ids = [];
        if ($which === 'use') {
            // phpcs:ignore WordPress.DB.PreparedSQL
            $ids = $wpdb->get_col($wpdb->prepare(
                "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s",
                $useKey
            ));
        } elseif ($which === 'manage') {
            // phpcs:ignore WordPress.DB.PreparedSQL
            $ids = $wpdb->get_col($wpdb->prepare(
                "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s",
                $manageKey
            ));
        } else {
            // phpcs:ignore WordPress.DB.PreparedSQL
            $ids = $wpdb->get_col($wpdb->prepare(
                "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key IN (%s, %s)",
                $useKey,
                $manageKey
            ));
        }

        if (!is_array($ids)) {
            $ids = [];
        }

        $actor = (int) get_current_user_id();
        $usersAffected = 0;
        $clearedUse = 0;
        $clearedManage = 0;

        foreach ($ids as $uid) {
            $uid = (int) $uid;
            if ($uid <= 0) {
                continue;
            }

            // Permissions: only clear for users we can edit.
            if (!current_user_can('edit_user', $uid)) {
                continue;
            }

            $didAny = false;

            if ($which === 'use' || $which === 'both') {
                $old = Capabilities::getUserAIUseOverride($uid);
                if ($old !== 'inherit') {
                    Capabilities::setUserAIUseOverride($uid, 'inherit');
                    $clearedUse++;
                    $didAny = true;
                }
            }

            if ($which === 'manage' || $which === 'both') {
                $old = Capabilities::getUserAIManageOverride($uid);
                if ($old !== 'inherit') {
                    Capabilities::setUserAIManageOverride($uid, 'inherit');
                    $clearedManage++;
                    $didAny = true;
                }
            }

            if ($didAny) {
                $usersAffected++;
            }
        }

        /**
         * Fired after clearing ALL per-user AI overrides.
         *
         * @param int   $actorUserId
         * @param array $stats { users_affected, cleared_use, cleared_manage, which }
         */
        do_action('wpsn/ai/overrides_cleared_all', $actor, [
            'users_affected' => $usersAffected,
            'cleared_use' => $clearedUse,
            'cleared_manage' => $clearedManage,
            'which' => $which,
        ]);

        $redirect = wp_get_referer();
        if (!is_string($redirect) || $redirect === '') {
            $redirect = admin_url('admin.php?page=wp-seo-next');
        }

        $redirect = add_query_arg([
            'wpsn_notice' => 'ai_overrides_cleared_all',
            'wpsn_ai_overrides_users' => (string) $usersAffected,
            'wpsn_ai_overrides_use' => (string) $clearedUse,
            'wpsn_ai_overrides_manage' => (string) $clearedManage,
            'wpsn_ai_overrides_which' => (string) $which,
        ], $redirect);

        wp_safe_redirect($redirect);
        exit;
    }

    public function enqueueAssets(string $hook): void
    {
        // Only load assets on our settings screen.
        if ($hook !== 'settings_page_wp-seo-next' && $hook !== (self::TOP_MENU_SLUG . '_page_wp-seo-next')) {
            return;
        }

        // Media picker for social default OG image.
        wp_enqueue_media();
        wp_enqueue_script(
            'wpsn-media-picker',
            WPSN_PLUGIN_URL . 'assets/admin/media-picker.js',
            ['jquery'],
            WPSN_VERSION,
            true
        );

        // Custom field token helper (cheat sheet + tester).
        wp_enqueue_script(
            'wpsn-custom-fields-token-helper',
            WPSN_PLUGIN_URL . 'assets/admin/custom-fields-token-helper.js',
            ['jquery'],
            WPSN_VERSION,
            true
        );

        wp_localize_script('wpsn-custom-fields-token-helper', 'wpsnCfTokenHelper', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('wpsn_cf_token_test'),
            'directUrl' => admin_url('admin-post.php'),
            'postAction' => 'wpsn_cf_token_test_post',
            'postNonceAction' => \WPSN\Admin\CustomFieldsTokenTester::getPostNonceAction(),
            'postNonce' => wp_create_nonce(\WPSN\Admin\CustomFieldsTokenTester::getPostNonceAction()),
            'i18n' => [
                'testing' => __('Testing…', 'wp-seo-next'),
                'empty' => __('Empty (no value).', 'wp-seo-next'),
                'error' => __('Error:', 'wp-seo-next'),
                'copied' => __('Copied.', 'wp-seo-next'),
            ],
        ]);

        // Serper.dev helpers (Settings → SERP Providers): test connection + backoff status.
        wp_enqueue_script(
            'wpsn-serp-gap-admin',
            WPSN_PLUGIN_URL . 'assets/admin/serp-gap.js',
            ['jquery'],
            WPSN_VERSION,
            true
        );

        $backoffUntil = (int) get_transient('wpsn_serper_backoff_until');

        wp_localize_script('wpsn-serp-gap-admin', 'WPSN_SERP_GAP', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('wpsn_serp_gap_ajax'),
            'serperBackoffUntil' => $backoffUntil,
            'now' => time(),
            'strings' => [
                'testing' => __('Testing…', 'wp-seo-next'),
                'error' => __('Error.', 'wp-seo-next'),
                'clearBackoffConfirm' => __('Clear Serper backoff? This only clears the plugin timer and does not change your Serper plan limits.', 'wp-seo-next'),
                'cleared' => __('Backoff cleared.', 'wp-seo-next'),
            ],
        ]);
    }
    public function applyUiModeMenu(): void
    {
        // Only hide items in the UI; functionality remains available.
        $settings = $this->container->get('settings');
        if (UI::isPro($settings)) {
            return;
        }

        foreach (UI::proOnlyToolsPages() as $slug) {
            remove_submenu_page(self::TOP_MENU_SLUG, 'tools.php?page=' . $slug);
        }

        // If we removed everything in a group, also remove the heading row.
        $this->removeEmptyGroupHeadings();
    }

    private function removeEmptyGroupHeadings(): void
    {
        global $submenu;
        if (!isset($submenu[self::TOP_MENU_SLUG]) || !is_array($submenu[self::TOP_MENU_SLUG])) {
            return;
        }

        $rows = $submenu[self::TOP_MENU_SLUG];
        // Our menu group heading pseudo-pages.
        $headingPrefix = 'wpsn-group-';

        // Collect heading slugs with their ranges.
        $headingIndexes = [];
        foreach ($rows as $i => $row) {
            if (!is_array($row) || !isset($row[2])) {
                continue;
            }
            $slug = (string) $row[2];
            if (strpos($slug, $headingPrefix) === 0) {
                $headingIndexes[] = (int) $i;
            }
        }

        if (empty($headingIndexes)) {
            return;
        }

        // For each heading, check if it has any items until the next heading.
        $headingIndexes[] = count($rows); // sentinel
        for ($h = 0; $h < count($headingIndexes) - 1; $h++) {
            $start = $headingIndexes[$h];
            $end = $headingIndexes[$h + 1];

            $headingRow = $rows[$start] ?? null;
            if (!is_array($headingRow) || !isset($headingRow[2])) {
                continue;
            }
            $headingSlug = (string) $headingRow[2];

            $hasItems = false;
            for ($i = $start + 1; $i < $end; $i++) {
                $row = $rows[$i] ?? null;
                if (!is_array($row) || !isset($row[2])) {
                    continue;
                }
                $slug = (string) $row[2];
                // Any non-heading submenu counts as an item.
                if ($slug !== '' && strpos($slug, $headingPrefix) !== 0) {
                    $hasItems = true;
                    break;
                }
            }

            if (!$hasItems) {
                remove_submenu_page(self::TOP_MENU_SLUG, $headingSlug);
            }
        }
    }

    public function uiModeNotices(): void
    {
        if (!is_admin()) {
            return;
        }

        // Optional one-click toggle feedback.
        $switched = isset($_GET['wpsn_ui_mode']) ? sanitize_key((string) wp_unslash($_GET['wpsn_ui_mode'])) : '';
        if (in_array($switched, [UI::MODE_SIMPLE, UI::MODE_PRO], true)) {
            $modeLabel = ($switched === UI::MODE_PRO)
                ? esc_html__('Pro (advanced)', 'wp-seo-next')
                : esc_html__('Simple (recommended)', 'wp-seo-next');
            echo '<div class="notice notice-success is-dismissible"><p>'
                . sprintf(
                    /* translators: %s is UI mode label */
                    esc_html__('Interface mode switched to %s.', 'wp-seo-next'),
                    '<strong>' . $modeLabel . '</strong>'
                )
                . '</p></div>';
        }

        $settings = $this->container->get('settings');
        if (UI::isPro($settings)) {
            return;
        }

        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if ($page === '') {
            return;
        }

        if (!in_array($page, UI::proOnlyToolsPages(), true)) {
            return;
        }

        $settingsUrl = admin_url('admin.php?page=wp-seo-next#wpsn-ui-mode');

        ?>
        <div class="notice notice-info">
            <p>
                <?php
                    echo wp_kses_post(
                        sprintf(
                            /* translators: %s is Settings URL */
                            __('This WP SEO Next page is hidden in <strong>Simple</strong> mode. Switch to <strong>Pro</strong> mode in <a href="%s">Settings</a> to show advanced tools in the menu.', 'wp-seo-next'),
                            esc_url($settingsUrl)
                        )
                    );
                ?>
            </p>
        </div>
        <?php
    }

    public function ajaxModeNotices(): void
    {
        if (!is_admin()) {
            return;
        }

        if (!$this->isWpsnAdminPage()) {
            return;
        }

        $settings = $this->container->get('settings');
        if (\WPSN\Core\AjaxMode::isEnabled($settings)) {
            return;
        }

        $modesUrl = admin_url('tools.php?page=wpsn-diagnostics');
        $msg = sprintf(
            /* translators: %s is Diagnostics URL */
            __('Admin-ajax live mode is disabled globally. Queue live runner still works through a direct PHP endpoint, but older AJAX-only sections stay hidden. You can change this in <a href="%s">Diagnostics → Modes</a>.', 'wp-seo-next'),
            esc_url($modesUrl)
        );

        echo '<div class="notice notice-warning is-dismissible"><p>' . wp_kses_post($msg) . '</p></div>';
    }

    public function filterAjaxBodyClass(string $classes): string
    {
        if (!$this->isWpsnAdminPage()) {
            return $classes;
        }

        $settings = $this->container->get('settings');
        $enabled = \WPSN\Core\AjaxMode::isEnabled($settings);

        $classes .= $enabled ? ' wpsn-ajax-enabled' : ' wpsn-ajax-disabled';
        return $classes;
    }

    public function enqueueAjaxUiAssets(string $hook): void
    {
        if (!$this->isWpsnAdminPage()) {
            return;
        }

        wp_enqueue_style('wpsn-ajax-ui', WPSN_PLUGIN_URL . 'assets/admin/ajax-ui.css', [], WPSN_VERSION);
    }

    private function isWpsnAdminPage(): bool
    {
        if (!is_admin()) {
            return false;
        }

        $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
        if ($page === '') {
            return false;
        }

        if ($page === 'wp-seo-next') {
            return true;
        }

        return strpos($page, 'wpsn') === 0;
    }

}
