<?php
namespace EasyChat\Controller;

use EasyChat\Library\EasyTokensGiftProvisioner;
use EasyChat\Library\EasyTokensClientHeaders;
use EasyChat\Library\CentralGatewayClient;
use EasyChat\Model\Setting as SettingModel;
use EasyChat\Model\Manager;
use EasyChat\Library\I18n;
use EasyChat\Library\Ai\VectorStorageGuard;
use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Http\ProxyPolicy;
use EasyChat\Library\Ai\AiResponseTimingPolicy;
use EasyChat\Library\Ai\PromptDefaults;
use EasyChat\Library\Catalog\CatalogIndexScheduler;
use EasyChat\Library\Catalog\CatalogSemanticBootstrap;
use EasyChat\Library\Catalog\CatalogSearchService;
use EasyChat\Library\Catalog\CatalogPublicUrlService;
use EasyChat\Library\Catalog\CmsCatalogCapability;
use EasyChat\Library\Catalog\CmsCatalogConnectionResolver;
use EasyChat\Library\WidgetDomainPolicy;
use EasyChat\Library\Messenger\SchedulerCliSupport;
use EasyChat\Library\Messenger\AvitoProvider;
use EasyChat\Library\Messenger\AvitoHistorySyncService;
use EasyChat\Library\Messenger\AvitoHealthService;
use EasyChat\Library\Messenger\YoulaProvider;
use EasyChat\Library\Messenger\VkProvider;
use EasyChat\Library\Messenger\TelegramProvider;
use EasyChat\Library\Messenger\SecretBox;
use EasyChat\Library\Messenger\ContactAvatarService;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Library\Queue\SchedulerHealthService;
use EasyChat\Library\Support\LogTailReader;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\Session as SessionModel;
use EasyChat\Model\Cms\CmsFactory;
use EasyChat\View\HtmxFragmentRenderer;
use EasyChat\View\SettingFragments;
use EasyChat\View\AccessibleColor;

class Setting extends Base {

    protected $layouts = 'layout';

    public function __construct($registry) {
        parent::__construct($registry);
         
        if (!isset(\EasyChat\Library\RequestContext::session()['manager_id'])) {
            header('Location: index.php?route=auth');
            exit;
        }
        if (\EasyChat\Library\RequestContext::session()['manager_role'] !== 'admin') {
            header('Location: index.php?route=index');
            exit;
        }
    }

    public function index() {
        $this->vars['title'] = 'Настройки | EasyChat';
        
        $model_setting = new SettingModel($this->registry);
        $model_manager = new Manager($this->registry);

         
        $settings = $model_setting->getAll();

         
        $defaults = [
            'chat_status'           => '1',
            'chat_telegram_logging' => '0',
            'chat_avito_logging'    => '0',
            'chat_youla_logging'     => '0',
            'chat_messenger_avatars_status' => '1',
            'chat_ai_status'        => '0',
            'chat_ai_provider'      => 'easytokens',
            'chat_ai_key'           => '',
            'chat_ai_model'         => 'easylite',
            'chat_ai_prompt'        => PromptDefaults::current(),
            'chat_ai_delay'         => '2',
            AiResponseTimingPolicy::SETTING_MODE => AiResponseTimingPolicy::MODE_STRICT_DELAY,
            AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS => (string)AiResponseTimingPolicy::DEFAULT_OFFLINE_DELAY_SECONDS,
            'chat_ai_live_typing'   => '1',
            'chat_voice_transcription_status' => '1',
            'chat_ai_host'          => '',
            'chat_ai_temp'          => '0.6',
            'chat_ai_logging'       => '0',
            'chat_ai_suggestions_status' => '0',
            'chat_ai_copilot_ghost_status' => '0',
            'chat_ai_suggestions_default_mode' => 'on',
            'chat_ai_suggestions_use_knowledge' => '1',
            'chat_ai_suggestions_context_limit' => '12',
            'chat_ai_avito_item_context_status' => '1',
            'chat_ai_youla_item_context_status' => '1',
            'chat_ai_avito_rag_mode' => 'auto',
            'chat_ai_avito_tools_mode' => 'auto',
            'chat_ai_avito_alternatives_mode' => 'on_request',
            'chat_widget_color'     => '#6366f1',
            'chat_on_accent_mode'   => AccessibleColor::MODE_AUTO,
            'chat_widget_theme'     => 'light',
            'chat_widget_position'  => 'right',
            'chat_widget_offset_x'  => '20',
            'chat_widget_offset_y'  => '20',
            'chat_widget_welcome'   => 'Здравствуйте! Чем я могу вам помочь?',
            'chat_widget_title'     => 'Задайте вопрос специалисту',
            'chat_widget_text'      => "Есть вопросы?\nНапишите нам в чат",
            'chat_admin_locale'     => 'ru',
            'chat_widget_locale'    => 'ru',
            'chat_widget_locale_auto' => '1',
            'chat_locale_source'    => 'manual',
            'chat_locale_manually_selected' => '0',
            'chat_widget_show_operators' => '1',
            'chat_widget_social_hover' => '1',
            'chat_dialog_tags_status'  => '1',
            'chat_dialog_notes_status' => '1',
            'chat_widget_phone'     => '',
            'chat_widget_whatsapp'  => '',
            'chat_widget_email'     => '',
            'chat_widget_telegram'  => '',
            'chat_widget_max'       => '',
            'chat_widget_vk'        => '',
            'chat_app_master_token' => '',
            'chat_install_id'       => '',
            'chat_install_secret'   => '',
            'chat_central_api'      => EasyTokensGiftProvisioner::defaultGatewayApi(),
            'chat_cms_type'         => 'none',
            'chat_cms_prefix'       => '',
            'chat_cms_search_limit'   => '5',
            'chat_cms_index_products' => '0',
            'chat_cms_index_pages'    => '0',
            'chat_productsearch'      => '0',
            CatalogSemanticBootstrap::SEARCH_AUTO_OPT_OUT_KEY => '0',
            'chat_cms_availability_policy' => 'smart',
            'chat_cms_show_exact_quantity' => '0',
            'chat_cms_semantic_enabled' => '0',
            'chat_cms_search_recommendation_dismissed' => '0',
            'chat_cms_logging'        => '0',
            'chat_ai_theme_status'  => '0',
            'chat_ai_theme_desc'    => "1. Приветствия, прощания, благодарности и стандартные вежливые фразы (например: здравствуйте, спасибо, всего доброго).\n2. Вопросы о товарах, услугах, ассортименте, характеристиках, ценах и наличии.\n3. Условия заказа, способы оплаты, варианты и сроки доставки, условия возврата.\n4. Контакты компании, график работы, адрес офиса/склада, телефон или способы связи.\n5. Передача клиентом своих контактных данных (номер телефона, email, имя для связи) для обратной связи с менеджером.",
            'chat_ai_theme_reject'  => 'Извините, я являюсь консультантом этого интернет-магазина и могу помочь вам только с вопросами по нашей продукции или работе магазина.',
            'chat_max_file_size'    => '5',
            'chat_ai_extract_status' => '1',
            'chat_fcm_logging'          => '0',
            'chat_api_auth_logging'     => '0',
            'chat_dadata_token'         => '',
            'chat_embedding_use_name'    => '1',
            'chat_embedding_use_desc'    => '1',
            'chat_embedding_use_attrs'   => '0',
            'chat_embedding_use_options' => '0',
            'chat_embedding_use_model'   => '0',
            'chat_embedding_use_sku'     => '1',
            'chat_embedding_dimension'   => '2',
            'chat_proxy_status'          => '0',
            'chat_proxy_host'            => '',
            'chat_proxy_port'            => '',
            'chat_proxy_login'           => '',
            'chat_proxy_password'        => '',
            'chat_proxy_routes_json'      => '',
            'chat_easytokens_gift_status'    => '',
            'chat_easytokens_gift_error'     => '',
            'chat_easytokens_gift_issued_at' => '',
            'chat_easytokens_gift_tokens'    => '',
            'chat_easytokens_gift_email'     => '',
            'chat_easytokens_gift_offer_seen' => '0',
            SettingModel::REVISION_KEY => '0',
        ];

        foreach ($defaults as $key => $def) {
            $this->vars[$key] = isset($settings[$key]) ? $settings[$key] : $def;
        }
        $this->vars['chat_widget_position'] = $this->normalizeWidgetPosition($this->vars['chat_widget_position']);
        $this->vars['chat_widget_offset_x'] = (string)$this->normalizeWidgetOffset($this->vars['chat_widget_offset_x']);
        $this->vars['chat_widget_offset_y'] = (string)$this->normalizeWidgetOffset($this->vars['chat_widget_offset_y']);
        $widget_domain_policy = WidgetDomainPolicy::fromSettings($model_setting);
        $this->vars['chat_widget_base_domain'] = $widget_domain_policy->baseDomain();
        $this->vars['chat_widget_install_host'] = WidgetDomainPolicy::installHost();
        $this->vars['chat_ai_provider'] = $this->normalizeAiProvider($this->vars['chat_ai_provider']);
        $this->vars['chat_ai_model'] = $this->normalizeAiModelForProvider($this->vars['chat_ai_provider'], $this->vars['chat_ai_model']);
        $this->vars[AiResponseTimingPolicy::SETTING_MODE] = AiResponseTimingPolicy::normalizeMode($this->vars[AiResponseTimingPolicy::SETTING_MODE]);
        $this->vars[AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS] = (string)AiResponseTimingPolicy::normalizeOfflineDelaySeconds($this->vars[AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS]);
        $catalog_connection = CmsCatalogConnectionResolver::resolve(
            $this->registry->get('db'),
            $settings,
            defined('EASYCHAT_CMS_EDITION') ? (string)EASYCHAT_CMS_EDITION : ''
        );
        if (!empty($catalog_connection['ready'])) {
            $this->vars['chat_cms_type'] = (string)$catalog_connection['type'];
            $this->vars['chat_cms_prefix'] = (string)$catalog_connection['prefix'];
            if ((string)($settings['chat_cms_type'] ?? '') !== $this->vars['chat_cms_type']
                || (string)($settings['chat_cms_prefix'] ?? '') !== $this->vars['chat_cms_prefix']) {
                 
                 
                 
                $model_setting->setManyAtomic(array(
                    'chat_cms_type' => $this->vars['chat_cms_type'],
                    'chat_cms_prefix' => $this->vars['chat_cms_prefix'],
                ));
            }
        }
        $this->vars['catalog_connection'] = $catalog_connection;
        $catalog_capability = CmsCatalogCapability::inspect(
            $this->registry->get('db'),
            (string)$this->vars['chat_cms_type'],
            (string)$this->vars['chat_cms_prefix']
        );
        $this->vars['catalog_capability'] = $catalog_capability;
        $this->vars['catalog_capability_message'] = $this->catalogCapabilityMessage($catalog_capability);
        $this->vars['catalog_search_recommendation_visible'] = empty($catalog_capability['ready'])
            ? false
            : ((string)$this->vars['chat_productsearch'] === '0'
                && trim((string)$this->vars['chat_ai_key']) !== ''
                && (string)$this->vars['chat_cms_search_recommendation_dismissed'] !== '1');

        $proxy_policy = new ProxyPolicy($this->vars);
        $proxy_routes = $proxy_policy->routes();
        $this->vars['chat_proxy_routes_json'] = ProxyPolicy::routesToJson($proxy_routes);
        $this->vars['chat_proxy_password_saved'] = trim((string)($this->vars['chat_proxy_password'] ?? '')) !== '';
        $this->vars['chat_proxy_password'] = '';
        $this->vars['proxy_route_groups'] = ProxyPolicy::groups();
        $this->vars['proxy_route_profiles'] = ProxyPolicy::profiles();
        $this->vars['proxy_route_states'] = $proxy_policy->routeStates();

         
        if (empty($this->vars['chat_app_master_token'])) {
            $master_token = bin2hex(random_bytes(16));
            $model_setting->set('chat_app_master_token', $master_token);
            $this->vars['chat_app_master_token'] = $master_token;
        }

        if (empty($this->vars['chat_install_id'])) {
            $install_id = 'inst_' . bin2hex(random_bytes(16));
            $model_setting->set('chat_install_id', $install_id);
            $this->vars['chat_install_id'] = $install_id;
        }

        if (empty($this->vars['chat_install_secret'])) {
            $install_secret = 'instsec_' . bin2hex(random_bytes(32));
            $model_setting->set('chat_install_secret', $install_secret);
            $this->vars['chat_install_secret'] = $install_secret;
        }

        $this->vars['easychat_settings_nonce'] = $this->ensureSettingsNonce();



         
        $is_wordpress_org_mode = defined('EASYCHAT_WPORG_MODE') && EASYCHAT_WPORG_MODE;
        $cron_mode = defined('EASYCHAT_CMS_CRON_MODE') ? (string)EASYCHAT_CMS_CRON_MODE : 'standalone';
        if ($is_wordpress_org_mode) {
            $cron_mode = 'wordpress_org';
        }
        if (!in_array($cron_mode, array('standalone', 'wordpress_org', 'webasyst_cli', 'bitrix_agent', 'opencart_cron'), true)) {
            $cron_mode = 'standalone';
        }

        $this->vars['cron_mode'] = $cron_mode;
        $this->vars['scheduler_health'] = SchedulerHealthService::state($model_setting->getAll());
        $this->vars['wp_cron_disabled'] = defined('DISABLE_WP_CRON') && DISABLE_WP_CRON;
        $this->vars['wp_cron_url'] = $is_wordpress_org_mode && function_exists('site_url')
            ? site_url('wp-cron.php?doing_wp_cron')
            : '';
        $cms_edition = defined('EASYCHAT_CMS_EDITION') ? (string)EASYCHAT_CMS_EDITION : 'standalone';
        $this->vars['cms_edition'] = $cms_edition;
        $cms_labels = array(
            'standalone' => 'Generic',
            'opencart' => 'OpenCart',
            'wordpress' => 'WordPress',
            'wordpress_org' => 'WordPress.org',
            'bitrix' => 'Bitrix',
            'webasyst_store' => 'Webasyst',
        );
        $this->vars['cms_edition_label'] = isset($cms_labels[$cms_edition]) ? $cms_labels[$cms_edition] : 'CMS';
        $this->vars['webasyst_cron_command'] = defined('EASYCHAT_WEBASYST_CLI_COMMAND')
            ? EASYCHAT_WEBASYST_CLI_COMMAND
            : 'php /path/to/webasyst/cli.php shop easychatPluginQueue';
        $document_root = $this->detectBitrixDocumentRoot();
        $this->vars['bitrix_cron_events_command'] = $document_root !== ''
            ? 'php ' . $document_root . DS . 'bitrix' . DS . 'modules' . DS . 'main' . DS . 'tools' . DS . 'cron_events.php'
            : 'php /path/to/site/bitrix/modules/main/tools/cron_events.php';
        $this->vars['opencart_cron_url'] = defined('HTTP_SERVER')
            ? rtrim(HTTP_SERVER, '/') . '/index.php?route=cron/cron'
            : '/index.php?route=cron/cron';

        if ($cron_mode !== 'standalone') {
            $this->vars['cron_path'] = '';
            $this->vars['cron_url'] = '';
            $this->vars['cron_cli_command'] = '';
            $this->vars['cron_cli_simple_command'] = '';
            $this->vars['cron_cli_runtime'] = array();
            $this->vars['cron_cli_profile'] = array();
            $this->vars['cron_flock_runtime'] = array();
            $this->vars['cron_protected_chain'] = array();
        } else {
            $this->vars['cron_path'] = SITE_PATH . DS . 'cron.php';
            $this->vars['cron_url'] = HTTP_SERVER . 'cron.php?secret=' . (defined('CRON_SECRET') ? CRON_SECRET : '');
            $cron_command_profile = SchedulerCliSupport::commandProfile($this->vars['cron_path']);
            $this->vars['cron_cli_profile'] = $cron_command_profile;
            $this->vars['cron_cli_runtime'] = (array)($cron_command_profile['runtime'] ?? []);
            $this->vars['cron_flock_runtime'] = (array)($cron_command_profile['flock'] ?? []);
            $this->vars['cron_protected_chain'] = (array)($cron_command_profile['protected_chain'] ?? []);
            $this->vars['cron_cli_command'] = (string)($cron_command_profile['protected_command'] ?? '');
            $this->vars['cron_cli_simple_command'] = (string)($cron_command_profile['simple_command'] ?? '');
        }

        $this->template->view('setting');
    }

     
    public function save() {
        header('Content-Type: application/json; charset=utf-8');

        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }

        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $model_setting = new SettingModel($this->registry);
        $previous_settings = $model_setting->getAll();
        $previous_semantic_enabled = (string)($previous_settings['chat_cms_semantic_enabled'] ?? '0') === '1';
        $expected_revision = max(0, (int)(\EasyChat\Library\RequestContext::post()['settings_revision'] ?? -1));
        if (!isset(\EasyChat\Library\RequestContext::post()['settings_revision'])) {
            $this->rejectSettingsRevisionJson((int)($previous_settings[SettingModel::REVISION_KEY] ?? 0));
        }

         
        $keys = [
            'chat_status',
            'chat_ai_status',
            'chat_ai_provider',
            'chat_ai_key',
            'chat_ai_model',
            'chat_ai_prompt',
            'chat_ai_delay',
            AiResponseTimingPolicy::SETTING_MODE,
            AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS,
            'chat_ai_live_typing',
            'chat_voice_transcription_status',
            'chat_ai_host',
            'chat_ai_temp',
            'chat_ai_logging',
            'chat_ai_suggestions_status',
            'chat_ai_copilot_ghost_status',
            'chat_ai_suggestions_default_mode',
            'chat_ai_suggestions_use_knowledge',
            'chat_ai_suggestions_context_limit',
            'chat_ai_avito_item_context_status',
            'chat_ai_avito_rag_mode',
            'chat_ai_avito_tools_mode',
            'chat_ai_avito_alternatives_mode',
            'chat_widget_color',
            'chat_on_accent_mode',
            'chat_widget_theme',
            'chat_widget_position',
            'chat_widget_offset_x',
            'chat_widget_offset_y',
            'chat_widget_welcome',
            'chat_cms_type',
            'chat_cms_prefix',
            'chat_cms_search_limit',
            'chat_cms_index_products',
            'chat_cms_index_pages',
            'chat_avito_logging',
            'chat_productsearch',
            'chat_cms_availability_policy',
            'chat_cms_show_exact_quantity',
            'chat_cms_semantic_enabled',
            'chat_cms_search_recommendation_dismissed',
            'chat_cms_logging',
            'chat_ai_theme_status',
            'chat_ai_theme_desc',
            'chat_ai_theme_reject',
            'chat_ai_extract_status',
            'chat_max_file_size',
            'chat_central_api',
            'chat_widget_title',
            'chat_widget_text',
            'chat_admin_locale',
            'chat_widget_locale',
            'chat_widget_locale_auto',
            'chat_widget_show_operators',
            'chat_widget_social_hover',
            'chat_widget_phone',
            'chat_widget_whatsapp',
            'chat_widget_email',
            'chat_widget_telegram',
            'chat_widget_max',
            'chat_widget_vk',
            'chat_install_id',
            'chat_fcm_logging',
            'chat_api_auth_logging',
            'chat_dadata_token',
            'chat_embedding_use_name',
            'chat_embedding_use_desc',
            'chat_embedding_use_attrs',
            'chat_embedding_use_options',
            'chat_embedding_use_model',
            'chat_embedding_use_sku',
            'chat_embedding_dimension',
            'chat_proxy_status',
            'chat_proxy_host',
            'chat_proxy_port',
            'chat_proxy_login',
            'chat_proxy_password',
            'chat_proxy_routes_json',
            'chat_dialog_tags_status',
            'chat_dialog_notes_status',
        ];

        $checkbox_keys = [
            'chat_status',
            'chat_ai_status',
            'chat_ai_live_typing',
            'chat_voice_transcription_status',
            'chat_ai_logging',
            'chat_ai_suggestions_status',
            'chat_ai_copilot_ghost_status',
            'chat_ai_suggestions_use_knowledge',
            'chat_ai_avito_item_context_status',
            'chat_productsearch',
            'chat_cms_show_exact_quantity',
            'chat_cms_semantic_enabled',
            'chat_cms_search_recommendation_dismissed',
            'chat_cms_logging',
            'chat_avito_logging',
            'chat_ai_theme_status',
            'chat_ai_extract_status',
            'chat_fcm_logging',
            'chat_api_auth_logging',
            'chat_widget_locale_auto',
            'chat_widget_show_operators',
            'chat_widget_social_hover',
            'chat_dialog_tags_status',
            'chat_dialog_notes_status',
            'chat_embedding_use_name',
            'chat_embedding_use_desc',
            'chat_embedding_use_attrs',
            'chat_embedding_use_options',
            'chat_embedding_use_model',
            'chat_embedding_use_sku',
            'chat_proxy_status',
            'chat_cms_index_products',
            'chat_cms_index_pages',
        ];

        $settings_data = [];
        foreach ($keys as $key) {
            if (in_array($key, $checkbox_keys)) {
                $val = isset(\EasyChat\Library\RequestContext::post()[$key]) ? \EasyChat\Library\RequestContext::post()[$key] : '0';
            } else {
                $val = isset(\EasyChat\Library\RequestContext::post()[$key]) ? \EasyChat\Library\RequestContext::post()[$key] : '';
            }
            if (is_array($val)) {
                $val = json_encode($val);
            }
            $settings_data[$key] = trim((string)$val);
        }

        $central_api = $this->normalizeCentralApi($settings_data['chat_central_api'] ?? '');
        $settings_data['chat_central_api'] = $central_api;
        $settings_data['chat_admin_locale'] = I18n::normalizeLocale($settings_data['chat_admin_locale'] ?? 'ru');
        $settings_data['chat_widget_locale'] = I18n::normalizeLocale($settings_data['chat_widget_locale'] ?? 'ru');
        $settings_data['chat_locale_source'] = 'manual';
        $settings_data['chat_locale_manually_selected'] = '1';
        $settings_data['chat_widget_position'] = $this->normalizeWidgetPosition($settings_data['chat_widget_position'] ?? 'right');
        $settings_data['chat_widget_color'] = AccessibleColor::normalizeHex($settings_data['chat_widget_color'] ?? '#6366f1');
        $settings_data['chat_on_accent_mode'] = AccessibleColor::normalizeMode($settings_data['chat_on_accent_mode'] ?? AccessibleColor::MODE_AUTO);
        $settings_data['chat_widget_offset_x'] = (string)$this->normalizeWidgetOffset($settings_data['chat_widget_offset_x'] ?? '20');
        $settings_data['chat_widget_offset_y'] = (string)$this->normalizeWidgetOffset($settings_data['chat_widget_offset_y'] ?? '20');
        $settings_data['chat_ai_provider'] = $this->normalizeAiProvider($settings_data['chat_ai_provider'] ?? 'easytokens');
        $settings_data['chat_ai_delay'] = (string)AiResponseTimingPolicy::normalizeDelayMinutes($settings_data['chat_ai_delay'] ?? AiResponseTimingPolicy::DEFAULT_DELAY_MINUTES);
        $settings_data[AiResponseTimingPolicy::SETTING_MODE] = AiResponseTimingPolicy::normalizeMode($settings_data[AiResponseTimingPolicy::SETTING_MODE] ?? AiResponseTimingPolicy::MODE_STRICT_DELAY);
        $settings_data[AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS] = (string)AiResponseTimingPolicy::normalizeOfflineDelaySeconds($settings_data[AiResponseTimingPolicy::SETTING_OFFLINE_DELAY_SECONDS] ?? AiResponseTimingPolicy::DEFAULT_OFFLINE_DELAY_SECONDS);
        $settings_data['chat_ai_model'] = $this->normalizeAiModelForProvider(
            $settings_data['chat_ai_provider'],
            $settings_data['chat_ai_model'] ?? ''
        );
        $settings_data['chat_proxy_routes_json'] = ProxyPolicy::routesToJson(
            ProxyPolicy::normalizeRoutes($settings_data['chat_proxy_routes_json'] ?? '', true)
        );
        $settings_data['chat_proxy_port'] = $this->normalizeProxyPort($settings_data['chat_proxy_port'] ?? '');
        if (!empty(\EasyChat\Library\RequestContext::post()['chat_proxy_password_clear'])) {
            $settings_data['chat_proxy_password'] = '';
        } elseif (($settings_data['chat_proxy_password'] ?? '') === '') {
            $settings_data['chat_proxy_password'] = (string)$model_setting->get('chat_proxy_password', '');
        }
        $settings_data['chat_ai_suggestions_default_mode'] = $this->normalizeAiSuggestionsDefaultMode($settings_data['chat_ai_suggestions_default_mode'] ?? 'on');
        $settings_data['chat_ai_suggestions_context_limit'] = (string)$this->normalizeAiSuggestionsContextLimit($settings_data['chat_ai_suggestions_context_limit'] ?? '12');
         
         
        $settings_data['chat_cms_type'] = $this->normalizeCatalogCmsType($previous_settings['chat_cms_type'] ?? 'none');
        try {
            $settings_data['chat_cms_prefix'] = $this->normalizeCatalogPrefix($previous_settings['chat_cms_prefix'] ?? '');
        } catch (\InvalidArgumentException $e) {
            $this->rejectJson(422, 'invalid_cms_prefix', $e->getMessage());
        }
        $settings_data['chat_cms_availability_policy'] = $this->normalizeCatalogAvailabilityPolicy($settings_data['chat_cms_availability_policy'] ?? 'smart');
        $settings_data['chat_cms_search_limit'] = (string)max(1, min(50, (int)($settings_data['chat_cms_search_limit'] ?? 5)));
        $settings_data = CatalogSemanticBootstrap::applyCatalogChoice($previous_settings, $settings_data);
        $search_enabled = (int)($settings_data['chat_productsearch'] ?? 0) > 0;
        if ($search_enabled && CmsCatalogCapability::shouldValidateOnSettingsSave($previous_settings, $settings_data)) {
            $capability = CmsCatalogCapability::inspect(
                $this->registry->get('db'),
                $settings_data['chat_cms_type'],
                $settings_data['chat_cms_prefix']
            );
            if (empty($capability['ready'])) {
                $this->rejectJson(422, 'catalog_not_ready', 'Не удалось подтвердить товарный каталог. Проверьте тип CMS и префикс таблиц.');
            }
        }
        $bootstrap_plan = array('enabled' => false, 'updates' => array());
        if (CatalogSemanticBootstrap::becameAiReady($previous_settings, $settings_data)) {
            $bootstrap_plan = CatalogSemanticBootstrap::planIfReady(
                $this->registry,
                array_merge($previous_settings, $settings_data),
                'ai_settings_ready'
            );
            if (!empty($bootstrap_plan['updates'])) {
                $settings_data = array_merge($settings_data, (array)$bootstrap_plan['updates']);
            }
        }

        $semantic_enabled = (string)($settings_data['chat_cms_semantic_enabled'] ?? '0') === '1';
        $source_changed = CatalogIndexScheduler::sourceSignature($previous_settings)
            !== CatalogIndexScheduler::sourceSignature($settings_data);
        $queue = new QueueManager($this->registry);
        if ($semantic_enabled && ($source_changed || !empty($bootstrap_plan['updates']))) {
             
             
            $settings_data['chat_cms_last_indexed_at'] = '';
            $settings_data['chat_cms_last_index_scheduled_at'] = date('c');
        }

        try {
            $write_result = $model_setting->compareAndSetMany(
                $expected_revision,
                $settings_data,
                function () use (
                    $queue,
                    $previous_semantic_enabled,
                    $semantic_enabled,
                    $source_changed,
                    $bootstrap_plan,
                    $settings_data
                ): void {
                    if ($previous_semantic_enabled && !$semantic_enabled) {
                        $queue->cancelPendingByDedupeKey(
                            CatalogIndexScheduler::DEDUPE_KEY,
                            'catalog_semantic_disabled'
                        );
                        return;
                    }
                    if (!$semantic_enabled || (!$source_changed && empty($bootstrap_plan['updates']))) {
                        return;
                    }
                    $queue->pushUnique(
                        'cms_index',
                        array(
                            'reason' => !empty($bootstrap_plan['updates'])
                                ? (string)($bootstrap_plan['queue_reason'] ?? 'ai_settings_ready')
                                : 'catalog_source_changed',
                            'source_signature' => CatalogIndexScheduler::sourceSignature($settings_data),
                        ),
                        CatalogIndexScheduler::DEDUPE_KEY,
                        0,
                        10
                    );
                }
            );
        } catch (\Throwable $e) {
            $this->rejectJson(500, 'settings_save_failed', 'Настройки не изменены: сервер не смог завершить атомарное сохранение.');
        }
        if (empty($write_result['success'])) {
            $this->rejectSettingsRevisionJson((int)($write_result['current_revision'] ?? 0));
        }

         
         
         
        $warning = '';
        if (trim((string)($settings_data['chat_ai_key'] ?? '')) !== '') {
            try {
                $dimension = $this->embeddingDimensionForProvider($settings_data['chat_ai_provider'] ?? 'gemini');
                VectorStorageGuard::ensureCompatible($this->registry, VECTOR_STORAGE, $dimension);
            } catch (\Throwable $e) {
                 
                 
                $warning = 'Настройки сохранены. Подготовка расширенного поиска продолжится через Cron.';
            }
        }

        \EasyChat\Library\RequestContext::session()['easychat_locale'] = $settings_data['chat_admin_locale'];
        echo json_encode(array(
            'success' => true,
            'settings_revision' => (int)$write_result['revision'],
            'catalog' => CatalogSemanticBootstrap::publicState($settings_data),
            'warning' => $warning,
        ), JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function toggleAiStatus() {
        header('Content-Type: application/json; charset=utf-8');

        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }

        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $enabled = isset(\EasyChat\Library\RequestContext::post()['chat_ai_status'])
            && (string)\EasyChat\Library\RequestContext::post()['chat_ai_status'] === '1';

        $model_setting = new SettingModel($this->registry);
        $previous_settings = $model_setting->getAll();
        if (!isset(\EasyChat\Library\RequestContext::post()['settings_revision'])) {
            $this->rejectSettingsRevisionJson((int)($previous_settings[SettingModel::REVISION_KEY] ?? 0));
        }
        $expected_revision = max(0, (int)\EasyChat\Library\RequestContext::post()['settings_revision']);
        $settings_updates = array('chat_ai_status' => $enabled ? '1' : '0');
        $bootstrap_plan = array('enabled' => false, 'updates' => array());
        $candidate = array_merge($previous_settings, $settings_updates);
        if ($enabled && CatalogSemanticBootstrap::becameAiReady($previous_settings, $candidate)) {
            $bootstrap_plan = CatalogSemanticBootstrap::planIfReady($this->registry, $candidate, 'ai_status_enabled');
            if (!empty($bootstrap_plan['updates'])) {
                $settings_updates = array_merge($settings_updates, (array)$bootstrap_plan['updates']);
            }
        }

        $queue = new QueueManager($this->registry);
        try {
            $write_result = $model_setting->compareAndSetMany(
                $expected_revision,
                $settings_updates,
                function () use ($queue, $bootstrap_plan, $previous_settings, $settings_updates): void {
                    if (empty($bootstrap_plan['updates'])) {
                        return;
                    }
                    $effective = array_merge($previous_settings, $settings_updates);
                    $queue->pushUnique(
                        'cms_index',
                        array(
                            'reason' => (string)($bootstrap_plan['queue_reason'] ?? 'ai_status_enabled'),
                            'source_signature' => CatalogIndexScheduler::sourceSignature($effective),
                        ),
                        CatalogIndexScheduler::DEDUPE_KEY,
                        0,
                        10
                    );
                }
            );
        } catch (\Throwable $e) {
            $this->rejectJson(500, 'ai_status_save_failed', 'Автоответы ИИ не изменены: сервер не завершил сохранение.');
        }
        if (empty($write_result['success'])) {
            $this->rejectSettingsRevisionJson((int)($write_result['current_revision'] ?? 0));
        }

        echo json_encode([
            'success' => true,
            'enabled' => $enabled,
            'settings_revision' => (int)$write_result['revision'],
            'catalog' => CatalogSemanticBootstrap::publicState(array_merge($previous_settings, $settings_updates)),
            'message' => $enabled ? 'Автоответы ИИ включены.' : 'Автоответы ИИ выключены.',
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function settingsNonce() {
        header('Content-Type: application/json; charset=utf-8');

        echo json_encode([
            'success' => true,
            'settings_nonce' => $this->ensureSettingsNonce(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

     
    public function getLogs() {
        header('Content-Type: application/json; charset=utf-8');
        $this->releaseSessionLockForReadOnlyJsonResponse();
        
        $type = isset(\EasyChat\Library\RequestContext::get()['type']) ? \EasyChat\Library\RequestContext::get()['type'] : 'ai';
        $filename = $this->resolveLogFilename($type);
        if ($filename === '') {
            echo json_encode(['success' => false, 'error' => 'Недопустимый тип лога.']);
            exit;
        }
        
        $log_file = rtrim(DIR_LOGS, DS) . DS . $filename;
        if (!is_file($log_file)) {
            echo json_encode(['success' => true, 'log' => 'Лог пуст или еще не создан.']);
            exit;
        }

        $log_content = (new LogTailReader())->read($log_file);
        if ($log_content === null) {
            echo json_encode(['success' => false, 'error' => 'Не удалось прочитать файл лога.']);
            exit;
        }

        echo json_encode(['success' => true, 'log' => $log_content], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        exit;
    }

     
    public function clearLogs() {
        header('Content-Type: application/json; charset=utf-8');

        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }

        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $type = isset(\EasyChat\Library\RequestContext::get()['type']) ? \EasyChat\Library\RequestContext::get()['type'] : (isset(\EasyChat\Library\RequestContext::post()['type']) ? \EasyChat\Library\RequestContext::post()['type'] : 'ai');
        $filename = $this->resolveLogFilename($type);
        if ($filename === '') {
            echo json_encode(['success' => false, 'error' => 'Недопустимый тип лога.']);
            exit;
        }

        $log_file = rtrim(DIR_LOGS, DS) . DS . $filename;
        if (is_file($log_file)) {
            if (!is_writable($log_file) || file_put_contents($log_file, '', LOCK_EX) === false) {
                $this->rejectJson(500, 'log_clear_failed', 'Не удалось очистить лог. Проверьте права на запись в runtime log.');
            }
        }

        echo json_encode(['success' => true]);
        exit;
    }

    public function easytokensBalance() {
        header('Content-Type: application/json; charset=utf-8');

        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (!$this->verifySettingsNonce()) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $api_key = $this->resolveEasyTokensApiKey();
        if ($api_key === '') {
            echo json_encode(['success' => false, 'error' => 'Введите API key EasyTokens.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (!function_exists('curl_init')) {
            echo json_encode([
                'success' => false,
                'error' => 'На сервере недоступно PHP-расширение cURL.',
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $response = $this->externalHttp()->request('GET', 'https://api.easytokens.ru/v1/balance', [
            'headers' => EasyTokensClientHeaders::append([
                'Accept: application/json',
                'Authorization: Bearer ' . $api_key,
            ]),
            'connect_timeout' => 4,
            'timeout' => 8,
            'trust_profile' => 'easytokens',
        ]);
        $raw = $response['raw'];
        $http_code = (int)($response['http_code'] ?? 0);

        if (empty($response['transport_ok']) || $raw === '') {
            echo json_encode([
                'success' => false,
                'error' => 'Не удалось получить баланс EasyTokens.',
                'details' => !empty($response['tls_fallback_used']) ? 'tls_fallback' : (!empty($response['curl_error']) ? 'curl' : 'empty_response'),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $payload = json_decode($raw, true);
        if (!is_array($payload)) {
            echo json_encode([
                'success' => false,
                'error' => 'EasyTokens вернул некорректный ответ.',
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if ($http_code < 200 || $http_code >= 300) {
            $message = ($http_code === 401 || $http_code === 404)
                ? 'API key EasyTokens не найден или не принят.'
                : 'EasyTokens временно не вернул баланс.';
            echo json_encode([
                'success' => false,
                'error' => $message,
                'http_code' => $http_code,
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        unset($payload['api_token']);
        echo json_encode([
            'success' => true,
            'balance' => [
                'object' => isset($payload['object']) ? (string)$payload['object'] : 'balance',
                'tokens' => isset($payload['tokens']) ? (int)$payload['tokens'] : 0,
                'expend_tokens' => isset($payload['expend_tokens']) ? (int)$payload['expend_tokens'] : 0,
                'expend_query' => isset($payload['expend_query']) ? (int)$payload['expend_query'] : 0,
                'low_balance' => !empty($payload['low_balance']),
                'domain' => isset($payload['domain']) ? (string)$payload['domain'] : '',
                'created_at' => isset($payload['created_at']) ? (string)$payload['created_at'] : '',
            ],
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function easytokensModels() {
        header('Content-Type: application/json; charset=utf-8');

        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (!$this->verifySettingsNonce()) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $selected_model = isset(\EasyChat\Library\RequestContext::post()['selected_model'])
            ? $this->normalizeEasyTokensModelId(\EasyChat\Library\RequestContext::post()['selected_model'])
            : $this->normalizeEasyTokensModelId('');

        if (!function_exists('curl_init')) {
            echo json_encode($this->easyTokensModelsFallbackResponse($selected_model), JSON_UNESCAPED_UNICODE);
            exit;
        }

        $api_key = $this->resolveEasyTokensApiKey();
        $headers = ['Accept: application/json'];
        if ($api_key !== '') {
            $headers[] = 'Authorization: Bearer ' . $api_key;
        }
        $headers = EasyTokensClientHeaders::append($headers);

        $response = $this->externalHttp()->request('GET', 'https://api.easytokens.ru/v1/models', [
            'headers' => $headers,
            'connect_timeout' => 4,
            'timeout' => 8,
            'trust_profile' => 'easytokens',
        ]);
        $raw = $response['raw'];
        $http_code = (int)($response['http_code'] ?? 0);

        if (empty($response['transport_ok']) || $raw === '' || $http_code < 200 || $http_code >= 300) {
            echo json_encode($this->easyTokensModelsFallbackResponse($selected_model), JSON_UNESCAPED_UNICODE);
            exit;
        }

        $payload = json_decode($raw, true);
        if (!is_array($payload)) {
            echo json_encode($this->easyTokensModelsFallbackResponse($selected_model), JSON_UNESCAPED_UNICODE);
            exit;
        }

        $models = $this->normalizeEasyTokensModelsPayload($payload);
        if (empty($models)) {
            echo json_encode($this->easyTokensModelsFallbackResponse($selected_model), JSON_UNESCAPED_UNICODE);
            exit;
        }

        echo json_encode([
            'success' => true,
            'fallback' => false,
            'models' => $models,
            'selected_model' => $this->easyTokensModelExists($models, $selected_model) ? $selected_model : 'easylite',
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function easytokensTopup() {
        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            header('Location: https://easytokens.ru/buy/');
            exit;
        }

        if (!$this->verifySettingsNonce()) {
            http_response_code(403);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'Сессия настроек устарела. Обновите страницу EasyChat.';
            exit;
        }

        $api_key = $this->resolveEasyTokensApiKey();
        if ($api_key === '') {
            http_response_code(400);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'Введите API key EasyTokens или сначала получите подарочные токены.';
            exit;
        }

        if (!function_exists('curl_init')) {
            http_response_code(503);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'На сервере недоступно PHP-расширение cURL.';
            exit;
        }

        $domain = EasyTokensGiftProvisioner::publicDomain(defined('HTTP_SERVER') ? HTTP_SERVER : '');
        $payload = json_encode([
            'purpose' => 'topup',
            'source' => 'easychat',
            'domain' => $domain,
            'return_url' => defined('HTTP_SERVER') ? rtrim(HTTP_SERVER, '/') . '/index.php?route=setting' : '',
        ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $response = $this->externalHttp()->request('POST', 'https://api.easytokens.ru/v1/handoffs', [
            'body' => $payload,
            'headers' => EasyTokensClientHeaders::append([
                'Accept: application/json',
                'Content-Type: application/json',
                'Authorization: Bearer ' . $api_key,
            ]),
            'connect_timeout' => 4,
            'timeout' => 8,
            'trust_profile' => 'easytokens',
        ]);
        $raw = $response['raw'];
        $http_code = (int)($response['http_code'] ?? 0);

        $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
        $target = is_array($decoded) && isset($decoded['url']) ? (string)$decoded['url'] : '';
        if ($http_code < 200 || $http_code >= 300 || !$this->isAllowedEasyTokensTopupUrl($target)) {
            http_response_code($http_code === 401 || $http_code === 404 ? 401 : 503);
            header('Content-Type: text/plain; charset=utf-8');
            echo $http_code === 401 || $http_code === 404
                ? 'API key EasyTokens не найден или не принят.'
                : 'EasyTokens временно не создал безопасную ссылку пополнения.';
            exit;
        }

        header('Referrer-Policy: no-referrer');
        header('Location: ' . $target);
        exit;
    }

    public function provisionEasyTokensGift() {
        header('Content-Type: application/json; charset=utf-8');

        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (!$this->verifySettingsNonce()) {
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $model_setting = new SettingModel($this->registry);
        $result = EasyTokensGiftProvisioner::provisionWithModel($model_setting, [
            'claim_easytokens_gift' => '1',
            'admin_email' => isset(\EasyChat\Library\RequestContext::post()['admin_email']) ? (string)\EasyChat\Library\RequestContext::post()['admin_email'] : '',
            'edition' => 'standalone',
            'version' => defined('EASYCHAT_VERSION') ? EASYCHAT_VERSION : '1.0.0',
            'site_url' => HTTP_SERVER,
            'registry' => $this->registry,
        ]);

        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function markEasyTokensGiftOfferSeen() {
        header('Content-Type: application/json; charset=utf-8');

        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $model_setting = new SettingModel($this->registry);
        $model_setting->set('chat_easytokens_gift_offer_seen', '1');

        echo json_encode([
            'success' => true,
            'settings_nonce' => $this->ensureSettingsNonce(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function normalizeCentralApi($url) {
        $url = trim((string)$url);
        if ($url === '') {
            return '';
        }

        $parts = parse_url($url);
        if (!$parts || strtolower($parts['scheme'] ?? '') !== 'https' || empty($parts['host'])) {
            return '';
        }

        $base_url = rtrim($url, '/');
        $path = rtrim($parts['path'] ?? '', '/');

         
         
        if ($path === '' || $path === '/') {
            return $base_url . '/api/v1';
        }
        if ($path !== '/api/v1' && substr($path, -7) !== '/api/v1') {
            return $base_url . '/api/v1';
        }

        return $base_url;
    }

    private function normalizeWidgetPosition($value) {
        return ((string)$value === 'left') ? 'left' : 'right';
    }

    private function normalizeWidgetOffset($value) {
        $offset = (int)$value;
        if ($offset < 0) {
            return 0;
        }
        if ($offset > 200) {
            return 200;
        }
        return $offset;
    }

    private function normalizeProxyPort($value) {
        $value = trim((string)$value);
        if ($value === '') {
            return '';
        }
        $port = (int)$value;
        if ($port < 1 || $port > 65535) {
            return '';
        }
        return (string)$port;
    }

    private function normalizeAiProvider($value) {
        $value = (string)$value;
        return in_array($value, ['gemini', 'openai', 'easytokens'], true) ? $value : 'easytokens';
    }

    private function normalizeAiSuggestionsDefaultMode($value) {
        return ((string)$value === 'off') ? 'off' : 'on';
    }

    private function normalizeAiSuggestionsContextLimit($value) {
        $limit = (int)$value;
        if ($limit < 4) {
            return 4;
        }
        if ($limit > 12) {
            return 12;
        }
        return $limit;
    }

    private function normalizeAvitoRagMode($value) {
        $value = (string)$value;
        return in_array($value, ['auto', 'general_chats', 'always', 'off'], true) ? $value : 'auto';
    }

    private function normalizeAvitoToolsMode($value) {
        $value = (string)$value;
        return in_array($value, ['auto', 'pages_only', 'products_pages', 'off'], true) ? $value : 'auto';
    }

    private function normalizeAvitoAlternativesMode($value) {
        $value = (string)$value;
        return in_array($value, ['on_request', 'soft', 'off'], true) ? $value : 'on_request';
    }

    private function defaultAiModelForProvider($provider) {
        if ($provider === 'gemini') {
            return 'gemini-3.5-flash';
        }
        if ($provider === 'openai') {
            return 'gpt-4o-mini';
        }
        return 'easylite';
    }

    private function normalizeAiModelForProvider($provider, $value) {
        $provider = $this->normalizeAiProvider($provider);
        $value = trim((string)$value);

        if ($provider === 'easytokens') {
            return $this->normalizeEasyTokensModelId($value);
        }

        if (stripos($value, 'models/') === 0) {
            $value = substr($value, 7);
        }

        if ($value === '' || !$this->isValidExternalAiModelId($value)) {
            return $this->defaultAiModelForProvider($provider);
        }

        $lower = strtolower($value);
        if ($provider === 'gemini') {
            return (strpos($lower, 'gemini-') === 0)
                ? $value
                : $this->defaultAiModelForProvider($provider);
        }

        if ($lower === 'easylite' || strpos($lower, 'gemini-') === 0 || strpos($lower, 'models/gemini-') === 0) {
            return $this->defaultAiModelForProvider($provider);
        }

        return $value;
    }

    private function normalizeEasyTokensModelId($value) {
        $value = trim((string)$value);
        return $this->isValidEasyTokensModelId($value) ? $value : 'easylite';
    }

    private function isValidEasyTokensModelId($value) {
        return is_string($value) && preg_match('/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/', $value) === 1;
    }

    private function isValidExternalAiModelId($value) {
        return is_string($value) && preg_match('/^[A-Za-z0-9][A-Za-z0-9._:\/-]{0,127}$/', $value) === 1;
    }

    private function embeddingDimensionForProvider($provider) {
        if ($provider === 'easytokens') {
            return 1024;
        }
        if ($provider === 'gemini') {
            return 768;
        }
        return 1536;
    }

    private function defaultEasyTokensModels() {
        return [
            [
                'id' => 'easylite',
                'label' => 'Наша Нейросеть',
                'vision' => true,
                'vision_mode' => 'native',
            ],
        ];
    }

    private function easyTokensModelsFallbackResponse($selected_model) {
        return [
            'success' => true,
            'fallback' => true,
            'message' => 'Не удалось обновить список моделей, используется модель по умолчанию.',
            'models' => $this->defaultEasyTokensModels(),
            'selected_model' => 'easylite',
        ];
    }

    private function normalizeEasyTokensModelsPayload(array $payload) {
        $items = [];
        if (isset($payload['data']) && is_array($payload['data'])) {
            $items = $payload['data'];
        } elseif (array_keys($payload) === range(0, count($payload) - 1)) {
            $items = $payload;
        }

        $models = [];
        $seen = [];

        foreach ($items as $item) {
            if (is_string($item)) {
                $this->appendEasyTokensModel($models, $seen, $item, $item, []);
                continue;
            }
            if (!is_array($item)) {
                continue;
            }

            $id = isset($item['id']) ? (string)$item['id'] : (isset($item['model']) ? (string)$item['model'] : '');
            $label = '';
            foreach (['display_name', 'name', 'label'] as $label_key) {
                if (isset($item[$label_key]) && trim((string)$item[$label_key]) !== '') {
                    $label = trim((string)$item[$label_key]);
                    break;
                }
            }
            $capabilities = isset($item['capabilities']) && is_array($item['capabilities'])
                ? $item['capabilities']
                : [];
            $this->appendEasyTokensModel($models, $seen, $id, $label, $capabilities);
        }

        if (empty($models)) {
            return [];
        }

        if (!isset($seen['easylite'])) {
            array_unshift($models, [
                'id' => 'easylite',
                'label' => 'Наша Нейросеть',
                'vision' => true,
                'vision_mode' => 'native',
            ]);
        }

        return $models;
    }

    private function appendEasyTokensModel(array &$models, array &$seen, $id, $label, array $capabilities = []) {
        $id = trim((string)$id);
        if (!$this->isValidEasyTokensModelId($id) || isset($seen[$id])) {
            return;
        }

        $label = trim((string)$label);
        if ($id === 'easylite') {
            $label = 'Наша Нейросеть';
        }
        if ($label === '') {
            $label = $id;
        }

        $seen[$id] = true;
        $vision_mode = strtolower(trim((string)($capabilities['vision_mode'] ?? '')));
        if (!in_array($vision_mode, ['native', 'local_fallback', 'none'], true)) {
            $vision_mode = !empty($capabilities['vision']) ? 'native' : 'local_fallback';
        }
        $models[] = [
            'id' => $id,
            'label' => function_exists('mb_substr') ? mb_substr($label, 0, 120, 'UTF-8') : substr($label, 0, 120),
            'vision' => !empty($capabilities['vision']) || $vision_mode !== 'none',
            'vision_mode' => $vision_mode,
        ];
    }

    private function easyTokensModelExists(array $models, $model_id) {
        foreach ($models as $model) {
            if (isset($model['id']) && $model['id'] === $model_id) {
                return true;
            }
        }

        return false;
    }

    private function resolveEasyTokensApiKey() {
        $value = isset(\EasyChat\Library\RequestContext::post()['api_key']) ? (string)\EasyChat\Library\RequestContext::post()['api_key'] : '';
        if (trim($value) === '') {
            $model_setting = new SettingModel($this->registry);
            $value = (string)$model_setting->get('chat_ai_key', '');
        }

        $lines = preg_split('/\r\n|\r|\n/', $value);
        if (!is_array($lines)) {
            return trim($value);
        }

        foreach ($lines as $line) {
            $key = trim((string)$line);
            if ($key !== '') {
                return $key;
            }
        }

        return '';
    }

    private function isAllowedEasyTokensTopupUrl($url) {
        if (!is_string($url) || $url === '' || strpos($url, 'api_token=') !== false) {
            return false;
        }
        $parts = parse_url($url);
        if (!is_array($parts)) {
            return false;
        }
        $scheme = isset($parts['scheme']) ? strtolower((string)$parts['scheme']) : '';
        $host = isset($parts['host']) ? strtolower((string)$parts['host']) : '';
        $path = isset($parts['path']) ? (string)$parts['path'] : '';

        return $scheme === 'https'
            && ($host === 'easytokens.ru' || substr($host, -14) === '.easytokens.ru')
            && (strpos($path, '/buy') === 0 || strpos($path, '/lk') === 0);
    }

    private function normalizeCatalogAvailabilityPolicy($value) {
        $value = strtolower(trim((string)$value));
        return in_array($value, array('smart', 'in_stock', 'all_active'), true) ? $value : 'smart';
    }

    private function catalogCapabilityMessage(array $capability) {
        $reason = (string)($capability['reason'] ?? 'catalog_probe_failed');
        $messages = array(
            'ready' => 'Подключение проверено: быстрый поиск использует актуальные данные магазина.',
            'cms_not_supported' => 'Выберите CMS в дополнительных настройках.',
            'invalid_table_prefix' => 'Проверьте префикс таблиц CMS.',
            'catalog_tables_missing' => 'Не найдены обязательные таблицы товарного каталога.',
            'woocommerce_not_detected' => 'WordPress найден, но WooCommerce не обнаружен.',
            'woocommerce_probe_failed' => 'Не удалось проверить состояние WooCommerce.',
            'catalog_probe_failed' => 'Каталог временно не ответил на безопасную проверку.',
        );
        return $messages[$reason] ?? $messages['catalog_probe_failed'];
    }

    private function normalizeCatalogCmsType($value) {
        $value = strtolower(trim((string)$value));
        return in_array($value, array('none', 'opencart', 'wordpress', 'bitrix', 'webasyst'), true)
            ? $value
            : 'none';
    }

    private function normalizeCatalogPrefix($value) {
        $value = trim((string)$value);
        if (!preg_match('/^[A-Za-z0-9_]{0,64}$/', $value)) {
            throw new \InvalidArgumentException('Префикс таблиц может содержать только латинские буквы, цифры и знак подчёркивания.');
        }
        return $value;
    }

    private function verifySettingsNonce() {
        $expected = isset(\EasyChat\Library\RequestContext::session()['easychat_settings_nonce']) ? (string)\EasyChat\Library\RequestContext::session()['easychat_settings_nonce'] : '';
        $actual = isset(\EasyChat\Library\RequestContext::post()['settings_nonce']) ? (string)\EasyChat\Library\RequestContext::post()['settings_nonce'] : '';

        return $expected !== '' && $actual !== '' && hash_equals($expected, $actual);
    }

    private function ensureSettingsNonce() {
        $session =& \EasyChat\Library\RequestContext::session();
        if (empty($session['easychat_settings_nonce'])) {
            $session['easychat_settings_nonce'] = bin2hex(random_bytes(16));
        }

        return (string)$session['easychat_settings_nonce'];
    }

    private function rejectSettingsNonceJson() {
        http_response_code(403);
        echo json_encode([
            'success' => false,
            'error' => 'settings_nonce_expired',
            'message' => 'Сессия настроек устарела. Повторяем действие с новой сессией.',
            'settings_nonce' => $this->ensureSettingsNonce(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function rejectSettingsRevisionJson($current_revision) {
        http_response_code(409);
        echo json_encode(array(
            'success' => false,
            'error' => 'settings_changed',
            'message' => 'Настройки уже изменились в другой вкладке. Обновите страницу, чтобы не перезаписать новые значения.',
            'settings_revision' => max(0, (int)$current_revision),
        ), JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function rejectJson($status, $code, $message) {
        http_response_code((int)$status);
        echo json_encode([
            'success' => false,
            'error' => (string)$code,
            'message' => (string)$message,
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function externalHttp(): ExternalHttpClient {
        return new ExternalHttpClient($this->registry);
    }

    private function resolveLogFilename($type) {
        $map = [
            'ai' => 'ai.log',
            'search' => 'search.log',
            'avito' => 'avito.log',
        ];
        $type = strtolower(trim((string)$type));
        return $map[$type] ?? '';
    }

    private function detectBitrixDocumentRoot(): string {
        if (!defined('SITE_PATH')) {
            return '';
        }

        $site_path = rtrim((string)SITE_PATH, '/\\');
        foreach ([
            DS . 'local' . DS . 'modules' . DS . 'wellsoft.easychat' . DS . 'easychat',
            DS . 'bitrix' . DS . 'modules' . DS . 'wellsoft.easychat' . DS . 'easychat',
        ] as $suffix) {
            if (substr($site_path, -strlen($suffix)) === $suffix) {
                return rtrim(substr($site_path, 0, -strlen($suffix)), '/\\');
            }
        }

        return '';
    }

    public function catalogSearchPreview() {
        $is_htmx = $this->isHtmxSettingsRequest();
        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->sendCatalogPreview(array(), 'danger', 'Метод запроса не поддерживается.', 405, $is_htmx);
        }
        if (!$this->verifySettingsNonce()) {
            $this->sendCatalogPreview(array(), 'warning', 'Сессия настроек обновлена. Повторите проверку.', 403, $is_htmx);
        }

        $post = \EasyChat\Library\RequestContext::post();
        $query = isset($post['query']) ? trim((string)$post['query']) : '';
        if ($query === '') {
            $this->sendCatalogPreview(array(), 'warning', 'Введите название, модель или SKU.', 422, $is_htmx);
        }

        $model_setting = new SettingModel($this->registry);
        $settings = $model_setting->getAll();
        $settings['chat_cms_availability_policy'] = $this->normalizeCatalogAvailabilityPolicy($post['chat_cms_availability_policy'] ?? ($settings['chat_cms_availability_policy'] ?? 'smart'));
        $settings['chat_cms_show_exact_quantity'] = isset($post['chat_cms_show_exact_quantity']) && (string)$post['chat_cms_show_exact_quantity'] === '1' ? '1' : '0';
        $cms_type = $this->normalizeCatalogCmsType($settings['chat_cms_type'] ?? 'none');
        try {
            $cms_prefix = $this->normalizeCatalogPrefix($settings['chat_cms_prefix'] ?? '');
        } catch (\InvalidArgumentException $e) {
            $this->sendCatalogPreview(array(), 'warning', $e->getMessage(), 422, $is_htmx);
        }
        $settings['chat_cms_type'] = $cms_type;
        $settings['chat_cms_prefix'] = $cms_prefix;
        $capability = CmsCatalogCapability::inspect($this->registry->get('db'), $cms_type, $cms_prefix);
        if (empty($capability['ready'])) {
            $this->sendCatalogPreview(array(), 'danger', 'Товарный каталог CMS не обнаружен. Проверьте тип CMS и префикс таблиц.', 422, $is_htmx);
        }

        try {
            $cms_model = CmsFactory::create($this->registry->get('db'), $cms_type, $cms_prefix, $settings);
            if (!$cms_model) {
                $this->sendCatalogPreview(array(), 'danger', 'Для выбранной CMS поиск пока недоступен.', 422, $is_htmx);
            }
            $intent = isset($post['intent']) ? (string)$post['intent'] : CatalogSearchService::INTENT_LOOKUP;
            $public_urls = new CatalogPublicUrlService($this->registry, $cms_model, $cms_type);
            $products = (new CatalogSearchService($cms_model, $settings, $public_urls))->search($query, 0.0, $intent);
            $this->sendCatalogPreview($products, empty($products) ? 'empty' : 'success', '', 200, $is_htmx);
        } catch (\Throwable $e) {
            $this->sendCatalogPreview(array(), 'danger', 'Каталог временно не ответил. Повторите проверку позже.', 500, $is_htmx);
        }
    }

    private function sendCatalogPreview(array $products, $state, $message, $status, $is_htmx) {
        $html = SettingFragments::catalogSearchPreview($products, $state, $message);
        if ($is_htmx) {
            $headers = array(
                'HX-Retarget' => '#catalog-preview-results',
                'HX-Reswap' => HtmxFragmentRenderer::settingsPanelSwap(),
            );
            if ((int)$status === 403) {
                $encoded = json_encode(array(
                    'easychat:settings-nonce' => array('settings_nonce' => $this->ensureSettingsNonce()),
                ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                $headers['HX-Trigger'] = is_string($encoded) ? $encoded : '{}';
            }
            (new HtmxFragmentRenderer())->sendHtml($html, (int)$status, $headers);
        }

        http_response_code((int)$status);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(array(
            'success' => $state === 'success' || $state === 'empty',
            'html' => $html,
            'settings_nonce' => $status === 403 ? $this->ensureSettingsNonce() : '',
        ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function triggerCmsIndex() {
        $is_htmx = $this->isHtmxSettingsRequest();

        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            if ($is_htmx) {
                $this->sendCmsIndexHtmxStatus('danger', 'Метод запроса не поддерживается.', 405);
            }
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }

        if (!$this->verifySettingsNonce()) {
            if ($is_htmx) {
                $this->sendCmsIndexHtmxStatus(
                    'warning',
                    'Сессия настроек обновлена. Нажмите «Индексировать сейчас» еще раз.',
                    403,
                    $this->ensureSettingsNonce()
                );
            }
            $this->rejectSettingsNonceJson();
        }

        $model_setting = new SettingModel($this->registry);
        $settings = $model_setting->getAll();
        $cms_type = (string)($settings['chat_cms_type'] ?? 'none');

        if ($cms_type === 'none') {
            if ($is_htmx) {
                $this->sendCmsIndexHtmxStatus('warning', 'Сначала выберите CMS и сохраните настройки.');
            }
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode(['success' => false, 'error' => 'CMS не выбрана в настройках.']);
            exit;
        }

        if ((string)($settings['chat_cms_semantic_enabled'] ?? '0') !== '1'
            || ((string)($settings['chat_cms_index_products'] ?? '0') !== '1'
                && (string)($settings['chat_cms_index_pages'] ?? '0') !== '1')) {
            if ($is_htmx) {
                $this->sendCmsIndexHtmxStatus('warning', 'Сначала включите расширенный поиск и сохраните настройки.', 422);
            }
            $this->rejectJson(422, 'semantic_search_disabled', 'Расширенный поиск не включён.');
        }


        $queue  = new QueueManager($this->registry);
        $job_id = $queue->pushUnique(
            'cms_index',
            array(
                'reason' => 'manual',
                'source_signature' => CatalogIndexScheduler::sourceSignature($settings),
            ),
            CatalogIndexScheduler::DEDUPE_KEY,
            0,
            10
        );
        $model_setting->set('chat_cms_last_index_scheduled_at', date('c'));

        if ($is_htmx) {
            $this->sendCmsIndexHtmxStatus(
                'success',
                'Задание поставлено в очередь. Cron обновит базу знаний в фоне.',
                200,
                '',
                $job_id
            );
        }

        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['success' => true, 'job_id' => $job_id]);
        exit;
    }

    public function setCatalogSemantic() {
        header('Content-Type: application/json; charset=utf-8');
        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }
        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $model_setting = new SettingModel($this->registry);
        $settings = $model_setting->getAll();
        if (!isset(\EasyChat\Library\RequestContext::post()['settings_revision'])) {
            $this->rejectSettingsRevisionJson((int)($settings[SettingModel::REVISION_KEY] ?? 0));
        }
        $expected_revision = max(0, (int)\EasyChat\Library\RequestContext::post()['settings_revision']);
        $enabled = isset(\EasyChat\Library\RequestContext::post()['enabled'])
            && (string)\EasyChat\Library\RequestContext::post()['enabled'] === '1';
        $queue = new QueueManager($this->registry);

        if (!$enabled) {
            $search_enabled = (int)($settings['chat_productsearch'] ?? 0) > 0;
            $updates = array(
                'chat_productsearch' => $search_enabled ? '2' : '0',
                'chat_cms_semantic_enabled' => '0',
                'chat_cms_index_products' => '0',
                'chat_cms_index_pages' => '0',
                'chat_cms_last_index_scheduled_at' => '',
                'chat_cms_semantic_auto_opt_out' => '1',
            );
            if (!$search_enabled) {
                $updates[CatalogSemanticBootstrap::SEARCH_AUTO_OPT_OUT_KEY] = '1';
            }
            $cancelled = 0;
            try {
                $write_result = $model_setting->compareAndSetMany(
                    $expected_revision,
                    $updates,
                    function () use ($queue, &$cancelled): void {
                        $cancelled = $queue->cancelPendingByDedupeKey(
                            CatalogIndexScheduler::DEDUPE_KEY,
                            'catalog_semantic_disabled'
                        );
                    }
                );
            } catch (\Throwable $e) {
                $this->rejectJson(500, 'semantic_save_failed', 'Расширенный поиск не изменён: сервер не завершил сохранение.');
            }
            if (empty($write_result['success'])) {
                $this->rejectSettingsRevisionJson((int)($write_result['current_revision'] ?? 0));
            }
            $effective = array_merge($settings, $updates);
            echo json_encode(array(
                'success' => true,
                'enabled' => false,
                'cancelled_jobs' => $cancelled,
                'settings_revision' => (int)$write_result['revision'],
                'catalog' => CatalogSemanticBootstrap::publicState($effective),
                'message' => 'Расширенный поиск отключён. Быстрый поиск продолжает работать.',
            ), JSON_UNESCAPED_UNICODE);
            exit;
        }

        if (trim((string)($settings['chat_ai_key'] ?? '')) === '') {
            $this->rejectJson(422, 'ai_key_required', 'Сначала подключите ИИ-провайдера: расширенный поиск создаёт embeddings.');
        }

        $cms_type = $this->normalizeCatalogCmsType($settings['chat_cms_type'] ?? 'none');
        try {
            $cms_prefix = $this->normalizeCatalogPrefix($settings['chat_cms_prefix'] ?? '');
        } catch (\InvalidArgumentException $e) {
            $this->rejectJson(422, 'invalid_cms_prefix', $e->getMessage());
        }
        $capability = CmsCatalogCapability::inspect($this->registry->get('db'), $cms_type, $cms_prefix);
        if (empty($capability['ready'])) {
            $this->rejectJson(422, 'catalog_not_ready', 'Сначала сохраните корректные настройки товарного каталога.');
        }

        $semantic_settings = array_merge($settings, array(
            'chat_productsearch' => '3',
            'chat_cms_semantic_enabled' => '1',
            'chat_cms_index_products' => '1',
            CatalogSemanticBootstrap::SEARCH_AUTO_OPT_OUT_KEY => '0',
        ));
        $updates = array(
            'chat_productsearch' => '3',
            'chat_cms_semantic_enabled' => '1',
            'chat_cms_index_products' => '1',
            'chat_cms_semantic_auto_opt_out' => '0',
            CatalogSemanticBootstrap::SEARCH_AUTO_OPT_OUT_KEY => '0',
            'chat_cms_last_index_scheduled_at' => date('c'),
        );
        $job_id = 0;
        try {
            $write_result = $model_setting->compareAndSetMany(
                $expected_revision,
                $updates,
                function () use ($queue, $semantic_settings, &$job_id): void {
                    $job_id = $queue->pushUnique(
                        'cms_index',
                        array(
                            'reason' => 'semantic_enabled',
                            'source_signature' => CatalogIndexScheduler::sourceSignature($semantic_settings),
                        ),
                        CatalogIndexScheduler::DEDUPE_KEY,
                        0,
                        10
                    );
                }
            );
        } catch (\Throwable $e) {
            $this->rejectJson(500, 'semantic_save_failed', 'Расширенный поиск не изменён: сервер не завершил сохранение.');
        }
        if (empty($write_result['success'])) {
            $this->rejectSettingsRevisionJson((int)($write_result['current_revision'] ?? 0));
        }
        $semantic_settings = array_merge($semantic_settings, $updates);

        echo json_encode(array(
            'success' => true,
            'enabled' => true,
            'job_id' => $job_id,
            'settings_revision' => (int)$write_result['revision'],
            'catalog' => CatalogSemanticBootstrap::publicState($semantic_settings),
            'message' => 'Расширенный поиск включён. Каталог обновляется в фоне.',
        ), JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function startSchedulerProbe() {
        header('Content-Type: application/json; charset=utf-8');
        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }
        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }

        $probe_id = bin2hex(random_bytes(16));
        $model = new SettingModel($this->registry);
        $queue = new QueueManager($this->registry);
        $queue->cancelPendingByDedupeKey('scheduler_health_probe', 'replaced_by_new_probe');
        $model->setMany(array(
            'chat_scheduler_probe_pending_id' => $probe_id,
            'chat_scheduler_probe_requested_at' => date('c'),
        ));
        $queue->pushUnique('scheduler_probe', array('probe_id' => $probe_id), 'scheduler_health_probe', 0, 2);

        echo json_encode(array('success' => true, 'probe_id' => $probe_id), JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function schedulerProbeStatus() {
        header('Content-Type: application/json; charset=utf-8');
        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            $this->rejectJson(405, 'method_not_allowed', 'Метод не поддерживается.');
        }
        if (!$this->verifySettingsNonce()) {
            $this->rejectSettingsNonceJson();
        }
        $probe_id = strtolower(trim((string)(\EasyChat\Library\RequestContext::post()['probe_id'] ?? '')));
        if (!preg_match('/^[a-f0-9]{32}$/', $probe_id)) {
            $this->rejectJson(422, 'invalid_probe_id', 'Некорректная проверка планировщика.');
        }
        $model = new SettingModel($this->registry);
        $completed = (string)$model->get('chat_scheduler_probe_completed_id', '');
        $requested = strtotime((string)$model->get('chat_scheduler_probe_requested_at', '')) ?: 0;
        $done = $completed !== '' && hash_equals($completed, $probe_id);
        echo json_encode(array(
            'success' => true,
            'status' => $done ? 'complete' : ((time() - $requested) > 180 ? 'timeout' : 'pending'),
            'health' => SchedulerHealthService::state($model->getAll()),
        ), JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function isHtmxSettingsRequest() {
        $server = \EasyChat\Library\RequestContext::server();
        return (string)($server['HTTP_X_EASYCHAT_HTMX'] ?? '') === '1'
            || strtolower((string)($server['HTTP_HX_REQUEST'] ?? '')) === 'true';
    }

    private function sendCmsIndexHtmxStatus($type, $message, $status = 200, $settings_nonce = '', $job_id = null) {
        $headers = [
            'HX-Retarget' => '#cms-index-status',
            'HX-Reswap' => HtmxFragmentRenderer::settingsPanelSwap(),
        ];

        if ($settings_nonce !== '') {
            $encoded = json_encode([
                'easychat:settings-nonce' => [
                    'settings_nonce' => (string)$settings_nonce,
                ],
            ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            $headers['HX-Trigger'] = is_string($encoded) ? $encoded : '{}';
        }

        (new HtmxFragmentRenderer())->sendHtml(
            SettingFragments::cmsIndexStatus($type, $message, $job_id),
            (int)$status,
            $headers
        );
    }
}
