# mxchat-basic/3.2.20/admin/class-ajax-handler.php

MxChat – AI Chatbot &amp; Content Generation for WordPress, version 3.2.20. 2,016 lines.

- Page: https://pluginprobe.com/plugins/mxchat-basic/3.2.20/code/admin/class-ajax-handler.php
- Raw: https://pluginprobe.com/plugins/mxchat-basic/3.2.20/raw/admin/class-ajax-handler.php
- Modified: 2026-08-28T11:35:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/mxchat-basic/3.2.20/code/admin/class-ajax-handler.php#L10-L20`.

```php
<?php
/**
 * File: admin/class-ajax-handler.php
 *
 * Handles all AJAX requests for MxChat admin functionality
 */

if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

class MxChat_Ajax_Handler {
    
    private $pinecone_manager = null;
        
    /**
     * Constructor - Register all AJAX hooks
     */
    public function __construct() {
        $this->mxchat_init_ajax_hooks();
    }
    

    /**
     * Register all AJAX action hooks
     */
private function mxchat_init_ajax_hooks() {
    // Settings AJAX
    add_action('wp_ajax_mxchat_save_setting', array($this, 'mxchat_save_setting_callback'));
    add_action('wp_ajax_mxchat_save_prompts_setting', array($this, 'mxchat_save_prompts_setting_callback'));
    add_action('wp_ajax_mxchat_acf_toggle_group', array($this, 'mxchat_acf_toggle_group_callback'));
    add_action('wp_ajax_migrate_pinecone_settings', array($this, 'ajax_migrate_pinecone_settings'));
    
    // License AJAX 
    add_action('wp_ajax_mxchat_handle_activate_license', array($this, 'mxchat_handle_activate_license'));
    add_action('wp_ajax_mxchat_check_license_status', array($this, 'mxchat_check_license_status'));
    add_action('wp_ajax_mxchat_deactivate_license', array($this, 'mxchat_deactivate_license'));
    
    // Actions & Intents AJAX
    add_action('wp_ajax_mxchat_toggle_action', array($this, 'mxchat_toggle_action'));
    add_action('wp_ajax_mxchat_update_intent_threshold', array($this, 'mxchat_update_intent_threshold'));

    add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
    add_action('wp_ajax_mxchat_check_api_keys', array($this, 'mxchat_check_api_keys'));

    // Debug & Optimization AJAX
    add_action('wp_ajax_mxchat_toggle_debug_mode', array($this, 'mxchat_toggle_debug_mode_callback'));
    add_action('wp_ajax_mxchat_get_debug_log', array($this, 'mxchat_get_debug_log_callback'));
    add_action('wp_ajax_mxchat_clear_debug_log', array($this, 'mxchat_clear_debug_log_callback'));
    add_action('wp_ajax_mxchat_export_settings', array($this, 'mxchat_export_settings_callback'));
    add_action('wp_ajax_mxchat_reset_all_settings', array($this, 'mxchat_reset_all_settings_callback'));

    // Global rate-limit usage counter reset (admin-only, nonce-guarded)
    add_action('wp_ajax_mxchat_reset_global_rate_limit', array($this, 'mxchat_reset_global_rate_limit_callback'));

    // Custom (OpenAI-compatible) Provider connection test
    add_action('wp_ajax_mxchat_test_custom_provider', array($this, 'mxchat_test_custom_provider_callback'));

    // Built-in provider key validation — cheap per-provider auth check (plan-mxchat-20260623-c41f74)
    add_action('wp_ajax_mxchat_test_provider_key', array($this, 'mxchat_test_provider_key_callback'));

    // Custom Post Meta discovery scan for the KB whitelist picker (plan-mxchat-20260709-fe8e4e)
    add_action('wp_ajax_mxchat_scan_custom_meta_keys', array($this, 'mxchat_scan_custom_meta_keys_callback'));
}

/**
 * Discover non-ACF custom post-meta keys present on published public content, so the
 * KB → Custom Post Meta section can offer a click-to-add picker instead of a blind
 * "type the exact key you already know" textarea. plan-mxchat-20260709-fe8e4e.
 *
 * Bounded + button-triggered only (never on page load). Returns up to 50 keys by
 * frequency, each with a short sample value, so the owner can judge relevance before
 * whitelisting. Underscore-prefixed (protected/internal) keys are hidden unless the
 * caller opts in; ACF-managed keys are excluded so this picker never double-lists the
 * sibling ACF discovery picker on the same page.
 */
public function mxchat_scan_custom_meta_keys_callback() {
    check_ajax_referer('mxchat_prompts_setting_nonce');

    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
    }

    global $wpdb;

    $include_internal = isset($_POST['include_internal']) && $_POST['include_internal'] === '1';

    // Restrict discovery to public post types (the content the KB actually embeds).
    $post_types = get_post_types(array('public' => true), 'names');
    if (empty($post_types)) {
        wp_send_json_success(array('keys' => array(), 'scanned' => 0));
    }
    $pt_placeholders = implode(',', array_fill(0, count($post_types), '%s'));

    // Build the set of ACF-managed meta keys to exclude. ACF stores, alongside each
    // value key `foo`, a reference key `_foo` whose value is the ACF field key
    // (`field_xxxxx`). Strip the leading underscore from every such reference key to
    // get the real meta key, and exclude those — the ACF picker on this page owns them.
    $acf_managed = array();
    $acf_refs = $wpdb->get_col(
        $wpdb->prepare(
            "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_value LIKE %s",
            $wpdb->esc_like('_') . '%',
            $wpdb->esc_like('field_') . '%'
        )
    );
    foreach ((array) $acf_refs as $ref_key) {
        if (strlen($ref_key) > 1 && $ref_key[0] === '_') {
            $acf_managed[substr($ref_key, 1)] = true;
        }
    }

    // Discover keys + counts + a sample value in one bounded aggregate query.
    // SUBSTRING(MIN(...)) keeps the sample selection ONLY_FULL_GROUP_BY-safe.
    $params = $post_types;
    $sql = "SELECT pm.meta_key AS mk, COUNT(*) AS n, SUBSTRING(MIN(pm.meta_value), 1, 200) AS sample
            FROM {$wpdb->postmeta} pm
            INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
            WHERE p.post_status = 'publish'
              AND p.post_type IN ($pt_placeholders)
              AND pm.meta_key <> ''";
    if (!$include_internal) {
        $sql .= " AND pm.meta_key NOT LIKE %s";
        $params[] = $wpdb->esc_like('_') . '%';
    }
    $sql .= " GROUP BY pm.meta_key ORDER BY n DESC, pm.meta_key ASC LIMIT 200";

    // phpcs:ignore WordPress.DB.PreparedSQL — placeholders assembled above, values in $params.
    $rows = $wpdb->get_results($wpdb->prepare($sql, $params));

    $keys = array();
    foreach ((array) $rows as $row) {
        $mk = $row->mk;
        if (isset($acf_managed[$mk])) {
            continue; // already offered by the ACF picker
        }

        $raw = (string) $row->sample;
        if ($raw !== '' && (is_serialized($raw) || preg_match('/^(a:\d+:\{|O:\d+:"|s:\d+:")/', $raw))) {
            $sample = esc_html__('[structured value]', 'mxchat');
        } else {
            $sample = trim(preg_replace('/\s+/', ' ', $raw));
            if (function_exists('mb_strlen') ? mb_strlen($sample) > 60 : strlen($sample) > 60) {
                $sample = (function_exists('mb_substr') ? mb_substr($sample, 0, 60) : substr($sample, 0, 60)) . '…';
            }
            if ($sample === '') {
                $sample = esc_html__('(empty value)', 'mxchat');
            }
        }

        $keys[] = array(
            'key'    => $mk,
            'count'  => (int) $row->n,
            'sample' => $sample,
        );

        if (count($keys) >= 50) {
            break;
        }
    }

    wp_send_json_success(array(
        'keys'    => $keys,
        'scanned' => is_array($rows) ? count($rows) : 0,
    ));
}

/**
 * Test connection to a Custom (OpenAI-compatible) provider by hitting its /models endpoint
 * with whichever auth scheme the user configured. Reports model count or a clean error.
 */
public function mxchat_test_custom_provider_callback() {
    check_ajax_referer('mxchat_test_custom_provider');
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat')));
    }

    $options    = get_option('mxchat_options', array());
    $base_url   = isset($options['custom_provider_base_url']) ? trim((string) $options['custom_provider_base_url']) : '';
    $api_key    = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : '';
    $auth       = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer';
    $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : '';

    if (empty($base_url)) {
        wp_send_json_error(array('message' => esc_html__('Base URL is empty. Save it first.', 'mxchat')));
    }

    $url = rtrim($base_url, '/') . '/models';
    if (!empty($api_version)) {
        $url = add_query_arg('api-version', $api_version, $url);
    }

    $headers = array('Content-Type' => 'application/json');
    if (!empty($api_key)) {
        if ($auth === 'api-key') {
            $headers['api-key'] = $api_key;
        } else {
            $headers['Authorization'] = 'Bearer ' . $api_key;
        }
    }

    $response = wp_remote_get($url, array(
        'headers' => $headers,
        'timeout' => 10,
    ));

    if (is_wp_error($response)) {
        wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message()))));
    }

    $code = (int) wp_remote_retrieve_response_code($response);
    if ($code === 401 || $code === 403) {
        wp_send_json_error(array('message' => sprintf(esc_html__('Auth rejected (HTTP %d). Check API key and auth scheme.', 'mxchat'), $code)));
    }
    if ($code === 404) {
        wp_send_json_error(array('message' => esc_html__('Endpoint not found (HTTP 404). Check the Base URL.', 'mxchat')));
    }
    if ($code < 200 || $code >= 300) {
        wp_send_json_error(array('message' => sprintf(esc_html__('Upstream returned HTTP %d.', 'mxchat'), $code)));
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    $count = 0;
    if (is_array($body)) {
        if (isset($body['data']) && is_array($body['data'])) {
            $count = count($body['data']);
        } elseif (isset($body['models']) && is_array($body['models'])) {
            $count = count($body['models']);
        }
    }

    wp_send_json_success(array(
        'message' => sprintf(esc_html__('Connection OK — %d model(s) reported.', 'mxchat'), $count),
        'count'   => $count,
    ));
}

/**
 * Validate a BUILT-IN provider key with the lightest authenticated call per
 * provider (a /models or key-info GET — never a generation). Reads the posted
 * key value so the owner can test BEFORE saving; falls back to the saved option
 * when the field is empty. Mirrors mxchat_test_custom_provider_callback and the
 * add-on test buttons (cf5bd5 veo / 8d16f1 perplexity). The key is never logged.
 * plan-mxchat-20260623-c41f74.
 */
public function mxchat_test_provider_key_callback() {
    check_ajax_referer('mxchat_test_provider_key');
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat')));
    }

    $provider   = isset($_POST['provider']) ? sanitize_key(wp_unslash($_POST['provider'])) : '';
    $posted_key = isset($_POST['key']) ? trim((string) wp_unslash($_POST['key'])) : '';

    $option_map = array(
        'openai'     => 'api_key',
        'xai'        => 'xai_api_key',
        'claude'     => 'claude_api_key',
        'deepseek'   => 'deepseek_api_key',
        'gemini'     => 'gemini_api_key',
        'openrouter' => 'openrouter_api_key',
    );
    if (!isset($option_map[$provider])) {
        wp_send_json_error(array('message' => esc_html__('Unknown provider.', 'mxchat')));
    }

    // Prefer the just-typed value (test-before-save); fall back to the saved key.
    $key = $posted_key;
    if ($key === '') {
        $options = get_option('mxchat_options', array());
        $key = isset($options[$option_map[$provider]]) ? trim((string) $options[$option_map[$provider]]) : '';
    }
    if ($key === '') {
        wp_send_json_error(array('message' => esc_html__('No API key entered or saved for this provider.', 'mxchat')));
    }

    // Lightest authenticated metadata call per provider — model-agnostic, no generation.
    $headers = array();
    switch ($provider) {
        case 'openai':
            $url = 'https://api.openai.com/v1/models';
            $headers = array('Authorization' => 'Bearer ' . $key);
            break;
        case 'xai':
            $url = 'https://api.x.ai/v1/models';
            $headers = array('Authorization' => 'Bearer ' . $key);
            break;
        case 'deepseek':
            $url = 'https://api.deepseek.com/models';
            $headers = array('Authorization' => 'Bearer ' . $key);
            break;
        case 'openrouter':
            // /auth/key validates the key itself (the public /models list does not).
            $url = 'https://openrouter.ai/api/v1/auth/key';
            $headers = array('Authorization' => 'Bearer ' . $key);
            break;
        case 'gemini':
            $url = add_query_arg(array('pageSize' => 1, 'key' => $key), 'https://generativelanguage.googleapis.com/v1beta/models');
            break;
        case 'claude':
            $url = 'https://api.anthropic.com/v1/models';
            $headers = array('x-api-key' => $key, 'anthropic-version' => '2023-06-01');
            break;
        default:
            wp_send_json_error(array('message' => esc_html__('Unknown provider.', 'mxchat')));
    }

    $response = wp_remote_get($url, array(
        'headers' => $headers,
        'timeout' => 10,
    ));

    if (is_wp_error($response)) {
        wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message()))));
    }

    $code = (int) wp_remote_retrieve_response_code($response);
    if ($code >= 200 && $code < 300) {
        wp_send_json_success(array('message' => esc_html__('Key is valid.', 'mxchat')));
    }

    // Surface the provider's own error text when present (trimmed; key never echoed).
    $detail = '';
    $body   = json_decode(wp_remote_retrieve_body($response), true);
    if (is_array($body)) {
        if (isset($body['error']['message'])) {
            $detail = $body['error']['message'];
        } elseif (isset($body['error']) && is_string($body['error'])) {
            $detail = $body['error'];
        } elseif (isset($body['message'])) {
            $detail = $body['message'];
        }
    }
    $detail = trim((string) $detail);
    if (strlen($detail) > 200) {
        $detail = substr($detail, 0, 200) . '…';
    }

    if ($code === 401 || $code === 403) {
        $msg = ($detail !== '')
            ? sprintf(esc_html__('Key rejected (HTTP %1$d): %2$s', 'mxchat'), $code, esc_html($detail))
            : sprintf(esc_html__('Key rejected (HTTP %d). Check the API key.', 'mxchat'), $code);
        wp_send_json_error(array('message' => $msg));
    }

    $msg = ($detail !== '')
        ? sprintf(esc_html__('Provider returned HTTP %1$d: %2$s', 'mxchat'), $code, esc_html($detail))
        : sprintf(esc_html__('Provider returned HTTP %d.', 'mxchat'), $code);
    wp_send_json_error(array('message' => $msg));
}

    // ========================================
    // SETTINGS AJAX HANDLERS
    // ========================================

/**
 * Validates and saves chat settings via AJAX request
 */
public function mxchat_save_setting_callback() {
    check_ajax_referer('mxchat_save_setting_nonce');
    if (!current_user_can('manage_options')) {
        ('MXChat Save: Unauthorized access attempt');
        wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
    }

    $name = isset($_POST['name']) ? $_POST['name'] : '';
    // Remove WP's added slashes before saving (wp_unslash is the canonical form; plan-3f8158).
    $value = isset($_POST['value']) ? wp_unslash($_POST['value']) : '';

    //error_log('MXChat Save: Processing field name: ' . $name);
    //error_log('MXChat Save: Field value: ' . $value);

    if (empty($name)) {
        //error_log('MXChat Save: Empty field name detected');
        wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
    }

    // Load the full options array
    $options = get_option('mxchat_options', []);
    //error_log('MXChat Save: Current options array: ' . print_r($options, true));

    // Extract field name from mxchat_options[field_name] format if present
    // But preserve the full name for special cases like rate_limits that need the full path
    $field_name = $name;
    if (preg_match('/^mxchat_options\[([^\[\]]+)\]$/', $name, $matches)) {
        $field_name = $matches[1];
    }

    // Handle special cases
    switch ($field_name) {
        // Editor Assistant enable toggle (plan-8cb0cb). STANDALONE option — NOT part
        // of mxchat_options, so it skips the mxchat_sanitize strip-trap entirely. Save
        // it directly and short-circuit (mirrors the mxchat_transcripts_options pattern
        // below); never falls through to the generic mxchat_options save. Default OFF.
        case 'mxchat_editor_assistant_enabled':
            $ea_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
            update_option('mxchat_editor_assistant_enabled', $ea_value);
            wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
            return;

        // Smart asset loading toggle (plan-915355). STANDALONE option, same
        // reasoning as the Editor Assistant case above — saved directly and
        // short-circuited so it never touches mxchat_options / mxchat_sanitize.
        // Default OFF (opt-in performance optimization).
        case 'mxchat_smart_asset_loading':
            $sal_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
            update_option('mxchat_smart_asset_loading', $sal_value);
            wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
            return;

        // Hybrid keyword boost toggle (plan-38ffa1). STANDALONE option, same
        // pattern. Enabling runs capability detection HERE, at admin-save time —
        // building the FULLTEXT index during a visitor's chat request is not
        // acceptable, and detection is a one-time cost the admin can wait on.
        case 'mxchat_hybrid_keyword_toggle':
            $hkb_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
            update_option('mxchat_hybrid_keyword_toggle', $hkb_value);
            if ($hkb_value === 'on') {
                MxChat_Utils::mxchat_hybrid_detect_capability(true);
            }
            wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
            return;

        // ACF→PDF import-time extraction (plan 11720c). STANDALONE option, same
        // pattern. Moved from a per-import modal checkbox to an install-level
        // setting on Knowledge → ACF Fields. Stored '1'/'0' to match the
        // knowledge page's sibling toggles. Default OFF.
        case 'mxchat_acf_pdf_extraction':
            $apx_value = ($value === 'on' || $value === '1') ? '1' : '0';
            update_option('mxchat_acf_pdf_extraction', $apx_value);
            wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
            return;

        // In-chat YouTube card: master switch + its own confidence floor
        // (plan f52492). STANDALONE options, same pattern as the cases above.
        // Default ON — the card already ships, so this is an opt-OUT.
        case 'mxchat_video_embed_enabled':
            $vce_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
            update_option('mxchat_video_embed_enabled', $vce_value);
            wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
            return;

        // Clamped to the same 20-95 the field advertises. An out-of-range or
        // non-numeric POST is corrected rather than refused, and the corrected
        // value is echoed back so the field can reconcile — a silently stored
        // 0 here would put a video on every answer, which is the bug.
        case 'mxchat_video_embed_threshold':
            $vct_value = is_numeric($value)
                ? (int) $value
                : MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT;
            if ($vct_value < 20) { $vct_value = 20; }
            if ($vct_value > 95) { $vct_value = 95; }
            update_option('mxchat_video_embed_threshold', $vct_value);
            wp_send_json_success([
                'message' => esc_html__('Setting saved', 'mxchat'),
                'value'   => $vct_value,
            ]);
            return;

        // Live-agent availability schedules (plans 8ccaa2 + 99d7a4). STANDALONE
        // options, same reasoning as the Editor Assistant case above — nested
        // structures that mxchat_sanitize() would strip on the next autosave of any
        // other field. Each channel's value arrives as JSON from its own hidden
        // input, which that channel's schedule editor keeps in sync; the class owns
        // all validation. The bare legacy name is kept as a defense against a
        // browser still running pre-split cached admin JS: that UI edited "both
        // channels" as one, so its save writes both.
        case 'live_agent_schedule_slack':
        case 'live_agent_schedule_telegram':
        case 'live_agent_schedule_webhook':
        case 'live_agent_schedule':
            if (!class_exists('MxChat_Live_Agent_Schedule')) {
                wp_send_json_error(['message' => esc_html__('Schedule unavailable', 'mxchat')]);
                return;
            }
            $decoded = json_decode($value, true);
            if (!is_array($decoded)) {
                wp_send_json_error(['message' => esc_html__('Invalid schedule', 'mxchat')]);
                return;
            }
            $channels = ($field_name === 'live_agent_schedule')
                ? array('slack', 'telegram')
                : array(substr($field_name, strlen('live_agent_schedule_')));
            $saved_schedule = null;
            foreach ($channels as $schedule_channel) {
                $saved_schedule = MxChat_Live_Agent_Schedule::save($schedule_channel, $decoded);
            }
            // Echo the normalized result so the editor can reconcile if it ever
            // disagrees with the server (e.g. a time the class rejected).
            wp_send_json_success([
                'message'  => esc_html__('Setting saved', 'mxchat'),
                'schedule' => $saved_schedule,
            ]);
            return;

        case 'model':
            //error_log('MXChat Save: Processing model selection');
            //error_log('MXChat Save: Model value received: ' . $value);
            //error_log('MXChat Save: Value type: ' . gettype($value));
            //error_log('MXChat Save: Value length: ' . strlen($value));
            //error_log('MXChat Save: Value === "openrouter": ' . ($value === 'openrouter' ? 'YES' : 'NO'));
            
            // Allow 'openrouter' or validate against whitelist
            if ($value === 'openrouter') {
                //error_log('MXChat Save: Setting model to openrouter');
                $options['model'] = 'openrouter';
            } else {
                //error_log('MXChat Save: Checking against whitelist');
                 // Catalog refactor (plan-d14e89): canonical allowlist lives in
                 // includes/class-mxchat-model-catalog.php. A new chat model
                 // added there is automatically accepted by autosave.
                 if (!class_exists('MxChat_Model_Catalog')) {
                     require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php';
                 }
                 $allowed_models = MxChat_Model_Catalog::chat_model_ids();
                
                //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO'));
                
                if (in_array($value, $allowed_models)) {
                    //error_log('MXChat Save: Model is in whitelist, saving');
                    $options['model'] = sanitize_text_field($value);
                } else {
                    //error_log('MXChat Save: Invalid model rejected: ' . $value);
                    //error_log('MXChat Save: Allowed models: ' . print_r($allowed_models, true));
                    wp_send_json_error(['message' => esc_html__('Invalid model selected', 'mxchat')]);
                    return;
                }
            }
            break;
            
        case 'openrouter_selected_model':
            //error_log('MXChat Save: Processing OpenRouter model: ' . $value);
            $options['openrouter_selected_model'] = sanitize_text_field($value);
            // Force immediate save for new keys
            //error_log('MXChat Save: OpenRouter model saved immediately');
            break;
        
        case 'openrouter_selected_model_name':
            //error_log('MXChat Save: Processing OpenRouter model name: ' . $value);
            $options['openrouter_selected_model_name'] = sanitize_text_field($value);
            // Force immediate save for new keys
            //error_log('MXChat Save: OpenRouter model name saved immediately');
            break;
            
        case 'openrouter_api_key':
            //error_log('MXChat Save: Processing OpenRouter API key');
            $options['openrouter_api_key'] = sanitize_text_field($value);
            break;
            
        // REMOVED DUPLICATE case 'openrouter_selected_model_name' HERE!
            
        case 'additional_popular_questions':
            //error_log('MXChat Save: Processing additional_popular_questions');
            $questions = json_decode($value, true); // No need for stripslashes here
            if (is_array($questions)) {
                $options[$field_name] = $questions;
                // Also update old option for backwards compatibility
                update_option('additional_popular_questions', $questions);
                //error_log('MXChat Save: Saved ' . count($questions) . ' additional questions');
            } else {
                //error_log('MXChat Save: Failed to decode questions JSON');
            }
            break;
        case 'email_blocker_header_content':
            //error_log('MXChat Save: Processing email_blocker_header_content');
            // Allow HTML content but sanitize it safely
            $options[$field_name] = wp_kses_post($value);
            break;
        case 'intro_message':
            // Stored-XSS hardening (Wordfence CWE-79, plan-3f8158): sanitize on save as
            // defense in depth. wp_kses_post mirrors mxchat_sanitize() (the options.php
            // save path) so both save routes treat intro_message identically and strip
            // <script>/</textarea> breakout while keeping basic formatting + {visitor_name}.
            $options[$field_name] = wp_kses_post($value);
            break;
        case 'email_blocker_button_text':
            //error_log('MXChat Save: Processing email_blocker_button_text');
            $options[$field_name] = sanitize_text_field($value);
            break;
        case 'name_field_placeholder':
            //error_log('MXChat Save: Processing name_field_placeholder');
            $options[$field_name] = sanitize_text_field($value);
            break;
        case 'consent_checkbox_label':
            // b062c4 — same allowlist as the options.php save path and the
            // widget render, so the stored label always equals the shown label.
            $options[$field_name] = MxChat_Utils::sanitize_consent_label($value);
            break;
        case 'similarity_threshold':
            //error_log('MXChat Save: Processing similarity_threshold');
            // Validate and save - enforce min 20, max 85
            $threshold = intval($value);
            if ($threshold < 20) $threshold = 20;
            if ($threshold > 85) $threshold = 85;
            $options[$field_name] = $threshold;
            break;
        case 'rag_sources_limit':
            //error_log('MXChat Save: Processing rag_sources_limit');
            // Validate and save - enforce min 3, max 10, default 6
            $rag_limit = intval($value);
            if ($rag_limit < 3) $rag_limit = 3;
            if ($rag_limit > 10) $rag_limit = 10;
            $options[$field_name] = $rag_limit;
            break;
        case 'rag_chunks_limit':
            // Validate and save - enforce min 8, max 20, default 15
            $chunks_limit = intval($value);
            if ($chunks_limit < 8) $chunks_limit = 8;
            if ($chunks_limit > 20) $chunks_limit = 20;
            $options[$field_name] = $chunks_limit;
            break;
        case 'live_agent_status':
            //error_log('MXChat Save: Processing live_agent_status');
            // Set the new value
            $options[$field_name] = ($value === 'on') ? 'on' : 'off';
            break;
        // Webhook handoff destination (plan d88e22). Status normalized like the
        // other channel toggles; the URL is refused outright when it isn't
        // https so the admin hears about it at save time instead of the
        // handoff silently never firing.
        case 'webhook_handoff_status':
            $options[$field_name] = ($value === 'on') ? 'on' : 'off';
            break;
        case 'webhook_handoff_url':
            $wh_url = trim((string) $value);
            if ($wh_url === '') {
                $options[$field_name] = '';
                break;
            }
            $wh_clean = esc_url_raw($wh_url, array('https'));
            if ($wh_clean === '' || stripos($wh_clean, 'https://') !== 0) {
                wp_send_json_error(['message' => esc_html__('Webhook URL must start with https://', 'mxchat')]);
                return;
            }
            $options[$field_name] = $wh_clean;
            break;
        case 'enable_web_search':
            //error_log('MXChat Save: Processing enable_web_search');
            $options[$field_name] = ($value === 'on') ? 'on' : 'off';
            break;
        case 'enable_woocommerce_integration':
            //error_log('MXChat Save: Processing enable_woocommerce_integration');
            // Handle values that used to be 1/0
            $options[$field_name] = ($value === 'on' || $value === '1') ? 'on' : 'off';
            break;
        case 'post_type_visibility_mode':
            // Validate mode value
            $allowed_modes = array('all', 'include', 'exclude');
            $options[$field_name] = in_array($value, $allowed_modes) ? $value : 'all';
            break;
        case 'post_type_visibility_list':
            // Handle JSON array of post types
            $post_types = json_decode($value, true);
            if (is_array($post_types)) {
                // Sanitize each post type slug
                $options[$field_name] = array_map('sanitize_key', $post_types);
            } else {
                $options[$field_name] = array();
            }
            break;
        case 'script_loading_strategy':
            // Validate script loading strategy value
            $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
            $options[$field_name] = in_array($value, $allowed_strategies) ? $value : 'default';
            break;
        case 'auto_retry_on_transient_error':
            // Boolean toggle — accept 1/0/on/off, default to '1' if any truthy value.
            $options[$field_name] = ($value === '1' || $value === 'on' || $value === 1 || $value === true) ? '1' : '0';
            break;
        default:
            // Handle transcripts options
            if (strpos($name, 'mxchat_transcripts_options') !== false) {
                // Extract field name from mxchat_transcripts_options[field_name]
                if (preg_match('/mxchat_transcripts_options\[([^\]]+)\]/', $name, $matches)) {
                    $field_name = $matches[1];

                    // Get current transcripts options
                    $transcripts_options = get_option('mxchat_transcripts_options', array());

                    // Ensure it's an array
                    if (!is_array($transcripts_options)) {
                        $transcripts_options = array();
                    }

                    // Handle checkbox values (convert 'on'/'off' to 1/0)
                    if ($field_name === 'mxchat_retention_days') {
                        // Number field, NOT a checkbox — without this branch a
                        // value of '1' would hit the 'on'/'1' coercion below
                        // (harmlessly) but nothing would clamp: this direct-DB
                        // path bypasses the registered sanitiser and its
                        // 0-3650 clamp entirely (plan-3c3338).
                        $transcripts_options[$field_name] = max(0, min(3650, (int) $value));
                    } else if ($value === 'on' || $value === '1') {
                        $transcripts_options[$field_name] = 1;
                    } else if ($value === 'off' || $value === '0' || $value === '') {
                        $transcripts_options[$field_name] = 0;
                    } else {
                        // For text/select fields, sanitize appropriately
                        if ($field_name === 'mxchat_notification_email') {
                            // sanitize_email() alone CANNOT validate this field: given
                            // "a@x.com, b@y.com" it returns the single concatenated
                            // address "a@x.comby.com", which is_email() then accepts.
                            // That is how two addresses used to be stored as one dead
                            // one, silently. MxChat_Utils validates the raw parts first
                            // and refuses the whole list if any of them is bad.
                            $parsed = MxChat_Utils::parse_notification_emails($value);
                            if ($parsed['error'] !== '') {
                                // Reject: the previously stored value stays untouched.
                                wp_send_json_error(array('message' => $parsed['error']));
                            }
                            $transcripts_options[$field_name] = implode(', ', $parsed['emails']);
                        } else {
                            $transcripts_options[$field_name] = sanitize_text_field($value);
                        }
                    }

                    // Use direct database update to bypass any filters
                    global $wpdb;

                    // Serialize the options array
                    $serialized = maybe_serialize($transcripts_options);

                    // Check if the option already exists in the database
                    $existing = $wpdb->get_var("SELECT option_id FROM {$wpdb->options} WHERE option_name = 'mxchat_transcripts_options'");

                    if ($existing) {
                        // Option exists, do an update
                        $result = $wpdb->update(
                            $wpdb->options,
                            array('option_value' => $serialized),
                            array('option_name' => 'mxchat_transcripts_options'),
                            array('%s'),
                            array('%s')
                        );
                    } else {
                        // Option doesn't exist (new install), do an insert
                        $result = $wpdb->insert(
                            $wpdb->options,
                            array(
                                'option_name' => 'mxchat_transcripts_options',
                                'option_value' => $serialized,
                                'autoload' => 'yes'
                            ),
                            array('%s', '%s', '%s')
                        );
                    }

                    // Clear all caches after direct DB update
                    wp_cache_delete('mxchat_transcripts_options', 'options');
                    wp_cache_delete('alloptions', 'options');
                    wp_cache_flush();

                    wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
                    return;
                }
            }
            // Whole-chatbot global cap (sits in mxchat_options['rate_limits_global']).
            // Field names: mxchat_options[rate_limits_global][limit|timeframe|limit_custom]
            else if (strpos($name, 'mxchat_options[rate_limits_global]') !== false) {
                preg_match('/\[rate_limits_global\]\[(.*?)\]/', $name, $matches);
                if (isset($matches[1])) {
                    $setting_key = $matches[1];
                    if (!isset($options['rate_limits_global']) || !is_array($options['rate_limits_global'])) {
                        $options['rate_limits_global'] = array('limit' => 'unlimited', 'timeframe' => 'daily');
                    }
                    if ($setting_key === 'limit') {
                        // Selection from the preset dropdown. If __custom__, resolve from limit_custom; otherwise store directly.
                        if ($value === '__custom__') {
                            $custom = isset($options['rate_limits_global']['limit_custom']) ? (string) $options['rate_limits_global']['limit_custom'] : '';
                            if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
                                $options['rate_limits_global']['limit'] = $custom;
                            }
                            // else leave existing limit untouched until the custom value arrives
                        } else {
                            $options['rate_limits_global']['limit'] = $value;
                        }
                    } elseif ($setting_key === 'limit_custom') {
                        $clean = preg_replace('/[^0-9]/', '', (string) $value);
                        $options['rate_limits_global']['limit_custom'] = $clean;
                        // Mirror a valid custom value into limit UNCONDITIONALLY (plan-74eb86).
                        // The custom number input is only editable when the dropdown is on
                        // "Custom…" (the toggle JS hides it for presets/unlimited) and autosave
                        // sends one field per change event, so a limit_custom change only fires
                        // in custom mode — there is no preset to clobber. The old guard required
                        // limit to already be non-preset, which it isn't on a first-time custom
                        // entry (the limit=__custom__ event arrives before limit_custom is set),
                        // so the value never landed in limit on the first save and reverted on refresh.
                        if ($clean !== '' && (int) $clean >= 1) {
                            $options['rate_limits_global']['limit'] = $clean;
                        }
                    } elseif ($setting_key === 'timeframe') {
                        $allowed_tf = array('hourly','daily','weekly','monthly');
                        $options['rate_limits_global']['timeframe'] = in_array($value, $allowed_tf, true) ? $value : 'daily';
                    }
                }
            }
            // First check for rate limits settings
            else if (strpos($name, 'mxchat_options[rate_limits]') !== false) {
                //error_log('MXChat Save: Detected rate_limits field: ' . $name);

                // Extract role ID and setting from the name
                preg_match('/\[rate_limits\]\[(.*?)\]\[(.*?)\]/', $name, $matches);
                //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));

                if (isset($matches[1]) && isset($matches[2])) {
                    $role_id = $matches[1];
                    $setting_key = $matches[2]; // limit, timeframe, message, or limit_custom

                    //error_log('MXChat Save: Role ID = ' . $role_id . ', Setting Key = ' . $setting_key);

                    // Initialize rate_limits if it doesn't exist
                    if (!isset($options['rate_limits'])) {
                       // //error_log('MXChat Save: Initializing rate_limits array');
                        $options['rate_limits'] = [];
                    }

                    // Initialize role settings if it doesn't exist
                    if (!isset($options['rate_limits'][$role_id])) {
                        //error_log('MXChat Save: Initializing rate_limits for role: ' . $role_id);
                        $options['rate_limits'][$role_id] = [
                            'limit' => ($role_id === 'logged_out') ? '10' : '100',
                            'timeframe' => 'daily',
                            'message' => 'Rate limit exceeded. Please try again later.'
                        ];
                    }

                    if ($setting_key === 'limit') {
                        if ($value === '__custom__') {
                            // Pull the integer from limit_custom that may have arrived (or will arrive).
                            $custom = isset($options['rate_limits'][$role_id]['limit_custom']) ? (string) $options['rate_limits'][$role_id]['limit_custom'] : '';
                            if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
                                $options['rate_limits'][$role_id]['limit'] = $custom;
                            }
                        } else {
                            $options['rate_limits'][$role_id]['limit'] = $value;
                        }
                    } elseif ($setting_key === 'limit_custom') {
                        $clean = preg_replace('/[^0-9]/', '', (string) $value);
                        $options['rate_limits'][$role_id]['limit_custom'] = $clean;
                        // Mirror a valid custom value into limit UNCONDITIONALLY — same reasoning
                        // as the global branch above (plan-74eb86). The per-role custom input is
                        // only editable in custom mode and autosave is one-field-per-change, so
                        // this never clobbers a preset; it fixes the first-time-save revert.
                        if ($clean !== '' && (int) $clean >= 1) {
                            $options['rate_limits'][$role_id]['limit'] = $clean;
                        }
                    } else {
                        // Update the specific setting (timeframe, message)
                        $options['rate_limits'][$role_id][$setting_key] = $value;
                    }
                    //error_log('MXChat Save: Updated rate_limits[' . $role_id . '][' . $setting_key . '] = ' . $value);
                } else {
                    //error_log('MXChat Save: Failed to parse rate_limits pattern: ' . $name);
                }
            }
            // Then check for role rate limits (old format)
            else if (strpos($name, 'mxchat_options[role_rate_limits]') !== false) {
                //error_log('MXChat Save: Processing role_rate_limits field: ' . $name);
                // Extract role ID from the name
                preg_match('/\[role_rate_limits\]\[(.*?)\]/', $name, $matches);
                //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));

                if (isset($matches[1])) {
                    $role_id = $matches[1];
                    // Initialize role_rate_limits if it doesn't exist
                    if (!isset($options['role_rate_limits'])) {
                        //error_log('MXChat Save: Initializing role_rate_limits array');
                        $options['role_rate_limits'] = [];
                    }
                    // Update the specific role's rate limit
                    $options['role_rate_limits'][$role_id] = sanitize_text_field($value);
                    //error_log('MXChat Save: Updated role_rate_limits[' . $role_id . '] = ' . $value);
                } else {
                    //error_log('MXChat Save: Failed to parse role_rate_limits pattern: ' . $name);
                }
            }
            // Handle toggles - check both extracted field_name and original name for toggle detection
            else if (strpos($field_name, 'toggle') !== false || in_array($field_name, [
                'chat_persistence_toggle',
                'privacy_toggle',
                'complianz_toggle',
                'chat_toolbar_toggle',
                'show_pdf_upload_button',
                'show_word_upload_button',
                'enable_streaming_toggle',
                'contextual_awareness_toggle',
                'citation_links_toggle',
                'enable_email_block',
                'enable_name_field',
                'enable_consent_checkbox',
                'consent_checkbox_required',
                'custom_provider_for_embeddings',
                'custom_provider_for_images',
                'print_button_enabled',
                'reset_chat_enabled'
            ])) {
                //error_log('MXChat Save: Processing toggle: ' . $field_name);
                $options[$field_name] = ($value === 'on') ? 'on' : 'off';
            } else {
                //error_log('MXChat Save: Processing standard field: ' . $field_name);
                // Store all other values directly using the extracted field name
                $options[$field_name] = $value;
            }
            break;
    }

    // Save all updates to the options array
    $updated = update_option('mxchat_options', $options);
    //error_log('MXChat Save: Update result: ' . ($updated ? 'success' : 'unchanged') . ' for field: ' . $name);
    //error_log('MXChat Save: Updated options array: ' . print_r($options, true));

    // Log the save action if debug mode is enabled
    if ( class_exists( 'MxChat_Admin' ) ) {
        MxChat_Admin::mxchat_log_debug(
            'settings_save',
            sprintf( 'Field saved: %s', $field_name ),
            array(
                'field'   => $field_name,
                'updated' => $updated,
            )
        );
    }

    // Always return success even if WordPress says nothing changed
    // (which happens when the value is the same as before)
    wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
}

/**
 * Save the selected bot for knowledge base operations
 */
public function mxchat_save_selected_bot() {
    // Check nonce
    if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
        wp_send_json_error('Invalid nonce');
    }
    
    // Check permissions
    if (!current_user_can('manage_options')) {
        wp_send_json_error('Unauthorized');
    }
    
    $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
    
    // Save as user meta for the current user
    $user_id = get_current_user_id();
    update_user_meta($user_id, 'mxchat_selected_knowledge_bot', $bot_id);
    
    // Also save as an option for site-wide default
    update_option('mxchat_current_knowledge_bot', $bot_id);
    
    // No cache clearing needed since we removed caching
    
    wp_send_json_success(array(
        'message' => 'Bot selection saved',
        'bot_id' => $bot_id
    ));
}

    /**
     * Handles AJAX request for saving chat settings
     */
     public function mxchat_save_prompts_setting_callback() {
         check_ajax_referer('mxchat_prompts_setting_nonce');

         if (!current_user_can('manage_options')) {
             wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
         }

         $name = isset($_POST['name']) ? $_POST['name'] : '';
         $value = isset($_POST['value']) ? stripslashes($_POST['value']) : '';

         //error_log('[MXCHAT-PROMPTS] Saving setting: ' . $name . ' = ' . $value);

         if (empty($name)) {
             wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
         }

// Handle Pinecone settings - BYPASS WORDPRESS SANITIZATION
if (strpos($name, 'mxchat_pinecone_addon_options') !== false) {
    //error_log('[MXCHAT-PROMPTS] Processing Pinecone setting: ' . $name);

    // Extract the field name
    if (preg_match('/mxchat_pinecone_addon_options\[([^\]]+)\]/', $name, $matches)) {
        $field_name = $matches[1];
        //error_log('[MXCHAT-PROMPTS] Extracted field name: ' . $field_name);

        // Get current options directly from database - NO WordPress filters
        global $wpdb;
        $current_options_raw = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
                'mxchat_pinecone_addon_options'
            )
        );

        // FIX: Handle the case where the option doesn't exist yet
        if ($current_options_raw === null) {
            // Option doesn't exist, create it with default values
            $current_options = array(
                'mxchat_use_pinecone' => '0',
                'mxchat_pinecone_api_key' => '',
                'mxchat_pinecone_host' => '',
                'mxchat_pinecone_index' => '',
                'mxchat_pinecone_environment' => ''
            );
            //error_log('[MXCHAT-PROMPTS] Option does not exist, creating with defaults');
        } else {
            // Unserialize the raw data
            $current_options = maybe_unserialize($current_options_raw);
            if (!is_array($current_options)) {
                // Fallback to defaults if unserialization fails
                $current_options = array(
                    'mxchat_use_pinecone' => '0',
                    'mxchat_pinecone_api_key' => '',
                    'mxchat_pinecone_host' => '',
                    'mxchat_pinecone_index' => '',
                    'mxchat_pinecone_environment' => ''
                );
                //error_log('[MXCHAT-PROMPTS] Failed to unserialize, using defaults');
            }
        }

        //error_log('[MXCHAT-PROMPTS] Current options from DB: ' . print_r($current_options, true));

        // Update the specific field with proper sanitization
        switch ($field_name) {
            case 'mxchat_use_pinecone':
                $new_value = ($value === '1') ? '1' : '0';
                break;
            case 'mxchat_pinecone_api_key':
            case 'mxchat_pinecone_host':
            case 'mxchat_pinecone_index':
            case 'mxchat_pinecone_environment':
                $new_value = sanitize_text_field($value);
                if ($field_name === 'mxchat_pinecone_host') {
                    $new_value = str_replace(['https://', 'http://'], '', $new_value);
                }
                break;
            case 'mxchat_pinecone_top_k':
                // d0cae1: out-of-range and junk normalize to the default 50 —
                // same clamp the read site applies.
                $top_k = absint($value);
                $new_value = (string) (($top_k >= 1 && $top_k <= 1000) ? $top_k : 50);
                break;
            default:
                wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]);
        }

        $current_options[$field_name] = $new_value;
        //error_log('[MXCHAT-PROMPTS] New value for ' . $field_name . ': "' . $new_value . '"');
        //error_log('[MXCHAT-PROMPTS] Updated options: ' . print_r($current_options, true));

        // Save directly to database to bypass WordPress sanitization
        $serialized_options = maybe_serialize($current_options);
        
        // FIX: Use INSERT ... ON DUPLICATE KEY UPDATE or separate INSERT/UPDATE logic
        $option_exists = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
                'mxchat_pinecone_addon_options'
            )
        );

        if ($option_exists > 0) {
            // Update existing option
            $save_result = $wpdb->update(
                $wpdb->options,
                array('option_value' => $serialized_options),
                array('option_name' => 'mxchat_pinecone_addon_options'),
                array('%s'),
                array('%s')
            );
            //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
        } else {
            // Insert new option
            // Credential option — must NOT autoload (holds the Pinecone secret;
            // autoloaded rows are read into memory on every request).
            $save_result = $wpdb->insert(
                $wpdb->options,
                array(
                    'option_name' => 'mxchat_pinecone_addon_options',
                    'option_value' => $serialized_options,
                    'autoload' => 'off'
                ),
                array('%s', '%s', '%s')
            );
            //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
        }

        // Clear any WordPress option cache to ensure get_option() returns fresh data
        wp_cache_delete('mxchat_pinecone_addon_options', 'options');

        // IMPROVED VERIFICATION - Check if the database operation succeeded
        if ($save_result !== false) {
            // Double-check by reading fresh from database
            $verification_raw = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
                    'mxchat_pinecone_addon_options'
                )
            );
            $verification_options = maybe_unserialize($verification_raw);
            $verified_value = isset($verification_options[$field_name]) ? $verification_options[$field_name] : 'NOT_FOUND';

            //error_log('[MXCHAT-PROMPTS] Final verification - Expected: "' . $new_value . '", Got: "' . $verified_value . '"');

            // Use loose comparison (==) instead of strict (===) to avoid type issues
            if ($verified_value == $new_value || $save_result > 0) {
                wp_send_json_success(['message' => esc_html__('Pinecone setting saved', 'mxchat')]);
            } else {
                // Still return success if the DB operation worked, even if verification is quirky
                //error_log('[MXCHAT-PROMPTS] Verification mismatch but DB operation succeeded');
                wp_send_json_success(['message' => esc_html__('Pinecone setting saved (DB success)', 'mxchat')]);
            }
        } else {
            wp_send_json_error(['message' => esc_html__('Database save failed', 'mxchat')]);
        }
    } else {
        wp_send_json_error(['message' => esc_html__('Invalid field name format', 'mxchat')]);
    }

    return; // Exit here for Pinecone settings
}
         // Handle auto-sync settings (existing functionality)
         if (strpos($name, 'mxchat_auto_sync_') === 0) {
             $value = ($value === 'on' || $value === '1') ? '1' : '0';
             $updated = update_option($name, $value);

             if ($updated || get_option($name) === $value) {
                 wp_send_json_success(['message' => esc_html__('Auto-sync setting saved', 'mxchat')]);
             } else {
                 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
             }
         }

         // Handle chunking settings - use direct DB access to bypass WordPress filters
         if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') {
             global $wpdb;

             // Get current options directly from database
             $current_options_raw = $wpdb->get_var(
                 $wpdb->prepare(
                     "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
                     'mxchat_options'
                 )
             );

             $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array();
             if (!is_array($options)) {
                 $options = array();
             }

             // Update the specific chunking field
             if ($name === 'mxchat_chunking_enabled') {
                 $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true);
             } elseif ($name === 'mxchat_chunk_size') {
                 $options['chunk_size'] = max(1000, min(10000, intval($value)));
             }

             // Save directly to database
             $serialized_options = maybe_serialize($options);

             $option_exists = $wpdb->get_var(
                 $wpdb->prepare(
                     "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
                     'mxchat_options'
                 )
             );

             if ($option_exists > 0) {
                 $save_result = $wpdb->update(
                     $wpdb->options,
                     array('option_value' => $serialized_options),
                     array('option_name' => 'mxchat_options'),
                     array('%s'),
                     array('%s')
                 );
             } else {
                 $save_result = $wpdb->insert(
                     $wpdb->options,
                     array(
                         'option_name' => 'mxchat_options',
                         'option_value' => $serialized_options,
                         'autoload' => 'yes'
                     ),
                     array('%s', '%s', '%s')
                 );
             }

             // Clear object cache for this option
             wp_cache_delete('mxchat_options', 'options');

             if ($save_result !== false) {
                 wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]);
             } else {
                 wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]);
             }
             return;
         }

         // Handle ACF field exclusion toggles
         if (strpos($name, 'mxchat_acf_field_') === 0) {
             // The identifier after the prefix is the ACF field KEY (unique per
             // field), not the field name — names are shared across groups and
             // collide (plan 30e81f). Reject anything that isn't key-shaped so a
             // stale pre-3.2.20 page (or its exit beacon) posting a bare name
             // can't write junk into the key-based list.
             $field_key = str_replace('mxchat_acf_field_', '', $name);
             if (!preg_match('/^field_[A-Za-z0-9_\-]+$/', $field_key)) {
                 wp_send_json_error(['message' => esc_html__('Invalid ACF field identifier', 'mxchat')]);
                 return;
             }
             $is_enabled = ($value === 'on' || $value === '1');

             // Get current excluded fields
             $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
             if (!is_array($excluded_fields)) {
                 $excluded_fields = array();
             }

             if ($is_enabled) {
                 // Remove from exclusion list (field should be included)
                 $excluded_fields = array_values(array_diff($excluded_fields, array($field_key)));
                 // Lazy legacy-name conversion: if this field's NAME is still
                 // stored (its group was inactive when the 30e81f migration
                 // ran), including this one field must not silently include
                 // its same-named twins — swap the name entry for the keys of
                 // every OTHER field currently wearing that name.
                 $excluded_fields = $this->mxchat_expand_legacy_acf_name_entry($excluded_fields, $field_key);
             } else {
                 // Add to exclusion list (field should be excluded)
                 if (!in_array($field_key, $excluded_fields, true)) {
                     $excluded_fields[] = $field_key;
                 }
             }

             $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields);

             if ($updated || true) { // Always report success since the state may already be correct
                 wp_send_json_success([
                     'message' => $is_enabled
                         ? esc_html__('Field will be included in imports', 'mxchat')
                         : esc_html__('Field will be excluded from imports', 'mxchat')
                 ]);
             } else {
                 wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]);
             }
             return;
         }

         // Handle custom post meta whitelist
         if ($name === 'mxchat_custom_meta_whitelist') {
             $updated = update_option('mxchat_custom_meta_whitelist', sanitize_textarea_field($value));

             if ($updated || true) { // Always report success since the state may already be correct
                 wp_send_json_success([
                     'message' => esc_html__('Custom meta whitelist saved', 'mxchat')
                 ]);
             } else {
                 wp_send_json_error(['message' => esc_html__('Failed to save custom meta whitelist', 'mxchat')]);
             }
             return;
         }

         // Handle other prompts options (autoload false — can hold the Pinecone secret)
         $options = get_option('mxchat_prompts_options', []);
         $options[$name] = $value;
         $updated = update_option('mxchat_prompts_options', $options, false);

         if ($updated) {
             wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
         } else {
             wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
         }
     }

    /**
     * If the field behind $included_key still has its NAME stored in the
     * exclusion list (a legacy entry the 30e81f migration could not resolve
     * because the group was inactive), replace that name with the keys of
     * every OTHER current field wearing it. Including one field must never
     * silently include its same-named twins — that would be the original
     * collision bug in reverse, in the unsafe (privacy-losing) direction.
     */
    private function mxchat_expand_legacy_acf_name_entry($excluded_fields, $included_key) {
        if (!function_exists('acf_get_field')) {
            return $excluded_fields;
        }
        $field = acf_get_field($included_key);
        if (!$field || empty($field['name'])) {
            return $excluded_fields;
        }
        $field_name = $field['name'];
        if (!in_array($field_name, $excluded_fields, true)) {
            return $excluded_fields;
        }
        $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
        foreach ($this->mxchat_acf_keys_for_name($field_name) as $twin_key) {
            if ($twin_key !== $included_key && !in_array($twin_key, $excluded_fields, true)) {
                $excluded_fields[] = $twin_key;
            }
        }
        return $excluded_fields;
    }

    /**
     * Keys of every currently-registered top-level ACF field with this name.
     */
    private function mxchat_acf_keys_for_name($field_name) {
        $keys = array();
        if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
            return $keys;
        }
        foreach (acf_get_field_groups() as $group) {
            $group_fields = acf_get_fields($group['key']);
            if (empty($group_fields)) {
                continue;
            }
            foreach ($group_fields as $field) {
                if (isset($field['name'], $field['key']) && $field['name'] === $field_name) {
                    $keys[] = $field['key'];
                }
            }
        }
        return $keys;
    }

    /**
     * Group-level ACF toggle (plan bf57e0): sets every field in one ACF field
     * group included or excluded in a SINGLE option write. The client must
     * never loop the per-field endpoint for this — get_option → modify →
     * update_option once per field from twenty concurrent requests is a
     * lost-update race that silently drops most of the group.
     */
    public function mxchat_acf_toggle_group_callback() {
        check_ajax_referer('mxchat_prompts_setting_nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions', 'mxchat')], 403);
            return;
        }

        if (!function_exists('acf_get_fields')) {
            wp_send_json_error(['message' => esc_html__('ACF is not active', 'mxchat')]);
            return;
        }

        $group_key = isset($_POST['group_key']) ? sanitize_text_field(wp_unslash($_POST['group_key'])) : '';
        if (!preg_match('/^group_[A-Za-z0-9_\-]+$/', $group_key)) {
            wp_send_json_error(['message' => esc_html__('Invalid ACF group identifier', 'mxchat')]);
            return;
        }
        $state = isset($_POST['state']) ? sanitize_text_field(wp_unslash($_POST['state'])) : '';
        $include = ($state === 'on' || $state === '1');

        // Resolve the group's fields SERVER-side — a client-supplied key list
        // is not trusted. This is the same call the settings UI lists from,
        // so the toggle covers exactly the rendered set (top-level fields).
        $group_fields = acf_get_fields($group_key);
        if (empty($group_fields)) {
            wp_send_json_error(['message' => esc_html__('No fields found for this group', 'mxchat')]);
            return;
        }

        $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
        if (!is_array($excluded_fields)) {
            $excluded_fields = array();
        }

        $touched = array();
        foreach ($group_fields as $field) {
            if (empty($field['key'])) {
                continue;
            }
            if ($include) {
                $excluded_fields = array_values(array_diff($excluded_fields, array($field['key'])));
                $excluded_fields = $this->mxchat_expand_legacy_acf_name_entry($excluded_fields, $field['key']);
            } elseif (!in_array($field['key'], $excluded_fields, true)) {
                $excluded_fields[] = $field['key'];
            }
            $touched[] = array(
                'name'  => 'mxchat_acf_field_' . $field['key'],
                'value' => $include ? 'on' : 'off',
            );
        }

        // The one write — the whole point of this endpoint.
        update_option('mxchat_acf_excluded_fields', array_values($excluded_fields));

        wp_send_json_success([
            'message' => $include
                ? esc_html__('All fields in this group will be included in imports', 'mxchat')
                : esc_html__('All fields in this group will be excluded from imports', 'mxchat'),
            'fields'  => $touched,
        ]);
    }

    /**
     * Handles AJAX request for Pinecone settings migration
     */
     public function ajax_migrate_pinecone_settings() {
         // Verify nonce
         if (!wp_verify_nonce($_POST['_ajax_nonce'] ?? '', 'mxchat_save_setting_nonce')) {
             wp_send_json_error('Invalid nonce');
         }

         // Check permissions
         if (!current_user_can('manage_options')) {
             wp_send_json_error('Unauthorized access');
         }

         // Check if old Pinecone addon options exist
         $old_options = get_option('mxchat_pinecone_addon_options', array());

         if (empty($old_options)) {
             wp_send_json_success(array('migrated' => false, 'message' => 'No old settings found'));
         }

         // Get current core plugin options
         $current_options = get_option('mxchat_pinecone_addon_options', array());

         // Only migrate if core options are empty or if explicitly requested
         $should_migrate = empty($current_options) ||
                          (empty($current_options['mxchat_pinecone_api_key']) && !empty($old_options['mxchat_pinecone_api_key']));

         if ($should_migrate) {
             // Migrate settings with proper sanitization
             $migrated_options = array(
                 'mxchat_use_pinecone' => $old_options['mxchat_use_pinecone'] ?? '0',
                 'mxchat_pinecone_api_key' => sanitize_text_field($old_options['mxchat_pinecone_api_key'] ?? ''),
                 'mxchat_pinecone_host' => sanitize_text_field($old_options['mxchat_pinecone_host'] ?? ''),
                 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''),
                 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '')
             );

             update_option('mxchat_pinecone_addon_options', $migrated_options, false);

             wp_send_json_success(array(
                 'migrated' => true,
                 'message' => 'Settings migrated successfully from Pinecone add-on'
             ));
         } else {
             wp_send_json_success(array(
                 'migrated' => false,
                 'message' => 'Settings already exist in core plugin'
             ));
         }
     }


    // ========================================
    // LICENSE AJAX HANDLERS
    // ========================================

/**
 * Validates and activates chat license via AJAX
 */
public function mxchat_handle_activate_license() {
    // Check nonce
    if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
        wp_send_json_error(esc_html__('Invalid security token', 'mxchat'));
        return;
    }
    
    // Verify user capabilities
    if (!current_user_can('manage_options')) {
        wp_send_json_error(esc_html__('Unauthorized access', 'mxchat'));
        return;
    }
    
    $license_key = isset($_POST['mxchat_activation_key']) ? sanitize_text_field($_POST['mxchat_activation_key']) : '';
    $customer_email = isset($_POST['mxchat_pro_email']) ? sanitize_email($_POST['mxchat_pro_email']) : '';
    
    if (empty($license_key) || empty($customer_email)) {
        wp_send_json_error(esc_html__('Email or License Key is missing', 'mxchat'));
        return;
    }
    
    $product_id = 'MxChatPRO';
    $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain
    
    // Call WooCommerce Software API for activation (not just validation)
    $response = wp_remote_get(
        add_query_arg(
            array(
                'wc-api' => 'software-api',
                'request' => 'activation',
                'email' => $customer_email,
                'license_key' => $license_key,
                'product_id' => $product_id,
                'instance' => $domain, // THIS IS KEY - include the domain as instance
                'platform' => 'wordpress' // Optional but good to include
            ),
            'https://mxchat.ai/'
        ),
        array(
            'timeout' => 60,
            'sslverify' => true
        )
    );
    
    if (is_wp_error($response)) {
        $error_message = $response->get_error_message();
        //error_log('MxChat License Activation Error: ' . $error_message);
        wp_send_json_error(esc_html__('Activation failed due to a server error: ', 'mxchat') . $error_message);
        return;
    }
    
    $response_code = wp_remote_retrieve_response_code($response);
    $body = wp_remote_retrieve_body($response);
    
    // Log response for debugging
    //error_log('MxChat License Response Code: ' . $response_code);
    //error_log('MxChat License Response Body: ' . $body);
    
    if ($response_code !== 200) {
        wp_send_json_error(esc_html__('Server returned error code: ', 'mxchat') . $response_code);
        return;
    }
    
    $data = json_decode($body);
    
    if ($data && isset($data->activated) && $data->activated) {
        // Success - save local options
        update_option('mxchat_license_status', 'active');
        update_option('mxchat_pro_email', $customer_email);
        update_option('mxchat_activation_key', $license_key);
        delete_option('mxchat_license_error');
        
        // Also track on your website (this is your existing domain tracking)
        $this->track_domain_on_website($license_key, $customer_email, $domain);
        
        wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat')));
    } else {
        $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat');
        update_option('mxchat_license_status', 'inactive');
        update_option('mxchat_license_error', $error_message);
        
        //error_log('MxChat Activation failed: ' . $error_message);
        wp_send_json_error($error_message);
    }
}

/**
 * Track domain on your website (separate from WooCommerce activation)
 */
private function track_domain_on_website($license_key, $email, $domain) {
    // This calls your website's tracking API
    wp_remote_post('https://mxchat.ai/mxchat-api/activate-license', array(
        'body' => array(
            'mxchat_pro_email' => $email,
            'mxchat_activation_key' => $license_key,
            'domain' => $domain
        ),
        'timeout' => 10,
        'sslverify' => true
    ));
}

    /**
     * Validates license via AJAX with email and key
     */
    public function mxchat_check_license_status() {
        // Verify nonce
        if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
            wp_send_json_error('Security check failed');
            return;
        }
    
        // Add isset checks for safety
        $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
        $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
    
        // Check if this license is actually active in your system
        $is_active = (get_option('mxchat_license_status') === 'active' &&
                      get_option('mxchat_pro_email') === $email &&
                      get_option('mxchat_activation_key') === $key);
    
        wp_send_json(array(
            'is_active' => $is_active
        ));
    }
     
/**
 * Handle license deactivation - Complete version for plugin
 */
function mxchat_deactivate_license() {
    // Add debugging
    //error_log('MxChat deactivate function called');
    
    // Check nonce
    if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
        //error_log('MxChat deactivate: Nonce check failed');
        wp_send_json_error('Security check failed.');
        return;
    }

    // plan-mxchat-20260731-c63fb6 — nonce is not authorization. Without this,
    // any authenticated user holding the nonce could revoke the site's PRO
    // licence. Every sibling handler in this file already checks.
    if (!current_user_can('manage_options')) {
        wp_send_json_error(esc_html__('Unauthorized', 'mxchat'), 403);
        return;
    }

    //error_log('MxChat deactivate: Nonce check passed');
    
    $license_key = get_option('mxchat_activation_key');
    $email = get_option('mxchat_pro_email');
    $domain = parse_url(home_url(), PHP_URL_HOST);
    
    //error_log('MxChat deactivate: License: ' . $license_key . ', Email: ' . $email . ', Domain: ' . $domain);
    
    if (empty($license_key) || empty($email)) {
        //error_log('MxChat deactivate: No active license found');
        wp_send_json_error('No active license found.');
        return;
    }
    
    // Clear local license data first
    delete_option('mxchat_license_status');
    delete_option('mxchat_pro_email');
    delete_option('mxchat_activation_key');
    delete_option('mxchat_license_error');
    
    //error_log('MxChat deactivate: Local data cleared');
    
    // Notify your website's API to properly deactivate
    $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array(
        'body' => array(
            'license_key' => $license_key,
            'email' => $email,
            'domain' => $domain
        ),
        'timeout' => 15,
        'sslverify' => true
    ));
    
    if (is_wp_error($response)) {
        //error_log('MxChat deactivate: Server error - ' . $response->get_error_message());
        wp_send_json_success(array(
            'message' => 'License deactivated locally. Server could not be contacted to free activation slot.',
            'server_notified' => false
        ));
        return;
    }
    
    $response_body = wp_remote_retrieve_body($response);
    $response_data = json_decode($response_body, true);
    
    //error_log('MxChat deactivate: Server response - ' . $response_body);
    
    if (isset($response_data['success']) && $response_data['success']) {
        //error_log('MxChat deactivate: Success with server notification');
        wp_send_json_success(array(
            'message' => 'License deactivated successfully. Activation slot has been freed up.',
            'server_notified' => true
        ));
    } else {
        //error_log('MxChat deactivate: Server responded but deactivation may have failed');
        wp_send_json_success(array(
            'message' => 'License deactivated locally. Please check your account dashboard to verify the activation was freed.',
            'server_notified' => false
        ));
    }
}


    // ========================================
    // ACTIONS & INTENTS AJAX HANDLERS
    // ========================================

    /**
     * Validates nonce and returns JSON error on failure
     */
     public function mxchat_toggle_action() {
         // Check nonce
         if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_actions_nonce')) {
             wp_send_json_error(array('message' => 'Security check failed'));
             return;
         }

         // Check permissions
         if (!current_user_can('manage_options')) {
             wp_send_json_error(array('message' => 'Permission denied'));
             return;
         }

         // Validate params
         $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
         $enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false;

         if (!$intent_id) {
             wp_send_json_error(array('message' => 'Invalid action ID'));
             return;
         }

         // Update the intent/action status in the database
         global $wpdb;
         $table_name = $wpdb->prefix . 'mxchat_intents';

         // Using the 'enabled' field - add this field if it doesn't exist
         $result = $wpdb->update(
             $table_name,
             array('enabled' => $enabled ? 1 : 0),
             array('id' => $intent_id),
             array('%d'),
             array('%d')
         );

         if ($result === false) {
             wp_send_json_error(array('message' => 'Database error'));
             return;
         }

         wp_send_json_success();
     }


    /**
     * Validates permissions for AJAX request handling
     */
     public function mxchat_update_intent_threshold() {
         // Check permissions
         if (!current_user_can('manage_options')) {
             if (wp_doing_ajax()) {
                 wp_send_json_error(array('message' => 'Unauthorized user'));
                 return;
             }
             wp_die(esc_html__('Unauthorized user', 'mxchat'));
         }

         // Verify nonce
         check_admin_referer('mxchat_update_intent_threshold_nonce');

         // Process the update if we have valid data
         if (isset($_POST['intent_id'], $_POST['intent_threshold'])) {
             global $wpdb;
             $table_name = $wpdb->prefix . 'mxchat_intents';
             $intent_id = intval($_POST['intent_id']);
             $threshold_percentage = max(70, min(95, intval($_POST['intent_threshold'])));
             $similarity_threshold = $threshold_percentage / 100;

             $result = $wpdb->update(
                 $table_name,
                 ['similarity_threshold' => $similarity_threshold],
                 ['id' => $intent_id],
                 ['%f'],
                 ['%d']
             );

             // Handle AJAX requests
             if (wp_doing_ajax()) {
                 if ($result === false) {
                     wp_send_json_error(array('message' => 'Failed to update threshold'));
                 } else {
                     wp_send_json_success(array('threshold' => $threshold_percentage));
                 }
                 return;
             }
         }

         // Redirect for regular form submissions
         wp_safe_redirect(admin_url('admin.php?page=mxchat-actions&updated=true'));
         exit;
     }

    // ========================================
    // HELPER METHODS
    // ========================================

    /**
     * Returns a specific nonce action string
     */
     private function mxchat_get_nonce_action() {
         return 'mxchat_license_nonce';
     }

    /**
     * Check API key status for all providers
     */
    public function mxchat_check_api_keys() {
        // Check nonce
        if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
            wp_send_json_error('Invalid nonce');
        }

        // Check permissions
        if (!current_user_can('manage_options')) {
            wp_send_json_error('Unauthorized');
        }

        // Get current options
        $options = get_option('mxchat_options', array());

        // Check which API keys are present
        $api_key_status = array(
            'openai' => !empty($options['api_key']),
            'claude' => !empty($options['claude_api_key']),
            'xai' => !empty($options['xai_api_key']),
            'deepseek' => !empty($options['deepseek_api_key']),
            'gemini' => !empty($options['gemini_api_key']),
            'openrouter' => !empty($options['openrouter_api_key']),
            'voyage' => !empty($options['voyage_api_key'])
        );

        wp_send_json_success($api_key_status);
    }

    // ========================================
    // DEBUG & OPTIMIZATION AJAX HANDLERS
    // ========================================

    /**
     * Toggle debug mode on/off
     */
    public function mxchat_toggle_debug_mode_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        $enabled = isset( $_POST['enabled'] ) && $_POST['enabled'] === 'on';

        $options = get_option( 'mxchat_options', array() );

        if ( $enabled ) {
            $options['debug_mode'] = 'on';
            update_option( 'mxchat_options', $options );
            MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode enabled' );
        } else {
            // Log before disabling
            MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode disabled' );
            $options['debug_mode'] = 'off';
            update_option( 'mxchat_options', $options );
        }

        wp_send_json_success( array(
            'message' => $enabled ? esc_html__( 'Debug mode enabled', 'mxchat' ) : esc_html__( 'Debug mode disabled', 'mxchat' ),
            'enabled' => $enabled,
        ) );
    }

    /**
     * Get the debug log entries
     */
    public function mxchat_get_debug_log_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        $log = MxChat_Admin::mxchat_get_debug_log();

        wp_send_json_success( array(
            'log'   => $log,
            'count' => count( $log ),
        ) );
    }

    /**
     * Clear the debug log
     */
    public function mxchat_clear_debug_log_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        MxChat_Admin::mxchat_clear_debug_log();

        // Log that the log was cleared (this will be the first entry in the new log)
        MxChat_Admin::mxchat_log_debug( 'debug_log', 'Debug log cleared by user' );

        wp_send_json_success( array( 'message' => esc_html__( 'Debug log cleared', 'mxchat' ) ) );
    }

    /**
     * Export settings as JSON
     */
    public function mxchat_export_settings_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        $export = MxChat_Admin::mxchat_export_settings();

        // Log the export
        MxChat_Admin::mxchat_log_debug( 'settings_export', 'Settings exported by user' );

        wp_send_json_success( array(
            'settings' => $export,
            'filename' => 'mxchat-settings-' . gmdate( 'Y-m-d-His' ) . '.json',
        ) );
    }

    /**
     * Reset all settings to defaults
     */
    public function mxchat_reset_all_settings_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        // Require confirmation code
        $confirmation = isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : '';

        if ( strtoupper( $confirmation ) !== 'RESET' ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Invalid confirmation code. Please type RESET to confirm.', 'mxchat' ) ) );
        }

        // Perform the reset
        MxChat_Admin::mxchat_reset_all_settings();

        wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) );
    }

    /**
     * Reset the global rate-limit usage counter to zero on demand.
     *
     * Zeroes the WP option mxchat_chat_limit_<bot>_global that the integrator
     * increments per message, then returns a freshly-formatted readout string
     * so the settings page can update without a reload. Does NOT change any
     * enforcement config — purely clears the running counter.
     */
    public function mxchat_reset_global_rate_limit_callback() {
        // Verify nonce
        if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
        }

        // Check permissions
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
        }

        // Resolve the per-bot counter key the same way the integrator does.
        $bot_id   = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default';
        $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id );
        if ( $safe_bot === '' ) {
            $safe_bot = 'default';
        }
        $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global';

        $now = time();
        update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) );

        // Recompute the display string so the front-end can update in place.
        $all_options = get_option( 'mxchat_options', array() );
        $global_cfg  = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] )
            ? $all_options['rate_limits_global']
            : array();
        $limit_raw   = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited';
        // Defensive: if a raw __custom__ ever slips through, fall back to the custom value.
        if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) {
            $limit_raw = (string) $global_cfg['limit_custom'];
        }
        $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily';
        $windows   = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 );
        $window    = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400;
        $reset_at  = $now + $window;
        $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0;

        $text = sprintf(
            /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */
            esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ),
            number_format_i18n( 0 ),
            number_format_i18n( $limit_int ),
            number_format_i18n( $limit_int ),
            human_time_diff( $now, $reset_at )
        );

        wp_send_json_success( array(
            'count'    => 0,
            'limit'    => $limit_int,
            'left'     => $limit_int,
            'reset_at' => $reset_at,
            'pct'      => 0,
            'text'     => $text,
            'message'  => esc_html__( 'Usage counter reset.', 'mxchat' ),
        ) );
    }

}

// Initialize the AJAX handler
new MxChat_Ajax_Handler();

```
