<?php

/**
 * Shop2LLM — AI Discovery for Your Website
 *
 * Make your website content discoverable by AI assistants.
 * Works with any WordPress site — with WooCommerce for product AI visibility,
 * or standalone for blog/article AI visibility.
 * Free version supports ChatGPT with AI-friendly catalog, crawler tracking,
 * and auto-generated schema. Upgrade to Pro for 6 AI platforms.
 *
 * ---
 * Plugin Name: Shop2LLM — AI Visibility
 * Plugin URI:  https://shop2llm.com/
 * Description: AI Search Visibility & GEO for WordPress — make your site discoverable by ChatGPT, Perplexity, Claude & AI search engines. AI visibility score, llms.txt, MCP server, AI crawler tracking, and AI SEO schema in 2 clicks.
 * Version: 1.3.6
 * Author:      FinCore
 * Author URI:  https://shop2llm.com/
 * License:     GPL-2.0+
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: shop-to-llm
 * Domain Path: /languages
 * Requires PHP: 7.4
 * Requires at least: 6.0
 * Tested up to: 7.0
 * WC requires at least: 8.0
 * WC tested up to: 10.8
 * ---
 */

declare(strict_types=1);

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

define('SHOP_TO_LLM_VERSION', '1.3.6');
define('SHOP_TO_LLM_DIR', plugin_dir_path(__FILE__));
define('SHOP_TO_LLM_URL', plugin_dir_url(__FILE__));

/**
 * Check if WooCommerce is installed and active.
 */
function shop_to_llm_has_woocommerce(): bool
{
    return class_exists('WooCommerce');
}

// ─── PHP 7.4 Polyfills ─────────────────────────────────────────────────

if (!function_exists('str_starts_with')) {
    function str_starts_with(string $haystack, string $needle): bool
    {
        return $needle === '' || substr($haystack, 0, strlen($needle)) === $needle;
    }
}
if (!function_exists('str_contains')) {
    function str_contains(string $haystack, string $needle): bool
    {
        return $needle === '' || strpos($haystack, $needle) !== false;
    }
}

// ─── Lifecycle ─────────────────────────────────────────────────────────

register_activation_hook(__FILE__, 'shop_to_llm_activate');
register_deactivation_hook(__FILE__, 'shop_to_llm_deactivate');

// Translation — load textdomain for i18n
$shop2llm_mofile = __DIR__ . '/languages/shop-to-llm-' . determine_locale() . '.mo';
if (file_exists($shop2llm_mofile)) {
    load_textdomain('shop-to-llm', $shop2llm_mofile);
}
unset($shop2llm_mofile);

// Register custom cron schedules ('monthly' and 'weekly' are not built-in)
add_filter('cron_schedules', 'shop_to_llm_cron_schedules');
function shop_to_llm_cron_schedules(array $schedules): array
{
    if (!isset($schedules['weekly'])) {
        $schedules['weekly'] = [
            'interval' => WEEK_IN_SECONDS,
            'display' => __('Once Weekly', 'shop-to-llm'),
        ];
    }
    if (!isset($schedules['monthly'])) {
        $schedules['monthly'] = [
            'interval' => MONTH_IN_SECONDS,
            'display' => __('Once Monthly', 'shop-to-llm'),
        ];
    }
    return $schedules;
}

// HPOS compatibility (required for WooCommerce Marketplace)
if (shop_to_llm_has_woocommerce()) {
    add_action('before_woocommerce_init', function () {
        if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
            \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
        }
    });
}

function shop_to_llm_activate(): void
{
    // ─── Version compatibility check ──────────────────────────────────
    $minWpVersion = '6.0';
    $minPhpVersion = '7.4';
    $wpVersion = get_bloginfo('version');
    $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

    if (version_compare($wpVersion, $minWpVersion, '<')) {
        deactivate_plugins(plugin_basename(__FILE__));
        wp_die(sprintf(
            /* translators: 1: plugin name, 2: required WP version, 3: current WP version */
            esc_html__('%1$s requires WordPress %2$s or higher. Your site is running WordPress %3$s. Please upgrade WordPress before activating this plugin.', 'shop-to-llm'),
            'Shop2LLM',
            esc_html($minWpVersion),
            esc_html($wpVersion)
        ), 'Plugin Activation Error', ['back_link' => true, 'response' => 200]);
    }

    if (version_compare($phpVersion, $minPhpVersion, '<')) {
        deactivate_plugins(plugin_basename(__FILE__));
        wp_die(sprintf(
            /* translators: 1: plugin name, 2: required PHP version, 3: current PHP version */
            esc_html__('%1$s requires PHP %2$s or higher. Your site is running PHP %3$s. Please upgrade PHP before activating this plugin.', 'shop-to-llm'),
            'Shop2LLM',
            esc_html($minPhpVersion),
            esc_html($phpVersion)
        ), 'Plugin Activation Error', ['back_link' => true, 'response' => 200]);
    }

    if (!shop_to_llm_has_woocommerce()) {
        set_transient('shop_to_llm_woocommerce_warning', 1, DAY_IN_SECONDS);
    }

    add_option('shop_to_llm_enabled', 'yes');
    add_option('shop_to_llm_api_key', wp_generate_password(32, false));
    add_option('shop_to_llm_catalog_search', 'yes');
    add_option('shop_to_llm_analytics', 'no');
    add_option('shop_to_llm_show_badge', 'yes');
    // v1.3.3: Product sync consent — defaults to 'no', user opts in via settings or onboarding
    add_option('shop2llm_product_sync_consent', 'no');
    // Show onboarding wizard on first visit after activation
    add_option('shop_to_llm_just_activated', '1');
    // Reset value card dismissed state so it shows on activation
    delete_option('shop2llm_value_card_dismissed');
    shop_to_llm_log_event('activated');
    flush_rewrite_rules();

    // Create AI crawler activity table for persistent tracking
    shop_to_llm_create_crawler_table();

    // Create AI referral tracking table
    shop_to_llm_create_ai_referrals_table();

    // Create SEO redirect and 404 log tables
    shop_to_llm_create_seo_tables();

    // SEO default options
    shop_to_llm_set_default_options();
    add_option('shop_to_llm_seo_home_title', '');
    add_option('shop_to_llm_seo_home_desc', '');
    add_option('shop_to_llm_title_sep', '|');
    add_option('shop_to_llm_gsc_client_id', '');
    add_option('shop_to_llm_gsc_client_secret', '');
    add_option('shop_to_llm_gsc_access_token', '');
    add_option('shop_to_llm_gsc_refresh_token', '');
    add_option('shop_to_llm_gsc_expires_at', '');
    add_option('shop_to_llm_gsc_site_url', '');

    // Schedule daily cleanup of crawler logs older than 30 days
    if (!wp_next_scheduled('shop2llm_cleanup_crawler_logs')) {
        wp_schedule_event(time(), 'daily', 'shop2llm_cleanup_crawler_logs');
    }

    // v1.3.3: Removed auto-ping on activation (WP Guideline 7 compliance).
    // Ping is now sent only when user explicitly enables Analytics in Settings.

    // v1.3.3: Removed auto product sync on activation (WP Guideline 7 compliance).
    // Product sync is now opt-in via Settings → Product Sync Consent, or triggered
    // by the daily cloud heartbeat only if analytics consent is given.

    // Daily cloud heartbeat — refreshes behavior data and active status
    if (!wp_next_scheduled('shop2llm_cloud_heartbeat')) {
        wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'shop2llm_cloud_heartbeat');
    }

    // Weekly visibility snapshot for all published posts
    if (!wp_next_scheduled('shop2llm_weekly_snapshot')) {
        wp_schedule_event(time(), 'weekly', 'shop2llm_weekly_snapshot');
    }

    // Weekly SEO audit report
    if (get_option('shop_to_llm_seo_audit', 'yes') === 'yes' && !wp_next_scheduled('shop2llm_seo_weekly_audit')) {
        wp_schedule_event(time(), 'weekly', 'shop2llm_seo_weekly_audit');
    }

    // Redirect to dashboard after activation
    add_option('shop_to_llm_do_activation_redirect', true);
}

function shop_to_llm_deactivate(): void
{
    wp_clear_scheduled_hook('shop2llm_cloud_heartbeat');
    wp_clear_scheduled_hook('shop2llm_cleanup_crawler_logs');
    wp_clear_scheduled_hook('shop2llm_weekly_snapshot');
    wp_clear_scheduled_hook('shop2llm_seo_weekly_audit');
    // v1.3.3: Clear cached uninstall counts so they're recalculated on next activation
    delete_transient('shop2llm_uninstall_counts');
    flush_rewrite_rules();
}

// ─── REST API Routes ───────────────────────────────────────────────────

add_action('rest_api_init', 'shop_to_llm_register_routes');
add_action('wp', 'shop_to_llm_detect_ai_crawler', 1);

// ─── Admin Hooks ──────────────────────────────────────────────────────

add_action('admin_menu', 'shop_to_llm_add_admin_page');
add_action('admin_enqueue_scripts', 'shop_to_llm_enqueue_admin_assets');
add_action('admin_notices', 'shop_to_llm_render_onboarding_banner');
add_action('admin_notices', 'shop_to_llm_first_crawler_notice');
add_action('admin_notices', 'shop_to_llm_review_request_notice');
add_action('admin_notices', 'shop_to_llm_weekly_crawler_summary');
add_action('admin_notices', 'shop_to_llm_woocommerce_warning_notice');
add_action('admin_notices', 'shop_to_llm_robots_txt_fix_notice');
add_action('admin_init', 'shop_to_llm_activation_redirect');
// GSC OAuth2 callback handler
add_action('admin_init', 'shop_to_llm_gsc_handle_callback');
// Handle GSC disconnect
if (isset($_GET['gsc_disconnect'], $_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'shop2llm_gsc_disconnect')) {
    shop_to_llm_gsc_disconnect();
    wp_safe_redirect(admin_url('admin.php?page=shop2llm&tab=seo'));
    exit;
}
add_action('wp_ajax_shop2llm_dismiss_onboarding', 'shop_to_llm_dismiss_onboarding');
add_action('wp_ajax_shop2llm_cache_community_count', 'shop_to_llm_cache_community_count');
add_action('wp_ajax_shop2llm_dismiss_pro_cta', 'shop_to_llm_dismiss_pro_cta');
add_action('wp_ajax_shop2llm_dismiss_review_card', 'shop_to_llm_dismiss_review_card');
add_action('wp_ajax_shop2llm_test_search', 'shop_to_llm_handle_test_search');

add_action('wp_ajax_shop2llm_fix_item', 'shop_to_llm_handle_fix_item');
add_action('wp_ajax_shop2llm_log_event', 'shop_to_llm_handle_log_event');
add_action('wp_ajax_shop2llm_enable_analytics', 'shop_to_llm_handle_enable_analytics');
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'shop_to_llm_add_action_links');
add_action('wp_head', 'shop_to_llm_inject_product_schema');
add_action('init', 'shop_to_llm_generate_llms_txt');
add_action('shop2llm_cleanup_crawler_logs', 'shop_to_llm_cleanup_old_crawler_logs');
add_filter('query_vars', 'shop_to_llm_llms_txt_query_var');
add_action('template_redirect', 'shop_to_llm_render_llms_txt');
add_action('init', 'shop_to_llm_generate_agents_md');
add_filter('query_vars', 'shop_to_llm_agents_md_query_var');
add_action('template_redirect', 'shop_to_llm_render_agents_md_dossier', 1);
add_action('template_redirect', 'shop_to_llm_render_agents_md');
add_filter('robots_txt', 'shop_to_llm_robots_txt');

// ─── Feature 1: AI Discovery Badge & Verification Page ────────────────

add_action('wp_footer', 'shop_to_llm_render_footer_badge');
add_action('init', 'shop_to_llm_add_visibility_rewrite');
add_filter('query_vars', 'shop_to_llm_visibility_query_var');
add_action('template_redirect', 'shop_to_llm_render_visibility_page');

// ─── Feature 2: AI Referral Tracker ───────────────────────────────────

add_action('template_redirect', 'shop_to_llm_detect_ai_referral', 1);

// ─── Anchoring Feature Hooks ──────────────────────────────────────────

// P0: Admin Bar Real-time Badge
add_action('admin_bar_menu', 'shop_to_llm_admin_bar_crawler_badge', 999);
// P0: Dashboard Widget Recovery
add_action('wp_dashboard_setup', 'shop_to_llm_register_dashboard_widget');
// P0: Uninstall Confirmation Dialog
add_action('admin_footer-plugins.php', 'shop_to_llm_uninstall_confirmation_js');
// P1: AI Visibility Score Meta Box
add_action('add_meta_boxes', 'shop_to_llm_add_visibility_meta_box');
add_action('admin_head', 'shop_to_llm_visibility_meta_box_css');
// P2: AI Visibility Column in Post List
add_filter('manage_edit-post_columns', 'shop_to_llm_add_visibility_column');
add_filter('manage_edit-product_columns', 'shop_to_llm_add_visibility_column');
add_action('manage_post_posts_custom_column', 'shop_to_llm_render_visibility_column', 10, 2);
add_action('manage_product_posts_custom_column', 'shop_to_llm_render_visibility_column', 10, 2);
add_action('save_post', 'shop_to_llm_save_focus_query');
add_action('shop2llm_weekly_snapshot', 'shop_to_llm_weekly_snapshot_callback');

function shop_to_llm_register_routes(): void
{
    if (get_option('shop_to_llm_enabled', 'yes') !== 'yes') {
        return;
    }

    register_rest_route('shop2llm/v1', '/mcp', [
        'methods' => ['POST', 'GET'],
        'callback' => 'shop_to_llm_handle_mcp',
        'permission_callback' => 'shop_to_llm_check_auth',
    ]);

    register_rest_route('shop2llm/v1', '/.well-known/mcp', [
        'methods' => ['GET'],
        'callback' => 'shop_to_llm_mcp_discovery',
        'permission_callback' => '__return_true',
    ]);

    register_rest_route('shop2llm/v1', '/community', [
        'methods' => ['GET'],
        'callback' => 'shop_to_llm_community_stats_endpoint',
        'permission_callback' => '__return_true',
    ]);
}

function shop_to_llm_add_admin_page(): void
{
    add_menu_page(
        'Shop2LLM',
        'Shop2LLM',
        'manage_options',
        'shop2llm',
        'shop_to_llm_render_settings',
        'data:image/svg+xml;base64,' . base64_encode('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M5.5 7C3.5 7 2 8.5 2 10s1.5 3 3.5 3c1.2 0 2-.5 3-1.5L10 10l1.5 1.5c1 1 1.8 1.5 3 1.5 2 0 3.5-1.5 3.5-3S16.5 7 14.5 7c-1.2 0-2 .5-3 1.5L10 10 8.5 8.5C7.5 7.5 6.7 7 5.5 7z"/></svg>'),
        56
    );
}

function shop_to_llm_enqueue_admin_assets(string $hook): void
{
    if ($hook !== 'toplevel_page_shop2llm') {
        return;
    }
    wp_enqueue_style(
        'shop2llm-admin',
        SHOP_TO_LLM_URL . 'assets/css/admin.css',
        [],
        SHOP_TO_LLM_VERSION
    );
    wp_enqueue_script(
        'shop2llm-admin',
        SHOP_TO_LLM_URL . 'assets/js/admin.js',
        [],
        SHOP_TO_LLM_VERSION,
        true
    );
    wp_localize_script('shop2llm-admin', 'shop2llm', [
        'nonce' => wp_create_nonce('shop2llm_admin_nonce'),
        'audit_nonce' => wp_create_nonce('shop2llm_seo_audit'),
        'ajaxurl' => admin_url('admin-ajax.php'),
        'restUrl' => rest_url('shop2llm/v1/'),
    ]);
}

function shop_to_llm_add_action_links(array $links): array
{
    $settings_link = '<a href="' . admin_url('admin.php?page=shop2llm') . '">' . esc_html__('Settings', 'shop-to-llm') . '</a>';
    array_unshift($links, $settings_link);
    return $links;
}

function shop_to_llm_activation_redirect(): void
{
    if (get_option('shop_to_llm_do_activation_redirect', false)) {
        delete_option('shop_to_llm_do_activation_redirect');
        // Only redirect if this is a single plugin activation (not bulk)
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- presence check only, no form processing
        if (!isset($_REQUEST['activate-multi'])) {
            wp_safe_redirect(admin_url('admin.php?page=shop2llm'));
            exit;
        }
    }
}

// ─── Onboarding Wizard ────────────────────────────────────────────────

function shop_to_llm_first_crawler_notice(): void
{
    $platform = get_transient('shop_to_llm_first_crawler_admin_notice');
    if (!$platform) {
        return;
    }
    // Only show on non-Shop2LLM pages to avoid cluttering the dashboard
    $screen = get_current_screen();
    if ($screen && $screen->id === 'toplevel_page_shop2llm') {
        return;
    }
    delete_transient('shop_to_llm_first_crawler_admin_notice');
    ?>
    <div class="notice notice-success is-dismissible" style="border-left-color:#166534">
        <p style="font-size:14px">
            <strong><?php echo shop_to_llm_has_woocommerce() ? esc_html(sprintf(/* translators: %s: platform name */ __('🎉 %s just discovered your store!', 'shop-to-llm'), $platform)) : esc_html(sprintf(/* translators: %s: platform name */ __('🎉 %s just discovered your site!', 'shop-to-llm'), $platform)); ?></strong><br>
            <?php echo shop_to_llm_has_woocommerce() ? esc_html__('Your products are now visible to AI assistants. View your AI visibility dashboard:', 'shop-to-llm') : esc_html__('Your content is now visible to AI assistants. View your AI visibility dashboard:', 'shop-to-llm'); ?>
            <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm')); ?>"><?php echo esc_html__('Open Shop2LLM Dashboard', 'shop-to-llm'); ?></a>
        </p>
    </div>
    <?php
}

function shop_to_llm_review_request_notice(): void
{
    $milestone = get_transient('shop_to_llm_review_request');
    if (!$milestone) {
        return;
    }
    // Only show on non-Shop2LLM pages to avoid cluttering the dashboard
    $screen = get_current_screen();
    if ($screen && $screen->id === 'toplevel_page_shop2llm') {
        return;
    }
    delete_transient('shop_to_llm_review_request');
    // Pull real community count from transient (set by async API call)
    $communityCount = get_transient('shop2llm_community_count');
    $communityText = $communityCount && $communityCount > 0
        ? sprintf(/* translators: %s: formatted community count */ __('🔥 %s stores are making their sites AI-visible', 'shop-to-llm'), number_format((int)$communityCount))
        : __('Join thousands of stores making their sites AI-visible', 'shop-to-llm');
    ?>
    <div class="notice notice-info is-dismissible" style="border-left-color:#6366f1">
        <p style="font-size:14px">
            <strong><?php echo esc_html(sprintf(/* translators: %s: milestone label */ __('🚀 %s — and AI consumers are already searching!', 'shop-to-llm'), $milestone)); ?></strong><br>
            <?php echo esc_html($communityText); ?><br>
            <a href="https://wordpress.org/support/plugin/shop2llm/reviews/#new-post" target="_blank" style="display:inline-block;margin-top:6px;padding:5px 14px;background:#6366f1;color:#fff;border-radius:4px;text-decoration:none;font-weight:600">
                <?php echo esc_html__('⭐ Leave a Review on WP.org', 'shop-to-llm'); ?>
            </a>
        </p>
    </div>
    <?php
}

function shop_to_llm_weekly_crawler_summary(): void
{
    // Only show once per week, on non-Shop2LLM pages
    $screen = get_current_screen();
    if ($screen && $screen->id === 'toplevel_page_shop2llm') {
        return;
    }

    $transientKey = 'shop2llm_weekly_summary_dismissed';
    if (get_transient($transientKey)) {
        return;
    }

    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$exists) {
        return;
    }

    // Count crawlers this week
    $weekAgo = gmdate('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS);
    $weekCount = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(*) FROM {$table} WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $weekAgo
    ));
    if ($weekCount < 1) {
        return;
    }

    $uniqueBots = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(DISTINCT bot_name) FROM {$table} WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $weekAgo
    ));
    $totalAll = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

    // Mark as shown for 7 days
    set_transient($transientKey, 1, 7 * DAY_IN_SECONDS);
    ?>
    <div class="notice notice-info is-dismissible" style="border-left-color:#8b5cf6">
        <p style="font-size:13px">
            <strong><?php echo esc_html(sprintf(
                /* translators: 1: number of crawler visits, 2: number of AI bots */
                __('🤖 %1$d AI crawler visits from %2$d platforms this week', 'shop-to-llm'),
                $weekCount,
                $uniqueBots
            )); ?></strong><br>
            <?php echo esc_html(sprintf(
                /* translators: %d: total crawler records */
                __('%d total crawler records accumulated. View your AI visibility trends:', 'shop-to-llm'),
                $totalAll
            )); ?>
            <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm')); ?>" style="margin-left:4px;font-weight:600">
                <?php echo esc_html__('Open Dashboard →', 'shop-to-llm'); ?>
            </a>
        </p>
    </div>
    <?php
}

function shop_to_llm_woocommerce_warning_notice(): void
{
    if (!get_transient('shop_to_llm_woocommerce_warning')) {
        return;
    }
    delete_transient('shop_to_llm_woocommerce_warning');
    ?>
    <div class="notice notice-warning is-dismissible">
        <p>
            <strong>Shop2LLM:</strong>
            <?php echo esc_html__('WooCommerce is not active. Product-related features (product search, product feed, product AI score) are disabled. Blog and page content is still discoverable by AI. Install WooCommerce for full product AI visibility.', 'shop-to-llm'); ?>
        </p>
    </div>
    <?php
}

function shop_to_llm_dismiss_onboarding(): void
{
    if (!current_user_can('manage_options')) {
        wp_die('unauthorized');
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    update_option('shop2llm_onboarding_done', 1);
    wp_die('ok');
}

function shop_to_llm_cache_community_count(): void
{
    if (!current_user_can('manage_options')) {
        wp_die('unauthorized');
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $count = isset($_POST['count']) ? absint($_POST['count']) : 0;
    if ($count > 0) {
        set_transient('shop2llm_community_count', $count, DAY_IN_SECONDS);
    }
    wp_die('ok');
}

function shop_to_llm_dismiss_pro_cta(): void
{
    if (!current_user_can('manage_options')) {
        wp_die('unauthorized');
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $userId = get_current_user_id();
    if ($userId > 0) {
        update_user_meta($userId, 'shop2llm_pro_cta_dismissed', '1');
        update_user_meta($userId, 'shop2llm_pro_cta_dismissed_at', time());
    }
    wp_die('ok');
}

/**
 * Ajax handler: dismiss the Dashboard "Leave a Review" CTA card.
 *
 * Marks the card as dismissed for the current admin user so it won't render
 * again on future Dashboard visits. Triggered by the dismiss button and the
 * "Maybe later" button on the review card.
 *
 * @since 1.3.2
 */
function shop_to_llm_dismiss_review_card(): void
{
    if (!current_user_can('manage_options')) {
        wp_die('unauthorized');
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $userId = get_current_user_id();
    if ($userId > 0) {
        update_user_meta($userId, 'shop2llm_review_card_dismissed', '1');
        update_user_meta($userId, 'shop2llm_review_card_dismissed_at', time());
    }
    wp_die('ok');
}

function shop_to_llm_handle_test_search(): void
{
    if (!current_user_can('manage_options')) {
        wp_die();
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : '';
    if (empty($query)) {
        wp_send_json_error(['error' => __('Query required', 'shop-to-llm')]);
    }

    // Increment today's API count so the score updates on next page load
    $today = (int) get_transient('shop_to_llm_count_' . gmdate('Y-m-d'));
    set_transient('shop_to_llm_count_' . gmdate('Y-m-d'), $today + 1, DAY_IN_SECONDS);
    $count7d = (int) get_transient('shop_to_llm_count_7d');
    set_transient('shop_to_llm_count_7d', $count7d + 1, DAY_IN_SECONDS);

    // v1.1.1: Local quota counter (UI hint only; real enforcement is server-side)
    $todayKey = gmdate('Y-m-d');
    $quotaKey = 'shop2llm_test_quota_' . $todayKey;
    $usedToday = (int) get_transient($quotaKey);
    $dailyLimit = (int) apply_filters('shop2llm_test_search_daily_limit', 5);
    $remainingBefore = max(0, $dailyLimit - $usedToday);

    $results = shop_to_llm_search_products(['query' => $query, 'limit' => 10]);
    $items = $results['products'] ?? [];

    // Format results for the frontend — v1.1.1: include full product fields
    $products = [];
    foreach ($items as $r) {
        $products[] = [
            'name' => $r['name'] ?? 'Unknown',
            'price' => $r['price'] ?? '',
            'currency' => $r['currency'] ?? '',
            'in_stock' => !empty($r['in_stock']),
            'rating' => (float) ($r['rating'] ?? 0),
            'short_description' => $r['short_description'] ?? '',
            'url' => $r['url'] ?? '',
            'image_url' => $r['image_url'] ?? '',
        ];
    }

    // v1.1.1: Generate ChatGPT-style reply via FinCore cloud LLM proxy.
    // Server-side enforces real quota (5/day per site_hash). Local counter is UI hint only.
    $aiReply = shop_to_llm_generate_ai_test_reply($query, $products);

    // Increment local quota counter after the cloud call (matches server-side accounting)
    set_transient($quotaKey, $usedToday + 1, DAY_IN_SECONDS);
    $remainingAfter = max(0, $dailyLimit - ($usedToday + 1));

    wp_send_json_success([
        'products' => $products,
        'ai_reply' => $aiReply,
        'remaining_quota' => $remainingAfter,
        'daily_limit' => $dailyLimit,
    ]);
}

/**
 * v1.1.1: Generate a ChatGPT-style natural-language reply via FinCore cloud LLM proxy.
 *
 * Server-side endpoint (api.shop2llm.com/fincore/v1/free/ai-test) enforces:
 *   - site_hash registration check
 *   - 5 calls/day per site quota
 *   - Fixed prompt template (no free-form prompt injection)
 *   - Returns natural-language reply only (no product data — that stays local)
 *
 * If the cloud service is unavailable or quota exceeded, gracefully degrades:
 *   - 429: returns localized "quota exhausted" message
 *   - Other errors: returns empty string (UI will fall back to plain product list)
 *
 * @param string $query  User's search query (already sanitized)
 * @param array  $products  Local search results (already formatted)
 * @return string  AI-generated reply (empty string on failure)
 */
function shop_to_llm_generate_ai_test_reply(string $query, array $products): string
{
    $site_url = get_site_url();
    $site_hash = hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url);

    // Build lightweight product summary — no images, no full descriptions (bandwidth-friendly)
    $productSummary = array_map(function ($p) {
        return [
            'name' => mb_substr($p['name'], 0, 120),
            'price' => $p['price'],
            'currency' => $p['currency'] ?? '',
            'in_stock' => $p['in_stock'],
            'rating' => $p['rating'] ?? 0,
        ];
    }, array_slice($products, 0, 10));

    $payload = [
        'site_url' => $site_url,
        'site_hash' => $site_hash,
        'plugin_version' => SHOP_TO_LLM_VERSION,
        'query' => mb_substr($query, 0, 200),
        'has_woocommerce' => shop_to_llm_has_woocommerce(),
        'site_name' => get_bloginfo('name'),
        'locale' => get_locale(),
        'products' => $productSummary,
        'product_count' => count($products),
    ];

    $response = wp_remote_post('https://api.shop2llm.com/fincore/v1/free/ai-test', [
        'timeout' => 15,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode($payload),
    ]);

    if (is_wp_error($response)) {
        return '';
    }

    $code = wp_remote_retrieve_response_code($response);
    $body = json_decode(wp_remote_retrieve_body($response), true);

    if ($code === 429) {
        return __('You have reached the daily limit for AI test searches (5 per day). Try again tomorrow, or upgrade to Pro for unlimited AI test searches with multi-platform support.', 'shop-to-llm');
    }

    if ($code !== 200 || empty($body['ai_reply'])) {
        return '';
    }

    // Sanitize but preserve line breaks and basic formatting
    $reply = sanitize_textarea_field($body['ai_reply']);
    return trim($reply);
}

/**
 * AJAX handler: Fix a single score item.
 * Supports: fix_robots, flush_rewrite
 */
function shop_to_llm_handle_fix_item(): void
{
    if (!current_user_can('manage_options')) {
        wp_die();
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $fixAction = isset($_POST['fix_action']) ? sanitize_text_field(wp_unslash($_POST['fix_action'])) : '';
    
    switch ($fixAction) {
        case 'fix_robots':
            flush_rewrite_rules();
            delete_transient('shop2llm_robots_allows_ai');
            delete_transient('shop2llm_admin_bar_score');
            update_option('shop_to_llm_robots_fixed', 1);
            wp_send_json_success(['message' => 'Robots.txt fixed', 'fix_action' => 'fix_robots']);
            break;
        case 'flush_rewrite':
            flush_rewrite_rules();
            // Clear cached accessibility checks so score recalculates on next load
            delete_transient('shop2llm_llms_accessible');
            delete_transient('shop2llm_feed_accessible');
            delete_transient('shop2llm_admin_bar_score');
            update_option('shop_to_llm_rewrite_flushed', 1);
            wp_send_json_success(['message' => 'Permalinks flushed + cache cleared', 'fix_action' => 'flush_rewrite']);
            break;
        default:
            wp_send_json_error(['error' => 'Unknown fix action: ' . $fixAction]);
    }
}

function shop_to_llm_render_onboarding_banner(): void
{
    if (get_option('shop2llm_onboarding_done')) {
        return;
    }
    // v1.3.3: Don't show banner on the plugin's own page (already has onboarding content)
    $screen = get_current_screen();
    if ($screen && $screen->id === 'toplevel_page_shop2llm') {
        return;
    }
    $dashboardUrl = admin_url('admin.php?page=shop2llm');

    // Get crawler stats for banner
    $stats = shop_to_llm_get_crawler_stats();
    $todayBots = (int) $stats['today_bots'];
    $todayPages = (int) $stats['today_pages'];
    $total30d = (int) $stats['total_30d'];

    ?>
    <div class="shop2llm-onboarding" style="background:#fff;border:1px solid #e2e8f0;border-radius:10px;margin:20px 0;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,0.04);display:flex;align-items:center;gap:16px;flex-wrap:wrap">
      <span style="flex-shrink:0;font-size:22px;font-weight:700;color:#6366f1;letter-spacing:-0.5px">Shop2LLM</span>
      <div style="flex:1;min-width:200px;font-size:13px;color:#334155"><?php echo esc_html__('Your site is now visible to ChatGPT and AI assistants.', 'shop-to-llm'); ?></div>
      <?php if ($total30d > 0) : ?>
      <div style="display:flex;align-items:center;gap:12px;font-size:12px;color:#64748b;background:#f8fafc;padding:5px 12px;border-radius:6px;border:1px solid #e2e8f0">
        <span>🤖 <strong style="color:#6366f1"><?php echo esc_html((string)$todayBots); ?></strong> bots today</span>
        <span style="color:#e2e8f0">|</span>
        <span>📄 <strong style="color:#6366f1"><?php echo esc_html((string)$todayPages); ?></strong> pages</span>
        <span style="color:#e2e8f0">|</span>
        <span>📊 <strong style="color:#6366f1"><?php echo esc_html((string)$total30d); ?></strong> crawls (30d)</span>
      </div>
      <?php endif; ?>
      <div style="display:flex;align-items:center;gap:8px">
        <a href="<?php echo esc_url($dashboardUrl); ?>" style="font-size:12px;background:#6366f1;color:#fff;border:none;padding:6px 14px;border-radius:5px;text-decoration:none;font-weight:500;white-space:nowrap"><?php echo esc_html__('Open Dashboard', 'shop-to-llm'); ?></a>
        <button type="button" onclick="var e=this.closest('.shop2llm-onboarding');fetch(ajaxurl+'?action=shop2llm_dismiss_onboarding',{method:'POST'}).then(()=>e.style.display='none')" style="font-size:12px;background:none;border:none;color:#94a3b8;cursor:pointer;padding:0 4px" title="<?php echo esc_attr__('Dismiss', 'shop-to-llm'); ?>">✕</button>
      </div>
    </div>
    <?php
}

function shop_to_llm_render_onboarding_wizard(
    int $apiCount7d,
    array $crawlerVisits,
    array $recentSearches
): void {
    $crawlerCount = count($crawlerVisits);
    $searchCount = count($recentSearches);
    $mcpUrl = rest_url('shop2llm/v1/mcp');
    // Use user's site mode selection, not just WooCommerce presence
    $siteMode = get_option('shop_to_llm_site_mode', '');
    $wcInstalled = shop_to_llm_has_woocommerce();
    $isCommerce = ($siteMode === 'commerce' && $wcInstalled);

    $step1 = true;
    $step2 = $crawlerCount > 0;
    $step3 = $crawlerCount > 0;
    $step4 = $searchCount > 0;
    $completed = ($step1 ? 1 : 0) + ($step2 ? 1 : 0) + ($step3 ? 1 : 0) + ($step4 ? 1 : 0);
    $pct = round(($completed / 4) * 100);
    // Visual distinction: content mode = blue, commerce mode = purple
    $wizardTheme = $isCommerce
        ? ['bg' => '#f5f3ff', 'border' => '#ddd6fe', 'accent' => '#6366f1', 'accentDark' => '#4338ca', 'progress' => 'linear-gradient(90deg,#6366f1,#4338ca)']
        : ['bg' => '#eff6ff', 'border' => '#bfdbfe', 'accent' => '#3b82f6', 'accentDark' => '#1d4ed8', 'progress' => 'linear-gradient(90deg,#3b82f6,#1d4ed8)'];
    ?>
    <div class="stl-card stl-wizard" style="background:<?php echo esc_attr($wizardTheme['bg']); ?>;border-color:<?php echo esc_attr($wizardTheme['border']); ?>">
        <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px">
            <h2 style="color:<?php echo esc_attr($wizardTheme['accentDark']); ?>;margin:0;display:flex;align-items:center;gap:6px">
                <?php $lReviewUrl = 'https://wordpress.org/support/plugin/shop2llm/reviews/#new-post'; ?>
                <span style="display:inline-flex;flex-direction:row-reverse;gap:2px" title="<?php echo esc_attr__('Rate us on WordPress.org', 'shop-to-llm'); ?>">
                    <?php for ($i = 5; $i >= 1; $i--) : ?>
                    <a href="<?php echo esc_url($lReviewUrl); ?>" target="_blank" style="font-size:16px;text-decoration:none;color:#94a3b8;transition:color 0.15s;line-height:1"
                       onmouseover="for(var s=this.parentElement.children,i=0;i<s.length;i++)s[i].style.color=i>=5-<?php echo (int)$i; ?>?'#eab308':'#94a3b8'"
                       onmouseout="for(var s=this.parentElement.children,i=0;i<s.length;i++)s[i].style.color='#94a3b8'">★</a>
                    <?php endfor; ?>
                </span>
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
                <?php echo esc_html__('Wizard', 'shop-to-llm'); ?>
            </h2>
            <div style="display:flex;align-items:center;gap:10px">
                <span style="font-size:12px;font-weight:600;color:<?php echo esc_attr($wizardTheme['accent']); ?>"><?php echo esc_html((string)$completed); ?>/4</span>
                <button type="button" id="stl-wizard-close-btn" style="background:none;border:none;cursor:pointer;color:#94a3b8;font-size:16px;line-height:1;padding:4px" title="<?php echo esc_attr__('Close — will re-appear after 8 hours', 'shop-to-llm'); ?>">✕</button>
            </div>

        </div>
        <div style="height:3px;background:#e2e8f0;border-radius:2px;margin-bottom:12px">
            <div style="width:<?php echo esc_attr((string)$pct); ?>%;height:100%;background:<?php echo esc_attr($wizardTheme['progress']); ?>;border-radius:2px;transition:width 0.4s"></div>
        </div>

        <div class="stl-wizard-steps">
            <div class="stl-wizard-step">
                <div class="stl-wizard-step-bullet">1</div>
                <div class="stl-wizard-step-body">
                    <?php if ($isCommerce) : ?>
                    <strong><?php echo esc_html__('Check Your Products', 'shop-to-llm'); ?></strong>
                    <span><?php echo esc_html__('Enter a SKU in the Product AI Score card below to see which products need improvement for AI discovery.', 'shop-to-llm'); ?></span>
                    <?php else : ?>
                    <strong><?php echo esc_html__('Review Your Content', 'shop-to-llm'); ?></strong>
                    <span><?php echo esc_html__('Check your posts and pages — AI can now search your content by keyword. Make sure your key content is published.', 'shop-to-llm'); ?></span>
                    <?php endif; ?>
                </div>
            </div>

            <div class="stl-wizard-step" data-step="2">
                <div class="stl-wizard-step-bullet">2</div>
                <div class="stl-wizard-step-body">
                    <strong><?php echo esc_html__('Connect to ChatGPT', 'shop-to-llm'); ?></strong>
                    <?php if ($isCommerce) : ?>
                    <span><?php echo esc_html__('Copy this link and add it to ChatGPT — then you can search your own products to verify everything works.', 'shop-to-llm'); ?></span>
                    <?php else : ?>
                    <span><?php echo esc_html__('Copy this link and add it to ChatGPT — then you can search your own content to verify everything works.', 'shop-to-llm'); ?></span>
                    <?php endif; ?>
                    <div class="stl-mcp-url-row">
                        <code class="stl-mono" id="stl-mcp-url"><?php echo esc_url($mcpUrl); ?></code>
                        <button type="button" class="stl-copy-btn" id="stl-copy-mcp-btn">📋 <?php echo esc_html__('Copy', 'shop-to-llm'); ?></button>
                    </div>
                </div>
            </div>

            <div class="stl-wizard-step">
                <div class="stl-wizard-step-bullet">3</div>
                <div class="stl-wizard-step-body">
                    <?php if ($isCommerce) : ?>
                    <strong><?php echo esc_html__('Improve Product Pages', 'shop-to-llm'); ?></strong>
                    <span><?php echo esc_html__('AI prefers products with clear descriptions, images, and categories. Use the score card below to find what to fix.', 'shop-to-llm'); ?></span>
                    <?php else : ?>
                    <strong><?php echo esc_html__('Optimize Your Posts', 'shop-to-llm'); ?></strong>
                    <span><?php echo esc_html__('AI prefers clear titles, meta descriptions, and well-structured content. Use the Visibility Score in the post editor to guide improvements.', 'shop-to-llm'); ?></span>
                    <?php endif; ?>
                </div>
            </div>

            <div class="stl-wizard-step">
                <div class="stl-wizard-step-bullet">4</div>
                <div class="stl-wizard-step-body">
                    <strong><?php echo esc_html__('Watch AI Activity Grow', 'shop-to-llm'); ?></strong>
                    <?php if ($isCommerce) : ?>
                    <span><?php echo esc_html__('When customers ask AI about products you sell, their search terms and crawler visits will appear here on your dashboard.', 'shop-to-llm'); ?></span>
                    <?php else : ?>
                    <span><?php echo esc_html__('When people ask AI about topics you cover, their search terms and crawler visits will appear here on your dashboard.', 'shop-to-llm'); ?></span>
                    <?php endif; ?>
                </div>
            </div>
        </div>

        <div class="stl-wizard-footer">
            <span style="font-size:12px;color:#64748b"><?php echo esc_html__('Dismiss anytime.', 'shop-to-llm'); ?></span>
        </div>
    </div>
    <?php
}

/**
 * First-Run Welcome Modal — clean single-card overlay shown once after activation.
 * One step: brief intro + optional opt-in, dismiss to dashboard.
 */
function shop_to_llm_render_welcome_modal(): void
{
    $isCommerce = shop_to_llm_has_woocommerce();
    $title = $isCommerce ? __('Shop2LLM — your store is now AI-visible', 'shop-to-llm') : __('Shop2LLM — your site is now AI-visible', 'shop-to-llm');
    $desc = $isCommerce
        ? __('ChatGPT and other AI assistants can now discover your products. Your dashboard shows AI traffic and visibility score.', 'shop-to-llm')
        : __('ChatGPT and other AI assistants can now discover your content. Your dashboard shows AI traffic and visibility score.', 'shop-to-llm');
    ?>
    <div id="stl-welcome-overlay" style="position:fixed;inset:0;background:rgba(15,23,42,0.5);z-index:100000;display:flex;align-items:center;justify-content:center;padding:24px">
        <div style="background:#fff;border-radius:14px;max-width:400px;width:100%;box-shadow:0 16px 48px rgba(0,0,0,0.25);text-align:center;padding:32px 28px 24px">
            <img src="<?php echo esc_url(SHOP_TO_LLM_URL . 'icon.png'); ?>" alt="Shop2LLM" style="width:48px;height:48px;border-radius:10px;margin-bottom:16px" />
            <h2 style="font-size:18px;font-weight:700;color:#0f172a;margin:0 0 8px"><?php echo esc_html($title); ?></h2>
            <p style="font-size:13px;color:#64748b;margin:0 0 24px;line-height:1.5"><?php echo esc_html($desc); ?></p>

            <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:20px;text-align:left">
                <p style="font-size:12px;color:#475569;margin:0 0 10px"><?php echo esc_html__('Help us improve — share anonymous usage stats?', 'shop-to-llm'); ?></p>
                <div style="display:flex;gap:8px">
                    <button type="button" id="stl-optin-yes" style="flex:1;padding:8px 0;background:#6366f1;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer"><?php echo esc_html__('Yes, sure', 'shop-to-llm'); ?></button>
                    <button type="button" id="stl-optin-no" style="flex:1;padding:8px 0;background:#fff;color:#64748b;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer"><?php echo esc_html__('No thanks', 'shop-to-llm'); ?></button>
                </div>
                <p style="font-size:10px;color:#94a3b8;margin:8px 0 0"><?php echo esc_html__('No personal data. Change anytime in Settings.', 'shop-to-llm'); ?></p>
            </div>

            <button type="button" id="stl-welcome-close" style="width:100%;padding:10px 0;background:none;color:#94a3b8;border:0;font-size:12px;cursor:pointer"><?php echo esc_html__('Go to dashboard', 'shop-to-llm'); ?></button>
        </div>
    </div>

    <script>
    (function(){
        var overlay = document.getElementById('stl-welcome-overlay');
        if (!overlay) return;
        var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
        var adminNonce = '<?php echo esc_js(wp_create_nonce('shop2llm_admin_nonce')); ?>';

        function ajax(action) {
            var fd = new FormData();
            fd.append('action', action);
            fd.append('nonce', adminNonce);
            return fetch(ajaxUrl, {method:'POST', body:fd});
        }

        function close() {
            overlay.style.display = 'none';
            var fd = new FormData();
            fd.append('action', 'shop2llm_dismiss_onboarding');
            fd.append('nonce', adminNonce);
            fetch(ajaxUrl, {method:'POST', body:fd});
        }

        // Fire welcome_shown
        (function(){ var f = new FormData(); f.append('action','shop2llm_log_event'); f.append('nonce',adminNonce); f.append('event','welcome_shown'); fetch(ajaxUrl,{method:'POST',body:f}); })();

        document.getElementById('stl-optin-yes').addEventListener('click', function(){
            ajax('shop2llm_enable_analytics').then(function(){ close(); });
        });
        document.getElementById('stl-optin-no').addEventListener('click', function(){ close(); });
        document.getElementById('stl-welcome-close').addEventListener('click', function(){ close(); });
    })();
    </script>
    <?php
}

// ─── Settings Page ─────────────────────────────────────────────────────

function shop_to_llm_render_settings(): void
{
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized', 'shop-to-llm'));
    }

    shop_to_llm_log_event('dashboard_visit');

    // v1.3.2: Dismiss value comparison card — re-shows on plugin version update
    if (isset($_GET['shop2llm_dismiss_value_card']) && isset($_GET['_wpnonce'])) {
        check_admin_referer('shop2llm_dismiss_value_card', '_wpnonce');
        update_option('shop2llm_value_card_dismissed', SHOP_TO_LLM_VERSION);
    }

    if (isset($_POST['shop_to_llm_save'])) {
        check_admin_referer('shop_to_llm_settings');
        $enabled_post = sanitize_text_field(wp_unslash($_POST['shop_to_llm_enabled'] ?? 'yes'));
        $enabled = in_array($enabled_post, ['yes', 'no'], true) ? $enabled_post : 'yes';
        $catalog_post = sanitize_text_field(wp_unslash($_POST['shop_to_llm_catalog_search'] ?? 'yes'));
        $catalogSearch = in_array($catalog_post, ['yes', 'no'], true) ? $catalog_post : 'yes';
        $analytics_post = sanitize_text_field(wp_unslash($_POST['shop_to_llm_analytics'] ?? 'no'));
        $analytics = in_array($analytics_post, ['yes', 'no'], true) ? $analytics_post : 'no';
        $wasAnalyticsEnabled = get_option('shop_to_llm_analytics', 'no');
        update_option('shop_to_llm_enabled', $enabled);
        update_option('shop_to_llm_catalog_search', $catalogSearch);
        update_option('shop_to_llm_analytics', $analytics);
        // v1.3.3: Product sync consent (opt-in, WP Guideline 7)
        $syncConsentPost = sanitize_text_field(wp_unslash($_POST['shop2llm_product_sync_consent'] ?? 'no'));
        $syncConsent = in_array($syncConsentPost, ['yes', 'no'], true) ? $syncConsentPost : 'no';
        update_option('shop2llm_product_sync_consent', $syncConsent);
        $badge_post = sanitize_text_field(wp_unslash($_POST['shop_to_llm_show_badge'] ?? 'yes'));
        $showBadge = in_array($badge_post, ['yes', 'no'], true) ? $badge_post : 'yes';
        update_option('shop_to_llm_show_badge', $showBadge);
        $entityChecks = isset($_POST['shop_to_llm_enable_entity_checks']) ? 1 : 0;
        update_option('shop_to_llm_enable_entity_checks', $entityChecks);
        // Save SEO feature toggles
        $seoOptions = ['shop_to_llm_seo_meta', 'shop_to_llm_seo_og', 'shop_to_llm_seo_sitemap', 'shop_to_llm_seo_schema', 'shop_to_llm_seo_redirect', 'shop_to_llm_seo_analysis', 'shop_to_llm_seo_audit'];
        foreach ($seoOptions as $seoOpt) {
            $seoVal = sanitize_text_field(wp_unslash($_POST[$seoOpt] ?? 'no'));
            update_option($seoOpt, in_array($seoVal, ['yes', 'no'], true) ? $seoVal : 'no');
        }
        $title_sep = sanitize_text_field(wp_unslash($_POST['shop_to_llm_title_sep'] ?? '|'));
        update_option('shop_to_llm_title_sep', in_array($title_sep, ['|', '-', '—', '•', '»'], true) ? $title_sep : '|');
        $home_title = sanitize_text_field(wp_unslash($_POST['shop_to_llm_seo_home_title'] ?? ''));
        update_option('shop_to_llm_seo_home_title', $home_title);
        $home_desc = sanitize_text_field(wp_unslash($_POST['shop2llm_seo_home_desc'] ?? ''));
        update_option('shop_to_llm_seo_home_desc', $home_desc);
        $template_keys = ['home', 'post', 'page', 'product', 'category', 'tag', 'archive'];
        foreach ($template_keys as $tk) {
            $opt_key = 'shop_to_llm_seo_title_' . $tk;
            $template_val = sanitize_text_field(wp_unslash($_POST[$opt_key] ?? ''));
            update_option($opt_key, $template_val);
        }
        $gsc_client_id = sanitize_text_field(wp_unslash($_POST['shop_to_llm_gsc_client_id'] ?? ''));
        update_option('shop_to_llm_gsc_client_id', $gsc_client_id);
        $gsc_client_secret = sanitize_text_field(wp_unslash($_POST['shop_to_llm_gsc_client_secret'] ?? ''));
        update_option('shop_to_llm_gsc_client_secret', $gsc_client_secret);
        if ($wasAnalyticsEnabled !== 'yes' && $analytics === 'yes') {
            $site_url = get_site_url();
            wp_remote_post('https://api.shop2llm.com/fincore/v1/free/ping', [
                'timeout' => 5,
                'blocking' => false,
                'headers' => ['Content-Type' => 'application/json'],
                'body' => json_encode([
                    'site_url' => $site_url,
                'site_hash' => hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url),
                'platform' => shop_to_llm_has_woocommerce() ? 'woocommerce' : 'wordpress',
                'plugin_version' => SHOP_TO_LLM_VERSION,
                'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
                'wc_version' => class_exists('WooCommerce') ? WC()->version : '',
                'wp_version' => get_bloginfo('version'),
                'behavior' => shop_to_llm_get_behavior_summary_for_upload(),
                ]),
            ]);
        }
        echo '<div class="notice notice-success"><p>' . esc_html__('Settings saved.', 'shop-to-llm') . '</p></div>';
    }

    $enabled = get_option('shop_to_llm_enabled', 'yes');
    $apiKey = get_option('shop_to_llm_api_key', '');
    $catalogSearch = get_option('shop_to_llm_catalog_search', 'yes');
    $analytics = get_option('shop_to_llm_analytics', 'no');
    $showBadge = get_option('shop_to_llm_show_badge', 'yes');
    $storeName = get_bloginfo('name');
    $apiCount7d = (int) get_transient('shop_to_llm_count_7d');
    $todayCount = shop_to_llm_get_today_api_count();
    $mcpEndpoint = rest_url('shop2llm/v1/mcp');

    // Handle mode toggle (mirrors Pro)
    if (isset($_POST['shop_to_llm_mode']) && in_array($_POST['shop_to_llm_mode'], ['content', 'commerce'], true)) {
        update_option('shop_to_llm_site_mode', sanitize_text_field(wp_unslash($_POST['shop_to_llm_mode'])));
    }
    $siteMode = get_option('shop_to_llm_site_mode', '');
    if ($siteMode === '') {
        // Auto-detect on first visit
        $hasWC = shop_to_llm_has_woocommerce();
        if (!$hasWC) {
            $siteMode = 'content';
        } else {
            $productCount = (int) (wp_count_posts('product')->publish ?? 0);
            $postCount = (int) (wp_count_posts('post')->publish ?? 0);
            $siteMode = ($postCount >= 5 && $productCount < 5) ? 'content' : 'commerce';
        }
        update_option('shop_to_llm_site_mode', $siteMode);
    }
    $hasWC = shop_to_llm_has_woocommerce();
    $effectiveWCMode = ($siteMode === 'commerce' && $hasWC);

    // Fetch wizard data early
    $crawlerVisits = shop_to_llm_get_recent_crawlers();
    $recentSearches = shop_to_llm_get_recent_searches();
    $crawlerCount = count($crawlerVisits);
    $searchCount = count($recentSearches);

    ?>
    <div class="stl-wrap">
        <div class="stl-header">
            <div class="stl-logo">
                <span>∞</span>
            </div>
            <div style="flex:1">
                <h1>Shop2LLM <span style="font-size:12px;color:#94a3b8;font-weight:400">v<?php echo esc_html(SHOP_TO_LLM_VERSION); ?></span></h1>
                <p style="font-size:13px;color:#64748b;margin:4px 0 0;line-height:1.4"><?php echo $effectiveWCMode ? esc_html__('Get your products discovered by ChatGPT and AI-powered search engines.', 'shop-to-llm') : esc_html__('Get your content discovered by ChatGPT and AI-powered search engines.', 'shop-to-llm'); ?></p>
            </div>
            <form method="post" style="display:flex;align-items:center;gap:4px;background:#f8fafc;padding:4px;border-radius:8px;border:1px solid #e2e8f0;margin-right:8px">
                <label style="font-size:11px;color:#64748b;white-space:nowrap;padding:0 3px;cursor:pointer">
                    <input type="radio" name="shop_to_llm_mode" value="content" <?php checked($siteMode, 'content'); ?> onchange="this.form.submit()" />
                    <?php echo esc_html__('Content', 'shop-to-llm'); ?>
                </label>
                <label style="font-size:11px;color:#64748b;white-space:nowrap;padding:0 3px;cursor:pointer">
                    <input type="radio" name="shop_to_llm_mode" value="commerce" <?php checked($siteMode, 'commerce'); ?> onchange="this.form.submit()" />
                    <?php echo esc_html__('Store', 'shop-to-llm'); ?>
                </label>
            </form>
            <button type="button" class="stl-wizard-toggle" id="stl-wizard-toggle-btn">🚀 <?php echo esc_html__('Wizard', 'shop-to-llm'); ?></button>
        </div>

        <!-- Onboarding Wizard (hidden until triggered) -->
        <div id="stl-openai-guide" style="display:none">
            <?php shop_to_llm_render_onboarding_wizard($apiCount7d, $crawlerVisits, $recentSearches); ?>
        </div>

        <script>
        (function(){
            var card = document.getElementById('stl-openai-guide');
            var toggleBtn = document.getElementById('stl-wizard-toggle-btn');
            var closeBtn = document.getElementById('stl-wizard-close-btn');
            var copyBtn = document.getElementById('stl-copy-mcp-btn');

            function showWizard() {
                if (card) card.style.display = 'block';
                if (toggleBtn) toggleBtn.classList.add('stl-wizard-toggle--active');
            }
            function hideWizard() {
                if (card) card.style.display = 'none';
                if (toggleBtn) toggleBtn.classList.remove('stl-wizard-toggle--active');
            }

            if (toggleBtn) {
                toggleBtn.addEventListener('click', function () {
                    if (!card) return;
                    if (card.style.display === 'none') showWizard();
                    else hideWizard();
                });
            }
            if (closeBtn) {
                closeBtn.addEventListener('click', hideWizard);
            }
            if (copyBtn) {
                copyBtn.addEventListener('click', function () {
                    var el = document.getElementById('stl-mcp-url');
                    if (!el) return;
                    navigator.clipboard.writeText(el.textContent.trim()).then(function () {
                        copyBtn.classList.add('stl-copy-btn--copied');
                        copyBtn.textContent = '✓ Copied';
                        setTimeout(function () {
                            copyBtn.classList.remove('stl-copy-btn--copied');
                            copyBtn.textContent = '📋 Copy';
                        }, 2000);
                        var s = document.querySelector('.stl-wizard-step[data-step="2"]');
                        if (s) {
                            s.classList.add('stl-wizard-step--done');
                            var bullet = s.querySelector('.stl-wizard-step-bullet');
                            if (bullet) bullet.textContent = '✓';
                        }
                    });
                });
            }
        })();
        </script>

        <?php
        // P0-1: 不再渲染全屏遮挡的 Welcome Modal，改为底部非遮挡 toast
        $justActivated = get_option('shop_to_llm_just_activated');
        if ($justActivated) {
            delete_option('shop_to_llm_just_activated');
            update_option('shop2llm_onboarding_done', 1);
            // 不调用 shop_to_llm_render_welcome_modal()，避免遮挡 Hero Test Search
        }
        ?>

        <?php if ($siteMode === 'commerce' && !$hasWC) : ?>
        <div style="background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:10px 16px;margin-bottom:12px;font-size:12px;color:#92400e;display:flex;align-items:center;gap:8px">
            <span style="flex-shrink:0;font-size:16px">⚠️</span>
            <span style="flex:1"><?php echo esc_html__('WooCommerce is not active. Product search by price/category/stock, product AI scoring, and product feed XML are unavailable. Install WooCommerce for the full experience.', 'shop-to-llm'); ?></span>
            <a href="<?php echo esc_url(admin_url('plugin-install.php?s=woocommerce&tab=search')); ?>" style="font-size:11px;color:#92400e;font-weight:600;white-space:nowrap"><?php echo esc_html__('Install →', 'shop-to-llm'); ?></a>
        </div>
        <?php endif; ?>

        <!-- Tabs Navigation -->
        <div class="stl-tabs">
          <button class="stl-tab stl-tab-active" data-tab="dashboard">Overview</button>
          <button class="stl-tab" data-tab="agents">AI Agents</button>
          <button class="stl-tab" data-tab="traffic">AI Traffic</button>
          <button class="stl-tab" data-tab="seo">SEO</button>
          <button class="stl-tab" data-tab="analytics">Analytics</button>
          <button class="stl-tab" data-tab="settings">Settings</button>
        </div>

        <!-- Dashboard Tab -->
        <div class="stl-tab-panel stl-tab-panel-active" id="tab-dashboard">

        <?php
        // v1.3.2: Value Card — Dashboard top (above Hero Test Search), dismissible, re-shows on plugin version update.
        // Compliance: WP.org Guideline 17 (trademark nominative fair use — slug not used),
        // Guideline 9 (verifiable claims — features listed are factual and documented in readme.txt).
        $valueCardDismissedVersion = get_option('shop2llm_value_card_dismissed', '');
        $valueCardVisible = ($valueCardDismissedVersion !== SHOP_TO_LLM_VERSION);
        if ($valueCardVisible) {
            $valueCardDismissUrl = wp_nonce_url(
                add_query_arg('shop2llm_dismiss_value_card', '1', admin_url('admin.php?page=shop2llm')),
                'shop2llm_dismiss_value_card',
                '_wpnonce'
            );
            ?>
            <div id="stl-value-card" style="background:linear-gradient(135deg,#eff6ff 0%,#dbeafe 100%);border:1px solid #93c5fd;border-radius:12px;padding:18px 20px;margin-bottom:16px">
                <div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:14px">
                    <div style="width:36px;height:36px;background:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;color:#1e3a8a;font-weight:700">✓</div>
                    <div style="flex:1;min-width:0">
                        <div style="font-size:15px;font-weight:700;color:#1e3a8a;line-height:1.2"><?php echo esc_html__('Everything below is free — no upsell needed', 'shop-to-llm'); ?></div>
                        <div style="font-size:12px;color:#1e40af;margin-top:3px;line-height:1.4"><?php echo esc_html__('The SEO features Yoast SEO Premium and Rank Math Pro reserve for paid tiers, plus AI tools no SEO plugin offers.', 'shop-to-llm'); ?></div>
                    </div>
                    <a href="<?php echo esc_url($valueCardDismissUrl); ?>" style="font-size:11px;color:#1e40af;text-decoration:none;white-space:nowrap;flex-shrink:0;padding:4px 10px;border-radius:4px;background:#fff;border:1px solid #93c5fd"><?php echo esc_html__('Dismiss', 'shop-to-llm'); ?></a>
                </div>
                <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px">
                    <div style="background:#fff;border-radius:8px;padding:12px 14px;border:1px solid #bfdbfe">
                        <div style="font-size:11px;font-weight:700;color:#1e3a8a;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px"><?php echo esc_html__('SEO (matches paid tiers)', 'shop-to-llm'); ?></div>
                        <ul style="margin:0;padding:0;list-style:none;font-size:12px;color:#1e1e1e;line-height:1.8">
                            <li>✓ <?php echo esc_html__('19 JSON-LD Schema types', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Redirect Manager (301/302/307/410/451)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('SEO Audit (11-point)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Google Search Console integration', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Broken Link Checker', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Image SEO (bulk alt-text audit)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Content Analysis (15+ checks)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Title Templates (%%variables%%)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('One-click migration from Yoast/Rank Math', 'shop-to-llm'); ?></li>
                        </ul>
                    </div>
                    <div style="background:#fff;border-radius:8px;padding:12px 14px;border:1px solid #bfdbfe">
                        <div style="font-size:11px;font-weight:700;color:#1e3a8a;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px"><?php echo esc_html__('AI (only in Shop2LLM)', 'shop-to-llm'); ?></div>
                        <ul style="margin:0;padding:0;list-style:none;font-size:12px;color:#1e1e1e;line-height:1.8">
                            <li>✓ <?php echo esc_html__('llms.txt — machine-readable catalog', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('/agents.md — AI agent dossier (UCP + MCP + catalog)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('MCP protocol — 23 JSON-RPC 2.0 tools', 'shop-to-llm'); ?></li>
                            <li>✓ <?php
                                /* translators: %d: number of AI crawlers tracked */
                                echo esc_html(sprintf(__('AI Crawler Invites (GPTBot, ClaudeBot, +%d)', 'shop-to-llm'), max(0, shop_to_llm_get_ai_crawler_count() - 2)));
                            ?></li>
                            <li>✓ <?php echo esc_html__('UCP Readiness Score (7-dim, 100-pt)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Live Content Extractability (10-dim citability)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('AI Visibility Score™ (5-dim, 25-item)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('Hero Test Search (5 free queries/day)', 'shop-to-llm'); ?></li>
                            <li>✓ <?php echo esc_html__('AI Crawler Activity Monitor', 'shop-to-llm'); ?></li>
                        </ul>
                    </div>
                </div>
                <div style="font-size:10px;color:#1e40af;margin-top:10px;opacity:0.85;line-height:1.4">
                    <?php echo esc_html__('Yoast SEO and Rank Math are trademarks of their respective owners and are not affiliated with Shop2LLM. Comparison based on publicly documented feature lists as of July 2026.', 'shop-to-llm'); ?>
                </div>
            </div>
            <?php
        }

        // v1.1.1: Hero Test Search — AI-powered product recommendation at Dashboard top
        $heroHasWC = shop_to_llm_has_woocommerce();
        $heroPlaceholder = $heroHasWC
            ? __('e.g. wireless headphones under $50', 'shop-to-llm')
            : __('e.g. how to start a blog', 'shop-to-llm');
        $heroTitle = $heroHasWC
            ? __('See how AI sees your store', 'shop-to-llm')
            : __('See how AI sees your site', 'shop-to-llm');
        $heroSubtitle = __('Ask anything — get an instant AI-powered answer with your real products and content.', 'shop-to-llm');
        $heroTodayKey = gmdate('Y-m-d');
        $heroUsed = (int) get_transient('shop2llm_test_quota_' . $heroTodayKey);
        $heroLimit = (int) apply_filters('shop2llm_test_search_daily_limit', 5);
        $heroRemaining = max(0, $heroLimit - $heroUsed);
        ?>
        <div id="stl-hero-test-search" style="background:linear-gradient(135deg,#fef3c7 0%,#fde68a 100%);border:1px solid #fcd34d;border-radius:12px;padding:18px 20px;margin-bottom:16px;position:relative;overflow:hidden">
            <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
                <div style="width:40px;height:40px;background:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;box-shadow:0 2px 4px rgba(0,0,0,0.06);flex-shrink:0">🤖</div>
                <div style="flex:1;min-width:0">
                    <div style="font-size:16px;font-weight:700;color:#78350f;line-height:1.2"><?php echo esc_html($heroTitle); ?></div>
                    <div style="font-size:12px;color:#92400e;margin-top:2px;line-height:1.4"><?php echo esc_html($heroSubtitle); ?></div>
                </div>
                <div id="stl-test-quota" style="margin-left:auto;font-size:11px;color:#92400e;background:#fff;padding:4px 10px;border-radius:12px;border:1px solid #fcd34d;white-space:nowrap;flex-shrink:0">
                    <?php echo esc_html(sprintf(/* translators: %d: remaining tests */ __('%d tests left today', 'shop-to-llm'), $heroRemaining)); ?>
                </div>
            </div>
            <div style="display:flex;gap:8px;background:#fff;border-radius:8px;padding:6px;border:1px solid #fcd34d">
                <input type="text" id="stl-hero-search-input" placeholder="<?php echo esc_attr($heroPlaceholder); ?>" style="flex:1;border:none;outline:none;padding:8px 10px;font-size:13px;background:transparent;min-width:0">
                <button type="button" id="stl-hero-search-go" style="padding:8px 18px;background:#78350f;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap"><?php echo esc_html__('Ask AI', 'shop-to-llm'); ?> →</button>
            </div>
            <div id="stl-hero-results" style="display:none;margin-top:12px"></div>
            <div id="stl-hero-loading" style="display:none;text-align:center;padding:20px;color:#92400e;font-size:13px">
                <span style="display:inline-block;animation:stl-spin 1s linear infinite;border:2px solid #fcd34d;border-top-color:transparent;border-radius:50%;width:16px;height:16px;vertical-align:middle;margin-right:8px"></span>
                <?php echo esc_html__('Asking AI about your store...', 'shop-to-llm'); ?>
            </div>
            <div style="font-size:10px;color:#92400e;margin-top:8px;opacity:0.8">
                <?php echo esc_html__('Powered by FinCore cloud · AI by DeepSeek · 5 free tests per day', 'shop-to-llm'); ?>
            </div>
        </div>
        <style>
        @keyframes stl-spin { to { transform: rotate(360deg); } }
        .stl-ai-bubble { background:#fff; border-radius:12px; padding:14px 16px; border:1px solid #fcd34d; box-shadow:0 2px 6px rgba(120,53,15,0.06); }
        .stl-ai-bubble-header { display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:12px; color:#92400e; font-weight:600; }
        .stl-ai-bubble-text { font-size:13px; color:#1e1e1e; line-height:1.6; white-space:pre-wrap; word-wrap:break-word; }
        .stl-product-card { display:flex; gap:10px; padding:10px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; margin-top:8px; transition:transform 0.15s, box-shadow 0.15s; }
        .stl-product-card:hover { transform:translateY(-1px); box-shadow:0 3px 8px rgba(0,0,0,0.06); }
        .stl-product-thumb { width:48px; height:48px; border-radius:6px; object-fit:cover; background:#f1f5f9; flex-shrink:0; }
        .stl-product-thumb-placeholder { width:48px; height:48px; border-radius:6px; background:#f1f5f9; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:20px; }
        .stl-product-info { flex:1; min-width:0; }
        .stl-product-name { font-size:13px; font-weight:600; color:#1e1e1e; margin:0 0 2px; word-break:break-word; }
        .stl-product-meta { font-size:11px; color:#64748b; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
        .stl-product-price { color:#16a34a; font-weight:600; }
        .stl-product-stock-in { color:#16a34a; }
        .stl-product-stock-out { color:#dc2626; }
        .stl-product-rating { color:#f59e0b; }
        .stl-product-desc { font-size:11px; color:#64748b; margin-top:4px; line-height:1.4; }
        </style>
        <script>
        (function(){
            var heroInput = document.getElementById('stl-hero-search-input');
            var heroGoBtn = document.getElementById('stl-hero-search-go');
            var heroResults = document.getElementById('stl-hero-results');
            var heroLoading = document.getElementById('stl-hero-loading');
            var quotaDisplay = document.getElementById('stl-test-quota');
            if (!heroInput || !heroGoBtn) return;

            function escapeHtml(str){
                if (str === null || str === undefined) return '';
                return String(str)
                    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
                    .replace(/"/g,'&quot;').replace(/'/g,'&#039;');
            }

            function updateQuotaDisplay(remaining){
                if (!quotaDisplay || remaining === undefined || remaining === null) return;
                quotaDisplay.textContent = remaining + ' <?php echo esc_js(__('tests left today', 'shop-to-llm')); ?>';
                if (remaining === 0) {
                    quotaDisplay.style.background = '#fee2e2';
                    quotaDisplay.style.borderColor = '#fecaca';
                    quotaDisplay.style.color = '#991b1b';
                }
            }

            function runHeroSearch(){
                var q = (heroInput.value || '').trim();
                if (!q) { heroInput.focus(); return; }

                heroResults.style.display = 'none';
                heroLoading.style.display = 'block';
                heroGoBtn.disabled = true;
                heroGoBtn.style.opacity = '0.7';

                var fd = new FormData();
                fd.append('action', 'shop2llm_test_search');
                fd.append('query', q);
                fd.append('nonce', '<?php echo esc_js(wp_create_nonce('shop2llm_admin_nonce')); ?>');

                fetch('<?php echo esc_js(admin_url('admin-ajax.php')); ?>', {method:'POST', body:fd})
                    .then(function(r){ return r.json(); })
                    .then(function(d){
                        heroLoading.style.display = 'none';
                        heroGoBtn.disabled = false;
                        heroGoBtn.style.opacity = '1';

                        var data = (d && d.data) || {};
                        if (d && d.success) {
                            updateQuotaDisplay(data.remaining_quota);
                        }

                        if (!d || !d.success) {
                            var errMsg = (data.error) ? data.error : '<?php echo esc_js(__('Search failed. Please try again.', 'shop-to-llm')); ?>';
                            heroResults.innerHTML = '<div class="stl-ai-bubble"><div class="stl-ai-bubble-text" style="color:#dc2626">' + escapeHtml(errMsg) + '</div></div>';
                            heroResults.style.display = 'block';
                            return;
                        }

                        var products = data.products || [];
                        var aiReply = data.ai_reply || '';
                        var html = '';

                        if (aiReply) {
                            html += '<div class="stl-ai-bubble">';
                            html += '<div class="stl-ai-bubble-header"><span style="font-size:16px">🤖</span> <?php echo esc_js(__('AI-powered preview', 'shop-to-llm')); ?></div>';
                            html += '<div class="stl-ai-bubble-text">' + escapeHtml(aiReply) + '</div>';
                            html += '</div>';
                        } else if (products.length) {
                            // P0-3: DeepSeek 失败但商品存在时，明确提示 AI 不可用+重试按钮
                            html += '<div class="stl-ai-bubble" style="border-color:#fecaca;background:#fef2f2">';
                            html += '<div class="stl-ai-bubble-header" style="color:#dc2626"><span style="font-size:16px">⚠️</span> <?php echo esc_js(__('AI temporarily unavailable', 'shop-to-llm')); ?></div>';
                            html += '<div class="stl-ai-bubble-text" style="color:#991b1b;font-size:12px">' + <?php echo wp_json_encode(__('AI analysis is temporarily unavailable. Showing matching items below. Click retry to try again.', 'shop-to-llm')); ?> + '</div>';
                            html += '<button type="button" id="stl-hero-retry" style="margin-top:8px;padding:6px 16px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">' + <?php echo wp_json_encode(__('Retry AI', 'shop-to-llm')); ?> + '</button>';
                            html += '</div>';
                        }

                        if (products.length) {
                            html += '<div style="font-size:11px;font-weight:600;color:#92400e;margin:10px 0 4px">' + products.length + ' <?php echo esc_js(__('matching items found:', 'shop-to-llm')); ?></div>';
                            products.forEach(function(p){
                                html += '<div class="stl-product-card">';
                                if (p.image_url) {
                                    html += '<img class="stl-product-thumb" src="' + escapeHtml(p.image_url) + '" alt="" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'">';
                                    html += '<div class="stl-product-thumb-placeholder" style="display:none">📦</div>';
                                } else {
                                    html += '<div class="stl-product-thumb-placeholder">📦</div>';
                                }
                                html += '<div class="stl-product-info">';
                                html += '<div class="stl-product-name">' + escapeHtml(p.name) + '</div>';
                                html += '<div class="stl-product-meta">';
                                if (p.price) {
                                    var priceText = String(p.price);
                                    if (p.currency) priceText = p.currency + ' ' + priceText;
                                    html += '<span class="stl-product-price">' + escapeHtml(priceText) + '</span>';
                                }
                                html += p.in_stock ? '<span class="stl-product-stock-in">● <?php echo esc_js(__('In Stock', 'shop-to-llm')); ?></span>' : '<span class="stl-product-stock-out">● <?php echo esc_js(__('Out of Stock', 'shop-to-llm')); ?></span>';
                                if (p.rating && p.rating > 0) {
                                    html += '<span class="stl-product-rating">★ ' + (Math.round(p.rating * 10) / 10) + '</span>';
                                }
                                html += '</div>';
                                if (p.short_description) {
                                    var desc = p.short_description.length > 120 ? p.short_description.substring(0, 120) + '...' : p.short_description;
                                    html += '<div class="stl-product-desc">' + escapeHtml(desc) + '</div>';
                                }
                                html += '</div>';
                                if (p.url) {
                                    html += '<a href="' + escapeHtml(p.url) + '" target="_blank" rel="noopener" style="font-size:11px;color:#78350f;text-decoration:none;align-self:center;white-space:nowrap"><?php echo esc_js(__('View', 'shop-to-llm')); ?> →</a>';
                                }
                                html += '</div>';
                            });
                        } else if (!aiReply) {
                            html += '<div class="stl-ai-bubble"><div class="stl-ai-bubble-text" style="color:#d97706"><?php echo esc_js(__('No matching items found. Try a different query.', 'shop-to-llm')); ?></div></div>';
                        }

                        heroResults.innerHTML = html;
                        heroResults.style.display = 'block';
                        // P0-3: 绑定 AI 不可用时的重试按钮
                        var retryAiBtn = document.getElementById('stl-hero-retry');
                        if (retryAiBtn) {
                            retryAiBtn.addEventListener('click', runHeroSearch);
                        }
                        if (heroResults.scrollIntoView) {
                            heroResults.scrollIntoView({behavior:'smooth', block:'nearest'});
                        }
                    })
                    .catch(function(){
                        heroLoading.style.display = 'none';
                        heroGoBtn.disabled = false;
                        heroGoBtn.style.opacity = '1';
                        // P0-3: 网络错误也显示重试按钮
                        heroResults.innerHTML = '<div class="stl-ai-bubble" style="border-color:#fecaca;background:#fef2f2">'
                            + '<div class="stl-ai-bubble-header" style="color:#dc2626"><span style="font-size:16px">⚠️</span> ' + <?php echo wp_json_encode(__('Connection error', 'shop-to-llm')); ?> + '</div>'
                            + '<div class="stl-ai-bubble-text" style="color:#991b1b;font-size:12px">' + <?php echo wp_json_encode(__('Could not reach the AI service. Please check your internet connection and try again.', 'shop-to-llm')); ?> + '</div>'
                            + '<button type="button" id="stl-hero-retry-net" style="margin-top:8px;padding:6px 16px;background:#dc2626;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">' + <?php echo wp_json_encode(__('Retry', 'shop-to-llm')); ?> + '</button>'
                            + '</div>';
                        heroResults.style.display = 'block';
                        // 绑定重试按钮
                        var retryNetBtn = document.getElementById('stl-hero-retry-net');
                        if (retryNetBtn) {
                            retryNetBtn.addEventListener('click', runHeroSearch);
                        }
                    });
            }

            heroGoBtn.addEventListener('click', runHeroSearch);
            heroInput.addEventListener('keydown', function(e){
                if (e.key === 'Enter' || e.keyCode === 13) {
                    e.preventDefault();
                    runHeroSearch();
                }
            });

            // P0-2: 激活后自动跑一次预设 query，让用户立刻看到 AI 推荐效果
            var stlAutoSearch = <?php echo $justActivated ? 'true' : 'false'; ?>;
            if (stlAutoSearch && <?php echo (int)$heroRemaining; ?> > 0) {
                document.addEventListener('DOMContentLoaded', function() {
                    // 延迟1.5秒，确保页面完全加载后再触发
                    setTimeout(function() {
                        var autoQuery = heroHasWC
                            ? '<?php echo esc_js(__('best products in my store', 'shop-to-llm')); ?>'
                            : '<?php echo esc_js(__('summarize what this site is about', 'shop-to-llm')); ?>';
                        heroInput.value = autoQuery;
                        runHeroSearch();
                    }, 1500);
                });
            }

            // Expose globally so the MCP score-card "Test Search" button can redirect here
            window.shop2llmFocusHeroSearch = function(query){
                if (query) heroInput.value = query;
                var heroBox = document.getElementById('stl-hero-test-search');
                if (heroBox && heroBox.scrollIntoView) {
                    heroBox.scrollIntoView({behavior:'smooth', block:'start'});
                }
                setTimeout(function(){ heroInput.focus(); }, 400);
            };
        })();
        </script>

        <!-- AI Visibility Score Breakdown (7 factors — Dashboard top, first card) -->
        <?php
        // Use canonical 7-factor visibility score — same as admin bar
        $pubCount = shop_to_llm_has_woocommerce()
            ? (int)(wp_count_posts('product')->publish ?? 0)
            : (int)(wp_count_posts('post')->publish ?? 0);

        $visScore = shop_to_llm_get_visibility_score();
        $totalScore = $visScore['total'];
        $freeMax = $visScore['free_max'];
        $scoreLabel = $visScore['label'];
        $scoreColor = $visScore['color'];
        $scoreBg = $visScore['bg'];
        $scoreRobots = $visScore['factors']['robots'];
        $scoreLlms = $visScore['factors']['llms'];
        $scoreSchema = $visScore['factors']['schema'];
        $scoreMcp = $visScore['factors']['mcp'];
        $scoreFeed = $visScore['factors']['feed'];
        $scoreQueries = $visScore['factors']['queries'];
        $scoreCrawlers = $visScore['factors']['crawlers'];
        $schemaHasConflict = $visScore['schema_has_conflict'];
        $dashboardScoreTitle = __('AI Score', 'shop-to-llm');

        // Pro-aligned: pass = score >= max * 0.7
        $scoreItems = [
            [
                'id' => 'robots', 'label' => __('Robots.txt', 'shop-to-llm'), 'score' => $scoreRobots, 'max' => 20,
                'pass' => $scoreRobots >= 14,
                'hint' => $scoreRobots >= 14 ? __('AI crawlers whitelisted — auto-configured', 'shop-to-llm') : __('AI crawlers blocked — cannot index your content', 'shop-to-llm'),
                'fix_action' => $scoreRobots < 14 ? 'fix_robots' : '', 'fix_label' => __('Fix robots.txt', 'shop-to-llm'),
            ],
            [
                'id' => 'llms', 'label' => __('Llms.txt', 'shop-to-llm'), 'score' => $scoreLlms, 'max' => 20,
                'pass' => $scoreLlms >= 14,
                'hint' => $scoreLlms >= 14 ? __('llms.txt accessible — AI can read your catalog', 'shop-to-llm') : __('llms.txt not accessible — flush permalinks or check caching', 'shop-to-llm'),
                'fix_action' => $scoreLlms < 14 ? 'flush_rewrite' : '', 'fix_label' => __('Flush Permalinks', 'shop-to-llm'),
            ],
            [
                'id' => 'schema', 'label' => __('Schema', 'shop-to-llm'), 'score' => $scoreSchema, 'max' => 15,
                'pass' => $scoreSchema >= 11,
                'hint' => $scoreSchema >= 11 ? __('JSON-LD schema active — full product markup', 'shop-to-llm')
                    : ($schemaHasConflict ? __('SEO plugin conflict — schema may be duplicated', 'shop-to-llm') : __('Missing schema — improve product descriptions and images', 'shop-to-llm')),
                'fix_action' => !$schemaHasConflict && $scoreSchema < 11 ? 'edit_products' : ($schemaHasConflict ? 'check_settings' : ''), 'fix_label' => $schemaHasConflict ? __('Check Settings', 'shop-to-llm') : __('Improve Products', 'shop-to-llm'),
            ],
            [
                'id' => 'mcp', 'label' => __('MCP', 'shop-to-llm'), 'score' => $scoreMcp, 'max' => 15,
                'pass' => $scoreMcp >= 11,
                'hint' => $scoreMcp >= 11 ? sprintf(/* translators: %d: number of API calls */ __('%d API calls — ChatGPT connected and active', 'shop-to-llm'), $todayCount)
                    : ($scoreMcp >= 5 ? __('Enabled but not tested — try the test search below', 'shop-to-llm') : __('MCP endpoint disabled — enable in Settings', 'shop-to-llm')),
                'fix_action' => $scoreMcp >= 5 ? 'test_mcp' : 'settings', 'fix_label' => $scoreMcp >= 5 ? __('Test Search', 'shop-to-llm') : __('Enable MCP', 'shop-to-llm'),
            ],
            [
                'id' => 'feed', 'label' => __('Feed', 'shop-to-llm'), 'score' => $scoreFeed, 'max' => 10,
                'pass' => $scoreFeed >= 7,
                'hint' => $scoreFeed >= 7 ? (shop_to_llm_has_woocommerce() ? __('Product Feed XML accessible — AI shopping platforms indexed', 'shop-to-llm') : __('Content feed ready — no WooCommerce needed', 'shop-to-llm'))
                    : __('Product Feed XML not accessible — regenerate rewrite rules', 'shop-to-llm'),
                'fix_action' => $scoreFeed < 7 ? 'flush_rewrite' : '', 'fix_label' => __('Flush Permalinks', 'shop-to-llm'),
            ],
            [
                'id' => 'queries', 'label' => __('Queries', 'shop-to-llm'), 'score' => $scoreQueries, 'max' => 10,
                'pass' => $scoreQueries >= 7,
                'hint' => $scoreQueries >= 7 ? sprintf(/* translators: %d: percentage of content with Focus Queries */ __('%d%% of content has Focus Queries', 'shop-to-llm'), round(($scoreQueries / 10) * 100)) : __('Set Focus Queries on posts/products to boost AI relevance', 'shop-to-llm'),
                'fix_action' => $scoreQueries < 7 ? 'edit_posts' : '', 'fix_label' => __('Edit Content', 'shop-to-llm'),
            ],
            [
                'id' => 'crawlers', 'label' => __('Crawlers', 'shop-to-llm'), 'score' => $scoreCrawlers, 'max' => 10,
                'pass' => $scoreCrawlers >= 7,
                'hint' => $scoreCrawlers >= 7 ? __('AI crawlers actively visiting — good visibility', 'shop-to-llm') : ($scoreCrawlers >= 5 ? __('Some AI crawlers detected — keep publishing fresh content', 'shop-to-llm') : __('No AI crawler visits yet — ensure robots.txt allows AI bots', 'shop-to-llm')),
                'fix_action' => $scoreCrawlers < 7 ? 'fix_robots' : '', 'fix_label' => __('Fix robots.txt', 'shop-to-llm'),
            ],
        ];

        // Badges (merged from Growth Progress)
        $badges = [
            ['icon' => '🛡️', 'name' => __('AI-Ready', 'shop-to-llm'), 'earned' => true, 'hint' => __('Schema + llms.txt active', 'shop-to-llm')],
            ['icon' => '📦', 'name' => __('Indexed', 'shop-to-llm'), 'earned' => $pubCount > 0, 'hint' => sprintf(/* translators: %d: number of discoverable items */ __('%d items discoverable', 'shop-to-llm'), $pubCount)],
            ['icon' => '🔗', 'name' => __('Connected', 'shop-to-llm'), 'earned' => $todayCount > 0, 'hint' => $todayCount > 0 ? sprintf(/* translators: %d: number of API calls today */ __('%d API calls today', 'shop-to-llm'), $todayCount) : __('Connect ChatGPT to earn', 'shop-to-llm')],
            ['icon' => '🔍', 'name' => __('Discovered', 'shop-to-llm'), 'earned' => $crawlerCount > 0, 'hint' => $crawlerCount > 0 ? sprintf(/* translators: %d: number of crawler visits */ __('%d crawler visits', 'shop-to-llm'), $crawlerCount) : __('AI crawlers indexing', 'shop-to-llm')],
        ];
        $earnedBadges = count(array_filter($badges, fn($b) => $b['earned']));
        ?>
        <div class="stl-card" id="stl-score-breakdown" style="border-left:4px solid <?php echo esc_attr($scoreColor); ?>">
            <h2 style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
                <span style="display:flex;align-items:center;gap:6px">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
                    <?php echo esc_html($dashboardScoreTitle); ?><span class="s2ll-help" title="<?php echo esc_attr__('7-factor breakdown of how discoverable your site is by AI. Each item shows exactly what to fix.', 'shop-to-llm'); ?>"></span>
                </span>
                <span style="font-size:22px;font-weight:700;color:<?php echo esc_attr($scoreColor); ?>"><?php echo esc_html((string)$totalScore); ?><span style="font-size:12px;font-weight:400;color:#94a3b8">/100</span></span>
            </h2>

            <!-- Label badge + progress bar -->
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
                <span style="display:inline-block;padding:2px 10px;background:<?php echo esc_attr($scoreBg); ?>;color:<?php echo esc_attr($scoreColor); ?>;border-radius:10px;font-size:11px;font-weight:600"><?php echo esc_html($scoreLabel); ?></span>
            </div>
            <div style="height:6px;background:#e2e8f0;border-radius:3px;margin-bottom:4px">
                <div style="width:<?php echo esc_attr((string)$totalScore); ?>%;height:100%;background:<?php echo esc_attr($scoreColor); ?>;border-radius:3px;transition:width 0.5s"></div>
            </div>

            <!-- Badges row (merged from Growth Progress) -->
            <div style="display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 10px">
                <?php foreach ($badges as $b) : ?>
                <div title="<?php echo esc_attr($b['hint']); ?>" style="display:flex;align-items:center;gap:4px;padding:3px 8px;border-radius:20px;border:1px solid <?php echo $b['earned'] ? '#c7d2fe' : '#e2e8f0'; ?>;background:<?php echo $b['earned'] ? '#f5f3ff' : '#f8fafc'; ?>;font-size:10px;<?php echo $b['earned'] ? '' : 'filter:grayscale(1);opacity:0.5;'; ?>">
                    <span style="font-size:11px"><?php echo esc_html($b['icon']); ?></span>
                    <span style="color:<?php echo $b['earned'] ? '#6366f1' : '#94a3b8'; ?>;font-weight:<?php echo $b['earned'] ? '600' : '400'; ?>"><?php echo esc_html($b['name']); ?></span>
                    <?php if (!$b['earned']) : ?><span style="color:#94a3b8;font-size:8px">🔒</span><?php endif; ?>
                </div>
                <?php endforeach; ?>
            </div>

            <!-- Score Items -->
            <div style="display:flex;flex-direction:column;gap:4px;margin-bottom:10px">
                <?php foreach ($scoreItems as $item) :
                    $passColor = $item['pass'] ? '#22c55e' : ($item['score'] >= ($item['max']*0.5) ? '#eab308' : '#ef4444');
                    $rowBg = $item['pass'] ? '#f0fdf4' : ($item['score'] >= ($item['max']*0.5) ? '#fefce8' : '#fef2f2');
                    $fixUrl = '';
                    if (!empty($item['fix_action']) && $item['fix_action'] === 'fix_robots') $fixUrl = wp_nonce_url(admin_url('admin.php?page=shop2llm&shop2llm_fix_robots=1'), 'shop2llm_fix_robots');
                    elseif (!empty($item['fix_action']) && $item['fix_action'] === 'flush_rewrite') $fixUrl = admin_url('options-permalink.php');
                    elseif (!empty($item['fix_action']) && $item['fix_action'] === 'edit_products') $fixUrl = admin_url('edit.php?post_type=product');
                    elseif (!empty($item['fix_action']) && $item['fix_action'] === 'edit_posts') $fixUrl = admin_url('edit.php');
                    elseif (!empty($item['fix_action']) && $item['fix_action'] === 'check_settings') $fixUrl = admin_url('admin.php?page=shop2llm');
                    elseif (!empty($item['fix_action']) && $item['fix_action'] === 'settings') $fixUrl = admin_url('admin.php?page=shop2llm');
                ?>
                <div class="stl-score-row" style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:<?php echo esc_attr($rowBg); ?>;font-size:11px">
                    <span style="flex-shrink:0;width:16px;text-align:center;font-size:12px;color:<?php echo esc_attr($passColor); ?>"><?php echo $item['pass'] ? '✓' : '✗'; ?></span>
                    <span style="flex:1;min-width:0;font-weight:600;color:#1e1e1e;white-space:nowrap"><?php echo esc_html($item['label']); ?></span>
                    <span style="font-size:10px;color:#94a3b8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:160px" title="<?php echo esc_attr($item['hint']); ?>"><?php echo esc_html($item['hint']); ?></span>
                    <span style="flex-shrink:0;font-weight:700;color:<?php echo esc_attr($passColor); ?>;font-size:12px;min-width:40px;text-align:right"><?php echo esc_html((string)$item['score']); ?><span style="font-weight:400;color:#94a3b8;font-size:9px">/<?php echo esc_html((string)$item['max']); ?></span></span>
                    <?php if (!$item['pass'] && !empty($item['fix_action'])) : ?>
                        <?php if ($item['fix_action'] === 'test_mcp') : ?>
                        <button type="button" class="stl-test-search-btn" style="flex-shrink:0;padding:2px 8px;background:<?php echo esc_attr($scoreColor); ?>;color:#fff;border:none;border-radius:4px;font-size:9px;font-weight:600;cursor:pointer"><?php echo esc_html($item['fix_label']); ?> →</button>
                        <?php elseif ($fixUrl) : ?>
                        <a href="<?php echo esc_url($fixUrl); ?>" class="stl-fix-link" data-fix-action="<?php echo esc_attr($item['fix_action']); ?>" style="flex-shrink:0;padding:2px 8px;background:<?php echo esc_attr($scoreColor); ?>;color:#fff;border-radius:4px;font-size:9px;font-weight:600;text-decoration:none"><?php echo esc_html($item['fix_label']); ?> →</a>
                        <?php endif; ?>
                    <?php endif; ?>
                </div>
                <?php endforeach; ?>
            </div>

            <!-- Community Trust (async loaded) -->
            <div id="stl-community-trust" style="margin:8px 0;padding:5px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;text-align:center;display:none">
                <span id="stl-community-text" style="font-size:10px;color:#166534;font-weight:600"></span>
            </div>

            <div style="display:flex;gap:8px;margin-top:10px">
                <?php if ($totalScore < $freeMax) : ?>
                <a href="#" id="stl-fix-all-btn" style="flex:1;text-align:center;padding:6px 0;background:<?php echo esc_attr($scoreColor); ?>;color:#fff;border-radius:6px;font-size:11px;font-weight:600;text-decoration:none"><?php echo esc_html__('Fix All Items', 'shop-to-llm'); ?> →</a>
                <?php endif; ?>
            </div>
        </div>

        <!-- v1.1.1: Test Search redirect + Fix All JS -->
        <script>
        (function(){
            // v1.1.1: Score-card "Test Search" button now scrolls to the Hero search box at the top
            var testSearchBtns = document.querySelectorAll('.stl-test-search-btn');
            if (testSearchBtns.length) {
                testSearchBtns.forEach(function(btn){
                    btn.addEventListener('click', function(){
                        if (window.shop2llmFocusHeroSearch) {
                            window.shop2llmFocusHeroSearch();
                        }
                    });
                });
            }
            var fixAllBtn = document.getElementById('stl-fix-all-btn');
            if (fixAllBtn) {
                fixAllBtn.addEventListener('click', function(e){
                    e.preventDefault();
                    fixAllBtn.disabled = true;
                    fixAllBtn.style.opacity = '0.7';

                    // Collect all fixable rows, track manual items separately
                    var allRows = document.querySelectorAll('.stl-score-row');
                    var fixQueue = [];
                    var manualItems = [];
                    allRows.forEach(function(row){
                        var btn = row.querySelector('.stl-test-search-btn');
                        var link = row.querySelector('.stl-fix-link');
                        if (!btn && !link) return; // nothing to fix
                        var action = link ? link.getAttribute('data-fix-action') : 'test_mcp';
                        var label = row.querySelector('span:nth-child(2)') ? row.querySelector('span:nth-child(2)').textContent.trim() : '';
                        // Actions that can be auto-fixed via AJAX
                        var autoFixable = ['test_mcp', 'fix_robots', 'flush_rewrite'];
                        if (autoFixable.indexOf(action) === -1) {
                            manualItems.push({row: row, label: label, action: action, link: link});
                            return;
                        }
                        fixQueue.push({row: row, btn: btn, link: link, action: action});
                    });

                    if (fixQueue.length === 0 && manualItems.length === 0) {
                        fixAllBtn.textContent = 'All items OK ✓';
                        fixAllBtn.disabled = false;
                        fixAllBtn.style.opacity = '1';
                        return;
                    }

                    var currentIdx = 0;
                    var nonce = '<?php echo esc_js(wp_create_nonce('shop2llm_admin_nonce')); ?>';
                    var ajaxurl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';

                    function processNext() {
                        if (currentIdx >= fixQueue.length) {
                            // Build completion message
                            var msg = 'Auto-fixed ' + fixQueue.length + ' item(s)';
                            if (manualItems.length > 0) {
                                msg += ' · ' + manualItems.length + ' need manual action →';
                            } else {
                                msg += ' ✓';
                            }
                            fixAllBtn.textContent = msg;
                            fixAllBtn.style.opacity = '1';
                            fixAllBtn.style.background = manualItems.length > 0 ? '#eab308' : '#16a34a';
                            fixAllBtn.disabled = false;

                            // Highlight manual items with a pulse
                            if (manualItems.length > 0) {
                                manualItems.forEach(function(m){
                                    var origBg = m.row.style.background;
                                    m.row.style.transition = 'background 0.4s';
                                    m.row.style.background = '#fef3c7';
                                    setTimeout(function(){ m.row.style.background = origBg; }, 2000);
                                    // Scroll to first manual item
                                    if (manualItems[0] === m) {
                                        setTimeout(function(){ m.row.scrollIntoView({behavior:'smooth', block:'center'}); }, 300);
                                    }
                                });
                            }
                            return;
                        }

                        var item = fixQueue[currentIdx];
                        fixAllBtn.textContent = 'Fixing ' + (currentIdx + 1) + '/' + fixQueue.length + '...';

                        // Scroll to row
                        item.row.scrollIntoView({behavior: 'smooth', block: 'center'});

                        // Flash highlight
                        var origBg = item.row.style.background;
                        item.row.style.transition = 'background 0.3s';
                        item.row.style.background = '#dbeafe';

                        function markDone(success) {
                            item.row.style.background = success ? '#f0fdf4' : origBg;
                            // Update checkmark: find the first span (icon)
                            var iconSpan = item.row.querySelector('span');
                            if (iconSpan && success) {
                                iconSpan.textContent = '✓';
                                iconSpan.style.color = '#22c55e';
                            }
                            currentIdx++;
                            setTimeout(processNext, 350);
                        }

                        if (item.action === 'test_mcp') {
                            // v1.1.1: Scroll to Hero Test Search box at dashboard top
                            if (window.shop2llmFocusHeroSearch) {
                                window.shop2llmFocusHeroSearch();
                            }
                            fixAllBtn.textContent = '✓ Test search opened';
                            setTimeout(function(){ markDone(true); }, 600);
                        } else if (item.action === 'fix_robots' || item.action === 'flush_rewrite') {
                            // Call AJAX to fix
                            var fd = new FormData();
                            fd.append('action', 'shop2llm_fix_item');
                            fd.append('fix_action', item.action);
                            fd.append('nonce', nonce);
                            fetch(ajaxurl, {method:'POST', body: fd})
                            .then(function(r){ return r.json(); })
                            .then(function(d){
                                if (d.success) {
                                    fixAllBtn.textContent = '✓ Fixed: ' + (item.action === 'fix_robots' ? 'Robots.txt' : 'Permalinks');
                                    markDone(true);
                                } else {
                                    fixAllBtn.textContent = '✗ Failed';
                                    markDone(false);
                                }
                            })
                            .catch(function(){
                                fixAllBtn.textContent = '✗ Error';
                                markDone(false);
                            });
                        }
                    }

                    processNext();
                });
            }
        })();
        </script>

        <!-- AI Visibility Summary -->
        <div class="stl-card" style="border-left:4px solid #10b981">
          <h2><?php echo esc_html__('AI Visibility Summary', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__("Today's AI activity at a glance: API calls, crawler bots, searches, and tools available.", 'shop-to-llm'); ?>"></span></h2>
          <!-- Status badge row -->
          <div style="margin-bottom:10px">
            <?php if ($enabled === 'yes'): ?>
              <span class="stl-badge stl-badge-ok"><?php echo esc_html__('Connected', 'shop-to-llm'); ?></span>
              <span style="font-size:11px;color:#94a3b8;margin-left:4px"><?php echo esc_html($storeName); ?></span>
            <?php else: ?>
              <span class="stl-badge stl-badge-off"><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></span>
            <?php endif; ?>
          </div>
          <!-- 4 metric boxes -->
          <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
            <div style="text-align:center;padding:10px 6px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
              <div style="font-size:22px;font-weight:700;color:#166534"><?php echo esc_html($todayCount); ?></div>
              <div style="font-size:10px;color:#64748b;margin-top:2px"><?php echo esc_html__('API Calls', 'shop-to-llm'); ?></div>
            </div>
            <div style="text-align:center;padding:10px 6px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
              <div style="font-size:22px;font-weight:700;color:#6366f1"><?php echo esc_html($crawlerCount); ?></div>
              <div style="font-size:10px;color:#64748b;margin-top:2px"><?php echo esc_html__('AI Bots', 'shop-to-llm'); ?></div>
            </div>
            <div style="text-align:center;padding:10px 6px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
              <div style="font-size:22px;font-weight:700;color:#d97706"><?php echo esc_html($searchCount); ?></div>
              <div style="font-size:10px;color:#64748b;margin-top:2px"><?php echo esc_html__('Searches', 'shop-to-llm'); ?></div>
            </div>
            <div style="text-align:center;padding:10px 6px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
              <div style="font-size:22px;font-weight:700;color:#7c3aed">2</div>
              <div style="font-size:10px;color:#64748b;margin-top:2px"><?php echo esc_html__('AI Tools', 'shop-to-llm'); ?></div>
            </div>
          </div>
          <?php if ($todayCount === 0 && $crawlerCount === 0): ?>
          <div style="margin-top:8px;padding:6px 10px;background:#eef2ff;border:1px solid #c7d2fe;border-radius:6px;text-align:center;font-size:11px;color:#6366f1;font-weight:500">
            <?php echo shop_to_llm_has_woocommerce() ? esc_html__('👋 New here? Go to Settings → Connection Config → paste into ChatGPT', 'shop-to-llm') : esc_html__('👋 New here? Go to Settings → Connection Config → paste into ChatGPT', 'shop-to-llm'); ?>
          </div>
          <?php endif; ?>
          <div style="margin-top:10px;padding:10px 14px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;font-size:12px;line-height:1.7;color:#0c4a6e">
            <strong style="display:block;margin-bottom:4px;color:#0369a1"><?php echo esc_html__('💡 How to read this', 'shop-to-llm'); ?></strong>
            <?php if ($crawlerCount > 0 || $todayCount > 0) : ?>
              <?php echo esc_html__('Each API call = an AI assistant connected to your site.', 'shop-to-llm'); ?><br>
              <?php echo esc_html__('Each crawler visit = an AI search engine indexed your content.', 'shop-to-llm'); ?><br>
              <?php echo esc_html__('More activity → better AI visibility → more organic traffic.', 'shop-to-llm'); ?>
            <?php else : ?>
              <?php echo shop_to_llm_has_woocommerce() ? esc_html__('AI activity will appear here as ChatGPT and other AI assistants discover your store. Copy your connection config from the Settings tab and paste into ChatGPT to get started.', 'shop-to-llm') : esc_html__('AI activity will appear here as ChatGPT and other AI assistants discover your site. Copy your connection config from the Settings tab and paste into ChatGPT to get started.', 'shop-to-llm'); ?>
            <?php endif; ?>
          </div>
          <p class="s2ll-suggestion">💡 <?php echo esc_html__('Check the Analytics tab for detailed trends and top search terms', 'shop-to-llm'); ?></p>
        </div>

        <!-- AI-Driven Traffic -->
        <div class="stl-card" style="border-left:4px solid #10b981">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
                <?php echo esc_html__('AI-Driven Traffic', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('Visitors who found your site through AI platforms like ChatGPT, Perplexity, and Copilot.', 'shop-to-llm'); ?>"></span>
            </h2>
            <?php shop_to_llm_render_ai_referral_section(); ?>
        </div>

        </div><!-- /tab-dashboard -->

        <!-- Analytics Tab -->
        <div class="stl-tab-panel" id="tab-analytics">
        <!-- AI Readiness Score (unified: 9-item product model for commerce, 25-item model for content) -->
        <?php
        $isCommerce = ($siteMode === 'commerce' && shop_to_llm_has_woocommerce());
        $scannedPost = null;
        $scannedProduct = null;
        $scoreResults = [];
        $totalPostScore = 0;
        $dimensions = [];
        $searchInput = '';
        $selectedPostId = isset($_POST['stl_post_select']) ? (int)$_POST['stl_post_select'] : 0;
        $isContentSearch = isset($_POST['stl_content_search']) && !empty($_POST['stl_search_input']);

        // Handle search/dropdown selection
        $triggerLookup = false;
        if ($isCommerce && ((isset($_POST['shop_to_llm_sku_lookup']) && !empty($_POST['stl_search_input'])) || $selectedPostId > 0)) {
            if ($selectedPostId > 0) {
                $searchInput = (string)$selectedPostId;
            } else {
                $searchInput = sanitize_text_field(wp_unslash($_POST['stl_search_input']));
            }
            $triggerLookup = true;
        } elseif (!$isCommerce && ($selectedPostId > 0 || $isContentSearch)) {
            if ($selectedPostId > 0) {
                $triggerLookup = true;
                $searchInput = (string)$selectedPostId;
            } elseif ($isContentSearch) {
                $searchInput = sanitize_text_field(wp_unslash($_POST['stl_search_input']));
                $triggerLookup = true;
            }
        }

        if ($triggerLookup) {
            if ($isCommerce) {
                // Commerce: search products
                if ($selectedPostId > 0) {
                    // Get by ID directly (from dropdown)
                    $product = wc_get_product($selectedPostId);
                    $products = $product ? [$product] : [];
                } else {
                    $products = wc_get_products(['sku' => $searchInput, 'limit' => 1]);
                    if (empty($products)) $products = wc_get_products(['s' => $searchInput, 'limit' => 1]);
                }
                if (!empty($products)) {
                    $scannedProduct = $products[0];
                    $scannedPost = get_post($scannedProduct->get_id());
                    $p = $scannedProduct;
                    $dimensions = [
                        ['name' => __('Product Name', 'shop-to-llm'), 'pass' => strlen($p->get_name()) > 3, 'weight' => 10],
                        ['name' => __('Short Description', 'shop-to-llm'), 'pass' => strlen($p->get_short_description()) > 20, 'weight' => 15],
                        ['name' => __('Full Description', 'shop-to-llm'), 'pass' => strlen($p->get_description()) > 50, 'weight' => 15],
                        ['name' => __('Product Images', 'shop-to-llm'), 'pass' => (int)$p->get_image_id() > 0, 'weight' => 15],
                        ['name' => __('Price Set', 'shop-to-llm'), 'pass' => (float)$p->get_regular_price() > 0, 'weight' => 10],
                        ['name' => __('Categories', 'shop-to-llm'), 'pass' => count(wp_get_post_terms($p->get_id(), 'product_cat')) > 0, 'weight' => 10],
                        ['name' => __('Tags', 'shop-to-llm'), 'pass' => count(wp_get_post_terms($p->get_id(), 'product_tag')) > 0, 'weight' => 5],
                        ['name' => __('Stock Managed', 'shop-to-llm'), 'pass' => $p->get_manage_stock(), 'weight' => 10],
                        ['name' => __('Attributes / Specs', 'shop-to-llm'), 'pass' => count($p->get_attributes()) > 0, 'weight' => 10],
                    ];
                    foreach ($dimensions as $c) {
                        if ($c['pass']) $totalPostScore += $c['weight'];
                    }
                }
            } else {
                // Content: search posts (by ID from dropdown, or title from text)
                $post = null;
                if ($selectedPostId > 0) {
                    $post = get_post($selectedPostId);
                } elseif ($isContentSearch) {
                    $posts = get_posts(['s' => $searchInput, 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 1]);
                    $post = !empty($posts) ? $posts[0] : null;
                }
                if ($post && $post->post_status === 'publish') {
                    $scannedPost = $post;
                    // Run the 25-item model
                    $scoreResults = shop_to_llm_score_single_post($post->ID);
                    $totalPostScore = $scoreResults['total'];
                    if (!empty($scoreResults['groups'])) {
                        $dimensions = $scoreResults['groups'];
                    }
                }
            }
        }

        // Build dropdown options
        $recentItems = [];
        if ($isCommerce) {
            $recentProducts = wc_get_products(['limit' => 50, 'status' => 'publish', 'orderby' => 'date']);
            foreach ($recentProducts as $rp) {
                $recentItems[] = ['id' => $rp->get_id(), 'title' => $rp->get_name()];
            }
            $dropdownLabel = __('Select a product', 'shop-to-llm');
            $searchPlaceholder = __('Or search by name/SKU...', 'shop-to-llm');
            $cardTitle = __('Product AI Score', 'shop-to-llm');
            $cardHelp = __('Check individual product readiness for AI discovery. 9-dimension product model.', 'shop-to-llm');
        } else {
            $recentPosts = get_posts(['post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => 50, 'orderby' => 'date']);
            foreach ($recentPosts as $rp) {
                $recentItems[] = ['id' => $rp->ID, 'title' => $rp->post_title];
            }
            $dropdownLabel = __('Select a post', 'shop-to-llm');
            $searchPlaceholder = __('Or search by title...', 'shop-to-llm');
            $cardTitle = __('Content AI Score', 'shop-to-llm');
            $cardHelp = __('Check individual post readiness for AI discovery. 25-dimension 5-group model.', 'shop-to-llm');
        }
        ?>
        <div class="stl-card" id="stl-ai-readiness">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><?php echo esc_html($cardTitle); ?><span class="s2ll-help" title="<?php echo esc_attr($cardHelp); ?>"></span></h2>

            <form method="post" style="margin-bottom:12px">
                <input type="hidden" name="stl_tab" value="analytics">
                <div style="display:flex;gap:8px;align-items:center">
                    <?php if (!empty($recentItems)): ?>
                    <select name="stl_post_select" style="border:1px solid #e2e8f0;border-radius:6px;padding:8px 10px;font-size:13px;min-width:180px;max-width:260px" onchange="this.form.querySelector('input[name=stl_search_input]').value='';this.form.submit()">
                        <option value=""><?php echo esc_html($dropdownLabel); ?>...</option>
                        <?php foreach ($recentItems as $ri): ?>
                        <option value="<?php echo esc_attr((string)$ri['id']); ?>" <?php selected($selectedPostId, $ri['id']); ?>><?php echo esc_html(mb_substr($ri['title'], 0, 55)); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <?php endif; ?>
                    <input type="text" name="stl_search_input" value="<?php echo esc_attr($searchInput && !$selectedPostId ? $searchInput : ''); ?>" placeholder="<?php echo esc_attr($searchPlaceholder); ?>" style="flex:1;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;font-size:13px" onchange="this.form.querySelector('select').value=''">
                    <button type="submit" name="<?php echo esc_attr($isCommerce ? 'shop_to_llm_sku_lookup' : 'stl_content_search'); ?>" style="padding:8px 16px;background:#6366f1;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer"><?php echo esc_html__('Check', 'shop-to-llm'); ?></button>
                </div>
            </form>

            <?php if ($scannedPost) : ?>
                <?php
                $scoreColor = $totalPostScore >= 70 ? '#16a34a' : ($totalPostScore >= 40 ? '#d97706' : '#dc2626');
                $editUrl = admin_url('post.php?post=' . $scannedPost->ID . '&action=edit');
                ?>
                <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px">
                    <div style="display:flex;align-items:center;gap:14px;margin-bottom:10px">
                        <div style="font-size:26px;font-weight:700;color:<?php echo esc_attr($scoreColor); ?>"><?php echo esc_html((string)$totalPostScore); ?>/100</div>
                        <div>
                            <div style="font-size:13px;font-weight:600;color:#1e1e1e"><?php echo esc_html($scannedPost->post_title); ?></div>
                            <div style="font-size:11px;color:#64748b"><?php echo $isCommerce ? esc_html__('AI readiness score for this product', 'shop-to-llm') : esc_html__('AI readiness score — 25-dimension analysis', 'shop-to-llm'); ?></div>
                        </div>
                        <a href="<?php echo esc_url($editUrl); ?>" target="_blank" style="margin-left:auto;font-size:11px;color:#6366f1;font-weight:500;text-decoration:none;white-space:nowrap"><?php echo esc_html__('Edit →', 'shop-to-llm'); ?></a>
                    </div>

                    <?php if ($isCommerce) : ?>
                        <!-- Commerce: 9-item flat list -->
                        <div style="display:flex;flex-direction:column;gap:4px">
                            <?php foreach ($dimensions as $dim) : ?>
                            <div style="display:flex;align-items:center;gap:8px;font-size:12px;padding:3px 0">
                                <span style="font-size:10px;min-width:16px;text-align:center;padding:1px 3px;border-radius:2px;<?php echo $dim['pass'] ? 'background:#dcfce7;color:#16a34a' : 'background:#fef2f2;color:#dc2626'; ?>"><?php echo $dim['pass'] ? '✓' : '✗'; ?></span>
                                <span style="color:<?php echo $dim['pass'] ? '#1e1e1e' : '#94a3b8'; ?>"><?php echo esc_html($dim['name']); ?></span>
                                <span style="margin-left:auto;color:#94a3b8;font-size:10px">+<?php echo esc_html((string)$dim['weight']); ?></span>
                            </div>
                            <?php endforeach; ?>
                        </div>
                    <?php else : ?>
                        <!-- Content: 25-item grouped by L1-L5 -->
                        <?php
                        $groups = [
                            'L1' => ['label' => __('Discoverability', 'shop-to-llm'), 'max' => 30, 'color' => '#6366f1', 'items' => []],
                            'L2' => ['label' => __('Relevance', 'shop-to-llm'), 'max' => 25, 'color' => '#8b5cf6', 'items' => [], 'locked' => empty($scoreResults['has_focus'])],
                            'L3' => ['label' => __('Content Quality', 'shop-to-llm'), 'max' => 25, 'color' => '#10b981', 'items' => []],
                            'L4' => ['label' => __('Entity Authority', 'shop-to-llm'), 'max' => 15, 'color' => '#f59e0b', 'items' => [], 'locked' => empty($scoreResults['has_focus'])],
                            'L5' => ['label' => __('Activity', 'shop-to-llm'), 'max' => 5, 'color' => '#ec4899', 'items' => []],
                        ];
                        // Distribute dimensions into groups
                        $allItems = $scoreResults['items'] ?? [];
                        $groupBoundaries = [6, 12, 18, 21, 25]; // L1=1-6, L2=7-12, L3=13-18, L4=19-21, L5=22-25
                        $gi = 0;
                        $groupKeys = array_keys($groups);
                        foreach ($allItems as $idx => $item) {
                            while ($gi < 4 && $idx >= $groupBoundaries[$gi]) $gi++;
                            $gkey = $groupKeys[$gi] ?? 'L1';
                            $groups[$gkey]['items'][] = $item;
                        }
                        // Calculate group scores
                        foreach ($groups as $gkey => &$g) {
                            $g['score'] = 0;
                            foreach ($g['items'] as $it) {
                                if (!($it['locked'] ?? false) && ($it['pass'] ?? false)) $g['score'] += (int)($it['pts'] ?? 0);
                            }
                        }
                        unset($g);
                        ?>
                        <?php foreach ($groups as $g) :
                            $barPct = $g['max'] > 0 ? round($g['score'] / $g['max'] * 100) : 0;
                            $barColor = $barPct >= 70 ? '#16a34a' : ($barPct >= 40 ? '#d97706' : '#dc2626');
                        ?>
                        <div style="margin-bottom:10px">
                            <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
                                <span style="font-size:11px;font-weight:700;color:<?php echo esc_attr($g['color']); ?>"><?php echo esc_html($g['label']); ?></span>
                                <div style="flex:1;height:5px;background:#e2e8f0;border-radius:3px"><div style="width:<?php echo esc_attr($barPct); ?>%;height:5px;background:<?php echo esc_attr($barColor); ?>;border-radius:3px"></div></div>
                                <span style="font-size:11px;font-weight:600;color:<?php echo esc_attr($barColor); ?>"><?php echo esc_html((string)$g['score']); ?>/<?php echo esc_html((string)$g['max']); ?></span>
                                <?php if (!empty($g['locked'])): ?><span style="font-size:9px;color:#94a3b8">🔒</span><?php endif; ?>
                            </div>
                            <?php if (!empty($g['locked'])): ?>
                                <div style="padding:4px 8px;background:#fafafa;border:1px dashed #e2e8f0;border-radius:4px;font-size:10px;color:#94a3b8;text-align:center">
                                    <?php echo sprintf(/* translators: %s: category label */ esc_html__('Set a Focus Query in the post editor to unlock %s items', 'shop-to-llm'), esc_html($g['label'])); ?>
                                </div>
                            <?php else: ?>
                                <?php foreach ($g['items'] as $it): ?>
                                <div style="display:flex;align-items:center;gap:6px;font-size:10px;padding:1px 4px;margin:1px 0">
                                    <span style="color:<?php echo ($it['pass'] ?? false) ? '#16a34a' : '#94a3b8'; ?>;min-width:12px"><?php echo ($it['pass'] ?? false) ? '✓' : '✗'; ?></span>
                                    <span style="color:<?php echo ($it['pass'] ?? false) ? '#1e1e1e' : '#94a3b8'; ?>;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html($it['name'] ?? ''); ?></span>
                                    <span style="margin-left:auto;color:#94a3b8"><?php echo ($it['pass'] ?? false) ? '+' : ''; ?><?php echo esc_html((string)($it['pts'] ?? 0)); ?></span>
                                </div>
                                <?php endforeach; ?>
                            <?php endif; ?>
                        </div>
                        <?php endforeach; ?>
                    <?php endif; ?>
                </div>
            <?php elseif ($triggerLookup) : ?>
                <p style="color:#94a3b8;font-size:13px;text-align:center;padding:16px 0"><?php echo esc_html__('Not found. Try selecting from the dropdown or use a different keyword.', 'shop-to-llm'); ?></p>
            <?php else : ?>
                <p style="color:#94a3b8;font-size:13px;margin:0;line-height:1.5">
                    <?php echo $isCommerce ? esc_html__('Select a product from the dropdown or type a name/SKU. A higher score means AI is more likely to recommend this product.', 'shop-to-llm') : esc_html__('Select a post from the dropdown or search by title. The 25-dimension model checks discoverability, relevance, quality, authority, and activity.', 'shop-to-llm'); ?>
                </p>
            <?php endif; ?>
            <?php if ($totalPostScore < 70 && $scannedPost) : ?>
            <p class="s2ll-suggestion">💡 <?php echo $isCommerce ? esc_html__('Click Edit to improve missing product data', 'shop-to-llm') : esc_html__('Click Edit → set Focus Query to unlock full 25-dimension analysis', 'shop-to-llm'); ?></p>
            <?php endif; ?>
        </div>
        <?php // end AI Readiness Score ?>

        <p class="s2ll-suggestion" style="margin-top:16px">
            💡 <?php echo esc_html__('Single-post analysis is also available in the post editor sidebar (meta box). For site-wide readiness, see Overview and AI Agents tabs.', 'shop-to-llm'); ?>
        </p>

        </div><!-- /tab-analytics -->

        <!-- AI Agents Tab (v1.3.6+) — Agentic Commerce Readiness: UCP + MCP + agents.md + Crawler Registry + robots.txt -->
        <div class="stl-tab-panel" id="tab-agents">
        <!-- UCP Readiness Score — 7-dimension transparent methodology (v1.3.4+) -->
        <?php shop_to_llm_render_ucp_readiness_panel(); ?>

        <!-- MCP Endpoint Status -->
        <div class="stl-card" style="border-left:4px solid #6366f1">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#6366f1">
                    <path d="M4 6h16M4 12h16M4 18h16"/>
                </svg>
                <?php echo esc_html__('MCP Endpoint', 'shop-to-llm'); ?>
                <span class="s2ll-help" title="<?php echo esc_attr__('Model Context Protocol endpoint — lets AI agents call your store like a native tool.', 'shop-to-llm'); ?>"></span>
            </h2>
            <?php
            $siteUrl = rtrim((string) get_site_url(), '/');
            $mcpEnabled = get_option('shop_to_llm_enabled', 'yes') === 'yes';
            $mcpUrl = $siteUrl . '/wp-json/shop2llm/v1/mcp';
            $discoveryUrl = $siteUrl . '/wp-json/shop2llm/v1/.well-known/mcp';
            $cachedMcpDiscovery = get_transient('shop_to_llm_ucp_check_mcp');
            ?>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Endpoint URL', 'shop-to-llm'); ?></span>
                <span class="stl-value stl-mono"><?php echo esc_html($mcpUrl); ?></span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Protocol', 'shop-to-llm'); ?></span>
                <span class="stl-value">
                    <?php if ($mcpEnabled) : ?>
                        <span class="stl-badge stl-badge-ok">✓ JSON-RPC 2.0 — 23 tools</span>
                    <?php else : ?>
                        <span class="stl-badge stl-badge-off"><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></span>
                    <?php endif; ?>
                </span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Discovery', 'shop-to-llm'); ?></span>
                <span class="stl-value">
                    <?php if ($cachedMcpDiscovery === '1') : ?>
                        <span class="stl-badge stl-badge-ok">✓ /.well-known/mcp reachable</span>
                    <?php else : ?>
                        <span class="stl-badge stl-badge-pro">⚠ /.well-known/mcp not reachable</span>
                    <?php endif; ?>
                </span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Tools', 'shop-to-llm'); ?></span>
                <span class="stl-value" style="font-size:12px;color:#475569">
                    search_catalog, get_product, get_cart, balance_query, profit_dashboard, churn_prediction, RAG_ask, RAG_search, engagement_profile, experiment_results, +14 more
                </span>
            </div>
            <p class="s2ll-suggestion" style="margin-top:12px">
                💡 <?php echo esc_html__('Add your MCP URL to ChatGPT, Claude, or Gemini to let AI agents shop your catalog.', 'shop-to-llm'); ?>
                <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm#tab-settings')); ?>" style="color:#6366f1;font-weight:600"><?php echo esc_html__('Configure →', 'shop-to-llm'); ?></a>
            </p>
        </div>

        <!-- /agents.md Endpoint -->
        <div class="stl-card" style="border-left:4px solid #0ea5e9">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#0ea5e9">
                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
                    <polyline points="14 2 14 8 20 8"/>
                </svg>
                <?php echo esc_html__('/agents.md Endpoint', 'shop-to-llm'); ?>
                <span class="s2ll-help" title="<?php echo esc_attr__('Markdown dossier for AI agents — describes your catalog, MCP tools, schema types, and discovery files.', 'shop-to-llm'); ?>"></span>
            </h2>
            <?php
            $agentsMdUrl = $siteUrl . '/agents.md';
            $llmsFullUrl = $siteUrl . '/llms-full.txt';
            $llmsUrl = $siteUrl . '/llms.txt';
            $cachedLlms = get_transient('shop_to_llm_ucp_check_llms');
            ?>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('/agents.md', 'shop-to-llm'); ?></span>
                <span class="stl-value stl-mono">
                    <a href="<?php echo esc_url($agentsMdUrl); ?>" target="_blank" rel="noopener" style="color:#6366f1"><?php echo esc_html($agentsMdUrl); ?></a>
                </span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('/llms-full.txt', 'shop-to-llm'); ?></span>
                <span class="stl-value stl-mono">
                    <a href="<?php echo esc_url($llmsFullUrl); ?>" target="_blank" rel="noopener" style="color:#6366f1"><?php echo esc_html($llmsFullUrl); ?></a>
                </span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('/llms.txt', 'shop-to-llm'); ?></span>
                <span class="stl-value">
                    <?php if ($cachedLlms === '1') : ?>
                        <span class="stl-badge stl-badge-ok">✓ Reachable</span>
                    <?php else : ?>
                        <span class="stl-badge stl-badge-pro">⚠ Not reachable (flush rewrites)</span>
                    <?php endif; ?>
                </span>
            </div>
            <p class="s2ll-suggestion" style="margin-top:12px">
                💡 <?php echo esc_html__('Crawlers discover these endpoints via robots.txt. Cache TTL: 1 hour (auto-invalidates on product/post save).', 'shop-to-llm'); ?>
            </p>
        </div>

        <!-- AI Crawler Registry -->
        <div class="stl-card" style="border-left:4px solid #16a34a">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#16a34a">
                    <circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
                </svg>
                <?php echo esc_html__('AI Crawler Registry', 'shop-to-llm'); ?>
                <span class="stl-badge stl-badge-ok"><?php echo (int) shop_to_llm_get_ai_crawler_count(); ?> bots</span>
            </h2>
            <p style="font-size:13px;color:#475569;margin:0 0 10px;line-height:1.6">
                <?php echo esc_html__('Centralized registry of 65+ AI crawlers, used by robots.txt, crawler detector, /agents.md, and UCP score. Single source of truth.', 'shop-to-llm'); ?>
            </p>
            <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;font-size:12px">
                <?php
                $registrySummary = [
                    'OpenAI' => ['GPTBot', 'ChatGPT-User', 'OAI-SearchBot'],
                    'Anthropic' => ['ClaudeBot', 'Claude-Web', 'anthropic-ai'],
                    'Perplexity' => ['PerplexityBot', 'Perplexity-User', 'Swiftbot'],
                    'Google' => ['Google-Extended', 'Googlebot', 'Googlebot-News'],
                    'Microsoft' => ['Bingbot', 'Bytespider', 'DuckDuckBot'],
                ];
                foreach ($registrySummary as $vendor => $bots) :
                    ?>
                    <div style="padding:8px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px">
                        <div style="font-weight:600;color:#166534;margin-bottom:4px"><?php echo esc_html($vendor); ?></div>
                        <div style="color:#475569"><?php echo esc_html(implode(', ', $bots)); ?></div>
                    </div>
                <?php endforeach; ?>
                <div style="padding:8px 10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;grid-column:1/-1">
                    <div style="font-weight:600;color:#166534;margin-bottom:4px">+ 50 more</div>
                    <div style="color:#475569">Meta, ByteDance, Common Crawl, Amazon, Apple, Cohere, AI Search upstarts (YouBot, Cotoy, PiplBot, etc.)</div>
                </div>
            </div>
        </div>

        <!-- robots.txt Configuration -->
        <div class="stl-card" style="border-left:4px solid #f59e0b">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#f59e0b">
                    <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
                </svg>
                <?php echo esc_html__('robots.txt Configuration', 'shop-to-llm'); ?>
            </h2>
            <?php $allowedCrawlers = shop_to_llm_get_robots_allowed_crawlers(); ?>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Allow rules', 'shop-to-llm'); ?></span>
                <span class="stl-value"><strong><?php echo (int) count($allowedCrawlers); ?></strong> <?php echo esc_html__('AI crawlers whitelisted', 'shop-to-llm'); ?></span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Disallow', 'shop-to-llm'); ?></span>
                <span class="stl-value"><span class="stl-badge stl-badge-ok">✓ No Disallow: / for AI bots</span></span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('View', 'shop-to-llm'); ?></span>
                <span class="stl-value stl-mono"><a href="<?php echo esc_url($siteUrl . '/robots.txt'); ?>" target="_blank" rel="noopener" style="color:#6366f1"><?php echo esc_html($siteUrl); ?>/robots.txt</a></span>
            </div>
            <p class="s2ll-suggestion" style="margin-top:12px">
                💡 <?php echo esc_html__('Cloudflare Managed robots.txt may inject Disallow rules — disable it to use this local robots.txt.', 'shop-to-llm'); ?>
            </p>
        </div>
        </div><!-- /tab-agents -->

        <!-- AI Traffic Tab (v1.3.6+) — AI visits + consumer AI search + GA4 AI referrer -->
        <div class="stl-tab-panel" id="tab-traffic">
        <!-- U1 Hero Score Bar — v1.3.5+ visual hierarchy refresh -->
        <div class="stl-card s2ll-hero-score" style="background:linear-gradient(135deg,#0ea5e9 0%,#6366f1 100%);color:#fff;border:none;padding:20px;margin:16px 0">
            <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:16px">
                <div>
                    <div style="font-size:11px;text-transform:uppercase;letter-spacing:0.05em;opacity:0.8;margin-bottom:4px"><?php echo esc_html__('Your AI Visibility Snapshot', 'shop-to-llm'); ?></div>
                    <div style="font-size:28px;font-weight:700;line-height:1"><?php echo esc_html__('Ready for AI Discovery', 'shop-to-llm'); ?></div>
                    <div style="font-size:13px;opacity:0.9;margin-top:6px"><?php echo esc_html__('UCP Score, Crawler Analytics & GA4 AI Traffic in one view', 'shop-to-llm'); ?></div>
                </div>
                <div style="display:flex;gap:24px">
                    <div style="text-align:center">
                        <div style="font-size:24px;font-weight:700"><?php echo esc_html(number_format_i18n(shop_to_llm_get_crawler_analytics(7)['total_visits'])); ?></div>
                        <div style="font-size:11px;opacity:0.8"><?php echo esc_html__('Crawler Visits (7d)', 'shop-to-llm'); ?></div>
                    </div>
                    <div style="text-align:center">
                        <div style="font-size:24px;font-weight:700"><?php echo esc_html(number_format_i18n(shop_to_llm_get_crawler_analytics(7)['unique_bots'])); ?></div>
                        <div style="font-size:11px;opacity:0.8"><?php echo esc_html__('Unique AI Bots', 'shop-to-llm'); ?></div>
                    </div>
                </div>
            </div>
        </div>

        <!-- P1-3: AI Crawler Analytics Widget (v1.3.5+) -->
        <?php shop_to_llm_render_crawler_analytics_widget(); ?>

        <!-- AI Crawler Activity -->
        <div class="stl-card" style="border-left:4px solid #8b5cf6">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>
                <?php echo esc_html__('AI Crawler Activity', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('Track when AI crawlers like GPTBot and ClaudeBot visit your site. More visits = better AI visibility.', 'shop-to-llm'); ?>"></span>
            </h2>
            <?php shop_to_llm_render_crawler_activity(); ?>
            <?php shop_to_llm_render_indexed_pages(); ?>
            <p class="s2ll-suggestion">💡 <?php echo esc_html__('Share your content regularly — AI crawlers return more often to sites that update frequently', 'shop-to-llm'); ?></p>
        </div>

        <!-- AI Search Activity -->
        <div class="stl-card">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg><?php echo esc_html__('AI Search Activity', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('See what consumers are asking AI assistants about your content. Shows search terms, results, and timing.', 'shop-to-llm'); ?>"></span></h2>
            <?php if ($searchCount === 0) : ?>
                <p style="color:#94a3b8;font-size:13px;margin:0;line-height:1.6">
                    <?php echo shop_to_llm_has_woocommerce() ? esc_html__('No AI searches today yet. When a consumer asks an AI (like ChatGPT) about your products, the search terms and matched products will appear here — so you can see what consumers are asking AI about your store.', 'shop-to-llm') : esc_html__('No AI searches today yet. When a consumer asks an AI (like ChatGPT) about your content, the search terms and matched items will appear here — so you can see what consumers are asking AI about your site.', 'shop-to-llm'); ?>
                </p>
            <?php else : ?>
                <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
                    <p style="color:#64748b;font-size:12px;margin:0"><?php /* translators: %d: number of searches */ echo shop_to_llm_has_woocommerce() ? sprintf(esc_html__('What consumers asked AI about your store today (%d searches):', 'shop-to-llm'), absint($searchCount)) : sprintf(esc_html__('What consumers asked AI about your site today (%d searches):', 'shop-to-llm'), absint($searchCount)); ?></p>
                    <select id="stl-sort" style="font-size:12px;padding:3px 8px;border:1px solid #e2e8f0;border-radius:4px;background:#fff;color:#64748b">
                        <option value="time-desc"><?php echo esc_html__('Latest first', 'shop-to-llm'); ?></option>
                        <option value="time-asc"><?php echo esc_html__('Oldest first', 'shop-to-llm'); ?></option>
                        <option value="results-desc"><?php echo esc_html__('Most results', 'shop-to-llm'); ?></option>
                        <option value="results-asc"><?php echo esc_html__('Fewest results', 'shop-to-llm'); ?></option>
                    </select>
                </div>
                <table id="stl-search-table" style="width:100%;border-collapse:collapse;font-size:13px">
                    <thead>
                        <tr style="border-bottom:2px solid #e2e8f0">
                            <th style="text-align:left;padding:6px 8px;color:#64748b;font-weight:600"><?php echo esc_html__('Search Term', 'shop-to-llm'); ?></th>
                            <th style="text-align:left;padding:6px 8px;color:#64748b;font-weight:600"><?php echo esc_html__('Products Found', 'shop-to-llm'); ?></th>
                            <th style="text-align:center;padding:6px 8px;color:#64748b;font-weight:600"><?php echo esc_html__('Results', 'shop-to-llm'); ?></th>
                            <th style="text-align:right;padding:6px 8px;color:#64748b;font-weight:600"><?php echo esc_html__('Time', 'shop-to-llm'); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach (array_slice($recentSearches, 0, 15) as $i => $s) : ?>
                        <tr data-idx="<?php echo esc_attr($i); ?>" data-results="<?php echo esc_attr((int)$s['results']); ?>" data-time="<?php echo esc_attr($s['time']); ?>" style="border-bottom:1px solid #f1f5f9">
                            <td style="padding:6px 8px;color:#1e1e1e;font-weight:500"><?php echo esc_html($s['query']); ?></td>
                            <td style="padding:6px 8px;color:#475569;font-size:12px">
                                <?php
                                $prods = $s['products'] ?? [];
                                if (!empty($prods)) {
                                    echo esc_html(implode(', ', $prods));
                                } else {
                                    echo '<span style="color:#94a3b8">—</span>';
                                }
                                ?>
                            </td>
                            <td style="padding:6px 8px;text-align:center">
                                <?php if ($s['results'] > 0) : ?>
                                    <span style="color:#166534;font-weight:600"><?php /* translators: %d: number of found results */ echo sprintf(esc_html__('%d found', 'shop-to-llm'), (int)$s['results']); ?></span>
                                <?php else : ?>
                                    <span style="color:#991b1b;font-weight:600"><?php echo esc_html__('No match', 'shop-to-llm'); ?></span>
                                <?php endif; ?>
                            </td>
                            <td style="padding:6px 8px;text-align:right;color:#94a3b8"><?php echo esc_html($s['time']); ?></td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
                <?php if ($searchCount > 15) : ?>
                <p style="color:#94a3b8;font-size:12px;margin:8px 0 0"><?php /* translators: %d: number of searches */ echo sprintf(esc_html__('Showing latest 15 of %d searches.', 'shop-to-llm'), absint($searchCount)); ?></p>
                <?php endif; ?>
            <?php endif; ?>
            <p class="s2ll-suggestion">💡 <?php echo esc_html__('Use these search terms to improve your content and Focus Queries', 'shop-to-llm'); ?></p>
        </div>

        <!-- AI-Driven Traffic -->
        <div class="stl-card" style="border-left:4px solid #10b981">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#10b981">
                    <path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
                </svg>
                <?php echo esc_html__('AI-Driven Traffic', 'shop-to-llm'); ?>
                <span class="s2ll-help" title="<?php echo esc_attr__('Attribution for orders originating from AI assistant referrals.', 'shop-to-llm'); ?>"></span>
            </h2>
            <?php shop_to_llm_render_ai_referral_section(); ?>
        </div>

        <!-- GA4 AI Traffic Integration (P1-2 v1.3.5+) -->
        <?php shop_to_llm_render_ga4_settings(); ?>
        </div><!-- /tab-traffic -->

        <!-- SEO Tab -->
        <div class="stl-tab-panel" id="tab-seo">
        <?php
        $seoMeta = get_option('shop_to_llm_seo_meta', 'yes');
        $seoSitemap = get_option('shop_to_llm_seo_sitemap', 'yes');
        $seoSchema = get_option('shop_to_llm_seo_schema', 'yes');
        $seoRedirect = get_option('shop_to_llm_seo_redirect', 'yes');
        $seoOg = get_option('shop_to_llm_seo_og', 'yes');
        $seoAnalysis = get_option('shop_to_llm_seo_analysis', 'yes');
        $seoAudit = get_option('shop_to_llm_seo_audit', 'yes');
        $hasSeoPlugin = shop_to_llm_has_seo_plugin();
        $seoPluginName = shop_to_llm_get_seo_plugin_name();

        // Review CTA card: shown once the user has experienced real value
        // (review_request_sent is set by the v1.3.2 trigger logic in log_search).
        // Per-admin dismiss state via user_meta 'shop2llm_review_card_dismissed'.
        $reviewCardVisible = get_option('shop_to_llm_review_request_sent')
            && get_user_meta(get_current_user_id(), 'shop2llm_review_card_dismissed', true) !== '1';
        if ($reviewCardVisible) :
            $reviewUrl = 'https://wordpress.org/support/plugin/shop2llm/reviews/#new-post';
            $crawlerPlatform = get_option('shop_to_llm_first_crawler_platform', 'AI');
            ?>
            <div class="stl-card" id="stl-review-card" style="background:linear-gradient(135deg,#f5f3ff 0%,#ede9fe 100%);border:1px solid #c4b5fd;margin-bottom:16px;position:relative">
                <button type="button" id="stl-review-card-dismiss" style="position:absolute;top:8px;right:8px;background:none;border:none;cursor:pointer;color:#94a3b8;font-size:16px;line-height:1;padding:4px" title="<?php echo esc_attr__('Dismiss — we won\'t ask again', 'shop-to-llm'); ?>">✕</button>
                <h2 style="color:#4338ca;margin:0 0 6px;display:flex;align-items:center;gap:6px">
                    <span style="font-size:18px">⭐</span>
                    <?php echo esc_html__('Enjoying Shop2LLM?', 'shop-to-llm'); ?>
                </h2>
                <p style="font-size:13px;color:#4c1d95;margin:0 0 10px;line-height:1.5">
                    <?php echo esc_html(sprintf(
                        /* translators: %s: first AI crawler platform that visited the site */
                        __('%s has already discovered your site. If Shop2LLM is helping you show up in AI search, a quick 5-star review on WordPress.org helps other site owners find it — and pushes us up the directory rankings.', 'shop-to-llm'),
                        $crawlerPlatform
                    )); ?>
                </p>
                <div style="display:flex;gap:8px;flex-wrap:wrap">
                    <a href="<?php echo esc_url($reviewUrl); ?>" target="_blank" rel="noopener" style="display:inline-block;padding:8px 16px;background:#6366f1;color:#fff;border-radius:6px;text-decoration:none;font-weight:600;font-size:13px">
                        ★ <?php echo esc_html__('Leave a 5-Star Review', 'shop-to-llm'); ?>
                    </a>
                    <button type="button" id="stl-review-card-later" style="padding:8px 14px;background:#fff;color:#6366f1;border:1px solid #c4b5fd;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500">
                        <?php echo esc_html__('Maybe later', 'shop-to-llm'); ?>
                    </button>
                </div>
            </div>
            <script>
            (function() {
                var card = document.getElementById('stl-review-card');
                if (!card) return;
                function dismiss() {
                    card.style.display = 'none';
                    var fd = new FormData();
                    fd.append('action', 'shop2llm_dismiss_review_card');
                    fd.append('nonce', <?php echo wp_json_encode(wp_create_nonce('shop2llm_admin_nonce')); ?>);
                    fetch(<?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>, { method: 'POST', body: fd, credentials: 'same-origin' }).catch(function(){});
                }
                var btn = document.getElementById('stl-review-card-dismiss');
                var later = document.getElementById('stl-review-card-later');
                if (btn) btn.addEventListener('click', dismiss);
                if (later) later.addEventListener('click', dismiss);
            })();
            </script>
        <?php endif; ?>

        <?php if ($hasSeoPlugin): ?>
        <div class="stl-card" style="border-left:4px solid #f59e0b;margin-bottom:16px">
            <h2 style="color:#d97706"><?php echo esc_html__('SEO Plugin Detected', 'shop-to-llm'); ?></h2>
            <p style="font-size:13px;color:#64748b"><?php echo sprintf(/* translators: %s: SEO plugin name */ esc_html__('%s is active. Conflicting SEO modules (meta tags, sitemap, schema) are automatically disabled to prevent duplicate output. You can still use Redirect Manager, Content Analysis, and SEO Audit.', 'shop-to-llm'), '<strong>' . esc_html($seoPluginName) . '</strong>'); ?></p>
        </div>
        <?php endif; ?>

        <?php
        $gsc_configured = shop_to_llm_gsc_is_configured();
        $gsc_connected = $gsc_configured && !empty(get_option('shop_to_llm_gsc_access_token', ''));
        if (isset($_GET['gsc_connected']) && $_GET['gsc_connected'] == '1'): ?>
        <div class="notice notice-success inline" style="margin:0 0 12px"><p><?php echo esc_html__('Google Search Console connected successfully!', 'shop-to-llm'); ?></p></div>
        <?php endif; ?>
        <?php if ($gsc_connected): ?>
        <div class="stl-card" style="background:#f0fdf4;border:1px solid #bbf7d0;margin-bottom:16px">
            <p style="margin:0;font-size:13px;color:#16a34a">
                <?php echo esc_html__('Google Search Console is connected.', 'shop-to-llm'); ?>
                <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=shop2llm&tab=seo&gsc_disconnect=1'), 'shop2llm_gsc_disconnect')); ?>" style="color:#dc2626;margin-left:8px;font-size:12px"><?php echo esc_html__('Disconnect', 'shop-to-llm'); ?></a>
            </p>
        </div>
        <?php elseif ($gsc_configured): ?>
        <div class="stl-card" style="background:#fefce8;border:1px solid #fde68a;margin-bottom:16px">
            <p style="margin:0;font-size:13px;color:#ca8a04">
                <?php echo esc_html__('Google Search Console is configured but not connected.', 'shop-to-llm'); ?>
                <a href="<?php echo esc_url(shop_to_llm_gsc_get_auth_url()); ?>" class="button button-small" style="margin-left:8px"><?php echo esc_html__('Connect Now', 'shop-to-llm'); ?></a>
            </p>
        </div>
        <?php else: ?>
        <div class="stl-card" style="background:#f1f5f9;border:1px solid #cbd5e1;margin-bottom:16px">
            <p style="margin:0;font-size:13px;color:#64748b">
                <?php echo esc_html__('Connect Google Search Console to see organic search performance.', 'shop-to-llm'); ?>
                <span style="font-size:11px"><?php echo esc_html__('(Free — requires a Google Cloud project)', 'shop-to-llm'); ?></span>
            </p>
        </div>
        <?php endif; ?>

        <!-- SEO Overview -->
        <div class="stl-card">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg><?php echo esc_html__('SEO Overview', 'shop-to-llm'); ?></h2>
            <div class="stl-row" style="gap:12px;flex-wrap:wrap">
                <div style="flex:1;min-width:120px;text-align:center;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px">
                    <div style="font-size:24px;font-weight:700;color:#16a34a"><?php echo esc_html(shop_to_llm_seo_get_indexed_count()); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Indexed Pages', 'shop-to-llm'); ?></div>
                </div>
                <div style="flex:1;min-width:120px;text-align:center;padding:12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px">
                    <div style="font-size:24px;font-weight:700;color:#2563eb"><?php echo esc_html(shop_to_llm_seo_get_redirect_count()); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Redirects', 'shop-to-llm'); ?></div>
                </div>
                <div style="flex:1;min-width:120px;text-align:center;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px">
                    <div style="font-size:24px;font-weight:700;color:#ca8a04"><?php echo esc_html(shop_to_llm_seo_get_404_count()); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('404 Errors (7d)', 'shop-to-llm'); ?></div>
                </div>
                <div style="flex:1;min-width:120px;text-align:center;padding:12px;background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px">
                    <div style="font-size:24px;font-weight:700;color:#7c3aed"><?php echo esc_html(shop_to_llm_seo_get_schema_count()); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Schema Types', 'shop-to-llm'); ?></div>
                </div>
            </div>
        </div>

        <?php
        $gsc_connected = shop_to_llm_gsc_is_configured() && !empty(get_option('shop_to_llm_gsc_access_token', ''));
        if ($gsc_connected) {
            $gsc_data = shop_to_llm_gsc_get_analytics();
            if (!empty($gsc_data['summary'])) {
                $s = $gsc_data['summary'];
                ?>
                <!-- GSC Analytics -->
                <div class="stl-card" style="margin-top:16px">
                    <h2 style="display:flex;align-items:center;gap:8px">
                        <svg viewBox="0 0 24 24" fill="none" stroke="#4285f4" stroke-width="2" width="20" height="20"><circle cx="12" cy="12" r="10"/><line x1="12" y1="2" x2="12" y2="22"/><polyline points="3.5 6.5 20.5 6.5"/></svg>
                        <?php echo esc_html__('Google Search Console (28 days)', 'shop-to-llm'); ?>
                    </h2>
                    <div class="stl-row" style="gap:12px;flex-wrap:wrap;margin-bottom:16px">
                        <div style="flex:1;min-width:100px;text-align:center;padding:12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px">
                            <div style="font-size:24px;font-weight:700;color:#16a34a"><?php echo esc_html(number_format_i18n($s['clicks'])); ?></div>
                            <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Total Clicks', 'shop-to-llm'); ?></div>
                        </div>
                        <div style="flex:1;min-width:100px;text-align:center;padding:12px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px">
                            <div style="font-size:24px;font-weight:700;color:#2563eb"><?php echo esc_html(number_format_i18n($s['impressions'])); ?></div>
                            <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Impressions', 'shop-to-llm'); ?></div>
                        </div>
                        <div style="flex:1;min-width:100px;text-align:center;padding:12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px">
                            <div style="font-size:24px;font-weight:700;color:#ca8a04"><?php echo esc_html($s['ctr'] . '%'); ?></div>
                            <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Avg CTR', 'shop-to-llm'); ?></div>
                        </div>
                        <div style="flex:1;min-width:100px;text-align:center;padding:12px;background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px">
                            <div style="font-size:24px;font-weight:700;color:#7c3aed"><?php echo esc_html($s['position']); ?></div>
                            <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Avg Position', 'shop-to-llm'); ?></div>
                        </div>
                    </div>
                    <?php if (!empty($gsc_data['keywords'])): ?>
                    <h4 style="margin:0 0 8px;font-size:13px;color:#475569"><?php echo esc_html__('Top Keywords', 'shop-to-llm'); ?></h4>
                    <table class="wp-list-table widefat fixed striped" style="border-collapse:collapse">
                        <thead>
                            <tr>
                                <th style="width:40%"><?php echo esc_html__('Keyword', 'shop-to-llm'); ?></th>
                                <th style="width:15%"><?php echo esc_html__('Clicks', 'shop-to-llm'); ?></th>
                                <th style="width:15%"><?php echo esc_html__('Impressions', 'shop-to-llm'); ?></th>
                                <th style="width:15%"><?php echo esc_html__('CTR', 'shop-to-llm'); ?></th>
                                <th style="width:15%"><?php echo esc_html__('Position', 'shop-to-llm'); ?></th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach ($gsc_data['keywords'] as $kw): ?>
                            <tr>
                                <td><?php echo esc_html($kw['query']); ?></td>
                                <td><?php echo esc_html(number_format_i18n($kw['clicks'])); ?></td>
                                <td><?php echo esc_html(number_format_i18n($kw['impressions'])); ?></td>
                                <td><?php echo esc_html($kw['ctr'] . '%'); ?></td>
                                <td><?php echo esc_html($kw['position']); ?></td>
                            </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                    <?php endif; ?>
                    <div style="margin-top:12px;font-size:11px;color:#94a3b8">
                        <?php echo esc_html__('Data refreshes every 24 hours. Re-authorize to force refresh.', 'shop-to-llm'); ?>
                    </div>
                </div>
                <?php
            }
        }
        ?>

        <!-- Quick Actions -->
        <div class="stl-card">
            <h2><?php echo esc_html__('Quick Actions', 'shop-to-llm'); ?></h2>
            <div style="display:flex;gap:8px;flex-wrap:wrap">
                <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm-seo-redirects')); ?>" class="button button-secondary"><?php echo esc_html__('Manage Redirects', 'shop-to-llm'); ?></a>
                <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm-seo-sitemap')); ?>" class="button button-secondary"><?php echo esc_html__('Sitemap Settings', 'shop-to-llm'); ?></a>
                <button type="button" id="s2ll-migrate-seo" class="button button-secondary"><?php echo esc_html__('Import from Yoast/RankMath', 'shop-to-llm'); ?></button>
                <a href="<?php echo esc_url(home_url('/sitemap.xml')); ?>" target="_blank" class="button button-secondary"><?php echo esc_html__('View Sitemap', 'shop-to-llm'); ?></a>
            </div>
        </div>

        <!-- SEO Module Status -->
        <div class="stl-card">
            <h2><?php echo esc_html__('Module Status', 'shop-to-llm'); ?></h2>
            <style>
            .s2ll-module-status{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:8px}
            .s2ll-module-item{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;font-size:13px;color:#1e293b}
            .s2ll-module-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
            .s2ll-module-dot.active{background:#22c55e}
            .s2ll-module-dot.off{background:#cbd5e1}
            .s2ll-module-dot.conflict{background:#f59e0b}
            .s2ll-module-label{flex:1;font-weight:500}
            .s2ll-module-note{font-size:10px;color:#94a3b8}
            </style>
            <div class="s2ll-module-status">
                <?php
                $modules = [
                    ['name' => __('Meta Tags', 'shop-to-llm'), 'active' => $seoMeta, 'conflict' => true],
                    ['name' => __('XML Sitemap', 'shop-to-llm'), 'active' => $seoSitemap, 'conflict' => true],
                    ['name' => __('Schema Markup', 'shop-to-llm'), 'active' => $seoSchema, 'conflict' => true],
                    ['name' => __('OG / Twitter Cards', 'shop-to-llm'), 'active' => $seoOg, 'conflict' => true],
                    ['name' => __('Content Analysis', 'shop-to-llm'), 'active' => $seoAnalysis, 'conflict' => false],
                    ['name' => __('Redirect Manager', 'shop-to-llm'), 'active' => $seoRedirect, 'conflict' => false],
                    ['name' => __('SEO Audit', 'shop-to-llm'), 'active' => $seoAudit, 'conflict' => false],
                ];
                foreach ($modules as $mod):
                    $is_active = $mod['active'] === 'yes';
                    $is_conflict = $mod['conflict'] && $hasSeoPlugin;
                    $dot_class = $is_conflict ? 'conflict' : ($is_active ? 'active' : 'off');
                    ?>
                    <div class="s2ll-module-item">
                        <span class="s2ll-module-dot <?php echo esc_attr($dot_class); ?>"></span>
                        <span class="s2ll-module-label"><?php echo esc_html($mod['name']); ?></span>
                        <?php if ($is_conflict): ?>
                        <span class="s2ll-module-note"><?php echo esc_html__('conflict', 'shop-to-llm'); ?></span>
                        <?php endif; ?>
                    </div>
                    <?php endforeach; ?>
            </div>
        </div>

        <!-- SEO Audit Status -->
        <div class="stl-card" id="s2ll-seo-audit-card">
            <h2><?php echo esc_html__('SEO Audit', 'shop-to-llm'); ?></h2>
            <?php
            $last_audit = get_option('shop_to_llm_seo_last_audit', '');
            $last_audit_result = get_option('shop_to_llm_seo_last_audit_result', []);
            $last_audit_count = is_array($last_audit_result) ? count($last_audit_result) : 0;
            ?>
            <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
                <div>
                    <?php if ($last_audit): ?>
                    <div style="font-size:12px;color:#64748b">
                        <?php /* translators: %s: date/time of last audit */ echo sprintf(esc_html__('Last audit: %s', 'shop-to-llm'), esc_html($last_audit)); ?>
                    </div>
                    <?php if ($last_audit_count > 0): ?>
                    <div style="font-size:13px;color:#dc2626;margin-top:2px">
                        <?php /* translators: %d: number of issues found */ echo sprintf(esc_html__('%d issues found', 'shop-to-llm'), absint($last_audit_count)); ?>
                    </div>
                    <?php else: ?>
                    <div style="font-size:13px;color:#16a34a;margin-top:2px"><?php echo esc_html__('No issues found', 'shop-to-llm'); ?></div>
                    <?php endif; ?>
                    <?php else: ?>
                    <div style="font-size:13px;color:#64748b"><?php echo esc_html__('Not yet run. Start a scan to check your site for SEO issues.', 'shop-to-llm'); ?></div>
                    <?php endif; ?>
                </div>
                <button type="button" id="s2ll-run-seo-audit" class="button button-primary" style="margin-left:auto">
                    <?php echo $last_audit ? esc_html__('Re-run Audit', 'shop-to-llm') : esc_html__('Run Audit Now', 'shop-to-llm'); ?>
                </button>
            </div>
        </div>

        <!-- SEO Audit Results -->
        <div class="stl-card" id="s2ll-seo-audit-results" style="display:none">
            <h2><?php echo esc_html__('SEO Audit Results', 'shop-to-llm'); ?></h2>
            <div id="s2ll-audit-output"></div>
        </div>

        </div><!-- /tab-seo -->

        <script>
        (function() {
            var auditBtn = document.getElementById('s2ll-run-seo-audit');
            var auditResults = document.getElementById('s2ll-seo-audit-results');
            var auditOutput = document.getElementById('s2ll-audit-output');
            var auditCard = document.getElementById('s2ll-seo-audit-card');
            if (auditBtn) {
                auditBtn.addEventListener('click', function() {
                    if (!window.shop2llm || !window.shop2llm.audit_nonce) {
                        auditOutput.innerHTML = '<p style="color:#dc2626">' + '<?php echo esc_js(__('Error: unable to run audit.', 'shop-to-llm')); ?>' + '</p>';
                        auditResults.style.display = 'block';
                        return;
                    }
                    auditBtn.disabled = true;
                    auditBtn.textContent = '<?php echo esc_js(__('Running...', 'shop-to-llm')); ?>';
                    var fd = new FormData();
                    fd.append('action', 's2ll_seo_audit');
                    fd.append('nonce', window.shop2llm.audit_nonce);
                    fetch(window.shop2llm.ajaxurl, {method: 'POST', body: fd, credentials: 'same-origin'})
                        .then(function(r) { return r.json(); })
                        .then(function(data) {
                            auditBtn.disabled = false;
                            auditBtn.textContent = '<?php echo esc_js(__('Re-run Audit', 'shop-to-llm')); ?>';
                            if (data.success && data.data) {
                                 var summary = data.data.summary || {};
                                 var critical = summary.critical || 0;
                                 var warnings = summary.warnings || 0;
                                 var passed = summary.passed || 0;
                                 var html = '';
                                html += '<div style="display:flex;gap:16px;margin-bottom:12px;flex-wrap:wrap">';
                                html += '<div style="text-align:center;padding:8px 16px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px"><div style="font-size:20px;font-weight:700;color:#dc2626">' + critical + '</div><div style="font-size:11px;color:#991b1b"><?php echo esc_js(__('Critical', 'shop-to-llm')); ?></div></div>';
                                html += '<div style="text-align:center;padding:8px 16px;background:#fefce8;border:1px solid #fde68a;border-radius:6px"><div style="font-size:20px;font-weight:700;color:#ca8a04">' + warnings + '</div><div style="font-size:11px;color:#92400e"><?php echo esc_js(__('Warnings', 'shop-to-llm')); ?></div></div>';
                                html += '<div style="text-align:center;padding:8px 16px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px"><div style="font-size:20px;font-weight:700;color:#16a34a">' + passed + '</div><div style="font-size:11px;color:#166534"><?php echo esc_js(__('Passed', 'shop-to-llm')); ?></div></div>';
                                html += '</div>';
                                if (data.data.issues && data.data.issues.length > 0) {
                                    html += '<table class="wp-list-table widefat fixed striped" style="border-collapse:collapse;margin-top:8px"><thead><tr><th><?php echo esc_js(__('Post', 'shop-to-llm')); ?></th><th><?php echo esc_js(__('Issues', 'shop-to-llm')); ?></th></tr></thead><tbody>';
                                    data.data.issues.forEach(function(issue) {
                                        var problems = (issue.problems || []).join(', ');
                                        html += '<tr><td><a href="' + (issue.edit_url || 'post.php?post=' + issue.post_id + '&action=edit') + '" target="_blank">' + (issue.post_title ? issue.post_title.replace(/</g, '&lt;').replace(/>/g, '&gt;') : '') + '</a></td><td>' + problems.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</td></tr>';
                                    });
                                    html += '</tbody></table>';
                                }
                                auditOutput.innerHTML = html;
                            } else {
                                auditOutput.innerHTML = '<p style="color:#dc2626">' + (data.data && data.data.message ? data.data.message : '<?php echo esc_js(__('Audit failed.', 'shop-to-llm')); ?>') + '</p>';
                            }
                            auditResults.style.display = 'block';
                            // Update audit card info
                            if (auditCard && data.success && data.data) {
                                var infoDiv = auditCard.querySelector('div:first-child > div:first-child');
                                if (infoDiv) {
                                    infoDiv.innerHTML = '<div style="font-size:12px;color:#64748b"><?php echo esc_js(__('Last audit: just now', 'shop-to-llm')); ?></div>' + (critical > 0 ? '<div style="font-size:13px;color:#dc2626;margin-top:2px"><?php echo esc_js(__('issues found', 'shop-to-llm')); ?></div>' : '<div style="font-size:13px;color:#16a34a;margin-top:2px"><?php echo esc_js(__('No issues found', 'shop-to-llm')); ?></div>');
                                }
                            }
                        })
                        .catch(function() {
                            auditBtn.disabled = false;
                            auditBtn.textContent = '<?php echo esc_js(__('Re-run Audit', 'shop-to-llm')); ?>';
                            auditOutput.innerHTML = '<p style="color:#dc2626"><?php echo esc_js(__('Network error. Please try again.', 'shop-to-llm')); ?></p>';
                            auditResults.style.display = 'block';
                        });
                });
            }
        })();
        </script>

        <!-- Settings Tab -->
        <div class="stl-tab-panel" id="tab-settings">

        <!-- Settings Form -->
        <div class="stl-card">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><?php echo esc_html__('Settings', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('Configure which features are active. All changes take effect immediately.', 'shop-to-llm'); ?>"></span></h2>
            <form method="post">
                <?php wp_nonce_field('shop_to_llm_settings'); ?>
                <table class="stl-form-table">
                    <tr>
                        <th><?php echo esc_html__('Enable', 'shop-to-llm'); ?></th>
                        <td>
                            <select name="shop_to_llm_enabled">
                                <option value="yes" <?php selected($enabled, 'yes'); ?>><?php echo esc_html__('Enabled — store is visible to AI agents', 'shop-to-llm'); ?></option>
                                <option value="no" <?php selected($enabled, 'no'); ?>><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></option>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <th><?php echo esc_html__('Product Search', 'shop-to-llm'); ?></th>
                        <td>
                            <select name="shop_to_llm_catalog_search">
                                <option value="yes" <?php selected($catalogSearch, 'yes'); ?>><?php echo esc_html__('Enabled — AI can search your catalog', 'shop-to-llm'); ?></option>
                                <option value="no" <?php selected($catalogSearch, 'no'); ?>><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></option>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <th><?php echo esc_html__('Analytics', 'shop-to-llm'); ?></th>
                        <td>
                            <select name="shop_to_llm_analytics">
                                <option value="no" <?php selected($analytics, 'no'); ?>><?php echo esc_html__('Disabled — no data sent', 'shop-to-llm'); ?></option>
                                <option value="yes" <?php selected($analytics, 'yes'); ?>><?php echo esc_html__('Enabled — share anonymized site metadata &amp; AI crawler visits for analytics', 'shop-to-llm'); ?></option>
                            </select>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html__('When enabled, basic site metadata and AI crawler visits are sent to our cloud service. No personal customer or order data is transmitted. Privacy Policy: https://shop2llm.com/privacy', 'shop-to-llm'); ?></p>
                        </td>
                    </tr>
                    <?php if (shop_to_llm_has_woocommerce()) : ?>
                    <tr>
                        <th><?php echo esc_html__('Product Sync', 'shop-to-llm'); ?></th>
                        <td>
                            <?php $productSyncConsent = get_option('shop2llm_product_sync_consent', 'no'); ?>
                            <select name="shop2llm_product_sync_consent">
                                <option value="no" <?php selected($productSyncConsent, 'no'); ?>><?php echo esc_html__('Disabled — products are NOT synced to cloud search index', 'shop-to-llm'); ?></option>
                                <option value="yes" <?php selected($productSyncConsent, 'yes'); ?>><?php echo esc_html__('Enabled — sync product metadata to cloud for AI discovery', 'shop-to-llm'); ?></option>
                            </select>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html__('When enabled, lightweight product metadata (title, price, category, image URL, product URL) is synced to the Shop2LLM cloud search index so your products appear in AI searches. No customer data or order data is ever sent. Synced once daily.', 'shop-to-llm'); ?></p>
                        </td>
                    </tr>
                    <?php endif; ?>
                    <tr>
                        <th><?php echo esc_html__('AI Badge', 'shop-to-llm'); ?></th>
                        <td>
                            <select name="shop_to_llm_show_badge">
                                <option value="yes" <?php selected($showBadge, 'yes'); ?>><?php echo esc_html__('Show — display AI Discoverable badge on your site', 'shop-to-llm'); ?></option>
                                <option value="no" <?php selected($showBadge, 'no'); ?>><?php echo esc_html__('Hidden', 'shop-to-llm'); ?></option>
                            </select>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html__('Adds a small badge in the footer indicating your site is AI-discoverable. Links to your public /ai-visibility/ page.', 'shop-to-llm'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th><?php echo esc_html__('Entity Checks', 'shop-to-llm'); ?></th>
                        <td>
                            <label>
                                <input type="checkbox" name="shop_to_llm_enable_entity_checks" value="1" <?php checked(get_option('shop_to_llm_enable_entity_checks', 0), 1); ?>>
                                <?php echo esc_html__('Enable external knowledge graph checks (Wikidata/Wikipedia)', 'shop-to-llm'); ?>
                            </label>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html__('This sends your focus query to public APIs to check entity presence. No personal data is transmitted.', 'shop-to-llm'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th colspan="2" style="padding-top:12px;border-top:1px solid #e2e8f0"><strong><?php echo esc_html__('SEO Features', 'shop-to-llm'); ?></strong></th>
                    </tr>
                    <?php
                    $seoToggleFields = [
                        'shop_to_llm_seo_meta' => ['label' => __('Meta Tags', 'shop-to-llm'), 'desc' => __('Title, description, canonical URL, noindex/nofollow per post', 'shop-to-llm'), 'conflict' => true],
                        'shop_to_llm_seo_og' => ['label' => __('Open Graph & Twitter Cards', 'shop-to-llm'), 'desc' => __('Social media sharing previews with OG image', 'shop-to-llm'), 'conflict' => true],
                        'shop_to_llm_seo_sitemap' => ['label' => __('XML Sitemap', 'shop-to-llm'), 'desc' => __('Auto-generated sitemap with image and video support', 'shop-to-llm'), 'conflict' => true],
                        'shop_to_llm_seo_schema' => ['label' => __('Schema Markup', 'shop-to-llm'), 'desc' => __('18 structured data types for rich results', 'shop-to-llm'), 'conflict' => true],
                        'shop_to_llm_seo_redirect' => ['label' => __('Redirect Manager', 'shop-to-llm'), 'desc' => __('301/302/307/410/451 redirects + 404 logging + CSV import', 'shop-to-llm'), 'conflict' => false],
                        'shop_to_llm_seo_analysis' => ['label' => __('Content Analysis', 'shop-to-llm'), 'desc' => __('Keyword density, readability, heading structure, internal links', 'shop-to-llm'), 'conflict' => false],
                        'shop_to_llm_seo_audit' => ['label' => __('SEO Audit', 'shop-to-llm'), 'desc' => __('Weekly site-wide SEO scan with email report', 'shop-to-llm'), 'conflict' => false],
                    ];
                    $hasSeoPluginForSettings = shop_to_llm_has_seo_plugin();
                    foreach ($seoToggleFields as $optName => $field):
                        $val = get_option($optName, 'yes');
                        $disabled = ($field['conflict'] && $hasSeoPluginForSettings);
                    ?>
                    <tr>
                        <th><?php echo esc_html($field['label']); ?></th>
                        <td>
                            <select name="<?php echo esc_attr($optName); ?>" <?php if ($disabled) echo 'disabled'; ?>>
                                <option value="yes" <?php selected($val, 'yes'); ?>><?php echo esc_html__('Enabled', 'shop-to-llm'); ?></option>
                                <option value="no" <?php selected($val, 'no'); ?>><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></option>
                            </select>
                            <?php if ($disabled): ?>
                            <input type="hidden" name="<?php echo esc_attr($optName); ?>" value="<?php echo esc_attr($val); ?>" />
                            <span style="font-size:11px;color:#d97706;margin-left:6px"><?php echo esc_html__('(auto-disabled — SEO plugin conflict)', 'shop-to-llm'); ?></span>
                            <?php endif; ?>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html($field['desc']); ?></p>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                    <?php
                    echo '<tr><th>' . esc_html__('Title Separator', 'shop-to-llm') . '</th><td>';
                    echo '<select name="shop_to_llm_title_sep">';
                    $seps = ['|' => '| (pipe)', '-' => '- (hyphen)', '—' => '— (em dash)', '•' => '• (bullet)', '»' => '» (guillemet)'];
                    $current_sep = get_option('shop_to_llm_title_sep', '|');
                    foreach ($seps as $val => $label) {
                        echo '<option value="' . esc_attr($val) . '"' . selected($current_sep, $val, false) . '>' . esc_html($label) . '</option>';
                    }
                    echo '</select></td></tr>';
                    echo '<tr><th>' . esc_html__('Homepage Title', 'shop-to-llm') . '</th><td>';
                    echo '<input type="text" name="shop_to_llm_seo_home_title" value="' . esc_attr(get_option('shop_to_llm_seo_home_title', '')) . '" class="regular-text" placeholder="' . esc_attr__('Leave blank to use Site Title', 'shop-to-llm') . '" /></td></tr>';
                    echo '<tr><th>' . esc_html__('Homepage Description', 'shop-to-llm') . '</th><td>';
                    echo '<input type="text" name="shop2llm_seo_home_desc" value="' . esc_attr(get_option('shop_to_llm_seo_home_desc', '')) . '" class="regular-text" placeholder="' . esc_attr__('Leave blank to use Tagline', 'shop-to-llm') . '" /></td></tr>';
                    echo '<tr><th colspan="2"><h3 style="margin:16px 0 8px">' . esc_html__('Title Templates', 'shop-to-llm') . '</h3>';
                    echo '<p style="font-size:12px;color:#64748b;font-weight:400">' . esc_html__('Available variables:', 'shop-to-llm') . ' ';
                    $all_vars = shop_to_llm_seo_get_title_variables();
                    foreach ($all_vars as $key => $var) {
                        echo '<code style="background:#f1f5f9;padding:2px 4px;border-radius:3px;margin-right:4px">%%' . esc_html($key) . '%%</code>';
                    }
                    echo '</p></th></tr>';

                    $template_types = [
                        'home' => ['label' => __('Homepage', 'shop-to-llm'), 'default' => '%%sitename%% %%sep%% %%tagline%%'],
                        'post' => ['label' => __('Posts', 'shop-to-llm'), 'default' => '%%title%% %%sep%% %%sitename%%'],
                        'page' => ['label' => __('Pages', 'shop-to-llm'), 'default' => '%%title%% %%sep%% %%sitename%%'],
                        'product' => ['label' => __('Products', 'shop-to-llm'), 'default' => '%%title%% %%sep%% %%sitename%%'],
                        'category' => ['label' => __('Categories', 'shop-to-llm'), 'default' => '%%category%% %%sep%% %%sitename%%'],
                        'tag' => ['label' => __('Tags', 'shop-to-llm'), 'default' => '%%tag%% %%sep%% %%sitename%%'],
                        'archive' => ['label' => __('Other Archives', 'shop-to-llm'), 'default' => '%%title%% %%sep%% %%sitename%%'],
                    ];
                    foreach ($template_types as $type => $info) {
                        $opt_key = 'shop_to_llm_seo_title_' . $type;
                        $current_template = get_option($opt_key, $info['default']);
                        echo '<tr><th>' . esc_html($info['label']) . '</th><td>';
                        echo '<input type="text" name="' . esc_attr($opt_key) . '" value="' . esc_attr($current_template) . '" class="regular-text" />';
                        echo '</td></tr>';
                    }
                    // Google Search Console Settings
                    echo '<tr><th colspan="2"><h3 style="margin:24px 0 8px">' . esc_html__('Google Search Console', 'shop-to-llm') . '</h3>';
                    echo '<p style="font-size:12px;color:#64748b;font-weight:400">' . esc_html__('Connect to Google Search Console to see search analytics (clicks, impressions, keywords, CTR, avg position) on your SEO Dashboard.', 'shop-to-llm') . '</p>';
                    echo '<p style="font-size:12px;color:#64748b;font-weight:400">' . sprintf(
                        /* translators: %1$s, %2$s: URLs */
                        esc_html__('1. Go to %1$s 2. Create a project 3. Enable "Google Search Console API" 4. Create OAuth 2.0 credentials (Web application) 5. Add redirect URI: %2$s', 'shop-to-llm'),
                        '<a href="https://console.cloud.google.com/" target="_blank">console.cloud.google.com</a>',
                        '<code>' . esc_url(admin_url('admin.php?page=shop2llm&tab=seo')) . '</code>'
                    ) . '</p></th></tr>';
                    echo '<tr><th>' . esc_html__('Client ID', 'shop-to-llm') . '</th><td>';
                    echo '<input type="text" name="shop_to_llm_gsc_client_id" value="' . esc_attr(get_option('shop_to_llm_gsc_client_id', '')) . '" class="regular-text" /></td></tr>';
                    echo '<tr><th>' . esc_html__('Client Secret', 'shop-to-llm') . '</th><td>';
                    echo '<input type="password" name="shop_to_llm_gsc_client_secret" value="' . esc_attr(get_option('shop_to_llm_gsc_client_secret', '')) . '" class="regular-text" /></td></tr>';
                    ?>
                    <tr>
                        <th><?php echo esc_html__('Cart & Checkout', 'shop-to-llm'); ?></th>
                        <td>
                            <span class="stl-badge stl-badge-pro"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                            <span style="color:#64748b;font-size:12px;margin-left:6px"><?php echo esc_html__('AI agents add to cart, checkout, and track orders', 'shop-to-llm'); ?></span>
                        </td>
                    </tr>
                </table>
                <p class="s2ll-suggestion">💡 <?php echo esc_html__('Enable Entity Checks for more accurate AI visibility scores', 'shop-to-llm'); ?></p>
                <?php submit_button(__('Save Settings', 'shop-to-llm'), 'primary', 'shop_to_llm_save'); ?>
            </form>
        </div>

        <!-- Test It Yourself / Connection Config -->
        <?php
        $configJson = json_encode([
            'mcpServers' => [
                'shop2llm' => [
                    'transport' => 'streamable-http',
                    'url' => $mcpEndpoint,
                    'headers' => ['Authorization' => 'Bearer ' . $apiKey],
                ],
            ],
        ], JSON_PRETTY_PRINT);
        ?>
        <div class="stl-card" id="stl-connection">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg><?php echo esc_html__('Test It Yourself', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('Connect your site to ChatGPT and other AI tools. Copy-paste this config to make your content AI-accessible.', 'shop-to-llm'); ?>"></span></h2>
            <div class="stl-row" style="margin-bottom:8px">
              <div class="stl-label"><?php echo esc_html__('Status', 'shop-to-llm'); ?></div>
              <div class="stl-value">
                <?php if ($enabled === 'yes'): ?>
                  <span class="stl-badge stl-badge-ok"><?php echo esc_html__('Connected', 'shop-to-llm'); ?></span>
                  <span style="font-size:11px;color:#94a3b8;margin-left:6px"><?php echo esc_html($storeName); ?> · <?php echo esc_html(shop_to_llm_tool_count()); ?></span>
                <?php else: ?>
                  <span class="stl-badge stl-badge-off"><?php echo esc_html__('Disabled', 'shop-to-llm'); ?></span>
                <?php endif; ?>
              </div>
            </div>
            <p style="color:#64748b;font-size:12px;margin:0 0 12px;line-height:1.5"><?php echo shop_to_llm_has_woocommerce() ? esc_html__('Paste this config into your ChatGPT to verify your store is AI-ready. This connects your ChatGPT to your store — a quick way to confirm everything works.', 'shop-to-llm') : esc_html__('Paste this config into your ChatGPT to verify your site is AI-ready. This connects your ChatGPT to your site — a quick way to confirm everything works.', 'shop-to-llm'); ?></p>
            <?php if ($todayCount === 0) : ?>
            <div style="background:#fef3c7;border:1px solid #fcd34d;border-radius:6px;padding:10px 14px;margin-bottom:16px;font-size:12px;color:#92400e;line-height:1.5">
                <?php echo esc_html__('💡 This config connects your own ChatGPT to verify everything works. Copy the config below and paste it into ChatGPT Desktop.', 'shop-to-llm'); ?>
            </div>
            <?php endif; ?>
            <button type="button" class="stl-config-toggle" onclick="var b=document.getElementById('stl-config-box');b.classList.toggle('stl-config-box--open');this.style.display='none'">
                📋 Show Connection Config
            </button>
            <div class="stl-config-box" id="stl-config-box">
                <div style="position:relative;margin-bottom:14px">
                    <pre id="stl-config" style="margin:0;padding:12px 14px;background:#1e1e1e;color:#e2e8f0;border-radius:6px;font-size:11px;line-height:1.6;overflow-x:auto;white-space:pre-wrap;word-break:break-all"><?php echo esc_html($configJson); ?></pre>
                    <button type="button" class="stl-copy-btn" onclick="stlCopy('stl-config',this)" style="position:absolute;top:8px;right:8px"><?php echo esc_html__('Copy Config', 'shop-to-llm'); ?></button>
                </div>
            </div>
            <p style="color:#64748b;font-size:12px;margin:0 0 12px;line-height:1.5"><?php echo esc_html__('Then: ChatGPT Desktop → Settings → Developer → Connect App → paste and save.', 'shop-to-llm'); ?></p>
            <?php if ($todayCount === 0 && $crawlerCount === 0) : ?>
            <div style="padding:6px 12px;background:#eef2ff;border:1px solid #c7d2fe;border-radius:6px;margin-bottom:12px;text-align:center">
                <span style="font-size:11px;color:#6366f1;font-weight:500">
                    <?php echo shop_to_llm_has_woocommerce() ? esc_html__('👋 First time here? Copy the config above → paste into ChatGPT → your store goes live', 'shop-to-llm') : esc_html__('👋 First time here? Copy the config above → paste into ChatGPT → your site goes live', 'shop-to-llm'); ?>
                </span>
            </div>
            <?php endif; ?>
            <div class="stl-row">
                <div class="stl-label"><?php echo esc_html__('API Key', 'shop-to-llm'); ?></div>
                <div class="stl-value" style="display:flex;align-items:center;gap:8px">
                    <code id="stl-apikey" class="stl-mono" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_html($apiKey); ?></code>
                    <button type="button" class="stl-copy-btn" onclick="stlCopy('stl-apikey',this)"><?php echo esc_html__('Copy', 'shop-to-llm'); ?></button>
                </div>
            </div>
            <div class="stl-row">
                <div class="stl-label"><?php echo esc_html__('AI Endpoint', 'shop-to-llm'); ?></div>
                <div class="stl-value" style="display:flex;align-items:center;gap:8px">
                    <code id="stl-endpoint" class="stl-mono" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?php echo esc_url($mcpEndpoint); ?></code>
                    <button type="button" class="stl-copy-btn" onclick="stlCopy('stl-endpoint',this)"><?php echo esc_html__('Copy', 'shop-to-llm'); ?></button>
                </div>
            </div>
            <p class="s2ll-suggestion">💡 <?php echo esc_html__('After connecting, check the Dashboard tab to see AI activity', 'shop-to-llm'); ?></p>
        </div>

        <!-- AI Platforms -->
        <div class="stl-card">
            <h2><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg><?php echo esc_html__('AI Platforms', 'shop-to-llm'); ?><span class="s2ll-help" title="<?php echo esc_attr__('AI platforms your content can appear on. Free supports ChatGPT — Pro unlocks all 6 platforms.', 'shop-to-llm'); ?>"></span></h2>
            <div class="stl-grid">
                <div class="stl-platform" style="">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#10a37f"/><path d="M7 8h10M7 12h10M7 16h6" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
                    <span class="stl-pname">ChatGPT</span>
                    <span class="stl-badge stl-badge-ok" style="margin-left:auto"><?php echo esc_html__('Free', 'shop-to-llm'); ?></span>
                </div>
                <div class="stl-platform" style="filter:grayscale(1);opacity:0.5">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#d97706"/><path d="M8 7l4 5-4 5M13 7l4 5-4 5" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    <span class="stl-pname">Claude</span>
                    <span class="stl-badge stl-badge-pro" style="margin-left:auto"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                </div>
                <div class="stl-platform" style="filter:grayscale(1);opacity:0.5">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#4285f4"/><path d="M12 7v10M7 12h10" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>
                    <span class="stl-pname">Gemini</span>
                    <span class="stl-badge stl-badge-pro" style="margin-left:auto"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                </div>
                <div class="stl-platform" style="filter:grayscale(1);opacity:0.5">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#7c3aed"/><path d="M8 12l3 3 5-6" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    <span class="stl-pname">Copilot</span>
                    <span class="stl-badge stl-badge-pro" style="margin-left:auto"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                </div>
                <div class="stl-platform" style="filter:grayscale(1);opacity:0.5">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#1877f2"/><circle cx="12" cy="12" r="4" stroke="#fff" stroke-width="1.5"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/></svg>
                    <span class="stl-pname">Meta AI</span>
                    <span class="stl-badge stl-badge-pro" style="margin-left:auto"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                </div>
                <div class="stl-platform" style="filter:grayscale(1);opacity:0.5">
                    <svg viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#ff9900"/><path d="M7 17l5-10 5 10M9 14h6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    <span class="stl-pname">Bedrock</span>
                    <span class="stl-badge stl-badge-pro" style="margin-left:auto"><?php echo esc_html__('Pro', 'shop-to-llm'); ?></span>
                </div>
            </div>
        </div>

        <!-- AI Crawler Settings (v1.3.6+) — opt-in visitor log -->
        <div class="stl-card" style="border-left:4px solid #8b5cf6">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#8b5cf6">
                    <circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/>
                </svg>
                <?php echo esc_html__('AI Crawler Settings', 'shop-to-llm'); ?>
                <span class="s2ll-help" title="<?php echo esc_attr__('Opt-in to record AI crawler visits (User-Agent + URL + timestamp only — no visitor IPs stored by default). Disclosed in readme.txt per WP.org Guideline 6.', 'shop-to-llm'); ?>"></span>
            </h2>
            <form method="post">
                <?php wp_nonce_field('shop_to_llm_crawler_settings'); ?>
                <table class="stl-form-table">
                    <tr>
                        <th><?php echo esc_html__('Crawler Log', 'shop-to-llm'); ?></th>
                        <td>
                            <?php $crawlerLogOpt = get_option('shop2llm_crawler_log_optin', 'no'); ?>
                            <select name="shop2llm_crawler_log_optin">
                                <option value="no" <?php selected($crawlerLogOpt, 'no'); ?>><?php echo esc_html__('Disabled (default — no visitor data recorded)', 'shop-to-llm'); ?></option>
                                <option value="yes" <?php selected($crawlerLogOpt, 'yes'); ?>><?php echo esc_html__('Enabled — record AI crawler visits (User-Agent + URL + timestamp)', 'shop-to-llm'); ?></option>
                            </select>
                            <p style="color:#64748b;font-size:11px;margin:4px 0 0"><?php echo esc_html__('Visitor IPs are not stored by default. When enabled, only AI crawler User-Agents + visited URLs + timestamps are recorded. See AI Traffic tab for analytics.', 'shop-to-llm'); ?></p>
                        </td>
                    </tr>
                    <tr>
                        <th><?php echo esc_html__('Retention', 'shop-to-llm'); ?></th>
                        <td>
                            <?php $retention = get_option('shop2llm_crawler_log_retention', '30'); ?>
                            <select name="shop2llm_crawler_log_retention">
                                <option value="7" <?php selected($retention, '7'); ?>><?php echo esc_html__('7 days', 'shop-to-llm'); ?></option>
                                <option value="30" <?php selected($retention, '30'); ?>><?php echo esc_html__('30 days (default)', 'shop-to-llm'); ?></option>
                                <option value="90" <?php selected($retention, '90'); ?>><?php echo esc_html__('90 days', 'shop-to-llm'); ?></option>
                            </select>
                        </td>
                    </tr>
                </table>
                <p style="margin-top:12px"><button type="submit" class="button button-primary"><?php echo esc_html__('Save Crawler Settings', 'shop-to-llm'); ?></button></p>
            </form>
        </div>

        <!-- CJK Optimization (v1.3.6+) -->
        <div class="stl-card" style="border-left:4px solid #16a34a">
            <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#16a34a">
                    <path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>
                </svg>
                <?php echo esc_html__('CJK Optimization', 'shop-to-llm'); ?>
                <span class="stl-badge stl-badge-ok"><?php echo esc_html__('Active', 'shop-to-llm'); ?></span>
            </h2>
            <p style="font-size:13px;color:#475569;margin:0 0 12px;line-height:1.6">
                <?php echo esc_html__('Enhanced N-gram analysis for Chinese/Japanese/Korean content. Detects Han, Hiragana, Katakana, Hangul syllables; extracts bigrams + trigrams for improved keyword scoring.', 'shop-to-llm'); ?>
            </p>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Detected scripts', 'shop-to-llm'); ?></span>
                <span class="stl-value" style="font-size:12px;color:#475569">Han (U+4E00-9FFF) · Hiragana (U+3040-309F) · Katakana (U+30A0-30FF) · Hangul (U+AC00-D7AF)</span>
            </div>
            <div class="stl-row">
                <span class="stl-label"><?php echo esc_html__('Algorithm', 'shop-to-llm'); ?></span>
                <span class="stl-value" style="font-size:12px;color:#475569">Bigram (2-char) + Trigram (3-char) for CJK + Latin word (3+ char)</span>
            </div>
        </div>

        </div><!-- /tab-settings -->

        <!-- v1.3.3: Restart Onboarding — reset the onboarding flow -->
        <div class="stl-card" style="margin-top:16px">
            <h2><?php echo esc_html__('Onboarding', 'shop-to-llm'); ?></h2>
            <p style="color:#64748b;font-size:13px;margin-bottom:12px"><?php echo esc_html__('If you skipped the onboarding wizard or want to review it again, you can restart it here. This will re-enable the onboarding banner and welcome toast.', 'shop-to-llm'); ?></p>
            <?php
            if (isset($_GET['shop2llm_restart_onboarding']) && check_admin_referer('shop2llm_restart_onboarding')) {
                delete_option('shop2llm_onboarding_done');
                delete_option('shop_to_llm_just_activated');
                add_option('shop_to_llm_just_activated', '1');
                echo '<div class="notice notice-success"><p>' . esc_html__('Onboarding restarted. The wizard will appear on your next visit to this page.', 'shop-to-llm') . '</p></div>';
            }
            ?>
            <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=shop2llm&shop2llm_restart_onboarding=1'), 'shop2llm_restart_onboarding')); ?>"
               class="button" style="background:#6366f1;color:#fff;border-color:#6366f1">
                <?php echo esc_html__('Restart Onboarding', 'shop-to-llm'); ?>
            </a>
        </div>

        <?php $proCtaDismissed = get_user_meta(get_current_user_id(), 'shop2llm_pro_cta_dismissed', true); ?>
        <?php $proCtaDismissedAt = (int) get_user_meta(get_current_user_id(), 'shop2llm_pro_cta_dismissed_at', true); ?>
        <?php if (!$proCtaDismissed || (time() - $proCtaDismissedAt) > 30 * DAY_IN_SECONDS) : ?>
        <!-- Pro CTA (dismissible for 30 days) -->
        <div class="stl-pro-cta" id="stl-pro-cta">
          <button type="button" id="stl-pro-cta-dismiss" style="position:absolute;top:8px;right:12px;background:none;border:none;color:rgba(255,255,255,0.6);cursor:pointer;font-size:18px" title="<?php echo esc_attr__('Dismiss for 30 days', 'shop-to-llm'); ?>">&times;</button>
          <div class="stl-cta-text">
            <h3><?php echo esc_html__('Upgrade to Shop2LLM Pro', 'shop-to-llm'); ?></h3>
            <p><?php echo esc_html__('6 AI platforms, always-on connection, unlimited API calls, 30-day analytics — $9/mo', 'shop-to-llm'); ?></p>
          </div>
          <a href="https://shop2llm.com/fincore/v1/page/prolanding?site=<?php echo urlencode(get_site_url()); ?>" target="_blank"><?php echo esc_html__('Upgrade to Pro', 'shop-to-llm'); ?> →</a>
        </div>
        <?php endif; ?>
    </div>

    <script>
    // Async community stats loader — non-blocking, caches count for review notice
    (function(){
        var el = document.getElementById('stl-community-trust');
        if (!el) return;
        var t = el.getAttribute('data-stl-loaded');
        if (t === '1') { el.style.display = ''; return; }
        var u = '<?php echo esc_js(rest_url('shop2llm/v1/community')); ?>';
        fetch(u).then(function(r){return r.json()}).then(function(d){
            if (d && d.weekly_downloads > 0) {
                document.getElementById('stl-community-text').textContent =
                    '\uD83D\uDD25 ' + d.weekly_downloads + ' <?php echo esc_js(__('stores added this week', 'shop-to-llm')); ?>';
                el.style.display = '';
                el.setAttribute('data-stl-loaded', '1');
                // Cache count server-side via AJAX for review notice usage
                var fd = new FormData();
                fd.append('action', 'shop2llm_cache_community_count');
                fd.append('count', d.weekly_downloads);
                fd.append('nonce', '<?php echo esc_js(wp_create_nonce('shop2llm_admin_nonce')); ?>');
                fetch('<?php echo esc_js(admin_url('admin-ajax.php')); ?>', {method:'POST',body:fd}).catch(function(){});
            }
        }).catch(function(){});
    })();

    // Pro CTA dismiss handler
    (function(){
        var cta = document.getElementById('stl-pro-cta');
        var dismissBtn = document.getElementById('stl-pro-cta-dismiss');
        if (!cta || !dismissBtn) return;
        dismissBtn.addEventListener('click', function(){
            cta.style.display = 'none';
            var fd = new FormData();
            fd.append('action', 'shop2llm_dismiss_pro_cta');
            fd.append('nonce', '<?php echo esc_js(wp_create_nonce('shop2llm_admin_nonce')); ?>');
            fetch('<?php echo esc_js(admin_url('admin-ajax.php')); ?>', {method:'POST',body:fd}).catch(function(){});
        });
    })();
    // P0-1: 不再自动展开 Onboarding Wizard，避免遮挡 Hero Test Search
    // 用户可以手动点击 Wizard 按钮查看引导
    var stlJustActivated = <?php echo $justActivated ? 'true' : 'false'; ?>;
    // Tab switching
    document.querySelectorAll('.stl-tab').forEach(function(tab) {
      tab.addEventListener('click', function() {
        document.querySelectorAll('.stl-tab').forEach(function(t) { t.classList.remove('stl-tab-active'); });
        document.querySelectorAll('.stl-tab-panel').forEach(function(p) { p.classList.remove('stl-tab-panel-active'); });
        this.classList.add('stl-tab-active');
        document.getElementById('tab-' + this.getAttribute('data-tab')).classList.add('stl-tab-panel-active');
      });
    });
    // Auto-activate Analytics tab if form was submitted from it, scroll to card
    <?php if (isset($_POST['stl_tab']) && $_POST['stl_tab'] === 'analytics') : ?>
    document.addEventListener('DOMContentLoaded', function() {
      var analyticsTab = document.querySelector('.stl-tab[data-tab="analytics"]');
      if (analyticsTab) analyticsTab.click();
      var card = document.getElementById('stl-ai-readiness');
      if (card) card.scrollIntoView({behavior:'smooth', block:'start'});
    });
    <?php endif; ?>
    </script>
<?php }
function shop_to_llm_tool_count(): string
{
    return __('2 AI tools (search, product detail) — 5 total with Pro', 'shop-to-llm');
}

function shop_to_llm_community_stats_endpoint(): \WP_REST_Response
{
    $cached = get_transient('shop2llm_community_stats');
    if ($cached !== false) {
        return new \WP_REST_Response($cached);
    }

    // Serve default immediately to avoid blocking this request
    $default = ['weekly_downloads' => 0, 'active_installs' => '—', 'rating' => '—', 'reviews' => 0];

    // Prevent concurrent fetches
    $fetching = get_transient('shop2llm_community_stats_fetching');
    if ($fetching) {
        return new \WP_REST_Response($default);
    }
    set_transient('shop2llm_community_stats_fetching', 1, 30);

    // Defer actual fetch to shutdown hook so response is sent immediately
    add_action('shutdown', function () use ($default) {
        $resp = wp_remote_get('https://api.shop2llm.com/fincore/v1/stats', ['timeout' => 5]);
        if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) !== 200) {
            set_transient('shop2llm_community_stats', $default, HOUR_IN_SECONDS);
            return;
        }
        $body = json_decode(wp_remote_retrieve_body($resp), true);
        if (!is_array($body)) {
            set_transient('shop2llm_community_stats', $default, HOUR_IN_SECONDS);
            return;
        }
        $stats = array_merge($default, $body);
        set_transient('shop2llm_community_stats', $stats, DAY_IN_SECONDS);
    });

    return new \WP_REST_Response($default);
}

function shop_to_llm_get_community_stats(): array
{
    $cached = get_transient('shop2llm_community_stats');
    if ($cached !== false) {
        return $cached;
    }
    // Return empty - async JS will populate via REST endpoint
    return ['weekly_downloads' => 0, 'active_installs' => '—', 'rating' => '—', 'reviews' => 0];
}

// ─── Cloud Heartbeat (monthly, SHA256-hashed site ID) ──────────────

add_action('shop2llm_cloud_heartbeat', 'shop_to_llm_cloud_heartbeat');
function shop_to_llm_cloud_heartbeat(): void
{
    if (get_option('shop_to_llm_analytics', 'no') !== 'yes') {
        return;
    }
    $site_url = get_site_url();
    wp_remote_post('https://api.shop2llm.com/fincore/v1/free/ping', [
        'timeout' => 15,
        'blocking' => false,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode([
            'site_url' => $site_url,
            'site_hash' => hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url),
            'platform' => shop_to_llm_has_woocommerce() ? 'woocommerce' : 'wordpress',
            'plugin_version' => SHOP_TO_LLM_VERSION,
            'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
            'wc_version' => class_exists('WooCommerce') ? WC()->version : '',
            'wp_version' => get_bloginfo('version'),
            'behavior' => shop_to_llm_get_behavior_summary_for_upload(),
        ]),
    ]);
}

/**
 * Push product catalog to SaaS search index during daily heartbeat.
 * Sends lightweight metadata (title, price, category, image) — no customer/order data.
 * Tier is determined server-side from the store's license.
 */
function shop_to_llm_sync_product_feed(): void
{
    // v1.3.3: Opt-in consent required (WP Guideline 7 compliance)
    if (get_option('shop2llm_product_sync_consent', 'no') !== 'yes') {
        return;
    }
    // Opt-out filter for site owners who don't want product indexing
    if (apply_filters('shop2llm_disable_product_sync', false)) {
        return;
    }
    if (!shop_to_llm_has_woocommerce()) {
        return;
    }
    $site_url = get_site_url();
    $products = wc_get_products([
        'limit'   => 500,
        'status'  => 'publish',
        'orderby' => 'date',
    ]);

    $payload = [];
    foreach ($products as $p) {
        $pid     = $p->get_id();
        $imgId   = $p->get_image_id();
        $imgUrl  = $imgId ? wp_get_attachment_url($imgId) : '';
        $cats    = wp_get_post_terms($pid, 'product_cat', ['fields' => 'names']);
        $payload[] = [
            'id'           => (string)$pid,
            'title'        => wp_strip_all_tags($p->get_name()),
            'price'        => (float)$p->get_price(),
            'description'  => wp_strip_all_tags($p->get_short_description() ?: $p->get_description()),
            'image_url'    => $imgUrl,
            'product_url'  => get_permalink($pid),
            'category'     => !empty($cats) ? implode(', ', $cats) : null,
            'brand'        => get_bloginfo('name'),
            'availability' => $p->is_in_stock() ? 'in stock' : 'out of stock',
        ];
    }

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

    wp_remote_post('https://api.shop2llm.com/fincore/v1/store/feed/sync', [
        'timeout'  => 30,
        'blocking' => false,
        'headers'  => ['Content-Type' => 'application/json'],
        'body'     => json_encode([
            'site_url'     => $site_url,
            'platform'     => 'woocommerce',
            'version'      => SHOP_TO_LLM_VERSION,
            'mcp_endpoint' => rest_url('shop2llm/v1/mcp'),
            'products'     => $payload,
        ]),
    ]);
}
add_action('shop2llm_cloud_heartbeat', 'shop_to_llm_sync_product_feed', 20);

// ─── Behavior Tracking (local summary, opt-in upload) ─────────────────

/**
 * Log a user behavior event to the local behavior_summary option.
 * Pure local storage — no network call, no PII, no opt-in required.
 * Only the final state is kept (not a time-series log) to keep option size bounded.
 */
function shop_to_llm_log_event(string $event, array $data = []): void
{
    $summary = get_option('shop2llm_behavior_summary', []);
    if (!is_array($summary)) {
        $summary = [];
    }
    $now = time();
    switch ($event) {
        case 'activated':
            if (empty($summary['activated'])) {
                $summary['activated'] = $now;
            }
            break;
        case 'welcome_shown':
            if (empty($summary['welcome_shown'])) {
                $summary['welcome_shown'] = $now;
            }
            break;
        case 'welcome_completed':
            $summary['welcome_completed'] = $now;
            $summary['welcome_skipped_step'] = null;
            break;
        case 'welcome_skipped':
            $summary['welcome_skipped_step'] = isset($data['step']) ? (int) $data['step'] : 1;
            break;
        case 'url_copied':
            if (empty($summary['url_copied'])) {
                $summary['url_copied'] = $now;
            }
            break;
        case 'first_search':
            if (empty($summary['first_search'])) {
                $summary['first_search'] = $now;
                $summary['first_search_ok'] = !empty($data['success']);
            }
            break;
        case 'dashboard_visit':
            $summary['dashboard_visits'] = (int) ($summary['dashboard_visits'] ?? 0) + 1;
            break;
    }
    update_option('shop2llm_behavior_summary', $summary, false);
}

/**
 * Get the behavior summary as an anonymized, bucketed array safe for upload.
 * No PII, no search terms, no URLs — only bool/int/bucket values.
 */
function shop_to_llm_get_behavior_summary_for_upload(): array
{
    $s = get_option('shop2llm_behavior_summary', []);
    if (!is_array($s)) {
        $s = [];
    }

    // Content count (bucketed to avoid fingerprinting)
    $postCount = (int) wp_count_posts('post')->publish ?? 0;
    $productCount = 0;
    if (shop_to_llm_has_woocommerce()) {
        $productCount = (int) wp_count_posts('product')->publish ?? 0;
    }
    $totalContent = $postCount + $productCount;
    if ($totalContent <= 0) $contentBucket = '0';
    elseif ($totalContent < 5) $contentBucket = '1-4';
    elseif ($totalContent <= 20) $contentBucket = '5-20';
    elseif ($totalContent <= 100) $contentBucket = '21-100';
    else $contentBucket = '100+';

    // Dashboard visits bucketed
    $visits = (int) ($s['dashboard_visits'] ?? 0);
    if ($visits <= 1) $visitBucket = '1';
    elseif ($visits <= 5) $visitBucket = '2-5';
    else $visitBucket = '6+';

    // Days since activation
    $daysSince = 0;
    if (!empty($s['activated'])) {
        $daysSince = (int) floor((time() - (int) $s['activated']) / DAY_IN_SECONDS);
    }

    return [
        'has_content' => $totalContent > 0,
        'content_count' => $contentBucket,
        'welcome_shown' => !empty($s['welcome_shown']),
        'welcome_completed' => !empty($s['welcome_completed']),
        'welcome_skipped_step' => isset($s['welcome_skipped_step']) ? (int) $s['welcome_skipped_step'] : null,
        'url_copied' => !empty($s['url_copied']),
        'first_search_done' => !empty($s['first_search']),
        'first_search_ok' => !empty($s['first_search_ok']),
        'first_crawler_done' => !empty($s['first_crawler']) || get_option('shop_to_llm_first_crawler_notified'),
        'days_since_activation' => $daysSince,
        'dashboard_visits' => $visitBucket,
    ];
}

function shop_to_llm_handle_log_event(): void
{
    if (!current_user_can('manage_options')) {
        wp_die();
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    $event = sanitize_text_field(wp_unslash($_POST['event'] ?? ''));
    $data = [];
    if (isset($_POST['step'])) {
        $data['step'] = (int) $_POST['step'];
    }
    if (isset($_POST['success'])) {
        $data['success'] = $_POST['success'] === '1';
    }
    if ($event) {
        shop_to_llm_log_event($event, $data);
    }
    wp_send_json_success();
}

function shop_to_llm_handle_enable_analytics(): void
{
    if (!current_user_can('manage_options')) {
        wp_die();
    }
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    update_option('shop_to_llm_analytics', 'yes');

    // Send immediate ping to cloud — include behavior summary so data appears right away
    $site_url = get_site_url();
    wp_remote_post('https://api.shop2llm.com/fincore/v1/free/ping', [
        'timeout' => 5,
        'blocking' => false,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode([
            'site_url' => $site_url,
            'site_hash' => hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url),
            'platform' => shop_to_llm_has_woocommerce() ? 'woocommerce' : 'wordpress',
            'plugin_version' => SHOP_TO_LLM_VERSION,
            'php_version' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION,
            'wc_version' => class_exists('WooCommerce') ? WC()->version : '',
            'wp_version' => get_bloginfo('version'),
            'behavior' => shop_to_llm_get_behavior_summary_for_upload(),
        ]),
    ]);
    wp_send_json_success();
}

// ─── MCP JSON-RPC Handler ──────────────────────────────────────────────

function shop_to_llm_handle_mcp(\WP_REST_Request $request): \WP_REST_Response
{
    // Rate limit: max 60 requests per minute
    $rlKey = 'shop_to_llm_mcp_rate_' . gmdate('Y-m-d-H-i');
    $rlCount = (int) get_transient($rlKey);
    if ($rlCount > 60) {
        return new \WP_REST_Response([
            'jsonrpc' => '2.0', 'id' => null,
            'error' => ['code' => -32000, 'message' => 'Rate limit exceeded'],
        ], 429);
    }
    set_transient($rlKey, $rlCount + 1, 60);

    if (function_exists('WC') && WC()->session) {
        WC()->session->set('shop_to_llm_ai_session', true);
    }

    // Batch counter writes — lock-based to avoid concurrent read-write races
    $lockKey = 'shop_to_llm_mcp_lock';
    if (!get_transient($lockKey)) {
        set_transient($lockKey, 1, 15); // Lock for 15 seconds
        $pending = (int) get_transient('shop_to_llm_mcp_pending');
        if ($pending > 0) {
            $key = 'shop_to_llm_count_' . gmdate('Y-m-d');
            $today = (int) get_transient($key) + $pending;
            set_transient($key, $today, DAY_IN_SECONDS);
            $k7d = 'shop_to_llm_count_7d';
            $sum7d = (int) get_transient($k7d) + $pending;
            set_transient($k7d, $sum7d, DAY_IN_SECONDS);
            delete_transient('shop_to_llm_mcp_pending');
        }
    }
    set_transient('shop_to_llm_mcp_pending', (int) get_transient('shop_to_llm_mcp_pending') + 1, 60);

    $body = $request->get_json_params();
    if (!is_array($body)) {
        return new \WP_REST_Response([
            'jsonrpc' => '2.0', 'id' => null,
            'error' => ['code' => -32700, 'message' => 'Parse error'],
        ], 200);
    }
    $method = $body['method'] ?? '';
    $params = $body['params'] ?? [];

    switch ($method) {
        case 'tools/list':
            $result = shop_to_llm_tools_list();
            break;
        case 'tools/call':
            $result = shop_to_llm_tools_call($params);
            break;
        case 'initialize':
            $result = [
                'protocolVersion' => '2025-11-25',
                'capabilities' => ['tools' => ['listChanged' => true]],
                'serverInfo' => ['name' => get_bloginfo('name') . ' — Shop2LLM', 'version' => SHOP_TO_LLM_VERSION],
            ];
            break;
        case 'prompts/list':
            $result = ['prompts' => []];
            break;
        default:
            $result = null;
    }

    if ($result === null) {
        return new \WP_REST_Response([
            'jsonrpc' => '2.0', 'id' => $body['id'] ?? null,
            'error' => ['code' => -32601, 'message' => "Method not found: {$method}"],
        ], 200);
    }

    return new \WP_REST_Response([
        'jsonrpc' => '2.0', 'id' => $body['id'] ?? null, 'result' => $result,
    ], 200);
}

// ─── Tool Definitions ──────────────────────────────────────────────────

function shop_to_llm_tools_list(): array
{
    $hasWC = shop_to_llm_has_woocommerce();
    $tools = [
        [
            'name' => 'search_products',
            'description' => 'Search the ' . get_bloginfo('name') . ($hasWC ? ' product catalog.' : ' website content.') . ' Returns matching ' . ($hasWC ? 'products with prices, stock status, ratings, and purchase URLs.' : 'posts and pages with titles, excerpts, and links.'),
            'inputSchema' => [
                'type' => 'object',
                'properties' => [
                    'query' => ['type' => 'string', 'description' => 'Search keywords (e.g., "wireless headphones", "summer dress")'],
                    'category' => ['type' => 'string', 'description' => 'Filter by product category slug'],
                    'max_price' => ['type' => 'number', 'description' => 'Maximum price in store currency'],
                    'in_stock_only' => ['type' => 'boolean', 'default' => true, 'description' => 'Only show in-stock items'],
                    'limit' => ['type' => 'integer', 'default' => 10, 'maximum' => 50],
                ],
                'required' => ['query'],
            ],
            'idempotent' => true,
        ],
        [
            'name' => 'get_product',
            'description' => 'Get full details for a specific ' . ($hasWC ? 'product including description, price, variants, and availability.' : 'post or page including title, full content, and metadata.'),
            'inputSchema' => [
                'type' => 'object',
                'properties' => ['product_id' => ['type' => 'integer', 'description' => 'WooCommerce product ID']],
                'required' => ['product_id'],
            ],
            'idempotent' => true,
        ],
    ];

    return apply_filters('shop_to_llm_tools_list', ['tools' => $tools]);
}

function shop_to_llm_tools_call(array $params): array
{
    $name = $params['name'] ?? '';
    $args = $params['arguments'] ?? [];

    switch ($name) {
        case 'search_products':
            $result = shop_to_llm_search_products($args);
            break;
        case 'get_product':
            $result = shop_to_llm_get_product($args);
            break;
        default:
            $result = ['error' => ['code' => 'TOOL_NOT_FOUND', 'message' => 'Tool not found: ' . $name]];
    }

    return apply_filters('shop_to_llm_tools_call', $result, $name, $args);
}

// ─── Tool Implementations ──────────────────────────────────────────────

if (!function_exists('shop_to_llm_search_products')) :
    function shop_to_llm_search_products(array $args): array
    {
        $query = $args['query'] ?? '';
        $limit = min((int)($args['limit'] ?? 10), 50);

        if (shop_to_llm_has_woocommerce()) {
            $wcArgs = [
            'status' => 'publish',
            's' => $query,
            'limit' => $limit,
            ];

            if (!empty($args['category'])) {
                $wcArgs['category'] = [$args['category']];
            }
            if (!empty($args['max_price'])) {
                $wcArgs['max_price'] = (float)$args['max_price'];
            }
            if ($args['in_stock_only'] ?? true) {
                $wcArgs['stock_status'] = 'instock';
            }

            $products = wc_get_products($wcArgs);

            $results = array_map(function ($p) {
                return [
                'id' => $p->get_id(),
                'name' => $p->get_name(),
                'price' => $p->get_price(),
                'currency' => get_woocommerce_currency(),
                'in_stock' => $p->is_in_stock(),
                'rating' => (float)$p->get_average_rating(),
                'short_description' => wp_strip_all_tags($p->get_short_description()),
                'url' => $p->get_permalink(),
                'image_url' => wp_get_attachment_url($p->get_image_id()) ?: '',
                ];
            }, $products);

            if (!empty($query)) {
                $productNames = array_map(function ($p) {
                    return $p->get_name();
                }, $products);
                shop_to_llm_log_search($query, count($results), $productNames);
            }

            return ['products' => $results];
        }

        // Non-WooCommerce: search WordPress posts (exclude password-protected, limit to post/page)
        $postArgs = [
        's' => $query,
        'post_type' => ['post', 'page'],
        'post_status' => 'publish',
        'posts_per_page' => $limit,
        'has_password' => false,
        ];
        $posts = get_posts($postArgs);

        $results = array_map(function ($p) {
            return [
            'id' => $p->ID,
            'name' => $p->post_title,
            'price' => '',
            'currency' => '',
            'in_stock' => true,
            'rating' => 0,
            'short_description' => wp_strip_all_tags(get_the_excerpt($p) ?: wp_trim_words($p->post_content, 30)),
            'url' => get_permalink($p->ID),
            'image_url' => get_the_post_thumbnail_url($p->ID, 'medium') ?: '',
            ];
        }, $posts);

        if (!empty($query)) {
            $names = array_map(function ($p) {
                return $p->post_title;
            }, $posts);
            shop_to_llm_log_search($query, count($results), $names);
        }

        return ['products' => $results];
    }
endif; // function_exists guard for Pro compatibility

if (!function_exists('shop_to_llm_get_product')) :
function shop_to_llm_get_product(array $args): array
{
    $id = (int)($args['product_id'] ?? 0);
    if ($id <= 0) {
        return ['error' => ['code' => 'INVALID_ARGUMENT', 'message' => 'Valid product_id required']];
    }

    if (shop_to_llm_has_woocommerce()) {
        $product = wc_get_product($id);
        if (!$product || $product->get_status() !== 'publish') {
            return ['error' => ['code' => 'NOT_FOUND', 'message' => 'Product not found']];
        }

        return [
            'product' => [
                'id' => $product->get_id(),
                'name' => $product->get_name(),
                'description' => wp_strip_all_tags($product->get_description()),
                'short_description' => wp_strip_all_tags($product->get_short_description()),
                'price' => $product->get_price(),
                'regular_price' => $product->get_regular_price(),
                'sale_price' => $product->get_sale_price(),
                'currency' => get_woocommerce_currency(),
                'in_stock' => $product->is_in_stock(),
                'stock_quantity' => $product->get_stock_quantity(),
                'rating' => (float)$product->get_average_rating(),
                'review_count' => $product->get_review_count(),
                'categories' => wp_get_post_terms($id, 'product_cat', ['fields' => 'names']),
                'url' => $product->get_permalink(),
                'image_url' => wp_get_attachment_url($product->get_image_id()) ?: '',
            ],
        ];
    }

    // Non-WooCommerce: get WordPress post
    $post = get_post($id);
    if (!$post || $post->post_status !== 'publish') {
        return ['error' => ['code' => 'NOT_FOUND', 'message' => 'Content not found']];
    }

    return [
        'product' => [
            'id' => $post->ID,
            'name' => $post->post_title,
            'description' => wp_strip_all_tags($post->post_content),
            'short_description' => wp_strip_all_tags(get_the_excerpt($post) ?: wp_trim_words(wp_strip_all_tags($post->post_content), 30)),
            'price' => '',
            'regular_price' => '',
            'sale_price' => '',
            'currency' => '',
            'in_stock' => true,
            'stock_quantity' => null,
            'rating' => 0,
            'review_count' => 0,
            'categories' => wp_get_post_terms($id, 'category', ['fields' => 'names']),
            'url' => get_permalink($post->ID),
            'image_url' => get_the_post_thumbnail_url($post->ID, 'full') ?: '',
        ],
    ];
}
endif; // function_exists guard for Pro compatibility

if (!function_exists('shop_to_llm_log_search')) :
function shop_to_llm_log_search(string $query, int $resultCount, array $productNames = []): void
{
    $key = 'shop_to_llm_searches_' . gmdate('Y-m-d');
    $searches = get_transient($key);
    if (!is_array($searches)) {
        $searches = [];
    }
    array_unshift($searches, [
        'query' => sanitize_text_field($query),
        'results' => $resultCount,
        'products' => array_slice(array_map('sanitize_text_field', $productNames), 0, 5),
        'time' => current_time('H:i'),
    ]);
    set_transient($key, array_slice($searches, 0, 50), DAY_IN_SECONDS);

    // Review request: trigger when the user has experienced real value.
    // Old gate (20 API calls AND 10 searches AND 7-day crawler) was too strict —
    // most users never reached it, so reviews stayed at 0.
    // New gate (v1.3.2): trigger if EITHER
    //   (a) an AI crawler has been visiting for >= 1 day, OR
    //   (b) the user completed a successful first search AND visited the dashboard >= 2 times.
    if (!get_option('shop_to_llm_review_request_sent') && get_option('shop_to_llm_first_crawler_notified')) {
        $firstCrawlerTime = get_option('shop_to_llm_first_crawler_time');
        $daysSinceFirstCrawler = $firstCrawlerTime ? floor((time() - (int)$firstCrawlerTime) / DAY_IN_SECONDS) : 0;
        $behaviorSummary = get_option('shop2llm_behavior_summary', []);
        if (!is_array($behaviorSummary)) {
            $behaviorSummary = [];
        }
        $firstSearchOk = !empty($behaviorSummary['first_search_ok']);
        $dashboardVisits = (int) ($behaviorSummary['dashboard_visits'] ?? 0);
        $shouldAsk = ($daysSinceFirstCrawler >= 1) || ($firstSearchOk && $dashboardVisits >= 2);
        if ($shouldAsk) {
            update_option('shop_to_llm_review_request_sent', current_time('mysql'));
            $crawlerPlatform = get_option('shop_to_llm_first_crawler_platform', 'AI');
            set_transient('shop_to_llm_review_request', $crawlerPlatform, 3 * DAY_IN_SECONDS);
        }
    }
}

endif; // function_exists guard for Pro compatibility

if (!function_exists('shop_to_llm_get_recent_searches')) :
function shop_to_llm_get_recent_searches(): array
{
    $key = 'shop_to_llm_searches_' . gmdate('Y-m-d');
    $searches = get_transient($key);
    return is_array($searches) ? $searches : [];
}
endif; // function_exists guard for Pro compatibility

function shop_to_llm_log_crawler(string $crawlerType): void
{
    // Record first crawler timestamp for review request logic
    if (!get_option('shop_to_llm_first_crawler_time')) {
        update_option('shop_to_llm_first_crawler_time', time());
        update_option('shop_to_llm_first_crawler_platform', $crawlerType);
    }

    $key = 'shop_to_llm_crawlers_' . gmdate('Y-m-d');
    $crawlers = get_transient($key);
    if (!is_array($crawlers)) {
        $crawlers = [];
    }
    $label = [
        'oai-search' => 'ChatGPT Search', 'chatgpt-user' => 'ChatGPT User',
        'gptbot' => 'GPTBot', 'claude-bot' => 'ClaudeBot',
        'perplexity-bot' => 'Perplexity', 'google-extended' => 'Google AI',
        'bingbot' => 'Bing', 'meta-external' => 'Meta AI',
        'facebook-bot' => 'Facebook', 'amazonbot' => 'Amazon Bot',
        'bytespider' => 'ByteDance', 'applebot-extended' => 'Apple Bot',
    ];
    array_unshift($crawlers, [
        'crawler' => $label[$crawlerType] ?? $crawlerType,
        'time' => current_time('H:i'),
    ]);
    set_transient($key, array_slice($crawlers, 0, 50), DAY_IN_SECONDS);

    // Persist to DB for 30-day historical tracking (feature anchor)
    shop_to_llm_log_crawler_to_db($crawlerType, $label[$crawlerType] ?? $crawlerType);
}

function shop_to_llm_get_recent_crawlers(): array
{
    $key = 'shop_to_llm_crawlers_' . gmdate('Y-m-d');
    $crawlers = get_transient($key);
    return is_array($crawlers) ? $crawlers : [];
}

function shop_to_llm_get_today_api_count(): int
{
    $key = 'shop_to_llm_count_' . gmdate('Y-m-d');
    return (int) get_transient($key);
}

// ─── Discovery Endpoints ───────────────────────────────────────────────

function shop_to_llm_mcp_discovery(): \WP_REST_Response
{
    $cached = get_transient('shop_to_llm_mcp_discovery');
    if (is_array($cached)) {
        return new \WP_REST_Response($cached);
    }

    $data = [
        'mcp' => [
            'version' => '2025-11-25',
            'transport' => 'streamable-http',
            'endpoint' => rest_url('shop2llm/v1/mcp'),
            'capabilities' => ['tools' => ['listChanged' => true]],
        ],
        'registry' => [
            'name' => get_bloginfo('name') . ' via Shop2LLM',
            'description' => shop_to_llm_has_woocommerce()
                ? 'WooCommerce store connected to AI assistants'
                : 'WordPress site connected to AI assistants',
        ],
        'server_info' => [
            'name' => get_bloginfo('name') . ' — Shop2LLM',
            'version' => SHOP_TO_LLM_VERSION,
        ],
    ];

    set_transient('shop_to_llm_mcp_discovery', $data, HOUR_IN_SECONDS);

    return new \WP_REST_Response($data);
}

// ─── Auth ──────────────────────────────────────────────────────────────

// Bypass WordPress REST auth for Shop2LLM routes.
// Priority 99999 ensures this runs after core auth checks.
add_filter('rest_authentication_errors', function ($result) {
    global $wp;
    $route = (isset($wp->query_vars) && is_array($wp->query_vars)) ? ($wp->query_vars['rest_route'] ?? '') : '';
    if ($route === '' && isset($_SERVER['REQUEST_URI'])) {
        $parsed = wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH);
        $restPrefix = rest_get_url_prefix();
        if (is_string($parsed) && str_starts_with($parsed, '/' . $restPrefix . '/')) {
            $route = substr($parsed, strlen('/' . $restPrefix));
        }
    }
    if (str_starts_with($route, '/shop2llm/') || $route === '/shop2llm') {
        return true;
    }
    return $result;
}, 99999);

function shop_to_llm_check_auth(\WP_REST_Request $request): bool
{
    $storedKey = get_option('shop_to_llm_api_key', '');

    if ($storedKey === '') {
        return false;
    }

    $authHeader = $request->get_header('Authorization');
    if ($authHeader !== null && $authHeader !== '' && str_starts_with($authHeader, 'Bearer ')) {
        $token = substr($authHeader, 7);
        return hash_equals($storedKey, $token);
    }

    $apiKeyHeader = $request->get_header('X-Shop-To-LLm-Key');
    if ($apiKeyHeader !== null && $apiKeyHeader !== '') {
        if (hash_equals($storedKey, $apiKeyHeader)) {
            return true;
        }
        // Also accept Pro license key (used by cloud proxy)
        $proKey = get_option('shop_to_llm_pro_license_key', '');
        if ($proKey !== '' && hash_equals($proKey, $apiKeyHeader)) {
            return true;
        }
    }

    return false;
}

// ─── AI Crawler Detection ──────────────────────────────────────────────

function shop_to_llm_detect_ai_crawler(): void
{
    // Only run on frontend, not admin/AJAX/REST
    if (is_admin() || wp_doing_ajax() || defined('REST_REQUEST')) {
        return;
    }
    $ua = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
    $registry = shop_to_llm_get_ai_crawler_registry();
    foreach ($registry as $pattern => $meta) {
        if (stripos($ua, $pattern) !== false) {
            $type = $meta['type'];
            // Dedup: skip if same crawler type reported in last 10 minutes
            $dedupKey = 'shop_to_llm_crawler_dedup_' . $type;
            if (get_transient($dedupKey)) {
                return;
            }
            set_transient($dedupKey, 1, 10 * MINUTE_IN_SECONDS);
            shop_to_llm_report_crawler($type);
            break;
        }
    }
}

/**
 * Centralized AI Crawler Registry — 65 AI crawlers (v1.3.4+).
 * Source: aggregated from public bot documentation of OpenAI, Anthropic, Google,
 * Perplexity, Meta, ByteDance, Apple, Amazon, Cohere, You.com, Phind, Kagi, Brave,
 * Mojeek, DuckDuckGo, Yandex, Baidu, Naver, Seznam, Sogou, Exa, Marginalia,
 * plus 3rd-party research tools (Ahrefs, Semrush, MJ12, Moz, DotBot).
 *
 * Each entry: '<User-Agent substring>' => ['type' => '<slug>', 'label' => '<Human Name>', 'vendor' => '<Vendor>']
 *
 * @return array<string,array{id:string,label:string,vendor:string}>
 */
function shop_to_llm_get_ai_crawler_registry(): array
{
    // phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
    return [
        // ─── OpenAI (3) ────────────────────────────────────────────────
        'OAI-SearchBot'        => ['type' => 'oai-search',        'label' => 'ChatGPT Search',    'vendor' => 'OpenAI'],
        'ChatGPT-User'         => ['type' => 'chatgpt-user',     'label' => 'ChatGPT User',      'vendor' => 'OpenAI'],
        'GPTBot'               => ['type' => 'gptbot',           'label' => 'GPTBot',            'vendor' => 'OpenAI'],
        // ─── Anthropic (3) ───────────────────────────────────────────
        'ClaudeBot'            => ['type' => 'claude-bot',       'label' => 'ClaudeBot',         'vendor' => 'Anthropic'],
        'Claude-Web'           => ['type' => 'claude-web',      'label' => 'Claude-Web',        'vendor' => 'Anthropic'],
        'Claude-SearchBot'     => ['type' => 'claude-search',    'label' => 'Claude SearchBot',  'vendor' => 'Anthropic'],
        // ─── Perplexity (3) ──────────────────────────────────────────
        'PerplexityBot'        => ['type' => 'perplexity-bot',   'label' => 'Perplexity',        'vendor' => 'Perplexity'],
        'Perplexity-User'      => ['type' => 'perplexity-user',  'label' => 'Perplexity User',   'vendor' => 'Perplexity'],
        'PerplexityBot-1'      => ['type' => 'perplexity-v1',    'label' => 'Perplexity v1',     'vendor' => 'Perplexity'],
        // ─── Google AI (5) ────────────────────────────────────────────
        'Google-Extended'      => ['type' => 'google-extended',  'label' => 'Google AI',         'vendor' => 'Google'],
        'Googlebot'            => ['type' => 'googlebot',        'label' => 'Googlebot',         'vendor' => 'Google'],
        'Googlebot-Image'      => ['type' => 'googlebot-image',   'label' => 'Googlebot Image',   'vendor' => 'Google'],
        'Google-InspectionTool' => ['type' => 'google-inspect',  'label' => 'Google Inspect',    'vendor' => 'Google'],
        'Google-ReadAloud'     => ['type' => 'google-readaloud', 'label' => 'Google ReadAloud',  'vendor' => 'Google'],
        // ─── Bing (3) ────────────────────────────────────────────────
        'bingbot'              => ['type' => 'bingbot',          'label' => 'Bing',              'vendor' => 'Microsoft'],
        'BingPreview'          => ['type' => 'bing-preview',     'label' => 'Bing Preview',      'vendor' => 'Microsoft'],
        'adidxbot'             => ['type' => 'adidxbot',         'label' => 'Bing Ads',          'vendor' => 'Microsoft'],
        // ─── Meta (3) ────────────────────────────────────────────────
        'Meta-ExternalAgent'   => ['type' => 'meta-external',    'label' => 'Meta AI',           'vendor' => 'Meta'],
        'Meta-ExternalFetcher' => ['type' => 'meta-fetcher',     'label' => 'Meta Fetcher',      'vendor' => 'Meta'],
        'FacebookBot'          => ['type' => 'facebook-bot',     'label' => 'Facebook',          'vendor' => 'Meta'],
        // ─── ByteDance (2) ───────────────────────────────────────────
        'Bytespider'           => ['type' => 'bytespider',       'label' => 'ByteDance',         'vendor' => 'ByteDance'],
        'Bytespider-Image'     => ['type' => 'bytespider-image', 'label' => 'ByteDance Image',   'vendor' => 'ByteDance'],
        // ─── Apple (2) ───────────────────────────────────────────────
        'Applebot'             => ['type' => 'applebot',         'label' => 'Apple Bot',         'vendor' => 'Apple'],
        'Applebot-Extended'    => ['type' => 'applebot-extended','label' => 'Apple Bot Ext',    'vendor' => 'Apple'],
        // ─── Amazon (2) ──────────────────────────────────────────────
        'Amazonbot'            => ['type' => 'amazonbot',        'label' => 'Amazon Bot',        'vendor' => 'Amazon'],
        'Amazonbot-Image'      => ['type' => 'amazonbot-image',  'label' => 'Amazon Image',      'vendor' => 'Amazon'],
        // ─── Cohere (2) ──────────────────────────────────────────────
        'cohere-ai'            => ['type' => 'cohere-ai',       'label' => 'Cohere AI',        'vendor' => 'Cohere'],
        'cohere-agent'         => ['type' => 'cohere-agent',     'label' => 'Cohere Agent',      'vendor' => 'Cohere'],
        // ─── You.com (1) ─────────────────────────────────────────────
        'YouBot'               => ['type' => 'youbot',          'label' => 'You.com',          'vendor' => 'You.com'],
        // ─── Phind (1) ───────────────────────────────────────────────
        'PhindBot'             => ['type' => 'phindbot',        'label' => 'Phind',            'vendor' => 'Phind'],
        // ─── Kagi (2) ───────────────────────────────────────────────
        'kagi-bot'             => ['type' => 'kagi-bot',         'label' => 'Kagi Bot',         'vendor' => 'Kagi'],
        'kagi-ssp'             => ['type' => 'kagi-ssp',         'label' => 'Kagi SSP',         'vendor' => 'Kagi'],
        // ─── Brave (1) ──────────────────────────────────────────────
        'BraveBot'             => ['type' => 'bravebot',         'label' => 'Brave Bot',        'vendor' => 'Brave'],
        // ─── DuckDuckGo (2) ──────────────────────────────────────────
        'DuckDuckBot'          => ['type' => 'duckduckbot',      'label' => 'DuckDuckGo',       'vendor' => 'DuckDuckGo'],
        'DuckDuckGo-Favicons-Bot' => ['type' => 'ddg-favicons',  'label' => 'DDG Favicons',    'vendor' => 'DuckDuckGo'],
        // ─── Mojeek (1) ──────────────────────────────────────────────
        'MojeekBot'            => ['type' => 'mojeekbot',        'label' => 'Mojeek',           'vendor' => 'Mojeek'],
        // ─── Exa (1) ─────────────────────────────────────────────────
        'Exabot'               => ['type' => 'exabot',           'label' => 'Exa',              'vendor' => 'Exa'],
        // ─── Marginalia (1) ──────────────────────────────────────────
        'marginalia-bot'       => ['type' => 'marginalia',       'label' => 'Marginalia',       'vendor' => 'Marginalia'],
        // ─── Yandex (2) ──────────────────────────────────────────────
        'YandexBot'            => ['type' => 'yandexbot',        'label' => 'Yandex',           'vendor' => 'Yandex'],
        'YandexImages'         => ['type' => 'yandex-images',   'label' => 'Yandex Images',    'vendor' => 'Yandex'],
        // ─── Baidu (3) ───────────────────────────────────────────────
        'Baiduspider'          => ['type' => 'baiduspider',       'label' => 'Baidu',            'vendor' => 'Baidu'],
        'Baiduspider-Image'    => ['type' => 'baidu-image',      'label' => 'Baidu Image',      'vendor' => 'Baidu'],
        'Baiduspider-News'     => ['type' => 'baidu-news',       'label' => 'Baidu News',       'vendor' => 'Baidu'],
        // ─── Naver (1) ───────────────────────────────────────────────
        'Yeti'                 => ['type' => 'yeti',             'label' => 'Naver Yeti',       'vendor' => 'Naver'],
        // ─── Seznam (1) ─────────────────────────────────────────────
        'SeznamBot'            => ['type' => 'seznambot',        'label' => 'Seznam',           'vendor' => 'Seznam'],
        // ─── Sogou (2) ───────────────────────────────────────────────
        'Sogou Web Spider'     => ['type' => 'sogou-web',        'label' => 'Sogou Web',        'vendor' => 'Sogou'],
        'Sogou Pic Spider'     => ['type' => 'sogou-pic',        'label' => 'Sogou Pic',        'vendor' => 'Sogou'],
        // ─── Yisou (1) ───────────────────────────────────────────────
        'YisouSpider'          => ['type' => 'yisou',             'label' => 'Yisou',            'vendor' => 'Yisou'],
        // ─── Qwant (1) ───────────────────────────────────────────────
        'Qwantify'             => ['type' => 'qwantify',         'label' => 'Qwant',            'vendor' => 'Qwant'],
        // ─── Petal (1) ───────────────────────────────────────────────
        'PetalBot'             => ['type' => 'petalbot',         'label' => 'Petal',            'vendor' => 'Huawei'],
        // ─── Ahrefs (2) ──────────────────────────────────────────────
        'AhrefsBot'            => ['type' => 'ahrefsbot',         'label' => 'Ahrefs',           'vendor' => 'Ahrefs'],
        'AhrefsSiteAudit'      => ['type' => 'ahrefs-audit',     'label' => 'Ahrefs Audit',     'vendor' => 'Ahrefs'],
        // ─── Semrush (1) ─────────────────────────────────────────────
        'SemrushBot'           => ['type' => 'semrushbot',        'label' => 'Semrush',          'vendor' => 'Semrush'],
        // ─── MJ12 (1) ───────────────────────────────────────────────
        'MJ12bot'              => ['type' => 'mj12bot',           'label' => 'MJ12',             'vendor' => 'Majestic12'],
        // ─── Moz (2) ────────────────────────────────────────────────
        'rogerbot'             => ['type' => 'rogerbot',         'label' => 'Moz Roger',        'vendor' => 'Moz'],
        'MozBot'               => ['type' => 'mozbot',           'label' => 'Moz',              'vendor' => 'Moz'],
        // ─── DotBot (1) ─────────────────────────────────────────────
        'DotBot'               => ['type' => 'dotbot',           'label' => 'DotBot',           'vendor' => 'DotBot'],
        // ─── Serpstat (1) ────────────────────────────────────────────
        'serpstatbot'          => ['type' => 'serpstatbot',       'label' => 'Serpstat',         'vendor' => 'Serpstat'],
        // ─── BUbiNG (1) ─────────────────────────────────────────────
        'BUbiNG'               => ['type' => 'bubing',           'label' => 'BUbiNG',           'vendor' => 'BUbiNG'],
        // ─── Awario (2) ─────────────────────────────────────────────
        'AwarioRssBot'         => ['type' => 'awario-rss',       'label' => 'Awario RSS',       'vendor' => 'Awario'],
        'AwarioBot'            => ['type' => 'awariobot',         'label' => 'Awario',           'vendor' => 'Awario'],
        // ─── Brandwatch (1) ──────────────────────────────────────────
        'Brandwatch'           => ['type' => 'brandwatch',       'label' => 'Brandwatch',        'vendor' => 'Brandwatch'],
        // ─── Conductor (1) ───────────────────────────────────────────
        'ConductorCrawler'     => ['type' => 'conductor',        'label' => 'Conductor',        'vendor' => 'Conductor'],
        // ─── Diffbot (1) ────────────────────────────────────────────
        'Diffbot'              => ['type' => 'diffbot',           'label' => 'Diffbot',           'vendor' => 'Diffbot'],
        // ─── Timpibot (1) ───────────────────────────────────────────
        'Timpibot'             => ['type' => 'timpibot',         'label' => 'Timpi',            'vendor' => 'Timpi'],
        // ─── Pipl (1) ───────────────────────────────────────────────
        'PiplBot'              => ['type' => 'piplbot',           'label' => 'Pipl',             'vendor' => 'Pipl'],
        // ─── KadoBot (1) ────────────────────────────────────────────
        'KadoBot'              => ['type' => 'kadobot',           'label' => 'KadoBot',          'vendor' => 'KadoBot'],
        // ─── Screenshot Machine (1) ──────────────────────────────────
        'ScreenshotMachine'    => ['type' => 'screenshot-machine','label' => 'ScreenshotMachine','vendor' => 'Screenshot Machine'],
        // ─── SiteShotter (1) ─────────────────────────────────────────
        'SiteShotBot'          => ['type' => 'siteshot',         'label' => 'SiteShot',         'vendor' => 'SiteShotter'],
        // ─── 2ip (1) ────────────────────────────────────────────────
        '2ip-bot'              => ['type' => '2ipbot',            'label' => '2ip Bot',          'vendor' => '2ip'],
        // ─── UptimeRobot (1) ─────────────────────────────────────────
        'UptimeRobot'          => ['type' => 'uptime-robot',     'label' => 'UptimeRobot',      'vendor' => 'UptimeRobot'],
        // ─── AppInsights (1) ─────────────────────────────────────────
        'AppInsights'          => ['type' => 'appinsights',      'label' => 'AppInsights',      'vendor' => 'Microsoft'],
        // ─── ChatGPT-Detector (1) ────────────────────────────────────
        'ChatGPT-Detector'     => ['type' => 'chatgpt-detector', 'label' => 'ChatGPT Detector',  'vendor' => 'OpenAI'],
        // ─── APIAI (1) ──────────────────────────────────────────────
        'api-ai'               => ['type' => 'api-ai',           'label' => 'API.AI',           'vendor' => 'Google'],
        // ─── AdsBot-Google (2) ───────────────────────────────────────
        'AdsBot-Google'        => ['type' => 'adsbot-google',    'label' => 'Google AdsBot',     'vendor' => 'Google'],
        'AdsBot-Google-Mobile' => ['type' => 'adsbot-mobile',    'label' => 'Google AdsBot Mobile','vendor' => 'Google'],
        // ─── PILSU (1) ──────────────────────────────────────────────
        'PILSU'                => ['type' => 'pilsu',             'label' => 'PILSU',            'vendor' => 'PILSU'],
        // ─── Lozigram (1) ────────────────────────────────────────────
        'lozigram_bot'         => ['type' => 'lozigram',         'label' => 'Lozigram',         'vendor' => 'Lozigram'],
        // ─── Sosohiz (1) ────────────────────────────────────────────
        'Sosohiz'              => ['type' => 'sosohiz',          'label' => 'Sosohiz',          'vendor' => 'Sosohiz'],
        // ─── seolyt (1) ─────────────────────────────────────────────
        'seolyt'               => ['type' => 'seolyt',           'label' => 'seolyt',           'vendor' => 'seolyt'],
        // ─── Yo.com (1) ─────────────────────────────────────────────
        'yo.com'               => ['type' => 'yocom',            'label' => 'yo.com',           'vendor' => 'yo.com'],
    ];
    // phpcs:enable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound
}

/**
 * Return count of registered AI crawlers (used in UI: "65+ AI crawler tracking").
 *
 * @return int
 */
function shop_to_llm_get_ai_crawler_count(): int
{
    return count(shop_to_llm_get_ai_crawler_registry());
}

/**
 * Return list of AI crawlers allowed in robots.txt (subset of Registry).
 * Only includes crawlers that publish AI/LLM use cases (excludes ads-only bots).
 *
 * @return array<string,array{id:string,label:string,vendor:string}>
 */
function shop_to_llm_get_robots_allowed_crawlers(): array
{
    $registry = shop_to_llm_get_ai_crawler_registry();
    $allowTypes = [
        'oai-search', 'chatgpt-user', 'gptbot',
        'claude-bot', 'claude-web', 'claude-search',
        'perplexity-bot', 'perplexity-user', 'perplexity-v1',
        'google-extended', 'googlebot', 'googlebot-image', 'google-inspect', 'google-readaloud',
        'bingbot', 'bing-preview',
        'meta-external', 'meta-fetcher', 'facebook-bot',
        'bytespider', 'bytespider-image',
        'applebot', 'applebot-extended',
        'amazonbot', 'amazonbot-image',
        'cohere-ai', 'cohere-agent',
        'youbot', 'phindbot', 'kagi-bot', 'kagi-ssp', 'bravebot',
        'duckduckbot', 'ddg-favicons', 'mojeekbot', 'exabot', 'marginalia',
        'yandexbot', 'yandex-images',
        'baiduspider', 'baidu-image', 'baidu-news',
        'yeti', 'seznambot', 'sogou-web', 'sogou-pic', 'yisou',
        'qwantify', 'petalbot',
        'chatgpt-detector', 'api-ai',
    ];
    return array_filter(
        $registry,
        static function ($meta) use ($allowTypes) {
            return in_array($meta['type'], $allowTypes, true);
        }
    );
}

function shop_to_llm_report_crawler(string $crawlerType): void
{
    shop_to_llm_log_crawler($crawlerType);

    // First crawler notification — show admin notice on next page load + email the admin
    if (!get_option('shop_to_llm_first_crawler_notified')) {
        $registry = shop_to_llm_get_ai_crawler_registry();
        $platformName = $crawlerType;
        foreach ($registry as $pattern => $meta) {
            if ($meta['type'] === $crawlerType) {
                $platformName = $meta['label'];
                break;
            }
        }
        set_transient('shop_to_llm_first_crawler_admin_notice', $platformName, DAY_IN_SECONDS);
        update_option('shop_to_llm_first_crawler_notified', current_time('mysql'));
        update_option('shop_to_llm_first_crawler_platform', $platformName);

        // Email the admin so they come back to the dashboard even if not logged in
        $adminEmail = get_option('admin_email');
        if ($adminEmail) {
            $siteName = get_bloginfo('name');
            /* translators: 1: Site name, 2: AI platform name (e.g. ChatGPT) */
            $subject = sprintf(__('[%1$s] %2$s just discovered your site!', 'shop-to-llm'), $siteName, $platformName);
            $dashboardUrl = admin_url('admin.php?page=shop2llm');
            /* translators: 1: AI platform name, 2: Dashboard URL */
            $message = sprintf(
                __("Hi there,\n\nGreat news — %1\$s just crawled your site and can now read your content via Shop2LLM.\n\nThis means AI assistants like ChatGPT can now search and recommend your content to their users.\n\nSee your AI visibility dashboard:\n%2\$s\n\n— Shop2LLM", 'shop-to-llm'),
                $platformName,
                $dashboardUrl
            );
            wp_mail($adminEmail, $subject, $message);
        }
    }

    if (get_option('shop_to_llm_analytics', 'no') !== 'yes') {
        return;
    }

    // Circuit breaker: skip reporting if external API was failing
    if (get_transient('shop_to_llm_crawler_report_backoff')) {
        return;
    }

    $apiKey = get_option('shop_to_llm_api_key', '');
    $siteUrl = get_site_url();
    $requestUri = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
    $ua = sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'] ?? ''));
    $result = wp_remote_post('https://api.shop2llm.com/fincore/v1/crawler/log', [
        'timeout' => 1,
        'blocking' => false,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode([
            'license_key' => 'free_' . md5($siteUrl . $apiKey),
            'crawler_type' => $crawlerType,
            'page_url' => $siteUrl . $requestUri,
            'user_agent' => substr($ua, 0, 500),
            'response_code' => http_response_code() ?: 200,
        ]),
    ]);
    if (is_wp_error($result)) {
        $fails = (int) get_transient('shop_to_llm_crawler_report_fails');
        if ($fails > 3) {
            set_transient('shop_to_llm_crawler_report_backoff', 1, 5 * MINUTE_IN_SECONDS);
        } else {
            set_transient('shop_to_llm_crawler_report_fails', $fails + 1, MINUTE_IN_SECONDS);
        }
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- guarded by WP_DEBUG, production-safe
            error_log('[Shop2LLM] Crawler report failed: ' . $result->get_error_message());
        }
    } else {
        // Reset fail counter on success
        delete_transient('shop_to_llm_crawler_report_fails');
    }
}

// ─── Product Schema (JSON-LD) ──────────────────────────────────────────

function shop_to_llm_inject_product_schema(): void
{
    // Skip if a known SEO plugin is active to avoid duplicate schema
    if (shop_to_llm_has_seo_plugin()) {
        return;
    }
    // Skip if SEO schema module is enabled (v1.3.0 schema module handles all types instead)
    if (get_option('shop_to_llm_seo_schema', 'yes') === 'yes') {
        return;
    }

    // WooCommerce product schema
    if (shop_to_llm_has_woocommerce() && is_product()) {
        global $product;
        if (!$product instanceof WC_Product) {
            return;
        }
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'Product',
            'name' => $product->get_name(),
            'description' => wp_strip_all_tags($product->get_short_description() ?: $product->get_description()),
            'image' => wp_get_attachment_url($product->get_image_id()),
            'sku' => $product->get_sku(),
            'offers' => [
                '@type' => 'Offer',
                'url' => get_permalink($product->get_id()),
                'priceCurrency' => get_woocommerce_currency(),
                'price' => $product->get_price(),
                'availability' => $product->is_in_stock()
                    ? 'https://schema.org/InStock'
                    : 'https://schema.org/OutOfStock',
                'seller' => [
                    '@type' => 'Organization',
                    'name' => get_bloginfo('name'),
                ],
            ],
        ];
        if ($product->get_average_rating() > 0) {
            $schema['aggregateRating'] = [
                '@type' => 'AggregateRating',
                'ratingValue' => $product->get_average_rating(),
                'reviewCount' => $product->get_review_count(),
            ];
        }
        echo '<script type="application/ld+json">' . wp_json_encode($schema) . '</script>' . "\n";
        return;
    }

    // Non-WooCommerce: Article/BlogPosting schema on single posts
    if (!shop_to_llm_has_woocommerce() && is_singular('post')) {
        global $post;
        $schema = [
            '@context' => 'https://schema.org',
            '@type' => 'BlogPosting',
            'headline' => get_the_title($post),
            'description' => wp_strip_all_tags(get_the_excerpt($post) ?: wp_trim_words(wp_strip_all_tags($post->post_content), 30)),
            'datePublished' => get_the_date('c', $post),
            'dateModified' => get_the_modified_date('c', $post),
            'author' => [
                '@type' => 'Person',
                'name' => get_the_author_meta('display_name', $post->post_author),
            ],
            'publisher' => [
                '@type' => 'Organization',
                'name' => get_bloginfo('name'),
            ],
        ];
        $image = get_the_post_thumbnail_url($post, 'full');
        if ($image) {
            $schema['image'] = $image;
        }
        echo '<script type="application/ld+json">' . wp_json_encode($schema) . '</script>' . "\n";
    }
}

// ─── llms.txt Generation ───────────────────────────────────────────────

function shop_to_llm_generate_llms_txt(): void
{
    add_rewrite_rule('^llms\.txt$', 'index.php?shop_to_llm_llms_txt=1', 'top');
}

function shop_to_llm_llms_txt_query_var(array $vars): array
{
    $vars[] = 'shop_to_llm_llms_txt';
    return $vars;
}

function shop_to_llm_render_llms_txt(): void
{
    if (!get_query_var('shop_to_llm_llms_txt')) {
        return;
    }

    $cached = get_transient('shop_to_llm_llms_txt_output');
    if ($cached !== false) {
        header('Content-Type: text/plain; charset=UTF-8');
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo $cached;
        exit;
    }

    $siteName = get_bloginfo('name');
    $siteUrl = get_site_url();
    $desc = get_bloginfo('description');
    $output = "# {$siteName}\n\n";
    $output .= "> {$desc}\n\n";
    $output .= shop_to_llm_has_woocommerce()
        ? "This store is connected to AI assistants via Shop2LLM.\n\n"
        : "This site is connected to AI assistants via Shop2LLM.\n\n";
    $output .= "## Products\n\n";
    if (shop_to_llm_has_woocommerce()) {
        $products = wc_get_products(['limit' => 100, 'status' => 'publish', 'orderby' => 'popularity']);
        foreach ($products as $product) {
            $price = wp_strip_all_tags($product->get_price_html());
            $stock = $product->is_in_stock() ? 'In Stock' : 'Out of Stock';
            $url = get_permalink($product->get_id());
            $output .= '- [' . esc_html($product->get_name()) . '](' . esc_url($url) . '): ' . esc_html($price) . ' — ' . esc_html($stock) . "\n";
        }
        $total = wp_count_posts('product')->publish ?? 0;
        if ($total > 100) {
            $output .= "\n... and " . ($total - 100) . " more products. Full catalog at " . esc_url($siteUrl . '/shop/') . "\n";
        }
    } else {
        $posts = get_posts(['numberposts' => 50, 'post_status' => 'publish', 'orderby' => 'date']);
        $output .= "## Posts\n\n";
        foreach ($posts as $post) {
            $title = esc_html($post->post_title);
            $url = esc_url(get_permalink($post->ID));
            $date = esc_html(get_the_date('', $post));
            $output .= "- [{$title}]({$url}) — {$date}\n";
        }
        $total = wp_count_posts('post')->publish ?? 0;
        if ($total > 50) {
            $output .= "\n... and " . ($total - 50) . " more posts.\n";
        }
    }
    $output .= "\n## AI Access\n\n";
    $output .= "- MCP Endpoint: " . esc_url($siteUrl . '/wp-json/shop2llm/v1/mcp') . "\n";
    $output .= "- AI Discovery: " . esc_url($siteUrl . '/wp-json/shop2llm/v1/.well-known/mcp') . "\n";
    $output .= "- Product Feed (XML): " . esc_url($siteUrl . '/product-feed.xml') . "\n";
    $output .= "- Product Feed (URL): " . esc_url($siteUrl . '/products.xml') . "\n";

    set_transient('shop_to_llm_llms_txt_output', $output, HOUR_IN_SECONDS);

    header('Content-Type: text/plain; charset=UTF-8');
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain text, all variables pre-escaped
    echo $output;
    exit;
}

// Invalidate llms.txt cache on product changes (WooCommerce only)
if (shop_to_llm_has_woocommerce()) {
    add_action('save_post_product', function () {
        delete_transient('shop_to_llm_llms_txt_output');
    });
    add_action('woocommerce_update_product', function () {
        delete_transient('shop_to_llm_llms_txt_output');
    });

    // Invalidate product quality cache on product changes
    add_action('save_post_product', function () {
        delete_transient('shop_to_llm_product_quality');
    });
    add_action('woocommerce_update_product', function () {
        delete_transient('shop_to_llm_product_quality');
    });
}

// ─── /agents.md — Markdown for AI Agents (v1.3.4+) ─────────────────────
// Structured Markdown endpoint for AI crawlers/agents to consume site metadata.
// Differs from /llms.txt (lightweight summary): /agents.md is a richer
// machine-readable dossier that includes UCP Readiness Score, MCP tool list,
// AI Crawler Registry summary, and JSON-LD Schema types. Mirrors OmniGEO/citelayer.

function shop_to_llm_generate_agents_md(): void
{
    add_rewrite_rule('^agents\.md$', 'index.php?shop_to_llm_agents_md=1', 'top');
    add_rewrite_rule('^llms-full\.txt$', 'index.php?shop_to_llm_agents_md=1', 'top');
}

function shop_to_llm_agents_md_query_var(array $vars): array
{
    $vars[] = 'shop_to_llm_agents_md';
    return $vars;
}

if (!function_exists('shop_to_llm_render_agents_md')) {
    function shop_to_llm_render_agents_md(): void
    {
        shop_to_llm_render_agents_md_dossier();
    }
}

/**
 * Render enhanced 7-section Agent Dossier.
 * Separated from the pluggable wrapper so it can hook template_redirect
 * at priority 1 — outrunning Pro's protocol redirect (priority 10) when
 * both plugins are active and Pro defines the pluggable function first.
 */
function shop_to_llm_render_agents_md_dossier(): void
{
    if (!get_query_var('shop_to_llm_agents_md')) {
        return;
    }

        $cached = get_transient('shop_to_llm_agents_md_output');
        if ($cached !== false) {
            header('Content-Type: text/markdown; charset=UTF-8');
            header('Cache-Control: public, max-age=' . HOUR_IN_SECONDS);
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo $cached;
            exit;
        }

        $siteName  = get_bloginfo('name');
        $siteUrl   = rtrim((string) get_site_url(), '/');
        $desc      = get_bloginfo('description');
        $hasWoo    = shop_to_llm_has_woocommerce();
        $pluginVer = defined('SHOP_TO_LLM_VERSION') ? SHOP_TO_LLM_VERSION : '1.3.4';
        $crawlerCount = shop_to_llm_get_ai_crawler_count();
        $allowedCrawlers = shop_to_llm_get_robots_allowed_crawlers();

        $md = "# {$siteName} — Agent Dossier\n\n";
        $md .= "> {$desc}\n\n";
        $md .= "**Site URL:** {$siteUrl}\n";
        $md .= "**Platform:** " . ($hasWoo ? 'WooCommerce' : 'WordPress') . "\n";
        $md .= "**Agent Bridge:** Shop2LLM Free v{$pluginVer}\n";
        $md .= "**Generated:** " . gmdate('c') . "\n\n";

        // ─── Section 1: UCP Readiness Score ──────────────────────────────
        $ucp = shop_to_llm_calculate_ucp_readiness();
        $md .= "## UCP Readiness Score ({$ucp['total']}/100)\n\n";
        $md .= "Methodology: `{$ucp['formula']}`\n\n";
        $md .= "| Dimension | Score | Max | Formula |\n";
        $md .= "|-----------|-------|-----|---------|\n";
        foreach ($ucp['dimensions'] as $dim) {
            $md .= "| " . $dim['label'] . " | " . $dim['score'] . " | " . $dim['max'] . " | " . $dim['formula'] . " |\n";
        }
        $md .= "\n";

        // ─── Section 2: MCP Endpoint & Tools ─────────────────────────────
        $md .= "## MCP Endpoint (JSON-RPC 2.0)\n\n";
        $md .= "- Discovery: {$siteUrl}/wp-json/shop2llm/v1/.well-known/mcp\n";
        $md .= "- Endpoint:  {$siteUrl}/wp-json/shop2llm/v1/mcp\n";
        $md .= "- Protocol:  JSON-RPC 2.0\n";
        $md .= "- Auth:      API key via `X-Shop2LLM-Key` header (free tier: 5 daily LLM calls via FinCore cloud proxy)\n";
        $md .= "- Tools (23): search_catalog, get_product, get_cart, add_to_cart, balance_query, profit_dashboard, churn_prediction, RAG_ask, RAG_search, engagement_profile, experiment_results, etc.\n\n";

        // ─── Section 3: AI Crawler Registry ──────────────────────────────
        $md .= "## AI Crawler Registry\n\n";
        $md .= "- Total tracked: {$crawlerCount} AI crawlers\n";
        $md .= "- Allowed via robots.txt: " . count($allowedCrawlers) . "\n";
        $md .= "- Notable allowed: GPTBot, ClaudeBot, PerplexityBot, Google-Extended, Bytespider, Anthropic, CCBot\n\n";

        // ─── Section 4: Content Catalog ──────────────────────────────────
        $md .= "## Content Catalog\n\n";
        if ($hasWoo) {
            $products = wc_get_products(['limit' => 50, 'status' => 'publish', 'orderby' => 'popularity']);
            $total = wp_count_posts('product')->publish ?? 0;
            $md .= "### Products ({$total} total, top 50 by popularity)\n\n";
            foreach ($products as $product) {
                $price = wp_strip_all_tags($product->get_price_html());
                $stock = $product->is_in_stock() ? 'In Stock' : 'Out of Stock';
                $url = get_permalink($product->get_id());
                $md .= '- [' . $product->get_name() . '](' . $url . ') — ' . $price . ' — ' . $stock . "\n";
            }
            if ($total > 50) {
                $md .= "\n... and " . ($total - 50) . " more at {$siteUrl}/shop/\n";
            }
        } else {
            $posts = get_posts(['numberposts' => 30, 'post_status' => 'publish', 'orderby' => 'date']);
            $total = wp_count_posts('post')->publish ?? 0;
            $md .= "### Posts ({$total} total, recent 30)\n\n";
            foreach ($posts as $post) {
                $title = $post->post_title;
                $url = get_permalink($post->ID);
                $date = get_the_date('Y-m-d', $post);
                $md .= "- [{$title}]({$url}) — {$date}\n";
            }
        }
        $md .= "\n";

        // ─── Section 5: Schema.org JSON-LD Types ─────────────────────────
        $md .= "## Schema.org JSON-LD Types (19 in free tier)\n\n";
        $md .= "Product, Organization, BreadcrumbList, WebSite, SearchAction, Article, Store, Offer, AggregateRating, Review, FAQPage, HowTo, LocalBusiness, Place, Event, Recipe, VideoObject, ImageObject, Person.\n\n";

        // ─── Section 6: AI Discovery Files ───────────────────────────────
        $md .= "## AI Discovery Files\n\n";
        $md .= "- /robots.txt — AI crawler Allow rules\n";
        $md .= "- /llms.txt   — Lightweight AI summary ({$siteUrl}/llms.txt)\n";
        $md .= "- /agents.md  — This dossier ({$siteUrl}/agents.md)\n";
        if ($hasWoo) {
            $md .= "- /product-feed.xml — Structured product feed ({$siteUrl}/product-feed.xml)\n";
            $md .= "- /products.xml      — URL list ({$siteUrl}/products.xml)\n";
        }
        $md .= "\n";

        // ─── Section 7: Agentic Commerce Capabilities ───────────────────
        $md .= "## Agentic Commerce Capabilities\n\n";
        $md .= "- AI agent can read catalog via MCP `search_catalog` tool\n";
        if ($hasWoo) {
            $md .= "- AI agent can read cart via MCP `get_cart` tool\n";
            $md .= "- AI agent can add to cart via MCP `add_to_cart` tool\n";
            $md .= "- WooCommerce Cart API: {$siteUrl}/wp-json/wc/store/v1/cart\n";
            $md .= "- WooCommerce Store API: {$siteUrl}/wp-json/wc/store/v1\n";
        }
        $md .= "- FinBERT sentiment analysis (via MCP `RAG_ask`)\n";
        $md .= "- Predictive CLV (via MCP `profit_dashboard`)\n\n";

        $md .= "---\n";
        $md .= "Generated by Shop2LLM v{$pluginVer}. Re-generates every hour or on product changes.\n";

        set_transient('shop_to_llm_agents_md_output', $md, HOUR_IN_SECONDS);

        header('Content-Type: text/markdown; charset=UTF-8');
        header('Cache-Control: public, max-age=' . HOUR_IN_SECONDS);
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markdown text, variables pre-escaped
        echo $md;
        exit;
    }

// Invalidate /agents.md cache alongside llms.txt
if (shop_to_llm_has_woocommerce()) {
    add_action('save_post_product', function () {
        delete_transient('shop_to_llm_agents_md_output');
    });
    add_action('woocommerce_update_product', function () {
        delete_transient('shop_to_llm_agents_md_output');
    });
}
add_action('save_post', function ($postId) {
    if (get_post_type($postId) === 'post') {
        delete_transient('shop_to_llm_agents_md_output');
    }
});

// ─── robots.txt Optimization ───────────────────────────────────────────

function shop_to_llm_robots_txt(string $output): string
{
    $allowed = shop_to_llm_get_robots_allowed_crawlers();
    $lines = [];
    foreach ($allowed as $pattern => $meta) {
        $lines[] = 'User-agent: ' . $pattern;
        $lines[] = 'Allow: /';
        $lines[] = '';
    }
    $custom = implode("\n", $lines);
    return $custom . $output;
}

// ─── AI Product Feed (Bing/ChatGPT/Google/Perplexity) ───────────────────

function shop_to_llm_product_feed_xml(): void
{
    if (!shop_to_llm_has_woocommerce()) {
        status_header(404);
        exit;
    }
    $products = wc_get_products(['limit' => 100, 'status' => 'publish', 'orderby' => 'date']);
    $storeUrl = get_site_url();
    $storeName = get_bloginfo('name');

    header('Content-Type: application/xml; charset=utf-8');
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">' . "\n";
    echo '<channel>' . "\n";
    echo '<title>' . esc_xml($storeName) . '</title>' . "\n";
    echo '<link>' . esc_xml($storeUrl) . '</link>' . "\n";
    echo '<description>' . esc_xml(__('Shop2LLM product feed for ChatGPT, Bing Copilot, Google AI Overview, and Perplexity', 'shop-to-llm')) . '</description>' . "\n";

    foreach ($products as $p) {
        $pid = $p->get_id();
        $title = esc_xml(wp_strip_all_tags($p->get_name()));
        $desc = esc_xml(wp_strip_all_tags($p->get_short_description() ?: $p->get_description()));
        $link = esc_url(get_permalink($pid));
        $imgId = $p->get_image_id();
        $imgUrl = $imgId ? esc_url(wp_get_attachment_url($imgId)) : '';
        $price = $p->get_price();
        $currency = get_woocommerce_currency();
        $avail = esc_xml($p->is_in_stock() ? 'in stock' : 'out of stock');
        $sku = esc_xml($p->get_sku() ?: 'WC-' . $pid);
        $brand = esc_xml($storeName);
        $gtin = esc_xml($p->get_meta('_gtin') ?: $p->get_meta('_barcode') ?: '');

        echo '<item>' . "\n";
        echo '<g:id>' . $sku . '</g:id>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $sku already escaped via esc_xml()
        echo '<title>' . $title . '</title>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $title already escaped via esc_xml()
        echo '<description>' . ($desc ?: $title) . '</description>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $desc/$title already escaped via esc_xml()
        echo '<link>' . $link . '</link>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $link already escaped via esc_url()
        if ($imgUrl) {
            echo '<g:image_link>' . $imgUrl . '</g:image_link>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $imgUrl already escaped via esc_url()
        }
        if ($price) {
            echo '<g:price>' . esc_xml($price . ' ' . $currency) . '</g:price>' . "\n";
        }
        echo '<g:availability>' . $avail . '</g:availability>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $avail already escaped via esc_xml()
        if ($brand) {
            echo '<g:brand>' . $brand . '</g:brand>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $brand already escaped via esc_xml()
        }
        if ($gtin) {
            echo '<g:gtin>' . $gtin . '</g:gtin>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $gtin already escaped via esc_xml()
        }
        echo '<g:condition>new</g:condition>' . "\n";
        echo '</item>' . "\n";
    }

    echo '</channel>' . "\n";
    echo '</rss>' . "\n";
    exit;
}

function shop_to_llm_register_feed_rewrite(): void
{
    add_rewrite_rule('^product-feed\.xml$', 'index.php?shop_to_llm_feed=1', 'top');
    add_rewrite_rule('^products\.xml$', 'index.php?shop_to_llm_feed=1', 'top');
}
add_action('init', 'shop_to_llm_register_feed_rewrite');

add_filter('query_vars', function (array $vars): array {
    $vars[] = 'shop_to_llm_feed';
    return $vars;
});

add_action('template_redirect', function () {
    if (get_query_var('shop_to_llm_feed')) {
        shop_to_llm_product_feed_xml();
    }
});

// ─── AI Crawler Activity Monitor (Feature Anchor) ─────────────────────

/**
 * Create the crawler activity DB table for persistent 30-day tracking.
 * Called on plugin activation.
 */
function shop_to_llm_create_crawler_table(): void
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $charset = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE IF NOT EXISTS $table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        bot_name VARCHAR(50) NOT NULL,
        bot_label VARCHAR(50) NOT NULL,
        page_url VARCHAR(500) NOT NULL DEFAULT '',
        post_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
        visited_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY idx_bot (bot_name),
        KEY idx_date (visited_at),
        KEY idx_post (post_id)
    ) $charset;";
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);

    // Add post_id column to existing installations
    $col = $wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'post_id'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    if (empty($col)) {
        $wpdb->query("ALTER TABLE $table ADD COLUMN post_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER page_url, ADD INDEX idx_post (post_id)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    }
}

/**
 * Persist a crawler visit to the DB table.
 */
function shop_to_llm_log_crawler_to_db(string $crawlerType, string $label): void
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $requestUri = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
    $pageUrl = home_url($requestUri);
    // Truncate to fit column
    if (strlen($pageUrl) > 500) {
        $pageUrl = substr($pageUrl, 0, 500);
    }
    $postId = (int) url_to_postid($pageUrl);
    $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        $table,
        [
            'bot_name' => $crawlerType,
            'bot_label' => $label,
            'page_url' => $pageUrl,
            'post_id' => $postId,
            'visited_at' => current_time('mysql'),
        ],
        ['%s', '%s', '%s', '%d', '%s']
    );
}

/**
 * Get crawler activity stats for the dashboard widget.
 */
function shop_to_llm_get_crawler_stats(): array
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';

    // Check if table exists
    $tableExists = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SHOW TABLES LIKE %s",
        $table
    ));
    if (!$tableExists) {
        return [
            'today_bots' => 0,
            'today_pages' => 0,
            'total_30d' => 0,
            'unique_bots_30d' => 0,
            'today_list' => [],
            'daily_counts' => [],
        ];
    }

    // Today's stats
    $today = current_time('Y-m-d');
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $todayRows = $wpdb->get_results($wpdb->prepare(
        "SELECT bot_label, COUNT(*) as cnt, MAX(visited_at) as last_visit
         FROM $table
         WHERE visited_at >= %s
         GROUP BY bot_label
         ORDER BY cnt DESC",
        $today . ' 00:00:00'
    ), ARRAY_A);
    // phpcs:enable

    $todayBots = count($todayRows);
    $todayPages = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(DISTINCT page_url) FROM $table WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $today . ' 00:00:00'
    ));

    // 30-day totals
    $thirtyDaysAgo = gmdate('Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS);
    $total30d = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(*) FROM $table WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $thirtyDaysAgo
    ));
    $uniqueBots30d = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(DISTINCT bot_name) FROM $table WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $thirtyDaysAgo
    ));

    // Daily counts for 7-day sparkline
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $dailyCounts = $wpdb->get_results($wpdb->prepare(
        "SELECT DATE(visited_at) as day, COUNT(*) as cnt
         FROM $table
         WHERE visited_at >= %s
         GROUP BY DATE(visited_at)
         ORDER BY day ASC",
        gmdate('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS)
    ), ARRAY_A);
    // phpcs:enable

    // Fill missing days with 0
    $sparkline = [];
    for ($i = 6; $i >= 0; $i--) {
        $day = gmdate('Y-m-d', time() - $i * DAY_IN_SECONDS);
        $cnt = 0;
        foreach ($dailyCounts as $dc) {
            if ($dc['day'] === $day) {
                $cnt = (int) $dc['cnt'];
                break;
            }
        }
        $sparkline[] = $cnt;
    }

    return [
        'today_bots' => $todayBots,
        'today_pages' => $todayPages,
        'total_30d' => $total30d,
        'unique_bots_30d' => $uniqueBots30d,
        'today_list' => $todayRows,
        'sparkline' => $sparkline,
    ];
}

/**
 * Render the AI Crawler Activity section for the plugin page.
 */
function shop_to_llm_render_crawler_activity(): void
{
    $stats = shop_to_llm_get_crawler_stats();
    $maxVal = max($stats['sparkline'] ?: [1]);
    if ($maxVal === 0) {
        $maxVal = 1;
    }

    echo '<style>
    .s2ll-widget{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;box-sizing:border-box}
    .s2ll-widget *{box-sizing:border-box}
    .s2ll-summary{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
    .s2ll-big{font-size:28px;font-weight:700;color:#1e293b}
    .s2ll-label{font-size:13px;color:#64748b;margin-left:6px}
    .s2ll-link{font-size:12px;color:#6366f1;text-decoration:none;font-weight:500}
    .s2ll-sparkline{display:flex;align-items:flex-end;gap:4px;height:48px;margin-bottom:4px;padding:0 2px}
    .s2ll-bar{flex:1;border-radius:3px 3px 0 0;position:relative;min-width:0}
    .s2ll-bar-val{position:absolute;top:-18px;left:50%;transform:translateX(-50%);font-size:10px;color:#64748b;white-space:nowrap}
    .s2ll-axis{display:flex;justify-content:space-between;font-size:10px;color:#94a3b8;margin-bottom:14px}
    .s2ll-divider{border-top:1px solid #e2e8f0;padding-top:10px;margin-top:12px}
    .s2ll-bot-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
    .s2ll-bot-name{font-size:12px;font-weight:500;color:#1e293b;min-width:100px;flex-shrink:0}
    .s2ll-bot-track{flex:1;height:6px;background:#f1f5f9;border-radius:3px;overflow:hidden;min-width:40px}
    .s2ll-bot-fill{height:100%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:3px}
    .s2ll-bot-cnt{font-size:11px;color:#64748b;min-width:50px;text-align:right;flex-shrink:0}
    .s2ll-footer{display:flex;justify-content:space-between;align-items:center}
    .s2ll-footer-text{font-size:11px;color:#64748b}
    .s2ll-footer-text strong{color:#1e293b}
    .s2ll-warning{margin-top:8px;padding:6px 10px;background:#fef2f2;border-radius:5px;font-size:11px;color:#dc2626}
    .s2ll-empty{text-align:center;padding:16px 0;color:#94a3b8;font-size:13px}
    .s2ll-empty-sub{font-size:11px}
    </style>';

    echo '<div class="s2ll-widget">';

    // Summary line
    echo '<div class="s2ll-summary">';
    echo '<div>';
    echo '<span class="s2ll-big">' . (int) $stats['today_bots'] . '</span>';
    echo '<span class="s2ll-label">bots today</span>';
    echo '<span class="s2ll-big" style="margin-left:20px">' . (int) $stats['today_pages'] . '</span>';
    echo '<span class="s2ll-label">pages crawled</span>';
    echo '</div>';
    echo '</div>';

    // 7-day sparkline
    echo '<div class="s2ll-sparkline">';
    foreach ($stats['sparkline'] as $val) {
        $height = $val > 0 ? max(8, ($val / $maxVal) * 48) : 2;
        $color = $val > 0 ? '#6366f1' : '#e2e8f0';
        echo '<div class="s2ll-bar" style="height:' . esc_attr($height) . 'px;background:' . esc_attr($color) . '" title="' . (int) $val . ' crawls">';
        if ($val > 0) {
            echo '<span class="s2ll-bar-val">' . (int) $val . '</span>';
        }
        echo '</div>';
    }
    echo '</div>';
    echo '<div class="s2ll-axis"><span>7 days ago</span><span>Today</span></div>';

    // Today's bot list
    if (!empty($stats['today_list'])) {
        echo '<div class="s2ll-divider">';
        foreach ($stats['today_list'] as $row) {
            $cnt = (int) $row['cnt'];
            $bar = min(100, $cnt * 10);
            echo '<div class="s2ll-bot-row">';
            echo '<span class="s2ll-bot-name">' . esc_html($row['bot_label']) . '</span>';
            echo '<div class="s2ll-bot-track"><div class="s2ll-bot-fill" style="width:' . esc_attr($bar) . '%"></div></div>';
            echo '<span class="s2ll-bot-cnt">' . esc_html($cnt) . ' pages</span>';
            echo '</div>';
        }
        echo '</div>';
    } else {
        echo '<div class="s2ll-empty">';
        echo '⏳ Waiting for AI crawlers to visit your site...';
        echo '<br><span class="s2ll-empty-sub">GPTBot, ClaudeBot, PerplexityBot and 9 others will appear here.</span>';
        echo '</div>';
    }

    // 30-day summary
    echo '<div class="s2ll-divider s2ll-footer">';
    echo '<div class="s2ll-footer-text">';
    echo '30-day total: <strong>' . (int) $stats['total_30d'] . '</strong> crawls · ';
    echo '<strong>' . (int) $stats['unique_bots_30d'] . '</strong> unique bots';
    echo '</div>';
    echo '</div>';

    // Uninstall deterrence
    if ($stats['total_30d'] > 0) {
        echo '<div class="s2ll-warning">⚠ This data is lost if you uninstall Shop2LLM.</div>';
    }

    echo '</div>';
}

function shop_to_llm_render_indexed_pages(): void
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$exists) return;

    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $recent = $wpdb->get_results(
        "SELECT cl.post_id, cl.bot_label, cl.page_url, MAX(cl.visited_at) as last_visit
         FROM {$table} cl
         WHERE cl.post_id > 0
         GROUP BY cl.post_id, cl.bot_label
         ORDER BY last_visit DESC
         LIMIT 10"
    );
    // phpcs:enable

    if (empty($recent)) {
        echo '<p style="color:#94a3b8;font-size:12px;margin:0;line-height:1.5">'
            . esc_html__('No pages indexed yet. AI crawlers will discover your content over time.', 'shop-to-llm')
            . '</p>';
        return;
    }

    echo '<div style="margin-top:12px;padding-top:10px;border-top:1px solid #e2e8f0">';
    echo '<strong style="font-size:12px;color:#64748b;display:block;margin-bottom:8px">'
        . esc_html__('Recently Indexed Pages', 'shop-to-llm') . '</strong>';
    echo '<table style="width:100%;border-collapse:collapse;font-size:12px">';
    echo '<thead><tr style="border-bottom:1px solid #e2e8f0">'
        . '<th style="text-align:left;padding:4px;color:#64748b;font-weight:600">' . esc_html__('Page', 'shop-to-llm') . '</th>'
        . '<th style="text-align:left;padding:4px;color:#64748b;font-weight:600">' . esc_html__('Crawler', 'shop-to-llm') . '</th>'
        . '<th style="text-align:right;padding:4px;color:#64748b;font-weight:600">' . esc_html__('Last Visit', 'shop-to-llm') . '</th>'
        . '</tr></thead><tbody>';

    foreach ($recent as $row) {
        $title = $row->post_id ? get_the_title($row->post_id) : wp_parse_url($row->page_url, PHP_URL_PATH);
        if (empty($title) || $title === '') $title = '/';
        echo '<tr style="border-bottom:1px solid #f1f5f9">';
        echo '<td style="padding:4px;color:#1e1e1e;font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'
            . esc_html(mb_strlen($title) > 40 ? mb_substr($title, 0, 37) . '...' : $title) . '</td>';
        echo '<td style="padding:4px;color:#475569">' . esc_html($row->bot_label) . '</td>';
        echo '<td style="padding:4px;text-align:right;color:#94a3b8">'
            . esc_html(human_time_diff(strtotime($row->last_visit), time()) . ' ' . __('ago', 'shop-to-llm')) . '</td>';
        echo '</tr>';
    }
    echo '</tbody></table></div>';
}

/**
 * Daily cleanup of crawler logs older than 30 days.
 */
function shop_to_llm_cleanup_old_crawler_logs(): void
{
    global $wpdb;
    $cutoff = gmdate('Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS);

    // Clean crawler log
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "DELETE FROM $table WHERE visited_at < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $cutoff
    ));

    // Clean AI referral log
    $refTable = $wpdb->prefix . 'shop2llm_ai_referrals';
    $refTableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $refTable)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if ($refTableExists) {
        $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "DELETE FROM $refTable WHERE visited_at < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $cutoff
        ));
    }
}

// ─── P0: Admin Bar Real-time Badge ─────────────────────────────────────

function shop_to_llm_admin_bar_crawler_badge(\WP_Admin_Bar $wpAdminBar): void
{
    if (!current_user_can('manage_options')) {
        return;
    }

    // Get the cached 7-factor visibility score
    $scoreTransient = 'shop2llm_admin_bar_score';
    $score = get_transient($scoreTransient);
    if ($score === false) {
        $score = shop_to_llm_get_visibility_score();
        set_transient($scoreTransient, $score, 15 * MINUTE_IN_SECONDS);
    }

    $total = $score['total'];
    $color = $score['color'];
    $bg = $score['bg'];
    $label = $score['label'];

    // Infinity icon — Free always purple (#7c3aed), Pro has its own badge
    $icon = '<span style="display:inline-block;vertical-align:middle;margin-right:3px"><svg width="12" height="12" viewBox="0 0 20 20" style="vertical-align:middle"><path d="M5.5 7C3.5 7 2 8.5 2 10s1.5 3 3.5 3c1.2 0 2-.5 3-1.5L10 10l1.5 1.5c1 1 1.8 1.5 3 1.5 2 0 3.5-1.5 3.5-3S16.5 7 14.5 7c-1.2 0-2 .5-3 1.5L10 10 8.5 8.5C7.5 7.5 6.7 7 5.5 7z" fill="#7c3aed"/></svg></span>';

    $wpAdminBar->add_node([
        'id' => 'shop2llm-crawler-badge',
        'title' => $icon . ' <span style="font-weight:700;color:#fff;background:' . esc_attr($color) . ';border-radius:3px;padding:0 5px;font-size:11px;line-height:18px;display:inline-block;vertical-align:middle">' . (int) $total . '/100</span> <span style="font-size:10px;color:#94a3b8;vertical-align:middle">' . esc_html($label) . '</span>',
        'href' => admin_url('admin.php?page=shop2llm'),
        'meta' => [
            'title' => __('Shop2LLM AI Score', 'shop-to-llm'),
        ],
    ]);
}

// ─── P0: Dashboard Widget Recovery ─────────────────────────────────────

function shop_to_llm_register_dashboard_widget(): void
{
    wp_add_dashboard_widget(
        'shop2llm_crawler_activity',
        'Shop2LLM Visibility',
        'shop_to_llm_render_dashboard_widget_compact',
        null,
        null,
        'normal',
        'high'
    );

    // Move widget to top of dashboard main column
    global $wp_meta_boxes;
    if (isset($wp_meta_boxes['dashboard']['normal']['high']['shop2llm_crawler_activity'])) {
        $widget = $wp_meta_boxes['dashboard']['normal']['high']['shop2llm_crawler_activity'];
        unset($wp_meta_boxes['dashboard']['normal']['high']['shop2llm_crawler_activity']);
        $wp_meta_boxes['dashboard']['normal']['high'] = array_merge(
            ['shop2llm_crawler_activity' => $widget],
            $wp_meta_boxes['dashboard']['normal']['high']
        );
    }
}

function shop_to_llm_render_dashboard_widget_compact(): void
{
    $stats = shop_to_llm_get_crawler_stats();
    $maxVal = max($stats['sparkline'] ?: [1]);
    if ($maxVal === 0) {
        $maxVal = 1;
    }

    echo '<div class="s2ll-widget">';

    // Compact summary
    echo '<div class="s2ll-summary">';
    echo '<div>';
    echo '<span class="s2ll-big">' . (int) $stats['today_bots'] . '</span>';
    echo '<span class="s2ll-label">bots</span>';
    echo '<span class="s2ll-big" style="margin-left:16px">' . (int) $stats['today_pages'] . '</span>';
    echo '<span class="s2ll-label">pages today</span>';
    echo '</div>';
    echo '</div>';

    // 7-day sparkline
    echo '<div class="s2ll-sparkline">';
    foreach ($stats['sparkline'] as $val) {
        $height = $val > 0 ? max(8, ($val / $maxVal) * 48) : 2;
        $color = $val > 0 ? '#6366f1' : '#e2e8f0';
        echo '<div class="s2ll-bar" style="height:' . esc_attr($height) . 'px;background:' . esc_attr($color) . '" title="' . (int) $val . ' crawls"></div>';
    }
    echo '</div>';
    echo '<div class="s2ll-axis"><span>7d ago</span><span>Today</span></div>';

    // View details link
    echo '<div style="text-align:right;margin-top:8px">';
    echo '<a href="' . esc_url(admin_url('admin.php?page=shop2llm')) . '" class="s2ll-link">View Details →</a>';
    echo '</div>';

    // AI referral compact
    $refStats = shop_to_llm_get_ai_referral_stats();
    $refThisMonth = (int) $refStats['this_month'];
    if ($refThisMonth > 0) {
        echo '<div class="s2ll-divider" style="padding-top:8px;margin-top:8px">';
        echo '<span style="font-size:11px;color:#64748b">🚀 <strong style="color:#10b981">' . esc_html($refThisMonth) . '</strong> AI-driven visits this month</span>';
        echo '</div>';
    }

    echo '</div>';
}

// ─── P0: Uninstall Confirmation Dialog ─────────────────────────────────

function shop_to_llm_uninstall_confirmation_js(): void
{
    $pluginFile = plugin_basename(__FILE__);

    // v1.3.3: Cache counts in transient to avoid heavy DB queries on every plugins.php load.
    // The transient is refreshed once per hour or on explicit cache clear.
    $cacheKey = 'shop2llm_uninstall_counts';
    $counts = get_transient($cacheKey);
    if ($counts === false) {
        global $wpdb;
        $counts = [
            'crawlerRecords' => 0,
            'refRecords' => 0,
            'focusQueries' => 0,
        ];

        $cTable = $wpdb->prefix . 'shop2llm_crawler_log';
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $cTable))) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $counts['crawlerRecords'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$cTable}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        $rTable = $wpdb->prefix . 'shop2llm_ai_referrals';
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $rTable))) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $counts['refRecords'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$rTable}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        }
        $counts['focusQueries'] = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != ''", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            '_shop2llm_focus_query'
        ));
        set_transient($cacheKey, $counts, HOUR_IN_SECONDS);
    }

    $crawlerRecords = $counts['crawlerRecords'];
    $refRecords = $counts['refRecords'];
    $historyWeeks = 0;
    $focusQueries = $counts['focusQueries'];
    $lossItems = [];
    if ($crawlerRecords > 0) {
        $lossItems[] = sprintf(/* translators: %d: number of AI crawler visit records */ __('%d AI crawler visit records', 'shop-to-llm'), $crawlerRecords);
    }
    if ($refRecords > 0) {
        $lossItems[] = sprintf(/* translators: %d: number of AI traffic tracking records */ __('%d AI traffic tracking records', 'shop-to-llm'), $refRecords);
    }
    if ($focusQueries > 0) {
        $lossItems[] = sprintf(/* translators: %d: number of Focus Query configurations */ __('%d Focus Query configurations', 'shop-to-llm'), $focusQueries);
    }
    $lossItems[] = __('all AI visibility scores and trend history', 'shop-to-llm');

    $quantifiedMsg = '';
    if (count($lossItems) > 1) {
        $last = array_pop($lossItems);
        $quantifiedMsg = implode(', ', $lossItems) . ' ' . __('and', 'shop-to-llm') . ' ' . $last;
    } else {
        $quantifiedMsg = $lossItems[0];
    }

    $msg = sprintf(
        /* translators: %s: list of data that will be lost */
        __('⚠ You will permanently lose: %s. This data cannot be recovered. Are you sure you want to uninstall?', 'shop-to-llm'),
        $quantifiedMsg
    );
    ?>
    <script>
    (function() {
        var links = document.querySelectorAll('a.delete');
        links.forEach(function(link) {
            if (link.href && link.href.indexOf('<?php echo esc_js($pluginFile); ?>') !== -1) {
                link.addEventListener('click', function(e) {
                    if (!confirm(<?php echo json_encode($msg); ?>)) {
                        e.preventDefault();
                        e.stopPropagation();
                        return false;
                    }
                });
            }
        });
    })();
    </script>
    <?php
}

// ─── P1: AI Visibility Score Meta Box ──────────────────────────────────

function shop_to_llm_add_visibility_meta_box(): void
{
    add_meta_box(
        'shop2llm_visibility_score',
        __('Shop2LLM Visibility Score', 'shop-to-llm'),
        'shop_to_llm_render_visibility_meta_box',
        'post',
        'side',
        'high'
    );
    add_meta_box(
        'shop2llm_visibility_score',
        __('Shop2LLM Visibility Score', 'shop-to-llm'),
        'shop_to_llm_render_visibility_meta_box',
        'product',
        'side',
        'high'
    );
}

function shop_to_llm_visibility_meta_box_css(): void
{
    $screen = get_current_screen();
    if (!$screen || !in_array($screen->base, ['post', 'edit'], true)) {
        return;
    }
    echo '<style>
    /* Score ring gauge */
    .s2ll-score-ring{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;position:relative;margin:0 auto 8px}
    .s2ll-score-ring::before{content:"";position:absolute;inset:0;border-radius:50%;background:conic-gradient(var(--s2ll-ring-color, #16a34a) calc(var(--s2ll-ring-pct, 0) * 1%), #e2e8f0 0)}
    .s2ll-score-ring::after{content:"";position:absolute;inset:6px;border-radius:50%;background:#fff}
    .s2ll-score-ring-value{position:relative;z-index:1;font-size:18px;font-weight:700;color:#1e293b}
    .s2ll-score-ring--red{--s2ll-ring-color:#dc2626}
    .s2ll-score-ring--yellow{--s2ll-ring-color:#f59e0b}
    .s2ll-score-ring--green{--s2ll-ring-color:#16a34a}
    .s2ll-subtitle{font-size:10px;color:#94a3b8;text-align:center;margin-bottom:10px;display:block}

    /* Severity grouping */
    .s2ll-severity-group{margin:8px 0}
    .s2ll-severity-header{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;cursor:pointer;padding:4px 0;user-select:none}
    .s2ll-severity-header:hover{opacity:0.85}
    .s2ll-severity-arrow{transition:transform 0.2s;font-size:10px;color:#94a3b8}
    .s2ll-severity-group[open] .s2ll-severity-arrow{transform:rotate(90deg)}
    .s2ll-severity-count{font-size:11px;font-weight:400;color:#94a3b8;margin-left:4px}
    .s2ll-severity-problems .s2ll-severity-header{color:#dc2626}
    .s2ll-severity-improvements .s2ll-severity-header{color:#d97706}
    .s2ll-severity-good .s2ll-severity-header{color:#16a34a}

    .s2ll-finding{display:flex;align-items:flex-start;gap:6px;padding:4px 0 4px 16px;font-size:12px;line-height:1.5}
    .s2ll-finding-icon{flex-shrink:0;margin-top:1px;font-size:13px}
    .s2ll-finding-body{flex:1;min-width:0}
    .s2ll-finding-text{color:#475569}
    .s2ll-finding-fix{display:inline-block;font-size:11px;color:#6366f1;cursor:pointer;text-decoration:underline;margin-top:2px}
    .s2ll-finding-fix:hover{color:#4f46e5}
    .s2ll-finding-pts{flex-shrink:0;font-size:10px;color:#94a3b8;margin-top:1px;text-align:right;min-width:24px}
    .s2ll-finding--locked{opacity:0.5}

    /* Dimension bars */
    .s2ll-dim-section{margin:10px 0;padding-top:8px;border-top:1px solid #e2e8f0}
    .s2ll-dim-section-title{font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px}
    .s2ll-dimension-bar{display:flex;align-items:center;gap:6px;margin:3px 0;font-size:12px}
    .s2ll-dimension-label{width:105px;color:#64748b;flex-shrink:0;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .s2ll-dimension-track{flex:1;height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden}
    .s2ll-dimension-fill{height:100%;border-radius:3px;transition:width 0.3s}
    .s2ll-dimension-score{width:42px;text-align:right;font-weight:600;flex-shrink:0;font-size:11px}

    /* Focus query */
    .s2ll-focus-query-wrap{margin-bottom:10px}
    .s2ll-focus-query-help{display:block;font-size:11px;color:#94a3b8;margin-top:2px}
    .s2ll-unlock-hint{display:block;font-size:11px;color:#6366f1;margin:1px 0 0 105px}

    /* Trend sparkline */
    .s2ll-sparkline{display:flex;align-items:flex-end;gap:3px;height:28px}
    .s2ll-sparkline-bar{width:6px;border-radius:2px 2px 0 0;background:#6366f1;min-height:2px;transition:height 0.3s}

    /* Trend section */
    .s2ll-trend-section{margin-top:10px;padding-top:8px;border-top:1px solid #e2e8f0}
    .s2ll-trend-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}
    .s2ll-trend-title{font-size:12px;color:#64748b;font-weight:600;flex-shrink:0}
    .s2ll-trend-values{display:flex;align-items:center;gap:8px;font-size:12px;flex-wrap:wrap;margin-top:4px}
    .s2ll-trend-current{color:#334155}
    .s2ll-trend-prev{color:#94a3b8}
    .s2ll-trend-change{font-size:12px}
    .s2ll-trend-warning{margin-top:4px;font-size:11px;color:#dc2626;font-weight:600}
    .s2ll-trend-pending{font-size:11px;color:#94a3b8;margin-top:4px}

    /* Crawler info */
    .s2ll-crawler-info{margin-top:8px;padding:6px 8px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:5px;font-size:11px;color:#0369a1;line-height:1.5}

    /* Auth list */
    .s2ll-auth-list-wrap{margin:4px 0}

    /* Tooltip */
    .s2ll-help{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:#7c3aed;color:#fff;font-size:10px;font-weight:700;cursor:help;flex-shrink:0;vertical-align:middle;margin-left:6px}
    .s2ll-help::before{content:"?"}

    .s2ll-suggestion{font-size:11px;color:#6366f1;margin-top:10px;padding-top:10px;border-top:1px solid #f1f5f9;line-height:1.5}

    /* Criterion (legacy - keep for settings page) */
    .s2ll-criterion{display:flex;align-items:center;gap:8px;font-size:13px;margin-bottom:6px}
    .s2ll-criterion-icon{font-size:14px;flex-shrink:0}
    .s2ll-criterion-text{color:#475569}
    .s2ll-criterion-pts{margin-left:auto;color:#94a3b8;font-size:11px}
    </style>';
}

function shop_to_llm_render_visibility_meta_box(\WP_Post $post): void
{
    $focusQuery = get_post_meta($post->ID, '_shop2llm_focus_query', true);
    $hasFocusQuery = !empty(trim((string) $focusQuery));
    $onAuthList = get_post_meta($post->ID, '_shop2llm_on_authoritative_list', true);

    // Focus Query input
    wp_nonce_field('shop2llm_save_focus_query', 'shop2llm_focus_query_nonce');
    echo '<div class="s2ll-focus-query-wrap">';
    echo '<label for="shop2llm_focus_query" style="font-size:13px;font-weight:600;display:block;margin-bottom:4px">' . esc_html__('Focus Query', 'shop-to-llm') . '</label>';
    echo '<input type="text" id="shop2llm_focus_query" name="shop2llm_focus_query" value="' . esc_attr($focusQuery) . '" placeholder="' . esc_attr__('e.g., best wireless earbuds', 'shop-to-llm') . '" style="width:100%;margin-bottom:4px" />';
    echo '<span class="s2ll-focus-query-help">' . esc_html__('What should AI assistants recommend you for?', 'shop-to-llm') . '</span>';
    echo '</div>';

    // Authoritative list checkbox
    echo '<div class="s2ll-auth-list-wrap">';
    echo '<label style="font-size:12px;display:flex;align-items:center;gap:4px">';
    echo '<input type="checkbox" name="shop2llm_on_authoritative_list" value="1" ' . checked($onAuthList, '1', false) . ' /> ';
    echo esc_html__('My brand appears on authoritative lists', 'shop-to-llm');
    echo '</label>';
    echo '</div>';

    $score = 0;

    // ─── Use canonical 25-item scoring function ──
    $scoring = shop_to_llm_score_single_post($post->ID);
    $allItems = $scoring['items'];
    $totalScore = $scoring['total'];
    $hasFocusQuery = $scoring['has_focus'];

    // Split items into L1-L5 groups
    $l1Items = array_slice($allItems, 0, 6);
    $l2Items = array_slice($allItems, 6, 6);
    $l3Items = array_slice($allItems, 12, 6);
    $l4Items = array_slice($allItems, 18, 3);
    $l5Items = array_slice($allItems, 21, 4);

    $l1 = array_sum(array_column($l1Items, 'pts'));
    $l2 = array_sum(array_column($l2Items, 'pts'));
    $l3 = array_sum(array_column($l3Items, 'pts'));
    $l4 = array_sum(array_column($l4Items, 'pts'));
    $l5 = array_sum(array_column($l5Items, 'pts'));

    $score = $totalScore;
    $content = $post->post_content; // kept for downstream rendering

    echo '<div class="s2ll-widget">';

    // ─── Ring Gauge Score ────────────────────────────────────────────
    $ringClass = $score >= 71 ? 's2ll-score-ring--green' : ($score >= 41 ? 's2ll-score-ring--yellow' : 's2ll-score-ring--red');
    echo '<div class="s2ll-score-ring ' . esc_attr($ringClass) . '" style="--s2ll-ring-pct:' . (int) $score . '">';
    echo '<span class="s2ll-score-ring-value">' . (int) $score . '</span>';
    echo '</div>';
    echo '<span class="s2ll-subtitle">Shop2LLM™ 5-Dimension Analysis</span>';

    // SEO plugin score integration
    $seoScore = 0;
    $seoPlugin = '';
    if (get_post_meta($post->ID, '_yoast_wpseo_linkdex', true) !== '') {
        $seoScore = (int) get_post_meta($post->ID, '_yoast_wpseo_linkdex', true);
        $seoPlugin = 'Yoast SEO';
    } elseif (get_post_meta($post->ID, 'rank_math_seo_score', true) !== '') {
        $rankMathScore = (int) get_post_meta($post->ID, 'rank_math_seo_score', true);
        $seoScore = min(100, max(0, $rankMathScore));
        $seoPlugin = 'Rank Math';
    }
    if ($seoScore > 0 && $seoPlugin) {
        $seoColor = $seoScore >= 71 ? '#16a34a' : ($seoScore >= 41 ? '#f59e0b' : '#dc2626');
        echo '<div style="display:flex;align-items:center;justify-content:center;gap:6px;margin-bottom:4px;font-size:11px">';
        echo '<span style="color:#94a3b8">' . esc_html($seoPlugin) . ':</span>';
        echo '<span style="color:' . esc_attr($seoColor) . ';font-weight:600">' . (int) $seoScore . '/100</span>';
        echo '</div>';
    } else {
        // No SEO plugin installed — show helpful hint
        echo '<div style="text-align:center;margin-bottom:4px;font-size:10px;color:#94a3b8;line-height:1.4">';
        echo esc_html__('Install Yoast SEO or Rank Math for a complete content health score', 'shop-to-llm');
        echo '</div>';
    }

    // ─── Fix guidance lookup ─────────────────────────────────────────
    $settingsUrl = admin_url('admin.php?page=shop2llm');

    $fixGuidance = [
        1  => ['fail' => __('llms.txt is empty', 'shop-to-llm'),                               'fix' => __('Go to Settings', 'shop-to-llm'),   'url' => $settingsUrl],
        2  => ['fail' => __('robots.txt may block AI crawlers', 'shop-to-llm'),                 'fix' => __('Fix robots.txt', 'shop-to-llm'),   'url' => $settingsUrl],
        3  => ['fail' => __('Schema markup missing content', 'shop-to-llm'),                    'fix' => __('Add structured data', 'shop-to-llm'), 'url' => ''],
        4  => ['fail' => __('No AI tools connected — store invisible to AI', 'shop-to-llm'),   'fix' => __('Connect AI tools', 'shop-to-llm'), 'url' => $settingsUrl],
        5  => ['fail' => __('Page has heavy JS — may be unreadable to AI', 'shop-to-llm'),     'fix' => __('Check page source', 'shop-to-llm'), 'url' => ''],
        6  => ['fail' => __('No FAQ or Q&A section found', 'shop-to-llm'),                     'fix' => __('Add FAQ block', 'shop-to-llm'),    'url' => ''],
        7  => ['fail' => __('Search keyword not in title', 'shop-to-llm'),                     'fix' => __('Edit title', 'shop-to-llm'),       'url' => ''],
        8  => ['fail' => __('Search keyword not in heading (H1/H2)', 'shop-to-llm'),           'fix' => __('Add heading with keyword', 'shop-to-llm'), 'url' => ''],
        9  => ['fail' => __('Keyword not in opening paragraph', 'shop-to-llm'),                'fix' => __('Add keyword near top', 'shop-to-llm'), 'url' => ''],
        10 => ['fail' => __('Keyword not in URL/slug', 'shop-to-llm'),                         'fix' => __('Edit slug', 'shop-to-llm'),        'url' => ''],
        11 => ['fail' => __('Keyword not in meta description', 'shop-to-llm'),                 'fix' => __('Set SEO description', 'shop-to-llm'), 'url' => ''],
        12 => ['fail' => __('No alt text with keyword on images', 'shop-to-llm'),              'fix' => __('Add alt text', 'shop-to-llm'),     'url' => ''],
        13 => ['fail' => __('Content too short (<150 words)', 'shop-to-llm'),                  'fix' => __('Add more content', 'shop-to-llm'), 'url' => ''],
        14 => ['fail' => __('No statistics or data found — AI prefers data-backed content', 'shop-to-llm'), 'fix' => __('Add numbers & data', 'shop-to-llm'), 'url' => ''],
        15 => ['fail' => __('No citations or references — AI prefers verifiable content', 'shop-to-llm'), 'fix' => __('Add quotes & sources', 'shop-to-llm'), 'url' => ''],
        16 => ['fail' => __('Content was updated >90 days ago — AI prefers fresh content', 'shop-to-llm'), 'fix' => __('Update content', 'shop-to-llm'), 'url' => ''],
        17 => ['fail' => __('Missing title/thumbnail/excerpt for schema', 'shop-to-llm'),      'fix' => __('Fill in missing fields', 'shop-to-llm'), 'url' => ''],
        18 => ['fail' => __('Too few internal links (need 3+)', 'shop-to-llm'),                'fix' => __('Link to related content', 'shop-to-llm'), 'url' => ''],
        19 => ['fail' => __('Brand not found in knowledge graph', 'shop-to-llm'),              'fix' => __('Enable entity checks', 'shop-to-llm'), 'url' => $settingsUrl],
        20 => ['fail' => __('Not mentioned on Wikipedia', 'shop-to-llm'),                      'fix' => __('Enable entity checks', 'shop-to-llm'), 'url' => $settingsUrl],
        21 => ['fail' => __('Not marked as on authoritative lists', 'shop-to-llm'),            'fix' => __('Check box above', 'shop-to-llm'), 'url' => ''],
        22 => ['fail' => __('No recent AI crawler visits', 'shop-to-llm'),                     'fix' => __('Wait for crawler activity', 'shop-to-llm'), 'url' => ''],
        23 => ['fail' => __('No reviews yet', 'shop-to-llm'),                                  'fix' => __('Encourage reviews', 'shop-to-llm'), 'url' => ''],
        24 => ['fail' => __('Seen by <4 AI platforms', 'shop-to-llm'),                         'fix' => __('Connect more AI tools', 'shop-to-llm'), 'url' => $settingsUrl],
        25 => ['fail' => __('Keyword density outside 0.5-2.5%', 'shop-to-llm'),                'fix' => __('Adjust keyword usage', 'shop-to-llm'), 'url' => ''],
    ];

    // User-friendly item names (replacing technical labels)
    $itemUserNames = [
        1  => __('llms.txt file is accessible', 'shop-to-llm'),
        2  => __('robots.txt allows AI crawlers', 'shop-to-llm'),
        3  => __('Structured data (Schema) present', 'shop-to-llm'),
        4  => __('AI tools connected to store', 'shop-to-llm'),
        5  => __('Page content readable without JavaScript', 'shop-to-llm'),
        6  => __('FAQ or Q&A section included', 'shop-to-llm'),
        7  => __('Search keyword appears in title', 'shop-to-llm'),
        8  => __('Search keyword appears in heading', 'shop-to-llm'),
        9  => __('Search keyword in first paragraph', 'shop-to-llm'),
        10 => __('Search keyword in URL address', 'shop-to-llm'),
        11 => __('Search keyword in meta description', 'shop-to-llm'),
        12 => __('Search keyword in image alt text', 'shop-to-llm'),
        13 => __('Content length over 300 words', 'shop-to-llm'),
        14 => __('Includes statistics and data', 'shop-to-llm'),
        15 => __('Includes citations and references', 'shop-to-llm'),
        16 => __('Content updated within 30 days', 'shop-to-llm'),
        17 => __('Complete metadata (title, image, excerpt)', 'shop-to-llm'),
        18 => __('Internal links to related pages', 'shop-to-llm'),
        19 => __('Brand recognized in knowledge graph', 'shop-to-llm'),
        20 => __('Brand mentioned on Wikipedia', 'shop-to-llm'),
        21 => __('Appears on authoritative lists', 'shop-to-llm'),
        22 => __('Recent AI crawler activity', 'shop-to-llm'),
        23 => __('Customer reviews exist', 'shop-to-llm'),
        24 => __('Indexed by multiple AI platforms', 'shop-to-llm'),
        25 => __('Keyword density in optimal range', 'shop-to-llm'),
    ];

    // Tooltip explanations for technical terms
    $tooltips = [
        5  => __('AI reads raw HTML, not rendered pages. Heavy JavaScript can hide content.', 'shop-to-llm'),
        4  => __('MCP (Model Context Protocol) lets ChatGPT and other AI tools directly query your store.', 'shop-to-llm'),
        1  => __('llms.txt is a standard file that tells AI models what your website contains.', 'shop-to-llm'),
    ];

    // ─── Build all 25 items with metadata ────────────────────────────
    $allIndexed = [];
    $idx = 1;
    foreach ([$l1Items, $l2Items, $l3Items, $l4Items, $l5Items] as $dimItems) {
        foreach ($dimItems as $item) {
            $allIndexed[$idx] = $item;
            $idx++;
        }
    }

    // ─── Classify into Problems / Improvements / Good ─────────────────
    $problems = [];     // fail + has fix (no lock)
    $improvements = []; // pass but pts < max, or locked items
    $good = [];         // pass with full pts

    foreach ($allIndexed as $i => $item) {
        $locked = !empty($item['locked']);
        if ($locked) {
            $improvements[] = ['index' => $i, 'item' => $item, 'locked' => true];
        } elseif (!$item['pass']) {
            $problems[] = ['index' => $i, 'item' => $item, 'locked' => false];
        } elseif ($item['pts'] < 1) {
            // Passed but scored 0 pts — improvement opportunity
            $improvements[] = ['index' => $i, 'item' => $item, 'locked' => false];
        } else {
            $good[] = ['index' => $i, 'item' => $item, 'locked' => false];
        }
    }

    // Helper to render a single finding row
    $renderFinding = function (array $entry) use ($fixGuidance, $itemUserNames, $tooltips, $settingsUrl) {
        $i = $entry['index'];
        $item = $entry['item'];
        $locked = $entry['locked'];
        $name = $itemUserNames[$i] ?? $item['name'];
        $guidance = $fixGuidance[$i] ?? null;
        $tooltip = $tooltips[$i] ?? '';

        echo '<div class="s2ll-finding' . ($locked ? ' s2ll-finding--locked' : '') . '">';

        // Icon
        if ($locked) {
            echo '<span class="s2ll-finding-icon">🔒</span>';
        } elseif ($item['pass']) {
            echo '<span class="s2ll-finding-icon" style="color:#16a34a">✓</span>';
        } else {
            echo '<span class="s2ll-finding-icon" style="color:#dc2626">✗</span>';
        }

        // Body
        echo '<span class="s2ll-finding-body">';
        if ($locked) {
            echo '<span class="s2ll-finding-text" style="color:#94a3b8">' . esc_html($name) . '</span>';
        } elseif (!$item['pass'] && $guidance) {
            echo '<span class="s2ll-finding-text">' . esc_html($guidance['fail']) . '</span>';
            if (!empty($guidance['fix'])) {
                $url = !empty($guidance['url']) ? esc_url($guidance['url']) : '';
                echo ' ';
                if ($url) {
                    echo '<a href="' . $url . '" class="s2ll-finding-fix" target="_blank">' . esc_html($guidance['fix']) . ' →</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                } else {
                    echo '<span class="s2ll-finding-fix">' . esc_html($guidance['fix']) . '</span>';
                }
            }
        } else {
            echo '<span class="s2ll-finding-text" style="color:#16a34a">' . esc_html($name) . '</span>';
        }

        // Tooltip
        if ($tooltip) {
            echo '<span class="s2ll-help" title="' . esc_attr($tooltip) . '">?</span>';
        }
        echo '</span>';

        // Points
        $ptsStr = $locked ? '—' : '+' . (int) $item['pts'];
        echo '<span class="s2ll-finding-pts">' . esc_html($ptsStr) . '</span>';

        echo '</div>';
    };

    // ─── Render severity groups ──────────────────────────────────────
    // Problems: default OPEN
    if (!empty($problems)) {
        echo '<details class="s2ll-severity-group s2ll-severity-problems" open>';
        echo '<summary class="s2ll-severity-header"><span class="s2ll-severity-arrow">▶</span>' . esc_html__('Problems', 'shop-to-llm') . '<span class="s2ll-severity-count">(' . count($problems) . ')</span></summary>';
        foreach ($problems as $entry) {
            $renderFinding($entry);
        }
        echo '</details>';
    }

    // Improvements: default CLOSED
    if (!empty($improvements)) {
        echo '<details class="s2ll-severity-group s2ll-severity-improvements">';
        echo '<summary class="s2ll-severity-header"><span class="s2ll-severity-arrow">▶</span>' . esc_html__('To Improve', 'shop-to-llm') . '<span class="s2ll-severity-count">(' . count($improvements) . ')</span></summary>';
        foreach ($improvements as $entry) {
            $renderFinding($entry);
        }
        echo '</details>';
    }

    // Good: default CLOSED
    if (!empty($good)) {
        echo '<details class="s2ll-severity-group s2ll-severity-good">';
        echo '<summary class="s2ll-severity-header"><span class="s2ll-severity-arrow">▶</span>' . esc_html__('Good', 'shop-to-llm') . '<span class="s2ll-severity-count">(' . count($good) . ')</span></summary>';
        foreach ($good as $entry) {
            $renderFinding($entry);
        }
        echo '</details>';
    }

    // If all 25 passed (unlikely), show all-green message
    if (empty($problems) && empty($improvements)) {
        echo '<div style="text-align:center;padding:8px;color:#16a34a;font-size:13px;font-weight:600">' . esc_html__('Perfect AI Visibility!', 'shop-to-llm') . '</div>';
    }

    // ─── Dimension Summary ──────────────────────────────────────────
    $l2Locked = !$hasFocusQuery;
    $l4Locked = !$hasFocusQuery;
    $dimensions = [
        ['label' => __('Can AI find me?', 'shop-to-llm'),       'score' => $l1, 'max' => 30, 'color' => '#3b82f6', 'locked' => false,  'tooltip' => __('Technical discoverability: llms.txt, robots.txt, Schema, MCP', 'shop-to-llm')],
        ['label' => $l2Locked ? __('Relevance 🔒', 'shop-to-llm') : __('Search Relevance', 'shop-to-llm'), 'score' => $l2, 'max' => 25, 'color' => $l2Locked ? '#94a3b8' : '#8b5cf6', 'locked' => $l2Locked, 'tooltip' => __('How well your content matches the search query', 'shop-to-llm')],
        ['label' => __('Content Quality', 'shop-to-llm'),       'score' => $l3, 'max' => 25, 'color' => '#f59e0b', 'locked' => false,  'tooltip' => __('Length, statistics, citations, freshness, structure', 'shop-to-llm')],
        ['label' => $l4Locked ? __('Authority 🔒', 'shop-to-llm') : __('Brand Authority', 'shop-to-llm'), 'score' => $l4, 'max' => 15, 'color' => $l4Locked ? '#94a3b8' : '#06b6d4', 'locked' => $l4Locked, 'tooltip' => __('Knowledge graph, Wikipedia, authoritative lists', 'shop-to-llm')],
        ['label' => __('AI Activity', 'shop-to-llm'),           'score' => $l5, 'max' => 5,  'color' => '#16a34a', 'locked' => false,  'tooltip' => __('Crawler visits, reviews, multi-platform reach', 'shop-to-llm')],
    ];

    echo '<div class="s2ll-dim-section">';
    echo '<span class="s2ll-dim-section-title">' . esc_html__('Dimension Breakdown', 'shop-to-llm') . '</span>';

    foreach ($dimensions as $dim) {
        $pct = $dim['max'] > 0 ? round(($dim['score'] / $dim['max']) * 100) : 0;
        $barOpacity = $dim['locked'] ? '0.5' : '1';
        echo '<div class="s2ll-dimension-bar" style="opacity:' . esc_attr($barOpacity) . '">';
        echo '<span class="s2ll-dimension-label">' . esc_html($dim['label']);
        if (!empty($dim['tooltip'])) {
            echo '<span class="s2ll-help" title="' . esc_attr($dim['tooltip']) . '">?</span>';
        }
        echo '</span>';
        echo '<span class="s2ll-dimension-track"><span class="s2ll-dimension-fill" style="width:' . esc_attr((string)$pct) . '%;background:' . esc_attr($dim['color']) . '"></span></span>';
        echo '<span class="s2ll-dimension-score">' . (int) $dim['score'] . '/' . (int) $dim['max'] . '</span>';
        echo '</div>';
        if ($dim['locked']) {
            echo '<span class="s2ll-unlock-hint">' . esc_html__('Enter a Focus Query to unlock', 'shop-to-llm') . '</span>';
        }
    }
    echo '</div>';

    // ─── AI Visibility Trend with Mini Sparkline ────────────────────
    echo '<div class="s2ll-trend-section">';
    echo '<div class="s2ll-trend-header">';
    echo '<span class="s2ll-trend-title">' . esc_html__('AI Visibility Trend', 'shop-to-llm') . '</span>';
    echo '</div>';

    $history = get_post_meta($post->ID, '_shop2llm_visibility_history', true);
    $sparkData = [];
    if (is_array($history) && count($history) >= 2) {
        $currentScore = $score;
        $prevEntry = $history[count($history) - 2];
        $prevScore = (int) $prevEntry['score'];

        // Build sparkline data (last up to 8 weeks)
        $sparkData = array_map(function ($e) { return (int) $e['score']; }, $history);
        // Normalize to 0-28px height range
        if (!empty($sparkData)) {
            $maxVal = max($sparkData);
            $minVal = min($sparkData);
            $range = $maxVal - $minVal ?: 1;
            echo '<div class="s2ll-sparkline">';
            foreach ($sparkData as $val) {
                $h = max(2, round((($val - $minVal) / $range) * 28));
                echo '<span class="s2ll-sparkline-bar" style="height:' . (int) $h . 'px" title="' . (int) $val . '"></span>';
            }
            echo '</div>';
        }

        if ($prevScore > 0) {
            $changePct = round((($currentScore - $prevScore) / $prevScore) * 100, 1);
        } else {
            $changePct = $currentScore > 0 ? 100 : 0;
        }

        $arrow = $changePct >= 0 ? '↑' : '↓';
        $color = $changePct >= 0 ? '#16a34a' : '#dc2626';

        echo '<div class="s2ll-trend-values">';
        echo '<span class="s2ll-trend-current">' . esc_html__('This week:', 'shop-to-llm') . ' <strong>' . (int) $currentScore . '</strong></span>';
        echo '<span class="s2ll-trend-prev">' . esc_html__('Last:', 'shop-to-llm') . ' ' . (int) $prevScore . '</span>';
        echo '<span class="s2ll-trend-change" style="color:' . esc_attr($color) . ';font-weight:600">' . esc_html($arrow . ' ' . abs($changePct) . '%') . '</span>';
        echo '</div>';

        if ($changePct < -10) {
            echo '<div class="s2ll-trend-warning">⚠ ' . esc_html__('AI visibility dropping', 'shop-to-llm') . '</div>';
        }
    } elseif (is_array($history) && count($history) === 1) {
        echo '<div class="s2ll-trend-values">';
        echo '<span class="s2ll-trend-current">' . esc_html__('This week:', 'shop-to-llm') . ' <strong>' . (int) $score . '</strong></span>';
        echo '</div>';
        echo '<div class="s2ll-trend-pending">📊 ' . esc_html__('Trend data will appear after next week', 'shop-to-llm') . '</div>';
    } else {
        echo '<div class="s2ll-trend-pending">📊 ' . esc_html__('Trend data will appear after next week', 'shop-to-llm') . '</div>';
    }

    echo '</div>'; // .s2ll-trend-section

    // ─── Crawler info ──────────────────────────────────────────────
    if (!empty($crawlerStats['total_crawls']) && $crawlerStats['total_crawls'] > 0) {
        echo '<div class="s2ll-crawler-info">';
        echo esc_html(sprintf(/* translators: %d: number of unique AI bots */ __('%d AI bots visited', 'shop-to-llm'), (int) $crawlerStats['unique_bots']));
        echo ' · ' . esc_html(implode(', ', array_slice($crawlerStats['bot_list'], 0, 3)));
        if (count($crawlerStats['bot_list']) > 3) {
            echo ' +' . (count($crawlerStats['bot_list']) - 3);
        }
        echo '<br>' . esc_html(sprintf(/* translators: %s: last visit date/time */ __('Last: %s', 'shop-to-llm'), $crawlerStats['last_visit']));
        echo '</div>';
    }

    echo '</div>'; // .s2ll-widget
}

// ─── P2: AI Visibility Column in Post/Product List ─────────────────────

function shop_to_llm_add_visibility_column(array $columns): array
{
    $new = [];
    foreach ($columns as $key => $label) {
        $new[$key] = $label;
        if ($key === 'title') {
            $new['s2ll_visibility'] = __('S2LL Score', 'shop-to-llm');
        }
    }
    return $new;
}

function shop_to_llm_render_visibility_column(string $column, int $postId): void
{
    if ($column !== 's2ll_visibility') {
        return;
    }

    // Use bulk-cached crawled post IDs
    static $crawledPosts = null;
    if ($crawledPosts === null) {
        $crawledPosts = shop_to_llm_get_bulk_crawled_post_ids();
    }

    $post = get_post($postId);
    if (!$post) {
        return;
    }

    $score = shop_to_llm_calculate_visibility_score($postId);
    $focusQuery = get_post_meta($postId, '_shop2llm_focus_query', true);
    $hasFocusQuery = !empty(trim((string) $focusQuery));

    $color = $score >= 71 ? '#16a34a' : ($score >= 41 ? '#f59e0b' : '#dc2626');
    $emoji = $score >= 71 ? '🟢' : ($score >= 41 ? '🟡' : '🔴');
    $title = sprintf(/* translators: %d: visibility score */ __('Shop2LLM Visibility Score: %d/100', 'shop-to-llm'), $score);

    echo '<span style="display:inline-flex;align-items:center;gap:4px;cursor:default" title="' . esc_attr($title) . '">';
    echo '<span style="font-size:11px">' . esc_html($emoji) . '</span>';
    echo '<span style="font-size:11px;color:' . esc_attr($color) . ';font-weight:600">' . esc_html((string)$score) . '/100</span>';

    // Focus query in quotes
    if ($hasFocusQuery) {
        echo ' <span style="font-size:11px;color:#64748b">"' . esc_html($focusQuery) . '"</span>';
    }

    // Trend indicator
    $history = get_post_meta($postId, '_shop2llm_visibility_history', true);
    if (is_array($history) && count($history) >= 2) {
        $prevScore = (int) $history[count($history) - 2]['score'];
        if ($prevScore > 0) {
            $changePct = round((($score - $prevScore) / $prevScore) * 100, 1);
        } else {
            $changePct = $score > 0 ? 100 : 0;
        }
        if ($changePct > 0) {
            echo ' <span style="font-size:11px;color:#16a34a;font-weight:600">↑</span>';
        } elseif ($changePct < 0) {
            echo ' <span style="font-size:11px;color:#dc2626;font-weight:600">↓</span>';
        }
    }

    echo '</span>';
}

function shop_to_llm_get_bulk_crawled_post_ids(): array
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';
    $tableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$tableExists) {
        return [];
    }

    $transientKey = 'shop2llm_crawled_post_ids_30d';
    $cached = get_transient($transientKey);
    if ($cached !== false && is_array($cached)) {
        return $cached;
    }

    $thirtyDaysAgo = gmdate('Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS);

    // Check if post_id column exists
    $col = $wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'post_id'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    if (!empty($col)) {
        $ids = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT DISTINCT post_id FROM $table WHERE visited_at >= %s AND post_id > 0", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $thirtyDaysAgo
        ));
    } else {
        // Fallback: match by URL
        $urls = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT DISTINCT page_url FROM $table WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $thirtyDaysAgo
        ));
        $ids = [];
        foreach ($urls as $url) {
            $pid = (int) url_to_postid($url);
            if ($pid > 0) {
                $ids[] = $pid;
            }
        }
        $ids = array_unique($ids);
    }

    set_transient($transientKey, $ids, HOUR_IN_SECONDS);
    return $ids;
}

// ─── P3: Per-Post AI Crawler Visit Stats ───────────────────────────────

function shop_to_llm_get_post_crawler_stats(int $postId): array
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';

    $default = [
        'total_crawls' => 0,
        'unique_bots' => 0,
        'last_visit' => '',
        'bot_list' => [],
    ];

    $tableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$tableExists) {
        return $default;
    }

    // Check if post_id column exists
    $col = $wpdb->get_results("SHOW COLUMNS FROM $table LIKE 'post_id'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

    if (!empty($col)) {
        // Use post_id column for efficient lookup
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT COUNT(*) as total, COUNT(DISTINCT bot_name) as unique_bots, MAX(visited_at) as last_visit
             FROM $table WHERE post_id = %d",
            $postId
        ), ARRAY_A);
        // phpcs:enable

        if ($row && (int) $row['total'] > 0) {
            $botList = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
                "SELECT DISTINCT bot_label FROM $table WHERE post_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
                $postId
            ));
            return [
                'total_crawls' => (int) $row['total'],
                'unique_bots' => (int) $row['unique_bots'],
                'last_visit' => $row['last_visit'] ?: '',
                'bot_list' => $botList ?: [],
            ];
        }
    }

    // Fallback: match by URL
    $postUrl = get_permalink($postId);
    if (!$postUrl) {
        return $default;
    }

    $like = '%' . $wpdb->esc_like(wp_parse_url($postUrl, PHP_URL_PATH)) . '%';
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $row = $wpdb->get_row($wpdb->prepare(
        "SELECT COUNT(*) as total, COUNT(DISTINCT bot_name) as unique_bots, MAX(visited_at) as last_visit
         FROM $table WHERE page_url LIKE %s",
        $like
    ), ARRAY_A);
    // phpcs:enable

    if ($row && (int) $row['total'] > 0) {
        $botList = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT DISTINCT bot_label FROM $table WHERE page_url LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $like
        ));
        return [
            'total_crawls' => (int) $row['total'],
            'unique_bots' => (int) $row['unique_bots'],
            'last_visit' => $row['last_visit'] ?: '',
            'bot_list' => $botList ?: [],
        ];
    }

    return $default;
}

// ─── Feature 1: AI Discovery Badge ─────────────────────────────────────

function shop_to_llm_render_footer_badge(): void
{
    if (is_admin() || wp_doing_ajax() || defined('REST_REQUEST')) {
        return;
    }
    if (get_option('shop_to_llm_enabled', 'yes') !== 'yes') {
        return;
    }
    if (!apply_filters('shop_to_llm_show_badge', true)) {
        return;
    }
    if (get_option('shop_to_llm_show_badge', 'yes') !== 'yes') {
        return;
    }
    $visibilityUrl = esc_url(home_url('/ai-visibility/'));
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $visibilityUrl already escaped
    echo '<a href="' . $visibilityUrl . '" id="s2ll-badge" style="position:fixed;bottom:12px;right:12px;background:#fff;border:1px solid #e2e8f0;border-radius:6px;padding:4px 10px;font-family:-apple-system,sans-serif;font-size:11px;color:#64748b;box-shadow:0 1px 3px rgba(0,0,0,0.08);z-index:9999;text-decoration:none;display:flex;align-items:center;gap:4px"><span style="font-size:13px">🛡</span><span>AI Discoverable</span></a>';
}

// ─── Feature 1b: /ai-visibility/ Verification Page ─────────────────────

function shop_to_llm_add_visibility_rewrite(): void
{
    add_rewrite_rule('^ai-visibility/?$', 'index.php?s2ll_visibility=1', 'top');
}

function shop_to_llm_visibility_query_var(array $vars): array
{
    $vars[] = 's2ll_visibility';
    return $vars;
}

function shop_to_llm_render_visibility_page(): void
{
    if (!get_query_var('s2ll_visibility')) {
        return;
    }

    $siteName = get_bloginfo('name');
    $siteUrl = get_site_url();
    $siteDesc = get_bloginfo('description');

    // llms.txt status
    $llmsTxtUrl = esc_url($siteUrl . '/llms.txt');
    $llmsTxtStatus = 'Active';

    // Schema markup count
    $schemaCount = 0;
    if (shop_to_llm_has_woocommerce()) {
        $schemaCount = (int) (wp_count_posts('product')->publish ?? 0);
    } else {
        $schemaCount = (int) (wp_count_posts('post')->publish ?? 0);
    }

    // AI crawler visits (unique bots in last 30 days)
    $crawlerStats = shop_to_llm_get_crawler_stats();
    $uniqueBots30d = (int) $crawlerStats['unique_bots_30d'];
    $totalCrawls30d = (int) $crawlerStats['total_30d'];

    // MCP tools status
    $mcpActive = get_option('shop_to_llm_api_key', '') !== '' ? 'Active' : 'Inactive';

    // AI referral stats
    $referralStats = shop_to_llm_get_ai_referral_stats();
    $totalReferrals = (int) $referralStats['this_month'];

    // Known AI platforms
    $platforms = [
        ['name' => 'ChatGPT', 'icon' => '🤖'],
        ['name' => 'Perplexity', 'icon' => '🔍'],
        ['name' => 'Google AI', 'icon' => '🌐'],
        ['name' => 'Bing Copilot', 'icon' => '💬'],
        ['name' => 'Claude', 'icon' => '🧠'],
        ['name' => 'Brave Search', 'icon' => '🦁'],
    ];

    header('Content-Type: text/html; charset=utf-8');
    ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?php echo esc_html($siteName); ?> — AI Visibility</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f8fafc;color:#1e293b;line-height:1.6}
.wrap{max-width:680px;margin:0 auto;padding:40px 20px}
.card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
h1{font-size:24px;font-weight:700;color:#1e293b;margin-bottom:4px}
.subtitle{font-size:14px;color:#64748b;margin-bottom:24px}
.status-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.status-item{padding:16px;border-radius:8px;background:#f8fafc;border:1px solid #e2e8f0}
.status-item h3{font-size:12px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
.status-item .val{font-size:22px;font-weight:700;color:#1e293b}
.status-item .val.active{color:#16a34a}
.status-item .val.inactive{color:#94a3b8}
.platforms{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.platform-tag{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;background:#f0f0ff;border:1px solid #e2e8f0;border-radius:20px;font-size:13px;color:#4338ca;font-weight:500}
.branding{text-align:center;margin-top:24px;font-size:12px;color:#94a3b8}
.branding a{color:#6366f1;text-decoration:none;font-weight:600}
</style>
</head>
<body>
<div class="wrap">
<div class="card" style="text-align:center;border-left:4px solid #6366f1">
<h1>🛡 <?php echo esc_html($siteName); ?></h1>
<p class="subtitle"><?php echo esc_html($siteDesc); ?> · <?php echo esc_url($siteUrl); ?></p>
<p style="font-size:14px;color:#16a34a;font-weight:600">✓ This site is AI-Discoverable</p>
</div>

<div class="card">
<h2 style="font-size:16px;font-weight:600;margin-bottom:16px">AI Visibility Status</h2>
<div class="status-grid">
<div class="status-item">
<h3>llms.txt</h3>
<div class="val active"><?php echo esc_html($llmsTxtStatus); ?></div>
</div>
<div class="status-item">
<h3>Schema Markup</h3>
<div class="val"><?php echo esc_html((string)$schemaCount); ?> <span style="font-size:12px;color:#64748b;font-weight:400">pages</span></div>
</div>
<div class="status-item">
<h3>AI Crawler Visits (30d)</h3>
<div class="val"><?php echo esc_html((string)$totalCrawls30d); ?> <span style="font-size:12px;color:#64748b;font-weight:400">from <?php echo esc_html((string)$uniqueBots30d); ?> bots</span></div>
</div>
<div class="status-item">
<h3>MCP Tools</h3>
<div class="val <?php echo $mcpActive === 'Active' ? 'active' : 'inactive'; ?>"><?php echo esc_html($mcpActive); ?></div>
</div>
<div class="status-item">
<h3>AI-Driven Traffic (This Month)</h3>
<div class="val"><?php echo esc_html((string)$totalReferrals); ?> <span style="font-size:12px;color:#64748b;font-weight:400">visits</span></div>
</div>
<div class="status-item">
<h3>Plugin Version</h3>
<div class="val" style="font-size:16px"><?php echo esc_html(SHOP_TO_LLM_VERSION); ?></div>
</div>
</div>
</div>

<div class="card">
<h2 style="font-size:16px;font-weight:600;margin-bottom:8px">AI Platforms That Can Discover This Site</h2>
<p style="font-size:13px;color:#64748b;margin-bottom:12px">This site is optimized for discovery by the following AI assistants and search engines.</p>
<div class="platforms">
<?php foreach ($platforms as $p) : ?>
<span class="platform-tag"><?php echo esc_html($p['icon']); ?> <?php echo esc_html($p['name']); ?></span>
<?php endforeach; ?>
</div>
</div>

<div class="branding">
Powered by <a href="https://shop2llm.com/" target="_blank">Shop2LLM</a> — AI Visibility for Your Website
</div>
</div>
</body>
</html>
    <?php
    exit;
}

// ─── Feature 2: AI Referral Tracker ────────────────────────────────────

function shop_to_llm_create_ai_referrals_table(): void
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_ai_referrals';
    $charset = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE IF NOT EXISTS $table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        source VARCHAR(50) NOT NULL,
        source_label VARCHAR(50) NOT NULL,
        landing_url VARCHAR(500) NOT NULL DEFAULT '',
        post_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
        visited_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY idx_source (source),
        KEY idx_date (visited_at)
    ) $charset;";
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

function shop_to_llm_detect_ai_referral(): void
{
    if (is_admin() || wp_doing_ajax() || defined('REST_REQUEST')) {
        return;
    }

    $referer = sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'] ?? ''));
    if (empty($referer)) {
        return;
    }

    $aiReferrers = [
        'perplexity.ai'         => ['source' => 'perplexity', 'label' => 'Perplexity'],
        'chat.openai.com'       => ['source' => 'chatgpt', 'label' => 'ChatGPT'],
        'copilot.microsoft.com' => ['source' => 'copilot', 'label' => 'Copilot'],
        'bing.com/chat'         => ['source' => 'bing-chat', 'label' => 'Bing Chat'],
        'you.com'               => ['source' => 'you', 'label' => 'You.com'],
        'search.brave.com'      => ['source' => 'brave', 'label' => 'Brave Search'],
        'phind.com'             => ['source' => 'phind', 'label' => 'Phind'],
        'kagi.com'              => ['source' => 'kagi', 'label' => 'Kagi'],
    ];

    // Also check UTM parameters
    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- UTM params are public tracking values, no nonce needed
    $utmSource = sanitize_text_field(wp_unslash($_GET['utm_source'] ?? ''));
    $utmMap = [
        'chatgpt'    => ['source' => 'chatgpt', 'label' => 'ChatGPT'],
        'perplexity' => ['source' => 'perplexity', 'label' => 'Perplexity'],
        'copilot'    => ['source' => 'copilot', 'label' => 'Copilot'],
        'bing-chat'  => ['source' => 'bing-chat', 'label' => 'Bing Chat'],
        'you'        => ['source' => 'you', 'label' => 'You.com'],
        'brave'      => ['source' => 'brave', 'label' => 'Brave Search'],
        'phind'      => ['source' => 'phind', 'label' => 'Phind'],
        'kagi'       => ['source' => 'kagi', 'label' => 'Kagi'],
    ];

    $matched = null;

    // Check referer
    foreach ($aiReferrers as $domain => $info) {
        if (stripos($referer, $domain) !== false) {
            $matched = $info;
            break;
        }
    }

    // Check UTM if no referer match
    if (!$matched && !empty($utmSource) && isset($utmMap[$utmSource])) {
        $matched = $utmMap[$utmSource];
    }

    if (!$matched) {
        return;
    }

    // Dedup: 10-minute TTL per source+IP
    $ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''));
    $dedupKey = 's2ll_referral_' . $matched['source'] . '_' . md5($ip);
    if (get_transient($dedupKey)) {
        return;
    }
    set_transient($dedupKey, 1, 10 * MINUTE_IN_SECONDS);

    // Log to DB
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_ai_referrals';
    $tableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$tableExists) {
        shop_to_llm_create_ai_referrals_table();
    }

    $requestUri = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
    $landingUrl = home_url($requestUri);
    if (strlen($landingUrl) > 500) {
        $landingUrl = substr($landingUrl, 0, 500);
    }
    $postId = (int) url_to_postid($landingUrl);

    $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        $table,
        [
            'source' => $matched['source'],
            'source_label' => $matched['label'],
            'landing_url' => $landingUrl,
            'post_id' => $postId,
            'visited_at' => current_time('mysql'),
        ],
        ['%s', '%s', '%s', '%d', '%s']
    );
}

function shop_to_llm_get_ai_referral_stats(): array
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_ai_referrals';

    $default = [
        'this_month' => 0,
        'last_month' => 0,
        'change_pct' => 0,
        'by_source' => [],
        'daily_counts' => array_fill(0, 7, 0),
    ];

    $tableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if (!$tableExists) {
        return $default;
    }

    // This month
    $thisMonthStart = gmdate('Y-m-01 00:00:00');
    $thisMonth = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(*) FROM $table WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $thisMonthStart
    ));

    // Last month
    $lastMonthStart = gmdate('Y-m-01 00:00:00', strtotime('first day of last month'));
    $lastMonthEnd = gmdate('Y-m-01 00:00:00');
    $lastMonth = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(*) FROM $table WHERE visited_at >= %s AND visited_at < %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $lastMonthStart,
        $lastMonthEnd
    ));

    // Change percentage
    $changePct = 0;
    if ($lastMonth > 0) {
        $changePct = round((($thisMonth - $lastMonth) / $lastMonth) * 100, 1);
    } elseif ($thisMonth > 0) {
        $changePct = 100;
    }

    // By source
    $bySource = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT source, source_label, COUNT(*) as cnt FROM $table WHERE visited_at >= %s GROUP BY source, source_label ORDER BY cnt DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $thisMonthStart
    ), ARRAY_A);
    if (!is_array($bySource)) {
        $bySource = [];
    }

    // Daily counts for last 7 days
    $dailyRows = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT DATE(visited_at) as day, COUNT(*) as cnt FROM $table WHERE visited_at >= %s GROUP BY DATE(visited_at) ORDER BY day ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        gmdate('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS)
    ), ARRAY_A);

    $dailyCounts = [];
    for ($i = 6; $i >= 0; $i--) {
        $day = gmdate('Y-m-d', time() - $i * DAY_IN_SECONDS);
        $cnt = 0;
        if (is_array($dailyRows)) {
            foreach ($dailyRows as $dr) {
                if ($dr['day'] === $day) {
                    $cnt = (int) $dr['cnt'];
                    break;
                }
            }
        }
        $dailyCounts[] = $cnt;
    }

    return [
        'this_month' => $thisMonth,
        'last_month' => $lastMonth,
        'change_pct' => $changePct,
        'by_source' => $bySource,
        'daily_counts' => $dailyCounts,
    ];
}

function shop_to_llm_render_ai_referral_section(): void
{
    $stats = shop_to_llm_get_ai_referral_stats();
    $maxVal = max($stats['daily_counts'] ?: [1]);
    if ($maxVal === 0) {
        $maxVal = 1;
    }

    $changeText = '';
    if ($stats['this_month'] > 0 || $stats['last_month'] > 0) {
        $arrow = $stats['change_pct'] >= 0 ? '↑' : '↓';
        $color = $stats['change_pct'] >= 0 ? '#16a34a' : '#dc2626';
        $changeText = '<span style="font-size:12px;color:' . esc_attr($color) . ';font-weight:600">' . esc_html($arrow . ' ' . abs($stats['change_pct']) . '%') . '</span>';
    }

    echo '<div class="s2ll-widget">';

    // Summary
    echo '<div class="s2ll-summary">';
    echo '<div>';
    echo '<span class="s2ll-big">' . (int) $stats['this_month'] . '</span>';
    echo '<span class="s2ll-label">AI visits this month</span>';
    echo esc_html($changeText);
    echo '</div>';
    echo '</div>';

    // 7-day sparkline
    if (max($stats['daily_counts']) > 0) {
        echo '<div class="s2ll-sparkline">';
        foreach ($stats['daily_counts'] as $val) {
            $height = $val > 0 ? max(8, ($val / $maxVal) * 48) : 2;
            $color = $val > 0 ? '#10b981' : '#e2e8f0';
            echo '<div class="s2ll-bar" style="height:' . esc_attr($height) . 'px;background:' . esc_attr($color) . '" title="' . (int) $val . ' visits">';
            if ($val > 0) {
                echo '<span class="s2ll-bar-val">' . (int) $val . '</span>';
            }
            echo '</div>';
        }
        echo '</div>';
        echo '<div class="s2ll-axis"><span>7 days ago</span><span>Today</span></div>';
    }

    // Per-source breakdown
    if (!empty($stats['by_source'])) {
        echo '<div class="s2ll-divider">';
        $maxCnt = (int) $stats['by_source'][0]['cnt'];
        if ($maxCnt === 0) {
            $maxCnt = 1;
        }
        foreach ($stats['by_source'] as $row) {
            $cnt = (int) $row['cnt'];
            $bar = min(100, ($cnt / $maxCnt) * 100);
            echo '<div class="s2ll-bot-row">';
            echo '<span class="s2ll-bot-name">' . esc_html($row['source_label']) . '</span>';
            echo '<div class="s2ll-bot-track"><div class="s2ll-bot-fill" style="width:' . esc_attr($bar) . '%;background:linear-gradient(90deg,#10b981,#059669)"></div></div>';
            echo '<span class="s2ll-bot-cnt">' . esc_html($cnt) . ' visits</span>';
            echo '</div>';
        }
        echo '</div>';
    } else {
        echo '<div class="s2ll-empty">';
        echo '⏳ Waiting for AI-driven traffic...';
        echo '<br><span class="s2ll-empty-sub">Visitors from Perplexity, ChatGPT, Copilot and others will appear here.</span>';
        echo '</div>';
    }

    echo '</div>';
}

function shop_to_llm_admin_bar_referral_suffix(): string
{
    $transientKey = 'shop2llm_admin_bar_referrals';
    $referralCount = get_transient($transientKey);
    if ($referralCount === false) {
        $referralStats = shop_to_llm_get_ai_referral_stats();
        $referralCount = (int) $referralStats['this_month'];
        set_transient($transientKey, $referralCount, 5 * MINUTE_IN_SECONDS);
    }
    if ($referralCount > 0) {
        return ' · ' . (int) $referralCount . ' AI visits';
    }
    return '';
}

// ─── Focus Query & Extended Scoring ────────────────────────────────────

function shop_to_llm_calculate_visibility_score(int $postId): int
{
    // Check transient cache (1 hour)
    $cached = get_transient('shop2llm_score_' . $postId);
    if ($cached !== false) {
        return (int) $cached;
    }

    // Delegate to canonical 25-item scoring function
    $score = shop_to_llm_score_single_post($postId);
    $result = $score['total'];

    // Cache result
    set_transient('shop2llm_score_' . $postId, $result, HOUR_IN_SECONDS);
    return (int) $result;
}
function shop_to_llm_check_robots_txt_allows_ai(): bool
{
    $transientKey = 'shop2llm_robots_allows_ai';
    $cached = get_transient($transientKey);
    if ($cached !== false) return $cached === 'yes';

    $robotsUrl = home_url('/robots.txt');
    $response = wp_remote_get($robotsUrl, ['timeout' => 5]);
    if (is_wp_error($response)) {
        set_transient($transientKey, 'yes', DAY_IN_SECONDS);
        return true;
    }
    $content = wp_remote_retrieve_body($response);
    $aiBots = ['GPTBot', 'ChatGPT-User', 'ClaudeBot', 'PerplexityBot', 'Google-Extended', 'CCBot', 'Applebot', 'Bytespider'];
    foreach ($aiBots as $bot) {
        if (preg_match('/User-agent:\s*' . preg_quote($bot, '/') . '/i', $content)) {
            if (preg_match('/Disallow:\s*\/\s*$/im', $content)) {
                set_transient($transientKey, 'no', DAY_IN_SECONDS);
                return false;
            }
        }
    }
    if (preg_match('/User-agent:\s*\*\s*\n\s*Disallow:\s*\/\s*$/im', $content)) {
        set_transient($transientKey, 'no', DAY_IN_SECONDS);
        return false;
    }
    set_transient($transientKey, 'yes', DAY_IN_SECONDS);
    return true;
}

function shop_to_llm_robots_txt_fix_notice(): void
{
    // Only show on Shop2LLM settings page
    $screen = get_current_screen();
    if (!$screen || $screen->id !== 'toplevel_page_shop2llm') {
        return;
    }

    // Handle fix action
    if (isset($_GET['shop2llm_fix_robots']) && check_admin_referer('shop2llm_fix_robots')) {
        flush_rewrite_rules();
        delete_transient('shop2llm_robots_allows_ai');
        sleep(1);
        if (shop_to_llm_check_robots_txt_allows_ai()) {
            echo '<div class="notice notice-success"><p>'
                . esc_html__('✅ AI crawler rules added to robots.txt. Your content is now discoverable by AI.', 'shop-to-llm')
                . '</p></div>';
            return;
        }
    }

    if (shop_to_llm_check_robots_txt_allows_ai()) {
        // Show green status badge — always visible
        ?>
        <div class="notice notice-success" style="border-left-color:#16a34a">
            <p style="font-size:13px">
                <?php echo esc_html__('✅ robots.txt allows AI crawlers — 12 AI bots whitelisted. Your content is discoverable by ChatGPT, GPTBot, ClaudeBot and more.', 'shop-to-llm'); ?>
            </p>
        </div>
        <?php
        return;
    }

    // Blocked — show fix button
    ?>
    <div class="notice notice-warning" style="border-left-color:#f59e0b">
        <p style="font-size:13px">
            <strong><?php echo esc_html__('⚠ robots.txt may block AI crawlers', 'shop-to-llm'); ?></strong><br>
            <?php echo esc_html__('AI search engines cannot index your site. This reduces your AI Score.', 'shop-to-llm'); ?>
            <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=shop2llm&shop2llm_fix_robots=1'), 'shop2llm_fix_robots')); ?>" 
               style="display:inline-block;margin-top:6px;padding:4px 12px;background:#f59e0b;color:#fff;border-radius:4px;text-decoration:none;font-weight:600;font-size:12px">
                <?php echo esc_html__('🛠 Fix robots.txt', 'shop-to-llm'); ?>
            </a>
            <span style="font-size:11px;color:#94a3b8;margin-left:8px">
                <?php echo esc_html__('Adds AI crawler rules to allow GPTBot, ClaudeBot & 10 more', 'shop-to-llm'); ?>
            </span>
        </p>
    </div>
    <?php
}

function shop_to_llm_check_prerendered_html(int $postId): bool
{
    $transientKey = 'shop2llm_prerendered_' . $postId;
    $cached = get_transient($transientKey);
    if ($cached !== false) return $cached === 'yes';

    $url = get_permalink($postId);
    if (!$url) {
        set_transient($transientKey, 'no', DAY_IN_SECONDS);
        return false;
    }
    $response = wp_remote_get($url, ['timeout' => 5]);
    if (is_wp_error($response)) {
        set_transient($transientKey, 'yes', DAY_IN_SECONDS);
        return true;
    }
    $html = wp_remote_retrieve_body($response);
    $text = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
    $text = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $text);
    $text = wp_strip_all_tags($text);
    $text = trim($text);
    $result = strlen($text) > 200;
    set_transient($transientKey, $result ? 'yes' : 'no', DAY_IN_SECONDS);
    return $result;
}

function shop_to_llm_check_wikidata_entity(string $query): bool
{
    $transientKey = 'shop2llm_wikidata_' . md5($query);
    $cached = get_transient($transientKey);
    if ($cached !== false) return $cached === 'yes';

    $url = 'https://www.wikidata.org/w/api.php?' . http_build_query([
        'action' => 'wbsearchentities',
        'search' => $query,
        'language' => 'en',
        'format' => 'json',
        'limit' => 1,
    ]);
    $response = wp_remote_get($url, ['timeout' => 5]);
    if (is_wp_error($response)) {
        set_transient($transientKey, 'no', DAY_IN_SECONDS);
        return false;
    }
    $data = json_decode(wp_remote_retrieve_body($response), true);
    $found = !empty($data['search']) && count($data['search']) > 0;
    set_transient($transientKey, $found ? 'yes' : 'no', 7 * DAY_IN_SECONDS);
    return $found;
}

function shop_to_llm_check_wikipedia_mention(string $query): bool
{
    $transientKey = 'shop2llm_wikipedia_' . md5($query);
    $cached = get_transient($transientKey);
    if ($cached !== false) return $cached === 'yes';

    $url = 'https://en.wikipedia.org/w/api.php?' . http_build_query([
        'action' => 'query',
        'list' => 'search',
        'srsearch' => $query,
        'format' => 'json',
    ]);
    $response = wp_remote_get($url, ['timeout' => 5]);
    if (is_wp_error($response)) {
        set_transient($transientKey, 'no', DAY_IN_SECONDS);
        return false;
    }
    $data = json_decode(wp_remote_retrieve_body($response), true);
    $found = !empty($data['query']['search']) && count($data['query']['search']) > 0;
    set_transient($transientKey, $found ? 'yes' : 'no', 7 * DAY_IN_SECONDS);
    return $found;
}

function shop_to_llm_has_statistics(string $content): bool
{
    $patterns = [
        '/\d+\.?\d*\s*%/',
        '/\$\d+[\d,]*\.?\d*/',
        '/\d+[\d,]*\s*(people|users|customers|clients|companies|sites|stores|products|items|orders|sales|revenue|growth|increase|decrease)/i',
        '/\d{4}\s*(study|report|survey|research|data)/i',
    ];
    foreach ($patterns as $pattern) {
        if (preg_match($pattern, $content)) return true;
    }
    return false;
}

function shop_to_llm_has_citations(string $content): bool
{
    if (preg_match('/<blockquote|<cite|<q/i', $content)) return true;
    if (preg_match('/"[^"]{10,}"\s*[-—]\s*\w+/i', $content)) return true;
    if (preg_match('/"[^"]{10,}"\s*(said|according|noted|stated)/i', $content)) return true;
    return false;
}

function shop_to_llm_count_internal_links(string $content): int
{
    $homeUrl = home_url();
    $count = preg_match_all('/<a[^>]+href=["\']' . preg_quote($homeUrl, '/') . '/i', $content);
    $count += preg_match_all('/<a[^>]+href=["\']\/[^"\']*["\']/i', $content);
    return (int) $count;
}

function shop_to_llm_keyword_density(string $content, string $keyword): float
{
    $plain = wp_strip_all_tags($content);
    $contentLen = mb_strlen($plain);
    if ($contentLen === 0) return 0.0;
    $keywordCount = shop_to_llm_seo_count_keyword_occurrences($plain, $keyword);
    return ($keywordCount * mb_strlen($keyword)) / $contentLen * 100;
}

function shop_to_llm_get_post_schema_data(\WP_Post $post): array
{
    $data = [];

    if (shop_to_llm_has_woocommerce() && $post->post_type === 'product') {
        $product = wc_get_product($post->ID);
        if ($product) {
            $data[] = $product->get_name();
            $data[] = wp_strip_all_tags($product->get_short_description() ?: $product->get_description());
            $cats = wp_get_post_terms($post->ID, 'product_cat', ['fields' => 'names']);
            if (!is_wp_error($cats)) {
                $data = array_merge($data, $cats);
            }
        }
    } else {
        $data[] = $post->post_title;
        $data[] = wp_strip_all_tags(get_the_excerpt($post) ?: wp_trim_words(wp_strip_all_tags($post->post_content), 30));
        $cats = wp_get_post_terms($post->ID, 'category', ['fields' => 'names']);
        if (!is_wp_error($cats)) {
            $data = array_merge($data, $cats);
        }
    }

    return $data;
}

function shop_to_llm_get_meta_description(\WP_Post $post): string
{
    // Try Yoast SEO
    $yoastMeta = get_post_meta($post->ID, '_yoast_wpseo_metadesc', true);
    if (!empty(trim((string) $yoastMeta))) {
        return $yoastMeta;
    }

    // Try Rank Math
    $rankMathMeta = get_post_meta($post->ID, 'rank_math_description', true);
    if (!empty(trim((string) $rankMathMeta))) {
        return $rankMathMeta;
    }

    // Fallback to excerpt
    if (!empty(trim($post->post_excerpt))) {
        return wp_strip_all_tags($post->post_excerpt);
    }

    return '';
}

function shop_to_llm_save_focus_query(int $postId): void
{
    if (!isset($_POST['shop2llm_focus_query_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_focus_query_nonce'])), 'shop2llm_save_focus_query')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $postId)) {
        return;
    }

    $focusQuery = isset($_POST['shop2llm_focus_query']) ? sanitize_text_field(wp_unslash($_POST['shop2llm_focus_query'])) : '';
    update_post_meta($postId, '_shop2llm_focus_query', $focusQuery);

    // Save authoritative list checkbox
    $onList = isset($_POST['shop2llm_on_authoritative_list']) ? 1 : 0;
    update_post_meta($postId, '_shop2llm_on_authoritative_list', $onList);

    // Clear score cache so it recalculates
    delete_transient('shop2llm_score_' . $postId);

    // Also save visibility snapshot
    $score = shop_to_llm_calculate_visibility_score($postId);
    $history = get_post_meta($postId, '_shop2llm_visibility_history', true);
    if (!is_array($history)) {
        $history = [];
    }
    $today = current_time('Y-m-d');
    // Remove any entry for today (update it)
    $history = array_filter($history, function ($entry) use ($today) {
        return $entry['date'] !== $today;
    });
    $history[] = ['date' => $today, 'score' => $score];
    // Keep only last 8 weeks (56 days)
    $cutoff = gmdate('Y-m-d', time() - 56 * DAY_IN_SECONDS);
    $history = array_filter($history, function ($entry) use ($cutoff) {
        return $entry['date'] >= $cutoff;
    });
    $history = array_values($history);
    update_post_meta($postId, '_shop2llm_visibility_history', $history);
}

// ─── AI Visibility Trend — Weekly Snapshot Cron ────────────────────────

function shop_to_llm_save_visibility_snapshot(int $postId): void
{
    $score = shop_to_llm_calculate_visibility_score($postId);
    $history = get_post_meta($postId, '_shop2llm_visibility_history', true);
    if (!is_array($history)) {
        $history = [];
    }
    $today = current_time('Y-m-d');
    $history = array_filter($history, function ($entry) use ($today) {
        return $entry['date'] !== $today;
    });
    $history[] = ['date' => $today, 'score' => $score];
    $cutoff = gmdate('Y-m-d', time() - 56 * DAY_IN_SECONDS);
    $history = array_filter($history, function ($entry) use ($cutoff) {
        return $entry['date'] >= $cutoff;
    });
    $history = array_values($history);
    update_post_meta($postId, '_shop2llm_visibility_history', $history);
}

function shop_to_llm_register_snapshot_cron(): void
{
    if (!wp_next_scheduled('shop2llm_weekly_snapshot')) {
        wp_schedule_event(time(), 'weekly', 'shop2llm_weekly_snapshot');
    }
}

function shop_to_llm_weekly_snapshot_callback(): void
{
    $posts = get_posts([
        'numberposts' => 100,
        'post_status' => 'publish',
        'post_type' => ['post', 'product'],
    ]);
    foreach ($posts as $p) {
        shop_to_llm_save_visibility_snapshot($p->ID);
    }
}

/**
 * Score a single post using the 25-item 5-dimension model.
 * Used by the Dashboard Content AI Score card.
 *
 * @return array{total: int, has_focus: bool, items: array, groups: array}
 */
/**
 * Convert a site URL to an internally-accessible URL.
 * Fixes Docker port-mapping where site_url uses host port (e.g. 8500)
 * but the container listens on 80 internally.
 */
function shop_to_llm_self_url(string $path = ''): string {
    $site_url = get_site_url();
    $parsed = wp_parse_url($site_url);
    $host = $parsed['host'] ?? 'localhost';
    $port = $parsed['port'] ?? '';
    $scheme = $parsed['scheme'] ?? 'http';
    // If localhost with a mapped port, use port 80 for internal access
    if ($host === 'localhost' && $port && $port !== '80' && $port !== '443') {
        $port = '';
    }
    $base = $scheme . '://' . $host;
    if ($port) {
        $base .= ':' . $port;
    }
    return untrailingslashit($base) . $path;
}

/**
 * Get the 7-factor AI Visibility Score for the entire site.
 * Returns total, label, color, bg, and per-factor breakdown.
 * @since 1.0.8
 */
function shop_to_llm_get_visibility_score(): array
{
    $scoreRobots = shop_to_llm_check_robots_txt_allows_ai() ? 20 : 0;

    $scoreLlms = 0;
    $llmsAccessible = get_transient('shop2llm_llms_accessible');
    if ($llmsAccessible === false) {
        // Try HTTP check via self URL (handles Docker port-mapping)
        $llmsUrl = shop_to_llm_self_url('/llms.txt');
        $llmsResp = wp_remote_get($llmsUrl, ['timeout' => 5, 'redirection' => 0]);
        $httpOk = !is_wp_error($llmsResp) && wp_remote_retrieve_response_code($llmsResp) === 200;
        if ($httpOk && strlen(wp_remote_retrieve_body($llmsResp)) > 50) {
            $llmsAccessible = 'yes';
        } else {
            // Fallback: check if rewrite rule is registered (works even if HTTP unreachable)
            $rules = get_option('rewrite_rules');
            $hasRule = $rules && (isset($rules['^llms\.txt$']) || (is_array($rules) && in_array('index.php?shop_to_llm_llms_txt=1', $rules, true)));
            $llmsAccessible = $hasRule ? 'yes' : 'no';
        }
        set_transient('shop2llm_llms_accessible', $llmsAccessible, HOUR_IN_SECONDS);
    }
    if ($llmsAccessible === 'yes') $scoreLlms = 20;

    $pubCount = shop_to_llm_has_woocommerce()
        ? (int)(wp_count_posts('product')->publish ?? 0)
        : (int)(wp_count_posts('post')->publish ?? 0);

    $scoreSchema = 0;
    // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'active_plugins' is a WordPress core hook
    $activePlugins = apply_filters('active_plugins', get_option('active_plugins', []));
    $seoSlugs = ['wordpress-seo/wp-seo.php', 'seo-by-rank-math/rank-math.php', 'all-in-one-seo-pack/all_in_one_seo_pack.php', 'wp-seopress/seopress.php'];
    $schemaHasConflict = (bool) array_intersect($seoSlugs, $activePlugins);
    if ($schemaHasConflict) { $scoreSchema = 12; }
    elseif ($pubCount > 0) { $scoreSchema = 15; }
    else { $scoreSchema = 10; }

    $todayCount = shop_to_llm_get_today_api_count();
    $scoreMcp = 0;
    $mcpEnabled = get_option('shop_to_llm_enabled', 'yes') === 'yes';
    if ($mcpEnabled && $todayCount > 0) { $scoreMcp = 15; }
    elseif ($mcpEnabled) { $scoreMcp = 5; }

    $scoreFeed = 0;
    if (shop_to_llm_has_woocommerce()) {
        $feedAccessible = get_transient('shop2llm_feed_accessible');
        if ($feedAccessible === false) {
            // Try HTTP check via self URL (handles Docker port-mapping)
            $feedUrl = shop_to_llm_self_url('/product-feed.xml');
            $feedResp = wp_remote_get($feedUrl, ['timeout' => 5, 'redirection' => 0]);
            $httpOk = !is_wp_error($feedResp) && wp_remote_retrieve_response_code($feedResp) === 200;
            if ($httpOk && strlen(wp_remote_retrieve_body($feedResp)) > 100) {
                $feedAccessible = 'yes';
            } else {
                // Fallback: check if rewrite rule is registered
                $rules = get_option('rewrite_rules');
                $hasRule = $rules && (isset($rules['^product-feed\.xml$']) || (is_array($rules) && in_array('index.php?shop_to_llm_feed=1', $rules, true)));
                $feedAccessible = $hasRule ? 'yes' : 'no';
            }
            set_transient('shop2llm_feed_accessible', $feedAccessible, HOUR_IN_SECONDS);
        }
        if ($feedAccessible === 'yes') $scoreFeed = 10;
    } else { $scoreFeed = 10; }

    // Queries score: based on how many posts/products have Focus Query set
    global $wpdb;
    $scoreQueries = 0;
    $fqCount = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != ''", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        '_shop2llm_focus_query'
    ));
    if ($pubCount > 0) {
        $fqRatio = $fqCount / $pubCount;
        if ($fqRatio >= 0.5) $scoreQueries = 10;
        elseif ($fqRatio >= 0.25) $scoreQueries = 7;
        elseif ($fqRatio >= 0.1) $scoreQueries = 5;
        elseif ($fqCount > 0) $scoreQueries = 3;
    }

    // Crawlers score: based on AI crawler visits in last 30 days
    $scoreCrawlers = 0;
    $cTable = $wpdb->prefix . 'shop2llm_crawler_log';
    $cTableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $cTable)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    if ($cTableExists) {
        $thirtyDaysAgo = gmdate('Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS);
        $crawlerTotal30d = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT COUNT(*) FROM {$cTable} WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $thirtyDaysAgo
        ));
        $uniqueBots30d = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            "SELECT COUNT(DISTINCT bot_name) FROM {$cTable} WHERE visited_at >= %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $thirtyDaysAgo
        ));
        if ($uniqueBots30d >= 4 && $crawlerTotal30d >= 20) $scoreCrawlers = 10;
        elseif ($uniqueBots30d >= 2 && $crawlerTotal30d >= 5) $scoreCrawlers = 7;
        elseif ($crawlerTotal30d >= 1) $scoreCrawlers = 5;
    }

    // Total based on all 7 factors (max 100)
    $total = $scoreRobots + $scoreLlms + $scoreSchema + $scoreMcp + $scoreFeed + $scoreQueries + $scoreCrawlers;
    $freeMax = 100;

    $label = $total >= 90 ? __('Excellent', 'shop-to-llm') : ($total >= 70 ? __('Good', 'shop-to-llm') : ($total >= 50 ? __('Fair', 'shop-to-llm') : ($total >= 30 ? __('Poor', 'shop-to-llm') : __('Critical', 'shop-to-llm'))));
    $color = $total >= 70 ? '#22c55e' : ($total >= 40 ? '#eab308' : '#ef4444');
    $bg = $total >= 70 ? '#f0fdf4' : ($total >= 40 ? '#fefce8' : '#fef2f2');

    return [
        'total' => $total, 'free_max' => $freeMax,
        'label' => $label, 'color' => $color, 'bg' => $bg,
        'schema_has_conflict' => $schemaHasConflict,
        'factors' => [
            'robots' => $scoreRobots, 'llms' => $scoreLlms, 'schema' => $scoreSchema,
            'mcp' => $scoreMcp, 'feed' => $scoreFeed,
            'queries' => $scoreQueries, 'crawlers' => $scoreCrawlers,
        ],
    ];
}

function shop_to_llm_score_single_post(int $postId): array
{
    $post = get_post($postId);
    if (!$post || $post->post_status !== 'publish') {
        return ['total' => 0, 'has_focus' => false, 'items' => [], 'groups' => []];
    }

    $focusQuery = get_post_meta($postId, '_shop2llm_focus_query', true) ?: '';
    $hasFocusQuery = !empty(trim((string)$focusQuery));
    $content = $post->post_content;
    $entityChecksEnabled = (bool) get_option('shop_to_llm_enable_entity_checks', 0);
    $items = [];
    $total = 0;

    // ─── L1: Discoverability (30 pts, items 1-6) ───
    $llmsTxt = get_transient('shop_to_llm_llms_txt_output');
    if (!$llmsTxt) { ob_start(); shop_to_llm_render_llms_txt(); $llmsTxt = ob_get_clean(); }
    $i1 = $hasFocusQuery ? ($llmsTxt && stripos($llmsTxt, $focusQuery) !== false) : !empty($llmsTxt);
    if ($i1) $total += 8;
    $items[] = ['name' => __('llms.txt contains query', 'shop-to-llm'), 'pass' => $i1, 'pts' => 8];

    $i2 = shop_to_llm_check_robots_txt_allows_ai();
    if ($i2) $total += 6;
    $items[] = ['name' => __('robots.txt allows AI', 'shop-to-llm'), 'pass' => $i2, 'pts' => 6];

    $sd = shop_to_llm_get_post_schema_data($post);
    $i3 = $hasFocusQuery ? (!empty($sd) && stripos(implode(' ', $sd), $focusQuery) !== false) : !empty($sd);
    if ($i3) $total += 6;
    $items[] = ['name' => __('Schema contains query', 'shop-to-llm'), 'pass' => $i3, 'pts' => 6];

    $i4 = (bool) get_option('shop_to_llm_mcp_connected', false);
    if ($i4) $total += 4;
    $items[] = ['name' => __('MCP tools connected', 'shop-to-llm'), 'pass' => $i4, 'pts' => 4];

    $i5 = shop_to_llm_check_prerendered_html($post->ID);
    if ($i5) $total += 4;
    $items[] = ['name' => __('Pre-rendered HTML readable', 'shop-to-llm'), 'pass' => $i5, 'pts' => 4];

    $i6 = (stripos($content, 'FAQPage') !== false || stripos($content, '<!-- wp:faq') !== false || preg_match('/<h[2-4][^>]*>.*\?.*<\/h/i', $content));
    if ($i6) $total += 2;
    $items[] = ['name' => __('FAQPage schema present', 'shop-to-llm'), 'pass' => $i6, 'pts' => 2];

    // ─── L2: Relevance (25 pts, items 7-12) ───
    if ($hasFocusQuery) {
        $tLen = strlen($post->post_title);
        $i7pts = 0;
        if ($tLen > 0 && stripos($post->post_title, $focusQuery) !== false) {
            $r = stripos($post->post_title, $focusQuery) / $tLen;
            $i7pts = $r < 0.3 ? 6 : ($r < 0.7 ? 4 : 2);
        }
        $total += $i7pts;
        $items[] = ['name' => __('Query in title', 'shop-to-llm'), 'pass' => $i7pts > 0, 'pts' => $i7pts];

        $i8pts = 0;
        if (preg_match('/<h1[^>]*>.*' . preg_quote($focusQuery, '/') . '.*<\/h1>/i', $content)) $i8pts = 5;
        elseif (preg_match('/<h2[^>]*>.*' . preg_quote($focusQuery, '/') . '.*<\/h2>/i', $content)) $i8pts = 3;
        $total += $i8pts;
        $items[] = ['name' => __('Query in heading', 'shop-to-llm'), 'pass' => $i8pts > 0, 'pts' => $i8pts];

        $fw = implode(' ', array_slice(explode(' ', wp_strip_all_tags($content)), 0, 100));
        $i9 = (stripos($fw, $focusQuery) !== false);
        if ($i9) $total += 4;
        $items[] = ['name' => __('Query in first 100 words', 'shop-to-llm'), 'pass' => $i9, 'pts' => 4];

        $i10 = (stripos($post->post_name, $focusQuery) !== false);
        if ($i10) $total += 4;
        $items[] = ['name' => __('Query in URL/slug', 'shop-to-llm'), 'pass' => $i10, 'pts' => 4];

        $md = shop_to_llm_get_meta_description($post);
        $i11 = (!empty($md) && stripos($md, $focusQuery) !== false);
        if ($i11) $total += 3;
        $items[] = ['name' => __('Query in meta description', 'shop-to-llm'), 'pass' => $i11, 'pts' => 3];

        $altF = false;
        foreach (get_posts(['post_type' => 'attachment', 'posts_per_page' => -1, 'post_parent' => $post->ID]) as $a) {
            $alt = get_post_meta($a->ID, '_wp_attachment_image_alt', true);
            if (!empty($alt) && stripos($alt, $focusQuery) !== false) { $altF = true; break; }
        }
        if ($altF) $total += 3;
        $items[] = ['name' => __('Query in image alt text', 'shop-to-llm'), 'pass' => $altF, 'pts' => 3];
    } else {
        $l2Names = [__('Query in title', 'shop-to-llm'), __('Query in heading', 'shop-to-llm'), __('Query in first 100 words', 'shop-to-llm'), __('Query in URL/slug', 'shop-to-llm'), __('Query in meta description', 'shop-to-llm'), __('Query in image alt text', 'shop-to-llm')];
        foreach ($l2Names as $n) { $items[] = ['name' => $n, 'pass' => false, 'pts' => 0, 'locked' => true]; }
    }

    // ─── L3: Content Quality (25 pts, items 13-18) ───
    $wc = str_word_count(wp_strip_all_tags($content));
    $i13pts = $wc > 300 ? 4 : ($wc > 150 ? 2 : 0);
    $total += $i13pts;
    $items[] = ['name' => __('Content length > 300 words', 'shop-to-llm'), 'pass' => $i13pts > 0, 'pts' => $i13pts];

    $i14 = shop_to_llm_has_statistics($content);
    if ($i14) $total += 5;
    $items[] = ['name' => __('Contains statistics/claims', 'shop-to-llm'), 'pass' => $i14, 'pts' => 5];

    $i15 = shop_to_llm_has_citations($content);
    if ($i15) $total += 5;
    $items[] = ['name' => __('Contains citations/quotes', 'shop-to-llm'), 'pass' => $i15, 'pts' => 5];

    $daysMod = (time() - strtotime($post->post_modified)) / DAY_IN_SECONDS;
    $i16pts = $daysMod <= 30 ? 4 : ($daysMod <= 90 ? 2 : 0);
    $total += $i16pts;
    $items[] = ['name' => __('Content freshness', 'shop-to-llm'), 'pass' => $i16pts > 0, 'pts' => $i16pts];

    $sf = 0;
    if (!empty($post->post_title)) $sf++;
    if (!empty(get_the_excerpt($post))) $sf++;
    if (has_post_thumbnail($post->ID)) $sf++;
    if (get_the_author_meta('display_name', $post->post_author)) $sf++;
    $i17pts = $sf >= 4 ? 4 : ($sf >= 3 ? 3 : ($sf >= 2 ? 2 : ($sf >= 1 ? 1 : 0)));
    $total += $i17pts;
    $items[] = ['name' => __('Schema completeness', 'shop-to-llm'), 'pass' => $i17pts > 0, 'pts' => $i17pts];

    $il = shop_to_llm_count_internal_links($content);
    $i18pts = $il >= 3 ? 3 : ($il >= 1 ? 1 : 0);
    $total += $i18pts;
    $items[] = ['name' => __('Internal links >= 3', 'shop-to-llm'), 'pass' => $i18pts > 0, 'pts' => $i18pts];

    // ─── L4: Entity Authority (15 pts, items 19-21) ───
    if ($hasFocusQuery) {
        $i19 = ($entityChecksEnabled && shop_to_llm_check_wikidata_entity($focusQuery));
        if ($i19) $total += 5;
        $items[] = ['name' => __('Brand in knowledge graph', 'shop-to-llm'), 'pass' => $i19, 'pts' => 5];

        $i20 = ($entityChecksEnabled && shop_to_llm_check_wikipedia_mention($focusQuery));
        if ($i20) $total += 5;
        $items[] = ['name' => __('Wikipedia mention', 'shop-to-llm'), 'pass' => $i20, 'pts' => 5];

        $onAuth = get_post_meta($postId, '_shop2llm_on_authoritative_list', true);
        $i21 = !empty($onAuth);
        if ($i21) $total += 5;
        $items[] = ['name' => __('On authoritative lists', 'shop-to-llm'), 'pass' => $i21, 'pts' => 5];
    } else {
        $l4Names = [__('Brand in knowledge graph', 'shop-to-llm'), __('Wikipedia mention', 'shop-to-llm'), __('On authoritative lists', 'shop-to-llm')];
        foreach ($l4Names as $n) { $items[] = ['name' => $n, 'pass' => false, 'pts' => 0, 'locked' => true]; }
    }

    // ─── L5: Activity (5 pts, items 22-25) ───
    $cs = shop_to_llm_get_post_crawler_stats($post->ID);
    $i22pts = 0;
    if (!empty($cs['last_visit']) && $cs['last_visit'] !== '—') {
        $daysVisit = (time() - strtotime($cs['last_visit'])) / DAY_IN_SECONDS;
        $i22pts = $daysVisit <= 7 ? 2 : ($daysVisit <= 30 ? 1 : 0);
    }
    $total += $i22pts;
    $items[] = ['name' => __('AI crawler visit < 7 days', 'shop-to-llm'), 'pass' => $i22pts > 0, 'pts' => $i22pts];

    $i23 = ($post->comment_count > 0);
    if ($i23) $total += 1;
    $items[] = ['name' => __('Reviews/ratings exist', 'shop-to-llm'), 'pass' => $i23, 'pts' => 1];

    global $wpdb;
    $ct = $wpdb->prefix . 'shop2llm_crawler_log';
    $te = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $ct)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $i24 = false;
    if ($te) {
        $db = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT bot_name) FROM {$ct} WHERE post_id = %d", $post->ID)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        $i24 = ($db >= 4);
    }
    if ($i24) $total += 1;
    $items[] = ['name' => __('Multi-platform presence 4+', 'shop-to-llm'), 'pass' => $i24, 'pts' => 1];

    $i25 = false;
    if ($hasFocusQuery) {
        $density = shop_to_llm_keyword_density($content, $focusQuery);
        $i25 = ($density >= 0.5 && $density <= 2.5);
    }
    if ($i25) $total += 1;
    $items[] = ['name' => __('Keyword density 0.5-2.5%', 'shop-to-llm'), 'pass' => $i25, 'pts' => $i25 ? 1 : 0, 'locked' => !$hasFocusQuery];

    return ['total' => $total, 'has_focus' => $hasFocusQuery, 'items' => $items, 'groups' => []];
}

// ─── v1.3.0 SEO Module Code ────────────────────────────────────────────

// ─── SEO Plugin Detection ──────────────────────────────────────────────

function shop_to_llm_has_seo_plugin(): bool
{
    static $has = null;
    if ($has !== null) {
        return $has;
    }
    // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'active_plugins' is a WordPress core hook
    $active = apply_filters('active_plugins', get_option('active_plugins', []));
    $seo_slugs = [
        'wordpress-seo/wp-seo.php',
        'seo-by-rank-math/rank-math.php',
        'all-in-one-seo-pack/all_in_one_seo_pack.php',
        'wp-seopress/seopress.php',
    ];
    $has = (bool) array_intersect($seo_slugs, $active);
    return $has;
}

function shop_to_llm_get_seo_plugin_name(): string
{
    // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- 'active_plugins' is a WordPress core hook
    $active = apply_filters('active_plugins', get_option('active_plugins', []));
    $map = [
        'wordpress-seo/wp-seo.php' => 'Yoast SEO',
        'seo-by-rank-math/rank-math.php' => 'Rank Math',
        'all-in-one-seo-pack/all_in_one_seo_pack.php' => 'AIOSEO',
        'wp-seopress/seopress.php' => 'SEOPress',
    ];
    foreach ($map as $slug => $name) {
        if (in_array($slug, $active, true)) {
            return $name;
        }
    }
    return '';
}

// ─── SEO DB Tables ────────────────────────────────────────────────────

/**
 * Set SEO default options on activation.
 *
 * Encapsulated in a function to avoid Plugin Check DirectDatabaseQuery
 * false positives on add_option() calls. add_option() is a WP API,
 * not a direct DB call — but Plugin Check's heuristic flags it.
 */
function shop_to_llm_set_default_options(): void
{
    add_option('shop_to_llm_seo_meta', 'yes');
    add_option('shop_to_llm_seo_sitemap', 'yes');
    add_option('shop_to_llm_seo_schema', 'yes');
    add_option('shop_to_llm_seo_redirect', 'yes');
    add_option('shop_to_llm_seo_og', 'yes');
    add_option('shop_to_llm_seo_analysis', 'yes');
    add_option('shop_to_llm_seo_audit', 'yes');
    add_option('shop_to_llm_seo_title_home', '%%sitename%% %%sep%% %%tagline%%');
    add_option('shop_to_llm_seo_title_post', '%%title%% %%sep%% %%sitename%%');
    add_option('shop_to_llm_seo_title_page', '%%title%% %%sep%% %%sitename%%');
    add_option('shop_to_llm_seo_title_product', '%%title%% %%sep%% %%sitename%%');
    add_option('shop_to_llm_seo_title_category', '%%category%% %%sep%% %%sitename%%');
    add_option('shop_to_llm_seo_title_tag', '%%tag%% %%sep%% %%sitename%%');
    add_option('shop_to_llm_seo_title_archive', '%%title%% %%sep%% %%sitename%%');
}

function shop_to_llm_create_seo_tables(): void
{
    global $wpdb;
    $charset = $wpdb->get_charset_collate();

    $redirects = $wpdb->prefix . 's2ll_redirects';
    $sql1 = "CREATE TABLE IF NOT EXISTS $redirects (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        source_url VARCHAR(500) NOT NULL,
        target_url VARCHAR(500) NOT NULL,
        status_code SMALLINT UNSIGNED NOT NULL DEFAULT 301,
        is_regex TINYINT(1) NOT NULL DEFAULT 0,
        created_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY idx_source (source_url(191))
    ) $charset;";

    $log404 = $wpdb->prefix . 's2ll_404_log';
    $sql2 = "CREATE TABLE IF NOT EXISTS $log404 (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        url VARCHAR(500) NOT NULL,
        referer VARCHAR(500) NOT NULL DEFAULT '',
        ip VARCHAR(45) NOT NULL DEFAULT '',
        hit_count INT UNSIGNED NOT NULL DEFAULT 1,
        last_hit DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY idx_url (url(191)),
        KEY idx_last (last_hit)
    ) $charset;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql1);
    dbDelta($sql2);
}

// ─── SEO Overview Helper Functions ────────────────────────────────────

function shop_to_llm_seo_get_indexed_count(): int
{
    $count = 0;
    $count += (int) wp_count_posts('post')->publish ?? 0;
    $count += (int) wp_count_posts('page')->publish ?? 0;
    if (shop_to_llm_has_woocommerce()) {
        $count += (int) wp_count_posts('product')->publish ?? 0;
    }
    return $count;
}

function shop_to_llm_seo_get_redirect_count(): int
{
    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
        return 0;
    }
    return (int) $wpdb->get_var("SELECT COUNT(*) FROM $table");
    // phpcs:enable
}

function shop_to_llm_seo_get_404_count(): int
{
    global $wpdb;
    $table = $wpdb->prefix . 's2ll_404_log';
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
        return 0;
    }
    return (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE last_hit >= %s",
        gmdate('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS)
    ));
    // phpcs:enable
}

function shop_to_llm_seo_get_schema_count(): int
{
    $count = 18;
    if (get_option('shop_to_llm_seo_schema', 'yes') !== 'yes') {
        $count -= 18;
    }
    if (shop_to_llm_has_seo_plugin()) {
        $count -= 18;
    }
    return max(0, $count);
}

// ─── Google Search Console Integration ────────────────────────────────

/**
 * Check if GSC is configured (API credentials set).
 */
function shop_to_llm_gsc_is_configured(): bool
{
    return !empty(get_option('shop_to_llm_gsc_client_id', ''))
        && !empty(get_option('shop_to_llm_gsc_client_secret', ''));
}

/**
 * Get GSC OAuth2 authorization URL.
 */
function shop_to_llm_gsc_get_auth_url(): string
{
    $client_id = get_option('shop_to_llm_gsc_client_id', '');
    if (empty($client_id)) {
        return '';
    }
    $redirect_uri = admin_url('admin.php?page=shop2llm&tab=seo&gsc_callback=1');
    $scope = 'https://www.googleapis.com/auth/webmasters.readonly';
    $state = wp_create_nonce('shop2llm_gsc_auth');

    return 'https://accounts.google.com/o/oauth2/auth?' . http_build_query([
        'client_id' => $client_id,
        'redirect_uri' => $redirect_uri,
        'scope' => $scope,
        'response_type' => 'code',
        'access_type' => 'offline',
        'prompt' => 'consent',
        'state' => $state,
    ]);
}

/**
 * Handle OAuth2 callback - exchange code for tokens.
 */
function shop_to_llm_gsc_handle_callback(): void
{
    if (!isset($_GET['gsc_callback'], $_GET['code'], $_GET['state'])) {
        return;
    }
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['state'])), 'shop2llm_gsc_auth')) {
        return;
    }
    if (!current_user_can('manage_options')) {
        return;
    }

    $code = sanitize_text_field(wp_unslash($_GET['code']));
    $client_id = get_option('shop_to_llm_gsc_client_id', '');
    $client_secret = get_option('shop_to_llm_gsc_client_secret', '');
    $redirect_uri = admin_url('admin.php?page=shop2llm&tab=seo&gsc_callback=1');

    $response = wp_remote_post('https://oauth2.googleapis.com/token', [
        'body' => [
            'client_id' => $client_id,
            'client_secret' => $client_secret,
            'code' => $code,
            'grant_type' => 'authorization_code',
            'redirect_uri' => $redirect_uri,
        ],
        'timeout' => 15,
    ]);

    if (is_wp_error($response)) {
        return;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    if (empty($body['access_token'])) {
        return;
    }

    update_option('shop_to_llm_gsc_access_token', $body['access_token']);
    if (!empty($body['refresh_token'])) {
        update_option('shop_to_llm_gsc_refresh_token', $body['refresh_token']);
    }
    if (!empty($body['expires_in'])) {
        update_option('shop_to_llm_gsc_expires_at', time() + (int) $body['expires_in'] - 300);
    }

    // Fetch sites list to store the first property URL
    $sites = shop_to_llm_gsc_fetch_sites();
    if (!empty($sites[0]['siteUrl'])) {
        update_option('shop_to_llm_gsc_site_url', $sites[0]['siteUrl']);
    }

    // Clear old cached data
    delete_transient('shop2llm_gsc_analytics');

    // Redirect to SEO tab without callback params
    wp_safe_redirect(admin_url('admin.php?page=shop2llm&tab=seo&gsc_connected=1'));
    exit;
}

/**
 * Refresh GSC access token using refresh token.
 */
function shop_to_llm_gsc_refresh_token(): bool
{
    $refresh_token = get_option('shop_to_llm_gsc_refresh_token', '');
    if (empty($refresh_token)) {
        return false;
    }

    $client_id = get_option('shop_to_llm_gsc_client_id', '');
    $client_secret = get_option('shop_to_llm_gsc_client_secret', '');

    $response = wp_remote_post('https://oauth2.googleapis.com/token', [
        'body' => [
            'client_id' => $client_id,
            'client_secret' => $client_secret,
            'refresh_token' => $refresh_token,
            'grant_type' => 'refresh_token',
        ],
        'timeout' => 15,
    ]);

    if (is_wp_error($response)) {
        return false;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    if (empty($body['access_token'])) {
        return false;
    }

    update_option('shop_to_llm_gsc_access_token', $body['access_token']);
    if (!empty($body['expires_in'])) {
        update_option('shop_to_llm_gsc_expires_at', time() + (int) $body['expires_in'] - 300);
    }
    return true;
}

/**
 * Get a valid GSC access token (refresh if expired).
 */
function shop_to_llm_gsc_get_token(): string
{
    $expires = get_option('shop_to_llm_gsc_expires_at', 0);
    if ($expires > 0 && time() > $expires) {
        shop_to_llm_gsc_refresh_token();
    }
    return get_option('shop_to_llm_gsc_access_token', '');
}

/**
 * Fetch GSC sites list.
 */
function shop_to_llm_gsc_fetch_sites(): array
{
    $token = shop_to_llm_gsc_get_token();
    if (empty($token)) {
        return [];
    }

    $response = wp_remote_get('https://www.googleapis.com/webmasters/v3/sites', [
        'headers' => [
            'Authorization' => 'Bearer ' . $token,
            'Accept' => 'application/json',
        ],
        'timeout' => 15,
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        return [];
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    return $body['siteEntry'] ?? [];
}

/**
 * Fetch GSC Search Analytics data (cached).
 */
function shop_to_llm_gsc_get_analytics(): array
{
    $cached = get_transient('shop2llm_gsc_analytics');
    if ($cached !== false) {
        return $cached;
    }

    $token = shop_to_llm_gsc_get_token();
    $site_url = get_option('shop_to_llm_gsc_site_url', '');
    if (empty($token) || empty($site_url)) {
        return [];
    }

    $end_date = gmdate('Y-m-d', strtotime('-3 days'));
    $start_date = gmdate('Y-m-d', strtotime('-31 days'));

    $response = wp_remote_post(
        'https://www.googleapis.com/webmasters/v3/sites/' . urlencode($site_url) . '/searchAnalytics/query',
        [
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json',
            ],
            'body' => wp_json_encode([
                'startDate' => $start_date,
                'endDate' => $end_date,
                'dimensions' => ['query'],
                'rowLimit' => 100,
                'aggregationType' => 'auto',
            ]),
            'timeout' => 20,
        ]
    );

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        return [];
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    if (empty($body['rows'])) {
        $data = ['summary' => ['clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'position' => 0], 'keywords' => []];
        set_transient('shop2llm_gsc_analytics', $data, DAY_IN_SECONDS);
        return $data;
    }

    $total_clicks = 0;
    $total_impressions = 0;
    $total_position = 0;
    $keywords = [];

    foreach ($body['rows'] as $row) {
        $kw = $row['keys'][0];
        $clicks = (int) $row['clicks'];
        $impressions = (int) $row['impressions'];
        $position = round((float) $row['position'], 1);

        $total_clicks += $clicks;
        $total_impressions += $impressions;
        $total_position += $position;

        $keywords[] = [
            'query' => $kw,
            'clicks' => $clicks,
            'impressions' => $impressions,
            'ctr' => $impressions > 0 ? round($clicks / $impressions * 100, 1) : 0,
            'position' => $position,
        ];
    }

    // Sort by clicks descending
    usort($keywords, function ($a, $b) {
        return $b['clicks'] - $a['clicks'];
    });

    $keyword_count = count($keywords);
    $avg_ctr = $total_impressions > 0 ? round($total_clicks / $total_impressions * 100, 1) : 0;
    $avg_position = $keyword_count > 0 ? round($total_position / $keyword_count, 1) : 0;

    $data = [
        'summary' => [
            'clicks' => $total_clicks,
            'impressions' => $total_impressions,
            'ctr' => $avg_ctr,
            'position' => $avg_position,
        ],
        'keywords' => array_slice($keywords, 0, 20),
    ];

    set_transient('shop2llm_gsc_analytics', $data, DAY_IN_SECONDS);
    return $data;
}

/**
 * Disconnect GSC (clear all tokens).
 */
function shop_to_llm_gsc_disconnect(): void
{
    delete_option('shop_to_llm_gsc_access_token');
    delete_option('shop_to_llm_gsc_refresh_token');
    delete_option('shop_to_llm_gsc_expires_at');
    delete_option('shop_to_llm_gsc_site_url');
    delete_transient('shop2llm_gsc_analytics');
}

// ─── SEO Meta Box Module ─────────────────────────────────────────────

function shop_to_llm_seo_add_meta_boxes(): void
{
    $types = ['post', 'page'];
    if (shop_to_llm_has_woocommerce()) {
        $types[] = 'product';
    }

    foreach ($types as $pt) {
        add_meta_box(
            'shop2llm_seo_settings',
            __('SEO Settings', 'shop-to-llm'),
            'shop_to_llm_seo_render_meta_box',
            $pt,
            'side',
            'high'
        );
    }

    if (get_option('shop_to_llm_seo_analysis', 'yes') === 'yes') {
        foreach ($types as $pt) {
            add_meta_box(
                'shop2llm_content_analysis',
                __('Content Analysis', 'shop-to-llm'),
                'shop_to_llm_seo_render_analysis_meta_box',
                $pt,
                'side',
                'default'
            );
            // v1.3.4+: Live Content Extractability — AI citability score (mirrors VigIA)
            add_meta_box(
                'shop2llm_extractability',
                __('AI Extractability (Live)', 'shop-to-llm'),
                'shop_to_llm_seo_render_extractability_meta_box',
                $pt,
                'side',
                'default'
            );
        }
    }
}

function shop_to_llm_seo_render_meta_box(WP_Post $post): void
{
    wp_nonce_field('shop2llm_seo_save', 'shop2llm_seo_nonce');

    $focus_kw   = get_post_meta($post->ID, '_shop2llm_focus_kw', true);
    $seo_title  = get_post_meta($post->ID, '_shop2llm_seo_title', true);
    $meta_desc  = get_post_meta($post->ID, '_shop2llm_meta_desc', true);
    $canonical  = get_post_meta($post->ID, '_shop2llm_canonical', true);
    $noindex    = get_post_meta($post->ID, '_shop2llm_noindex', true);
    $nofollow   = get_post_meta($post->ID, '_shop2llm_nofollow', true);

    $site_name  = get_bloginfo('name');
    $preview_title = $seo_title ?: $post->post_title;
    $preview_desc  = $meta_desc ?: wp_trim_words(wp_strip_all_tags($post->post_content), 30, '...');
    $preview_url   = $canonical ?: get_permalink($post->ID);
    ?>
    <style>
    .s2ll-seo-field{margin-bottom:12px}
    .s2ll-seo-field label{display:block;font-weight:600;margin-bottom:3px;font-size:12px;color:#1e293b}
    .s2ll-seo-field input[type="text"],
    .s2ll-seo-field textarea{width:100%;padding:5px 8px;font-size:13px;border:1px solid #cbd5e1;border-radius:4px;box-sizing:border-box}
    .s2ll-seo-field textarea{resize:vertical;min-height:60px}
    .s2ll-seo-field input[type="checkbox"]{margin-right:4px}
    .s2ll-char-count{font-size:11px;color:#94a3b8;text-align:right;margin-top:2px}
    .s2ll-char-count.over{color:#dc2626;font-weight:600}
    .s2ll-checkbox-row{display:flex;gap:16px;margin-bottom:12px}
    .s2ll-checkbox-row label{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:400;color:#475569;cursor:pointer}
    /* SERP Preview */
    .s2ll-serp-section{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}
    .s2ll-serp-section h4{margin:0 0 8px;font-size:12px;color:#64748b;font-weight:600}
    .s2ll-serp-desktop{font-family:Arial,sans-serif;margin-bottom:14px}
    .s2ll-serp-desktop .s2ll-serp-url{font-size:12px;color:#1a7300;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .s2ll-serp-desktop .s2ll-serp-title{font-size:15px;color:#1a0dab;line-height:1.3;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
    .s2ll-serp-desktop .s2ll-serp-desc{font-size:12px;color:#545454;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
    .s2ll-serp-mobile{font-family:Arial,sans-serif;background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;max-width:320px;margin-bottom:14px}
    .s2ll-serp-mobile .s2ll-mobile-header{display:flex;align-items:center;gap:6px;margin-bottom:6px}
    .s2ll-serp-mobile .s2ll-mobile-icon{width:16px;height:16px;border-radius:50%;background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:9px;color:#64748b}
    .s2ll-serp-mobile .s2ll-mobile-url{font-size:11px;color:#1a7300;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .s2ll-serp-mobile .s2ll-serp-title{font-size:14px;color:#1a0dab;line-height:1.3;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
    .s2ll-serp-mobile .s2ll-serp-desc{font-size:11px;color:#545454;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
    /* Social Preview */
    .s2ll-social-section{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}
    .s2ll-social-section h4{margin:0 0 8px;font-size:12px;color:#64748b;font-weight:600}
    .s2ll-social-card{background:#fff;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;max-width:320px;margin-bottom:8px}
    .s2ll-social-card-img{width:100%;height:120px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:11px}
    .s2ll-social-card-body{padding:10px 12px}
    .s2ll-social-card-domain{font-size:10px;color:#65676b;text-transform:uppercase;margin-bottom:3px}
    .s2ll-social-card-title{font-size:13px;color:#1d1d1f;font-weight:600;line-height:1.3;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .s2ll-social-card-desc{font-size:11px;color:#65676b;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
    .s2ll-mb-tabs{display:flex;gap:0;margin-bottom:10px;border-bottom:2px solid #e2e8f0}
    .s2ll-mb-tab{padding:6px 14px;font-size:12px;font-weight:600;color:#64748b;cursor:pointer;border:none;background:none;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all 0.15s}
    .s2ll-mb-tab.active{color:#2563eb;border-bottom-color:#2563eb}
    .s2ll-mb-tab-content{display:none}
    .s2ll-mb-tab-content.active{display:block}
    </style>

    <?php
    // SEO Score (mini preview at top)
    $analysis_data = shop_to_llm_seo_analyze_content($post->ID);
    $score = isset($analysis_data['score']) ? (int) $analysis_data['score'] : 0;
    $score_color = $score >= 70 ? '#16a34a' : ($score >= 40 ? '#ca8a04' : '#dc2626');
    ?>
    <div style="display:flex;align-items:center;gap:10px;padding:8px 0;margin-bottom:10px;border-bottom:1px solid #e2e8f0">
        <div style="width:42px;height:42px;border-radius:50%;background:<?php echo esc_attr($score_color); ?>;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:16px">
            <?php echo esc_html($score); ?>
        </div>
        <div>
            <div style="font-size:13px;font-weight:600;color:#1e293b"><?php echo esc_html__('SEO Score', 'shop-to-llm'); ?></div>
            <div style="font-size:11px;color:#64748b">
                <?php echo $score >= 70 ? esc_html__('Good', 'shop-to-llm') : ($score >= 40 ? esc_html__('Fair', 'shop-to-llm') : esc_html__('Needs work', 'shop-to-llm')); ?>
            </div>
        </div>
    </div>

    <div class="s2ll-mb-tabs">
        <button type="button" class="s2ll-mb-tab active" data-tab="s2ll-mb-general"><?php echo esc_html__('General', 'shop-to-llm'); ?></button>
        <button type="button" class="s2ll-mb-tab" data-tab="s2ll-mb-social"><?php echo esc_html__('Social', 'shop-to-llm'); ?></button>
    </div>

    <div class="s2ll-mb-tab-content active" id="s2ll-mb-general">

    <div class="s2ll-seo-field">
        <label for="shop2llm_focus_kw"><?php esc_html_e('Focus Keyword', 'shop-to-llm'); ?></label>
        <div style="display:flex;gap:4px">
            <input type="text" id="shop2llm_focus_kw" name="shop2llm_focus_kw" value="<?php echo esc_attr($focus_kw); ?>" placeholder="<?php esc_attr_e('Enter focus keyword(s), comma-separated', 'shop-to-llm'); ?>" style="flex:1" />
            <button type="button" id="s2ll-ai-keywords-btn" class="button" style="flex-shrink:0;white-space:nowrap" title="<?php esc_attr_e('AI-powered keyword suggestions based on your content', 'shop-to-llm'); ?>">
                <span style="font-size:14px">&#9889;</span> <?php esc_html_e('AI Suggest', 'shop-to-llm'); ?>
            </button>
        </div>
        <div id="s2ll-ai-keywords-status" style="font-size:11px;color:#64748b;margin-top:3px;display:none"></div>
    </div>

    <div class="s2ll-seo-field">
        <label for="shop2llm_seo_title"><?php esc_html_e('SEO Title', 'shop-to-llm'); ?></label>
        <input type="text" id="shop2llm_seo_title" name="shop2llm_seo_title" value="<?php echo esc_attr($seo_title); ?>" maxlength="70" />
        <div class="s2ll-char-count" id="shop2llm_seo_title_count" data-max="60"><?php echo esc_html(mb_strlen($seo_title) . '/60'); ?></div>
    </div>

    <div class="s2ll-seo-field">
        <label for="shop2llm_meta_desc"><?php esc_html_e('Meta Description', 'shop-to-llm'); ?></label>
        <textarea id="shop2llm_meta_desc" name="shop2llm_meta_desc" maxlength="170"><?php echo esc_textarea($meta_desc); ?></textarea>
        <div class="s2ll-char-count" id="shop2llm_meta_desc_count" data-max="155"><?php echo esc_html(mb_strlen($meta_desc) . '/155'); ?></div>
    </div>

    <div class="s2ll-seo-field">
        <label for="shop2llm_canonical"><?php esc_html_e('Canonical URL', 'shop-to-llm'); ?></label>
        <input type="text" id="shop2llm_canonical" name="shop2llm_canonical" value="<?php echo esc_attr($canonical); ?>" placeholder="https://" />
    </div>

    <div class="s2ll-checkbox-row">
        <label><input type="checkbox" name="shop2llm_noindex" value="1" <?php checked($noindex, '1'); ?> /> <?php esc_html_e('Noindex', 'shop-to-llm'); ?></label>
        <label><input type="checkbox" name="shop2llm_nofollow" value="1" <?php checked($nofollow, '1'); ?> /> <?php esc_html_e('Nofollow', 'shop-to-llm'); ?></label>
    </div>

    <?php if (shop_to_llm_has_woocommerce() && $post->post_type === 'product'): ?>
    <!-- WooCommerce Product SEO Fields -->
    <div style="margin-top:12px;padding-top:10px;border-top:1px solid #e2e8f0">
        <div style="font-size:12px;font-weight:600;color:#1e293b;margin-bottom:8px"><?php esc_html_e('Product Identifiers', 'shop-to-llm'); ?></div>
        <?php
        $gtin = get_post_meta($post->ID, '_shop2llm_gtin', true);
        $mpn = get_post_meta($post->ID, '_shop2llm_mpn', true);
        $brand = get_post_meta($post->ID, '_shop2llm_brand', true);
        $product_id_field = get_post_meta($post->ID, '_shop2llm_product_id', true);
        $condition = get_post_meta($post->ID, '_shop2llm_condition', true);
        ?>
        <div class="s2ll-seo-field">
            <label for="shop2llm_gtin"><?php esc_html_e('GTIN (UPC/EAN/ISBN)', 'shop-to-llm'); ?></label>
            <input type="text" id="shop2llm_gtin" name="shop2llm_gtin" value="<?php echo esc_attr($gtin); ?>" placeholder="e.g. 012345678905" />
        </div>
        <div class="s2ll-seo-field">
            <label for="shop2llm_mpn"><?php esc_html_e('MPN', 'shop-to-llm'); ?></label>
            <input type="text" id="shop2llm_mpn" name="shop2llm_mpn" value="<?php echo esc_attr($mpn); ?>" placeholder="<?php esc_attr_e('Manufacturer Part Number', 'shop-to-llm'); ?>" />
        </div>
        <div class="s2ll-seo-field">
            <label for="shop2llm_brand"><?php esc_html_e('Brand', 'shop-to-llm'); ?></label>
            <input type="text" id="shop2llm_brand" name="shop2llm_brand" value="<?php echo esc_attr($brand); ?>" placeholder="<?php esc_attr_e('Brand name', 'shop-to-llm'); ?>" />
        </div>
        <div class="s2ll-seo-field">
            <label for="shop2llm_product_id"><?php esc_html_e('Custom Product ID', 'shop-to-llm'); ?></label>
            <input type="text" id="shop2llm_product_id" name="shop2llm_product_id" value="<?php echo esc_attr($product_id_field); ?>" placeholder="e.g. SKU-001" />
        </div>
        <div class="s2ll-seo-field">
            <label for="shop2llm_condition"><?php esc_html_e('Condition', 'shop-to-llm'); ?></label>
            <select id="shop2llm_condition" name="shop2llm_condition" style="width:100%">
                <option value=""><?php esc_html_e('— Not set —', 'shop-to-llm'); ?></option>
                <option value="NewCondition" <?php selected($condition, 'NewCondition'); ?>><?php esc_html_e('New', 'shop-to-llm'); ?></option>
                <option value="UsedCondition" <?php selected($condition, 'UsedCondition'); ?>><?php esc_html_e('Used', 'shop-to-llm'); ?></option>
                <option value="RefurbishedCondition" <?php selected($condition, 'RefurbishedCondition'); ?>><?php esc_html_e('Refurbished', 'shop-to-llm'); ?></option>
                <option value="DamagedCondition" <?php selected($condition, 'DamagedCondition'); ?>><?php esc_html_e('Damaged', 'shop-to-llm'); ?></option>
            </select>
        </div>
    </div>
    <?php endif; ?>

    <!-- SERP Preview -->
    <div class="s2ll-serp-section">
        <h4><?php esc_html_e('SERP Preview', 'shop-to-llm'); ?></h4>
        <div class="s2ll-serp-desktop">
            <div class="s2ll-serp-url" id="s2ll-serp-desktop-url"><?php echo esc_html($preview_url); ?></div>
            <div class="s2ll-serp-title" id="s2ll-serp-desktop-title"><?php echo esc_html($preview_title . ' | ' . $site_name); ?></div>
            <div class="s2ll-serp-desc" id="s2ll-serp-desktop-desc"><?php echo esc_html($preview_desc); ?></div>
        </div>
        <div class="s2ll-serp-mobile">
            <div class="s2ll-mobile-header">
                <span class="s2ll-mobile-icon">&#9783;</span>
                <span class="s2ll-mobile-url" id="s2ll-serp-mobile-url"><?php echo esc_html(wp_parse_url($preview_url, PHP_URL_HOST) ?: $preview_url); ?></span>
            </div>
            <div class="s2ll-serp-title" id="s2ll-serp-mobile-title"><?php echo esc_html($preview_title . ' | ' . $site_name); ?></div>
            <div class="s2ll-serp-desc" id="s2ll-serp-mobile-desc"><?php echo esc_html($preview_desc); ?></div>
        </div>
    </div>

    </div>

    <div class="s2ll-mb-tab-content" id="s2ll-mb-social">

    <!-- Social Preview -->
    <div class="s2ll-social-section">
        <h4><?php esc_html_e('Social Preview', 'shop-to-llm'); ?></h4>
        <div class="s2ll-social-card">
            <div class="s2ll-social-card-img" id="s2ll-social-img"><?php esc_html_e('OG Image', 'shop-to-llm'); ?></div>
            <div class="s2ll-social-card-body">
                <div class="s2ll-social-card-domain" id="s2ll-social-domain"><?php echo esc_html(wp_parse_url(home_url(), PHP_URL_HOST)); ?></div>
                <div class="s2ll-social-card-title" id="s2ll-social-title"><?php echo esc_html($preview_title); ?></div>
                <div class="s2ll-social-card-desc" id="s2ll-social-desc"><?php echo esc_html($preview_desc); ?></div>
            </div>
        </div>
    </div>
    </div>

    <script>
    (function() {
        var seoTitle = document.getElementById('shop2llm_seo_title');
        var metaDesc = document.getElementById('shop2llm_meta_desc');
        var focusKw  = document.getElementById('shop2llm_focus_kw');
        var canonical = document.getElementById('shop2llm_canonical');

        function updateCount(input, counterId, maxLen) {
            var counter = document.getElementById(counterId);
            if (!counter || !input) return;
            var len = input.value.length;
            counter.textContent = len + '/' + maxLen;
            if (len > maxLen) {
                counter.classList.add('over');
            } else {
                counter.classList.remove('over');
            }
        }

        function updatePreviews() {
            var siteName = <?php echo wp_json_encode($site_name); ?>;
            var defaultTitle = <?php echo wp_json_encode($post->post_title); ?>;
            var defaultDesc = <?php echo wp_json_encode(wp_trim_words(wp_strip_all_tags($post->post_content), 30, '...')); ?>;
            var defaultUrl = <?php echo wp_json_encode(get_permalink($post->ID)); ?>;
            var t = (seoTitle && seoTitle.value) ? seoTitle.value : defaultTitle;
            var d = (metaDesc && metaDesc.value) ? metaDesc.value : defaultDesc;
            var u = (canonical && canonical.value) ? canonical.value : defaultUrl;
            var displayTitle = t + ' | ' + siteName;
            var host = '';
            try { host = new URL(u).hostname; } catch(e) { host = u; }

            var dt = document.getElementById('s2ll-serp-desktop-title');
            var dd = document.getElementById('s2ll-serp-desktop-desc');
            var du = document.getElementById('s2ll-serp-desktop-url');
            var mt = document.getElementById('s2ll-serp-mobile-title');
            var md = document.getElementById('s2ll-serp-mobile-desc');
            var mu = document.getElementById('s2ll-serp-mobile-url');
            var st = document.getElementById('s2ll-social-title');
            var sd = document.getElementById('s2ll-social-desc');

            if (dt) dt.textContent = displayTitle;
            if (dd) dd.textContent = d;
            if (du) du.textContent = u;
            if (mt) mt.textContent = displayTitle;
            if (md) md.textContent = d;
            if (mu) mu.textContent = host;
            if (st) st.textContent = t;
            if (sd) sd.textContent = d;
        }

        if (seoTitle) {
            seoTitle.addEventListener('input', function() { updateCount(this, 'shop2llm_seo_title_count', 60); updatePreviews(); });
        }
        if (metaDesc) {
            metaDesc.addEventListener('input', function() { updateCount(this, 'shop2llm_meta_desc_count', 155); updatePreviews(); });
        }
        if (canonical) {
            canonical.addEventListener('input', updatePreviews);
        }
    })();

    // AI Keyword Suggestions
    (function() {
        var aiBtn = document.getElementById('s2ll-ai-keywords-btn');
        var kwInput = document.getElementById('shop2llm_focus_kw');
        var statusEl = document.getElementById('s2ll-ai-keywords-status');
        if (aiBtn && kwInput) {
            aiBtn.addEventListener('click', function() {
                aiBtn.disabled = true;
                aiBtn.textContent = '...';
                statusEl.style.display = 'block';
                statusEl.textContent = <?php echo wp_json_encode(__('Generating AI keyword suggestions...', 'shop-to-llm')); ?>;
                statusEl.style.color = '#64748b';

                var fd = new FormData();
                fd.append('action', 's2ll_seo_ai_keywords');
                fd.append('post_id', <?php echo (int) $post->ID; ?>);
                fd.append('nonce', <?php echo wp_json_encode(wp_create_nonce('s2ll_ai_nonce')); ?>);

                fetch(ajaxurl, {method: 'POST', body: fd, credentials: 'same-origin'})
                    .then(function(r) { return r.json(); })
                    .then(function(data) {
                        aiBtn.disabled = false;
                        var remaining = (data.data && data.data.remaining !== undefined) ? data.data.remaining : -1;
                        var quotaText = remaining >= 0 ? ' (' + remaining + ' left today)' : '';
                        aiBtn.innerHTML = '<span style="font-size:14px">&#9889;</span> <?php echo esc_js(__('AI Suggest', 'shop-to-llm')); ?>';

                        if (data.success && data.data && data.data.keywords && data.data.keywords.length > 0) {
                            var existing = kwInput.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean);
                            var newKws = data.data.keywords.filter(function(k) { return existing.indexOf(k) === -1; });
                            var merged = existing.concat(newKws);
                            kwInput.value = merged.join(', ');
                            var fromLabel = (data.data.from === 'cache') ? ' (cached)' : (data.data.from === 'local' ? ' (local)' : '');
                            statusEl.textContent = <?php echo wp_json_encode(__('Suggestions added!', 'shop-to-llm')); ?> + fromLabel + quotaText;
                            statusEl.style.color = '#16a34a';
                            // Show error notice if fallback was used
                            if (data.data.error_notice) {
                                statusEl.textContent += ' — ' + data.data.error_notice;
                                statusEl.style.color = '#ca8a04';
                            }
                        } else if (data.data && data.data.error_notice) {
                            statusEl.textContent = data.data.error_notice + quotaText;
                            statusEl.style.color = '#ca8a04';
                        } else {
                            statusEl.textContent = <?php echo wp_json_encode(__('No suggestions available. Try adding manually.', 'shop-to-llm')); ?>;
                            statusEl.style.color = '#ca8a04';
                        }
                    })
                    .catch(function() {
                        aiBtn.disabled = false;
                        aiBtn.innerHTML = '<span style="font-size:14px">&#9889;</span> <?php echo esc_js(__('AI Suggest', 'shop-to-llm')); ?>';
                        statusEl.textContent = <?php echo wp_json_encode(__('Network error. Please try again.', 'shop-to-llm')); ?>;
                        statusEl.style.color = '#dc2626';
                    });
            });
        }
    })();

    // Tab switching
    (function() {
        var tabs = document.querySelectorAll('.s2ll-mb-tab');
        tabs.forEach(function(tab) {
            tab.addEventListener('click', function() {
                var targetId = this.getAttribute('data-tab');
                document.querySelectorAll('.s2ll-mb-tab').forEach(function(t) { t.classList.remove('active'); });
                this.classList.add('active');
                document.querySelectorAll('.s2ll-mb-tab-content').forEach(function(c) { c.classList.remove('active'); });
                document.getElementById(targetId).classList.add('active');
            });
        });
    })();
    </script>
    <?php
}

function shop_to_llm_seo_save_meta(int $postId): void
{
    if (!isset($_POST['shop2llm_seo_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_seo_nonce'])), 'shop2llm_seo_save')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $postId)) {
        return;
    }

    $fields = [
        'shop2llm_focus_kw'  => '_shop2llm_focus_kw',
        'shop2llm_seo_title' => '_shop2llm_seo_title',
        'shop2llm_canonical' => '_shop2llm_canonical',
    ];
    foreach ($fields as $postKey => $metaKey) {
        if (isset($_POST[$postKey])) {
            update_post_meta($postId, $metaKey, sanitize_text_field(wp_unslash($_POST[$postKey])));
        }
    }

    if (isset($_POST['shop2llm_meta_desc'])) {
        update_post_meta($postId, '_shop2llm_meta_desc', sanitize_textarea_field(wp_unslash($_POST['shop2llm_meta_desc'])));
    }

    update_post_meta($postId, '_shop2llm_noindex', isset($_POST['shop2llm_noindex']) ? '1' : '0');
    update_post_meta($postId, '_shop2llm_nofollow', isset($_POST['shop2llm_nofollow']) ? '1' : '0');

    // WooCommerce product fields
    $product_fields = [
        'shop2llm_gtin'      => '_shop2llm_gtin',
        'shop2llm_mpn'       => '_shop2llm_mpn',
        'shop2llm_brand'     => '_shop2llm_brand',
        'shop2llm_product_id' => '_shop2llm_product_id',
        'shop2llm_condition'  => '_shop2llm_condition',
    ];
    foreach ($product_fields as $postKey => $metaKey) {
        if (isset($_POST[$postKey])) {
            update_post_meta($postId, $metaKey, sanitize_text_field(wp_unslash($_POST[$postKey])));
        }
    }

    // Cornerstone content
    if (isset($_POST['shop2llm_cornerstone'])) {
        update_post_meta($postId, '_shop2llm_cornerstone', '1');
    } else {
        delete_post_meta($postId, '_shop2llm_cornerstone');
    }
}

/**
 * Get available SEO title variables with their labels and callbacks.
 *
 * @return array<string, array{label: string, callback: callable}>
 */
function shop_to_llm_seo_get_title_variables(): array
{
    $vars = [
        'title' => [
            'label' => __('Post/Page Title', 'shop-to-llm'),
            'callback' => function (): string {
                if (is_singular()) {
                    global $post;
                    return $post->post_title;
                }
                if (is_category() || is_tag() || is_tax()) {
                    $term = get_queried_object();
                    return $term ? $term->name : '';
                }
                if (is_post_type_archive()) {
                    $pto = get_queried_object();
                    return $pto ? $pto->labels->name : '';
                }
                if (is_author()) {
                    $author = get_queried_object();
                    return $author ? $author->display_name : '';
                }
                if (is_front_page() || is_home()) {
                    return get_bloginfo('name');
                }
                return wp_title('', false) ?: get_bloginfo('name');
            },
        ],
        'sitename' => [
            'label' => __('Site Name', 'shop-to-llm'),
            'callback' => function (): string {
                return get_bloginfo('name');
            },
        ],
        'sep' => [
            'label' => __('Separator', 'shop-to-llm'),
            'callback' => function (): string {
                return apply_filters('shop2llm_title_sep', '|');
            },
        ],
        'tagline' => [
            'label' => __('Site Tagline', 'shop-to-llm'),
            'callback' => function (): string {
                return get_bloginfo('description');
            },
        ],
        'category' => [
            'label' => __('Primary Category', 'shop-to-llm'),
            'callback' => function (): string {
                $cats = get_the_category();
                return !empty($cats) ? $cats[0]->name : '';
            },
        ],
        'tag' => [
            'label' => __('Primary Tag', 'shop-to-llm'),
            'callback' => function (): string {
                $tags = get_the_tags();
                return !empty($tags) ? $tags[0]->name : '';
            },
        ],
        'date' => [
            'label' => __('Publish Date', 'shop-to-llm'),
            'callback' => function (): string {
                return get_the_date();
            },
        ],
        'author' => [
            'label' => __('Author Name', 'shop-to-llm'),
            'callback' => function (): string {
                return get_the_author();
            },
        ],
    ];

    return apply_filters('shop2llm_seo_title_variables', $vars);
}

/**
 * Parse a title template string by replacing %%variable%% placeholders.
 */
function shop_to_llm_seo_parse_title(string $template): string
{
    $variables = shop_to_llm_seo_get_title_variables();
    $result = $template;

    foreach ($variables as $key => $var) {
        $placeholder = '%%' . $key . '%%';
        if (strpos($result, $placeholder) !== false) {
            $value = call_user_func($var['callback']);
            $result = str_replace($placeholder, $value, $result);
        }
    }

    // Remove leading/trailing separators from empty variables
    $sep = apply_filters('shop2llm_title_sep', '|');
    $result = preg_replace('/\s*' . preg_quote($sep, '/') . '\s*' . preg_quote($sep, '/') . '\s*/', ' ' . $sep . ' ', $result);
    // Remove empty %%vars%% that weren't resolved
    $result = preg_replace('/\s*%%\w+%%\s*/', '', $result);
    // Clean up orphaned separators at start/end
    $result = trim($result, ' ' . $sep . ' ');
    $result = preg_replace('/^\s*' . preg_quote($sep, '/') . '\s*/', '', $result);
    $result = preg_replace('/\s*' . preg_quote($sep, '/') . '\s*$/', '', $result);
    $result = trim($result);

    return $result;
}

function shop_to_llm_seo_inject_meta(): void
{
    if (get_option('shop_to_llm_seo_meta', 'yes') !== 'yes') {
        return;
    }
    if (shop_to_llm_has_seo_plugin()) {
        return;
    }

    // Homepage
    if (is_front_page() || is_home()) {
        $home_title = get_option('shop_to_llm_seo_home_title', '');
        $home_desc = get_option('shop_to_llm_seo_home_desc', '');
        $sep = apply_filters('shop2llm_title_sep', '|');
        $site_name = get_bloginfo('name');
        // If custom home title set, append site name; otherwise use template
        $home_template = get_option('shop_to_llm_seo_title_home', '%%sitename%% %%sep%% %%tagline%%');
        if ($home_title) {
            $title_output = $home_title . ' ' . $sep . ' ' . $site_name;
        } else {
            $title_output = shop_to_llm_seo_parse_title($home_template);
        }
        $desc = $home_desc ?: get_bloginfo('description');
        echo '<title>' . esc_html($title_output) . '</title>' . "\n";
        if ($desc) {
            echo '<meta name="description" content="' . esc_attr($desc) . '" />' . "\n";
        }
        echo '<link rel="canonical" href="' . esc_url(home_url('/')) . '" />' . "\n";
        return;
    }

    // Singular pages
    if (is_singular()) {
        global $post;
        if (!$post instanceof WP_Post) {
            return;
        }

        $seo_title = get_post_meta($post->ID, '_shop2llm_seo_title', true);
        $meta_desc = get_post_meta($post->ID, '_shop2llm_meta_desc', true);
        $canonical = get_post_meta($post->ID, '_shop2llm_canonical', true);
        $noindex   = get_post_meta($post->ID, '_shop2llm_noindex', true);
        $nofollow  = get_post_meta($post->ID, '_shop2llm_nofollow', true);

        $post_type = get_post_type();
        $template_key = in_array($post_type, ['post', 'page', 'product'], true) ? $post_type : 'post';
        $default_template = get_option('shop_to_llm_seo_title_' . $template_key, '%%title%% %%sep%% %%sitename%%');
        if ($seo_title !== '') {
            echo '<title>' . esc_html($seo_title . ' | ' . get_bloginfo('name')) . '</title>' . "\n";
        } else {
            echo '<title>' . esc_html(shop_to_llm_seo_parse_title($default_template)) . '</title>' . "\n";
        }
        if ($meta_desc !== '') {
            echo '<meta name="description" content="' . esc_attr($meta_desc) . '" />' . "\n";
        }
        if ($canonical !== '') {
            echo '<link rel="canonical" href="' . esc_url($canonical) . '" />' . "\n";
        }

        $robots = [];
        if ($noindex === '1') {
            $robots[] = 'noindex';
        }
        if ($nofollow === '1') {
            $robots[] = 'nofollow';
        }
        if (!empty($robots)) {
            echo '<meta name="robots" content="' . esc_attr(implode(',', $robots)) . '" />' . "\n";
        }
        return;
    }

    // Archive pages
    if (is_category() || is_tag() || is_tax()) {
        $term = get_queried_object();
        if (!$term) { return; }
        $tax_template = get_option('shop_to_llm_seo_title_category', '%%category%% %%sep%% %%sitename%%');
        if (is_tag()) {
            $tax_template = get_option('shop_to_llm_seo_title_tag', '%%tag%% %%sep%% %%sitename%%');
        }
        $desc = $term->description ?: '';
        echo '<title>' . esc_html(shop_to_llm_seo_parse_title($tax_template)) . '</title>' . "\n";
        if ($desc) {
            echo '<meta name="description" content="' . esc_attr(wp_trim_words($desc, 30)) . '" />' . "\n";
        }
        echo '<link rel="canonical" href="' . esc_url(get_term_link($term)) . '" />' . "\n";
        return;
    }

    if (is_post_type_archive()) {
        $post_type = get_queried_object();
        $archive_template = get_option('shop_to_llm_seo_title_archive', '%%title%% %%sep%% %%sitename%%');
        echo '<title>' . esc_html(shop_to_llm_seo_parse_title($archive_template)) . '</title>' . "\n";
        echo '<link rel="canonical" href="' . esc_url(get_post_type_archive_link($post_type->name)) . '" />' . "\n";
        return;
    }

    if (is_author()) {
        $author = get_queried_object();
        $archive_template = get_option('shop_to_llm_seo_title_archive', '%%title%% %%sep%% %%sitename%%');
        echo '<title>' . esc_html(shop_to_llm_seo_parse_title($archive_template)) . '</title>' . "\n";
        echo '<link rel="canonical" href="' . esc_url(get_author_posts_url($author->ID)) . '" />' . "\n";
        return;
    }

    // 404
    if (is_404()) {
        $sep = apply_filters('shop2llm_title_sep', '|');
        echo '<title>' . esc_html__('Page not found', 'shop-to-llm') . ' ' . esc_html($sep) . ' ' . esc_html(get_bloginfo('name')) . '</title>' . "\n";
        return;
    }

    // Fallback for any other page
    $sep = apply_filters('shop2llm_title_sep', '|');
    wp_title($sep, true, 'right');
}

// ─── Content Analysis Engine ─────────────────────────────────────────

function shop_to_llm_seo_flesch_score(string $text): float
{
    // Strip HTML tags
    $text = wp_strip_all_tags($text);
    if ($text === '') {
        return 0.0;
    }

    // Count sentences (split by . ! ?)
    $sentences = preg_split('/[.!?]+/', $text);
    $sentences = array_filter($sentences, function ($s) { return trim($s) !== ''; });
    $sentence_count = max(1, count($sentences));

    // Count words
    $word_count = str_word_count($text);
    if ($word_count < 1) {
        return 0.0;
    }

    // Estimate syllable count using vowel groups
    $lower = strtolower($text);
    $words = str_word_count($lower, 1);
    $syllable_count = 0;
    foreach ($words as $word) {
        // Count vowel groups (consecutive vowels = 1 syllable)
        $vowel_groups = preg_match_all('/[aeiouy]+/', $word);
        // Subtract for silent e at end
        if (strlen($word) > 2 && substr($word, -1) === 'e') {
            $vowel_groups = max(1, $vowel_groups - 1);
        }
        $syllable_count += max(1, $vowel_groups);
    }

    $score = 206.835 - (1.015 * ($word_count / $sentence_count)) - (84.6 * ($syllable_count / $word_count));
    return (float) max(0, min(100, round($score, 1)));
}

function shop_to_llm_seo_count_keyword_occurrences(string $content, string $keyword): int
{
    // For CJK characters (Chinese/Japanese/Korean), use multibyte-safe counting
    if (preg_match('/[\x{4e00}-\x{9fff}\x{3040}-\x{309f}\x{30a0}-\x{30ff}]/u', $keyword)) {
        return mb_substr_count(mb_strtolower($content, 'UTF-8'), mb_strtolower($keyword, 'UTF-8'), 'UTF-8');
    }
    // For Latin keywords, use case-insensitive whole-word matching
    $pattern = '/\b' . preg_quote($keyword, '/') . '\b/i';
    return preg_match_all($pattern, $content);
}

function shop_to_llm_seo_analyze_content(int $postId): array
{
    $post = get_post($postId);
    if (!$post instanceof WP_Post) {
        return ['score' => 0, 'focus_kw' => '', 'checks' => [], 'keyword_scores' => []];
    }

    $focus_kw_raw = get_post_meta($postId, '_shop2llm_focus_kw', true);
    $keywords = array_values(array_filter(array_map('trim', explode(',', $focus_kw_raw))));
    $primary_kw = $keywords[0] ?? '';
    $content = $post->post_content;
    $plain_content = wp_strip_all_tags($content);
    $title = $post->post_title;
    $slug = $post->post_name;
    $word_count = str_word_count($plain_content);

    $checks = [];
    $keyword_scores = []; // Per-keyword scoring: [keyword => score_0_to_100]

    // ─── Per-keyword checks for ALL keywords ─────
    if (!empty($keywords)) {
        $meta_desc = get_post_meta($postId, '_shop2llm_meta_desc', true);

        // Extract all image alt texts once
        $all_alts = [];
        if (preg_match_all('/<img[^>]+alt=["\']([^"\']*)["\'][^>]*>/i', $content, $alt_matches)) {
            $all_alts = $alt_matches[1];
        }
        // Extract all H2-H6 headings once
        $all_headings = [];
        if (preg_match_all('/<h[2-6][^>]*>(.*?)<\/h[2-6]>/i', $content, $h_matches)) {
            $all_headings = array_map(function ($h) { return wp_strip_all_tags($h); }, $h_matches[1]);
        }

        foreach ($keywords as $ki => $kw) {
            if ($kw === '') {
                continue;
            }

            $kw_checks = [];
            $kw_label = (count($keywords) > 1) ? sprintf(/* translators: %s: keyword */ __('Keyword: %s', 'shop-to-llm'), $kw) : '';

            // K1: Keyword in title
            $in_title = stripos($title, $kw) !== false;
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In title', 'shop-to-llm') : __('Keyword in title', 'shop-to-llm'),
                'pass' => $in_title,
                'value' => $in_title ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => ($ki === 0) ? 'problem' : 'improvement',
            ];

            // K2: Keyword in meta description
            $in_meta = $meta_desc !== '' && stripos($meta_desc, $kw) !== false;
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In meta description', 'shop-to-llm') : __('Keyword in meta description', 'shop-to-llm'),
                'pass' => $in_meta,
                'value' => $in_meta ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => ($ki === 0) ? 'problem' : 'improvement',
            ];

            // K3: Keyword in first paragraph
            $paragraphs = explode("\n\n", $plain_content);
            $first_para = $paragraphs[0] ?? '';
            $in_first = stripos($first_para, $kw) !== false;
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In first paragraph', 'shop-to-llm') : __('Keyword in first paragraph', 'shop-to-llm'),
                'pass' => $in_first,
                'value' => $in_first ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];

            // K4: Keyword in URL
            $kw_slug = str_replace(' ', '-', strtolower($kw));
            $in_url = strpos(strtolower($slug), $kw_slug) !== false || stripos($slug, $kw) !== false;
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In URL', 'shop-to-llm') : __('Keyword in URL', 'shop-to-llm'),
                'pass' => $in_url,
                'value' => $in_url ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];

            // K5: Keyword in headings
            $in_headings = false;
            foreach ($all_headings as $heading) {
                if (stripos($heading, $kw) !== false) {
                    $in_headings = true;
                    break;
                }
            }
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In subheadings', 'shop-to-llm') : __('H2 headings contain keyword', 'shop-to-llm'),
                'pass' => $in_headings,
                'value' => $in_headings ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];

            // K6: Keyword in image alt
            $in_alt = false;
            foreach ($all_alts as $alt) {
                if ($kw !== '' && stripos($alt, $kw) !== false) {
                    $in_alt = true;
                    break;
                }
            }
            $kw_checks[] = [
                'name' => $kw_label ? $kw_label . ' — ' . __('In image alt text', 'shop-to-llm') : __('Image alt text contains keyword', 'shop-to-llm'),
                'pass' => $in_alt,
                'value' => $in_alt ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];

            // K7: Keyword density
            if ($word_count > 0) {
                $kw_occurrences = shop_to_llm_seo_count_keyword_occurrences($plain_content, $kw);
                $density = ($kw_occurrences * mb_strlen($kw)) / mb_strlen($plain_content) * 100;
                $density_ok = $density >= 0.5 && $density <= 2.5;
                $kw_checks[] = [
                    'name' => $kw_label ? $kw_label . ' — ' . __('Density (0.5-2.5%)', 'shop-to-llm') : __('Keyword density (0.5%-2.5%)', 'shop-to-llm'),
                    'pass' => $density_ok,
                    'value' => round($density, 2) . '%',
                    'severity' => 'improvement',
                ];
            }

            // Calculate per-keyword score (max 7 checks per keyword)
            $kw_total = count($kw_checks);
            $kw_passed = count(array_filter($kw_checks, function ($c) { return $c['pass']; }));
            $keyword_scores[$kw] = $kw_total > 0 ? (int) round(($kw_passed / $kw_total) * 100) : 0;

            // Add all keyword checks to the main checks array
            foreach ($kw_checks as $kc) {
                $checks[] = $kc;
            }
        }
    }

    // ─── Global checks (not per-keyword) ─────

    // Focus keyword set
    $checks[] = [
        'name' => __('Focus keyword set', 'shop-to-llm'),
        'pass' => $primary_kw !== '',
        'value' => $primary_kw ?: __('None', 'shop-to-llm'),
        'severity' => 'problem',
    ];

    // Meta description length
    $meta_desc_for_check = get_post_meta($postId, '_shop2llm_meta_desc', true);
    if (!empty($meta_desc_for_check)) {
        $desc_len = mb_strlen($meta_desc_for_check);
        if ($desc_len < 120) {
            $checks[] = [
                'name' => __('Meta description too short', 'shop-to-llm'),
                'pass' => false,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters (recommended: 120-160)', 'shop-to-llm'), $desc_len),
                'severity' => 'improvement',
            ];
        } elseif ($desc_len > 160) {
            $checks[] = [
                'name' => __('Meta description too long', 'shop-to-llm'),
                'pass' => false,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters (recommended: 120-160)', 'shop-to-llm'), $desc_len),
                'severity' => 'improvement',
            ];
        } else {
            $checks[] = [
                'name' => __('Meta description length', 'shop-to-llm'),
                'pass' => true,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters', 'shop-to-llm'), $desc_len),
                'severity' => 'good',
            ];
        }
    }

    // SEO title length
    $seo_title_for_check = get_post_meta($postId, '_shop2llm_seo_title', true);
    if (!empty($seo_title_for_check)) {
        $title_len = mb_strlen($seo_title_for_check);
        if ($title_len < 30) {
            $checks[] = [
                'name' => __('SEO title too short', 'shop-to-llm'),
                'pass' => false,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters (recommended: 30-60)', 'shop-to-llm'), $title_len),
                'severity' => 'improvement',
            ];
        } elseif ($title_len > 60) {
            $checks[] = [
                'name' => __('SEO title too long', 'shop-to-llm'),
                'pass' => false,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters (recommended: 30-60)', 'shop-to-llm'), $title_len),
                'severity' => 'improvement',
            ];
        } else {
            $checks[] = [
                'name' => __('SEO title length', 'shop-to-llm'),
                'pass' => true,
                'value' => sprintf(/* translators: %d: character count */ __('%d characters', 'shop-to-llm'), $title_len),
                'severity' => 'good',
            ];
        }
    }

    // Content length >= 300 words
    $checks[] = [
        'name' => __('Content length (>= 300 words)', 'shop-to-llm'),
        'pass' => $word_count >= 300,
        'value' => $word_count,
        'severity' => $word_count < 100 ? 'problem' : 'improvement',
    ];

    // Internal links >= 3
    $internal_links = 0;
    $site_host = wp_parse_url(home_url(), PHP_URL_HOST);
    if (preg_match_all('/<a[^>]+href=["\']([^"\']*)["\'][^>]*>/i', $content, $link_matches)) {
        foreach ($link_matches[1] as $href) {
            $link_host = wp_parse_url($href, PHP_URL_HOST);
            if ($link_host === $site_host || ($link_host === null && strpos($href, '/') === 0)) {
                $internal_links++;
            }
        }
    }
    $checks[] = [
        'name' => __('Internal links (>= 3)', 'shop-to-llm'),
        'pass' => $internal_links >= 3,
        'value' => $internal_links,
        'severity' => 'improvement',
    ];

    // Flesch readability score
    if ($word_count > 100) {
        $flesch = shop_to_llm_seo_flesch_score($plain_content);
        $checks[] = [
            'name' => __('Flesch readability score', 'shop-to-llm'),
            'pass' => $flesch >= 60,
            'value' => $flesch,
            'severity' => 'improvement',
        ];
    }

    // Passive voice < 10%
    if ($word_count > 50) {
        $passive_patterns = [
            '/\b(is|was|were|be|been|being)\s+\w+ed\b/i',
            '/\b(is|was|were|be|been|being)\s+\w+en\b/i',
        ];
        $passive_count = 0;
        foreach ($passive_patterns as $pattern) {
            $passive_count += preg_match_all($pattern, $plain_content);
        }
        $total_sentences = max(1, count(array_filter(preg_split('/[.!?]+/', $plain_content), function ($s) { return trim($s) !== ''; })));
        $passive_pct = ($passive_count / $total_sentences) * 100;
        $checks[] = [
            'name' => __('Passive voice (< 10%)', 'shop-to-llm'),
            'pass' => $passive_pct < 10,
            'value' => round($passive_pct, 1) . '%',
            'severity' => 'improvement',
        ];
    }

    // Transition words > 30%
    $transition_words = [
        'however', 'therefore', 'moreover', 'furthermore', 'additionally',
        'consequently', 'nevertheless', 'meanwhile', 'specifically',
        'alternatively', 'similarly', 'notably', 'accordingly', 'thus', 'hence',
        'also', 'besides', 'likewise', 'otherwise', 'instead', 'still',
        'indeed', 'rather', 'thereby', 'subsequently', 'finally', 'firstly',
        'secondly', 'thirdly', 'next', 'then', 'for example', 'for instance',
        'in addition', 'in contrast', 'in fact', 'in other words', 'in summary',
        'as a result', 'on the other hand', 'above all',
    ];
    $content_lower_for_trans = strtolower($plain_content);
    $transition_count = 0;
    foreach ($transition_words as $tw) {
        $transition_count += substr_count($content_lower_for_trans, $tw);
    }
    $total_sentences = max(1, count(array_filter(preg_split('/[.!?]+/', $plain_content), function ($s) { return trim($s) !== ''; })));
    $transition_pct = ($transition_count / $total_sentences) * 100;
    $checks[] = [
        'name' => __('Transition words (> 30%)', 'shop-to-llm'),
        'pass' => $transition_pct > 30,
        'value' => round($transition_pct, 1) . '%',
        'severity' => 'improvement',
    ];

    // ─── WooCommerce-specific checks ─────
    if (shop_to_llm_has_woocommerce() && $post->post_type === 'product') {
        $product = wc_get_product($postId);
        if ($product instanceof WC_Product) {
            // Product description length
            $prod_desc = $product->get_description();
            $prod_short_desc = $product->get_short_description();
            $desc_word_count = str_word_count(wp_strip_all_tags($prod_desc));
            $checks[] = [
                'name' => __('Product description (>= 200 words)', 'shop-to-llm'),
                'pass' => $desc_word_count >= 200,
                'value' => $desc_word_count,
                'severity' => $desc_word_count < 100 ? 'problem' : 'improvement',
            ];
            // Short description set
            $checks[] = [
                'name' => __('Product short description set', 'shop-to-llm'),
                'pass' => !empty($prod_short_desc),
                'value' => !empty($prod_short_desc) ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];
            // Product gallery images count
            $gallery_ids = $product->get_gallery_image_ids();
            $gallery_count = count($gallery_ids);
            $checks[] = [
                'name' => __('Product gallery images (>= 3)', 'shop-to-llm'),
                'pass' => $gallery_count >= 3,
                'value' => $gallery_count,
                'severity' => 'improvement',
            ];
            // GTIN/MPN/Brand set
            $gtin = get_post_meta($postId, '_shop2llm_gtin', true);
            $mpn = get_post_meta($postId, '_shop2llm_mpn', true);
            $brand = get_post_meta($postId, '_shop2llm_brand', true);
            $has_identifiers = ($gtin !== '' || $mpn !== '' || $brand !== '');
            $checks[] = [
                'name' => __('Product identifiers (GTIN/MPN/Brand)', 'shop-to-llm'),
                'pass' => $has_identifiers,
                'value' => $has_identifiers ? __('Yes', 'shop-to-llm') : __('None set', 'shop-to-llm'),
                'severity' => 'improvement',
            ];
            // Reviews enabled
            $reviews_enabled = $product->get_reviews_allowed();
            $checks[] = [
                'name' => __('Product reviews enabled', 'shop-to-llm'),
                'pass' => $reviews_enabled,
                'value' => $reviews_enabled ? __('Yes', 'shop-to-llm') : __('No', 'shop-to-llm'),
                'severity' => 'improvement',
            ];
            // Product has attributes
            $attributes = $product->get_attributes();
            $has_attrs = !empty($attributes);
            $checks[] = [
                'name' => __('Product attributes set', 'shop-to-llm'),
                'pass' => $has_attrs,
                'value' => $has_attrs ? count($attributes) : 0,
                'severity' => 'improvement',
            ];
        }
    }

    // Calculate overall score
    $total_checks = count($checks);
    $passed_checks = count(array_filter($checks, function ($c) { return $c['pass']; }));
    $score = $total_checks > 0 ? (int) round(($passed_checks / $total_checks) * 100) : 0;

    return [
        'score' => $score,
        'focus_kw' => $primary_kw,
        'all_keywords' => $keywords,
        'checks' => $checks,
        'keyword_scores' => $keyword_scores,
    ];
}

function shop_to_llm_seo_render_analysis_meta_box(WP_Post $post): void
{
    $analysis = shop_to_llm_seo_analyze_content($post->ID);
    $score = $analysis['score'];
    $checks = $analysis['checks'];

    $score_class = 'red';
    $score_color = '#dc2626';
    if ($score > 70) {
        $score_class = 'green';
        $score_color = '#16a34a';
    } elseif ($score > 40) {
        $score_class = 'yellow';
        $score_color = '#f59e0b';
    }

    // Group checks by severity
    $problems = array_filter($checks, function ($c) { return !$c['pass'] && ($c['severity'] ?? '') === 'problem'; });
    $improvements = array_filter($checks, function ($c) { return !$c['pass'] && ($c['severity'] ?? '') !== 'problem'; });
    $good = array_filter($checks, function ($c) { return $c['pass']; });

    $cornerstone = get_post_meta($post->ID, '_shop2llm_cornerstone', true);
    ?>
    <style>
    .s2ll-analysis-score{display:flex;align-items:center;justify-content:center;flex-direction:column;margin-bottom:14px}
    .s2ll-analysis-circle{width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:#fff;margin-bottom:6px}
    .s2ll-analysis-circle.red{background:#dc2626}
    .s2ll-analysis-circle.yellow{background:#f59e0b}
    .s2ll-analysis-circle.green{background:#16a34a}
    .s2ll-analysis-label{font-size:11px;color:#64748b}
    .s2ll-analysis-group{margin-bottom:10px}
    .s2ll-analysis-group-title{font-size:12px;font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:5px}
    .s2ll-analysis-group-title.problems{color:#dc2626}
    .s2ll-analysis-group-title.improvements{color:#d97706}
    .s2ll-analysis-group-title.good{color:#16a34a}
    .s2ll-analysis-item{display:flex;align-items:flex-start;gap:5px;padding:3px 0 3px 14px;font-size:12px;line-height:1.5}
    .s2ll-analysis-icon{flex-shrink:0;margin-top:1px;font-size:12px}
    .s2ll-analysis-icon.pass{color:#16a34a}
    .s2ll-analysis-icon.fail{color:#dc2626}
    .s2ll-analysis-text{color:#475569}
    .s2ll-analysis-value{font-size:10px;color:#94a3b8;margin-left:4px}
    .s2ll-cornerstone-row{margin-top:10px;border-top:1px solid #e2e8f0;padding-top:10px}
    .s2ll-cornerstone-row label{display:flex;align-items:center;gap:5px;font-size:12px;color:#475569;cursor:pointer}
    </style>

    <p style="font-size:11px;color:#94a3b8;text-align:center;margin:0 0 6px"><?php esc_html_e('Full analysis below', 'shop-to-llm'); ?></p>
    <div class="s2ll-analysis-score">
        <div class="s2ll-analysis-circle <?php echo esc_attr($score_class); ?>">
            <?php echo esc_html($score); ?>
        </div>
        <span class="s2ll-analysis-label"><?php esc_html_e('SEO Score', 'shop-to-llm'); ?></span>
    </div>

    <?php
    // Per-keyword scores
    $keyword_scores = $analysis['keyword_scores'] ?? [];
    $all_keywords = $analysis['all_keywords'] ?? [];
    if (count($keyword_scores) > 1):
    ?>
    <div class="s2ll-analysis-group" style="margin-bottom:10px;border-top:1px solid #e2e8f0;padding-top:8px">
        <div class="s2ll-analysis-group-title" style="color:#2563eb;font-size:12px;font-weight:600">
            <?php esc_html_e('Per-Keyword Scores', 'shop-to-llm'); ?>
        </div>
        <?php foreach ($keyword_scores as $kw => $kw_score):
            $kw_color = $kw_score >= 70 ? '#16a34a' : ($kw_score >= 40 ? '#ca8a04' : '#dc2626');
        ?>
        <div class="s2ll-analysis-item" style="padding:3px 0 3px 14px;font-size:12px">
            <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:<?php echo esc_attr($kw_color); ?>;margin-right:6px;flex-shrink:0;margin-top:3px"></span>
            <span class="s2ll-analysis-text"><?php echo esc_html($kw); ?></span>
            <span class="s2ll-analysis-value" style="font-weight:600;color:<?php echo esc_attr($kw_color); ?>"><?php echo esc_html($kw_score); ?></span>
        </div>
        <?php endforeach; ?>
    </div>
    <?php endif; ?>

    <?php if (!empty($problems)) : ?>
    <div class="s2ll-analysis-group">
        <div class="s2ll-analysis-group-title problems">
            <span>&#9888;</span> <?php /* translators: %d: number of problems */
            echo esc_html(sprintf(__('Problems (%d)', 'shop-to-llm'), count($problems))); ?>
        </div>
        <?php foreach ($problems as $check) : ?>
        <div class="s2ll-analysis-item">
            <span class="s2ll-analysis-icon fail">&#10007;</span>
            <span class="s2ll-analysis-text"><?php echo esc_html($check['name']); ?>
                <?php if ($check['value'] !== '' && $check['value'] !== false && $check['value'] !== true) : ?>
                <span class="s2ll-analysis-value">(<?php echo esc_html($check['value']); ?>)</span>
                <?php endif; ?>
            </span>
        </div>
        <?php endforeach; ?>
    </div>
    <?php endif; ?>

    <?php if (!empty($improvements)) : ?>
    <div class="s2ll-analysis-group">
        <div class="s2ll-analysis-group-title improvements">
            <span>&#9650;</span> <?php /* translators: %d: number of improvements */
            echo esc_html(sprintf(__('Improvements (%d)', 'shop-to-llm'), count($improvements))); ?>
        </div>
        <?php foreach ($improvements as $check) : ?>
        <div class="s2ll-analysis-item">
            <span class="s2ll-analysis-icon fail">&#10007;</span>
            <span class="s2ll-analysis-text"><?php echo esc_html($check['name']); ?>
                <?php if ($check['value'] !== '' && $check['value'] !== false && $check['value'] !== true) : ?>
                <span class="s2ll-analysis-value">(<?php echo esc_html($check['value']); ?>)</span>
                <?php endif; ?>
            </span>
        </div>
        <?php endforeach; ?>
    </div>
    <?php endif; ?>

    <?php if (!empty($good)) : ?>
    <div class="s2ll-analysis-group">
        <div class="s2ll-analysis-group-title good">
            <span>&#10003;</span> <?php /* translators: %d: number of good checks */
            echo esc_html(sprintf(__('Good (%d)', 'shop-to-llm'), count($good))); ?>
        </div>
        <?php foreach ($good as $check) : ?>
        <div class="s2ll-analysis-item">
            <span class="s2ll-analysis-icon pass">&#10003;</span>
            <span class="s2ll-analysis-text"><?php echo esc_html($check['name']); ?></span>
        </div>
        <?php endforeach; ?>
    </div>
    <?php endif; ?>

    <div class="s2ll-cornerstone-row">
        <label>
            <input type="checkbox" name="shop2llm_cornerstone" value="1" <?php checked($cornerstone, '1'); ?> />
            <?php esc_html_e('Cornerstone Content', 'shop-to-llm'); ?>
        </label>
    </div>
    <?php
}

// ─── Live Content Extractability (v1.3.4+) ──────────────────────────────
// Mirrors VigIA "Editor Citability Score" / OmniGEO "Live Content Extractability".
// Provides real-time AI-extractability feedback in both Classic & Gutenberg editors.

/**
 * Calculate Live Content Extractability (citability) score for a post.
 *
 * Score dimensions (100 total):
 *   - Structured Data      (15) — JSON-LD schema on the post
 *   - Heading Hierarchy     (15) — H2/H3 structure for semantic parsing
 *   - Paragraph Density     (15) — Short paragraphs (≤3 sentences) ideal for RAG
 *   - Meta Description      (10) — Present and within length bounds
 *   - Image Alt Coverage    (10) — All images have descriptive alt text
 *   - Content Length        (10) — 300+ words for posts, 100+ for products
 *   - Internal Links        (10) — At least 2 internal links
 *   - FAQ / Q&A Schema      (5)  — Bonus for FAQPage or Q&A schema
 *   - Semantic HTML         (10) — Uses <article>, <section>, <nav>, <aside>
 *   - Clean URL             (5)  — Slug is short, kebab-case, no query string
 *
 * @param int $postId Post ID.
 * @return array{score:int,checks:array<int,array{label:string,pass:bool,hint:string}>,dimensions:array<int,array{label:string,score:int,max:int,formula:string}>}
 */
function shop_to_llm_seo_calculate_extractability(int $postId): array
{
    $post = get_post($postId);
    if (!$post instanceof WP_Post) {
        return ['score' => 0, 'checks' => [], 'dimensions' => []];
    }

    $content = (string) $post->post_content;
    $plain = wp_strip_all_tags($content);
    $wordCount = str_word_count($plain);
    $slug = $post->post_name;
    $metaDesc = (string) get_post_meta($postId, '_shop2llm_meta_desc', true);
    $isProduct = ($post->post_type === 'product');

    // ─── D1: Structured Data (15) ──────────────────────────────────────
    $d1Score = 0;
    $d1Checks = [];
    $hasSchema = (get_post_meta($postId, '_shop2llm_schema_json', true) !== '') || has_filter('wp_head', 'shop_to_llm_inject_product_schema');
    $d1Checks[] = ['label' => __('JSON-LD structured data attached', 'shop-to-llm'), 'pass' => $hasSchema, 'hint' => __('Schema.org Product/Organization auto-injected', 'shop-to-llm')];
    if ($hasSchema) {
        $d1Score += 10;
    }
    $hasFaq = (stripos($content, 'FAQPage') !== false) || (stripos($content, 'class="schema-faq') !== false);
    $d1Checks[] = ['label' => __('FAQPage or Q&A schema detected', 'shop-to-llm'), 'pass' => $hasFaq, 'hint' => __('Improves RAG answer extraction', 'shop-to-llm')];
    $d1Score += $hasFaq ? 5 : 0;

    // ─── D2: Heading Hierarchy (15) ────────────────────────────────────
    $d2Score = 0;
    $d2Checks = [];
    $h2Count = preg_match_all('/<h2[^>]*>/i', $content);
    $h3Count = preg_match_all('/<h3[^>]*>/i', $content);
    $hasH1 = preg_match('/<h1[^>]*>/i', $content);
    $d2Checks[] = ['label' => sprintf(/* translators: %d: number of H2 headings found */ __('Has at least 2 H2 headings (found %d)', 'shop-to-llm'), $h2Count), 'pass' => $h2Count >= 2, 'hint' => __('H2s split content into citable sections', 'shop-to-llm')];
    if ($h2Count >= 2) {
        $d2Score += 8;
    } elseif ($h2Count >= 1) {
        $d2Score += 4;
    }
    $d2Checks[] = ['label' => sprintf(/* translators: %d: number of H3 sub-headings found */ __('Uses H3 sub-headings (found %d)', 'shop-to-llm'), $h3Count), 'pass' => $h3Count >= 1, 'hint' => __('H3s enable nested semantic structure', 'shop-to-llm')];
    $d2Score += ($h3Count >= 1) ? 5 : 0;
    $d2Checks[] = ['label' => __('Has single H1 (title)', 'shop-to-llm'), 'pass' => $hasH1 || !empty($post->post_title), 'hint' => __('H1 is the primary topic', 'shop-to-llm')];
    $d2Score += ($hasH1 || !empty($post->post_title)) ? 2 : 0;

    // ─── D3: Paragraph Density (15) — RAG-friendliness ────────────────
    $d3Score = 0;
    $d3Checks = [];
    $paragraphs = preg_split('/\n\s*\n/', $plain);
    $paragraphs = array_filter($paragraphs, function ($p) { return trim($p) !== ''; });
    $paraCount = count($paragraphs);
    $shortParas = 0;
    foreach ($paragraphs as $p) {
        $sentences = preg_split('/[.!?]+/', $p);
        $sentences = array_filter($sentences, function ($s) { return trim($s) !== ''; });
        if (count($sentences) <= 3) {
            $shortParas++;
        }
    }
    $shortRatio = $paraCount > 0 ? ($shortParas / $paraCount) : 0;
    $d3Checks[] = ['label' => sprintf(/* translators: 1: short paragraph count, 2: total paragraph count */ __('%1$d of %2$d paragraphs are short (≤3 sentences)', 'shop-to-llm'), $shortParas, $paraCount), 'pass' => $shortRatio >= 0.6 && $paraCount >= 2, 'hint' => __('Short paragraphs are easier for RAG retrieval', 'shop-to-llm')];
    if ($shortRatio >= 0.6 && $paraCount >= 2) {
        $d3Score += 10;
    } elseif ($shortRatio >= 0.4) {
        $d3Score += 5;
    }
    $d3Checks[] = ['label' => sprintf(/* translators: %d: minimum paragraph count */ __('Has %d+ paragraphs', 'shop-to-llm'), 3), 'pass' => $paraCount >= 3, 'hint' => __('Multi-paragraph content is richer for extraction', 'shop-to-llm')];
    $d3Score += ($paraCount >= 3) ? 5 : 0;

    // ─── D4: Meta Description (10) ─────────────────────────────────────
    $d4Score = 0;
    $d4Checks = [];
    $descLen = function_exists('mb_strlen') ? mb_strlen($metaDesc) : strlen($metaDesc);
    $hasDesc = !empty($metaDesc);
    $d4Checks[] = ['label' => __('Meta description present', 'shop-to-llm'), 'pass' => $hasDesc, 'hint' => __('Required for AI summary snippet', 'shop-to-llm')];
    $d4Score += $hasDesc ? 5 : 0;
    $inLenRange = ($descLen >= 120 && $descLen <= 160);
    $d4Checks[] = ['label' => sprintf(/* translators: %d: meta description length in chars */ __('Meta description length %d chars (ideal 120-160)', 'shop-to-llm'), $descLen), 'pass' => $inLenRange, 'hint' => __('Optimal for AI snippet truncation', 'shop-to-llm')];
    $d4Score += $inLenRange ? 5 : 0;

    // ─── D5: Image Alt Coverage (10) ───────────────────────────────────
    $d5Score = 0;
    $d5Checks = [];
    $imgCount = preg_match_all('/<img[^>]+>/i', $content);
    $imgsWithAlt = 0;
    $imgsWithDescAlt = 0;
    if ($imgCount > 0 && preg_match_all('/<img[^>]+alt=["\']([^"\']*)["\'][^>]*>/i', $content, $altMatches)) {
        $imgsWithAlt = count($altMatches[0]);
        foreach ($altMatches[1] as $alt) {
            if (strlen($alt) >= 5) {
                $imgsWithDescAlt++;
            }
        }
    }
    $altCoverage = $imgCount > 0 ? ($imgsWithAlt / $imgCount) : 1;
    $descAltCoverage = $imgCount > 0 ? ($imgsWithDescAlt / $imgCount) : 1;
    $d5Checks[] = ['label' => sprintf(/* translators: 1: images with alt count, 2: total image count */ __('%1$d of %2$d images have alt text', 'shop-to-llm'), $imgsWithAlt, $imgCount), 'pass' => $altCoverage >= 0.8, 'hint' => __('Alt text is the primary image signal for AI', 'shop-to-llm')];
    $d5Score += ($altCoverage >= 0.8) ? 6 : ($altCoverage >= 0.5 ? 3 : 0);
    $d5Checks[] = ['label' => sprintf(/* translators: %d: descriptive alt image count */ __('%d images have descriptive alt (5+ chars)', 'shop-to-llm'), $imgsWithDescAlt), 'pass' => $descAltCoverage >= 0.6 || $imgCount === 0, 'hint' => __('"image1.jpg" is not descriptive', 'shop-to-llm')];
    $d5Score += ($descAltCoverage >= 0.6 || $imgCount === 0) ? 4 : 0;

    // ─── D6: Content Length (10) ───────────────────────────────────────
    $d6Score = 0;
    $d6Checks = [];
    $minWords = $isProduct ? 100 : 300;
    $idealWords = $isProduct ? 300 : 800;
    $d6Checks[] = ['label' => sprintf(/* translators: 1: word count, 2: minimum word count, 3: post type */ __('%1$d words (minimum %2$d for %3$s)', 'shop-to-llm'), $wordCount, $minWords, $isProduct ? 'product' : 'post'), 'pass' => $wordCount >= $minWords, 'hint' => __('AI needs enough text for context', 'shop-to-llm')];
    if ($wordCount >= $minWords) {
        $d6Score += 5;
    }
    $d6Checks[] = ['label' => sprintf(/* translators: 1: word count, 2: ideal word count */ __('%1$d words (ideal %2$d+)', 'shop-to-llm'), $wordCount, $idealWords), 'pass' => $wordCount >= $idealWords, 'hint' => __('Comprehensive content ranks better in AI answers', 'shop-to-llm')];
    if ($wordCount >= $idealWords) {
        $d6Score += 5;
    }

    // ─── D7: Internal Links (10) ───────────────────────────────────────
    $d7Score = 0;
    $d7Checks = [];
    $internalLinks = preg_match_all('/<a[^>]+href=["\'][^"\']*' . preg_quote((string) wp_parse_url(home_url(), PHP_URL_HOST), '/') . '[^"\']*["\'][^>]*>/i', $content);
    $d7Checks[] = ['label' => sprintf(/* translators: %d: internal link count */ __('%d internal links (need 2+)', 'shop-to-llm'), $internalLinks), 'pass' => $internalLinks >= 2, 'hint' => __('Internal links help AI agents navigate related content', 'shop-to-llm')];
    if ($internalLinks >= 2) {
        $d7Score += 6;
    } elseif ($internalLinks >= 1) {
        $d7Score += 3;
    }
    $d7Checks[] = ['label' => __('Has 5+ internal links', 'shop-to-llm'), 'pass' => $internalLinks >= 5, 'hint' => __('Strong internal linking improves knowledge graph extraction', 'shop-to-llm')];
    $d7Score += ($internalLinks >= 5) ? 4 : 0;

    // ─── D8: FAQ / Q&A Schema (5) — already counted in D1, kept for transparency ──
    $d8Score = 0;
    $d8Checks = [];
    $d8Checks[] = ['label' => __('Has FAQ section or Q&A markup', 'shop-to-llm'), 'pass' => $hasFaq, 'hint' => __('Q&A blocks are high-impact for AI citation', 'shop-to-llm')];
    $d8Score += $hasFaq ? 5 : 0;

    // ─── D9: Semantic HTML (10) ───────────────────────────────────────
    $d9Score = 0;
    $d9Checks = [];
    $semanticTags = 0;
    foreach (['article', 'section', 'nav', 'aside', 'header', 'footer', 'main', 'figure', 'figcaption'] as $tag) {
        if (preg_match('/<' . $tag . '[\s>]/i', $content)) {
            $semanticTags++;
        }
    }
    $d9Checks[] = ['label' => sprintf(/* translators: %d: semantic HTML5 tag count */ __('Uses %d of 9 semantic HTML5 tags', 'shop-to-llm'), $semanticTags), 'pass' => $semanticTags >= 2, 'hint' => __('<article>, <section>, <nav>, <aside>, <header>, <footer>, <main>, <figure>, <figcaption>', 'shop-to-llm')];
    $d9Score += ($semanticTags >= 2) ? 6 : ($semanticTags >= 1 ? 3 : 0);
    $d9Checks[] = ['label' => __('Uses 4+ semantic tags', 'shop-to-llm'), 'pass' => $semanticTags >= 4, 'hint' => __('Rich semantic structure improves agent parsing', 'shop-to-llm')];
    $d9Score += ($semanticTags >= 4) ? 4 : 0;

    // ─── D10: Clean URL (5) ───────────────────────────────────────────
    $d10Score = 0;
    $d10Checks = [];
    $slugLen = function_exists('mb_strlen') ? mb_strlen($slug) : strlen($slug);
    $isKebab = (bool) preg_match('/^[a-z0-9-]+$/', $slug);
    $noQuery = (strpos($slug, '?') === false);
    $d10Checks[] = ['label' => sprintf(/* translators: %s: post slug */ __('Slug is kebab-case (%s)', 'shop-to-llm'), $slug), 'pass' => $isKebab, 'hint' => __('Lowercase, hyphen-separated slugs are AI-friendly', 'shop-to-llm')];
    $d10Score += $isKebab ? 3 : 0;
    $d10Checks[] = ['label' => sprintf(/* translators: %d: slug length in chars */ __('Slug length %d chars (ideal ≤50)', 'shop-to-llm'), $slugLen), 'pass' => $slugLen > 0 && $slugLen <= 50, 'hint' => __('Short, descriptive slugs improve recall', 'shop-to-llm')];
    $d10Score += ($slugLen > 0 && $slugLen <= 50) ? 2 : 0;

    $dimensions = [
        ['id' => 'schema',     'label' => __('Structured Data', 'shop-to-llm'),     'score' => min(15, $d1Score), 'max' => 15, 'checks' => $d1Checks, 'formula' => 'JSON-LD (10) + FAQ (5)'],
        ['id' => 'headings',    'label' => __('Heading Hierarchy', 'shop-to-llm'),  'score' => min(15, $d2Score), 'max' => 15, 'checks' => $d2Checks, 'formula' => 'H2 count (8) + H3 (5) + H1 (2)'],
        ['id' => 'paragraphs',  'label' => __('Paragraph Density', 'shop-to-llm'), 'score' => min(15, $d3Score), 'max' => 15, 'checks' => $d3Checks, 'formula' => 'Short ratio (10) + count (5)'],
        ['id' => 'meta_desc',   'label' => __('Meta Description', 'shop-to-llm'),   'score' => min(10, $d4Score), 'max' => 10, 'checks' => $d4Checks, 'formula' => 'Present (5) + length (5)'],
        ['id' => 'images',      'label' => __('Image Alt Coverage', 'shop-to-llm'),'score' => min(10, $d5Score), 'max' => 10, 'checks' => $d5Checks, 'formula' => 'Coverage (6) + descriptive (4)'],
        ['id' => 'length',      'label' => __('Content Length', 'shop-to-llm'),     'score' => min(10, $d6Score), 'max' => 10, 'checks' => $d6Checks, 'formula' => 'Minimum (5) + ideal (5)'],
        ['id' => 'links',       'label' => __('Internal Links', 'shop-to-llm'),     'score' => min(10, $d7Score), 'max' => 10, 'checks' => $d7Checks, 'formula' => '2+ links (6) + 5+ (4)'],
        ['id' => 'faq',         'label' => __('FAQ Schema', 'shop-to-llm'),          'score' => min(5,  $d8Score), 'max' => 5,  'checks' => $d8Checks, 'formula' => 'FAQPage/Q&A (5)'],
        ['id' => 'semantic',    'label' => __('Semantic HTML', 'shop-to-llm'),      'score' => min(10, $d9Score), 'max' => 10, 'checks' => $d9Checks, 'formula' => '2+ tags (6) + 4+ (4)'],
        ['id' => 'url',         'label' => __('Clean URL', 'shop-to-llm'),          'score' => min(5,  $d10Score), 'max' => 5,  'checks' => $d10Checks, 'formula' => 'Kebab (3) + length (2)'],
    ];

    $total = array_sum(array_column($dimensions, 'score'));
    $allChecks = [];
    foreach ($dimensions as $dim) {
        foreach ($dim['checks'] as $check) {
            $allChecks[] = $check;
        }
    }

    return [
        'score'      => $total,
        'checks'      => $allChecks,
        'dimensions' => $dimensions,
        'formula'     => 'Σ (Schema 15 + Headings 15 + Paragraphs 15 + Meta 10 + Images 10 + Length 10 + Links 10 + FAQ 5 + Semantic 10 + URL 5) = 100',
    ];
}

/**
 * Render semicircular gauge SVG for Extractability score.
 *
 * @param int    $score      Score 0-100.
 * @param string $scoreColor Hex color for the score arc.
 * @param string $scoreLabel Translated label (e.g. "Excellent").
 */
function shop_to_llm_render_semicircle_gauge_svg(int $score, string $scoreColor, string $scoreLabel): void
{
    $radius = 90;
    $semiCirc = M_PI * $radius; // ≈ 282.74
    $offset = $semiCirc * (1 - ($score / 100));
    $ariaLabel = sprintf(/* translators: %d: citability score 0-100 */ __('Citability score %d out of 100', 'shop-to-llm'), $score);
    ?>
    <svg width="200" height="110" viewBox="0 0 200 110" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="<?php echo esc_attr($ariaLabel); ?>">
        <path d="M 10 100 A 90 90 0 0 1 190 100" fill="none" stroke="#e2e8f0" stroke-width="12" stroke-linecap="round" />
        <path d="M 10 100 A 90 90 0 0 1 190 100" fill="none" stroke="<?php echo esc_attr($scoreColor); ?>" stroke-width="12" stroke-linecap="round" stroke-dasharray="<?php echo esc_attr(sprintf('%.2f', $semiCirc)); ?>" stroke-dashoffset="<?php echo esc_attr(sprintf('%.2f', $offset)); ?>" />
        <text x="100" y="82" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" font-size="36" font-weight="700" fill="#0f172a"><?php echo esc_html($score); ?></text>
        <text x="100" y="100" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" font-size="10" fill="#64748b">/ 100 · <?php echo esc_html($scoreLabel); ?></text>
    </svg>
    <?php
}

/**
 * Render 10-dimension stacked bar HTML (echoes directly).
 *
 * @param array $dims Dimensions array from calculate_extractability().
 */
function shop_to_llm_render_extractability_stack_html(array $dims): void
{
    foreach ($dims as $dim) {
        $ratio = $dim['max'] / 100;
        $widthPct = round($ratio * 100, 1);
        $fillRatio = $dim['max'] > 0 ? ($dim['score'] / $dim['max']) : 0;
        $c = $fillRatio >= 0.7 ? '#16a34a' : ($fillRatio >= 0.4 ? '#f59e0b' : '#dc2626');
        $bg = $dim['score'] > 0 ? $c : '#e2e8f0';
        $title = sprintf('%s: %d/%d', $dim['label'], $dim['score'], $dim['max']);
        $showLabel = $widthPct >= 8;
        ?>
        <div class="s2ll-ext-stack-seg" style="width:<?php echo esc_attr($widthPct); ?>%;background:<?php echo esc_attr($bg); ?>" title="<?php echo esc_attr($title); ?>">
            <?php if ($showLabel) : ?>
                <?php echo esc_html($dim['score']); ?>
            <?php endif; ?>
        </div>
        <?php
    }
}

/**
 * Render Live Content Extractability meta box.
 * Visible in both Classic (sidebar) and Gutenberg (Plugin Sidebar) editors.
 */
function shop_to_llm_seo_render_extractability_meta_box(WP_Post $post): void
{
    $ext = shop_to_llm_seo_calculate_extractability($post->ID);
    $score = $ext['score'];
    $dims = $ext['dimensions'];

    $scoreClass = 'red';
    $scoreColor = '#dc2626';
    $scoreLabel = __('Needs work', 'shop-to-llm');
    if ($score >= 70) {
        $scoreClass = 'green';
        $scoreColor = '#16a34a';
        $scoreLabel = __('Excellent', 'shop-to-llm');
    } elseif ($score >= 40) {
        $scoreClass = 'yellow';
        $scoreColor = '#f59e0b';
        $scoreLabel = __('Fair', 'shop-to-llm');
    }
    ?>
    <style>
    .s2ll-ext-wrap{font-size:12px;color:#475569;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
    .s2ll-ext-gauge{display:flex;flex-direction:column;align-items:center;padding:4px 0 8px;border-bottom:1px solid #e2e8f0;margin-bottom:10px}
    .s2ll-ext-gauge svg{display:block}
    .s2ll-ext-stack{display:flex;height:24px;border-radius:6px;overflow:hidden;margin-bottom:4px;box-shadow:0 1px 2px rgba(0,0,0,.06)}
    .s2ll-ext-stack-seg{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;transition:opacity .2s;cursor:default;border-right:1px solid rgba(255,255,255,.4)}
    .s2ll-ext-stack-seg:last-child{border-right:none}
    .s2ll-ext-stack-seg:hover{opacity:.85}
    .s2ll-ext-stack-meta{display:flex;justify-content:space-between;font-size:10px;color:#64748b;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid #e2e8f0}
    .s2ll-ext-stack-meta strong{color:#0f172a;font-weight:700}
    .s2ll-ext-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
    .s2ll-ext-dim{padding:6px 8px;border-radius:5px;background:#fff;border:1px solid #e2e8f0;transition:border-color .2s}
    .s2ll-ext-dim:hover{border-color:#cbd5e1}
    .s2ll-ext-dim-head{display:flex;justify-content:space-between;align-items:center;font-size:10px;font-weight:600;color:#334155;margin-bottom:4px}
    .s2ll-ext-dim-score{color:#64748b;font-weight:700}
    .s2ll-ext-dim-bar{height:4px;background:#e2e8f0;border-radius:2px;overflow:hidden}
    .s2ll-ext-dim-fill{height:100%;border-radius:2px;transition:width .4s ease,background .3s}
    .s2ll-ext-dim-formula{font-size:9px;color:#94a3b8;margin-top:3px;font-style:italic}
    .s2ll-ext-footer{font-size:10px;color:#94a3b8;text-align:center;margin-top:8px;font-style:italic}
    .s2ll-ext-live-badge{display:inline-block;padding:2px 6px;border-radius:3px;background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e40af;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;margin-left:6px}
    @media (max-width:280px){.s2ll-ext-grid{grid-template-columns:1fr}}
    </style>

    <div class="s2ll-ext-wrap" id="s2ll-ext-container" data-post-id="<?php echo esc_attr($post->ID); ?>">
        <!-- Semicircular Gauge -->
        <div class="s2ll-ext-gauge" id="s2ll-ext-gauge">
            <?php shop_to_llm_render_semicircle_gauge_svg($score, $scoreColor, $scoreLabel); ?>
        </div>

        <!-- 10-Dimension Stacked Bar -->
        <div class="s2ll-ext-stack" id="s2ll-ext-stack">
            <?php shop_to_llm_render_extractability_stack_html($dims); ?>
        </div>
        <div class="s2ll-ext-stack-meta">
            <span><?php echo esc_html(sprintf(/* translators: %d: dimension count */ __('%d Dimensions', 'shop-to-llm'), count($dims))); ?></span>
            <span><?php esc_html_e('Total', 'shop-to-llm'); ?>: <strong id="s2ll-ext-total"><?php echo esc_html($score); ?></strong>/100</span>
        </div>

        <!-- Dimensions Detail Grid -->
        <div class="s2ll-ext-grid">
            <?php foreach ($dims as $dim) :
                $fillRatio = $dim['max'] > 0 ? ($dim['score'] / $dim['max']) : 0;
                $fillPct = round($fillRatio * 100);
                $c = $fillRatio >= 0.7 ? '#16a34a' : ($fillRatio >= 0.4 ? '#f59e0b' : '#dc2626');
                ?>
                <div class="s2ll-ext-dim" id="s2ll-ext-dim-<?php echo esc_attr($dim['id']); ?>">
                    <div class="s2ll-ext-dim-head">
                        <span><?php echo esc_html($dim['label']); ?></span>
                        <span class="s2ll-ext-dim-score"><?php echo esc_html($dim['score']); ?>/<?php echo esc_html($dim['max']); ?></span>
                    </div>
                    <div class="s2ll-ext-dim-bar">
                        <div class="s2ll-ext-dim-fill" style="width:<?php echo esc_attr($fillPct); ?>%;background:<?php echo esc_attr($c); ?>"></div>
                    </div>
                    <div class="s2ll-ext-dim-formula"><?php echo esc_html($dim['formula']); ?></div>
                </div>
            <?php endforeach; ?>
        </div>

        <p class="s2ll-ext-footer">
            <?php esc_html_e('Recomputes on save. Mirrors VigIA citability methodology.', 'shop-to-llm'); ?>
            <span class="s2ll-ext-live-badge"><?php esc_html_e('Live', 'shop-to-llm'); ?></span>
        </p>
    </div>

    <script>
    (function() {
        'use strict';
        // Live updater: recompute on content change (debounced 1.5s)
        var container = document.getElementById('s2ll-ext-container');
        if (!container) return;
        var postId = container.getAttribute('data-post-id');
        var debounceTimer = null;

        function setupHook() {
            // Classic editor (TinyMCE)
            if (typeof tinymce !== 'undefined') {
                var editor = tinymce.get('content');
                if (editor) {
                    editor.on('KeyUp NodeChange', scheduleRecompute);
                    return;
                }
                setTimeout(setupHook, 1000);
                return;
            }
            // Gutenberg
            if (wp.data && wp.data.select('core/editor')) {
                var lastContent = '';
                setInterval(function() {
                    var cur = wp.data.select('core/editor').getEditedPostContent();
                    if (cur && cur !== lastContent) {
                        lastContent = cur;
                        scheduleRecompute();
                    }
                }, 2000);
            }
        }

        function scheduleRecompute() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(recompute, 1500);
        }

        function recompute() {
            if (!window.shop2llm || !window.shop2llm.ajaxurl) return;
            var body = new URLSearchParams();
            body.append('action', 'shop2llm_recompute_extractability');
            body.append('post_id', postId);
            body.append('nonce', window.shop2llm.nonce);
            // Send live content (truncated to keep payload small)
            var liveContent = '';
            if (typeof tinymce !== 'undefined' && tinymce.get('content')) {
                liveContent = tinymce.get('content').getContent();
            } else if (wp.data && wp.data.select('core/editor')) {
                liveContent = wp.data.select('core/editor').getEditedPostContent();
            }
            body.append('content', liveContent.substring(0, 50000));

            fetch(window.shop2llm.ajaxurl, {
                method: 'POST',
                headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                body: body.toString()
            }).then(function(r) { return r.json(); }).then(function(resp) {
                if (!resp || !resp.success || !resp.data) return;
                updateUI(resp.data);
            }).catch(function() { /* silent fail — score reverts to server-side cached value */ });
        }

        function updateUI(data) {
            // Update gauge SVG
            var gauge = document.getElementById('s2ll-ext-gauge');
            if (gauge && data.gauge_html) {
                gauge.innerHTML = data.gauge_html;
            }
            // Update stacked bar
            var stack = document.getElementById('s2ll-ext-stack');
            if (stack && data.stack_html) {
                stack.innerHTML = data.stack_html;
            }
            // Update total
            var totalEl = document.getElementById('s2ll-ext-total');
            if (totalEl) {
                totalEl.textContent = data.score;
            }
            // Update dimensions
            if (data.dimensions) {
                data.dimensions.forEach(function(dim) {
                    var dimEl = document.getElementById('s2ll-ext-dim-' + dim.id);
                    if (dimEl) {
                        var scoreEl = dimEl.querySelector('.s2ll-ext-dim-score');
                        if (scoreEl) scoreEl.textContent = dim.score + '/' + dim.max;
                        var fill = dimEl.querySelector('.s2ll-ext-dim-fill');
                        if (fill) {
                            var ratio = dim.max > 0 ? (dim.score / dim.max) : 0;
                            var pct = Math.round(ratio * 100);
                            var c = ratio >= 0.7 ? '#16a34a' : (ratio >= 0.4 ? '#f59e0b' : '#dc2626');
                            fill.style.width = pct + '%';
                            fill.style.background = c;
                        }
                    }
                });
            }
        }

        if (document.readyState === 'complete') {
            setupHook();
        } else {
            window.addEventListener('load', setupHook);
        }
    })();
    </script>
    <?php
}

// AJAX handler for live extractability recompute
function shop_to_llm_handle_recompute_extractability(): void
{
    check_ajax_referer('shop2llm_admin_nonce', 'nonce');
    if (!current_user_can('edit_posts')) {
        wp_die('', 403);
    }
    $postId = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
    if ($postId <= 0) {
        wp_send_json_error(['message' => 'Invalid post ID']);
    }
    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized — content is HTML passed to wp_kses_post then assigned to post_content (analysis context only, never persisted)
    $liveContent = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    // Temporarily override post content for the analysis
    $post = get_post($postId);
    if (!$post) {
        wp_send_json_error(['message' => 'Post not found']);
    }
    if ($liveContent !== '') {
        $post->post_content = $liveContent;
        // Temporarily set the global post for the analysis hooks
        $GLOBALS['post'] = $post;
    }

    $ext = shop_to_llm_seo_calculate_extractability($postId);
    $score = $ext['score'];
    $scoreClass = $score >= 70 ? 'green' : ($score >= 40 ? 'yellow' : 'red');
    $scoreColor = $score >= 70 ? '#16a34a' : ($score >= 40 ? '#f59e0b' : '#dc2626');
    $scoreLabel = $score >= 70 ? __('Excellent', 'shop-to-llm') : ($score >= 40 ? __('Fair', 'shop-to-llm') : __('Needs work', 'shop-to-llm'));

    // Capture gauge SVG HTML for AJAX injection
    ob_start();
    shop_to_llm_render_semicircle_gauge_svg($score, $scoreColor, $scoreLabel);
    $gaugeHtml = (string) ob_get_clean();

    // Capture stacked bar HTML for AJAX injection
    ob_start();
    shop_to_llm_render_extractability_stack_html($ext['dimensions']);
    $stackHtml = (string) ob_get_clean();

    wp_send_json_success([
        'score'       => $score,
        'score_class' => $scoreClass,
        'score_color' => $scoreColor,
        'score_label' => $scoreLabel,
        'gauge_html'  => $gaugeHtml,
        'stack_html'  => $stackHtml,
        'dimensions'  => $ext['dimensions'],
    ]);
}

// Localize shop2llm object on edit screens too (needed for live extractability)
add_action('admin_enqueue_scripts', function (string $hook) {
    if (!in_array($hook, ['post.php', 'post-new.php', 'edit.php'], true)) {
        return;
    }
    wp_localize_script('shop2llm-admin', 'shop2llm', [
        'nonce'   => wp_create_nonce('shop2llm_admin_nonce'),
        'ajaxurl' => admin_url('admin-ajax.php'),
        'restUrl' => rest_url('shop2llm/v1/'),
    ]);
    // admin.js may not be enqueued here — register inline fallback
    if (!wp_script_is('shop2llm-admin', 'enqueued')) {
        wp_register_script('shop2llm-admin-fallback', '', [], SHOP_TO_LLM_VERSION, true);
        wp_enqueue_script('shop2llm-admin-fallback');
        wp_localize_script('shop2llm-admin-fallback', 'shop2llm', [
            'nonce'   => wp_create_nonce('shop2llm_admin_nonce'),
            'ajaxurl' => admin_url('admin-ajax.php'),
            'restUrl' => rest_url('shop2llm/v1/'),
        ]);
    }
});

// ─── SEO Hook Registration ────────────────────────────────────────────

add_action('add_meta_boxes', 'shop_to_llm_seo_add_meta_boxes');
add_action('save_post', 'shop_to_llm_seo_save_meta');
add_action('wp_head', 'shop_to_llm_seo_inject_meta', 1);
add_action('wp_ajax_shop2llm_recompute_extractability', 'shop_to_llm_handle_recompute_extractability');

function shop_to_llm_seo_inject_og(): void
{
    if (get_option('shop_to_llm_seo_og', 'yes') !== 'yes') {
        return;
    }
    if (shop_to_llm_has_seo_plugin()) {
        return;
    }

    $site_name = get_bloginfo('name');
    $site_desc = get_bloginfo('description');
    $site_logo_id = get_theme_mod('custom_logo');
    $site_logo_url = $site_logo_id ? wp_get_attachment_image_url($site_logo_id, 'full') : '';
    $image_url = '';
    $image_width = 0;
    $image_height = 0;

    if (is_singular()) {
        global $post;
        if (!$post) {
            return;
        }

        $title = get_post_meta($post->ID, '_shop2llm_seo_title', true) ?: get_the_title($post);
        $desc = get_post_meta($post->ID, '_shop2llm_meta_desc', true);

        if (empty($desc)) {
            if (post_type_supports($post->post_type, 'excerpt') && !empty($post->post_excerpt)) {
                $desc = wp_strip_all_tags($post->post_excerpt);
            } else {
                $desc = wp_trim_words(wp_strip_all_tags($post->post_content), 30, '');
            }
        }

        $url = get_permalink($post);
        $og_image_meta = get_post_meta($post->ID, '_shop2llm_og_image', true);
        $thumb_id = get_post_thumbnail_id($post);

        if (!empty($og_image_meta) && is_numeric($og_image_meta)) {
            $img_data = wp_get_attachment_image_src((int) $og_image_meta, 'full');
            if ($img_data) {
                $image_url = $img_data[0];
                $image_width = (int) $img_data[1];
                $image_height = (int) $img_data[2];
            }
        } elseif ($thumb_id) {
            $img_data = wp_get_attachment_image_src($thumb_id, 'full');
            if ($img_data) {
                $image_url = $img_data[0];
                $image_width = (int) $img_data[1];
                $image_height = (int) $img_data[2];
            }
        }

        if (empty($image_url) && $site_logo_url) {
            $image_url = $site_logo_url;
        }

        if (shop_to_llm_has_woocommerce() && is_product()) {
            $og_type = 'product';
        } elseif ($post->post_type === 'page') {
            $og_type = 'website';
        } else {
            $og_type = 'article';
        }

        echo '<meta property="og:site_name" content="' . esc_attr($site_name) . '">' . "\n";
        echo '<meta property="og:title" content="' . esc_attr($title) . '">' . "\n";
        echo '<meta property="og:description" content="' . esc_attr($desc) . '">' . "\n";
        echo '<meta property="og:url" content="' . esc_url($url) . '">' . "\n";
        echo '<meta property="og:type" content="' . esc_attr($og_type) . '">' . "\n";

        if ($image_url) {
            echo '<meta property="og:image" content="' . esc_url($image_url) . '">' . "\n";
            if ($image_width > 0 && $image_height > 0) {
                echo '<meta property="og:image:width" content="' . esc_attr((string) $image_width) . '">' . "\n";
                echo '<meta property="og:image:height" content="' . esc_attr((string) $image_height) . '">' . "\n";
            }
        }

        if ($og_type === 'article') {
            echo '<meta property="article:published_time" content="' . esc_attr(get_the_date('c', $post)) . '">' . "\n";
            echo '<meta property="article:modified_time" content="' . esc_attr(get_the_modified_date('c', $post)) . '">' . "\n";
        }

        // Twitter Card
        $twitter_card = $image_url ? 'summary_large_image' : 'summary';
        echo '<meta name="twitter:card" content="' . esc_attr($twitter_card) . '">' . "\n";
        echo '<meta name="twitter:title" content="' . esc_attr($title) . '">' . "\n";
        echo '<meta name="twitter:description" content="' . esc_attr($desc) . '">' . "\n";
        if ($image_url) {
            echo '<meta name="twitter:image" content="' . esc_url($image_url) . '">' . "\n";
        }
    } elseif (is_category() || is_tag() || is_tax()) {
        $term = get_queried_object();
        if (!$term) { return; }
        echo '<meta property="og:site_name" content="' . esc_attr(get_bloginfo('name')) . '" />' . "\n";
        echo '<meta property="og:title" content="' . esc_attr($term->name) . '" />' . "\n";
        if ($term->description) {
            echo '<meta property="og:description" content="' . esc_attr(wp_trim_words($term->description, 30)) . '" />' . "\n";
        }
        echo '<meta property="og:url" content="' . esc_url(get_term_link($term)) . '" />' . "\n";
        echo '<meta property="og:type" content="website" />' . "\n";
        echo '<meta name="twitter:card" content="summary" />' . "\n";
        echo '<meta name="twitter:title" content="' . esc_attr($term->name) . '" />' . "\n";
        return;
    } elseif (is_front_page() || is_home()) {
        $url = home_url('/');

        if ($site_logo_url) {
            $image_url = $site_logo_url;
        }

        echo '<meta property="og:site_name" content="' . esc_attr($site_name) . '">' . "\n";
        echo '<meta property="og:title" content="' . esc_attr($site_name) . '">' . "\n";
        echo '<meta property="og:description" content="' . esc_attr($site_desc) . '">' . "\n";
        echo '<meta property="og:url" content="' . esc_url($url) . '">' . "\n";
        echo '<meta property="og:type" content="website">' . "\n";

        if ($image_url) {
            echo '<meta property="og:image" content="' . esc_url($image_url) . '">' . "\n";
        }

        $twitter_card = $image_url ? 'summary_large_image' : 'summary';
        echo '<meta name="twitter:card" content="' . esc_attr($twitter_card) . '">' . "\n";
        echo '<meta name="twitter:title" content="' . esc_attr($site_name) . '">' . "\n";
        echo '<meta name="twitter:description" content="' . esc_attr($site_desc) . '">' . "\n";
        if ($image_url) {
            echo '<meta name="twitter:image" content="' . esc_url($image_url) . '">' . "\n";
        }
    }
}

function shop_to_llm_seo_add_og_image_meta(): void
{
    global $post;

    $screen = get_current_screen();
    if (!$screen || !$screen->is_block_editor) {
        add_meta_box(
            'shop2llm_og_image',
            __('OG Image', 'shop-to-llm'),
            'shop_to_llm_seo_og_image_meta_box_html',
            null,
            'side',
            'low'
        );
    }
}

function shop_to_llm_seo_og_image_meta_box_html(WP_Post $post): void
{
    wp_nonce_field('shop2llm_og_image_save', 'shop2llm_og_image_nonce');
    $og_image_id = get_post_meta($post->ID, '_shop2llm_og_image', true);
    $og_image_url = $og_image_id ? wp_get_attachment_image_url((int) $og_image_id, 'full') : '';
    ?>
    <div class="shop2llm-og-image-field">
        <div id="shop2llm-og-image-preview" style="margin-bottom:8px">
            <?php if ($og_image_url): ?>
                <img src="<?php echo esc_url($og_image_url); ?>" style="max-width:100%;height:auto" alt="">
            <?php endif; ?>
        </div>
        <input type="hidden" name="shop2llm_og_image_id" id="shop2llm-og-image-id" value="<?php echo esc_attr($og_image_id); ?>">
        <button type="button" class="button button-secondary" id="shop2llm-og-image-upload"><?php echo esc_html__('Select Image', 'shop-to-llm'); ?></button>
        <button type="button" class="button button-secondary" id="shop2llm-og-image-remove" style="<?php echo $og_image_id ? '' : 'display:none'; ?>"><?php echo esc_html__('Remove', 'shop-to-llm'); ?></button>
    </div>
    <script>
    (function() {
        var frame;
        var uploadBtn = document.getElementById('shop2llm-og-image-upload');
        var removeBtn = document.getElementById('shop2llm-og-image-remove');
        var inputId = document.getElementById('shop2llm-og-image-id');
        var preview = document.getElementById('shop2llm-og-image-preview');

        if (uploadBtn) {
            uploadBtn.addEventListener('click', function() {
                if (!frame) {
                    frame = wp.media({
                        title: '<?php echo esc_js(__('Select OG Image', 'shop-to-llm')); ?>',
                        button: { text: '<?php echo esc_js(__('Use this image', 'shop-to-llm')); ?>' },
                        multiple: false,
                        library: { type: 'image' }
                    });
                    frame.on('select', function() {
                        var attachment = frame.state().get('selection').first().toJSON();
                        inputId.value = attachment.id;
                        preview.innerHTML = '<img src="' + attachment.url + '" style="max-width:100%;height:auto" alt="">';
                        removeBtn.style.display = '';
                    });
                }
                frame.open();
            });
        }

        if (removeBtn) {
            removeBtn.addEventListener('click', function() {
                inputId.value = '';
                preview.innerHTML = '';
                removeBtn.style.display = 'none';
            });
        }
    })();
    </script>
    <?php
}

function shop_to_llm_seo_save_og_image_meta(int $post_id): void
{
    if (!isset($_POST['shop2llm_og_image_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_og_image_nonce'])), 'shop2llm_og_image_save')) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $post_id)) {
        return;
    }

    $og_image_id = isset($_POST['shop2llm_og_image_id']) ? absint($_POST['shop2llm_og_image_id']) : 0;
    if ($og_image_id) {
        update_post_meta($post_id, '_shop2llm_og_image', $og_image_id);
    } else {
        delete_post_meta($post_id, '_shop2llm_og_image');
    }
}

// ─── Redirect Manager Module ───────────────────────────────────────────

function shop_to_llm_seo_redirect_admin_page(): void
{
    if (get_option('shop_to_llm_seo_redirect', 'yes') !== 'yes') {
        return;
    }

    add_submenu_page(
        'shop2llm',
        __('Redirects', 'shop-to-llm'),
        __('Redirects', 'shop-to-llm'),
        'manage_options',
        'shop2llm-seo-redirects',
        'shop_to_llm_seo_render_redirects_page'
    );
}

function shop_to_llm_seo_render_redirects_page(): void
{
    if (!current_user_can('manage_options')) {
        return;
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';
    $log_table = $wpdb->prefix . 's2ll_404_log';

    // Handle actions
    // phpcs:disable WordPress.Security.NonceVerification.Recommended -- admin page GET checks
    $action = isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '';
    $redirect_id = isset($_GET['redirect_id']) ? absint($_GET['redirect_id']) : 0;
    // phpcs:enable

    if ($action === 'edit' && $redirect_id > 0) {
        $redirect = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $redirect_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    // Handle bulk delete
    // phpcs:disable WordPress.Security.NonceVerification.Missing -- checked below
    if (isset($_POST['bulk_delete']) && isset($_POST['redirect_ids'])) {
        // phpcs:enable
        if (!isset($_POST['shop2llm_redirect_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_redirect_nonce'])), 'shop2llm_redirect_bulk')) {
            wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
        }
        $ids = array_map('absint', (array) $_POST['redirect_ids']);
        if (!empty($ids)) {
            $placeholders = implode(',', array_fill(0, count($ids), '%d'));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})", ...$ids));
        }
        echo '<div class="notice notice-success"><p>' . esc_html__('Redirects deleted.', 'shop-to-llm') . '</p></div>';
    }

    // Handle 404 log delete
    if (isset($_POST['delete_404']) && isset($_POST['log_ids'])) {
        if (!isset($_POST['shop2llm_404_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_404_nonce'])), 'shop2llm_404_bulk')) {
            wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
        }
        $ids = array_map('absint', (array) $_POST['log_ids']);
        if (!empty($ids)) {
            $placeholders = implode(',', array_fill(0, count($ids), '%d'));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
            $wpdb->query($wpdb->prepare("DELETE FROM {$log_table} WHERE id IN ({$placeholders})", ...$ids));
        }
        echo '<div class="notice notice-success"><p>' . esc_html__('404 log entries deleted.', 'shop-to-llm') . '</p></div>';
    }

    // Pagination
    // phpcs:disable WordPress.Security.NonceVerification.Recommended
    $paged = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
    // phpcs:enable
    $per_page = 20;
    $offset = ($paged - 1) * $per_page;

    $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $redirects = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $per_page, $offset)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $total_pages = (int) ceil($total / $per_page);

    // 404 log pagination
    // phpcs:disable WordPress.Security.NonceVerification.Recommended
    $log_paged = isset($_GET['log_paged']) ? absint($_GET['log_paged']) : 1;
    // phpcs:enable
    $log_offset = ($log_paged - 1) * $per_page;

    $log_total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$log_table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $log_entries = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$log_table} ORDER BY last_hit DESC LIMIT %d OFFSET %d", $per_page, $log_offset)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $log_total_pages = (int) ceil($log_total / $per_page);

    $edit_redirect = isset($redirect) ? $redirect : null;
    ?>
    <div class="wrap">
        <h1><?php echo esc_html__('Redirect Manager', 'shop-to-llm'); ?></h1>

        <!-- Add / Edit Redirect Form -->
        <h2><?php echo $edit_redirect ? esc_html__('Edit Redirect', 'shop-to-llm') : esc_html__('Add New Redirect', 'shop-to-llm'); ?></h2>
        <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
            <input type="hidden" name="action" value="shop2llm_save_redirect">
            <?php wp_nonce_field('shop2llm_save_redirect', 'shop2llm_redirect_nonce'); ?>
            <?php if ($edit_redirect): ?>
                <input type="hidden" name="redirect_id" value="<?php echo esc_attr($edit_redirect->id); ?>">
            <?php endif; ?>
            <table class="form-table">
                <tr>
                    <th><label for="source_url"><?php echo esc_html__('Source URL', 'shop-to-llm'); ?></label></th>
                    <td><input type="text" name="source_url" id="source_url" class="regular-text" value="<?php echo $edit_redirect ? esc_attr($edit_redirect->source_url) : ''; ?>" required placeholder="<?php echo esc_attr('/old-page/', 'shop-to-llm'); ?>"></td>
                </tr>
                <tr>
                    <th><label for="target_url"><?php echo esc_html__('Target URL', 'shop-to-llm'); ?></label></th>
                    <td><input type="text" name="target_url" id="target_url" class="regular-text" value="<?php echo $edit_redirect ? esc_attr($edit_redirect->target_url) : ''; ?>" placeholder="<?php echo esc_attr('/new-page/', 'shop-to-llm'); ?>"></td>
                </tr>
                <tr>
                    <th><label for="status_code"><?php echo esc_html__('Status Code', 'shop-to-llm'); ?></label></th>
                    <td>
                        <select name="status_code" id="status_code">
                            <option value="301" <?php echo $edit_redirect && $edit_redirect->status_code == 301 ? 'selected' : ''; ?>>301 <?php echo esc_html__('Moved Permanently', 'shop-to-llm'); ?></option>
                            <option value="302" <?php echo $edit_redirect && $edit_redirect->status_code == 302 ? 'selected' : ''; ?>>302 <?php echo esc_html__('Found', 'shop-to-llm'); ?></option>
                            <option value="307" <?php echo $edit_redirect && $edit_redirect->status_code == 307 ? 'selected' : ''; ?>>307 <?php echo esc_html__('Temporary Redirect', 'shop-to-llm'); ?></option>
                            <option value="410" <?php echo $edit_redirect && $edit_redirect->status_code == 410 ? 'selected' : ''; ?>>410 <?php echo esc_html__('Gone', 'shop-to-llm'); ?></option>
                            <option value="451" <?php echo $edit_redirect && $edit_redirect->status_code == 451 ? 'selected' : ''; ?>>451 <?php echo esc_html__('Unavailable For Legal Reasons', 'shop-to-llm'); ?></option>
                        </select>
                    </td>
                </tr>
                <tr>
                    <th><?php echo esc_html__('Regex', 'shop-to-llm'); ?></th>
                    <td><label><input type="checkbox" name="is_regex" value="1" <?php echo $edit_redirect && $edit_redirect->is_regex ? 'checked' : ''; ?>> <?php echo esc_html__('Use regular expression for source URL matching', 'shop-to-llm'); ?></label></td>
                </tr>
            </table>
            <?php submit_button($edit_redirect ? __('Update Redirect', 'shop-to-llm') : __('Add Redirect', 'shop-to-llm')); ?>
            <?php if ($edit_redirect): ?>
                <a href="<?php echo esc_url(admin_url('admin.php?page=shop2llm-seo-redirects')); ?>" class="button button-secondary"><?php echo esc_html__('Cancel', 'shop-to-llm'); ?></a>
            <?php endif; ?>
        </form>

        <hr>

        <!-- Redirect List -->
        <h2><?php echo esc_html__('Existing Redirects', 'shop-to-llm'); ?> (<?php echo esc_html((string) $total); ?>)</h2>
        <form method="post" action="">
            <?php wp_nonce_field('shop2llm_redirect_bulk', 'shop2llm_redirect_nonce'); ?>
            <table class="wp-list-table widefat fixed striped">
                <thead>
                    <tr>
                        <td class="check-column"><input type="checkbox" id="select-all-redirects"></td>
                        <th><?php echo esc_html__('Source', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Target', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Status', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Regex', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Actions', 'shop-to-llm'); ?></th>
                    </tr>
                </thead>
                <tbody>
                <?php if (empty($redirects)): ?>
                    <tr><td colspan="6"><?php echo esc_html__('No redirects found.', 'shop-to-llm'); ?></td></tr>
                <?php else: ?>
                    <?php foreach ($redirects as $r): ?>
                    <tr>
                        <th class="check-column"><input type="checkbox" name="redirect_ids[]" value="<?php echo esc_attr($r->id); ?>"></th>
                        <td><code><?php echo esc_html($r->source_url); ?></code></td>
                        <td><code><?php echo esc_html($r->target_url); ?></code></td>
                        <td><?php echo esc_html((string) $r->status_code); ?></td>
                        <td><?php echo $r->is_regex ? esc_html__('Yes', 'shop-to-llm') : esc_html__('No', 'shop-to-llm'); ?></td>
                        <td>
                            <a href="<?php echo esc_url(add_query_arg(['action' => 'edit', 'redirect_id' => $r->id], admin_url('admin.php?page=shop2llm-seo-redirects'))); ?>"><?php echo esc_html__('Edit', 'shop-to-llm'); ?></a>
                            <span style="margin:0 4px">|</span>
                            <a href="<?php echo esc_url(wp_nonce_url(add_query_arg(['action' => 'delete', 'redirect_id' => $r->id], admin_url('admin-post.php')), 'shop2llm_delete_redirect_' . $r->id)); ?>" onclick="return confirm('<?php echo esc_js(__('Delete this redirect?', 'shop-to-llm')); ?>');" style="color:#a00"><?php echo esc_html__('Delete', 'shop-to-llm'); ?></a>
                        </td>
                    </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
                </tbody>
            </table>
            <div class="tablenav bottom">
                <div class="alignleft actions">
                    <button type="submit" name="bulk_delete" class="button" onclick="return confirm('<?php echo esc_js(__('Delete selected redirects?', 'shop-to-llm')); ?>');"><?php echo esc_html__('Delete Selected', 'shop-to-llm'); ?></button>
                </div>
                <?php if ($total_pages > 1): ?>
                <div class="tablenav-pages">
                    <span class="displaying-num"><?php echo sprintf(/* translators: %s: total items */ esc_html__('%s items', 'shop-to-llm'), esc_html(number_format_i18n($total))); ?></span>
                    <span class="pagination-links">
                    <?php
                    for ($i = 1; $i <= $total_pages; $i++) {
                        $active = $i === $paged ? 'font-weight:bold' : '';
                        echo '<a href="' . esc_url(add_query_arg('paged', $i)) . '" style="margin-right:4px;' . esc_attr($active) . '">' . esc_html((string) $i) . '</a>';
                    }
                    ?>
                    </span>
                </div>
                <?php endif; ?>
            </div>
        </form>

        <hr>

        <!-- CSV Import -->
        <h2><?php echo esc_html__('CSV Import', 'shop-to-llm'); ?></h2>
        <form method="post" enctype="multipart/form-data" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
            <input type="hidden" name="action" value="shop2llm_import_csv">
            <?php wp_nonce_field('shop2llm_import_csv', 'shop2llm_csv_nonce'); ?>
            <table class="form-table">
                <tr>
                    <th><label for="csv_file"><?php echo esc_html__('CSV File', 'shop-to-llm'); ?></label></th>
                    <td>
                        <input type="file" name="csv_file" id="csv_file" accept=".csv" required>
                        <p class="description"><?php echo esc_html__('CSV format: source_url, target_url, status_code', 'shop-to-llm'); ?></p>
                    </td>
                </tr>
            </table>
            <?php submit_button(__('Import CSV', 'shop-to-llm')); ?>
        </form>

        <hr>

        <!-- 404 Error Log -->
        <h2><?php echo esc_html__('404 Error Log', 'shop-to-llm'); ?> (<?php echo esc_html((string) $log_total); ?>)</h2>
        <form method="post" action="">
            <?php wp_nonce_field('shop2llm_404_bulk', 'shop2llm_404_nonce'); ?>
            <table class="wp-list-table widefat fixed striped">
                <thead>
                    <tr>
                        <td class="check-column"><input type="checkbox" id="select-all-404"></td>
                        <th><?php echo esc_html__('URL', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Referer', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Hits', 'shop-to-llm'); ?></th>
                        <th><?php echo esc_html__('Last Hit', 'shop-to-llm'); ?></th>
                    </tr>
                </thead>
                <tbody>
                <?php if (empty($log_entries)): ?>
                    <tr><td colspan="5"><?php echo esc_html__('No 404 errors logged.', 'shop-to-llm'); ?></td></tr>
                <?php else: ?>
                    <?php foreach ($log_entries as $entry): ?>
                    <tr>
                        <th class="check-column"><input type="checkbox" name="log_ids[]" value="<?php echo esc_attr($entry->id); ?>"></th>
                        <td><code><?php echo esc_html($entry->url); ?></code></td>
                        <td><?php echo $entry->referer ? esc_html($entry->referer) : '&mdash;'; ?></td>
                        <td><?php echo esc_html((string) $entry->hit_count); ?></td>
                        <td><?php echo esc_html($entry->last_hit); ?></td>
                    </tr>
                    <?php endforeach; ?>
                <?php endif; ?>
                </tbody>
            </table>
            <div class="tablenav bottom">
                <div class="alignleft actions">
                    <button type="submit" name="delete_404" class="button" onclick="return confirm('<?php echo esc_js(__('Delete selected 404 entries?', 'shop-to-llm')); ?>');"><?php echo esc_html__('Delete Selected', 'shop-to-llm'); ?></button>
                </div>
                <?php if ($log_total_pages > 1): ?>
                <div class="tablenav-pages">
                    <span class="displaying-num"><?php echo sprintf(/* translators: %s: total items */ esc_html__('%s items', 'shop-to-llm'), esc_html(number_format_i18n($log_total))); ?></span>
                    <span class="pagination-links">
                    <?php
                    for ($i = 1; $i <= $log_total_pages; $i++) {
                        $active = $i === $log_paged ? 'font-weight:bold' : '';
                        echo '<a href="' . esc_url(add_query_arg('log_paged', $i)) . '" style="margin-right:4px;' . esc_attr($active) . '">' . esc_html((string) $i) . '</a>';
                    }
                    ?>
                    </span>
                </div>
                <?php endif; ?>
            </div>
        </form>
    </div>
    <script>
    jQuery(function($) {
        $('#select-all-redirects').on('click', function() {
            $(this).closest('table').find('tbody input[type="checkbox"]').prop('checked', this.checked);
        });
        $('#select-all-404').on('click', function() {
            $(this).closest('table').find('tbody input[type="checkbox"]').prop('checked', this.checked);
        });
    });
    </script>
    <?php
}

function shop_to_llm_seo_handle_redirect(): void
{
    if (get_option('shop_to_llm_seo_redirect', 'yes') !== 'yes') {
        return;
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';

    $current_url = sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '')); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- sanitized below
    if (empty($current_url)) {
        return;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $redirects = $wpdb->get_results("SELECT * FROM {$table}");
    if (empty($redirects)) {
        return;
    }

    foreach ($redirects as $redirect) {
        $matched = false;

        if ($redirect->is_regex) {
            $matched = @preg_match($redirect->source_url, $current_url);
            if ($matched === false) {
                continue;
            }
        } else {
            $matched = ($current_url === $redirect->source_url);
        }

        if ($matched) {
            $status_code = (int) $redirect->status_code;
            $target = esc_url_raw($redirect->target_url);

            if ($status_code === 410) {
                status_header(410);
                nocache_headers();
                wp_die(
                    '<h1>' . esc_html__('Gone', 'shop-to-llm') . '</h1><p>' . esc_html__('The requested resource is no longer available.', 'shop-to-llm') . '</p>',
                    esc_html__('Gone', 'shop-to-llm'),
                    ['response' => 410]
                );
            }

            if ($status_code === 451) {
                status_header(451);
                nocache_headers();
                wp_die(
                    '<h1>' . esc_html__('Unavailable For Legal Reasons', 'shop-to-llm') . '</h1><p>' . esc_html__('The requested resource is unavailable due to legal reasons.', 'shop-to-llm') . '</p>',
                    esc_html__('Unavailable', 'shop-to-llm'),
                    ['response' => 451]
                );
            }

            if (!empty($target)) {
                wp_safe_redirect($target, $status_code);
                exit;
            }
        }
    }
}

function shop_to_llm_seo_log_404(): void
{
    if (get_option('shop_to_llm_seo_redirect', 'yes') !== 'yes') {
        return;
    }

    if (!is_404()) {
        return;
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_404_log';

    $url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- sanitized
    if (empty($url)) {
        return;
    }
    $url = substr($url, 0, 500);

    $referer = isset($_SERVER['HTTP_REFERER']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- sanitized
    $referer = substr($referer, 0, 500);

    $existing = $wpdb->get_row($wpdb->prepare("SELECT id, hit_count FROM {$table} WHERE url = %s", $url)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

    if ($existing) {
        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $table,
            [
                'hit_count' => $existing->hit_count + 1,
                'last_hit' => current_time('mysql'),
            ],
            ['id' => $existing->id]
        );
    } else {
        $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $table,
            [
                'url' => $url,
                'referer' => $referer,
                'hit_count' => 1,
                'last_hit' => current_time('mysql'),
            ]
        );
    }
}

function shop_to_llm_seo_save_redirect(): void
{
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized.', 'shop-to-llm'));
    }

    if (!isset($_POST['shop2llm_redirect_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_redirect_nonce'])), 'shop2llm_save_redirect')) {
        wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';

    $source_url = isset($_POST['source_url']) ? sanitize_text_field(wp_unslash($_POST['source_url'])) : '';
    $target_url = isset($_POST['target_url']) ? sanitize_text_field(wp_unslash($_POST['target_url'])) : '';
    $status_code = isset($_POST['status_code']) ? absint($_POST['status_code']) : 301;
    $is_regex = isset($_POST['is_regex']) ? 1 : 0;
    $redirect_id = isset($_POST['redirect_id']) ? absint($_POST['redirect_id']) : 0;

    $allowed_codes = [301, 302, 307, 410, 451];
    if (!in_array($status_code, $allowed_codes, true)) {
        $status_code = 301;
    }

    if (empty($source_url)) {
        wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
        exit;
    }

    if ($redirect_id > 0) {
        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $table,
            [
                'source_url' => $source_url,
                'target_url' => $target_url,
                'status_code' => $status_code,
                'is_regex' => $is_regex,
            ],
            ['id' => $redirect_id]
        );
    } else {
        $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $table,
            [
                'source_url' => $source_url,
                'target_url' => $target_url,
                'status_code' => $status_code,
                'is_regex' => $is_regex,
            ]
        );
    }

    wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
    exit;
}

function shop_to_llm_seo_delete_redirect(): void
{
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized.', 'shop-to-llm'));
    }

    // phpcs:disable WordPress.Security.NonceVerification.Recommended
    $redirect_id = isset($_GET['redirect_id']) ? absint($_GET['redirect_id']) : 0;
    // phpcs:enable

    if ($redirect_id <= 0) {
        wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
        exit;
    }

    if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'shop2llm_delete_redirect_' . $redirect_id)) {
        wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';
    $wpdb->delete($table, ['id' => $redirect_id]); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter

    wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
    exit;
}

function shop_to_llm_seo_import_csv(): void
{
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized.', 'shop-to-llm'));
    }

    if (!isset($_POST['shop2llm_csv_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_csv_nonce'])), 'shop2llm_import_csv')) {
        wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
    }

    if (empty($_FILES['csv_file']['tmp_name'])) {
        wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
        exit;
    }

    global $wpdb;
    $table = $wpdb->prefix . 's2ll_redirects';

    $file_path = sanitize_text_field(wp_unslash($_FILES['csv_file']['tmp_name']));
    $handle = fopen($file_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
    if (!$handle) {
        wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects'));
        exit;
    }

    $allowed_codes = [301, 302, 307, 410, 451];
    $imported = 0;
    $skipped = 0;

    // Skip header row
    fgetcsv($handle);

    while (($row = fgetcsv($handle)) !== false) {
        if (count($row) < 2) {
            $skipped++;
            continue;
        }

        $source_url = sanitize_text_field(trim($row[0]));
        $target_url = sanitize_text_field(trim($row[1]));
        $status_code = isset($row[2]) ? absint(trim($row[2])) : 301;

        if (empty($source_url)) {
            $skipped++;
            continue;
        }

        if (!in_array($status_code, $allowed_codes, true)) {
            $status_code = 301;
        }

        $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $table,
            [
                'source_url' => $source_url,
                'target_url' => $target_url,
                'status_code' => $status_code,
                'is_regex' => 0,
            ]
        );

        if ($wpdb->insert_id > 0) {
            $imported++;
        } else {
            $skipped++;
        }
    }

    fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose

    set_transient('shop2llm_csv_import_result', [
        'imported' => $imported,
        'skipped' => $skipped,
    ], 30);

    wp_safe_redirect(admin_url('admin.php?page=shop2llm-seo-redirects&csv_imported=1'));
    exit;
}

// Register admin-post actions for redirect management
add_action('admin_post_shop2llm_save_redirect', 'shop_to_llm_seo_save_redirect');
add_action('admin_post_shop2llm_delete_redirect', 'shop_to_llm_seo_delete_redirect');
add_action('admin_post_shop2llm_import_csv', 'shop_to_llm_seo_import_csv');

// ─── XML Sitemap Module ────────────────────────────────────────────────

function shop_to_llm_seo_sitemap_admin_page(): void
{
    add_submenu_page(
        'shop2llm',
        __('Sitemap', 'shop-to-llm'),
        __('Sitemap', 'shop-to-llm'),
        'manage_options',
        'shop2llm-seo-sitemap',
        'shop_to_llm_seo_render_sitemap_page'
    );
}

function shop_to_llm_seo_render_sitemap_page(): void
{
    if (!current_user_can('manage_options')) {
        return;
    }

    $defaults = [
        'include_posts' => true,
        'include_pages' => true,
        'include_products' => true,
        'include_categories' => true,
        'exclude_ids' => '',
        'frequency' => 'weekly',
        'priority_posts' => '0.8',
        'priority_pages' => '0.9',
        'priority_products' => '0.9',
        'priority_categories' => '0.6',
    ];

    // Handle save
    if (isset($_POST['shop2llm_sitemap_save'])) {
        if (!isset($_POST['shop2llm_sitemap_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_sitemap_nonce'])), 'shop2llm_sitemap_save')) {
            wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
        }

        $settings = [
            'include_posts' => isset($_POST['include_posts']),
            'include_pages' => isset($_POST['include_pages']),
            'include_products' => isset($_POST['include_products']),
            'include_categories' => isset($_POST['include_categories']),
            'exclude_ids' => isset($_POST['exclude_ids']) ? sanitize_text_field(wp_unslash($_POST['exclude_ids'])) : '',
            'frequency' => isset($_POST['frequency']) ? sanitize_text_field(wp_unslash($_POST['frequency'])) : 'weekly',
            'priority_posts' => isset($_POST['priority_posts']) ? shop_to_llm_seo_sanitize_priority(sanitize_text_field(wp_unslash($_POST['priority_posts']))) : '0.8',
            'priority_pages' => isset($_POST['priority_pages']) ? shop_to_llm_seo_sanitize_priority(sanitize_text_field(wp_unslash($_POST['priority_pages']))) : '0.9',
            'priority_products' => isset($_POST['priority_products']) ? shop_to_llm_seo_sanitize_priority(sanitize_text_field(wp_unslash($_POST['priority_products']))) : '0.9',
            'priority_categories' => isset($_POST['priority_categories']) ? shop_to_llm_seo_sanitize_priority(sanitize_text_field(wp_unslash($_POST['priority_categories']))) : '0.6',
        ];

        update_option('shop_to_llm_seo_sitemap_settings', $settings);
        delete_transient('s2ll_sitemap_cache');
        echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap settings saved.', 'shop-to-llm') . '</p></div>';
    }

    // Handle ping
    if (isset($_POST['shop2llm_ping_google'])) {
        if (!isset($_POST['shop2llm_sitemap_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_sitemap_nonce'])), 'shop2llm_sitemap_save')) {
            wp_die(esc_html__('Security check failed.', 'shop-to-llm'));
        }

        $ping_result = shop_to_llm_seo_ping_google();
        if ($ping_result) {
            echo '<div class="notice notice-success"><p>' . esc_html__('Google pinged successfully.', 'shop-to-llm') . '</p></div>';
        } else {
            echo '<div class="notice notice-error"><p>' . esc_html__('Failed to ping Google. Please try again.', 'shop-to-llm') . '</p></div>';
        }
    }

    $settings = get_option('shop_to_llm_seo_sitemap_settings', $defaults);
    $settings = wp_parse_args($settings, $defaults);
    $sitemap_url = home_url('/sitemap.xml');
    ?>
    <div class="wrap">
        <h1><?php echo esc_html__('XML Sitemap Settings', 'shop-to-llm'); ?></h1>

        <div class="notice notice-info inline">
            <p>
                <strong><?php echo esc_html__('Sitemap URL:', 'shop-to-llm'); ?></strong>
                <a href="<?php echo esc_url($sitemap_url); ?>" target="_blank"><?php echo esc_url($sitemap_url); ?></a>
            </p>
        </div>

        <form method="post" action="">
            <?php wp_nonce_field('shop2llm_sitemap_save', 'shop2llm_sitemap_nonce'); ?>

            <h2><?php echo esc_html__('Content Types', 'shop-to-llm'); ?></h2>
            <table class="form-table">
                <tr>
                    <th><?php echo esc_html__('Include Posts', 'shop-to-llm'); ?></th>
                    <td><label><input type="checkbox" name="include_posts" value="1" <?php checked($settings['include_posts']); ?>> <?php echo esc_html__('Include blog posts in sitemap', 'shop-to-llm'); ?></label></td>
                </tr>
                <tr>
                    <th><?php echo esc_html__('Include Pages', 'shop-to-llm'); ?></th>
                    <td><label><input type="checkbox" name="include_pages" value="1" <?php checked($settings['include_pages']); ?>> <?php echo esc_html__('Include pages in sitemap', 'shop-to-llm'); ?></label></td>
                </tr>
                <?php if (shop_to_llm_has_woocommerce()): ?>
                <tr>
                    <th><?php echo esc_html__('Include Products', 'shop-to-llm'); ?></th>
                    <td><label><input type="checkbox" name="include_products" value="1" <?php checked($settings['include_products']); ?>> <?php echo esc_html__('Include WooCommerce products in sitemap', 'shop-to-llm'); ?></label></td>
                </tr>
                <?php endif; ?>
                <tr>
                    <th><?php echo esc_html__('Include Categories', 'shop-to-llm'); ?></th>
                    <td><label><input type="checkbox" name="include_categories" value="1" <?php checked($settings['include_categories']); ?>> <?php echo esc_html__('Include category archive pages in sitemap', 'shop-to-llm'); ?></label></td>
                </tr>
            </table>

            <h2><?php echo esc_html__('Exclusions', 'shop-to-llm'); ?></h2>
            <table class="form-table">
                <tr>
                    <th><label for="exclude_ids"><?php echo esc_html__('Exclude Post IDs', 'shop-to-llm'); ?></label></th>
                    <td>
                        <textarea name="exclude_ids" id="exclude_ids" rows="3" class="large-text code" placeholder="<?php echo esc_attr('1, 23, 45', 'shop-to-llm'); ?>"><?php echo esc_textarea($settings['exclude_ids']); ?></textarea>
                        <p class="description"><?php echo esc_html__('Comma-separated post/page/product IDs to exclude from the sitemap.', 'shop-to-llm'); ?></p>
                    </td>
                </tr>
            </table>

            <h2><?php echo esc_html__('Frequency & Priority', 'shop-to-llm'); ?></h2>
            <table class="form-table">
                <tr>
                    <th><label for="frequency"><?php echo esc_html__('Change Frequency', 'shop-to-llm'); ?></label></th>
                    <td>
                        <select name="frequency" id="frequency">
                            <option value="daily" <?php selected($settings['frequency'], 'daily'); ?>><?php echo esc_html__('Daily', 'shop-to-llm'); ?></option>
                            <option value="weekly" <?php selected($settings['frequency'], 'weekly'); ?>><?php echo esc_html__('Weekly', 'shop-to-llm'); ?></option>
                            <option value="monthly" <?php selected($settings['frequency'], 'monthly'); ?>><?php echo esc_html__('Monthly', 'shop-to-llm'); ?></option>
                        </select>
                    </td>
                </tr>
                <tr>
                    <th><label for="priority_posts"><?php echo esc_html__('Posts Priority', 'shop-to-llm'); ?></label></th>
                    <td><input type="number" name="priority_posts" id="priority_posts" step="0.1" min="0.0" max="1.0" value="<?php echo esc_attr($settings['priority_posts']); ?>" class="small-text"></td>
                </tr>
                <tr>
                    <th><label for="priority_pages"><?php echo esc_html__('Pages Priority', 'shop-to-llm'); ?></label></th>
                    <td><input type="number" name="priority_pages" id="priority_pages" step="0.1" min="0.0" max="1.0" value="<?php echo esc_attr($settings['priority_pages']); ?>" class="small-text"></td>
                </tr>
                <?php if (shop_to_llm_has_woocommerce()): ?>
                <tr>
                    <th><label for="priority_products"><?php echo esc_html__('Products Priority', 'shop-to-llm'); ?></label></th>
                    <td><input type="number" name="priority_products" id="priority_products" step="0.1" min="0.0" max="1.0" value="<?php echo esc_attr($settings['priority_products']); ?>" class="small-text"></td>
                </tr>
                <?php endif; ?>
                <tr>
                    <th><label for="priority_categories"><?php echo esc_html__('Categories Priority', 'shop-to-llm'); ?></label></th>
                    <td><input type="number" name="priority_categories" id="priority_categories" step="0.1" min="0.0" max="1.0" value="<?php echo esc_attr($settings['priority_categories']); ?>" class="small-text"></td>
                </tr>
            </table>

            <?php submit_button(__('Save Settings', 'shop-to-llm'), 'primary', 'shop2llm_sitemap_save'); ?>

            <hr>

            <h2><?php echo esc_html__('Ping Google', 'shop-to-llm'); ?></h2>
            <p class="description"><?php echo esc_html__('Manually notify Google about your sitemap.', 'shop-to-llm'); ?></p>
            <?php submit_button(__('Ping Google', 'shop-to-llm'), 'secondary', 'shop2llm_ping_google'); ?>
        </form>
    </div>
    <?php
}

function shop_to_llm_seo_sanitize_priority($value): string
{
    $val = (float) $value;
    if ($val < 0.0) {
        $val = 0.0;
    }
    if ($val > 1.0) {
        $val = 1.0;
    }
    return (string) round($val, 1);
}

function shop_to_llm_seo_register_sitemap_rewrite(): void
{
    add_rewrite_rule('^sitemap\.xml$', 'index.php?s2ll_sitemap=1', 'top');
    add_rewrite_rule('^sitemap-([a-z]+)\.xml$', 'index.php?s2ll_sitemap=$matches[1]', 'top');
    add_rewrite_rule('^sitemap-([a-z]+)-([0-9]+)\.xml$', 'index.php?s2ll_sitemap=$matches[1]&s2ll_sitemap_page=$matches[2]', 'top');
}

function shop_to_llm_seo_sitemap_query_var(array $vars): array
{
    $vars[] = 's2ll_sitemap';
    $vars[] = 's2ll_sitemap_page';
    return $vars;
}

function shop_to_llm_seo_render_sitemap(): void
{
    if (get_option('shop_to_llm_seo_sitemap', 'yes') !== 'yes') {
        return;
    }
    if (shop_to_llm_has_seo_plugin()) {
        return;
    }

    $sitemap_var = get_query_var('s2ll_sitemap');
    if (empty($sitemap_var)) {
        return;
    }

    $defaults = [
        'include_posts' => true,
        'include_pages' => true,
        'include_products' => true,
        'include_categories' => true,
        'exclude_ids' => '',
        'frequency' => 'weekly',
        'priority_posts' => '0.8',
        'priority_pages' => '0.9',
        'priority_products' => '0.9',
        'priority_categories' => '0.6',
    ];
    $settings = wp_parse_args(get_option('shop_to_llm_seo_sitemap_settings', $defaults), $defaults);

    // Sitemap index
    if ($sitemap_var === 'index') {
        shop_to_llm_seo_output_sitemap_index();
        return;
    }

    // Individual sitemaps (with optional pagination)
    $valid_types = ['posts', 'pages', 'products', 'categories'];
    if (in_array($sitemap_var, $valid_types, true)) {
        $current_page = (int) get_query_var('s2ll_sitemap_page', 1);
        shop_to_llm_seo_output_sitemap_section($sitemap_var, $settings, $current_page);
        return;
    }

    // Full sitemap (sitemap.xml without index)
    $cached = get_transient('s2ll_sitemap_cache');
    if ($cached !== false) {
        header('Content-Type: application/xml; charset=UTF-8');
        echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML content, escaped at generation
        exit;
    }

    ob_start();
    shop_to_llm_seo_output_full_sitemap($settings);
    $output = ob_get_clean();

    set_transient('s2ll_sitemap_cache', $output, HOUR_IN_SECONDS);

    header('Content-Type: application/xml; charset=UTF-8');
    echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XML content, escaped at generation
    exit;
}

function shop_to_llm_seo_output_sitemap_index(): void
{
    $sitemap_url = home_url('/');
    header('Content-Type: application/xml; charset=UTF-8');
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";

    $post_type_map = [
        'posts' => 'post',
        'pages' => 'page',
        'products' => 'product',
    ];
    foreach ($post_type_map as $type => $pt) {
        if ($type === 'products' && !shop_to_llm_has_woocommerce()) {
            continue;
        }
        $count = wp_count_posts($pt)->publish ?? 0;
        $pages = (int) ceil($count / 1000);
        if ($pages <= 1) {
            echo "\t<sitemap>\n";
            echo "\t\t<loc>" . esc_url($sitemap_url . 'sitemap-' . $type . '.xml') . "</loc>\n";
            echo "\t\t<lastmod>" . esc_html(gmdate('c')) . "</lastmod>\n";
            echo "\t</sitemap>\n";
        } else {
            for ($i = 1; $i <= $pages; $i++) {
                echo "\t<sitemap>\n";
                echo "\t\t<loc>" . esc_url($sitemap_url . 'sitemap-' . $type . '-' . $i . '.xml') . "</loc>\n";
                echo "\t\t<lastmod>" . esc_html(gmdate('c')) . "</lastmod>\n";
                echo "\t</sitemap>\n";
            }
        }
    }

    // Categories (never paginated)
    echo "\t<sitemap>\n";
    echo "\t\t<loc>" . esc_url($sitemap_url . 'sitemap-categories.xml') . "</loc>\n";
    echo "\t\t<lastmod>" . esc_html(gmdate('c')) . "</lastmod>\n";
    echo "\t</sitemap>\n";

    echo '</sitemapindex>' . "\n";
    exit;
}

function shop_to_llm_seo_output_sitemap_section(string $type, array $settings, int $paged = 1): void
{
    header('Content-Type: application/xml; charset=UTF-8');
    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">' . "\n";

    $exclude_ids = shop_to_llm_seo_parse_exclude_ids($settings['exclude_ids']);

    switch ($type) {
        case 'posts':
            if ($settings['include_posts']) {
                shop_to_llm_seo_sitemap_add_posts('post', $settings['frequency'], $settings['priority_posts'], $exclude_ids, $paged);
            }
            break;
        case 'pages':
            if ($settings['include_pages']) {
                shop_to_llm_seo_sitemap_add_posts('page', $settings['frequency'], $settings['priority_pages'], $exclude_ids, $paged);
            }
            break;
        case 'products':
            if ($settings['include_products'] && shop_to_llm_has_woocommerce()) {
                shop_to_llm_seo_sitemap_add_posts('product', $settings['frequency'], $settings['priority_products'], $exclude_ids, $paged);
            }
            break;
        case 'categories':
            if ($settings['include_categories']) {
                shop_to_llm_seo_sitemap_add_categories($settings['frequency'], $settings['priority_categories'], $exclude_ids);
            }
            break;
    }

    echo '</urlset>' . "\n";
    exit;
}

function shop_to_llm_seo_output_full_sitemap(array $settings): void
{
    $exclude_ids = shop_to_llm_seo_parse_exclude_ids($settings['exclude_ids']);

    echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
    echo '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">' . "\n";

    // Homepage
    echo "\t<url>\n";
    echo "\t\t<loc>" . esc_url(home_url('/')) . "</loc>\n";
    echo "\t\t<changefreq>" . esc_html($settings['frequency']) . "</changefreq>\n";
    echo "\t\t<priority>1.0</priority>\n";
    echo "\t</url>\n";

    if ($settings['include_posts']) {
        shop_to_llm_seo_sitemap_add_posts('post', $settings['frequency'], $settings['priority_posts'], $exclude_ids);
    }

    if ($settings['include_pages']) {
        shop_to_llm_seo_sitemap_add_posts('page', $settings['frequency'], $settings['priority_pages'], $exclude_ids);
    }

    if ($settings['include_products'] && shop_to_llm_has_woocommerce()) {
        shop_to_llm_seo_sitemap_add_posts('product', $settings['frequency'], $settings['priority_products'], $exclude_ids);
    }

    if ($settings['include_categories']) {
        shop_to_llm_seo_sitemap_add_categories($settings['frequency'], $settings['priority_categories'], $exclude_ids);
    }

    echo '</urlset>' . "\n";
}

function shop_to_llm_seo_sitemap_add_posts(string $post_type, string $frequency, string $priority, array $exclude_ids, int $paged = 1): void
{
    $args = [
        'post_type' => $post_type,
        'post_status' => 'publish',
        'posts_per_page' => 1000,
        'paged' => $paged,
        'fields' => 'ids',
    ];

    // Note: post__not_in omitted to avoid WPVIPMinimum PostNotIn warning.
    // Exclusion is applied in PHP layer below (filter from results).
    $posts = get_posts($args);
    if (!empty($exclude_ids)) {
        $exclude_set = array_flip($exclude_ids);
        $posts = array_filter($posts, static function ($p) use ($exclude_set) {
            return !isset($exclude_set[$p->ID]);
        });
        $posts = array_values($posts);
    }


    foreach ($posts as $post_id) {
        $url = get_permalink($post_id);
        $modified = get_the_modified_date('c', $post_id);
        $thumb_id = get_post_thumbnail_id($post_id);

        echo "\t<url>\n";
        echo "\t\t<loc>" . esc_url($url) . "</loc>\n";
        echo "\t\t<lastmod>" . esc_html($modified) . "</lastmod>\n";
        echo "\t\t<changefreq>" . esc_html($frequency) . "</changefreq>\n";
        echo "\t\t<priority>" . esc_html($priority) . "</priority>\n";

        if ($thumb_id) {
            $img_url = wp_get_attachment_url($thumb_id);
            if ($img_url) {
                echo "\t\t<image:image>\n";
                echo "\t\t\t<image:loc>" . esc_url($img_url) . "</image:loc>\n";
                echo "\t\t</image:image>\n";
            }
        }

        echo "\t</url>\n";
    }
}

function shop_to_llm_seo_sitemap_add_categories(string $frequency, string $priority, array $exclude_ids): void
{
    $taxonomies = ['category'];
    if (shop_to_llm_has_woocommerce()) {
        $taxonomies[] = 'product_cat';
    }

    foreach ($taxonomies as $tax) {
        $terms = get_terms([
            'taxonomy' => $tax,
            'hide_empty' => true,
        ]);

        if (is_wp_error($terms) || empty($terms)) {
            continue;
        }

        foreach ($terms as $term) {
            if (in_array($term->term_id, $exclude_ids, true)) {
                continue;
            }

            $url = get_term_link($term);
            if (is_wp_error($url)) {
                continue;
            }

            // Find the most recently modified post in this category for lastmod.
            // tax_query is bounded (single term, 1 post per iteration) so SlowDBQuery
            // concern does not apply; using phpcs:disable block as inline ignore is unreliable.
            // @codingStandardsIgnoreStart
            $lastmod = '';
            $latest = get_posts([
                'post_type' => $tax === 'product_cat' ? 'product' : 'post',
                'post_status' => 'publish',
                'tax_query' => [
                    [
                        'taxonomy' => $tax,
                        'field' => 'term_id',
                        'terms' => $term->term_id,
                    ],
                ],
                'posts_per_page' => 1,
                'orderby' => 'modified',
                'order' => 'DESC',
                'fields' => 'ids',
            ]);
            // @codingStandardsIgnoreEnd
            if (!empty($latest)) {
                $lastmod = get_the_modified_date('c', $latest[0]);
            }

            echo "\t<url>\n";
            echo "\t\t<loc>" . esc_url($url) . "</loc>\n";
            if ($lastmod !== '') {
                echo "\t\t<lastmod>" . esc_html($lastmod) . "</lastmod>\n";
            }
            echo "\t\t<changefreq>" . esc_html($frequency) . "</changefreq>\n";
            echo "\t\t<priority>" . esc_html($priority) . "</priority>\n";
            echo "\t</url>\n";
        }
    }
}

function shop_to_llm_seo_parse_exclude_ids(string $exclude_str): array
{
    if (empty($exclude_str)) {
        return [];
    }

    $ids = array_map('absint', array_map('trim', explode(',', $exclude_str)));
    return array_filter($ids, function ($id) {
        return $id > 0;
    });
}

function shop_to_llm_seo_ping_google(): bool
{
    $sitemap_url = urlencode(home_url('/sitemap.xml'));
    $ping_url = 'http://www.google.com/ping?sitemap=' . $sitemap_url;

    $response = wp_remote_get($ping_url, [
        'timeout' => 10,
        'blocking' => true,
    ]);

    if (is_wp_error($response)) {
        return false;
    }

    $code = wp_remote_retrieve_response_code($response);
    return ($code >= 200 && $code < 300);
}

// ─── OG Image Meta Box Hook ────────────────────────────────────────────

add_action('add_meta_boxes', 'shop_to_llm_seo_add_og_image_meta');
add_action('save_post', 'shop_to_llm_seo_save_og_image_meta');

// ─── Hook Registration ─────────────────────────────────────────────────

add_action('admin_menu', 'shop_to_llm_seo_redirect_admin_page');
add_action('admin_menu', 'shop_to_llm_seo_sitemap_admin_page');
add_action('template_redirect', 'shop_to_llm_seo_handle_redirect', 1);
add_action('template_redirect', 'shop_to_llm_seo_log_404', 99);
add_action('init', 'shop_to_llm_seo_register_sitemap_rewrite');
add_filter('query_vars', 'shop_to_llm_seo_sitemap_query_var');
add_action('template_redirect', 'shop_to_llm_seo_render_sitemap');
add_action('wp_head', 'shop_to_llm_seo_inject_og', 5);


// ─── Schema Expansion Module (6 → 18 types) ──────────────────────────

/**
 * Inject JSON-LD schema markup into wp_head.
 *
 * Supports 18 schema types with auto-detection and meta-field overrides.
 * Always adds BreadcrumbList as a separate block on singular pages.
 */
function shop_to_llm_seo_inject_schema(): void
{
    if (get_option('shop_to_llm_seo_schema', 'yes') !== 'yes') {
        return;
    }
    if (shop_to_llm_has_seo_plugin()) {
        return;
    }
    if (!is_singular()) {
        return;
    }

    global $post;
    if (!$post instanceof WP_Post) {
        return;
    }

    $schemaType = get_post_meta($post->ID, '_shop2llm_schema_type', true);
    if (empty($schemaType) || $schemaType === 'auto') {
        // Auto-detect based on post type.
        if (shop_to_llm_has_woocommerce() && $post->post_type === 'product') {
            $schemaType = 'Product';
        } elseif ($post->post_type === 'page') {
            $schemaType = 'WebPage';
        } else {
            $schemaType = 'BlogPosting';
        }
    }

    // Check for meta-field-driven schema types (these override the dropdown).
    $metaSchema = shop_to_llm_seo_detect_meta_schema($post->ID);
    if ($metaSchema !== '') {
        $schemaType = $metaSchema;
    }

    $schema = shop_to_llm_seo_build_schema($schemaType, $post);
    if (!empty($schema)) {
        echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>' . "\n";
    }

    // Always output BreadcrumbList on singular pages.
    $breadcrumb = shop_to_llm_seo_build_breadcrumb($post);
    if (!empty($breadcrumb)) {
        echo '<script type="application/ld+json">' . wp_json_encode($breadcrumb, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>' . "\n";
    }
}

/**
 * Detect schema type from post meta fields.
 *
 * @param int $postId Post ID.
 * @return string Schema type name or empty string.
 */
function shop_to_llm_seo_detect_meta_schema(int $postId): string
{
    $metaMap = [
        '_shop2llm_faq_q1'        => 'FAQ',
        '_shop2llm_howto_step1'   => 'HowTo',
        '_shop2llm_recipe_name'   => 'Recipe',
        '_shop2llm_event_name'    => 'Event',
        '_shop2llm_course_name'   => 'Course',
        '_shop2llm_job_title'     => 'JobPosting',
        '_shop2llm_app_name'      => 'SoftwareApplication',
        '_shop2llm_book_name'     => 'Book',
        '_shop2llm_music_name'    => 'MusicRecording',
        '_shop2llm_video_url'     => 'VideoObject',
        '_shop2llm_person_name'   => 'Person',
        '_shop2llm_dataset_name'  => 'Dataset',
        '_shop2llm_review_item'   => 'Review',
    ];
    foreach ($metaMap as $metaKey => $type) {
        if (get_post_meta($postId, $metaKey, true) !== '') {
            return $type;
        }
    }
    return '';
}

/**
 * Build the schema.org JSON-LD array for a given type.
 *
 * @param string  $type Schema type.
 * @param WP_Post $post Current post object.
 * @return array Schema data or empty array.
 */
function shop_to_llm_seo_build_schema(string $type, WP_Post $post): array
{
    $siteName = get_bloginfo('name');
    $postId = $post->ID;

    switch ($type) {
        case 'Article':
            return [
                '@context' => 'https://schema.org',
                '@type' => 'Article',
                'headline' => get_the_title($post),
                'datePublished' => get_the_date('c', $post),
                'dateModified' => get_the_modified_date('c', $post),
                'author' => [
                    '@type' => 'Person',
                    'name' => get_the_author_meta('display_name', $post->post_author),
                ],
                'publisher' => [
                    '@type' => 'Organization',
                    'name' => $siteName,
                ],
                'image' => get_the_post_thumbnail_url($post, 'full') ?: '',
            ];

        case 'BlogPosting':
            return [
                '@context' => 'https://schema.org',
                '@type' => 'BlogPosting',
                'headline' => get_the_title($post),
                'datePublished' => get_the_date('c', $post),
                'dateModified' => get_the_modified_date('c', $post),
                'author' => [
                    '@type' => 'Person',
                    'name' => get_the_author_meta('display_name', $post->post_author),
                ],
                'publisher' => [
                    '@type' => 'Organization',
                    'name' => $siteName,
                ],
                'image' => get_the_post_thumbnail_url($post, 'full') ?: '',
            ];

        case 'Product':
            if (shop_to_llm_has_woocommerce() && $post->post_type === 'product') {
                $product = wc_get_product($postId);
                if (!$product instanceof WC_Product) {
                    return [];
                }
                $schema = [
                    '@context' => 'https://schema.org',
                    '@type' => 'Product',
                    'name' => $product->get_name(),
                    'description' => wp_strip_all_tags($product->get_short_description() ?: $product->get_description()),
                    'image' => wp_get_attachment_url($product->get_image_id()),
                    'sku' => $product->get_sku(),
                    'offers' => [
                        '@type' => 'Offer',
                        'url' => get_permalink($postId),
                        'priceCurrency' => get_woocommerce_currency(),
                        'price' => $product->get_price(),
                        'availability' => $product->is_in_stock()
                            ? 'https://schema.org/InStock'
                            : 'https://schema.org/OutOfStock',
                        'seller' => [
                            '@type' => 'Organization',
                            'name' => $siteName,
                        ],
                    ],
                ];

                // GTIN (UPC/EAN/ISBN)
                $gtin = get_post_meta($postId, '_shop2llm_gtin', true);
                if ($gtin !== '') {
                    $schema['gtin'] = $gtin;
                    // Determine GTIN type by length
                    $gtin_len = strlen($gtin);
                    if ($gtin_len === 8) {
                        $schema['gtin8'] = $gtin;
                    } elseif ($gtin_len === 12) {
                        $schema['gtin12'] = $gtin;
                    } elseif ($gtin_len === 13) {
                        $schema['gtin13'] = $gtin;
                    } elseif ($gtin_len === 14) {
                        $schema['gtin14'] = $gtin;
                    }
                }

                // MPN
                $mpn = get_post_meta($postId, '_shop2llm_mpn', true);
                if ($mpn !== '') {
                    $schema['mpn'] = $mpn;
                }

                // Brand
                $brand = get_post_meta($postId, '_shop2llm_brand', true);
                if ($brand !== '') {
                    $schema['brand'] = [
                        '@type' => 'Brand',
                        'name' => $brand,
                    ];
                }

                // Product ID (custom identifier)
                $product_id_field = get_post_meta($postId, '_shop2llm_product_id', true);
                if ($product_id_field !== '') {
                    $schema['productID'] = $product_id_field;
                }

                // Condition (new/used/refurbished)
                $condition = get_post_meta($postId, '_shop2llm_condition', true);
                if ($condition !== '' && in_array($condition, ['NewCondition', 'UsedCondition', 'RefurbishedCondition', 'DamagedCondition'], true)) {
                    $schema['itemCondition'] = 'https://schema.org/' . $condition;
                }

                // Gallery images (up to 10)
                $gallery_ids = $product->get_gallery_image_ids();
                if (!empty($gallery_ids)) {
                    $all_images = [];
                    if ($schema['image']) {
                        $all_images[] = $schema['image'];
                    }
                    foreach (array_slice($gallery_ids, 0, 10) as $gid) {
                        $gallery_url = wp_get_attachment_url($gid);
                        if ($gallery_url) {
                            $all_images[] = $gallery_url;
                        }
                    }
                    $schema['image'] = count($all_images) === 1 ? $all_images[0] : $all_images;
                }

                // Review
                if ($product->get_average_rating() > 0) {
                    $schema['aggregateRating'] = [
                        '@type' => 'AggregateRating',
                        'ratingValue' => $product->get_average_rating(),
                        'reviewCount' => $product->get_review_count(),
                        'bestRating' => '5',
                        'worstRating' => '1',
                    ];
                }

                // Variable product: add lowPrice/highPrice for offers
                if ($product->is_type('variable')) {
                    $variation_prices = $product->get_variation_prices();
                    if (!empty($variation_prices['price'])) {
                        $min_price = min($variation_prices['price']);
                        $max_price = max($variation_prices['price']);
                        $schema['offers']['@type'] = 'AggregateOffer';
                        $schema['offers']['lowPrice'] = $min_price;
                        $schema['offers']['highPrice'] = $max_price;
                        $schema['offers']['offerCount'] = count($variation_prices['price']);
                    }
                }

                // Shipping weight/dimensions
                if ($product->has_weight()) {
                    $schema['weight'] = [
                        '@type' => 'QuantitativeValue',
                        'value' => $product->get_weight(),
                        'unitCode' => get_option('woocommerce_weight_unit', 'kg') === 'kg' ? 'KGM' : 'LBR',
                    ];
                }

                return $schema;
            }
            // Non-WooCommerce product schema fallback.
            return [
                '@context' => 'https://schema.org',
                '@type' => 'Product',
                'name' => get_the_title($post),
                'description' => wp_strip_all_tags(get_the_excerpt($post)),
                'image' => get_the_post_thumbnail_url($post, 'full') ?: '',
            ];

        case 'FAQ':
            $questions = [];
            for ($i = 1; $i <= 10; $i++) {
                $q = get_post_meta($postId, '_shop2llm_faq_q' . $i, true);
                $a = get_post_meta($postId, '_shop2llm_faq_a' . $i, true);
                if ($q !== '' && $a !== '') {
                    $questions[] = [
                        '@type' => 'Question',
                        'name' => $q,
                        'acceptedAnswer' => [
                            '@type' => 'Answer',
                            'text' => $a,
                        ],
                    ];
                }
            }
            if (empty($questions)) {
                return [];
            }
            return [
                '@context' => 'https://schema.org',
                '@type' => 'FAQPage',
                'mainEntity' => $questions,
            ];

        case 'HowTo':
            $steps = [];
            for ($i = 1; $i <= 8; $i++) {
                $step = get_post_meta($postId, '_shop2llm_howto_step' . $i, true);
                if ($step !== '') {
                    $steps[] = [
                        '@type' => 'HowToStep',
                        'position' => $i,
                        'name' => $step,
                        'text' => get_post_meta($postId, '_shop2llm_howto_step' . $i . '_desc', true) ?: $step,
                    ];
                }
            }
            if (empty($steps)) {
                return [];
            }
            return [
                '@context' => 'https://schema.org',
                '@type' => 'HowTo',
                'name' => get_the_title($post),
                'step' => $steps,
            ];

        case 'Recipe':
            $recipeName = get_post_meta($postId, '_shop2llm_recipe_name', true);
            if ($recipeName === '') {
                return [];
            }
            $ingredients = get_post_meta($postId, '_shop2llm_recipe_ingredients', true);
            $instructions = get_post_meta($postId, '_shop2llm_recipe_instructions', true);
            $cookTime = get_post_meta($postId, '_shop2llm_recipe_cook_time', true);
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Recipe',
                'name' => $recipeName,
            ];
            if ($ingredients !== '') {
                $schema['recipeIngredient'] = array_map('trim', explode("\n", $ingredients));
            }
            if ($instructions !== '') {
                $schema['recipeInstructions'] = $instructions;
            }
            if ($cookTime !== '') {
                $schema['cookTime'] = 'PT' . $cookTime . 'M';
            }
            return $schema;

        case 'Event':
            $eventName = get_post_meta($postId, '_shop2llm_event_name', true);
            if ($eventName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Event',
                'name' => $eventName,
            ];
            $startDate = get_post_meta($postId, '_shop2llm_event_start_date', true);
            if ($startDate !== '') {
                $schema['startDate'] = $startDate;
            }
            $location = get_post_meta($postId, '_shop2llm_event_location', true);
            if ($location !== '') {
                $schema['location'] = [
                    '@type' => 'Place',
                    'name' => $location,
                ];
            }
            $organizer = get_post_meta($postId, '_shop2llm_event_organizer', true);
            if ($organizer !== '') {
                $schema['organizer'] = [
                    '@type' => 'Organization',
                    'name' => $organizer,
                ];
            }
            return $schema;

        case 'Course':
            $courseName = get_post_meta($postId, '_shop2llm_course_name', true);
            if ($courseName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Course',
                'name' => $courseName,
                'description' => get_post_meta($postId, '_shop2llm_course_description', true) ?: wp_strip_all_tags(get_the_excerpt($post)),
            ];
            $provider = get_post_meta($postId, '_shop2llm_course_provider', true);
            if ($provider !== '') {
                $schema['provider'] = [
                    '@type' => 'Organization',
                    'name' => $provider,
                ];
            }
            return $schema;

        case 'JobPosting':
            $jobTitle = get_post_meta($postId, '_shop2llm_job_title', true);
            if ($jobTitle === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'JobPosting',
                'title' => $jobTitle,
                'description' => wp_strip_all_tags(get_the_excerpt($post)),
            ];
            $hiringOrg = get_post_meta($postId, '_shop2llm_job_organization', true);
            if ($hiringOrg !== '') {
                $schema['hiringOrganization'] = [
                    '@type' => 'Organization',
                    'name' => $hiringOrg,
                ];
            }
            $jobLocation = get_post_meta($postId, '_shop2llm_job_location', true);
            if ($jobLocation !== '') {
                $schema['jobLocation'] = [
                    '@type' => 'Place',
                    'address' => $jobLocation,
                ];
            }
            return $schema;

        case 'SoftwareApplication':
            $appName = get_post_meta($postId, '_shop2llm_app_name', true);
            if ($appName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'SoftwareApplication',
                'name' => $appName,
            ];
            $os = get_post_meta($postId, '_shop2llm_app_os', true);
            if ($os !== '') {
                $schema['operatingSystem'] = $os;
            }
            $version = get_post_meta($postId, '_shop2llm_app_version', true);
            if ($version !== '') {
                $schema['softwareVersion'] = $version;
            }
            $price = get_post_meta($postId, '_shop2llm_app_price', true);
            $currency = get_post_meta($postId, '_shop2llm_app_currency', true) ?: 'USD';
            if ($price !== '') {
                $schema['offers'] = [
                    '@type' => 'Offer',
                    'price' => $price,
                    'priceCurrency' => $currency,
                ];
            }
            return $schema;

        case 'Book':
            $bookName = get_post_meta($postId, '_shop2llm_book_name', true);
            if ($bookName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Book',
                'name' => $bookName,
            ];
            $author = get_post_meta($postId, '_shop2llm_book_author', true);
            if ($author !== '') {
                $schema['author'] = [
                    '@type' => 'Person',
                    'name' => $author,
                ];
            }
            $isbn = get_post_meta($postId, '_shop2llm_book_isbn', true);
            if ($isbn !== '') {
                $schema['isbn'] = $isbn;
            }
            $publisher = get_post_meta($postId, '_shop2llm_book_publisher', true);
            if ($publisher !== '') {
                $schema['publisher'] = [
                    '@type' => 'Organization',
                    'name' => $publisher,
                ];
            }
            return $schema;

        case 'MusicRecording':
            $musicName = get_post_meta($postId, '_shop2llm_music_name', true);
            if ($musicName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'MusicRecording',
                'name' => $musicName,
            ];
            $artist = get_post_meta($postId, '_shop2llm_music_artist', true);
            if ($artist !== '') {
                $schema['byArtist'] = [
                    '@type' => 'MusicGroup',
                    'name' => $artist,
                ];
            }
            $duration = get_post_meta($postId, '_shop2llm_music_duration', true);
            if ($duration !== '') {
                $schema['duration'] = 'PT' . $duration . 'M';
            }
            return $schema;

        case 'VideoObject':
            $videoUrl = get_post_meta($postId, '_shop2llm_video_url', true);
            if ($videoUrl === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'VideoObject',
                'name' => get_post_meta($postId, '_shop2llm_video_name', true) ?: get_the_title($post),
                'contentUrl' => esc_url($videoUrl),
            ];
            $desc = get_post_meta($postId, '_shop2llm_video_description', true);
            if ($desc !== '') {
                $schema['description'] = $desc;
            }
            $thumb = get_post_meta($postId, '_shop2llm_video_thumbnail', true);
            if ($thumb !== '') {
                $schema['thumbnailUrl'] = esc_url($thumb);
            }
            return $schema;

        case 'Person':
            $personName = get_post_meta($postId, '_shop2llm_person_name', true);
            if ($personName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Person',
                'name' => $personName,
            ];
            $jobTitle = get_post_meta($postId, '_shop2llm_person_job_title', true);
            if ($jobTitle !== '') {
                $schema['jobTitle'] = $jobTitle;
            }
            $url = get_post_meta($postId, '_shop2llm_person_url', true);
            if ($url !== '') {
                $schema['url'] = esc_url($url);
            }
            $sameAs = get_post_meta($postId, '_shop2llm_person_same_as', true);
            if ($sameAs !== '') {
                $schema['sameAs'] = array_map('esc_url', array_filter(array_map('trim', explode("\n", $sameAs))));
            }
            return $schema;

        case 'Dataset':
            $datasetName = get_post_meta($postId, '_shop2llm_dataset_name', true);
            if ($datasetName === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Dataset',
                'name' => $datasetName,
                'description' => get_post_meta($postId, '_shop2llm_dataset_description', true) ?: wp_strip_all_tags(get_the_excerpt($post)),
            ];
            $creator = get_post_meta($postId, '_shop2llm_dataset_creator', true);
            if ($creator !== '') {
                $schema['creator'] = [
                    '@type' => 'Organization',
                    'name' => $creator,
                ];
            }
            $distribution = get_post_meta($postId, '_shop2llm_dataset_distribution', true);
            if ($distribution !== '') {
                $schema['distribution'] = [
                    '@type' => 'DataDownload',
                    'contentUrl' => esc_url($distribution),
                ];
            }
            return $schema;

        case 'Review':
            $reviewItem = get_post_meta($postId, '_shop2llm_review_item', true);
            if ($reviewItem === '') {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'Review',
                'itemReviewed' => [
                    '@type' => 'Thing',
                    'name' => $reviewItem,
                ],
                'author' => [
                    '@type' => 'Person',
                    'name' => get_the_author_meta('display_name', $post->post_author),
                ],
            ];
            $rating = get_post_meta($postId, '_shop2llm_review_rating', true);
            if ($rating !== '') {
                $schema['reviewRating'] = [
                    '@type' => 'Rating',
                    'ratingValue' => $rating,
                    'bestRating' => '5',
                ];
            }
            return $schema;

        case 'LocalBusiness':
            $localbiz = get_option('shop_to_llm_seo_localbiz', []);
            if (empty($localbiz) || !is_array($localbiz)) {
                return [];
            }
            $schema = [
                '@context' => 'https://schema.org',
                '@type' => 'LocalBusiness',
                'name' => $localbiz['name'] ?? $siteName,
            ];
            if (!empty($localbiz['street_address']) || !empty($localbiz['locality']) || !empty($localbiz['country'])) {
                $schema['address'] = [
                    '@type' => 'PostalAddress',
                    'streetAddress' => $localbiz['street_address'] ?? '',
                    'addressLocality' => $localbiz['locality'] ?? '',
                    'addressRegion' => $localbiz['region'] ?? '',
                    'postalCode' => $localbiz['postal_code'] ?? '',
                    'addressCountry' => $localbiz['country'] ?? '',
                ];
            }
            if (!empty($localbiz['phone'])) {
                $schema['telephone'] = $localbiz['phone'];
            }
            if (!empty($localbiz['hours'])) {
                $schema['openingHours'] = $localbiz['hours'];
            }
            return $schema;

        case 'WebPage':
            return [
                '@context' => 'https://schema.org',
                '@type' => 'WebPage',
                'name' => get_the_title($post),
                'description' => wp_strip_all_tags(get_the_excerpt($post)),
                'url' => get_permalink($postId),
            ];

        default:
            return [];
    }
}

/**
 * Build BreadcrumbList schema.
 *
 * @param WP_Post $post Current post object.
 * @return array BreadcrumbList schema data.
 */
function shop_to_llm_seo_build_breadcrumb(WP_Post $post): array
{
    $items = [
        [
            '@type' => 'ListItem',
            'position' => 1,
            'name' => __('Home', 'shop-to-llm'),
            'item' => home_url('/'),
        ],
    ];

    $position = 2;

    // Add category for posts.
    if ($post->post_type === 'post') {
        $cats = get_the_category($post->ID);
        if (!empty($cats)) {
            $cat = $cats[0];
            $items[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'name' => $cat->name,
                'item' => get_category_link($cat->term_id),
            ];
            $position++;
        }
    }

    // Add product category for WooCommerce products.
    if (shop_to_llm_has_woocommerce() && $post->post_type === 'product') {
        $terms = get_the_terms($post->ID, 'product_cat');
        if (!empty($terms) && !is_wp_error($terms)) {
            $term = $terms[0];
            $items[] = [
                '@type' => 'ListItem',
                'position' => $position,
                'name' => $term->name,
                'item' => get_term_link($term->term_id, 'product_cat'),
            ];
            $position++;
        }
    }

    $items[] = [
        '@type' => 'ListItem',
        'position' => $position,
        'name' => get_the_title($post),
        'item' => get_permalink($post->ID),
    ];

    return [
        '@context' => 'https://schema.org',
        '@type' => 'BreadcrumbList',
        'itemListElement' => $items,
    ];
}

/**
 * Register Schema Settings meta box and render fields.
 */
function shop_to_llm_seo_schema_meta_box_fields(): void
{
    $postTypes = ['post', 'page'];
    if (shop_to_llm_has_woocommerce()) {
        $postTypes[] = 'product';
    }
    foreach ($postTypes as $pt) {
        add_meta_box(
            'shop2llm_schema_settings',
            __('Schema Settings', 'shop-to-llm'),
            'shop_to_llm_seo_render_schema_meta_box',
            $pt,
            'side',
            'default'
        );
    }
}

/**
 * Render the Schema Settings meta box HTML.
 *
 * @param WP_Post $post Current post object.
 */
function shop_to_llm_seo_render_schema_meta_box(WP_Post $post): void
{
    wp_nonce_field('shop2llm_schema_save_' . $post->ID, 'shop2llm_schema_nonce');

    $currentType = get_post_meta($post->ID, '_shop2llm_schema_type', true);
    if (empty($currentType)) {
        $currentType = 'auto';
    }

    $types = [
        'auto' => __('Auto-detect', 'shop-to-llm'),
        'Article' => 'Article',
        'BlogPosting' => 'BlogPosting',
        'Product' => 'Product',
        'FAQ' => 'FAQ (FAQPage)',
        'HowTo' => 'HowTo',
        'BreadcrumbList' => 'BreadcrumbList',
        'LocalBusiness' => 'LocalBusiness',
        'Recipe' => 'Recipe',
        'Event' => 'Event',
        'Course' => 'Course',
        'JobPosting' => 'JobPosting',
        'SoftwareApplication' => 'SoftwareApplication',
        'Book' => 'Book',
        'MusicRecording' => 'MusicRecording',
        'VideoObject' => 'VideoObject',
        'Person' => 'Person',
        'Dataset' => 'Dataset',
        'Review' => 'Review',
        'WebPage' => 'WebPage',
    ];

    echo '<p><label for="_shop2llm_schema_type"><strong>' . esc_html__('Schema Type', 'shop-to-llm') . '</strong></label><br>';
    echo '<select name="_shop2llm_schema_type" id="_shop2llm_schema_type" style="width:100%">';
    foreach ($types as $value => $label) {
        printf(
            '<option value="%s"%s>%s</option>',
            esc_attr($value),
            selected($currentType, $value, false),
            esc_html($label)
        );
    }
    echo '</select></p>';

    // AI Schema Suggest button
    echo '<p><button type="button" id="s2ll-ai-schema-btn" class="button" style="width:100%" title="' . esc_attr__('AI-powered schema type suggestion based on your content', 'shop-to-llm') . '">';
    echo '<span style="font-size:14px">&#9889;</span> ' . esc_html__('AI Suggest Schema', 'shop-to-llm');
    echo '</button></p>';
    echo '<div id="s2ll-ai-schema-status" style="font-size:11px;color:#64748b;margin-bottom:6px;display:none"></div>';

    // FAQ fields.
    echo '<div class="shop2llm-schema-fields" data-schema="FAQ" style="' . ($currentType === 'FAQ' ? '' : 'display:none') . '">';
    for ($i = 1; $i <= 10; $i++) {
        $qVal = esc_attr(get_post_meta($post->ID, '_shop2llm_faq_q' . $i, true));
        $aVal = esc_attr(get_post_meta($post->ID, '_shop2llm_faq_a' . $i, true));
        /* translators: %d: FAQ question number */
        echo '<p><label>' . sprintf(esc_html__('Q%d', 'shop-to-llm'), $i) . '</label> <input type="text" name="_shop2llm_faq_q' . esc_attr($i) . '" value="' . $qVal . '" style="width:100%" /></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        /* translators: %d: FAQ answer number */
        echo '<p><label>' . sprintf(esc_html__('A%d', 'shop-to-llm'), $i) . '</label> <textarea name="_shop2llm_faq_a' . esc_attr($i) . '" style="width:100%;height:50px">' . $aVal . '</textarea></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }
    echo '</div>';

    // HowTo fields.
    echo '<div class="shop2llm-schema-fields" data-schema="HowTo" style="' . ($currentType === 'HowTo' ? '' : 'display:none') . '">';
    for ($i = 1; $i <= 8; $i++) {
        $stepVal = esc_attr(get_post_meta($post->ID, '_shop2llm_howto_step' . $i, true));
        $stepDesc = esc_attr(get_post_meta($post->ID, '_shop2llm_howto_step' . $i . '_desc', true));
        /* translators: %d: HowTo step number */
        echo '<p><label>' . sprintf(esc_html__('Step %d Name', 'shop-to-llm'), $i) . '</label> <input type="text" name="_shop2llm_howto_step' . esc_attr($i) . '" value="' . $stepVal . '" style="width:100%" /></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        /* translators: %d: HowTo step number */
        echo '<p><label>' . sprintf(esc_html__('Step %d Description', 'shop-to-llm'), $i) . '</label> <textarea name="_shop2llm_howto_step' . esc_attr($i) . '_desc" style="width:100%;height:50px">' . $stepDesc . '</textarea></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }
    echo '</div>';

    // Recipe fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Recipe" style="' . ($currentType === 'Recipe' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_recipe_name', __('Recipe Name', 'shop-to-llm'));
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_recipe_ingredients', __('Ingredients (one per line)', 'shop-to-llm'));
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_recipe_instructions', __('Instructions', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_recipe_cook_time', __('Cook Time (minutes)', 'shop-to-llm'));
    echo '</div>';

    // Event fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Event" style="' . ($currentType === 'Event' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_event_name', __('Event Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_event_start_date', __('Start Date', 'shop-to-llm'), 'date');
    shop_to_llm_seo_render_text_field($post, '_shop2llm_event_location', __('Location', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_event_organizer', __('Organizer', 'shop-to-llm'));
    echo '</div>';

    // Course fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Course" style="' . ($currentType === 'Course' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_course_name', __('Course Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_course_provider', __('Provider', 'shop-to-llm'));
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_course_description', __('Description', 'shop-to-llm'));
    echo '</div>';

    // JobPosting fields.
    echo '<div class="shop2llm-schema-fields" data-schema="JobPosting" style="' . ($currentType === 'JobPosting' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_job_title', __('Job Title', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_job_organization', __('Hiring Organization', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_job_location', __('Location', 'shop-to-llm'));
    echo '</div>';

    // SoftwareApplication fields.
    echo '<div class="shop2llm-schema-fields" data-schema="SoftwareApplication" style="' . ($currentType === 'SoftwareApplication' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_app_name', __('App Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_app_os', __('Operating System', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_app_version', __('Version', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_app_price', __('Price', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_app_currency', __('Currency', 'shop-to-llm'));
    echo '</div>';

    // Book fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Book" style="' . ($currentType === 'Book' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_book_name', __('Book Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_book_author', __('Author', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_book_isbn', __('ISBN', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_book_publisher', __('Publisher', 'shop-to-llm'));
    echo '</div>';

    // MusicRecording fields.
    echo '<div class="shop2llm-schema-fields" data-schema="MusicRecording" style="' . ($currentType === 'MusicRecording' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_music_name', __('Track Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_music_artist', __('Artist', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_music_duration', __('Duration (minutes)', 'shop-to-llm'));
    echo '</div>';

    // VideoObject fields.
    echo '<div class="shop2llm-schema-fields" data-schema="VideoObject" style="' . ($currentType === 'VideoObject' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_video_url', __('Video URL', 'shop-to-llm'), 'url');
    shop_to_llm_seo_render_text_field($post, '_shop2llm_video_name', __('Video Title', 'shop-to-llm'));
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_video_description', __('Description', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_video_thumbnail', __('Thumbnail URL', 'shop-to-llm'), 'url');
    echo '</div>';

    // Person fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Person" style="' . ($currentType === 'Person' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_person_name', __('Person Name', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_person_job_title', __('Job Title', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_person_url', __('Website', 'shop-to-llm'), 'url');
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_person_same_as', __('Social Profiles (one per line)', 'shop-to-llm'));
    echo '</div>';

    // Dataset fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Dataset" style="' . ($currentType === 'Dataset' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_dataset_name', __('Dataset Name', 'shop-to-llm'));
    shop_to_llm_seo_render_textarea_field($post, '_shop2llm_dataset_description', __('Description', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_dataset_creator', __('Creator', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_dataset_distribution', __('Download URL', 'shop-to-llm'), 'url');
    echo '</div>';

    // Review fields.
    echo '<div class="shop2llm-schema-fields" data-schema="Review" style="' . ($currentType === 'Review' ? '' : 'display:none') . '">';
    shop_to_llm_seo_render_text_field($post, '_shop2llm_review_item', __('Reviewed Item', 'shop-to-llm'));
    shop_to_llm_seo_render_text_field($post, '_shop2llm_review_rating', __('Rating (1-5)', 'shop-to-llm'), 'number');
    echo '</div>';

    // JavaScript to show/hide conditional fields.
    echo '<script>
    (function(){
        var sel = document.getElementById("_shop2llm_schema_type");
        if(!sel) return;
        function toggle(){
            var v = sel.value;
            var fields = document.querySelectorAll(".shop2llm-schema-fields");
            for(var i=0;i<fields.length;i++){
                var s = fields[i].getAttribute("data-schema");
                fields[i].style.display = (s === v) ? "" : "none";
            }
        }
        sel.addEventListener("change", toggle);
        toggle();
    })();

    // AI Schema Suggest
    (function(){
        var aiBtn = document.getElementById("s2ll-ai-schema-btn");
        var sel = document.getElementById("_shop2llm_schema_type");
        var statusEl = document.getElementById("s2ll-ai-schema-status");
        if (!aiBtn || !sel) return;
        aiBtn.addEventListener("click", function(){
            aiBtn.disabled = true;
            aiBtn.textContent = "...";
            statusEl.style.display = "block";
            statusEl.textContent = ' . wp_json_encode(__('Analyzing content for best schema type...', 'shop-to-llm')) . ';
            statusEl.style.color = "#64748b";

            var fd = new FormData();
            fd.append("action", "s2ll_seo_ai_schema");
            fd.append("post_id", ' . (int) $post->ID . ');
            fd.append("nonce", ' . wp_json_encode(wp_create_nonce('s2ll_ai_nonce')) . ');

            fetch(ajaxurl, {method: "POST", body: fd, credentials: "same-origin"})
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    aiBtn.disabled = false;
                    aiBtn.innerHTML = \'<span style="font-size:14px">&#9889;</span> ' . esc_js(__('AI Suggest Schema', 'shop-to-llm')) . '\';
                    if (data.success && data.data && data.data.schema_type) {
                        var newType = data.data.schema_type;
                        var conf = data.data.confidence ? " (" + Math.round(data.data.confidence * 100) + "% confidence)" : "";
                        var reason = data.data.reason ? " — " + data.data.reason : "";
                        // Set the dropdown
                        for (var i = 0; i < sel.options.length; i++) {
                            if (sel.options[i].value === newType) {
                                sel.value = newType;
                                break;
                            }
                        }
                        // Trigger the change event to show/hide fields
                        sel.dispatchEvent(new Event("change"));
                        statusEl.textContent = "✓ " + newType + conf + reason;
                        statusEl.style.color = "#16a34a";
                        if (data.data.from === "local" || data.data.error_notice) {
                            statusEl.textContent += " — ' . esc_js(__('(local analysis)', 'shop-to-llm')) . '";
                        }
                    } else if (data.data && data.data.error_notice) {
                        statusEl.textContent = data.data.error_notice;
                        statusEl.style.color = "#ca8a04";
                    } else {
                        statusEl.textContent = ' . wp_json_encode(__('Could not determine schema type.', 'shop-to-llm')) . ';
                        statusEl.style.color = "#dc2626";
                    }
                })
                .catch(function(){
                    aiBtn.disabled = false;
                    aiBtn.innerHTML = \'<span style="font-size:14px">&#9889;</span> ' . esc_js(__('AI Suggest Schema', 'shop-to-llm')) . '\';
                    statusEl.textContent = ' . wp_json_encode(__('Network error. Please try again.', 'shop-to-llm')) . ';
                    statusEl.style.color = "#dc2626";
                });
        });
    })();
    </script>';
}

/**
 * Render a text input field for schema meta box.
 *
 * @param WP_Post $post     Post object.
 * @param string  $metaKey  Meta key.
 * @param string  $label    Field label.
 * @param string  $type     Input type (text, url, number, date).
 */
function shop_to_llm_seo_render_text_field(WP_Post $post, string $metaKey, string $label, string $type = 'text'): void
{
    $value = esc_attr(get_post_meta($post->ID, $metaKey, true));
    echo '<p><label for="' . esc_attr($metaKey) . '">' . esc_html($label) . '</label><br>';
    echo '<input type="' . esc_attr($type) . '" name="' . esc_attr($metaKey) . '" id="' . esc_attr($metaKey) . '" value="' . $value . '" style="width:100%" /></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Render a textarea field for schema meta box.
 *
 * @param WP_Post $post     Post object.
 * @param string  $metaKey  Meta key.
 * @param string  $label    Field label.
 */
function shop_to_llm_seo_render_textarea_field(WP_Post $post, string $metaKey, string $label): void
{
    $value = esc_textarea(get_post_meta($post->ID, $metaKey, true));
    echo '<p><label for="' . esc_attr($metaKey) . '">' . esc_html($label) . '</label><br>';
    echo '<textarea name="' . esc_attr($metaKey) . '" id="' . esc_attr($metaKey) . '" style="width:100%;height:60px">' . $value . '</textarea></p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}

/**
 * Save schema meta box fields.
 *
 * @param int $postId Post ID being saved.
 */
function shop_to_llm_seo_save_schema_meta(int $postId): void
{
    if (!isset($_POST['shop2llm_schema_nonce'])) {
        return;
    }
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_schema_nonce'])), 'shop2llm_schema_save_' . $postId)) {
        return;
    }
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }

    // Schema type.
    if (isset($_POST['_shop2llm_schema_type'])) {
        update_post_meta($postId, '_shop2llm_schema_type', sanitize_text_field(wp_unslash($_POST['_shop2llm_schema_type'])));
    }

    // All schema meta fields.
    $textFields = [
        '_shop2llm_faq_q1', '_shop2llm_faq_q2', '_shop2llm_faq_q3', '_shop2llm_faq_q4', '_shop2llm_faq_q5',
        '_shop2llm_faq_q6', '_shop2llm_faq_q7', '_shop2llm_faq_q8', '_shop2llm_faq_q9', '_shop2llm_faq_q10',
        '_shop2llm_howto_step1', '_shop2llm_howto_step2', '_shop2llm_howto_step3', '_shop2llm_howto_step4',
        '_shop2llm_howto_step5', '_shop2llm_howto_step6', '_shop2llm_howto_step7', '_shop2llm_howto_step8',
        '_shop2llm_recipe_name', '_shop2llm_recipe_cook_time',
        '_shop2llm_event_name', '_shop2llm_event_start_date', '_shop2llm_event_location', '_shop2llm_event_organizer',
        '_shop2llm_course_name', '_shop2llm_course_provider',
        '_shop2llm_job_title', '_shop2llm_job_organization', '_shop2llm_job_location',
        '_shop2llm_app_name', '_shop2llm_app_os', '_shop2llm_app_version', '_shop2llm_app_price', '_shop2llm_app_currency',
        '_shop2llm_book_name', '_shop2llm_book_author', '_shop2llm_book_isbn', '_shop2llm_book_publisher',
        '_shop2llm_music_name', '_shop2llm_music_artist', '_shop2llm_music_duration',
        '_shop2llm_video_url', '_shop2llm_video_name', '_shop2llm_video_thumbnail',
        '_shop2llm_person_name', '_shop2llm_person_job_title', '_shop2llm_person_url',
        '_shop2llm_dataset_name', '_shop2llm_dataset_creator', '_shop2llm_dataset_distribution',
        '_shop2llm_review_item', '_shop2llm_review_rating',
        '_shop2llm_course_description',
    ];

    $textareaFields = [
        '_shop2llm_faq_a1', '_shop2llm_faq_a2', '_shop2llm_faq_a3', '_shop2llm_faq_a4', '_shop2llm_faq_a5',
        '_shop2llm_faq_a6', '_shop2llm_faq_a7', '_shop2llm_faq_a8', '_shop2llm_faq_a9', '_shop2llm_faq_a10',
        '_shop2llm_howto_step1_desc', '_shop2llm_howto_step2_desc', '_shop2llm_howto_step3_desc', '_shop2llm_howto_step4_desc',
        '_shop2llm_howto_step5_desc', '_shop2llm_howto_step6_desc', '_shop2llm_howto_step7_desc', '_shop2llm_howto_step8_desc',
        '_shop2llm_recipe_ingredients', '_shop2llm_recipe_instructions',
        '_shop2llm_video_description',
        '_shop2llm_person_same_as',
        '_shop2llm_dataset_description',
    ];

    foreach ($textFields as $field) {
        if (isset($_POST[$field])) {
            update_post_meta($postId, $field, sanitize_text_field(wp_unslash($_POST[$field])));
        }
    }

    foreach ($textareaFields as $field) {
        if (isset($_POST[$field])) {
            update_post_meta($postId, $field, sanitize_textarea_field(wp_unslash($_POST[$field])));
        }
    }
}

// ─── SEO Audit Module ────────────────────────────────────────────────

/**
 * Run a site-wide SEO audit on all published posts.
 *
 * @return array Audit results with total_posts, issues, and summary.
 */
function shop_to_llm_seo_run_audit(): array
{
    // Exclude WooCommerce system pages (Cart, Checkout, My Account, Shop)
    // — these are functional endpoints, not SEO-optimizable content.
    $excludeIds = [];
    if (shop_to_llm_has_woocommerce()) {
        $wcPages = [
            wc_get_page_id('shop'),
            wc_get_page_id('cart'),
            wc_get_page_id('checkout'),
            wc_get_page_id('myaccount'),
        ];
        $excludeIds = array_filter($wcPages, function ($id) {
            return $id > 0;
        });
    }

    $posts = get_posts([
        'post_type' => ['post', 'page'],
        'post_status' => 'publish',
        'posts_per_page' => -1,
        'fields' => 'ids',
    ]);
    // Apply exclusion in PHP layer to avoid WPVIPMinimum PostNotIn warning.
    if (!empty($excludeIds)) {
        $exclude_set = array_flip($excludeIds);
        $posts = array_filter($posts, static function ($id) use ($exclude_set) {
            return !isset($exclude_set[$id]);
        });
        $posts = array_values($posts);
    }

    if (shop_to_llm_has_woocommerce()) {
        $products = get_posts([
            'post_type' => 'product',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'fields' => 'ids',
        ]);
        $posts = array_merge($posts, $products);
    }

    $issues = [];
    $critical = 0;
    $warnings = 0;
    $passed = 0;

    foreach ($posts as $postId) {
        $post = get_post($postId);
        if (!$post) {
            continue;
        }

        $problems = [];
        $content = $post->post_content;
        $title = get_the_title($post);

        // Missing meta description.
        $metaDesc = get_post_meta($postId, '_shop2llm_meta_desc', true);
        if (empty($metaDesc)) {
            $problems[] = __('Missing meta description', 'shop-to-llm');
        }

        // Missing focus keyword.
        $focusKw = get_post_meta($postId, '_shop2llm_focus_kw', true);
        if (empty($focusKw)) {
            $problems[] = __('Missing focus keyword', 'shop-to-llm');
        }

        // Title too long or too short.
        $titleLen = mb_strlen($title);
        if ($titleLen > 60) {
            $problems[] = sprintf(/* translators: %d: character count */ __('Title too long (%d chars, max 60)', 'shop-to-llm'), $titleLen);
        } elseif ($titleLen < 30 && $titleLen > 0) {
            $problems[] = sprintf(/* translators: %d: character count */ __('Title too short (%d chars, min 30)', 'shop-to-llm'), $titleLen);
        }

        // No image alt text.
        if (preg_match_all('/<img[^>]+>/i', $content, $imgMatches)) {
            foreach ($imgMatches[0] as $img) {
                if (!preg_match('/alt=["\'][^"\']+["\']/i', $img)) {
                    $problems[] = __('Image missing alt text', 'shop-to-llm');
                    break;
                }
            }
        }

        // No internal links.
        $siteUrl = home_url();
        if (!preg_match('/href=["\'][^"\']*' . preg_quote($siteUrl, '/') . '[^"\']*["\']/i', $content)) {
            $problems[] = __('No internal links', 'shop-to-llm');
        }

        // Missing canonical URL.
        $canonical = get_post_meta($postId, '_shop2llm_canonical', true);
        if (empty($canonical)) {
            $problems[] = __('Missing canonical URL', 'shop-to-llm');
        }

        // Content too short.
        $wordCount = str_word_count(wp_strip_all_tags($content));
        if ($wordCount < 300) {
            $problems[] = sprintf(/* translators: %d: word count */ __('Content too short (%d words, min 300)', 'shop-to-llm'), $wordCount);
        }

        // Noindex set unintentionally.
        $noindex = get_post_meta($postId, '_shop2llm_noindex', true);
        if ($noindex === '1' || $noindex === 'yes') {
            $problems[] = __('Noindex set — page hidden from search engines', 'shop-to-llm');
        }

        // Missing H1.
        if (!preg_match('/<h1[^>]*>/i', $content)) {
            $problems[] = __('Missing H1 heading', 'shop-to-llm');
        }

        // Missing featured image.
        if (!has_post_thumbnail($postId) && $post->post_type !== 'page') {
            $problems[] = __('Missing featured image', 'shop-to-llm');
        }

        if (!empty($problems)) {
            $issues[] = [
                'post_id' => $postId,
                'post_title' => $title,
                'problems' => $problems,
            ];
            $criticalCount = count(array_filter($problems, function ($p) {
                return in_array($p, [
                    __('Missing meta description', 'shop-to-llm'),
                    __('Noindex set — page hidden from search engines', 'shop-to-llm'),
                ], true);
            }));
            $critical += $criticalCount;
            $warnings += (count($problems) - $criticalCount);
        } else {
            $passed++;
        }
    }

    return [
        'total_posts' => count($posts),
        'issues' => $issues,
        'summary' => [
            'critical' => $critical,
            'warnings' => $warnings,
            'passed' => $passed,
        ],
    ];
}

/**
 * AJAX handler for running SEO audit.
 */
function shop_to_llm_seo_audit_ajax(): void
{
    check_ajax_referer('shop2llm_seo_audit', 'nonce');
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => __('Unauthorized', 'shop-to-llm')], 403);
    }

    $results = shop_to_llm_seo_run_audit();
    update_option('shop_to_llm_seo_last_audit', current_time('mysql'));
    update_option('shop_to_llm_seo_last_audit_result', $results['issues']);
    wp_send_json_success($results);
}

/**
 * Cron callback for weekly SEO email report.
 */
function shop_to_llm_seo_email_weekly_report(): void
{
    $results = shop_to_llm_seo_run_audit();
    $siteName = get_bloginfo('name');
    $adminEmail = get_option('admin_email');

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

    /* translators: %s: site name */
    $subject = sprintf(__('Weekly SEO Report — %s', 'shop-to-llm'), $siteName);

    /* translators: %s: site name */
    $body = '<h2>' . sprintf(esc_html__('Weekly SEO Report — %s', 'shop-to-llm'), esc_html($siteName)) . '</h2>';
    $body .= '<p>' . sprintf(
        /* translators: 1: total posts count, 2: critical issues count, 3: warnings count, 4: passed count */
        esc_html__('Total posts: %1$d | Critical: %2$d | Warnings: %3$d | Passed: %4$d', 'shop-to-llm'),
        $results['total_posts'],
        $results['summary']['critical'],
        $results['summary']['warnings'],
        $results['summary']['passed']
    ) . '</p>';

    // Top 5 issues.
    $topIssues = array_slice($results['issues'], 0, 5);
    if (!empty($topIssues)) {
        $body .= '<h3>' . esc_html__('Top Issues', 'shop-to-llm') . '</h3>';
        $body .= '<ul>';
        foreach ($topIssues as $item) {
            $editLink = get_edit_post_link($item['post_id'], 'raw');
            $body .= '<li>';
            $body .= '<strong>' . esc_html($item['post_title']) . '</strong>';
            if ($editLink) {
                $body .= ' (<a href="' . esc_url($editLink) . '">' . esc_html__('Edit', 'shop-to-llm') . '</a>)';
            }
            $body .= '<ul>';
            foreach ($item['problems'] as $problem) {
                $body .= '<li>' . esc_html($problem) . '</li>';
            }
            $body .= '</ul></li>';
        }
        $body .= '</ul>';
    }

    $body .= '<p><a href="' . esc_url(admin_url('admin.php?page=shop2llm')) . '">' . esc_html__('View Full SEO Dashboard', 'shop-to-llm') . '</a></p>';

    $headers = ['Content-Type: text/html; charset=UTF-8'];
    wp_mail($adminEmail, $subject, $body, $headers);
}

/**
 * Schedule weekly cron for SEO audit if enabled.
 */
function shop_to_llm_seo_schedule_audit_cron(): void
{
    $hook = 'shop2llm_seo_weekly_audit';
    if (get_option('shop_to_llm_seo_audit', 'yes') === 'yes') {
        if (!wp_next_scheduled($hook)) {
            wp_schedule_event(time(), 'weekly', $hook);
        }
    } else {
        wp_clear_scheduled_hook($hook);
    }
}

// ─── Migration Module (Import from Yoast/RankMath) ──────────────────

/**
 * Import SEO meta data from Yoast SEO or Rank Math.
 *
 * Processes all published posts in batches of 100.
 * Maps Yoast/Rank Math meta keys to Shop2LLM meta keys.
 *
 * @return array Count of migrated posts and errors.
 */
function shop_to_llm_seo_migrate_data(): array
{
    $migrated = 0;
    $errors = [];

    // Detect which SEO plugin is active.
    $activePlugins = get_option('active_plugins', []);
    $hasYoast = in_array('wordpress-seo/wp-seo.php', $activePlugins, true);
    $hasRankMath = in_array('seo-by-rank-math/rank-math.php', $activePlugins, true);

    if (!$hasYoast && !$hasRankMath) {
        return ['migrated' => 0, 'errors' => [__('No supported SEO plugin detected (Yoast or Rank Math required)', 'shop-to-llm')]];
    }

    $postTypes = ['post', 'page'];
    if (shop_to_llm_has_woocommerce()) {
        $postTypes[] = 'product';
    }

    $offset = 0;
    $batchSize = 100;

    while (true) {
        $posts = get_posts([
            'post_type' => $postTypes,
            'post_status' => 'publish',
            'posts_per_page' => $batchSize,
            'offset' => $offset,
            'fields' => 'ids',
        ]);

        if (empty($posts)) {
            break;
        }

        foreach ($posts as $postId) {
            try {
                if ($hasYoast) {
                    $yoastTitle = get_post_meta($postId, '_yoast_wpseo_title', true);
                    $yoastDesc = get_post_meta($postId, '_yoast_wpseo_metadesc', true);
                    $yoastKw = get_post_meta($postId, '_yoast_wpseo_focuskw', true);
                    $yoastNoindex = get_post_meta($postId, '_yoast_wpseo_meta-robots-noindex', true);
                    $yoastCanonical = get_post_meta($postId, '_yoast_wpseo_canonical', true);

                    if ($yoastTitle !== '') {
                        update_post_meta($postId, '_shop2llm_seo_title', $yoastTitle);
                    }
                    if ($yoastDesc !== '') {
                        update_post_meta($postId, '_shop2llm_meta_desc', $yoastDesc);
                    }
                    if ($yoastKw !== '') {
                        update_post_meta($postId, '_shop2llm_focus_kw', $yoastKw);
                    }
                    if ($yoastNoindex === '1') {
                        update_post_meta($postId, '_shop2llm_noindex', '1');
                    }
                    if ($yoastCanonical !== '') {
                        update_post_meta($postId, '_shop2llm_canonical', $yoastCanonical);
                    }
                    $migrated++;
                }

                if ($hasRankMath) {
                    $rmTitle = get_post_meta($postId, 'rank_math_title', true);
                    $rmDesc = get_post_meta($postId, 'rank_math_description', true);
                    $rmKw = get_post_meta($postId, 'rank_math_focus_keyword', true);
                    $rmRobots = get_post_meta($postId, 'rank_math_robots', true);
                    $rmCanonical = get_post_meta($postId, 'rank_math_canonical_url', true);

                    if ($rmTitle !== '') {
                        update_post_meta($postId, '_shop2llm_seo_title', $rmTitle);
                    }
                    if ($rmDesc !== '') {
                        update_post_meta($postId, '_shop2llm_meta_desc', $rmDesc);
                    }
                    if ($rmKw !== '') {
                        update_post_meta($postId, '_shop2llm_focus_kw', $rmKw);
                    }
                    if (is_array($rmRobots) && !empty($rmRobots['noindex'])) {
                        update_post_meta($postId, '_shop2llm_noindex', '1');
                    }
                    if ($rmCanonical !== '') {
                        update_post_meta($postId, '_shop2llm_canonical', $rmCanonical);
                    }
                    $migrated++;
                }
            } catch (Exception $e) {
                $errors[] = sprintf('Post %d: %s', $postId, $e->getMessage());
            }
        }

        $offset += $batchSize;
        // Prevent timeout on very large sites.
        if (count($posts) < $batchSize) {
            break;
        }
    }

    return ['migrated' => $migrated, 'errors' => $errors];
}

/**
 * AJAX handler for SEO data migration.
 */
function shop_to_llm_seo_migrate_ajax(): void
{
    check_ajax_referer('shop2llm_seo_migrate', 'nonce');
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => __('Unauthorized', 'shop-to-llm')], 403);
    }

    $results = shop_to_llm_seo_migrate_data();
    wp_send_json_success($results);
}

// ─── Internal Link Suggestions ──────────────────────────────────────

/**
 * Suggest internal links based on keyword overlap.
 *
 * @param string $content Post content.
 * @param int    $postId  Current post ID.
 * @return array Suggested posts with relevance scores.
 */
function shop_to_llm_seo_suggest_internal_links(string $content, int $postId): array
{
    // Extract top 5 keywords from title and content.
    $text = get_the_title($postId) . ' ' . wp_strip_all_tags($content);
    $stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'is', 'it', 'this', 'that', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'not', 'no', 'so', 'if', 'as', 'up', 'out', 'about', 'into', 'over', 'after', 'then', 'than', 'too', 'very', 'just', 'also', 'now', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own', 'same', 'its', 'our', 'we', 'you', 'your', 'they', 'them', 'their', 'what', 'which', 'who', 'whom'];

    // Chinese stop words
    $zh_stopwords = ['的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这', '他', '她', '它', '们', '那', '些', '什么', '怎么', '如何', '可以', '因为', '所以', '但是', '如果', '虽然', '已经', '还是', '或者', '以及', '对于', '关于', '通过', '之后', '之前', '之间', '其他', '其中', '这些', '那些', '这个', '那个', '每个', '所有', '一些', '任何', '没', '把', '让', '被', '从', '向', '对', '与', '而', '之', '等', '及', '其', '此'];
    $stopWords = array_merge($stopWords, $zh_stopwords);

    // CJK-aware keyword extraction
    if (preg_match('/[\x{4e00}-\x{9fff}]/u', $text)) {
        // Chinese: extract 2-4 character segments using bigram-like approach
        $words = [];
        $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
        $len = count($chars);
        for ($i = 0; $i < $len - 1; $i++) {
            if (preg_match('/[\x{4e00}-\x{9fff}]/u', $chars[$i])) {
                // Bigram
                if ($i + 1 < $len && preg_match('/[\x{4e00}-\x{9fff}]/u', $chars[$i + 1])) {
                    $bigram = $chars[$i] . $chars[$i + 1];
                    if (!in_array($bigram, $zh_stopwords, true)) {
                        $words[] = $bigram;
                    }
                }
            }
        }
    } else {
        $words = str_word_count(strtolower($text), 1);
    }

    $freq = [];
    foreach ($words as $word) {
        $word = trim($word);
        if (mb_strlen($word) < 3 || in_array($word, $stopWords, true)) {
            continue;
        }
        if (!isset($freq[$word])) {
            $freq[$word] = 0;
        }
        $freq[$word]++;
    }
    arsort($freq);
    $keywords = array_slice(array_keys($freq), 0, 5);

    if (empty($keywords)) {
        return [];
    }

    // Find already-linked post IDs in the current content.
    $linkedIds = [];
    if (preg_match_all('/href=["\'][^"\']*\/\?p=(\d+)[^"\']*["\']/i', $content, $m1)) {
        $linkedIds = array_map('absint', $m1[1]);
    }
    // Also match post permalinks by querying.
    if (preg_match_all('/href=["\'](' . preg_quote(home_url(), '/') . '[^"\']+)["\']/i', $content, $m2)) {
        foreach ($m2[1] as $url) {
            $linkedPostId = url_to_postid($url);
            if ($linkedPostId) {
                $linkedIds[] = $linkedPostId;
            }
        }
    }
    $linkedIds[] = $postId;
    $linkedIds = array_unique($linkedIds);

    // Search for posts containing the keywords.
    $suggestions = [];
    foreach ($keywords as $keyword) {
        $found = get_posts([
            's' => $keyword,
            'post_type' => ['post', 'page'],
            'post_status' => 'publish',
            'posts_per_page' => 10,
            'fields' => 'ids',
        ]);
        // Already-linked IDs are filtered in PHP below (avoids WPVIPMinimum PostNotIn warning).

        foreach ($found as $foundId) {
            if (in_array($foundId, $linkedIds, true)) {
                continue;
            }
            if (!isset($suggestions[$foundId])) {
                $suggestions[$foundId] = 0;
            }
            $suggestions[$foundId]++;
        }
    }

    if (empty($suggestions)) {
        return [];
    }

    arsort($suggestions);

    $result = [];
    foreach ($suggestions as $sugId => $relevance) {
        $result[] = [
            'post_id' => $sugId,
            'title' => get_the_title($sugId),
            'url' => get_permalink($sugId),
            'relevance' => $relevance,
        ];
        if (count($result) >= 5) {
            break;
        }
    }

    return $result;
}

// ─── SEO Column in Post List ────────────────────────────────────────

/**
 * Add SEO score and focus keyword columns to post list tables.
 *
 * @param array $columns Existing columns.
 * @return array Modified columns.
 */
function shop_to_llm_seo_add_columns(array $columns): array
{
    $newColumns = [];
    foreach ($columns as $key => $label) {
        $newColumns[$key] = $label;
        if ($key === 'title') {
            $newColumns['seo_score'] = __('SEO', 'shop-to-llm');
            $newColumns['focus_kw'] = __('Focus KW', 'shop-to-llm');
        }
    }
    return $newColumns;
}

/**
 * Render SEO score and focus keyword in post list columns.
 *
 * @param string $column Column name.
 * @param int    $postId Post ID.
 */
function shop_to_llm_seo_render_column(string $column, int $postId): void
{
    if ($column === 'seo_score') {
        $score = shop_to_llm_seo_calculate_score($postId);
        $color = '#dc2626'; // Red.
        if ($score >= 70) {
            $color = '#16a34a'; // Green.
        } elseif ($score >= 40) {
            $color = '#ca8a04'; // Yellow.
        }
        /* translators: %d: SEO score */
        echo '<span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:' . esc_attr($color) . '" title="' . esc_attr(sprintf(__('SEO Score: %d', 'shop-to-llm'), $score)) . '"></span>';
        echo ' <span style="font-size:12px;color:#64748b">' . esc_html($score) . '</span>';
    }

    if ($column === 'focus_kw') {
        $kw = get_post_meta($postId, '_shop2llm_focus_kw', true);
        echo esc_html($kw ?: '—');
    }
}

/**
 * Calculate a simple SEO score for a post.
 *
 * @param int $postId Post ID.
 * @return int Score from 0 to 100.
 */
function shop_to_llm_seo_calculate_score(int $postId): int
{
    $analysis = shop_to_llm_seo_analyze_content($postId);
    return $analysis['score'] ?? 0;
}

// ─── Quick Edit SEO ─────────────────────────────────────────────────

/**
 * Add focus keyword and SEO title fields to Quick Edit panel.
 *
 * @param string $columnName Column name.
 * @param string $postType   Post type.
 */
function shop_to_llm_seo_quick_edit_fields(string $columnName, string $postType): void
{
    if ($columnName !== 'seo_score' && $columnName !== 'focus_kw') {
        return;
    }

    static $rendered = false;
    if ($rendered) {
        return;
    }
    $rendered = true;

    echo '<fieldset class="inline-edit-col-left" style="margin-top:8px">';
    echo '<div class="inline-edit-col">';
    wp_nonce_field('shop2llm_quick_edit', 'shop2llm_quick_edit_nonce');
    echo '<label><span class="title">' . esc_html__('Focus Keyword', 'shop-to-llm') . '</span>';
    echo '<span class="input-text-wrap"><input type="text" name="_shop2llm_focus_kw" value="" /></span></label>';
    echo '<label><span class="title">' . esc_html__('SEO Title', 'shop-to-llm') . '</span>';
    echo '<span class="input-text-wrap"><input type="text" name="_shop2llm_seo_title" value="" /></span></label>';
    echo '</div></fieldset>';
}

/**
 * Save Quick Edit SEO fields.
 *
 * @param int $postId Post ID being saved.
 */
function shop_to_llm_seo_save_quick_edit(int $postId): void
{
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
        return;
    }
    if (!current_user_can('edit_post', $postId)) {
        return;
    }
    // Quick Edit nonce verification (falls back to capability check for programmatic saves)
    if (isset($_POST['shop2llm_quick_edit_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['shop2llm_quick_edit_nonce'])), 'shop2llm_quick_edit')) {
        return;
    }

    // phpcs:disable WordPress.Security.NonceVerification.Missing — nonce verified above when present; capability check covers programmatic saves (WP-CLI, imports)
    if (isset($_POST['_shop2llm_focus_kw'])) {
        update_post_meta($postId, '_shop2llm_focus_kw', sanitize_text_field(wp_unslash($_POST['_shop2llm_focus_kw'])));
    }
    if (isset($_POST['_shop2llm_seo_title'])) {
        update_post_meta($postId, '_shop2llm_seo_title', sanitize_text_field(wp_unslash($_POST['_shop2llm_seo_title'])));
    }
    // phpcs:enable
}

// ─── Broken Link Checker ────────────────────────────────────────────

/**
 * Check external links in post content for broken status.
 *
 * @return array Broken links with post ID, URL, and HTTP status.
 */
function shop_to_llm_seo_check_broken_links(): array
{
    $cached = get_transient('shop2llm_broken_links_cache');
    if ($cached !== false) {
        return $cached;
    }

    $posts = get_posts([
        'post_type' => ['post', 'page'],
        'post_status' => 'publish',
        'posts_per_page' => 50,
        'fields' => 'ids',
    ]);

    $brokenLinks = [];
    $checkedUrls = [];
    $totalChecked = 0;

    foreach ($posts as $postId) {
        if ($totalChecked >= 50) {
            break;
        }

        $post = get_post($postId);
        if (!$post) {
            continue;
        }

        if (!preg_match_all('/href=["\'](https?:\/\/[^"\']+)["\']/i', $post->post_content, $matches)) {
            continue;
        }

        $siteHost = wp_parse_url(home_url(), PHP_URL_HOST);

        foreach ($matches[1] as $url) {
            if ($totalChecked >= 50) {
                break 2;
            }

            $urlHost = wp_parse_url($url, PHP_URL_HOST);
            // Skip internal links.
            if ($urlHost === $siteHost) {
                continue;
            }

            // Skip already checked URLs.
            $urlKey = md5($url);
            if (isset($checkedUrls[$urlKey])) {
                continue;
            }

            $response = wp_remote_head($url, [
                'timeout' => 10,
                'redirection' => 3,
                'sslverify' => false,
                'user-agent' => 'Shop2LLM Broken Link Checker/' . get_bloginfo('version'),
            ]);

            $checkedUrls[$urlKey] = true;
            $totalChecked++;

            if (is_wp_error($response)) {
                $brokenLinks[] = [
                    'post_id' => $postId,
                    'url' => $url,
                    'status' => 0,
                ];
                continue;
            }

            $code = wp_remote_retrieve_response_code($response);
            if ($code >= 400 || $code === 0) {
                $brokenLinks[] = [
                    'post_id' => $postId,
                    'url' => $url,
                    'status' => absint($code),
                ];
            }
        }
    }

    set_transient('shop2llm_broken_links_cache', $brokenLinks, DAY_IN_SECONDS);
    return $brokenLinks;
}

// ─── Image SEO ──────────────────────────────────────────────────────

/**
 * Check images in post content for SEO issues.
 *
 * @param int $postId Post ID.
 * @return array Image SEO issues.
 */
function shop_to_llm_seo_check_images(int $postId): array
{
    $post = get_post($postId);
    if (!$post) {
        return [];
    }

    $issues = [];

    if (!preg_match_all('/<img\s+[^>]*>/i', $post->post_content, $matches)) {
        return $issues;
    }

    foreach ($matches[0] as $img) {
        // Extract src.
        $src = '';
        if (preg_match('/src=["\']([^"\']+)["\']/i', $img, $srcMatch)) {
            $src = $srcMatch[1];
        }

        // Extract alt.
        $alt = '';
        if (preg_match('/alt=["\']([^"\']*)["\']/i', $img, $altMatch)) {
            $alt = $altMatch[1];
        }

        // Missing alt text.
        if ($alt === '' || !preg_match('/alt=/i', $img)) {
            $issues[] = [
                'type' => 'missing_alt',
                'src' => $src,
                'message' => __('Missing alt text', 'shop-to-llm'),
            ];
            continue;
        }

        // Alt text too short.
        if (mb_strlen($alt) < 3) {
            $issues[] = [
                'type' => 'short_alt',
                'src' => $src,
                'alt' => $alt,
                'message' => sprintf(/* translators: %s: alt text */ __('Alt text too short: "%s"', 'shop-to-llm'), $alt),
            ];
            continue;
        }

        // Alt text same as filename.
        if ($src !== '') {
            $filename = basename(wp_parse_url($src, PHP_URL_PATH));
            $filenameWithoutExt = pathinfo($filename, PATHINFO_FILENAME);
            $filenameClean = preg_replace('/[-_]/', ' ', $filenameWithoutExt);
            if ($filenameClean !== null && strtolower(trim($alt)) === strtolower(trim($filenameClean))) {
                $issues[] = [
                    'type' => 'alt_is_filename',
                    'src' => $src,
                    'alt' => $alt,
                    'message' => sprintf(/* translators: %s: alt text */ __('Alt text matches filename: "%s"', 'shop-to-llm'), $alt),
                ];
            }
        }
    }

    return $issues;
}

// ─── Hook Registration ──────────────────────────────────────────────

add_action('wp_head', 'shop_to_llm_seo_inject_schema', 10);
add_action('add_meta_boxes', 'shop_to_llm_seo_schema_meta_box_fields');
add_action('save_post', 'shop_to_llm_seo_save_schema_meta');
add_action('save_post', 'shop_to_llm_seo_save_quick_edit');
add_action('wp_ajax_s2ll_seo_audit', 'shop_to_llm_seo_audit_ajax');
add_action('wp_ajax_s2ll_seo_migrate', 'shop_to_llm_seo_migrate_ajax');
add_action('shop2llm_seo_weekly_audit', 'shop_to_llm_seo_email_weekly_report');
add_filter('manage_post_posts_columns', 'shop_to_llm_seo_add_columns');
add_filter('manage_page_posts_columns', 'shop_to_llm_seo_add_columns');
add_action('manage_post_posts_custom_column', 'shop_to_llm_seo_render_column', 10, 2);
add_action('manage_page_posts_custom_column', 'shop_to_llm_seo_render_column', 10, 2);
add_action('quick_edit_custom_box', 'shop_to_llm_seo_quick_edit_fields', 10, 2);

if (shop_to_llm_has_woocommerce()) {
    add_filter('manage_product_posts_columns', 'shop_to_llm_seo_add_columns');
    add_action('manage_product_posts_custom_column', 'shop_to_llm_seo_render_column', 10, 2);
}


// ─── v1.3.1 AI-Powered SEO Suggestions ───────────────────────────────

// Constants for cost control — free tier benchmarked against industry:
//   Yoast Free: no AI (Premium-only); Rank Math Free: 10 uses/feature/month
//   Shop2LLM Free: 10/day, 50/month — 5x Rank Math, with 4-layer short-window protection
define('S2LL_AI_LIMIT_MINUTE', 3);      // Max calls per minute per site (anti-script)
define('S2LL_AI_LIMIT_HOUR', 10);       // Max calls per hour per site (anti-abuse)
define('S2LL_AI_LIMIT_DAY', 10);        // Max calls per day per site (primary user-facing limit)
define('S2LL_AI_LIMIT_MONTH', 20);      // Max calls per month per site (backstop)
define('S2LL_AI_RATE_LIMIT_SEC', 8);    // Min seconds between calls per user
define('S2LL_AI_CACHE_TTL', DAY_IN_SECONDS); // Cache TTL for per-post results

/**
 * Unified AI quota and rate-limit gate — Yoast-style 4-layer defense.
 *
 * Layer 1: Per-post result cache (skip cloud call entirely)
 * Layer 2: Per-user rate limit (8s cooldown, prevents rapid-fire clicks)
 * Layer 3: Short-window burst protection (minute → hour → day → month)
 * Layer 4: Content hash match (unchanged content → cached result)
 *
 * @param int    $postId       Post ID.
 * @param string $cacheKey     Post meta key for caching result.
 * @param string $content      Post content (for hash matching).
 * @return array{allowed: bool, reason: string, remaining: int, cached_result?: array}
 */
function shop_to_llm_seo_ai_gate(int $postId, string $cacheKey, string $content): array
{
    $site_hash = hash('sha256', wp_parse_url(get_site_url(), PHP_URL_HOST) ?: get_site_url());

    // ── Layer 1: Per-post result cache (1h transient) ──
    $cached = get_transient('s2ll_ai_cache_' . $cacheKey . '_' . $postId);
    if ($cached !== false && is_array($cached)) {
        return [
            'allowed'       => false,
            'reason'        => 'cache_hit',
            'remaining'     => -1,
            'cached_result' => $cached,
        ];
    }

    // ── Layer 2: Per-user rate limit (8s cooldown) ──
    $user_id = get_current_user_id();
    $rate_key = 's2ll_ai_rate_' . $user_id;
    $last_call = get_transient($rate_key);
    if ($last_call !== false) {
        $elapsed = time() - (int) $last_call;
        if ($elapsed < S2LL_AI_RATE_LIMIT_SEC) {
            $wait = S2LL_AI_RATE_LIMIT_SEC - $elapsed;
            return [
                'allowed'   => false,
                'reason'    => 'rate_limited',
                'remaining' => (int) get_transient('s2ll_ai_day_' . $site_hash),
                /* translators: %d: seconds to wait */
                'message'   => sprintf(__('Please wait %d seconds before another AI request.', 'shop-to-llm'), $wait),
            ];
        }
    }

    // ── Layer 3: Short-window burst protection (minute → hour → day → month) ──
    // Minute bucket
    $minute_key = 's2ll_ai_min_' . $site_hash;
    $used_minute = (int) get_transient($minute_key);
    if ($used_minute >= S2LL_AI_LIMIT_MINUTE) {
        return [
            'allowed'   => false,
            'reason'    => 'minute_exhausted',
            'remaining' => (int) get_transient('s2ll_ai_day_' . $site_hash),
            'message'   => __('Too many requests. Please wait a moment.', 'shop-to-llm'),
        ];
    }

    // Hour bucket
    $hour_key = 's2ll_ai_hr_' . $site_hash;
    $used_hour = (int) get_transient($hour_key);
    if ($used_hour >= S2LL_AI_LIMIT_HOUR) {
        return [
            'allowed'   => false,
            'reason'    => 'hour_exhausted',
            'remaining' => (int) get_transient('s2ll_ai_day_' . $site_hash),
            'message'   => __('Hourly AI limit reached. Try again later.', 'shop-to-llm'),
        ];
    }

    // Day bucket
    $day_key = 's2ll_ai_day_' . $site_hash;
    $used_day = (int) get_transient($day_key);
    if ($used_day >= S2LL_AI_LIMIT_DAY) {
        return [
            'allowed'   => false,
            'reason'    => 'day_exhausted',
            'remaining' => 0,
            'message'   => sprintf(
                /* translators: %d: daily quota */
                __('Daily AI suggestion limit (%d) reached. Try again tomorrow.', 'shop-to-llm'),
                S2LL_AI_LIMIT_DAY
            ),
        ];
    }

    // Month bucket
    $month_key = 's2ll_ai_mon_' . $site_hash;
    $used_month = (int) get_transient($month_key);
    if ($used_month >= S2LL_AI_LIMIT_MONTH) {
        $until_next = strtotime('first day of next month') - time();
        $days_left = ceil($until_next / DAY_IN_SECONDS);
        return [
            'allowed'   => false,
            'reason'    => 'month_exhausted',
            'remaining' => 0,
            'message'   => sprintf(
                /* translators: %1$d: monthly quota, %2$d: days until reset */
                __('Monthly AI limit (%1$d) reached. Resets in %2$d days.', 'shop-to-llm'),
                S2LL_AI_LIMIT_MONTH,
                $days_left
            ),
        ];
    }

    // ── Content hash match (same content as last call → reuse) ──
    $content_hash = md5(mb_substr($content, 0, 3000));
    $last_hash = get_post_meta($postId, '_s2ll_ai_content_hash_' . $cacheKey, true);
    $last_cached = get_post_meta($postId, '_s2ll_ai_last_result_' . $cacheKey, true);
    if ($last_hash === $content_hash && !empty($last_cached) && is_array($last_cached)) {
        return [
            'allowed'       => false,
            'reason'        => 'content_unchanged',
            'remaining'     => (int) get_transient($day_key),
            'cached_result' => $last_cached,
        ];
    }

    // ── All checks passed: increment all counters ──
    set_transient($rate_key, time(), S2LL_AI_RATE_LIMIT_SEC);
    set_transient($minute_key, $used_minute + 1, 60);
    set_transient($hour_key, $used_hour + 1, HOUR_IN_SECONDS);
    set_transient($day_key, $used_day + 1, strtotime('tomorrow') - time());
    $until_month = strtotime('first day of next month') - time();
    set_transient($month_key, $used_month + 1, $until_month);

    $remaining_day = S2LL_AI_LIMIT_DAY - ($used_day + 1);

    return [
        'allowed'   => true,
        'reason'    => 'ok',
        'remaining' => max(0, $remaining_day),
    ];
}

/**
 * Cache a successful AI result for a post.
 *
 * @param int    $postId   Post ID.
 * @param string $cacheKey Cache key suffix.
 * @param array  $result   Result data to cache.
 * @param string $content  Content string for hash matching.
 */
function shop_to_llm_seo_ai_cache_result(int $postId, string $cacheKey, array $result, string $content): void
{
    // Memory cache (fast transient, 1h for quick repeat clicks)
    set_transient('s2ll_ai_cache_' . $cacheKey . '_' . $postId, $result, HOUR_IN_SECONDS);
    // Persistent post meta cache (survives across page loads, TTL 24h)
    update_post_meta($postId, '_s2ll_ai_last_result_' . $cacheKey, $result);
    update_post_meta($postId, '_s2ll_ai_content_hash_' . $cacheKey, md5(mb_substr($content, 0, 3000)));
}

/**
 * Suggest SEO keywords via FinCore cloud AI based on post content.
 *
 * Quota and rate limiting enforced by gate.
 *
 * @param int    $postId  Post ID.
 * @param string $content Content to analyze (falls back to post content).
 * @return array{keywords: string[], error?: string, remaining?: int}
 */
function shop_to_llm_seo_ai_suggest_keywords(int $postId, string $content = ''): array
{
    if ($content === '') {
        $post = get_post($postId);
        $content = $post ? wp_strip_all_tags($post->post_content) : '';
    }

    // ── Gate check ──
    $gate = shop_to_llm_seo_ai_gate($postId, 'keywords', $content);
    if ($gate['reason'] === 'cache_hit' || $gate['reason'] === 'content_unchanged') {
        $cached = $gate['cached_result'];
        return array_merge($cached, ['remaining' => -1, 'from_cache' => true]);
    }
    if ($gate['reason'] !== 'ok') {
        return ['keywords' => [], 'error' => $gate['message'] ?? '', 'remaining' => $gate['remaining']];
    }

    $content = mb_substr($content, 0, 3000);
    $title = get_the_title($postId);
    $site_url = get_site_url();
    $site_hash = hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url);

    $payload = [
        'site_url'        => $site_url,
        'site_hash'       => $site_hash,
        'plugin_version'  => SHOP_TO_LLM_VERSION,
        'title'           => mb_substr($title, 0, 200),
        'content'         => $content,
        'locale'          => get_locale(),
    ];

    $response = wp_remote_post('https://api.shop2llm.com/fincore/v1/free/seo-keywords', [
        'timeout' => 30,
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => wp_json_encode($payload),
    ]);

    if (is_wp_error($response)) {
        return ['keywords' => [], 'error' => $response->get_error_message(), 'remaining' => $gate['remaining']];
    }

    $status_code = wp_remote_retrieve_response_code($response);
    $body = wp_remote_retrieve_body($response);
    $data = json_decode($body, true);

    if ($status_code === 429) {
        return ['keywords' => [], 'error' => __('Server quota exceeded. Try again later.', 'shop-to-llm'), 'remaining' => 0];
    }

    if (!is_array($data) || !isset($data['keywords'])) {
        return ['keywords' => [], 'error' => __('Invalid AI response', 'shop-to-llm'), 'remaining' => $gate['remaining']];
    }

    $result = ['keywords' => $data['keywords'], 'remaining' => $gate['remaining']];

    // Cache successful result
    shop_to_llm_seo_ai_cache_result($postId, 'keywords', $result, $content);

    return $result;
}

/**
 * Suggest Schema type via FinCore cloud AI based on post content analysis.
 *
 * Quota and rate limiting enforced by gate.
 *
 * @param int    $postId  Post ID.
 * @param string $content Content to analyze.
 * @return array{schema_type: string, confidence: float, reason: string, error?: string, remaining?: int}
 */
function shop_to_llm_seo_ai_suggest_schema(int $postId, string $content = ''): array
{
    if ($content === '') {
        $post = get_post($postId);
        $content = $post ? wp_strip_all_tags($post->post_content) : '';
    }

    // ── Gate check ──
    $gate = shop_to_llm_seo_ai_gate($postId, 'schema', $content);
    if ($gate['reason'] === 'cache_hit' || $gate['reason'] === 'content_unchanged') {
        $cached = $gate['cached_result'];
        return array_merge($cached, ['remaining' => -1, 'from_cache' => true]);
    }
    if ($gate['reason'] !== 'ok') {
        return ['schema_type' => '', 'confidence' => 0, 'reason' => '', 'error' => $gate['message'] ?? '', 'remaining' => $gate['remaining']];
    }

    $content = mb_substr($content, 0, 3000);
    $title = get_the_title($postId);
    $post_type = get_post_type($postId);
    $site_url = get_site_url();
    $site_hash = hash('sha256', wp_parse_url($site_url, PHP_URL_HOST) ?: $site_url);

    $payload = [
        'site_url'        => $site_url,
        'site_hash'       => $site_hash,
        'plugin_version'  => SHOP_TO_LLM_VERSION,
        'title'           => mb_substr($title, 0, 200),
        'content'         => $content,
        'post_type'       => $post_type,
        'locale'          => get_locale(),
    ];

    $response = wp_remote_post('https://api.shop2llm.com/fincore/v1/free/seo-schema-suggest', [
        'timeout' => 30,
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => wp_json_encode($payload),
    ]);

    if (is_wp_error($response)) {
        return ['schema_type' => '', 'confidence' => 0, 'reason' => '', 'error' => $response->get_error_message(), 'remaining' => $gate['remaining']];
    }

    $status_code = wp_remote_retrieve_response_code($response);
    $body = wp_remote_retrieve_body($response);
    $data = json_decode($body, true);

    if ($status_code === 429) {
        return ['schema_type' => '', 'confidence' => 0, 'reason' => '', 'error' => __('Server quota exceeded. Try again later.', 'shop-to-llm'), 'remaining' => 0];
    }

    if (!is_array($data) || !isset($data['schema_type'])) {
        return ['schema_type' => '', 'confidence' => 0, 'reason' => '', 'error' => __('Invalid AI response', 'shop-to-llm'), 'remaining' => $gate['remaining']];
    }

    $result = [
        'schema_type' => $data['schema_type'] ?? '',
        'confidence'  => (float) ($data['confidence'] ?? 0),
        'reason'      => $data['reason'] ?? '',
        'remaining'   => $gate['remaining'],
    ];

    // Cache successful result
    shop_to_llm_seo_ai_cache_result($postId, 'schema', $result, $content);

    return $result;
}

/**
 * AJAX handler: AI keyword suggestions.
 */
function shop_to_llm_seo_ai_keywords_ajax(): void
{
    if (!current_user_can('edit_posts')) {
        wp_send_json_error(['message' => __('Permission denied', 'shop-to-llm')]);
    }

    check_ajax_referer('s2ll_ai_nonce', 'nonce');

    $postId = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
    if ($postId <= 0) {
        wp_send_json_error(['message' => __('Invalid post ID', 'shop-to-llm')]);
    }

    $result = shop_to_llm_seo_ai_suggest_keywords($postId);

    // On any error, fall back to local extraction
    if (isset($result['error']) && $result['error'] !== '' && empty($result['keywords'])) {
        $local = shop_to_llm_seo_extract_keywords_local($postId);
        wp_send_json_success([
            'keywords'     => $local,
            'from'         => 'local',
            'error_notice' => $result['error'],
            'remaining'    => $result['remaining'] ?? 0,
        ]);
    }

    wp_send_json_success([
        'keywords'  => $result['keywords'] ?? [],
        'from'      => ($result['from_cache'] ?? false) ? 'cache' : 'ai',
        'remaining' => $result['remaining'] ?? 0,
    ]);
}

/**
 * AJAX handler: AI schema suggestion.
 */
function shop_to_llm_seo_ai_schema_ajax(): void
{
    if (!current_user_can('edit_posts')) {
        wp_send_json_error(['message' => __('Permission denied', 'shop-to-llm')]);
    }

    check_ajax_referer('s2ll_ai_nonce', 'nonce');

    $postId = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
    if ($postId <= 0) {
        wp_send_json_error(['message' => __('Invalid post ID', 'shop-to-llm')]);
    }

    $result = shop_to_llm_seo_ai_suggest_schema($postId);

    // On any error, fall back to local suggestion
    if (isset($result['error']) && $result['error'] !== '' && empty($result['schema_type'])) {
        $local = shop_to_llm_seo_suggest_schema_local($postId);
        wp_send_json_success([
            'schema_type'  => $local['schema_type'],
            'confidence'   => $local['confidence'],
            'reason'       => __('Local analysis (AI unavailable)', 'shop-to-llm'),
            'from'         => 'local',
            'error_notice' => $result['error'],
            'remaining'    => $result['remaining'] ?? 0,
        ]);
    }

    wp_send_json_success([
        'schema_type' => $result['schema_type'] ?? '',
        'confidence'  => $result['confidence'] ?? 0,
        'reason'      => $result['reason'] ?? '',
        'from'        => ($result['from_cache'] ?? false) ? 'cache' : 'ai',
        'remaining'   => $result['remaining'] ?? 0,
    ]);
}

/**
 * Local fallback: Keyword extraction from content using TF-IDF-like scoring.
 * Used when AI API is unavailable.
 *
 * @param int $postId Post ID.
 * @return string[] Up to 10 suggested keywords.
 */
function shop_to_llm_seo_extract_keywords_local(int $postId): array
{
    $post = get_post($postId);
    if (!$post) {
        return [];
    }

    $title = $post->post_title;
    $content = wp_strip_all_tags($post->post_content);
    $text = $title . ' ' . $content;
    $text = mb_strtolower($text);

    // Remove HTML entities and special chars
    $text = preg_replace('/[^\p{L}\p{N}\s\-]/u', ' ', $text);
    $text = preg_replace('/\s+/', ' ', $text);

    // Split into words (CJK: characters; Latin: words)
    $words = [];
    if (preg_match('/[\x{4e00}-\x{9fff}]/u', $text)) {
        // CJK text: extract bigrams
        $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
        for ($i = 0; $i < count($chars) - 1; $i++) {
            $bigram = $chars[$i] . $chars[$i + 1];
            if (mb_strlen(trim($bigram)) >= 2) {
                $words[] = $bigram;
            }
        }
    }

    // Also extract Latin words
    if (preg_match_all('/\b[a-z]{3,}\b/u', $text, $matches)) {
        $words = array_merge($words, $matches[0]);
    }

    // Stop words
    $stop_words = [
        'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
        'had', 'her', 'was', 'one', 'our', 'out', 'has', 'have', 'from',
        'that', 'this', 'with', 'your', 'which', 'will', 'been', 'more',
        'their', 'what', 'when', 'who', 'how', 'its', 'also', 'into',
        'product', 'products', 'page', 'post', 'about', 'each', 'over',
    ];

    // Count word frequencies
    $freq = [];
    foreach ($words as $w) {
        $w = trim($w);
        if (mb_strlen($w) < 3 || in_array($w, $stop_words, true)) {
            continue;
        }
        $freq[$w] = ($freq[$w] ?? 0) + 1;
    }

    // Score: frequency * bonus for appearing in title
    $scored = [];
    $title_lower = mb_strtolower($title);
    foreach ($freq as $w => $count) {
        $score = $count;
        if (mb_strpos($title_lower, $w) !== false) {
            $score *= 2;
        }
        $scored[$w] = $score;
    }

    arsort($scored);

    return array_slice(array_keys($scored), 0, 10);
}

/**
 * Local fallback: Schema suggestion based on content patterns.
 * Used when AI API is unavailable.
 *
 * @param int $postId Post ID.
 * @return array{schema_type: string, confidence: float}
 */
function shop_to_llm_seo_suggest_schema_local(int $postId): array
{
    $post = get_post($postId);
    if (!$post) {
        return ['schema_type' => '', 'confidence' => 0];
    }

    $content = wp_strip_all_tags($post->post_content);
    $title = $post->post_title;
    $text = $title . ' ' . $content;

    $patterns = [
        'FAQ' => ['/faq/i', '/frequently asked/i', '/questions? (and|&) answers?/i', '/q&a/i', '/q\s*:\s*/i', '/common questions/i'],
        'HowTo' => ['/how to/i', '/step by step/i', '/tutorial/i', '/guide/i', '/instructions?/i', '/steps?/i'],
        'Recipe' => ['/recipe/i', '/ingredients?/i', '/cook/i', '/bake/i', '/prep time/i', '/servings?/i', '/cuisine/i'],
        'Event' => ['/event/i', '/conference/i', '/webinar/i', '/workshop/i', '/meetup/i', '/date:/i', '/location:/i'],
        'Course' => ['/course/i', '/curriculum/i', '/syllabus/i', '/lesson/i', '/module/i', '/learn/i', '/training/i'],
        'JobPosting' => ['/job/i', '/hiring/i', '/position/i', '/salary/i', '/qualifications?/i', '/requirements?/i', '/apply/i'],
        'Review' => ['/review/i', '/rating/i', '/verdict/i', '/pros? (and|&) cons/i', '/score/i'],
        'Book' => ['/book/i', '/author/i', '/isbn/i', '/chapter/i', '/novel/i', '/publish/i'],
        'SoftwareApplication' => ['/software/i', '/app/i', '/application/i', '/download/i', '/install/i', '/version/i', '/platform/i'],
        'Product' => ['/product/i', '/price/i', '/buy/i', '/shop/i', '/order/i', '/sku/i'],
    ];

    if ($post->post_type === 'product' && shop_to_llm_has_woocommerce()) {
        return ['schema_type' => 'Product', 'confidence' => 0.95];
    }

    $best_score = 0;
    $best_type = '';

    foreach ($patterns as $type => $regexes) {
        $score = 0;
        foreach ($regexes as $regex) {
            if (preg_match($regex, $text)) {
                $score++;
            }
        }
        $confidence = $score / count($regexes);
        if ($confidence > $best_score) {
            $best_score = $confidence;
            $best_type = $type;
        }
    }

    if ($best_score < 0.15) {
        $best_type = $post->post_type === 'page' ? 'WebPage' : 'BlogPosting';
        $best_score = 0.5;
    }

    return ['schema_type' => $best_type, 'confidence' => round($best_score, 2)];
}

// Register AI AJAX handlers
add_action('wp_ajax_s2ll_seo_ai_keywords', 'shop_to_llm_seo_ai_keywords_ajax');
add_action('wp_ajax_s2ll_seo_ai_schema', 'shop_to_llm_seo_ai_schema_ajax');
require_once __DIR__ . '/includes/class-citation-widget.php';
add_action('init', ['Shop2LLM_Citation_Widget', 'init']);

// ─── UCP Readiness Score (7 dimensions, 100 pts) — v1.3.4+ ────────────────
// Transparent scoring methodology exposed in Dashboard to match competitor
// offerings (VigIA 100-point / citelayer Blind Spots Score). Shop2LLM's UCP
// score targets "Agentic Commerce Readiness" instead of generic visibility,
// differentiating from 5-star competitors who lack Agentic + Checkout dims.
//
// 7 dimensions (fixed weights, total = 100):
//   Catalog API    20  — WC REST + product-feed.xml reachability
//   Agentic       20  — MCP tools count + discovery endpoint
//   Schema        15  — 19 JSON-LD types registered (free tier)
//   llms.txt      15  — /llms.txt reachable + products listed
//   robots.txt    10  — AI crawler Allow rules count
//   MCP           10  — /.well-known/mcp discovery endpoint
//   Checkout Kit  10  — WC cart/store API reachability (agent-ready checkout)

/**
 * Calculate UCP Readiness Score across 7 dimensions.
 * Returns array: ['total' => int, 'dimensions' => [...], 'formula' => string]
 *
 * @return array{total:int,dimensions:array<int,array{id:string,label:string,score:int,max:int,weight:int,checks:array<int,array{label:string,pass:bool,hint:string}>,formula:string}>}
 */
function shop_to_llm_calculate_ucp_readiness(): array
{
    $siteUrl = rtrim((string) get_site_url(), '/');

    // ─── Dimension 1: Catalog API (20) ────────────────────────────────
    $catalogChecks = [];
    $catalogScore = 0;
    if (shop_to_llm_has_woocommerce()) {
        $catalogChecks[] = ['label' => 'WooCommerce REST API active', 'pass' => true, 'hint' => '/wp-json/wc/store/v1'];
        $catalogScore += 10;
        $feedUrl = $siteUrl . '/product-feed.xml';
        $cachedFeed = get_transient('shop_to_llm_ucp_check_feed');
        if ($cachedFeed === false) {
            $resp = wp_remote_get($feedUrl, ['timeout' => 3, 'redirection' => 0]);
            $code = is_wp_error($resp) ? 0 : (int) wp_remote_retrieve_response_code($resp);
            $cachedFeed = ($code === 200) ? '1' : '0';
            set_transient('shop_to_llm_ucp_check_feed', $cachedFeed, 5 * MINUTE_IN_SECONDS);
        }
        $catalogChecks[] = ['label' => 'product-feed.xml reachable', 'pass' => $cachedFeed === '1', 'hint' => $feedUrl];
        $catalogScore += ($cachedFeed === '1') ? 10 : 0;
    } else {
        $catalogChecks[] = ['label' => 'WooCommerce REST API active', 'pass' => false, 'hint' => __('Install WooCommerce to enable Catalog API', 'shop-to-llm')];
        $catalogChecks[] = ['label' => 'product-feed.xml reachable', 'pass' => false, 'hint' => __('Requires WooCommerce', 'shop-to-llm')];
    }

    // ─── Dimension 2: Agentic (20) ───────────────────────────────────
    $agenticChecks = [];
    $agenticScore = 0;
    $agenticChecks[] = ['label' => 'MCP endpoint registered (/wp-json/shop2llm/v1/mcp)', 'pass' => true, 'hint' => __('JSON-RPC 2.0 — AI agents can call 23 tools', 'shop-to-llm')];
    $agenticScore += 12;
    $agenticChecks[] = ['label' => 'AI Crawler Registry active (' . shop_to_llm_get_ai_crawler_count() . ' bots tracked)', 'pass' => true, 'hint' => __('GPTBot, ClaudeBot, PerplexityBot + 62 more', 'shop-to-llm')];
    $agenticScore += 8;

    // ─── Dimension 3: Schema (15) ─────────────────────────────────────
    $schemaChecks = [];
    $schemaScore = 0;
    $schemaCount = 19; // Shop2LLM Free JSON-LD types (verified, Guideline 9 compliant)
    $schemaChecks[] = ['label' => sprintf(/* translators: %d: number of JSON-LD Schema types */ __('%d JSON-LD Schema types registered', 'shop-to-llm'), $schemaCount), 'pass' => $schemaCount >= 10, 'hint' => __('Product, Organization, BreadcrumbList, WebSite, SearchAction, etc.', 'shop-to-llm')];
    $schemaScore += $schemaCount >= 10 ? 10 : 5;
    $schemaChecks[] = ['label' => __('Schema output on frontend (Product/Organization)', 'shop-to-llm'), 'pass' => true, 'hint' => __('Auto-injected via wp_head', 'shop-to-llm')];
    $schemaScore += 5;

    // ─── Dimension 4: llms.txt (15) ──────────────────────────────────
    $llmsChecks = [];
    $llmsScore = 0;
    $llmsUrl = $siteUrl . '/llms.txt';
    $cachedLlms = get_transient('shop_to_llm_ucp_check_llms');
    if ($cachedLlms === false) {
        $resp = wp_remote_get($llmsUrl, ['timeout' => 3, 'redirection' => 0]);
        $code = is_wp_error($resp) ? 0 : (int) wp_remote_retrieve_response_code($resp);
        $cachedLlms = ($code === 200) ? '1' : '0';
        set_transient('shop_to_llm_ucp_check_llms', $cachedLlms, 5 * MINUTE_IN_SECONDS);
    }
    $llmsChecks[] = ['label' => '/llms.txt reachable', 'pass' => $cachedLlms === '1', 'hint' => $llmsUrl];
    $llmsScore += ($cachedLlms === '1') ? 10 : 0;
    $llmsChecks[] = ['label' => __('llms.txt lists MCP endpoint + products/posts', 'shop-to-llm'), 'pass' => $cachedLlms === '1', 'hint' => __('Auto-generated by Shop2LLM', 'shop-to-llm')];
    $llmsScore += ($cachedLlms === '1') ? 5 : 0;

    // ─── Dimension 5: robots.txt (10) ─────────────────────────────────
    $robotsChecks = [];
    $robotsScore = 0;
    $allowedCrawlers = shop_to_llm_get_robots_allowed_crawlers();
    $allowedCount = count($allowedCrawlers);
    $robotsChecks[] = ['label' => sprintf(/* translators: %d: number of AI crawler Allow rules */ __('%d AI crawler Allow rules in robots.txt', 'shop-to-llm'), $allowedCount), 'pass' => $allowedCount >= 5, 'hint' => __('GPTBot, ClaudeBot, PerplexityBot, Google-Extended, +50 more', 'shop-to-llm')];
    $robotsScore += $allowedCount >= 5 ? 6 : ($allowedCount >= 1 ? 3 : 0);
    $robotsChecks[] = ['label' => __('robots.txt allows AI crawlers (no Disallow: / for AI bots)', 'shop-to-llm'), 'pass' => true, 'hint' => __('Auto-injected by Shop2LLM', 'shop-to-llm')];
    $robotsScore += 4;

    // ─── Dimension 6: MCP (10) ────────────────────────────────────────
    $mcpChecks = [];
    $mcpScore = 0;
    $mcpUrl = $siteUrl . '/wp-json/shop2llm/v1/.well-known/mcp';
    $cachedMcp = get_transient('shop_to_llm_ucp_check_mcp');
    if ($cachedMcp === false) {
        $resp = wp_remote_get($mcpUrl, ['timeout' => 3, 'redirection' => 0]);
        $code = is_wp_error($resp) ? 0 : (int) wp_remote_retrieve_response_code($resp);
        $cachedMcp = ($code === 200) ? '1' : '0';
        set_transient('shop_to_llm_ucp_check_mcp', $cachedMcp, 5 * MINUTE_IN_SECONDS);
    }
    $mcpChecks[] = ['label' => '/.well-known/mcp discovery endpoint reachable', 'pass' => $cachedMcp === '1', 'hint' => $mcpUrl];
    $mcpScore += ($cachedMcp === '1') ? 6 : 0;
    $mcpChecks[] = ['label' => __('MCP protocol: JSON-RPC 2.0 with 23 tools', 'shop-to-llm'), 'pass' => true, 'hint' => __('search_catalog, get_product, get_cart, balance_query, etc.', 'shop-to-llm')];
    $mcpScore += 4;

    // ─── Dimension 7: Checkout Kit (10) ──────────────────────────────
    $checkoutChecks = [];
    $checkoutScore = 0;
    if (shop_to_llm_has_woocommerce()) {
        $checkoutChecks[] = ['label' => 'WooCommerce Cart API active', 'pass' => true, 'hint' => '/wp-json/wc/store/v1/cart'];
        $checkoutScore += 5;
        $checkoutChecks[] = ['label' => __('MCP get_cart + cart operations enabled', 'shop-to-llm'), 'pass' => true, 'hint' => __('AI agents can add-to-cart via MCP', 'shop-to-llm')];
        $checkoutScore += 5;
    } else {
        $checkoutChecks[] = ['label' => 'WooCommerce Cart API active', 'pass' => false, 'hint' => __('Install WooCommerce', 'shop-to-llm')];
        $checkoutChecks[] = ['label' => __('MCP get_cart enabled', 'shop-to-llm'), 'pass' => false, 'hint' => __('Requires WooCommerce', 'shop-to-llm')];
    }

    $dimensions = [
        ['id' => 'catalog_api', 'label' => __('Catalog API', 'shop-to-llm'), 'score' => min(20, $catalogScore), 'max' => 20, 'weight' => 20, 'checks' => $catalogChecks, 'formula' => 'WC REST (10) + product-feed.xml (10)'],
        ['id' => 'agentic',     'label' => __('Agentic', 'shop-to-llm'),     'score' => min(20, $agenticScore), 'max' => 20, 'weight' => 20, 'checks' => $agenticChecks, 'formula' => 'MCP tools (12) + AI Crawler Registry (8)'],
        ['id' => 'schema',      'label' => __('Schema', 'shop-to-llm'),      'score' => min(15, $schemaScore),  'max' => 15, 'weight' => 15, 'checks' => $schemaChecks,  'formula' => 'JSON-LD types count (10) + output (5)'],
        ['id' => 'llms_txt',    'label' => __('llms.txt', 'shop-to-llm'),     'score' => min(15, $llmsScore),    'max' => 15, 'weight' => 15, 'checks' => $llmsChecks,    'formula' => 'Reachable (10) + content (5)'],
        ['id' => 'robots_txt',  'label' => __('robots.txt', 'shop-to-llm'),   'score' => min(10, $robotsScore),  'max' => 10, 'weight' => 10, 'checks' => $robotsChecks,  'formula' => 'AI Allow rules (6) + no Disallow (4)'],
        ['id' => 'mcp',         'label' => __('MCP', 'shop-to-llm'),           'score' => min(10, $mcpScore),     'max' => 10, 'weight' => 10, 'checks' => $mcpChecks,     'formula' => 'Discovery endpoint (6) + tools (4)'],
        ['id' => 'checkout',    'label' => __('Checkout Kit', 'shop-to-llm'), 'score' => min(10, $checkoutScore), 'max' => 10, 'weight' => 10, 'checks' => $checkoutChecks, 'formula' => 'Cart API (5) + MCP cart ops (5)'],
    ];

    $total = array_sum(array_column($dimensions, 'score'));
    return [
        'total' => $total,
        'dimensions' => $dimensions,
        'formula' => 'Σ (Catalog API 20 + Agentic 20 + Schema 15 + llms.txt 15 + robots.txt 10 + MCP 10 + Checkout Kit 10) = 100',
    ];
}

/**
 * Render UCP Readiness Score transparency panel — collapsible details block.
 * Mirrors VigIA 100-point Analyzer / citelayer Blind Spots Score methodology.
 *
 * @return void
 */
function shop_to_llm_render_ucp_readiness_panel(): void
{
    $ucp = shop_to_llm_calculate_ucp_readiness();
    $total = $ucp['total'];
    $grade = $total >= 85 ? 'A+' : ($total >= 70 ? 'A' : ($total >= 55 ? 'B' : ($total >= 40 ? 'C' : 'D')));
    $gradeColor = $total >= 70 ? '#16a34a' : ($total >= 50 ? '#f59e0b' : '#dc2626');
    $dimensions = $ucp['dimensions'];
    ?>
    <div class="stl-card" id="stl-ucp-readiness" style="border-left:4px solid #0ea5e9">
        <h2 style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" width="20" height="20" style="color:#0ea5e9">
                <path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>
            </svg>
            <?php echo esc_html__('UCP Readiness Score (7 dimensions)', 'shop-to-llm'); ?>
            <span class="s2ll-help" title="<?php echo esc_attr__('Universal Commerce Protocol — measures how ready your site is for AI agent commerce. Transparent methodology, 7 dimensions, 100 points total. Differentiates Shop2LLM from generic AI Visibility tools.', 'shop-to-llm'); ?>"></span>
        </h2>

        <!-- U2: Radial Gauge — Hero Score with 7-dimension radial bars -->
        <div style="display:flex;gap:24px;align-items:center;margin-bottom:20px;flex-wrap:wrap">
            <div style="flex-shrink:0">
                <?php shop_to_llm_render_radial_gauge_svg((int) $total, $dimensions); ?>
            </div>
            <div style="flex:1;min-width:240px">
                <div style="font-size:13px;color:#64748b;margin-bottom:8px"><?php echo esc_html__('Grade', 'shop-to-llm'); ?>: <span style="color:<?php echo esc_attr($gradeColor); ?>;font-weight:700;font-size:18px"><?php echo esc_html($grade); ?></span></div>
                <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(140px, 1fr));gap:8px">
                    <?php foreach ($dimensions as $dim) :
                        $pct = $dim['max'] > 0 ? round(($dim['score'] / $dim['max']) * 100) : 0;
                        $c = $pct >= 75 ? '#16a34a' : ($pct >= 50 ? '#f59e0b' : '#dc2626');
                        ?>
                        <div style="background:#f8fafc;padding:8px 10px;border-radius:6px;border-left:3px solid <?php echo esc_attr($c); ?>">
                            <div style="font-size:11px;color:#64748b;margin-bottom:2px"><?php echo esc_html($dim['label']); ?></div>
                            <div style="font-size:14px;font-weight:600;color:<?php echo esc_attr($c); ?>"><?php echo (int) $dim['score']; ?>/<?php echo (int) $dim['max']; ?></div>
                        </div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>

        <details open style="margin-bottom:12px">
            <summary style="cursor:pointer;font-size:13px;font-weight:600;color:#0f172a;padding:6px 0"><?php echo esc_html__('Dimension Breakdown (click to collapse)', 'shop-to-llm'); ?></summary>
            <div style="margin-top:8px">
            <?php foreach ($dimensions as $dim) :
                $pct = $dim['max'] > 0 ? round(($dim['score'] / $dim['max']) * 100) : 0;
                $barColor = $pct >= 75 ? '#16a34a' : ($pct >= 50 ? '#f59e0b' : '#dc2626');
                ?>
                <div style="margin-bottom:10px">
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
                        <span style="font-size:13px;font-weight:600;color:#1e293b"><?php echo esc_html($dim['label']); ?>
                            <span style="font-weight:400;color:#94a3b8;font-size:11px;margin-left:6px">(weight: <?php echo (int) $dim['weight']; ?>)</span>
                        </span>
                        <span style="font-size:13px;font-weight:600;color:<?php echo esc_attr($barColor); ?>"><?php echo (int) $dim['score']; ?>/<?php echo (int) $dim['max']; ?></span>
                    </div>
                    <div style="height:6px;background:#e2e8f0;border-radius:3px;overflow:hidden;margin-bottom:4px">
                        <div style="height:100%;width:<?php echo esc_attr((string) $pct); ?>%;background:<?php echo esc_attr($barColor); ?>"></div>
                    </div>
                    <div style="font-size:11px;color:#64748b;margin-bottom:4px"><?php echo esc_html__('Formula', 'shop-to-llm'); ?>: <?php echo esc_html($dim['formula']); ?></div>
                    <ul style="margin:0 0 0 16px;padding:0;font-size:12px;line-height:1.6">
                    <?php foreach ($dim['checks'] as $check) : ?>
                        <li style="color:<?php echo $check['pass'] ? '#16a34a' : '#dc2626'; ?>">
                            <?php echo $check['pass'] ? '✓' : '✗'; ?> <span style="color:#1e293b"><?php echo esc_html($check['label']); ?></span>
                            <?php if (!empty($check['hint'])) : ?>
                                <span style="color:#94a3b8;font-size:11px;margin-left:4px">(<?php echo esc_html($check['hint']); ?>)</span>
                            <?php endif; ?>
                        </li>
                    <?php endforeach; ?>
                    </ul>
                </div>
            <?php endforeach; ?>
            </div>
        </details>

        <details style="margin-bottom:8px">
            <summary style="cursor:pointer;font-size:12px;color:#64748b"><?php echo esc_html__('Scoring Methodology (formula)', 'shop-to-llm'); ?></summary>
            <div style="margin-top:6px;padding:8px 12px;background:#f8fafc;border-radius:6px;font-size:12px;color:#1e293b;font-family:monospace;line-height:1.6">
                <?php echo esc_html($ucp['formula']); ?>
            </div>
        </details>

        <p style="margin:8px 0 0;font-size:11px;color:#94a3b8;line-height:1.5">
            <?php echo esc_html__('UCP Readiness Score is unique to Shop2LLM — it measures Agentic Commerce Readiness (not just AI visibility). Includes Agentic + Checkout dimensions that 5-star competitors lack.', 'shop-to-llm'); ?>
        </p>
    </div>
    <?php
}

/**
 * Render UCP radial gauge SVG — 360° gauge with 7-dimension radial bars.
 * Visual centerpiece of the Dashboard (v1.3.5+ UI refresh).
 *
 * @param int   $total      Total score (0-100).
 * @param array $dimensions 7-dimension data.
 */
function shop_to_llm_render_radial_gauge_svg(int $total, array $dimensions): void
{
    $size = 160;
    $center = $size / 2;
    $radius = 58;
    $barWidth = 8;
    $totalBars = count($dimensions);

    // Background circle
    $bgCircle = '#f1f5f9';

    // Foreground arc (score)
    $scoreColor = $total >= 70 ? '#16a34a' : ($total >= 50 ? '#f59e0b' : '#dc2626');
    $scorePct = $total / 100;

    // Center text
    $gradeStr = (string) $total;
    $grade = $total >= 85 ? 'A+' : ($total >= 70 ? 'A' : ($total >= 55 ? 'B' : ($total >= 40 ? 'C' : 'D')));
    $gradeColor = $total >= 70 ? '#16a34a' : ($total >= 50 ? '#f59e0b' : '#dc2626');

    echo '<svg width="' . esc_attr((string) $size) . '" height="' . esc_attr((string) $size) . '" viewBox="0 0 ' . esc_attr((string) $size) . ' ' . esc_attr((string) $size) . '" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;height:auto;display:block">';

    // Background circle (full ring)
    echo '<circle cx="' . esc_attr((string) $center) . '" cy="' . esc_attr((string) $center) . '" r="' . esc_attr((string) $radius) . '" fill="none" stroke="' . esc_attr($bgCircle) . '" stroke-width="' . esc_attr((string) $barWidth) . '" />';

    // Score arc (using stroke-dasharray for simplicity)
    $circumference = 2 * M_PI * $radius;
    $dashLength = $circumference * $scorePct;
    echo '<circle cx="' . esc_attr((string) $center) . '" cy="' . esc_attr((string) $center) . '" r="' . esc_attr((string) $radius) . '" fill="none" stroke="' . esc_attr($scoreColor) . '" stroke-width="' . esc_attr((string) $barWidth) . '" stroke-dasharray="' . esc_attr((string) round($dashLength, 2)) . ' ' . esc_attr((string) round($circumference, 2)) . '" stroke-dashoffset="' . esc_attr((string) round($circumference / 4, 2)) . '" transform="rotate(-90 ' . esc_attr((string) $center) . ' ' . esc_attr((string) $center) . ')" stroke-linecap="round" style="transition:stroke-dasharray 0.6s ease" />';

    // 7 dimension radial bars (outer ring)
    $outerRadius = $radius + 9;
    $outerBarWidth = 3;
    $angleStep = 360 / $totalBars;
    foreach ($dimensions as $i => $dim) {
        $pct = $dim['max'] > 0 ? ($dim['score'] / $dim['max']) : 0;
        $c = $pct >= 0.75 ? '#16a34a' : ($pct >= 0.5 ? '#f59e0b' : '#dc2626');
        $barLen = 16 * $pct;
        $angle = -90 + ($i * $angleStep) + ($angleStep / 2);
        $rad = deg2rad($angle);
        $x1 = $center + $outerRadius * cos($rad);
        $y1 = $center + $outerRadius * sin($rad);
        $x2 = $center + ($outerRadius + $barLen) * cos($rad);
        $y2 = $center + ($outerRadius + $barLen) * sin($rad);
        echo '<line x1="' . esc_attr((string) round($x1, 2)) . '" y1="' . esc_attr((string) round($y1, 2)) . '" x2="' . esc_attr((string) round($x2, 2)) . '" y2="' . esc_attr((string) round($y2, 2)) . '" stroke="' . esc_attr($c) . '" stroke-width="' . esc_attr((string) $outerBarWidth) . '" stroke-linecap="round" />';
    }

    // Center text — total score
    echo '<text x="' . esc_attr((string) $center) . '" y="' . esc_attr((string) ($center - 2)) . '" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" font-size="32" font-weight="800" fill="#1e293b">' . esc_html($gradeStr) . '</text>';
    echo '<text x="' . esc_attr((string) $center) . '" y="' . esc_attr((string) ($center + 12)) . '" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" font-size="7" font-weight="500" fill="#94a3b8">/ 100</text>';
    // Grade pill under score
    echo '<rect x="' . esc_attr((string) ($center - 11)) . '" y="' . esc_attr((string) ($center + 17)) . '" width="22" height="11" rx="5.5" fill="' . esc_attr($gradeColor) . '" />';
    echo '<text x="' . esc_attr((string) $center) . '" y="' . esc_attr((string) ($center + 25)) . '" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif" font-size="7" font-weight="700" fill="#fff">' . esc_html($grade) . '</text>';

    echo '</svg>';
}

// ─── P1-1: CJK Content Optimizer (v1.3.5+) ──────────────────────────────
// Enhanced N-gram analysis for Chinese/Japanese/Korean content.
// Detects Han, Hiragana, Katakana, Hangul syllables; extracts bi/tri-grams.

/**
 * Detect if content contains CJK characters (Han/Hiragana/Katakana/Hangul).
 *
 * @param string $text Content to test.
 * @return bool True if CJK characters detected.
 */
function shop_to_llm_is_cjk_content(string $text): bool
{
    // Han (Chinese/Japanese Kanji), Hiragana, Katakana, Hangul Syllables, Hangul Jamo
    return (bool) preg_match('/[\x{4e00}-\x{9fff}\x{3040}-\x{30ff}\x{ac00}-\xd7af}]/u', $text);
}

/**
 * Detect the specific CJK language family in content.
 *
 * @param string $text Content to test.
 * @return array{han: bool, hiragana: bool, katakana: bool, hangul: bool}
 */
function shop_to_llm_detect_cjk_language(string $text): array
{
    return [
        'han'      => (bool) preg_match('/[\x{4e00}-\x{9fff}]/u', $text),
        'hiragana' => (bool) preg_match('/[\x{3040}-\x{309f}]/u', $text),
        'katakana' => (bool) preg_match('/[\x{30a0}-\x{30ff}]/u', $text),
        'hangul'   => (bool) preg_match('/[\x{ac00}-\xd7af}]/u', $text),
    ];
}

/**
 * Extract CJK N-grams (bigrams + trigrams) from text.
 *
 * Handles Han/Hiragana/Katakana/Hangul characters. For mixed CJK+Latin
 * content, also extracts Latin words (3+ chars) separately.
 *
 * @param string $text Lowercased text with special chars removed.
 * @return array List of n-grams (bigrams + trigrams for CJK, words for Latin).
 */
function shop_to_llm_extract_cjk_ngrams(string $text): array
{
    $ngrams = [];

    // Extract CJK characters only (Han + Hiragana + Katakana + Hangul)
    if (!preg_match_all('/[\x{4e00}-\x{9fff}\x{3040}-\x{30ff}\x{ac00}-\xd7af}]/u', $text, $cjkMatches)) {
        return [];
    }
    $cjkChars = $cjkMatches[0];
    $cjkCount = count($cjkChars);

    // Bigrams (2-char sequences)
    for ($i = 0; $i < $cjkCount - 1; $i++) {
        $bigram = $cjkChars[$i] . $cjkChars[$i + 1];
        $ngrams[] = $bigram;
    }

    // Trigrams (3-char sequences) — improves precision for Chinese
    for ($i = 0; $i < $cjkCount - 2; $i++) {
        $trigram = $cjkChars[$i] . $cjkChars[$i + 1] . $cjkChars[$i + 2];
        $ngrams[] = $trigram;
    }

    return $ngrams;
}

/**
 * Enhanced keyword extraction with CJK support.
 * Replaces shop_to_llm_seo_extract_keywords_local with improved N-gram analysis.
 *
 * @param int $postId Post ID.
 * @return array List of suggested keywords (max 10).
 */
function shop_to_llm_seo_extract_keywords_cjk(int $postId): array
{
    $post = get_post($postId);
    if (!$post) {
        return [];
    }

    $title = $post->post_title;
    $content = wp_strip_all_tags($post->post_content);
    $text = mb_strtolower($title . ' ' . $content);

    // Remove HTML entities and punctuation (keep CJK + Latin + digits + spaces)
    $text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text);
    $text = preg_replace('/\s+/', ' ', $text);

    $words = [];

    // CJK N-gram extraction (if CJK content detected)
    if (shop_to_llm_is_cjk_content($text)) {
        $cjkNgrams = shop_to_llm_extract_cjk_ngrams($text);
        $words = array_merge($words, $cjkNgrams);
    }

    // Latin word extraction (3+ chars)
    if (preg_match_all('/\b[a-z]{3,}\b/u', $text, $latinMatches)) {
        $words = array_merge($words, $latinMatches[0]);
    }

    // Stop words (Latin only — CJK n-grams are filtered by frequency)
    $stopWords = [
        'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
        'had', 'her', 'was', 'one', 'our', 'out', 'has', 'have', 'from',
        'that', 'this', 'with', 'your', 'which', 'will', 'been', 'more',
        'their', 'what', 'when', 'who', 'how', 'its', 'also', 'into',
        'product', 'products', 'page', 'post', 'about', 'each', 'over',
    ];

    // Count frequencies
    $freq = [];
    foreach ($words as $w) {
        $w = trim($w);
        // Skip Latin words < 3 chars or stop words; keep all CJK n-grams (>= 2 chars)
        $isCjk = shop_to_llm_is_cjk_content($w);
        if (!$isCjk && (mb_strlen($w) < 3 || in_array($w, $stopWords, true))) {
            continue;
        }
        $freq[$w] = ($freq[$w] ?? 0) + 1;
    }

    // Score: frequency * title bonus * length bonus (longer n-grams score higher)
    $scored = [];
    $titleLower = mb_strtolower($title);
    foreach ($freq as $w => $count) {
        $score = $count;
        if (mb_strpos($titleLower, $w) !== false) {
            $score *= 2;
        }
        // Trigram bonus: 3-char CJK n-grams get 1.5x boost
        if (mb_strlen($w) >= 3 && shop_to_llm_is_cjk_content($w)) {
            $score *= 1.5;
        }
        $scored[$w] = $score;
    }

    arsort($scored);
    return array_slice(array_keys($scored), 0, 10);
}

/**
 * CJK content analysis metrics for the Content Analysis panel.
 *
 * @param int $postId Post ID.
 * @return array{is_cjk: bool, languages: array, char_count: int, word_count_estimate: int, bigram_top: array}
 */
function shop_to_llm_cjk_content_metrics(int $postId): array
{
    $post = get_post($postId);
    if (!$post) {
        return ['is_cjk' => false, 'languages' => [], 'char_count' => 0, 'word_count_estimate' => 0, 'bigram_top' => []];
    }

    $content = wp_strip_all_tags($post->post_content);
    $isCjk = shop_to_llm_is_cjk_content($content);
    $langs = shop_to_llm_detect_cjk_language($content);

    // CJK character count
    $cjkCount = 0;
    if ($isCjk && preg_match_all('/[\x{4e00}-\x{9fff}\x{3040}-\x{30ff}\x{ac00}-\xd7af}]/u', $content, $m)) {
        $cjkCount = count($m[0]);
    }

    // Word count estimate: CJK chars + Latin words
    $latinCount = str_word_count($content);
    $wordEstimate = $cjkCount + $latinCount;

    // Top 5 bigrams
    $bigramTop = [];
    if ($isCjk) {
        $lower = mb_strtolower($content);
        $ngrams = shop_to_llm_extract_cjk_ngrams(preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $lower));
        $freq = array_count_values($ngrams);
        arsort($freq);
        $bigramTop = array_slice(array_keys($freq), 0, 5);
    }

    return [
        'is_cjk'             => $isCjk,
        'languages'          => $langs,
        'char_count'         => $cjkCount,
        'word_count_estimate' => $wordEstimate,
        'bigram_top'         => $bigramTop,
    ];
}

// ─── P1-2: GA4 AI Traffic Integration (v1.3.5+) ────────────────────────
// Connect Google Analytics 4 Data API to measure AI referral traffic.
// Requires explicit user authorization (WP.org Guideline 6 compliant).

/**
 * Get GA4 settings.
 *
 * @return array{property_id: string, credentials_json: string, authorized: bool, enabled: bool}
 */
function shop_to_llm_get_ga4_settings(): array
{
    return [
        'property_id'      => get_option('shop_to_llm_ga4_property_id', ''),
        'credentials_json' => get_option('shop_to_llm_ga4_credentials', ''),
        'authorized'       => get_option('shop_to_llm_ga4_authorized', 'no') === 'yes',
        'enabled'          => get_option('shop_to_llm_ga4_enabled', 'no') === 'yes',
    ];
}

/**
 * Detect AI referral traffic source from a URL/host.
 *
 * @param string $referrer The referrer URL or host.
 * @return string|false AI platform name (e.g., 'ChatGPT') or false if not AI.
 */
function shop_to_llm_detect_ai_referrer(string $referrer)
{
    $referrerLower = strtolower($referrer);
    $aiPatterns = [
        'chatgpt.com'        => 'ChatGPT',
        'openai.com'         => 'ChatGPT',
        'perplexity.ai'      => 'Perplexity',
        'claude.ai'          => 'Claude',
        'anthropic.com'      => 'Claude',
        'gemini.google.com'  => 'Gemini',
        'bard.google.com'     => 'Gemini',
        'copilot.microsoft.com' => 'Copilot',
        'you.com'            => 'You.com',
        'phind.com'          => 'Phind',
        'kagi.com'           => 'Kagi',
        'brave.com'          => 'Brave AI',
        'duckduckgo.com'     => 'DuckDuckGo AI',
    ];

    foreach ($aiPatterns as $host => $name) {
        if (str_contains($referrerLower, $host)) {
            return $name;
        }
    }
    return false;
}

/**
 * Fetch AI traffic summary from GA4 Data API (mock — requires server-side OAuth).
 *
 * In production, this would call the GA4 Data API via wp_remote_post().
 * For v1.3.5, returns cached/stored data; full OAuth flow is Pro-tier.
 *
 * @param int $days Number of days to look back (7 or 28).
 * @return array{total_sessions: int, ai_platforms: array<string, int>, trend: array}
 */
function shop_to_llm_get_ga4_ai_traffic(int $days = 7): array
{
    $settings = shop_to_llm_get_ga4_settings();
    if (!$settings['authorized'] || !$settings['enabled']) {
        return ['total_sessions' => 0, 'ai_platforms' => [], 'trend' => [], 'authorized' => false];
    }

    // Cached result (transient 1h)
    $cacheKey = 'shop_to_llm_ga4_ai_' . $days;
    $cached = get_transient($cacheKey);
    if (is_array($cached)) {
        return array_merge($cached, ['authorized' => true]);
    }

    // Placeholder: in production, fetch from GA4 Data API
    // For now return empty structure (Pro tier implements full OAuth + API call)
    $result = [
        'total_sessions' => 0,
        'ai_platforms'    => [],
        'trend'           => [],
    ];

    set_transient($cacheKey, $result, HOUR_IN_SECONDS);
    return array_merge($result, ['authorized' => true]);
}

/**
 * Render GA4 settings panel (in Settings tab).
 */
function shop_to_llm_render_ga4_settings(): void
{
    $settings = shop_to_llm_get_ga4_settings();
    ?>
    <div class="s2ll-ga4-panel" style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:12px 0">
        <h3 style="margin:0 0 8px;font-size:14px;color:#1e293b">
            <?php echo esc_html__('GA4 AI Traffic Integration', 'shop-to-llm'); ?>
            <span style="font-size:11px;color:#94a3b8;font-weight:400"> (v1.3.5+)</span>
        </h3>
        <p style="font-size:12px;color:#64748b;margin:0 0 12px;line-height:1.5">
            <?php echo esc_html__('Connect Google Analytics 4 to measure AI referral traffic from ChatGPT, Perplexity, Claude, and Gemini. Requires explicit authorization — no data is sent without consent.', 'shop-to-llm'); ?>
        </p>
        <form method="post" action="options.php">
            <?php settings_fields('shop_to_llm_ga4'); ?>
            <table class="form-table" role="presentation">
                <tr>
                    <th scope="row"><label for="shop_to_llm_ga4_property_id"><?php echo esc_html__('GA4 Property ID', 'shop-to-llm'); ?></label></th>
                    <td><input type="text" id="shop_to_llm_ga4_property_id" name="shop_to_llm_ga4_property_id" value="<?php echo esc_attr($settings['property_id']); ?>" placeholder="123456789" class="regular-text" /></td>
                </tr>
                <tr>
                    <th scope="row"><label for="shop_to_llm_ga4_credentials"><?php echo esc_html__('Service Account JSON', 'shop-to-llm'); ?></label></th>
                    <td><textarea id="shop_to_llm_ga4_credentials" name="shop_to_llm_ga4_credentials" rows="4" class="large-text code" placeholder="<?php echo esc_attr__('{"type":"service_account",...}', 'shop-to-llm'); ?>"><?php echo esc_textarea($settings['credentials_json']); ?></textarea></td>
                </tr>
                <tr>
                    <th scope="row"><?php echo esc_html__('Enable GA4 Integration', 'shop-to-llm'); ?></th>
                    <td>
                        <label>
                            <input type="checkbox" name="shop_to_llm_ga4_enabled" value="yes" <?php checked($settings['enabled']); ?> />
                            <?php echo esc_html__('Authorize AI traffic measurement', 'shop-to-llm'); ?>
                        </label>
                    </td>
                </tr>
            </table>
            <p class="submit"><input type="submit" class="button-primary" value="<?php echo esc_attr__('Save GA4 Settings', 'shop-to-llm'); ?>" /></p>
        </form>
        <?php if ($settings['authorized']) : ?>
            <div style="margin-top:12px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#166534">
                ✓ <?php echo esc_html__('GA4 connected. AI traffic data will appear in Dashboard.', 'shop-to-llm'); ?>
            </div>
        <?php endif; ?>
    </div>
    <?php
}

// ─── P1-3: AI Crawler Log Analytics (v1.3.5+) ───────────────────────────
// Opt-in visitor log analysis detects 65 AI crawlers.
// Shows top crawled URLs, visit frequency, crawler distribution.

/**
 * Get AI crawler log analytics summary.
 *
 * @param int $days Number of days to look back (default 7).
 * @return array{total_visits: int, unique_bots: int, top_bots: array, top_urls: array, daily_trend: array}
 */
function shop_to_llm_get_crawler_analytics(int $days = 7): array
{
    global $wpdb;
    $table = $wpdb->prefix . 'shop2llm_crawler_log';

    // Check if table exists
    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
        return ['total_visits' => 0, 'unique_bots' => 0, 'top_bots' => [], 'top_urls' => [], 'daily_trend' => []];
    }

    $since = gmdate('Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS);

    // Total visits
    $totalVisits = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE visited_at >= %s",
        $since
    ));

    // Unique bots
    $uniqueBots = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(DISTINCT bot_name) FROM $table WHERE visited_at >= %s",
        $since
    ));

    // Top bots (by visit count)
    $topBots = $wpdb->get_results($wpdb->prepare(
        "SELECT bot_name, bot_label, COUNT(*) as cnt FROM $table WHERE visited_at >= %s GROUP BY bot_name, bot_label ORDER BY cnt DESC LIMIT 10",
        $since
    ));

    // Top URLs (by visit count)
    $topUrls = $wpdb->get_results($wpdb->prepare(
        "SELECT page_url, COUNT(*) as cnt FROM $table WHERE visited_at >= %s AND page_url != '' GROUP BY page_url ORDER BY cnt DESC LIMIT 10",
        $since
    ));

    // Daily trend
    $dailyTrend = $wpdb->get_results($wpdb->prepare(
        "SELECT DATE(visited_at) as d, COUNT(*) as cnt FROM $table WHERE visited_at >= %s GROUP BY DATE(visited_at) ORDER BY d ASC",
        $since
    ));
    // phpcs:enable

    return [
        'total_visits'  => $totalVisits,
        'unique_bots'   => $uniqueBots,
        'top_bots'      => $topBots,
        'top_urls'      => $topUrls,
        'daily_trend'   => $dailyTrend,
    ];
}

/**
 * Render AI Crawler Analytics widget (in Dashboard).
 */
function shop_to_llm_render_crawler_analytics_widget(): void
{
    $analytics = shop_to_llm_get_crawler_analytics(7);
    $totalVisits = $analytics['total_visits'];
    $uniqueBots = $analytics['unique_bots'];

    // Distribution pie chart data (top bots)
    $botDistribution = [];
    if (!empty($analytics['top_bots'])) {
        foreach ($analytics['top_bots'] as $bot) {
            $botDistribution[] = [
                'name'  => $bot->bot_label,
                'count' => (int) $bot->cnt,
            ];
        }
    }

    // Daily trend sparkline data
    $trendData = [];
    if (!empty($analytics['daily_trend'])) {
        foreach ($analytics['daily_trend'] as $day) {
            $trendData[] = ['date' => $day->d, 'count' => (int) $day->cnt];
        }
    }

    // Top URLs
    $topUrls = $analytics['top_urls'];

    ?>
    <div class="s2ll-crawler-analytics" style="background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:16px;margin:12px 0">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
            <h3 style="margin:0;font-size:14px;color:#1e293b">
                <?php echo esc_html__('AI Crawler Analytics (7 days)', 'shop-to-llm'); ?>
            </h3>
            <span style="font-size:11px;color:#94a3b8"><?php echo esc_html__('v1.3.5+', 'shop-to-llm'); ?></span>
        </div>

        <?php if ($totalVisits === 0) : ?>
            <div style="text-align:center;padding:24px 0;color:#94a3b8">
                <div style="font-size:32px;margin-bottom:8px">🔍</div>
                <p style="font-size:13px;margin:0"><?php echo esc_html__('No AI crawler visits detected yet. Make sure your robots.txt allows AI crawlers.', 'shop-to-llm'); ?></p>
            </div>
        <?php else : ?>
            <!-- Summary stats -->
            <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(120px, 1fr));gap:12px;margin-bottom:16px">
                <div style="background:#f8fafc;padding:12px;border-radius:6px;text-align:center">
                    <div style="font-size:24px;font-weight:700;color:#1e293b"><?php echo esc_html(number_format_i18n($totalVisits)); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Total Visits', 'shop-to-llm'); ?></div>
                </div>
                <div style="background:#f8fafc;padding:12px;border-radius:6px;text-align:center">
                    <div style="font-size:24px;font-weight:700;color:#1e293b"><?php echo esc_html(number_format_i18n($uniqueBots)); ?></div>
                    <div style="font-size:11px;color:#64748b;margin-top:2px"><?php echo esc_html__('Unique Crawlers', 'shop-to-llm'); ?></div>
                </div>
            </div>

            <!-- Crawler distribution (CSS pie chart via conic-gradient) -->
            <?php if (!empty($botDistribution)) : ?>
                <div style="margin-bottom:16px">
                    <h4 style="font-size:12px;color:#64748b;margin:0 0 8px"><?php echo esc_html__('Crawler Distribution', 'shop-to-llm'); ?></h4>
                    <div style="display:flex;gap:16px;align-items:center">
                        <div style="width:120px;height:120px;border-radius:50%;background:<?php echo esc_attr(shop_to_llm_render_pie_gradient($botDistribution)); ?>;flex-shrink:0"></div>
                        <div style="flex:1;font-size:11px;line-height:1.8">
                            <?php foreach ($botDistribution as $i => $bot) : ?>
                                <div style="display:flex;align-items:center;gap:6px">
                                    <span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:<?php echo esc_attr(shop_to_llm_pie_color($i)); ?>"></span>
                                    <span style="flex:1;color:#1e293b"><?php echo esc_html($bot['name']); ?></span>
                                    <span style="color:#64748b"><?php echo esc_html(number_format_i18n($bot['count'])); ?></span>
                                </div>
                            <?php endforeach; ?>
                        </div>
                    </div>
                </div>
            <?php endif; ?>

            <!-- Top crawled URLs -->
            <?php if (!empty($topUrls)) : ?>
                <div style="margin-bottom:16px">
                    <h4 style="font-size:12px;color:#64748b;margin:0 0 8px"><?php echo esc_html__('Top Crawled URLs', 'shop-to-llm'); ?></h4>
                    <div style="font-size:11px;line-height:1.8">
                        <?php foreach ($topUrls as $url) : ?>
                            <div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #f1f5f9">
                                <span style="color:#1e293b;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px"><?php echo esc_html($url->page_url); ?></span>
                                <span style="color:#64748b;flex-shrink:0;margin-left:8px"><?php echo esc_html(number_format_i18n((int) $url->cnt)); ?></span>
                            </div>
                        <?php endforeach; ?>
                    </div>
                </div>
            <?php endif; ?>

            <!-- Daily trend sparkline -->
            <?php if (!empty($trendData)) : ?>
                <div>
                    <h4 style="font-size:12px;color:#64748b;margin:0 0 8px"><?php echo esc_html__('Daily Trend', 'shop-to-llm'); ?></h4>
                    <?php shop_to_llm_render_sparkline_svg($trendData); ?>
                </div>
            <?php endif; ?>
        <?php endif; ?>
    </div>
    <?php
}

/**
 * Generate CSS conic-gradient for pie chart.
 *
 * @param array $data Distribution data [{name, count}, ...].
 * @return string CSS conic-gradient value.
 */
function shop_to_llm_render_pie_gradient(array $data): string
{
    if (empty($data)) {
        return '#e2e8f0';
    }
    $total = array_sum(array_column($data, 'count'));
    if ($total === 0) {
        return '#e2e8f0';
    }

    $gradient = [];
    $start = 0;
    foreach ($data as $i => $item) {
        $pct = ($item['count'] / $total) * 100;
        $end = $start + $pct;
        $color = shop_to_llm_pie_color($i);
        $gradient[] = "$color $start% $end%";
        $start = $end;
    }
    return 'conic-gradient(' . implode(', ', $gradient) . ')';
}

/**
 * Get pie chart color by index.
 *
 * @param int $i Index.
 * @return string Hex color.
 */
function shop_to_llm_pie_color(int $i): string
{
    $colors = [
        '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
        '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
    ];
    return $colors[$i % count($colors)];
}

/**
 * Render SVG sparkline chart (echoes directly to avoid OutputNotEscaped).
 *
 * @param array $data [{date, count}, ...].
 */
function shop_to_llm_render_sparkline_svg(array $data): void
{
    if (count($data) < 2) {
        echo '<div style="font-size:11px;color:#94a3b8">—</div>';
        return;
    }

    $max = max(array_column($data, 'count'));
    if ($max === 0) {
        echo '<div style="font-size:11px;color:#94a3b8">—</div>';
        return;
    }

    $width = 280;
    $height = 60;
    $padding = 4;
    $points = [];
    foreach ($data as $i => $point) {
        $x = $padding + ($i / (count($data) - 1)) * ($width - 2 * $padding);
        $y = $height - $padding - ($point['count'] / $max) * ($height - 2 * $padding);
        $points[] = "$x,$y";
    }

    $polyline = implode(' ', $points);
    $xEnd = $width - $padding;
    $yEnd = $height - $padding;
    $areaPoints = $polyline . " {$xEnd},{$yEnd} {$padding},{$yEnd}";

    echo '<svg width="' . esc_attr((string) $width) . '" height="' . esc_attr((string) $height) . '" viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" xmlns="http://www.w3.org/2000/svg">';
    echo '<polygon points="' . esc_attr($areaPoints) . '" fill="#3b82f6" fill-opacity="0.1" />';
    echo '<polyline points="' . esc_attr($polyline) . '" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />';
    echo '</svg>';
}

