<?php

namespace WPSN\Modules\AIReadiness\Admin;

use WPSN\Admin\SettingsRepository;
use WPSN\Core\Branding;
use WPSN\Core\Container;
use WPSN\Core\Capabilities;
use WPSN\Core\Settings;
use WPSN\Modules\AIReadiness\AiBotsCatalog;
use WPSN\Modules\AIReadiness\BotBlockStatsRepository;
use WPSN\Modules\AIReadiness\LlmsTxtService;
use WPSN\Modules\AIReadiness\PolicyLogRepository;

defined('ABSPATH') || exit;

final class AiReadinessAdmin
{
    private const PAGE_SLUG = 'wpsn-ai-readiness';

    private Container $container;
    private SettingsRepository $repo;
    private Settings $settings;

    private ?string $hookSuffix = null;

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

    public function register(): void
    {
        add_action('admin_menu', [$this, 'addMenu']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);

        add_action('admin_post_wpsn_ai_readiness_save_llms', [$this, 'handleSaveLlms']);
        add_action('admin_post_wpsn_ai_readiness_save_bots', [$this, 'handleSaveBots']);
        add_action('admin_post_wpsn_ai_readiness_clear_bot_stats', [$this, 'handleClearBotStats']);
        add_action('admin_post_wpsn_ai_readiness_export_bot_stats', [$this, 'handleExportBotStats']);
    }

    public function addMenu(): void
    {
        $this->hookSuffix = add_management_page(
            __('AI Search Readiness', 'wp-seo-next'),
            __('AI Search Readiness', 'wp-seo-next'),
            Capabilities::CAP_TOOLS,
            self::PAGE_SLUG,
            [$this, 'render']
        );
    }

    public function enqueueAssets(string $hookSuffix): void
    {
        if ($this->hookSuffix === null || $hookSuffix !== $this->hookSuffix) {
            return;
        }

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

    private function currentView(): string
{
    $view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'center';
    return in_array($view, ['center', 'llms', 'crawlers', 'stats', 'audit'], true) ? $view : 'center';
}


    private function pageUrl(array $args = []): string
    {
        $base = admin_url('tools.php');
        $args = array_merge(['page' => self::PAGE_SLUG], $args);
        return (string) add_query_arg($args, $base);
    }

    private function redirectWithNotice(string $message, string $type = 'success', array $args = []): void
    {
        $url = $this->pageUrl(array_merge($args, [
            'wpsn_notice' => rawurlencode($message),
            'wpsn_notice_type' => $type,
        ]));
        wp_safe_redirect($url);
        exit;
    }

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

        $view = $this->currentView();

        $notice = isset($_GET['wpsn_notice']) ? sanitize_text_field((string) wp_unslash($_GET['wpsn_notice'])) : '';
        $noticeType = isset($_GET['wpsn_notice_type']) ? sanitize_key((string) wp_unslash($_GET['wpsn_notice_type'])) : 'success';
        if (!in_array($noticeType, ['success', 'error', 'warning', 'info'], true)) {
            $noticeType = 'success';
        }

        echo '<div class="wrap wpsn-ai-readiness">';
        echo '<h1>' . esc_html__('AI Search Readiness', 'wp-seo-next') . '</h1>';
        echo '<p class="description">' . esc_html__('Manage llms.txt and AI crawler policies (robots.txt). Keep a simple audit trail of changes.', 'wp-seo-next') . '</p>';

        if ($notice !== '') {
            $class = 'notice-' . $noticeType;
            echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($notice) . '</p></div>';
        }

        
// Tabs.
echo '<h2 class="nav-tab-wrapper" style="margin-top:10px;">';
echo '<a href="' . esc_url($this->pageUrl(['view' => 'center'])) . '" class="nav-tab ' . ($view === 'center' ? 'nav-tab-active' : '') . '">' . esc_html__('Overview', 'wp-seo-next') . '</a>';
echo '<a href="' . esc_url($this->pageUrl(['view' => 'llms'])) . '" class="nav-tab ' . ($view === 'llms' ? 'nav-tab-active' : '') . '">' . esc_html__('llms.txt', 'wp-seo-next') . '</a>';
echo '<a href="' . esc_url($this->pageUrl(['view' => 'crawlers'])) . '" class="nav-tab ' . ($view === 'crawlers' ? 'nav-tab-active' : '') . '">' . esc_html__('AI Crawlers', 'wp-seo-next') . '</a>';
echo '<a href="' . esc_url($this->pageUrl(['view' => 'stats'])) . '" class="nav-tab ' . ($view === 'stats' ? 'nav-tab-active' : '') . '">' . esc_html__('Bot Blocker stats', 'wp-seo-next') . '</a>';
echo '<a href="' . esc_url($this->pageUrl(['view' => 'audit'])) . '" class="nav-tab ' . ($view === 'audit' ? 'nav-tab-active' : '') . '">' . esc_html__('Audit log', 'wp-seo-next') . '</a>';
echo '</h2>';

        echo '<hr class="wp-header-end" />';

        
if ($view === 'center') {
    $this->renderCenterTab();
} elseif ($view === 'crawlers') {
    $this->renderCrawlersTab();
} elseif ($view === 'stats') {
    $this->renderStatsTab();
} elseif ($view === 'audit') {
    $this->renderAuditTab();
} else {
    $this->renderLlmsTab();
}

        echo '</div>';
    }


private function renderCenterTab(): void
{
    $all = $this->repo->getAll();
    $ar = isset($all['ai_readiness']) && is_array($all['ai_readiness']) ? $all['ai_readiness'] : [];

    $llms = isset($ar['llms']) && is_array($ar['llms']) ? $ar['llms'] : [];
    $llmsEnabled = !empty($llms['enabled']);
    $llmsMode = isset($llms['mode']) ? sanitize_key((string) $llms['mode']) : 'auto';
    if (!in_array($llmsMode, ['auto', 'manual'], true)) {
        $llmsMode = 'auto';
    }

    $bots = isset($ar['bots']) && is_array($ar['bots']) ? $ar['bots'] : [];
    $botsEnabled = !empty($bots['enabled']);
    $allow = isset($bots['allow']) && is_array($bots['allow']) ? $bots['allow'] : [];

    $blocker = isset($bots['blocker']) && is_array($bots['blocker']) ? $bots['blocker'] : [];
    $blockerEnabled = !empty($blocker['enabled']);
    $blockerCode = isset($blocker['status_code']) ? (int) $blocker['status_code'] : 403;
    if (!in_array($blockerCode, [403, 429], true)) {
        $blockerCode = 403;
    }
    $blockerRetryAfter = isset($blocker['retry_after']) ? (int) $blocker['retry_after'] : 60;
    if ($blockerRetryAfter < 1) {
        $blockerRetryAfter = 1;
    }
    if ($blockerRetryAfter > 3600) {
        $blockerRetryAfter = 3600;
    }

    $statsEnabled = !empty($blocker['stats_enabled']);
    $statsRetentionDays = isset($blocker['stats_retention_days']) ? (int) $blocker['stats_retention_days'] : 90;
    if ($statsRetentionDays < 1) {
        $statsRetentionDays = 1;
    }
    if ($statsRetentionDays > 365) {
        $statsRetentionDays = 365;
    }

    $llmsUrl = home_url('/llms.txt');
    $robotsUrl = home_url('/robots.txt');

    $catalog = AiBotsCatalog::catalog();
    $blockedBots = [];
    foreach ($catalog as $key => $bot) {
        $isAllowed = array_key_exists($key, $allow) ? (bool) $allow[$key] : (bool) ($bot['default_allow'] ?? true);
        if (!$isAllowed) {
            $blockedBots[] = [
                'key' => (string) $key,
                'label' => (string) ($bot['label'] ?? $key),
                'ua' => (string) ($bot['ua'] ?? $key),
                'group' => (string) ($bot['group'] ?? ''),
            ];
        }
    }

    $blockedCount = count($blockedBots);
    $blockedPreview = '';
    if ($blockedCount > 0) {
        $labels = array_map(static function ($b) {
            return is_array($b) ? (string) ($b['label'] ?? '') : '';
        }, $blockedBots);
        $labels = array_values(array_filter(array_map('trim', $labels), static fn($v) => $v !== ''));
        $labels = array_slice($labels, 0, 6);
        $blockedPreview = implode(', ', $labels);
        if ($blockedCount > 6) {
            $blockedPreview .= ' +' . (string) ($blockedCount - 6);
        }
    }

    $statsRepo = new BotBlockStatsRepository();
    $hits7 = $statsEnabled ? $statsRepo->totalHits(7) : 0;
    $top7 = $statsEnabled ? $statsRepo->totals(7) : [];
    $top7 = array_slice($top7, 0, 6);

    $auditRepo = new PolicyLogRepository();
    $recent = $auditRepo->query([], 8, 0);

    $badge = static function (bool $ok, string $okText, string $warnText): string {
        $cls = $ok ? 'ok' : 'warn';
        $txt = $ok ? $okText : $warnText;
        return '<span class="wpsn-badge ' . esc_attr($cls) . '">' . esc_html($txt) . '</span>';
    };

    echo '<h2 style="margin-top:0;">' . esc_html__('Publisher control center', 'wp-seo-next') . '</h2>';
    echo '<p class="description">' . esc_html__('Quick overview of your AI crawler policy and what is publicly available to bots.', 'wp-seo-next') . '</p>';

    echo '<div class="wpsn-ai-center-actions" style="margin:10px 0 16px 0; display:flex; gap:8px; flex-wrap:wrap;">';
    echo '<a class="button" href="' . esc_url($this->pageUrl(['view' => 'llms'])) . '">' . esc_html__('Edit llms.txt', 'wp-seo-next') . '</a>';
    echo '<a class="button" href="' . esc_url($this->pageUrl(['view' => 'crawlers'])) . '">' . esc_html__('Edit AI Crawlers', 'wp-seo-next') . '</a>';
    echo '<a class="button" href="' . esc_url($this->pageUrl(['view' => 'stats'])) . '">' . esc_html__('View Bot Blocker stats', 'wp-seo-next') . '</a>';
    echo '<a class="button" href="' . esc_url($this->pageUrl(['view' => 'audit'])) . '">' . esc_html__('View audit log', 'wp-seo-next') . '</a>';
    echo '</div>';

    echo '<div class="wpsn-ai-center-grid">';
    echo '<div class="wpsn-ai-card">';
    echo '<h3>' . esc_html__('llms.txt', 'wp-seo-next') . ' ' . $badge($llmsEnabled, __('ON', 'wp-seo-next'), __('OFF', 'wp-seo-next')) . '</h3>';
    echo '<p class="description">' . esc_html__('Public URL:', 'wp-seo-next') . ' <a href="' . esc_url($llmsUrl) . '" target="_blank" rel="noopener">' . esc_html($llmsUrl) . '</a></p>';
    echo '<p>' . esc_html__('Mode:', 'wp-seo-next') . ' <code>' . esc_html($llmsMode) . '</code></p>';
    echo '</div>';

    echo '<div class="wpsn-ai-card">';
    echo '<h3>' . esc_html__('robots.txt AI controls', 'wp-seo-next') . ' ' . $badge($botsEnabled, __('ON', 'wp-seo-next'), __('OFF', 'wp-seo-next')) . '</h3>';
    echo '<p class="description">' . esc_html__('Public URL:', 'wp-seo-next') . ' <a href="' . esc_url($robotsUrl) . '" target="_blank" rel="noopener">' . esc_html($robotsUrl) . '</a></p>';
    echo '<p>' . esc_html__('Blocked bots:', 'wp-seo-next') . ' <strong>' . esc_html((string) $blockedCount) . '</strong></p>';
    if ($blockedPreview !== '') {
        echo '<p class="description">' . esc_html__('Preview:', 'wp-seo-next') . ' ' . esc_html($blockedPreview) . '</p>';
    }
    echo '</div>';

    echo '<div class="wpsn-ai-card">';
    echo '<h3>' . esc_html__('Bot Blocker (server-side)', 'wp-seo-next') . ' ' . $badge($blockerEnabled, __('ON', 'wp-seo-next'), __('OFF', 'wp-seo-next')) . '</h3>';
    if ($blockerEnabled) {
        echo '<p>' . esc_html__('Response:', 'wp-seo-next') . ' <code>' . esc_html((string) $blockerCode) . '</code>';
        if ($blockerCode === 429) {
            echo ' <span class="description">(' . esc_html__('Retry-After', 'wp-seo-next') . ': ' . esc_html((string) $blockerRetryAfter) . 's)</span>';
        }
        echo '</p>';
    } else {
        echo '<p class="description">' . esc_html__('Optional: blocks disallowed bots by User-Agent at runtime.', 'wp-seo-next') . '</p>';
    }
    echo '</div>';

    echo '<div class="wpsn-ai-card">';
    echo '<h3>' . esc_html__('Bot Blocker stats', 'wp-seo-next') . ' ' . $badge($statsEnabled, __('ON', 'wp-seo-next'), __('OFF', 'wp-seo-next')) . '</h3>';
    if ($statsEnabled) {
        echo '<p>' . esc_html__('Last 7 days blocked hits:', 'wp-seo-next') . ' <strong>' . esc_html((string) $hits7) . '</strong></p>';
        echo '<p class="description">' . esc_html__('Retention:', 'wp-seo-next') . ' ' . esc_html((string) $statsRetentionDays) . ' ' . esc_html__('days', 'wp-seo-next') . '</p>';
    } else {
        echo '<p class="description">' . esc_html__('Disabled. Enable in AI Crawlers tab → Bot Blocker → Stats.', 'wp-seo-next') . '</p>';
    }
    echo '</div>';
    echo '</div>'; // grid

    // Stats preview.
    echo '<h2 style="margin-top:18px;">' . esc_html__('Recent blocked hits (7 days)', 'wp-seo-next') . '</h2>';
    if (!$statsEnabled) {
        echo '<p class="description">' . esc_html__('Stats are disabled, so we have nothing to show here.', 'wp-seo-next') . '</p>';
    } elseif (empty($top7)) {
        echo '<p class="description">' . esc_html__('No blocked hits recorded in the selected period.', 'wp-seo-next') . '</p>';
    } else {
        echo '<table class="widefat striped" style="max-width: 1200px;"><thead><tr>';
        echo '<th>' . esc_html__('Bot', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Hits', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Last seen', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Last path', 'wp-seo-next') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($top7 as $row) {
            $label = isset($row['bot_label']) ? (string) $row['bot_label'] : (string) ($row['bot_key'] ?? '');
            $hits = isset($row['hits']) ? (int) $row['hits'] : 0;
            $lastSeen = isset($row['last_seen']) ? (string) $row['last_seen'] : '';
            $lastPath = isset($row['last_path']) ? (string) $row['last_path'] : '';

            echo '<tr>';
            echo '<td><strong>' . esc_html($label) . '</strong><br/><span class="description"><code>' . esc_html((string) ($row['bot_key'] ?? '')) . '</code></span></td>';
            echo '<td>' . esc_html((string) $hits) . '</td>';
            echo '<td>' . esc_html($lastSeen) . '</td>';
            echo '<td><code>' . esc_html($lastPath) . '</code></td>';
            echo '</tr>';
        }

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

    // Recent policy changes.
    echo '<h2 style="margin-top:18px;">' . esc_html__('Recent policy changes', 'wp-seo-next') . '</h2>';
    if (empty($recent)) {
        echo '<p class="description">' . esc_html__('No changes recorded yet.', 'wp-seo-next') . '</p>';
    } else {
        echo '<table class="widefat striped" style="max-width: 1200px;"><thead><tr>';
        echo '<th>' . esc_html__('When (UTC)', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Area', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Action', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Message', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('User', 'wp-seo-next') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($recent as $r) {
            $when = isset($r['created_at']) ? (string) $r['created_at'] : '';
            $ctx = isset($r['context']) ? (string) $r['context'] : '';
            $action = isset($r['action']) ? (string) $r['action'] : '';
            $msg = isset($r['message']) ? (string) $r['message'] : '';
            $uid = isset($r['user_id']) ? (int) $r['user_id'] : 0;

            $userLabel = '';
            if ($uid > 0) {
                $u = get_user_by('id', $uid);
                if ($u && isset($u->display_name)) {
                    $userLabel = (string) $u->display_name;
                }
            }

            echo '<tr>';
            echo '<td>' . esc_html($when) . '</td>';
            echo '<td><code>' . esc_html($ctx) . '</code></td>';
            echo '<td><code>' . esc_html($action) . '</code></td>';
            echo '<td>' . esc_html($msg) . '</td>';
            echo '<td>' . esc_html($userLabel !== '' ? $userLabel : '—') . '</td>';
            echo '</tr>';
        }

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

    // Summary + copy button.
    $lines = [];
    $lines[] = 'AI Search Readiness summary';
    $lines[] = 'Site: ' . home_url('/');
    $lines[] = '';
    $lines[] = 'llms.txt: ' . ($llmsEnabled ? 'ON' : 'OFF') . ' (mode: ' . $llmsMode . ') ' . $llmsUrl;
    $lines[] = 'AI crawler controls (robots.txt): ' . ($botsEnabled ? 'ON' : 'OFF') . ' — blocked bots: ' . (string) $blockedCount;
    if ($blockedCount > 0) {
        $uas = array_map(static function ($b) {
            $ua = is_array($b) ? (string) ($b['ua'] ?? '') : '';
            return trim($ua);
        }, $blockedBots);
        $uas = array_values(array_filter($uas, static fn($v) => $v !== ''));
        $uas = array_slice($uas, 0, 12);
        if (!empty($uas)) {
            $lines[] = 'Blocked preview: ' . implode(', ', $uas) . ($blockedCount > 12 ? ' …' : '');
        }
    }
    $lines[] = 'Bot Blocker: ' . ($blockerEnabled ? ('ON (HTTP ' . (string) $blockerCode . ')') : 'OFF');
    $lines[] = 'Bot Blocker stats: ' . ($statsEnabled ? ('ON — last 7d hits: ' . (string) $hits7 . ' — retention: ' . (string) $statsRetentionDays . 'd') : 'OFF');
    $lines[] = '';
    if (!empty($recent)) {
        $lines[] = 'Recent changes:';
        foreach (array_slice($recent, 0, 5) as $r) {
            $when = isset($r['created_at']) ? (string) $r['created_at'] : '';
            $ctx = isset($r['context']) ? (string) $r['context'] : '';
            $action = isset($r['action']) ? (string) $r['action'] : '';
            $msg = isset($r['message']) ? (string) $r['message'] : '';
            $lines[] = '- ' . $when . ' [' . $ctx . '/' . $action . '] ' . $msg;
        }
    }

    $summary = implode("\n", $lines);

    echo '<h2 style="margin-top:18px;">' . esc_html__('Copyable summary', 'wp-seo-next') . '</h2>';
    echo '<p class="description">' . esc_html__('Handy for agency reports or sharing your policy with clients.', 'wp-seo-next') . '</p>';
    echo '<div style="max-width:1200px; display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:6px 0 10px 0;">';
    echo '<button type="button" class="button button-primary wpsn-copy-btn" data-wpsn-copy-target="wpsn-ai-center-summary" data-wpsn-copy-status="wpsn-ai-center-copy-status">' . esc_html__('Copy summary', 'wp-seo-next') . '</button>';
    echo '<span class="description" id="wpsn-ai-center-copy-status" style="display:none;">' . esc_html__('Copied.', 'wp-seo-next') . '</span>';
    echo '</div>';
    echo '<pre class="wpsn-pre" id="wpsn-ai-center-summary">' . esc_html($summary) . '</pre>';

    // Edge/CDN helper snippets.
    echo '<h2 style="margin-top:18px;">' . esc_html__('Edge/CDN helper', 'wp-seo-next') . '</h2>';
    echo '<p class="description">' . esc_html__('If you use Cloudflare or Nginx, you can enforce the same AI bot policy at the edge to reduce origin load. Snippets below are generated from your current blocked bots list.', 'wp-seo-next') . '</p>';

    if ($blockedCount <= 0) {
        echo '<p class="description">' . esc_html__('You are not blocking any bots right now, so there is nothing to generate.', 'wp-seo-next') . '</p>';
    } else {
        $uaTokens = array_map(static function ($b) {
            $ua = is_array($b) ? (string) ($b['ua'] ?? '') : '';
            return trim($ua);
        }, $blockedBots);
        $uaTokens = array_values(array_filter($uaTokens, static fn($v) => $v !== ''));

        // Cloudflare expression (case-insensitive match).
        $tokensLower = array_values(array_unique(array_map('strtolower', $uaTokens)));
        $exprParts = [];
        foreach ($tokensLower as $t) {
            $t = trim(str_replace('"', '', (string) $t));
            if ($t === '') {
                continue;
            }
            $tEsc = str_replace('"', '\\"', $t);
            $exprParts[] = 'lower(http.user_agent) contains "' . $tEsc . '"';
        }
        $cfExpr = '';
        if (!empty($exprParts)) {
            $cfExpr = '(' . implode(' or ', $exprParts) . ') and not (http.request.uri.path in {"/robots.txt" "/llms.txt"})';
        }

        // Nginx best-effort snippet.
        $regexParts = [];
        foreach ($tokensLower as $t) {
            $t = trim($t);
            if ($t === '') {
                continue;
            }
            $regexParts[] = preg_quote($t, '/');
        }
        $uaRegex = !empty($regexParts) ? implode('|', $regexParts) : '';

        $nginx = [];
        $nginx[] = '# WP SEO Next — AI Bot Blocker (generated)';
        $nginx[] = '# Add inside your Nginx server{} block (or appropriate location{}).';
        $nginx[] = '# Note: this is best-effort. Test on staging first.';
        $nginx[] = '';
        $nginx[] = 'set $wpsn_ai_bot_block 0;';
        if ($uaRegex !== '') {
            $nginx[] = 'if ($http_user_agent ~* "(' . $uaRegex . ')") {';
            $nginx[] = '    set $wpsn_ai_bot_block 1;';
            $nginx[] = '}';
        }
        $nginx[] = '';
        $nginx[] = '# Always allow policy files so bots can read your rules.';
        $nginx[] = 'if ($uri ~* "^/(robots\\.txt|llms\\.txt)$") {';
        $nginx[] = '    set $wpsn_ai_bot_block 0;';
        $nginx[] = '}';
        $nginx[] = '';
        $nginx[] = 'if ($wpsn_ai_bot_block) {';
        if ($blockerCode === 429) {
            $nginx[] = '    add_header Retry-After ' . (string) $blockerRetryAfter . ' always;';
            $nginx[] = '    return 429;';
        } else {
            $nginx[] = '    return 403;';
        }
        $nginx[] = '}';

        $nginxSnippet = implode("\n", $nginx);

        echo '<div class="wpsn-ai-snippets">';

        echo '<div class="wpsn-ai-snippet">';
        echo '<h3><span>' . esc_html__('Cloudflare Firewall/WAF expression', 'wp-seo-next') . '</span>';
        echo '<span class="wpsn-ai-snippet-actions">';
        echo '<button type="button" class="button button-small wpsn-copy-btn" data-wpsn-copy-target="wpsn-ai-snippet-cf" data-wpsn-copy-status="wpsn-ai-snippet-cf-status">' . esc_html__('Copy', 'wp-seo-next') . '</button>';
        echo '<span class="description" id="wpsn-ai-snippet-cf-status" style="display:none;">' . esc_html__('Copied.', 'wp-seo-next') . '</span>';
        echo '</span></h3>';
        if ($cfExpr === '') {
            echo '<p class="description">' . esc_html__('Unable to generate expression (no User-Agent tokens).', 'wp-seo-next') . '</p>';
        } else {
            echo '<p class="description">' . esc_html__('Use as a Block rule. This matches blocked bots and excludes /robots.txt and /llms.txt.', 'wp-seo-next') . '</p>';
            echo '<pre class="wpsn-pre" id="wpsn-ai-snippet-cf">' . esc_html($cfExpr) . '</pre>';
        }
        echo '</div>';

        echo '<div class="wpsn-ai-snippet">';
        echo '<h3><span>' . esc_html__('Nginx snippet', 'wp-seo-next') . '</span>';
        echo '<span class="wpsn-ai-snippet-actions">';
        echo '<button type="button" class="button button-small wpsn-copy-btn" data-wpsn-copy-target="wpsn-ai-snippet-nginx" data-wpsn-copy-status="wpsn-ai-snippet-nginx-status">' . esc_html__('Copy', 'wp-seo-next') . '</button>';
        echo '<span class="description" id="wpsn-ai-snippet-nginx-status" style="display:none;">' . esc_html__('Copied.', 'wp-seo-next') . '</span>';
        echo '</span></h3>';
        echo '<p class="description">' . esc_html__('Best-effort server block snippet. Adjust to your config (esp. if you already use complex rewrites).', 'wp-seo-next') . '</p>';
        echo '<pre class="wpsn-pre" id="wpsn-ai-snippet-nginx">' . esc_html($nginxSnippet) . '</pre>';
        echo '</div>';

        echo '</div>'; // snippets
    }

    echo '<script>(function(){\n'
        . 'function fallbackCopy(text){\n'
        . '  var ta=document.createElement("textarea");\n'
        . '  ta.value=text;\n'
        . '  ta.setAttribute("readonly","readonly");\n'
        . '  ta.style.position="absolute"; ta.style.left="-9999px";\n'
        . '  document.body.appendChild(ta);\n'
        . '  ta.select();\n'
        . '  try { document.execCommand("copy"); } catch(e) {}\n'
        . '  document.body.removeChild(ta);\n'
        . '}\n'
        . 'function showStatus(st){ if(!st){return;} st.style.display="inline"; setTimeout(function(){ st.style.display="none"; }, 1200); }\n'
        . 'function doCopy(text){\n'
        . '  if(navigator.clipboard && navigator.clipboard.writeText){\n'
        . '    return navigator.clipboard.writeText(text);\n'
        . '  }\n'
        . '  fallbackCopy(text);\n'
        . '  return Promise.resolve();\n'
        . '}\n'
        . 'var btns=document.querySelectorAll("[data-wpsn-copy-target]");\n'
        . 'if(!btns||!btns.length){return;}\n'
        . 'btns.forEach(function(btn){\n'
        . '  btn.addEventListener("click", function(){\n'
        . '    var tid=btn.getAttribute("data-wpsn-copy-target");\n'
        . '    if(!tid){return;}\n'
        . '    var pre=document.getElementById(tid);\n'
        . '    var sid=btn.getAttribute("data-wpsn-copy-status");\n'
        . '    var st=sid?document.getElementById(sid):null;\n'
        . '    var text=pre?(pre.textContent||""):"";\n'
        . '    if(!text){return;}\n'
        . '    doCopy(text).then(function(){ showStatus(st); }).catch(function(){ fallbackCopy(text); showStatus(st); });\n'
        . '  });\n'
        . '});\n'
        . '})();</script>';
}

    private function renderLlmsTab(): void
    {
        $all = $this->repo->getAll();
        $ar = isset($all['ai_readiness']) && is_array($all['ai_readiness']) ? $all['ai_readiness'] : [];
        $llms = isset($ar['llms']) && is_array($ar['llms']) ? $ar['llms'] : [];
        $auto = isset($llms['auto']) && is_array($llms['auto']) ? $llms['auto'] : [];

        $enabled = !empty($llms['enabled']);
        $mode = isset($llms['mode']) ? sanitize_key((string) $llms['mode']) : 'auto';
        if (!in_array($mode, ['auto', 'manual'], true)) {
            $mode = 'auto';
        }

        $manual = isset($llms['manual']) ? (string) $llms['manual'] : '';
        $maxLinks = isset($auto['max_links']) ? (int) $auto['max_links'] : 20;
        $includePages = !empty($auto['include_pages']);
        $includePosts = !empty($auto['include_posts']);

        $llmsUrl = home_url('/llms.txt');

        echo '<h2 style="margin-top:0;">' . esc_html__('llms.txt', 'wp-seo-next') . '</h2>';
        echo '<p>';
        printf(
            /* translators: %s is the llms.txt url */
            esc_html__('Public URL: %s', 'wp-seo-next'),
            '<a href="' . esc_url($llmsUrl) . '" target="_blank" rel="noopener">' . esc_html($llmsUrl) . '</a>'
        );
        echo '</p>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_ai_readiness_save_llms" />';
        wp_nonce_field('wpsn_ai_readiness_save_llms');

        echo '<table class="form-table" role="presentation"><tbody>';

        echo '<tr><th scope="row">' . esc_html__('Enable llms.txt', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="wpsn_ai_readiness[llms][enabled]" value="1" ' . checked($enabled, true, false) . ' /> ';
        echo esc_html__('Serve llms.txt at /llms.txt', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Mode', 'wp-seo-next') . '</th><td>';
        echo '<select name="wpsn_ai_readiness[llms][mode]">';
        echo '<option value="auto" ' . selected($mode, 'auto', false) . '>' . esc_html__('Auto-generate', 'wp-seo-next') . '</option>';
        echo '<option value="manual" ' . selected($mode, 'manual', false) . '>' . esc_html__('Manual (custom markdown)', 'wp-seo-next') . '</option>';
        echo '</select>';
        echo '<p class="description">' . esc_html__('Auto mode generates a small curated list (home + some pages/posts + sitemap). Manual mode prints your markdown as-is.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Auto options', 'wp-seo-next') . '</th><td>';
        echo '<label style="display:block;margin:2px 0;"><input type="checkbox" name="wpsn_ai_readiness[llms][auto][include_pages]" value="1" ' . checked($includePages, true, false) . ' /> ' . esc_html__('Include Pages', 'wp-seo-next') . '</label>';
        echo '<label style="display:block;margin:2px 0;"><input type="checkbox" name="wpsn_ai_readiness[llms][auto][include_posts]" value="1" ' . checked($includePosts, true, false) . ' /> ' . esc_html__('Include recent Posts', 'wp-seo-next') . '</label>';
        echo '<p style="margin-top:8px;"><label>' . esc_html__('Max links', 'wp-seo-next') . ' <input type="number" min="5" max="200" name="wpsn_ai_readiness[llms][auto][max_links]" value="' . esc_attr((string) $maxLinks) . '" /></label></p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Manual markdown', 'wp-seo-next') . '</th><td>';
        echo '<textarea class="large-text code" rows="12" name="wpsn_ai_readiness[llms][manual]" placeholder="# Site\n\n> Short summary\n\n## Key pages\n- [Homepage](...)\n">' . esc_textarea($manual) . '</textarea>';
        echo '<p class="description">' . esc_html__('If empty, auto mode will be used as fallback.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

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

        submit_button(__('Save llms.txt settings', 'wp-seo-next'));
        echo '</form>';

        // Preview.
        $service = new LlmsTxtService($this->container);
        $preview = $service->build();

        echo '<h2>' . esc_html__('Preview', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('This is the content that will be served at /llms.txt.', 'wp-seo-next') . '</p>';
        echo '<pre class="wpsn-pre">' . esc_html($preview) . '</pre>';
    }

    public function handleSaveLlms(): void
    {
        if (!current_user_can(Capabilities::CAP_TOOLS)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_ai_readiness_save_llms');

        $incoming = $_POST['wpsn_ai_readiness'] ?? [];
        if (!is_array($incoming)) {
            $incoming = [];
        }

        $all = $this->repo->getAll();
        $prev = $all['ai_readiness']['llms'] ?? [];
        if (!is_array($prev)) {
            $prev = [];
        }

        // Merge: only llms subtree.
        if (!isset($all['ai_readiness']) || !is_array($all['ai_readiness'])) {
            $all['ai_readiness'] = [];
        }
        $all['ai_readiness']['llms'] = isset($incoming['llms']) && is_array($incoming['llms']) ? $incoming['llms'] : [];

        $this->repo->update($all);
        $this->settings->flush();
        flush_rewrite_rules(false);

        // Audit log.
        $newAll = $this->repo->getAll();
        $new = $newAll['ai_readiness']['llms'] ?? [];
        if (!is_array($new)) {
            $new = [];
        }
        $msg = $this->describeLlmsChange($prev, $new);
        (new PolicyLogRepository())->log('llms', 'llms_updated', $msg, $prev, $new, get_current_user_id());

        $this->redirectWithNotice(__('llms.txt settings saved.', 'wp-seo-next'), 'success', ['view' => 'llms']);
    }

    private function describeLlmsChange(array $prev, array $new): string
    {
        $parts = [];
        $prevEnabled = !empty($prev['enabled']);
        $newEnabled = !empty($new['enabled']);
        if ($prevEnabled !== $newEnabled) {
            $parts[] = $newEnabled ? __('Enabled llms.txt.', 'wp-seo-next') : __('Disabled llms.txt.', 'wp-seo-next');
        }

        $prevMode = isset($prev['mode']) ? sanitize_key((string) $prev['mode']) : '';
        $newMode = isset($new['mode']) ? sanitize_key((string) $new['mode']) : '';
        if ($prevMode !== $newMode && $newMode !== '') {
            $parts[] = sprintf(
                /* translators: %s is mode */
                __('Mode: %s', 'wp-seo-next'),
                $newMode
            );
        }

        if (empty($parts)) {
            return __('Updated llms.txt settings.', 'wp-seo-next');
        }
        return implode(' ', $parts);
    }

    private function renderCrawlersTab(): void
    {
        $all = $this->repo->getAll();
        $ar = isset($all['ai_readiness']) && is_array($all['ai_readiness']) ? $all['ai_readiness'] : [];
        $bots = isset($ar['bots']) && is_array($ar['bots']) ? $ar['bots'] : [];

        $enabled = !empty($bots['enabled']);
        $allow = isset($bots['allow']) && is_array($bots['allow']) ? $bots['allow'] : [];

        $blocker = isset($bots['blocker']) && is_array($bots['blocker']) ? $bots['blocker'] : [];
        $blockerEnabled = !empty($blocker['enabled']);
        $blockerCode = isset($blocker['status_code']) ? (int) $blocker['status_code'] : 403;
        if (!in_array($blockerCode, [403, 429], true)) {
            $blockerCode = 403;
        }
        $blockerRetryAfter = isset($blocker['retry_after']) ? (int) $blocker['retry_after'] : 60;
        if ($blockerRetryAfter < 1) {
            $blockerRetryAfter = 1;
        }
        if ($blockerRetryAfter > 3600) {
            $blockerRetryAfter = 3600;
        }
        $blockerAllowLoggedIn = array_key_exists('allow_logged_in', $blocker) ? !empty($blocker['allow_logged_in']) : true;

        $statsEnabled = !empty($blocker['stats_enabled']);
        $statsRetentionDays = isset($blocker['stats_retention_days']) ? (int) $blocker['stats_retention_days'] : 90;
        if ($statsRetentionDays < 1) {
            $statsRetentionDays = 1;
        }
        if ($statsRetentionDays > 365) {
            $statsRetentionDays = 365;
        }

        $robotsUrl = home_url('/robots.txt');

        echo '<h2 style="margin-top:0;">' . esc_html__('AI crawler controls (robots.txt)', 'wp-seo-next') . '</h2>';
        echo '<p>';
        printf(
            /* translators: %s is robots.txt url */
            esc_html__('Robots.txt: %s', 'wp-seo-next'),
            '<a href="' . esc_url($robotsUrl) . '" target="_blank" rel="noopener">' . esc_html($robotsUrl) . '</a>'
        );
        echo '</p>';
        echo '<p class="description">' . esc_html__('These toggles add explicit Disallow rules for selected AI bots. If you use another plugin to manage robots.txt, avoid conflicting rules.', 'wp-seo-next') . '</p>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_ai_readiness_save_bots" />';
        wp_nonce_field('wpsn_ai_readiness_save_bots');

        echo '<table class="form-table" role="presentation"><tbody>';

        echo '<tr><th scope="row">' . esc_html__('Enable AI crawler controls', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="wpsn_ai_readiness[bots][enabled]" value="1" ' . checked($enabled, true, false) . ' /> ' . esc_html__('Apply settings into robots.txt output', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Bot Blocker (server-side)', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="wpsn_ai_readiness[bots][blocker][enabled]" value="1" ' . checked($blockerEnabled, true, false) . ' /> ' . esc_html__('Block disallowed AI bots by User-Agent (optional).', 'wp-seo-next') . '</label>';
        echo '<p class="description">' . esc_html__('If enabled, requests from bots you set to “disallow” below will be blocked with 403/429. robots.txt and llms.txt are always allowed so bots can read your policy.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Blocker response', 'wp-seo-next') . '</th><td>';
        echo '<select name="wpsn_ai_readiness[bots][blocker][status_code]">';
        echo '<option value="403" ' . selected($blockerCode, 403, false) . '>' . esc_html__('403 Forbidden (hard block)', 'wp-seo-next') . '</option>';
        echo '<option value="429" ' . selected($blockerCode, 429, false) . '>' . esc_html__('429 Too Many Requests (soft block)', 'wp-seo-next') . '</option>';
        echo '</select>';
        echo ' <label style="margin-left:10px;">' . esc_html__('Retry-After (seconds)', 'wp-seo-next') . ' <input type="number" min="1" max="3600" name="wpsn_ai_readiness[bots][blocker][retry_after]" value="' . esc_attr((string) $blockerRetryAfter) . '" /></label>';
        echo '<p class="description">' . esc_html__('When using 429, we send a Retry-After header. Some crawlers will back off instead of hammering your site.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Safe bypass', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="wpsn_ai_readiness[bots][blocker][allow_logged_in]" value="1" ' . checked($blockerAllowLoggedIn, true, false) . ' /> ' . esc_html__('Do not block logged-in users (recommended).', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Stats', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="wpsn_ai_readiness[bots][blocker][stats_enabled]" value="1" ' . checked($statsEnabled, true, false) . ' /> ' . esc_html__('Store aggregated bot-block stats (DB table).', 'wp-seo-next') . '</label>';
        echo ' <label style="margin-left:10px;">' . esc_html__('Retention (days)', 'wp-seo-next') . ' <input type="number" min="1" max="365" name="wpsn_ai_readiness[bots][blocker][stats_retention_days]" value="' . esc_attr((string) $statsRetentionDays) . '" /></label>';
        $statsUrl = $this->pageUrl(['view' => 'stats']);
        echo '<p class="description">' . esc_html__('If enabled, we store per-day counters for blocked bots (no IPs). This does one DB write per blocked request. You can view it in the “Bot Blocker stats” tab.', 'wp-seo-next') . ' <a href="' . esc_url($statsUrl) . '">' . esc_html__('Open stats', 'wp-seo-next') . '</a></p>';
        echo '</td></tr>';

        // Render grouped bot list.
        $catalog = AiBotsCatalog::catalog();
        $groups = [];
        foreach ($catalog as $key => $bot) {
            $g = isset($bot['group']) ? (string) $bot['group'] : __('Other', 'wp-seo-next');
            if (!isset($groups[$g])) {
                $groups[$g] = [];
            }
            $groups[$g][$key] = $bot;
        }

        echo '<tr><th scope="row">' . esc_html__('Bots', 'wp-seo-next') . '</th><td>';

        // Presets (client-side only; user still needs to click Save).
        $allKeys = array_keys($catalog);
        $trainingKeys = array_values(array_intersect(['gptbot', 'claudebot', 'google_extended', 'ccbot', 'bytespider'], $allKeys));
        $searchKeys = array_values(array_intersect(['oai_searchbot', 'claude_searchbot', 'perplexitybot'], $allKeys));

        echo '<div class="wpsn-ai-bots-presets" style="margin:0 0 10px 0; display:flex; flex-wrap:wrap; gap:6px; align-items:center;">';
        echo '<strong style="margin-right:4px;">' . esc_html__('Presets:', 'wp-seo-next') . '</strong>';
        echo '<button type="button" class="button button-small" data-wpsn-bot-preset="allow_all">' . esc_html__('Allow all', 'wp-seo-next') . '</button>';
        echo '<button type="button" class="button button-small" data-wpsn-bot-preset="block_training">' . esc_html__('Block training bots', 'wp-seo-next') . '</button>';
        echo '<button type="button" class="button button-small" data-wpsn-bot-preset="search_only">' . esc_html__('Search-only', 'wp-seo-next') . '</button>';
        echo '<button type="button" class="button button-small" data-wpsn-bot-preset="block_all">' . esc_html__('Block all', 'wp-seo-next') . '</button>';
        echo '<span class="description" style="margin-left:6px;">' . esc_html__('Choose a preset, then click “Save crawler settings”.', 'wp-seo-next') . '</span>';
        echo '</div>';

        $jsAll = wp_json_encode(array_values($allKeys));
        $jsTraining = wp_json_encode(array_values($trainingKeys));
        $jsSearch = wp_json_encode(array_values($searchKeys));

        echo '<script>(function(){\n'
            . 'var ALL=' . $jsAll . ';\n'
            . 'var TRAINING=' . $jsTraining . ';\n'
            . 'var SEARCH=' . $jsSearch . ';\n'
            . 'function elByKey(key){\n'
            . '  try { return document.querySelector(' . wp_json_encode('input[name="wpsn_ai_readiness[bots][allow][') . ' + key + ' . wp_json_encode(']"]') . '); } catch(e){ return null; }\n'
            . '}\n'
            . 'function setKey(key, checked){\n'
            . '  var el = elByKey(key);\n'
            . '  if (el) { el.checked = !!checked; }\n'
            . '}\n'
            . 'function setAll(checked){\n'
            . '  ALL.forEach(function(k){ setKey(k, checked); });\n'
            . '}\n'
            . 'function applyPreset(p){\n'
            . '  if (p === "allow_all") { setAll(true); return; }\n'
            . '  if (p === "block_all") { setAll(false); return; }\n'
            . '  if (p === "block_training") { setAll(true); TRAINING.forEach(function(k){ setKey(k, false); }); return; }\n'
            . '  if (p === "search_only") { setAll(false); SEARCH.forEach(function(k){ setKey(k, true); }); return; }\n'
            . '}\n'
            . 'document.addEventListener("click", function(e){\n'
            . '  var t = e.target;\n'
            . '  if (!t) return;\n'
            . '  var btn = (t.closest ? t.closest("button[data-wpsn-bot-preset]") : null);\n'
            . '  if (!btn) return;\n'
            . '  e.preventDefault();\n'
            . '  applyPreset(btn.getAttribute("data-wpsn-bot-preset") || "");\n'
            . '});\n'
            . '})();</script>';

        foreach ($groups as $groupName => $botsInGroup) {
            echo '<fieldset style="margin:0 0 14px 0; padding:10px 12px; border:1px solid rgba(0,0,0,.08); border-radius:8px; max-width: 920px;">';
            echo '<legend style="font-weight:600; padding:0 6px;">' . esc_html($groupName) . '</legend>';

            foreach ($botsInGroup as $key => $bot) {
                $isAllowed = array_key_exists($key, $allow) ? (bool) $allow[$key] : (bool) ($bot['default_allow'] ?? true);
                $label = isset($bot['label']) ? (string) $bot['label'] : $key;
                $ua = isset($bot['ua']) ? (string) $bot['ua'] : '';
                $purpose = isset($bot['purpose']) ? (string) $bot['purpose'] : '';

                echo '<label style="display:block; margin:6px 0;">';
                echo '<input type="checkbox" name="wpsn_ai_readiness[bots][allow][' . esc_attr($key) . ']" value="1" ' . checked($isAllowed, true, false) . ' /> ';
                echo '<strong>' . esc_html($label) . '</strong>'; 
                if ($ua !== '' && $ua !== $label) {
                    echo ' <code style="opacity:.7">' . esc_html($ua) . '</code>';
                }
                if ($purpose !== '') {
                    echo '<br/><span class="description">' . esc_html($purpose) . '</span>';
                }
                echo '</label>';
            }

            echo '</fieldset>';
        }

        echo '<p class="description">' . esc_html__('Checked = allow (no explicit block). Unchecked = add "Disallow: /" for that user-agent.', 'wp-seo-next') . '</p>';

        echo '</td></tr>';

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

        submit_button(__('Save crawler settings', 'wp-seo-next'));
        echo '</form>';

        // Preview robots block.
        $blocked = [];
        foreach ($catalog as $key => $bot) {
            $isAllowed = array_key_exists($key, $allow) ? (bool) $allow[$key] : (bool) ($bot['default_allow'] ?? true);
            if (!$isAllowed) {
                $ua = isset($bot['ua']) ? (string) $bot['ua'] : '';
                if ($ua !== '') {
                    $blocked[] = $ua;
                }
            }
        }

        echo '<h2>' . esc_html__('Robots.txt snippet', 'wp-seo-next') . '</h2>';
        if (!$enabled) {
            echo '<p class="description">' . esc_html__('AI crawler controls are disabled, so nothing will be appended.', 'wp-seo-next') . '</p>';
        }

        $brand = Branding::brand($this->settings);
        $brand = sanitize_text_field($brand);
        $brand = str_replace(['--', '>', '<'], '', $brand);
        $snippet = "# " . ($brand !== '' ? ($brand . " — ") : '') . "AI crawler controls\n";
        if ($enabled && !empty($blocked)) {
            foreach ($blocked as $ua) {
                $snippet .= "User-agent: {$ua}\nDisallow: /\n\n";
            }
        } else {
            $snippet .= "# (no bots blocked)\n";
        }

        echo '<pre class="wpsn-pre">' . esc_html(rtrim($snippet) . "\n") . '</pre>';

        if ($blockerEnabled) {
            $desc = sprintf(
                /* translators: %d is HTTP status code */
                __('Bot Blocker is enabled (HTTP %d). Requests from blocked bots will be blocked server-side. robots.txt and llms.txt are always allowed.', 'wp-seo-next'),
                (int) $blockerCode
            );
            echo '<p class="description">' . esc_html($desc) . '</p>';
        }
    }

    private function renderStatsTab(): void
    {
        $all = $this->repo->getAll();
        $ar = isset($all['ai_readiness']) && is_array($all['ai_readiness']) ? $all['ai_readiness'] : [];
        $bots = isset($ar['bots']) && is_array($ar['bots']) ? $ar['bots'] : [];
        $allow = isset($bots['allow']) && is_array($bots['allow']) ? $bots['allow'] : [];
        $blocker = isset($bots['blocker']) && is_array($bots['blocker']) ? $bots['blocker'] : [];

        $statsEnabled = !empty($blocker['stats_enabled']);

        $days = isset($_GET['days']) ? (int) $_GET['days'] : 7;
        if (!in_array($days, [1, 7, 30, 90, 180, 365], true)) {
            $days = 7;
        }

        echo '<h2 style="margin-top:0;">' . esc_html__('Bot Blocker stats', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('Aggregated counters for blocked AI bots (no IPs stored). Enable it in the AI Crawlers tab under Bot Blocker.', 'wp-seo-next') . '</p>';

        // Controls.
        echo '<div style="margin:12px 0; display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';

        echo '<form method="get" action="' . esc_url(admin_url('tools.php')) . '">';
        echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
        echo '<input type="hidden" name="view" value="stats" />';
        echo '<label>' . esc_html__('Period', 'wp-seo-next') . ' ';
        echo '<select name="days">';
        foreach ([1, 7, 30, 90, 180, 365] as $d) {
            $label = $d === 1 ? __('Last 24h', 'wp-seo-next') : sprintf(
                /* translators: %d is number of days */
                __('Last %d days', 'wp-seo-next'),
                (int) $d
            );
            echo '<option value="' . esc_attr((string) $d) . '" ' . selected($days, $d, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select></label> ';
        submit_button(__('Apply', 'wp-seo-next'), 'secondary', '', false);
        echo '</form>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_ai_readiness_export_bot_stats" />';
        echo '<input type="hidden" name="days" value="' . esc_attr((string) $days) . '" />';
        wp_nonce_field('wpsn_ai_readiness_export_bot_stats');
        submit_button(__('Export CSV', 'wp-seo-next'), 'secondary', 'submit', false);
        echo '</form>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" onsubmit="return confirm(' . wp_json_encode(__('Clear all bot stats? This cannot be undone.', 'wp-seo-next')) . ');">';
        echo '<input type="hidden" name="action" value="wpsn_ai_readiness_clear_bot_stats" />';
        wp_nonce_field('wpsn_ai_readiness_clear_bot_stats');
        submit_button(__('Clear stats', 'wp-seo-next'), 'delete', 'submit', false);
        echo '</form>';

        echo '</div>';

        if (!$statsEnabled) {
            echo '<div class="notice notice-warning"><p>' . esc_html__('Stats are disabled. Enable “Store aggregated bot-block stats” in AI Crawlers → Bot Blocker.', 'wp-seo-next') . '</p></div>';
            return;
        }

        $repo = new BotBlockStatsRepository();
        $totalHits = $repo->totalHits($days);
        $rows = $repo->totals($days);

        echo '<p><strong>' . esc_html__('Blocked hits (total):', 'wp-seo-next') . '</strong> ' . esc_html(number_format_i18n($totalHits)) . '</p>';

        $catalog = AiBotsCatalog::catalog();

        echo '<table class="widefat striped" style="max-width: 1100px;">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('Bot', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Group', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Hits', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Status', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Last seen', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Last path', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Last user-agent', 'wp-seo-next') . '</th>';
        echo '</tr></thead>';
        echo '<tbody>';

        if (empty($rows)) {
            echo '<tr><td colspan="7">' . esc_html__('No blocked hits yet for the selected period.', 'wp-seo-next') . '</td></tr>';
        } else {
            foreach ($rows as $r) {
                $key = isset($r['bot_key']) ? sanitize_key((string) $r['bot_key']) : '';
                $label = isset($r['bot_label']) ? (string) $r['bot_label'] : $key;
                $hits = isset($r['hits']) ? (int) $r['hits'] : 0;
                $lastSeen = isset($r['last_seen']) ? (string) $r['last_seen'] : '';
                $lastPath = isset($r['last_path']) ? (string) $r['last_path'] : '';
                $lastUA = isset($r['last_user_agent']) ? (string) $r['last_user_agent'] : '';

                $group = __('Other', 'wp-seo-next');
                $defaultAllow = true;
                if ($key !== '' && isset($catalog[$key]) && is_array($catalog[$key])) {
                    $group = isset($catalog[$key]['group']) ? (string) $catalog[$key]['group'] : $group;
                    $defaultAllow = (bool) ($catalog[$key]['default_allow'] ?? true);
                }

                $isAllowed = $defaultAllow;
                if ($key !== '' && array_key_exists($key, $allow)) {
                    $isAllowed = (bool) $allow[$key];
                }

                $status = $isAllowed ? __('Allowed', 'wp-seo-next') : __('Blocked', 'wp-seo-next');
                $badgeClass = $isAllowed ? 'ok' : 'warn';

                $lastSeenOut = $lastSeen !== '' && function_exists('get_date_from_gmt')
                    ? (string) get_date_from_gmt($lastSeen, 'Y-m-d H:i')
                    : ($lastSeen !== '' ? $lastSeen : '');

                echo '<tr>';
                echo '<td><strong>' . esc_html($label !== '' ? $label : $key) . '</strong><br/><code style="opacity:.75">' . esc_html($key) . '</code></td>';
                echo '<td>' . esc_html($group) . '</td>';
                echo '<td>' . esc_html(number_format_i18n($hits)) . '</td>';
                echo '<td><span class="wpsn-badge ' . esc_attr($badgeClass) . '">' . esc_html($status) . '</span></td>';
                echo '<td>' . esc_html($lastSeenOut !== '' ? $lastSeenOut : '-') . '</td>';
                echo '<td><code style="opacity:.85">' . esc_html($lastPath !== '' ? $lastPath : '-') . '</code></td>';
                echo '<td><code style="opacity:.75">' . esc_html($lastUA !== '' ? $lastUA : '-') . '</code></td>';
                echo '</tr>';
            }
        }

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

    public function handleSaveBots(): void
    {
        if (!current_user_can(Capabilities::CAP_TOOLS)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_ai_readiness_save_bots');

        $incoming = $_POST['wpsn_ai_readiness'] ?? [];
        if (!is_array($incoming)) {
            $incoming = [];
        }

        $all = $this->repo->getAll();
        $prev = $all['ai_readiness']['bots'] ?? [];
        if (!is_array($prev)) {
            $prev = [];
        }

        if (!isset($all['ai_readiness']) || !is_array($all['ai_readiness'])) {
            $all['ai_readiness'] = [];
        }
        $all['ai_readiness']['bots'] = isset($incoming['bots']) && is_array($incoming['bots']) ? $incoming['bots'] : [];

        $this->repo->update($all);
        $this->settings->flush();

        $newAll = $this->repo->getAll();
        $new = $newAll['ai_readiness']['bots'] ?? [];
        if (!is_array($new)) {
            $new = [];
        }

        $msg = $this->describeBotsChange($prev, $new);
        (new PolicyLogRepository())->log('bots', 'bots_updated', $msg, $prev, $new, get_current_user_id());

        $this->redirectWithNotice(__('Crawler settings saved.', 'wp-seo-next'), 'success', ['view' => 'crawlers']);
    }

    public function handleClearBotStats(): void
    {
        if (!current_user_can(Capabilities::CAP_TOOLS)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_ai_readiness_clear_bot_stats');

        (new BotBlockStatsRepository())->clearAll();

        (new PolicyLogRepository())->log('bots', 'bot_stats_cleared', __('Cleared Bot Blocker stats.', 'wp-seo-next'), [], [], get_current_user_id());

        $this->redirectWithNotice(__('Stats cleared.', 'wp-seo-next'), 'success', ['view' => 'stats']);
    }

    public function handleExportBotStats(): void
    {
        if (!current_user_can(Capabilities::CAP_TOOLS)) {
            wp_die(esc_html__('You do not have permission to do this action.', 'wp-seo-next'));
        }
        check_admin_referer('wpsn_ai_readiness_export_bot_stats');

        $days = isset($_POST['days']) ? (int) $_POST['days'] : 7;
        if (!in_array($days, [1, 7, 30, 90, 180, 365], true)) {
            $days = 7;
        }

        $repo = new BotBlockStatsRepository();
        $rows = $repo->totals($days);

        $filename = 'wpsn-bot-blocker-stats-' . gmdate('Ymd-His') . '-last-' . $days . 'd.csv';
        nocache_headers();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename=' . $filename);

        $out = fopen('php://output', 'w');
        if ($out === false) {
            exit;
        }

        fputcsv($out, ['bot_key', 'bot_label', 'hits', 'last_seen', 'last_path', 'last_user_agent']);
        foreach ($rows as $r) {
            fputcsv($out, [
                (string) ($r['bot_key'] ?? ''),
                (string) ($r['bot_label'] ?? ''),
                (int) ($r['hits'] ?? 0),
                (string) ($r['last_seen'] ?? ''),
                (string) ($r['last_path'] ?? ''),
                (string) ($r['last_user_agent'] ?? ''),
            ]);
        }

        fclose($out);
        exit;
    }

    private function describeBotsChange(array $prev, array $new): string
    {
        $prevEnabled = !empty($prev['enabled']);
        $newEnabled = !empty($new['enabled']);
        $allowPrev = isset($prev['allow']) && is_array($prev['allow']) ? $prev['allow'] : [];
        $allowNew = isset($new['allow']) && is_array($new['allow']) ? $new['allow'] : [];

        $blockPrev = isset($prev['blocker']) && is_array($prev['blocker']) ? $prev['blocker'] : [];
        $blockNew = isset($new['blocker']) && is_array($new['blocker']) ? $new['blocker'] : [];
        $prevBlockerEnabled = !empty($blockPrev['enabled']);
        $newBlockerEnabled = !empty($blockNew['enabled']);

        $prevBlockerCode = isset($blockPrev['status_code']) ? (int) $blockPrev['status_code'] : 403;
        $newBlockerCode  = isset($blockNew['status_code']) ? (int) $blockNew['status_code'] : 403;
        if (!in_array($prevBlockerCode, [403, 429], true)) {
            $prevBlockerCode = 403;
        }
        if (!in_array($newBlockerCode, [403, 429], true)) {
            $newBlockerCode = 403;
        }

        $prevRetryAfter = isset($blockPrev['retry_after']) ? (int) $blockPrev['retry_after'] : 60;
        $newRetryAfter  = isset($blockNew['retry_after']) ? (int) $blockNew['retry_after'] : 60;

        $prevAllowLoggedIn = array_key_exists('allow_logged_in', $blockPrev) ? !empty($blockPrev['allow_logged_in']) : true;
        $newAllowLoggedIn  = array_key_exists('allow_logged_in', $blockNew) ? !empty($blockNew['allow_logged_in']) : true;

        $prevStatsEnabled = !empty($blockPrev['stats_enabled']);
        $newStatsEnabled  = !empty($blockNew['stats_enabled']);
        $prevRetention = isset($blockPrev['stats_retention_days']) ? (int) $blockPrev['stats_retention_days'] : 90;
        $newRetention  = isset($blockNew['stats_retention_days']) ? (int) $blockNew['stats_retention_days'] : 90;

        $changedAllowed = [];
        $changedBlocked = [];
        foreach (AiBotsCatalog::catalog() as $key => $bot) {
            $p = array_key_exists($key, $allowPrev) ? (bool) $allowPrev[$key] : (bool) ($bot['default_allow'] ?? true);
            $n = array_key_exists($key, $allowNew) ? (bool) $allowNew[$key] : (bool) ($bot['default_allow'] ?? true);
            if ($p === $n) {
                continue;
            }
            $label = isset($bot['label']) ? (string) $bot['label'] : $key;
            if ($n) {
                $changedAllowed[] = $label;
            } else {
                $changedBlocked[] = $label;
            }
        }

        $parts = [];
        if ($prevEnabled !== $newEnabled) {
            $parts[] = $newEnabled ? __('Enabled AI crawler controls.', 'wp-seo-next') : __('Disabled AI crawler controls.', 'wp-seo-next');
        }

        // Bot Blocker changes.
        if ($prevBlockerEnabled !== $newBlockerEnabled) {
            $parts[] = $newBlockerEnabled ? __('Enabled Bot Blocker.', 'wp-seo-next') : __('Disabled Bot Blocker.', 'wp-seo-next');
        }
        if ($newBlockerEnabled && $prevBlockerCode !== $newBlockerCode) {
            $parts[] = sprintf(
                /* translators: %d is HTTP status code */
                __('Bot Blocker response: %d.', 'wp-seo-next'),
                (int) $newBlockerCode
            );
        }
        if ($newBlockerEnabled && $newBlockerCode === 429 && $prevRetryAfter !== $newRetryAfter) {
            $parts[] = sprintf(
                /* translators: %d is seconds */
                __('Bot Blocker Retry-After: %ds.', 'wp-seo-next'),
                (int) $newRetryAfter
            );
        }
        if ($prevAllowLoggedIn !== $newAllowLoggedIn) {
            $parts[] = $newAllowLoggedIn
                ? __('Bot Blocker bypass for logged-in users enabled.', 'wp-seo-next')
                : __('Bot Blocker will also block logged-in users.', 'wp-seo-next');
        }

        if ($prevStatsEnabled !== $newStatsEnabled) {
            $parts[] = $newStatsEnabled
                ? __('Enabled Bot Blocker stats.', 'wp-seo-next')
                : __('Disabled Bot Blocker stats.', 'wp-seo-next');
        }
        if ($newStatsEnabled && $prevRetention !== $newRetention) {
            $parts[] = sprintf(
                /* translators: %d is number of days */
                __('Bot Blocker stats retention: %d days.', 'wp-seo-next'),
                (int) $newRetention
            );
        }
        if (!empty($changedBlocked)) {
            $parts[] = sprintf(
                /* translators: %s is list of bots */
                __('Blocked: %s.', 'wp-seo-next'),
                implode(', ', $changedBlocked)
            );
        }
        if (!empty($changedAllowed)) {
            $parts[] = sprintf(
                /* translators: %s is list of bots */
                __('Allowed: %s.', 'wp-seo-next'),
                implode(', ', $changedAllowed)
            );
        }

        if (empty($parts)) {
            return __('Updated AI crawler settings.', 'wp-seo-next');
        }
        return implode(' ', $parts);
    }

    private function renderAuditTab(): void
    {
        $repo = new PolicyLogRepository();

        $context = isset($_GET['context']) ? sanitize_key((string) wp_unslash($_GET['context'])) : 'all';
        if (!in_array($context, ['all', 'llms', 'bots'], true)) {
            $context = 'all';
        }

        $action = isset($_GET['action_filter']) ? sanitize_key((string) wp_unslash($_GET['action_filter'])) : 'all';
        $actions = $repo->distinctActions();
        if ($action !== 'all' && !in_array($action, $actions, true)) {
            $action = 'all';
        }

        $perPage = isset($_GET['per_page']) ? (int) $_GET['per_page'] : 20;
        if (!in_array($perPage, [20, 50, 100, 200], true)) {
            $perPage = 20;
        }

        $paged = isset($_GET['paged']) ? (int) $_GET['paged'] : 1;
        if ($paged < 1) {
            $paged = 1;
        }
        $offset = ($paged - 1) * $perPage;

        $filters = [
            'context' => $context,
            'action' => $action,
        ];

        $total = $repo->count($filters);
        $rows = $repo->query($filters, $perPage, $offset);

        echo '<h2 style="margin-top:0;">' . esc_html__('Policy audit log', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('Records changes made in AI Search Readiness settings (who, when, what).', 'wp-seo-next') . '</p>';

        // Filters.
        echo '<form method="get" action="' . esc_url(admin_url('tools.php')) . '" style="margin: 10px 0; max-width: 1200px;">';
        echo '<input type="hidden" name="page" value="' . esc_attr(self::PAGE_SLUG) . '" />';
        echo '<input type="hidden" name="view" value="audit" />';
        echo '<p class="search-box" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">';
        echo '<select name="context">';
        echo '<option value="all" ' . selected($context, 'all', false) . '>' . esc_html__('All contexts', 'wp-seo-next') . '</option>';
        echo '<option value="llms" ' . selected($context, 'llms', false) . '>' . esc_html__('llms.txt', 'wp-seo-next') . '</option>';
        echo '<option value="bots" ' . selected($context, 'bots', false) . '>' . esc_html__('AI crawlers', 'wp-seo-next') . '</option>';
        echo '</select>';

        echo '<select name="action_filter">';
        echo '<option value="all" ' . selected($action, 'all', false) . '>' . esc_html__('All actions', 'wp-seo-next') . '</option>';
        foreach ($actions as $a) {
            echo '<option value="' . esc_attr($a) . '" ' . selected($action, $a, false) . '>' . esc_html($a) . '</option>';
        }
        echo '</select>';

        echo '<select name="per_page">';
        foreach ([20, 50, 100, 200] as $n) {
            echo '<option value="' . (int) $n . '" ' . selected($perPage, (int) $n, false) . '>' . esc_html(sprintf(__('%d / page', 'wp-seo-next'), (int) $n)) . '</option>';
        }
        echo '</select>';

        echo '<input type="submit" class="button" value="' . esc_attr__('Filter', 'wp-seo-next') . '" />';
        echo '<a class="button button-secondary" href="' . esc_url($this->pageUrl(['view' => 'audit'])) . '">' . esc_html__('Reset', 'wp-seo-next') . '</a>';
        echo '</p>';
        echo '</form>';

        if ($total <= 0) {
            echo '<p class="description">' . esc_html__('No audit entries yet.', 'wp-seo-next') . '</p>';
            return;
        }

        $totalPages = (int) ceil($total / $perPage);

        echo '<table class="widefat striped" style="max-width: 1200px;">';
        echo '<thead><tr>';
        echo '<th style="width:70px;">' . esc_html__('ID', 'wp-seo-next') . '</th>';
        echo '<th style="width:170px;">' . esc_html__('Date (UTC)', 'wp-seo-next') . '</th>';
        echo '<th style="width:140px;">' . esc_html__('User', 'wp-seo-next') . '</th>';
        echo '<th style="width:90px;">' . esc_html__('Context', 'wp-seo-next') . '</th>';
        echo '<th style="width:120px;">' . esc_html__('Action', 'wp-seo-next') . '</th>';
        echo '<th>' . esc_html__('Message', 'wp-seo-next') . '</th>';
        echo '<th style="width:260px;">' . esc_html__('Details', 'wp-seo-next') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($rows as $r) {
            $id = isset($r['id']) ? (int) $r['id'] : 0;
            $created = isset($r['created_at']) ? (string) $r['created_at'] : '';
            $userId = isset($r['user_id']) ? (int) $r['user_id'] : 0;
            $ctx = isset($r['context']) ? (string) $r['context'] : '';
            $act = isset($r['action']) ? (string) $r['action'] : '';
            $msg = isset($r['message']) ? (string) $r['message'] : '';

            $userHtml = '<span class="description">' . esc_html__('System', 'wp-seo-next') . '</span>';
            if ($userId > 0) {
                $user = get_user_by('id', $userId);
                if ($user instanceof \WP_User) {
                    $name = $user->display_name !== '' ? $user->display_name : $user->user_login;
                    $editUrl = admin_url('user-edit.php?user_id=' . $userId);
                    $userHtml = '<a href="' . esc_url($editUrl) . '">' . esc_html($name) . '</a>';
                } else {
                    $userHtml = '#' . (int) $userId;
                }
            }

            $old = isset($r['old_value']) ? (string) $r['old_value'] : '';
            $new = isset($r['new_value']) ? (string) $r['new_value'] : '';

            echo '<tr>';
            echo '<td>' . (int) $id . '</td>';
            echo '<td>' . esc_html($created) . '</td>';
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo '<td>' . $userHtml . '</td>';
            echo '<td><code>' . esc_html($ctx) . '</code></td>';
            echo '<td><code>' . esc_html($act) . '</code></td>';
            echo '<td>' . esc_html($msg) . '</td>';

            echo '<td>';
            echo '<details><summary>' . esc_html__('View JSON', 'wp-seo-next') . '</summary>';
            echo '<div style="margin-top:6px;">';
            if ($old !== '') {
                echo '<div style="margin-bottom:8px;"><strong>' . esc_html__('Old', 'wp-seo-next') . '</strong><pre class="wpsn-pre" style="max-height:160px; overflow:auto;">' . esc_html($old) . '</pre></div>';
            }
            if ($new !== '') {
                echo '<div><strong>' . esc_html__('New', 'wp-seo-next') . '</strong><pre class="wpsn-pre" style="max-height:160px; overflow:auto;">' . esc_html($new) . '</pre></div>';
            }
            echo '</div>';
            echo '</details>';
            echo '</td>';

            echo '</tr>';
        }

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

        // Pagination.
        if ($totalPages > 1) {
            $baseArgs = [
                'view' => 'audit',
                'context' => $context,
                'action_filter' => $action,
                'per_page' => $perPage,
            ];

            echo '<div class="tablenav" style="max-width:1200px;"><div class="tablenav-pages">';

            $prev = max(1, $paged - 1);
            $next = min($totalPages, $paged + 1);

            if ($paged > 1) {
                echo '<a class="button" href="' . esc_url($this->pageUrl(array_merge($baseArgs, ['paged' => $prev]))) . '">« ' . esc_html__('Prev', 'wp-seo-next') . '</a> ';
            }
            echo '<span class="displaying-num" style="margin:0 8px;">' . esc_html(sprintf(__('%1$d of %2$d pages', 'wp-seo-next'), $paged, $totalPages)) . '</span>';
            if ($paged < $totalPages) {
                echo ' <a class="button" href="' . esc_url($this->pageUrl(array_merge($baseArgs, ['paged' => $next]))) . '">' . esc_html__('Next', 'wp-seo-next') . ' »</a>';
            }

            echo '</div></div>';
        }
    }
}
