<?php
namespace EasyChat\Controller;

use EasyChat\Library\EasyTokensGiftProvisioner;
use EasyChat\Library\EasyTokensClientHeaders;
use EasyChat\Model\Setting as SettingModel;
use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Http\TransportErrorCatalog;
use EasyChat\Library\Http\ProxyPolicy;
use EasyChat\Library\WidgetDomainPolicy;
use EasyChat\Library\Messenger\MaxBotProvider;
use EasyChat\Library\Messenger\MaxProvider;
use EasyChat\Library\Messenger\MaxAuthService;
use EasyChat\Library\Messenger\MaxCliSupport;
use EasyChat\Library\Messenger\MaxInboundScope;
use EasyChat\Library\Messenger\MaxPollingService;
use EasyChat\Library\Messenger\MaxRuntime;
use EasyChat\Library\Messenger\MaxHealthPresenter;
use EasyChat\Library\Messenger\MaxPersonalPresenter;
use EasyChat\Library\Messenger\MaxPersonalSettingsConflictException;
use EasyChat\Library\Messenger\MaxPersonalSettingsService;
use EasyChat\Library\Messenger\MaxSessionAuthorizationRequiredException;
use EasyChat\Library\Messenger\MaxSessionLifecycle;
use EasyChat\Library\Messenger\MaxSessionLifecycleBusyException;
use EasyChat\Library\Messenger\WhatsAppPersonalAuthService;
use EasyChat\Library\Messenger\WhatsAppPersonalPresenter;
use EasyChat\Library\Messenger\WhatsAppPersonalProvider;
use EasyChat\Library\Messenger\AvitoProvider;
use EasyChat\Library\Messenger\AvitoHistorySyncService;
use EasyChat\Library\Messenger\AvitoHealthService;
use EasyChat\Library\Messenger\AvitoAccessIncidentService;
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\Support\LogTailReader;
use EasyChat\Library\Messenger\Marketplace\MarketplaceAdminPresenter;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\EmailAccount;
use EasyChat\Library\Email\EmailProviderCatalog;
use EasyChat\View\HtmxFragmentRenderer;
use EasyChat\View\IntegrationFragments;
use EasyChat\Library\QuickReplyAiAssistant;
use EasyChat\Library\Commerce\MoySkladConnection;
use EasyChat\Library\Commerce\OpenCartDeliveryMethodCatalog;
use EasyChat\Library\Delivery\DeliveryConnection;

class Integration 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);

         
        $settings = $model_setting->getAll();
        
        $defaults = [
            'chat_messenger_avatars_status' => '1',
            'chat_telegram_logging' => '0',
            'chat_avito_logging'    => '0',
            'chat_youla_logging'     => '0',
            'chat_max_logging'       => '0',
            '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_ai_key' => '',
            'chat_ai_provider' => 'easytokens',
            'chat_proxy_routes_json' => '',
            'chat_install_secret' => '',
            'chat_bot_start_greeting_status' => '0',
            'chat_bot_start_notify_status' => '0',
            'chat_bot_start_message' => "Добро пожаловать!\n\nЯ помогу проверить оформленный заказ или подобрать подходящий товар.\n\n1 — Я уже оформил заказ\n0 — Мне нужна консультация\n\nМожно отправить цифру или просто написать вопрос своими словами.",
            'chat_bot_start_message_max_bot' => '',
            'chat_bot_start_message_telegram' => '',
            'chat_bot_start_message_vk' => '',
        ];

        foreach ($defaults as $key => $def) {
            $this->vars[$key] = isset($settings[$key]) ? $settings[$key] : $def;
        }

        $this->vars['chat_ai_avito_rag_mode'] = $this->normalizeAvitoRagMode($this->vars['chat_ai_avito_rag_mode'] ?? 'auto');
        $this->vars['chat_ai_avito_tools_mode'] = $this->normalizeAvitoToolsMode($this->vars['chat_ai_avito_tools_mode'] ?? 'auto');
        $this->vars['chat_ai_avito_alternatives_mode'] = $this->normalizeAvitoAlternativesMode($this->vars['chat_ai_avito_alternatives_mode'] ?? 'on_request');
        $proxy_policy = new ProxyPolicy($this->vars);
        $this->vars['proxy_route_states'] = $proxy_policy->routeStates();

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

         
        $max_bot_channel = $this->ensureMaxBotChannel();
        $max_bot_credentials = $this->decodeChannelCredentials($max_bot_channel);
        $max_bot_token = $this->decryptChannelSecret((string)($max_bot_credentials['bot_token'] ?? ''));
        $this->vars['max_bot_channel'] = $max_bot_channel;
        $this->vars['chat_max_bot_status'] = (($max_bot_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_max_bot_token_mask'] = SecretBox::mask($max_bot_token);
        $this->vars['chat_max_bot_webhook_url'] = $this->buildMaxBotWebhookUrl($max_bot_channel);
        $this->vars['chat_max_bot_name'] = (string)($max_bot_channel['bot_name'] ?? '');
        $this->vars['chat_max_bot_username'] = (string)($max_bot_channel['bot_username'] ?? '');
        $this->vars['chat_max_bot_user_id'] = (string)($max_bot_channel['bot_user_id'] ?? '');
        $this->vars['chat_max_bot_last_error'] = (string)($max_bot_channel['last_error'] ?? '');
        $this->vars['chat_max_bot_webhook_synced_at'] = (string)($max_bot_channel['webhook_synced_at'] ?? '');
        $this->vars['chat_max_bot_connection'] = $this->buildMaxBotConnectionState($max_bot_channel, $max_bot_token);

         
        $max_channel = $this->ensureMaxChannel();
        $max_health = (new MaxHealthPresenter($this->registry))->build($max_channel);
        $max_channel = is_array($max_health['channel'] ?? null) ? $max_health['channel'] : $max_channel;
        $max_settings = is_array($max_health['settings'] ?? null)
            ? $max_health['settings']
            : $this->decodeChannelSettings($max_channel);
        $this->vars['max_channel'] = $max_channel;
        $this->vars['chat_max_status'] = (($max_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_max_health'] = $this->publicMaxHealth($max_health);
        $this->vars['chat_max_health_items'] = is_array($max_health['items'] ?? null) ? $max_health['items'] : [];
        $this->vars['chat_max_connection'] = $this->buildMaxConnectionState($max_channel, $max_health);
        $this->vars['chat_max_session_security_label'] = (($max_health['session']['status'] ?? '') === 'ready')
            ? 'Одна защищенная сессия'
            : 'Сессия еще не создана';
        $this->vars['chat_max_auth_state'] = (new MaxAuthService($this->registry))->authState($max_channel);
        $this->vars['chat_max_dedicated_cron_modes'] = MaxPollingService::dedicatedCronModeOptions();
        $this->vars['chat_max_dedicated_cron_mode'] = MaxPollingService::normalizeDedicatedCronMode((string)($max_settings['dedicated_cron_mode'] ?? ''));
        $this->vars['chat_max_personal_inbound_scope'] = MaxInboundScope::fromSettings($max_settings);
        $max_cron_path = SITE_PATH . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'max_polling.php';
        $max_cron_profile = MaxCliSupport::commandProfile($max_cron_path);
        $this->vars['chat_max_cli_profile'] = $max_cron_profile;
        $this->vars['chat_max_cli_runtime'] = (array)($max_cron_profile['runtime'] ?? []);
        $this->vars['chat_max_flock_runtime'] = (array)($max_cron_profile['flock'] ?? []);
        $this->vars['chat_max_protected_chain'] = (array)($max_cron_profile['protected_chain'] ?? []);
        $this->vars['chat_max_dedicated_cron_command'] = (string)($max_cron_profile['protected_command'] ?? '');
        $this->vars['chat_max_dedicated_cron_simple_command'] = (string)($max_cron_profile['simple_command'] ?? '');
        $this->vars['chat_max_dedicated_cron_state'] = $this->buildMaxDedicatedCronState($max_channel, $max_health);
        $this->vars['chat_max_personal'] = (new MaxPersonalPresenter($this->registry))->build(
            $max_channel,
            $max_health,
            $this->vars['chat_max_auth_state']
        );

         
         
        $whatsapp_channel = $this->ensureWhatsAppPersonalChannel();
        $whatsapp_profile = MaxCliSupport::commandProfile(
            SITE_PATH . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'whatsapp_polling.php',
            ['curl', 'pdo_mysql', 'zlib']
        );
        $this->vars['chat_whatsapp_personal'] = (new WhatsAppPersonalPresenter($this->registry))->build($whatsapp_channel);
        $this->vars['chat_whatsapp_personal_channel'] = $whatsapp_channel;
        $this->vars['chat_whatsapp_personal_cli_profile'] = $whatsapp_profile;
        $this->vars['chat_whatsapp_personal_cron_command'] = (string)(
            ($whatsapp_profile['protected_command'] ?? '') ?: ($whatsapp_profile['simple_command'] ?? '')
        );
        $this->vars['chat_whatsapp_personal_cron_simple_command'] = (string)($whatsapp_profile['simple_command'] ?? '');

         
        $avito_channel = $this->ensureAvitoChannel();
        $avito_credentials = $this->decodeChannelCredentials($avito_channel);
        $avito_credentials = $this->quarantineAutofilledAvitoCredentials($avito_channel, $avito_credentials);
        $avito_client_id = $this->decryptChannelSecretOrPlain((string)($avito_credentials['client_id'] ?? ''));
        $avito_client_secret = $this->decryptChannelSecretOrPlain((string)($avito_credentials['client_secret'] ?? ''));
        $this->vars['avito_channel'] = $avito_channel;
        $this->vars['chat_avito_status'] = (($avito_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_avito_client_id_mask'] = SecretBox::mask($avito_client_id);
        $this->vars['chat_avito_client_secret_mask'] = SecretBox::mask($avito_client_secret);
        $this->vars['chat_avito_webhook_url'] = $this->buildAvitoWebhookUrl($avito_channel);
        $this->vars['chat_avito_bot_name'] = (string)($avito_channel['bot_name'] ?? '');
        $this->vars['chat_avito_bot_username'] = (string)($avito_channel['bot_username'] ?? '');
        $this->vars['chat_avito_bot_user_id'] = (string)($avito_channel['bot_user_id'] ?? '');
        $this->vars['chat_avito_last_error'] = (string)($avito_channel['last_error'] ?? '');
        $this->vars['chat_avito_webhook_synced_at'] = (string)($avito_channel['webhook_synced_at'] ?? '');
        $this->vars['chat_avito_connection'] = $this->buildAvitoConnectionState($avito_channel, $avito_client_id);
        $this->vars['chat_avito_access_incident'] = (new AvitoAccessIncidentService($this->registry))->summary();

         
        $youla_channel = $this->ensureYoulaChannel();
        $youla_credentials = $this->decodeChannelCredentials($youla_channel);
        $youla_token = $this->youlaBearerFromCredentials($youla_credentials);
        $this->vars['youla_channel'] = $youla_channel;
        $this->vars['chat_youla_status'] = (($youla_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_youla_token_mask'] = SecretBox::mask($youla_token);
        $this->vars['chat_youla_webhook_url'] = $this->buildYoulaWebhookUrl($youla_channel);
        $this->vars['chat_youla_bot_name'] = (string)($youla_channel['bot_name'] ?? '');
        $this->vars['chat_youla_bot_username'] = (string)($youla_channel['bot_username'] ?? '');
        $this->vars['chat_youla_bot_user_id'] = (string)($youla_channel['bot_user_id'] ?? '');
        $this->vars['chat_youla_last_error'] = (string)($youla_channel['last_error'] ?? '');
        $this->vars['chat_youla_webhook_synced_at'] = (string)($youla_channel['webhook_synced_at'] ?? '');
        $this->vars['chat_youla_connection'] = $this->buildYoulaConnectionState($youla_channel, $youla_token);

         
        $vk_channel = $this->ensureVkChannel();
        $vk_credentials = $this->decodeChannelCredentials($vk_channel);
        $vk_token = $this->decryptChannelSecret((string)($vk_credentials['bot_token'] ?? ''));
        $this->vars['vk_channel'] = $vk_channel;
        $this->vars['chat_vk_status'] = (($vk_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_vk_token_mask'] = SecretBox::mask($vk_token);
        $this->vars['chat_vk_webhook_url'] = $this->buildVkWebhookUrl($vk_channel);
        $this->vars['chat_vk_bot_name'] = (string)($vk_channel['bot_name'] ?? '');
        $this->vars['chat_vk_bot_username'] = (string)($vk_channel['bot_username'] ?? '');
        $this->vars['chat_vk_bot_user_id'] = (string)($vk_channel['bot_user_id'] ?? '');
        $this->vars['chat_vk_last_error'] = (string)($vk_channel['last_error'] ?? '');
        $this->vars['chat_vk_webhook_synced_at'] = (string)($vk_channel['webhook_synced_at'] ?? '');
        $this->vars['chat_vk_connection'] = $this->buildVkConnectionState($vk_channel, $vk_token);
        $this->vars['chat_vk_setup_steps'] = $this->buildVkSetupSteps($vk_channel, $vk_credentials, $vk_token);

         
        $telegram_channel = $this->ensureTelegramChannel();
        $telegram_credentials = $this->decodeChannelCredentials($telegram_channel);
        $telegram_token = $this->decryptChannelSecret((string)($telegram_credentials['bot_token'] ?? ''));
        $this->vars['telegram_channel'] = $telegram_channel;
        $this->vars['chat_telegram_status'] = (($telegram_channel['status'] ?? 'disabled') === 'active') ? '1' : '0';
        $this->vars['chat_telegram_bot_token_mask'] = SecretBox::mask($telegram_token);
        $this->vars['chat_telegram_webhook_url'] = $this->buildTelegramWebhookUrl($telegram_channel);
        $this->vars['chat_telegram_bot_name'] = (string)($telegram_channel['bot_name'] ?? '');
        $this->vars['chat_telegram_bot_username'] = (string)($telegram_channel['bot_username'] ?? '');
        $this->vars['chat_telegram_bot_user_id'] = (string)($telegram_channel['bot_user_id'] ?? '');
        $this->vars['chat_telegram_last_error'] = (string)($telegram_channel['last_error'] ?? '');
        $this->vars['chat_telegram_webhook_synced_at'] = (string)($telegram_channel['webhook_synced_at'] ?? '');
        $this->vars['chat_telegram_connection'] = $this->buildTelegramConnectionState($telegram_channel, $telegram_token);

        $cron_mode = defined('EASYCHAT_CMS_CRON_MODE') ? (string)EASYCHAT_CMS_CRON_MODE : 'standalone';
        if (defined('EASYCHAT_WPORG_MODE') && EASYCHAT_WPORG_MODE) {
            $cron_mode = 'wordpress_org';
        }
        $this->vars['cron_mode'] = $cron_mode;

        $marketplace_view = (new MarketplaceAdminPresenter($this->registry))->build();
        $this->vars['marketplace_providers'] = $marketplace_view['providers'];
        $this->vars['marketplace_accounts_by_provider'] = $marketplace_view['accounts_by_provider'];
        $this->vars['marketplace_storage_ready'] = !empty($marketplace_view['storage_ready']);
        $this->vars['marketplace_csrf_token'] = $this->ensureCsrfToken();

         
         
        $emailAccount = (new EmailAccount($this->registry))->get(false);
        $this->vars['email_account'] = $emailAccount;
        $this->vars['email_secret_saved'] = !empty($emailAccount);
        $this->vars['email_providers'] = EmailProviderCatalog::publicList();
        $emailSession =& \EasyChat\Library\RequestContext::session();
        if (empty($emailSession['easychat_email_nonce'])) {
            $emailSession['easychat_email_nonce'] = bin2hex(random_bytes(16));
        }
        $this->vars['email_nonce'] = (string)$emailSession['easychat_email_nonce'];

        $moyskladSession =& \EasyChat\Library\RequestContext::session();
        if (empty($moyskladSession['easychat_moysklad_nonce'])) {
            $moyskladSession['easychat_moysklad_nonce'] = bin2hex(random_bytes(16));
        }
        $this->vars['moysklad_nonce'] = (string)$moyskladSession['easychat_moysklad_nonce'];
        try {
            $this->vars['moysklad_connection'] = (new MoySkladConnection($this->registry))->publicState();
        } catch (\Throwable $e) {
            $this->vars['moysklad_connection'] = array('enabled'=>false,'token_saved'=>false,'last_error'=>'storage_unavailable');
        }

        $deliverySession =& \EasyChat\Library\RequestContext::session();
        if (empty($deliverySession['easychat_delivery_nonce'])) {
            $deliverySession['easychat_delivery_nonce'] = bin2hex(random_bytes(16));
        }
        $this->vars['delivery_nonce'] = (string)$deliverySession['easychat_delivery_nonce'];
        try {
            $this->vars['delivery_connection'] = (new DeliveryConnection($this->registry))->publicState();
        } catch (\Throwable $e) {
            $this->vars['delivery_connection'] = array('russian_post'=>array(), 'cdek'=>array());
        }
        try {
            $edition = defined('EASYCHAT_CMS_EDITION') ? (string)EASYCHAT_CMS_EDITION : '';
            $this->vars['opencart_delivery_methods'] = (new OpenCartDeliveryMethodCatalog($this->registry->get('db'), $settings, $edition))->discover();
        } catch (\Throwable $e) {
             
             
            $this->vars['opencart_delivery_methods'] = array('available'=>false,'items'=>array(),'manual_count'=>0);
        }

        $requested_tab = strtolower(trim((string)(\EasyChat\Library\RequestContext::get()['tab'] ?? 'telegram')));
        $allowed_tabs = ['telegram', 'vk', 'avito', 'youla', 'wildberries', 'ozon', 'yandex_market', 'max', 'whatsapp', 'email', 'moysklad', 'delivery', 'params'];
        $this->vars['integration_active_tab'] = in_array($requested_tab, $allowed_tabs, true)
            ? $requested_tab
            : 'telegram';

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

    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);

        $keys = [
            'chat_messenger_avatars_status',
            'chat_telegram_logging',
            'chat_avito_logging',
            'chat_youla_logging',
            'chat_ai_avito_item_context_status',
            'chat_ai_youla_item_context_status',
            'chat_ai_avito_rag_mode',
            'chat_ai_avito_tools_mode',
            'chat_ai_avito_alternatives_mode',
            'chat_bot_start_greeting_status',
            'chat_bot_start_notify_status',
            'chat_bot_start_message',
            'chat_bot_start_message_max_bot',
            'chat_bot_start_message_telegram',
            'chat_bot_start_message_vk',
        ];

        $checkbox_keys = [
            'chat_messenger_avatars_status',
            'chat_telegram_logging',
            'chat_avito_logging',
            'chat_youla_logging',
            'chat_ai_avito_item_context_status',
            'chat_ai_youla_item_context_status',
            'chat_bot_start_greeting_status',
            'chat_bot_start_notify_status',
        ];

        $settings_data = [];
        foreach ($keys as $key) {
            if (in_array($key, $checkbox_keys, true)) {
                $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] : '';
            }
            $settings_data[$key] = trim((string)$val);
        }

        $settings_data['chat_ai_avito_rag_mode'] = $this->normalizeAvitoRagMode($settings_data['chat_ai_avito_rag_mode'] ?? 'auto');
        $settings_data['chat_ai_avito_tools_mode'] = $this->normalizeAvitoToolsMode($settings_data['chat_ai_avito_tools_mode'] ?? 'auto');
        $settings_data['chat_ai_avito_alternatives_mode'] = $this->normalizeAvitoAlternativesMode($settings_data['chat_ai_avito_alternatives_mode'] ?? 'on_request');
        foreach (['chat_bot_start_message', 'chat_bot_start_message_max_bot', 'chat_bot_start_message_telegram', 'chat_bot_start_message_vk'] as $message_key) {
            $settings_data[$message_key] = mb_substr(trim((string)($settings_data[$message_key] ?? '')), 0, 2000);
        }
        if (($settings_data['chat_bot_start_greeting_status'] ?? '0') === '1' && ($settings_data['chat_bot_start_message'] ?? '') === '') {
            $this->rejectJson(422, 'greeting_empty', 'Добавьте общее приветствие перед включением отправки.');
        }

        $avatars_was_enabled = $model_setting->get('chat_messenger_avatars_status', '1') === '1';
        $db = $this->registry->get('db');
        $db->query('START TRANSACTION');
        try {
            $channel_saves = array(
                array('save' => function () { return $this->saveMaxBotChannelConfig(); }, 'tab' => 'max', 'fallback' => 'Не удалось сохранить настройки MAX-бота.'),
                array('save' => function () { return $this->saveAvitoChannelConfig(); }, 'tab' => 'avito', 'fallback' => 'Не удалось сохранить настройки Avito.'),
                array('save' => function () { return $this->saveYoulaChannelConfig(); }, 'tab' => 'youla', 'fallback' => 'Не удалось сохранить настройки Юлы.'),
                array('save' => function () { return $this->saveVkChannelConfig(); }, 'tab' => 'vk', 'fallback' => 'Не удалось сохранить настройки VK.'),
                array('save' => function () { return $this->saveTelegramChannelConfig(); }, 'tab' => 'telegram', 'fallback' => 'Не удалось сохранить настройки Telegram.'),
            );
            foreach ($channel_saves as $channel_save) {
                $channel_result = $channel_save['save']();
                if (!empty($channel_result['success'])) {
                    continue;
                }
                $db->query('ROLLBACK');
                echo json_encode(array(
                    'success' => false,
                    'error' => $channel_result['error'] ?? $channel_save['fallback'],
                    'tab' => $channel_save['tab'],
                ), JSON_UNESCAPED_UNICODE);
                exit;
            }

            $model_setting->setMany($settings_data);
            $db->query('COMMIT');
        } catch (\Throwable $e) {
            try {
                $db->query('ROLLBACK');
            } catch (\Throwable $rollback_error) {
                 
                 
            }
            $this->rejectJson(
                500,
                'integration_save_failed',
                'Интеграции не изменены: сервер не смог завершить атомарное сохранение.'
            );
        }

        $avatars_enabled = ($settings_data['chat_messenger_avatars_status'] ?? '1') === '1';
        $warning = '';
        if ($avatars_was_enabled !== $avatars_enabled) {
            try {
                $avatar_service = new ContactAvatarService($this->registry);
                $avatar_service->touchChatsWithPreparedAvatars();
                if ($avatars_enabled) {
                    $avatar_service->enqueueBackfill();
                }
            } catch (\Throwable $e) {
                 
                 
                $warning = 'Интеграции сохранены. Обновление старых аватарок продолжится в фоне.';
            }
        }

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

    public function greetingSuggest() {
        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();
        }
        $session = &\EasyChat\Library\RequestContext::session();
        $now = time();
        if (($now - (int)($session['greeting_suggest_at'] ?? 0)) < 5) {
            $this->rejectJson(429, 'rate_limited', 'Подождите несколько секунд перед повтором.');
        }
        $session['greeting_suggest_at'] = $now;
        $business = mb_substr(trim((string)(\EasyChat\Library\RequestContext::post()['business'] ?? '')), 0, 500);
        if ($business === '') {
            $this->rejectJson(422, 'business_required', 'Коротко опишите бизнес и цель первого сообщения.');
        }
        $assistant = new QuickReplyAiAssistant($this->registry);
        $result = $assistant->suggest([
            'task' => 'generate',
            'style' => 'friendly',
            'title' => 'Приветствие',
            'instruction' => "Подготовь короткое стартовое приветствие бота для бизнеса: {$business}. Дай 2–3 понятных варианта следующего действия. Не выдумывай факты. Не пиши «нажмите кнопку»: универсальный текст должен предлагать отправить цифру или написать вопрос своими словами.",
            'use_knowledge' => false,
        ]);
        if (empty($result['success'])) {
            $this->rejectJson(422, (string)($result['error'] ?? 'suggest_failed'), 'Не удалось подготовить вариант. Проверьте AI-ключ и повторите.');
        }
        echo json_encode(['success' => true, 'text' => mb_substr((string)$result['text'], 0, 2000)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function maxWebhookSync() {
        $this->maxBotWebhookSync();
    }

    public function maxBotWebhookSync() {
        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;
        }

        $channel = $this->ensureMaxBotChannel();
        $credentials = $this->decodeChannelCredentials($channel);
        $token = $this->decryptChannelSecret((string)($credentials['bot_token'] ?? ''));
        $secret = $this->decryptChannelSecret((string)($credentials['webhook_secret'] ?? ''));

        if ($token === '') {
            echo json_encode(['success' => false, 'error' => 'Сначала сохраните токен MAX-бота.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if ($secret === '') {
            $secret = $this->generateMaxWebhookSecret();
            $credentials['webhook_secret'] = $this->encryptChannelSecret($secret);
        }

        $webhook_url = $this->buildMaxBotWebhookUrl($channel);
        if (strpos($webhook_url, 'https://') !== 0) {
            echo json_encode(['success' => false, 'error' => 'Webhook MAX должен быть доступен по HTTPS на 443 порту.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $provider = new MaxBotProvider($this->registry);
        $sync = $provider->syncWebhook($channel, $token, $webhook_url, $secret);

        $model_messenger = new MessengerModel($this->registry);
        if (empty($sync['success'])) {
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'last_error' => (string)($sync['error'] ?? 'max_sync_failed'),
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'MAX не подтвердил подключение: ' . (string)($sync['error'] ?? 'ошибка API'),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $bot = is_array($sync['bot'] ?? null) ? $sync['bot'] : [];
        $synced_at = date('Y-m-d H:i:s');
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'webhook_synced_at' => $synced_at,
            'bot_user_id' => (string)($bot['user_id'] ?? $bot['id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'last_error' => '',
        ]);

        echo json_encode([
            'success' => true,
            'message' => 'Webhook MAX подключен.',
            'bot_user_id' => (string)($bot['user_id'] ?? $bot['id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'token_mask' => SecretBox::mask($token),
            'synced_at' => $synced_at,
            'webhook_url' => $webhook_url,
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function maxQrStart() {
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) {
            return $service->startQr($channel);
        });
    }

    public function maxQrCheck() {
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) {
            return $service->checkQr($channel);
        }, false);
    }

    public function maxSmsStart() {
        $phone = trim((string)(\EasyChat\Library\RequestContext::post()['phone'] ?? ''));
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) use ($phone) {
            return $service->startSms($channel, $phone);
        });
    }

    public function maxSmsSubmit() {
        $code = trim((string)(\EasyChat\Library\RequestContext::post()['code'] ?? ''));
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) use ($code) {
            return $service->submitSms($channel, $code);
        });
    }

    public function maxPasswordSubmit() {
        $password = (string)(\EasyChat\Library\RequestContext::post()['password'] ?? '');
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) use ($password) {
            return $service->submitPassword($channel, $password);
        });
    }

    public function maxAuthClear() {
        $this->jsonMaxAuth(function (MaxAuthService $service, array $channel) {
            return $service->clearAuth($channel);
        });
    }

    public function whatsappQrStart() {
        $this->jsonWhatsAppAuth(function (WhatsAppPersonalAuthService $service, array $channel) {
            $presenter = (new WhatsAppPersonalPresenter($this->registry))->build($channel);
            if (empty($presenter['cron']['ready'])) {
                http_response_code(409);
                return [
                    'success' => false,
                    'error' => 'whatsapp_cron_required',
                    'message' => 'Сначала подтвердите запуск отдельной задачи каждую минуту.',
                ];
            }
            return $service->startQr($channel);
        });
    }

    public function whatsappQrCheck() {
        $this->jsonWhatsAppAuth(function (WhatsAppPersonalAuthService $service, array $channel) {
            return $service->check($channel);
        });
    }

    public function whatsappAuthClear() {
        $this->jsonWhatsAppAuth(function (WhatsAppPersonalAuthService $service, array $channel) {
            return $service->clear($channel);
        });
    }

    public function whatsappPersonalToggle() {
        $enabled = (string)(\EasyChat\Library\RequestContext::post()['enabled'] ?? '0') === '1';
        $this->jsonWhatsAppAuth(function (WhatsAppPersonalAuthService $service, array $channel) use ($enabled) {
            return $service->setEnabled($channel, $enabled);
        });
    }

    public function whatsappHealth() {
        $this->jsonWhatsAppAuth(function (WhatsAppPersonalAuthService $service, array $channel) {
            return ['success' => true, 'state' => $service->state($channel)];
        });
    }

    public function maxHealth() {
        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();
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_write_close();
        }

        $health = (new MaxHealthPresenter($this->registry))->build($this->ensureMaxChannel());
        $personal = (new MaxPersonalPresenter($this->registry))->build(
            is_array($health['channel'] ?? null) ? $health['channel'] : $this->ensureMaxChannel(),
            $health
        );
        echo json_encode([
            'success' => true,
            'health' => $this->publicMaxHealth($health),
            'personal_max' => $this->publicMaxPersonal($personal),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function maxPersonalSettings() {
        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();
        }
        $post = \EasyChat\Library\RequestContext::post();
        $scope = $post['scope'] ?? [];
        if (is_string($scope)) {
            $decoded_scope = json_decode($scope, true);
            $scope = is_array($decoded_scope) ? $decoded_scope : [];
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_write_close();
        }
        $channel = $this->ensureMaxChannel();
        try {
            (new MaxPersonalSettingsService($this->registry))->save($channel, [
                'enabled' => (string)($post['enabled'] ?? '0') === '1',
                'scope' => $scope,
                'dedicated_cron_mode' => (string)($post['dedicated_cron_mode'] ?? ''),
                'revision' => max(0, (int)($post['revision'] ?? 0)),
            ]);
        } catch (MaxPersonalSettingsConflictException $e) {
            http_response_code(409);
            $personal = $this->buildMaxPersonalPublic();
            echo json_encode([
                'success' => false,
                'error' => 'max_settings_conflict',
                'message' => 'Настройки изменились в другой вкладке. EasyChat показал актуальный вариант.',
                'personal_max' => $personal,
            ], JSON_UNESCAPED_UNICODE);
            exit;
        } catch (MaxSessionAuthorizationRequiredException $e) {
            $this->rejectJson(422, 'max_auth_required', 'Сначала войдите в личный MAX по QR или телефону.');
        } catch (MaxSessionLifecycleBusyException $e) {
            $this->rejectJson(409, 'max_session_busy', 'Сессия MAX обновляется. Повторите действие через несколько секунд.');
        } catch (\InvalidArgumentException $e) {
            $this->rejectJson(422, 'max_scope_required', 'Выберите хотя бы один источник сообщений.');
        } catch (\Throwable $e) {
            $this->rejectJson(500, 'max_settings_failed', 'Не удалось безопасно сохранить настройки личного MAX.');
        }

        echo json_encode([
            'success' => true,
            'message' => 'Настройки личного MAX сохранены.',
            'personal_max' => $this->buildMaxPersonalPublic(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function maxDiagnosticsStart() {
        $this->setMaxDiagnosticsLease(true);
    }

    public function maxDiagnosticsStop() {
        $this->setMaxDiagnosticsLease(false);
    }

    public function avitoWebhookSync() {
        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;
        }

        $channel = $this->ensureAvitoChannel();
        $config = $this->buildAvitoChannelConfigFromPost($channel, true);
        if (!empty($config['validation_error'])) {
            echo json_encode(['success' => false, 'error' => $config['validation_error']], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $credentials = $config['credentials'];
        $token = (string)$config['token'];

        if ($token === '') {
            echo json_encode(['success' => false, 'error' => 'Введите Client ID и Client Secret Авито. Сохранять страницу отдельно не нужно.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $webhook_url = $this->buildAvitoWebhookUrl($channel);
        if (strpos($webhook_url, 'https://') !== 0) {
            echo json_encode(['success' => false, 'error' => 'Webhook Авито должен быть доступен по HTTPS на 443 порту.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $provider = new AvitoProvider($this->registry);
        $model_messenger = new MessengerModel($this->registry);
        $model_setting = new SettingModel($this->registry);
         
         
        $sync = $provider->syncWebhook($channel, $token, $webhook_url, '', [
            'persist_credentials' => false,
        ]);

        if (empty($sync['success'])) {
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'last_error' => (string)($sync['error'] ?? 'avito_sync_failed'),
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'Авито не подтвердил подключение: ' . (string)($sync['error'] ?? 'ошибка API'),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $bot = is_array($sync['bot'] ?? null) ? $sync['bot'] : [];
        $synced_at = date('Y-m-d H:i:s');
        $fresh_channel = $model_messenger->getChannelByProvider('avito');
        if ($fresh_channel && empty($config['credentials_changed'])) {
            $fresh_credentials = $this->decodeChannelCredentials($fresh_channel);
            foreach (['access_token', 'access_token_expires'] as $cache_key) {
                if (array_key_exists($cache_key, $fresh_credentials)) {
                    $credentials[$cache_key] = $fresh_credentials[$cache_key];
                }
            }
        }
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'webhook_synced_at' => $synced_at,
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'last_error' => '',
        ]);

        $history_backfill_queued = (new AvitoHistorySyncService($this->registry))->enqueueBackfill(
            (int)$channel['channel_id'],
            300,
            'settings_sync'
        );
        $avatar_backfill_queued = 0;
        if ($model_setting->get('chat_messenger_avatars_status', '1') === '1') {
            $avatar_backfill_queued = (new ContactAvatarService($this->registry))->enqueueBackfill(300);
        }

        echo json_encode([
            'success' => true,
            'message' => 'Интеграция Авито подключена.',
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'synced_at' => $synced_at,
            'webhook_url' => $webhook_url,
            'client_id_mask' => SecretBox::mask((string)$config['client_id']),
            'client_secret_mask' => SecretBox::mask((string)$config['client_secret']),
            'history_backfill_queued' => $history_backfill_queued,
            'avatar_backfill_queued' => $avatar_backfill_queued,
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function avitoHealth() {
        $is_htmx = $this->isHtmxSettingsRequest();
        header('Content-Type: application/json; charset=utf-8');
        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            if ($is_htmx) {
                $this->sendAvitoHealthHtmx(['success' => false, 'error' => 'Метод не поддерживается.'], 405);
            }
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if (!$this->verifySettingsNonce()) {
            if ($is_htmx) {
                $this->sendAvitoHealthHtmx(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], 403);
            }
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        try {
            $report = (new AvitoHealthService($this->registry))->report();
            if ($is_htmx) {
                $this->sendAvitoHealthHtmx($report);
            }
            echo json_encode($report, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } catch (\Throwable $e) {
            if ($is_htmx) {
                $this->sendAvitoHealthHtmx([
                    'success' => false,
                    'error' => 'Проверка Avito Health не завершилась. Проверьте журнал PHP и повторите запрос.',
                ], 500);
            }
            http_response_code(500);
            echo json_encode([
                'success' => false,
                'error' => 'Проверка Avito Health не завершилась. Проверьте журнал PHP и повторите запрос.',
            ], JSON_UNESCAPED_UNICODE);
        }
        exit;
    }

    public function youlaWebhookSync() {
        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;
        }

        $channel = $this->ensureYoulaChannel();
        $config = $this->buildYoulaChannelConfigFromPost($channel, true);
        $credentials = $config['credentials'];
        $token = (string)$config['token'];
        if (!empty($config['token_invalid'])) {
            echo json_encode(['success' => false, 'error' => 'Bearer token Юлы содержит недопустимые символы или пуст после префикса Bearer. Скопируйте токен заново.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if ($token === '') {
            echo json_encode(['success' => false, 'error' => 'Введите Bearer token Юлы.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $webhook_url = $this->buildYoulaWebhookUrl($channel);
        if (strpos($webhook_url, 'https://') !== 0) {
            echo json_encode(['success' => false, 'error' => 'Webhook Юлы должен быть доступен по HTTPS.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $model_messenger = new MessengerModel($this->registry);
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'last_error' => '',
        ]);

        $sync = (new YoulaProvider($this->registry))->syncWebhook($channel, $token, $webhook_url, '');
        if (empty($sync['success'])) {
            $sync_error = $this->formatYoulaSyncError($sync, 'youla_sync_failed');
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'last_error' => $sync_error,
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'Юла не подтвердила доступ: ' . $sync_error,
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $bot = is_array($sync['bot'] ?? null) ? $sync['bot'] : [];
        $synced_at = date('Y-m-d H:i:s');
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'webhook_synced_at' => $synced_at,
            'bot_user_id' => '',
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => '',
            'last_error' => '',
        ]);

        echo json_encode([
            'success' => true,
            'message' => 'Доступ Юлы проверен. Webhook URL готов для подключения в партнерском кабинете Юлы.',
            'manual_webhook' => 1,
            'bot_user_id' => '',
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => '',
            'synced_at' => $synced_at,
            'webhook_url' => $webhook_url,
            'token_mask' => SecretBox::mask((string)$config['token_value']),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function telegramWebhookSync() {
        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;
        }

        $channel = $this->ensureTelegramChannel();
        $credentials = $this->decodeChannelCredentials($channel);
        $settings = $this->decodeChannelSettings($channel);
        $token_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_telegram_bot_token'] ?? ''));
        if ($token_input !== '') {
            $credentials['bot_token'] = $this->encryptChannelSecret($token_input);
        }
        $token = $this->decryptChannelSecret((string)($credentials['bot_token'] ?? ''));
        $secret = $this->decryptChannelSecret((string)($credentials['webhook_secret'] ?? ''));

        if ($token === '') {
            echo json_encode(['success' => false, 'error' => 'Сначала сохраните токен Telegram-бота.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if ($secret === '') {
            $secret = $this->generateMaxWebhookSecret();
            $credentials['webhook_secret'] = $this->encryptChannelSecret($secret);
        }

        $webhook_url = $this->buildTelegramWebhookUrl($channel);
        if (strpos($webhook_url, 'https://') !== 0) {
            echo json_encode(['success' => false, 'error' => 'Webhook Telegram должен быть доступен по HTTPS на 443 порту.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $provider = new TelegramProvider($this->registry);
        $sync = $provider->syncWebhook($channel, $token, $webhook_url, $secret);

        $model_messenger = new MessengerModel($this->registry);
        if (empty($sync['success'])) {
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'last_error' => (string)($sync['error'] ?? 'telegram_sync_failed'),
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'Telegram не подтвердил подключение: ' . (string)($sync['error'] ?? 'ошибка API'),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $bot = is_array($sync['bot'] ?? null) ? $sync['bot'] : [];
        $synced_at = date('Y-m-d H:i:s');
        $settings['delivery_mode'] = 'webhook';
        $settings['webhook_synced_by_admin_at'] = $synced_at;
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'settings_json' => json_encode($settings, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'webhook_synced_at' => $synced_at,
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'last_error' => '',
        ]);

        echo json_encode([
            'success' => true,
            'message' => 'Webhook Telegram подключен.',
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'synced_at' => $synced_at,
            'webhook_url' => $webhook_url,
            'token_mask' => SecretBox::mask($token),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function vkWebhookSync() {
        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;
        }

        $channel = $this->ensureVkChannel();
        $config = $this->buildVkChannelConfigFromPost($channel, true);
        $credentials = $config['credentials'];
        $token = (string)$config['token'];
        $secret_key = (string)$config['secret_key'];
        $confirmation_code = (string)$config['confirmation_code'];
        $token_changed = !empty($config['token_changed']);

        if ($token === '') {
            echo json_encode(['success' => false, 'error' => 'Введите Токен сообщества VK. Сохранять страницу отдельно не нужно.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $webhook_url = $this->buildVkWebhookUrl($channel);
        $stored_webhook_url = (string)($channel['webhook_url'] ?? '');
        if (strpos($webhook_url, 'https://') !== 0) {
            echo json_encode(['success' => false, 'error' => 'Webhook VK должен быть доступен по HTTPS на 443 порту.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $model_messenger = new MessengerModel($this->registry);
        $this->clearVkRateLimit($credentials);

        $initial_update = [
            'status' => 'active',
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $webhook_url,
            'last_error' => '',
        ];
        if ($token_changed) {
            $initial_update['webhook_synced_at'] = null;
        }
        $model_messenger->updateChannel((int)$channel['channel_id'], $initial_update);
        $channel['status'] = 'active';
        $channel['credentials_json'] = $initial_update['credentials_json'];
        $channel['webhook_url'] = $webhook_url;
        if ($token_changed) {
            $channel['webhook_synced_at'] = '';
        }

        $provider = new VkProvider($this->registry);
        $saved_bot_info = $this->buildVkSavedBotInfo($config, $channel);
        $bot_info = $saved_bot_info;
        if ($bot_info === null) {
            $bot_info = $provider->getBotInfo($token);
        }

        if (empty($bot_info['success'])) {
            if (!empty($bot_info['rate_limited'])) {
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'active',
                    'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                    'webhook_url' => $webhook_url,
                    'last_error' => '',
                ]);
                echo json_encode($this->vkRateLimitResponse($bot_info, $webhook_url, $token, '', $this->buildVkSetupSteps($channel, $credentials, $token, (string)($bot_info['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                exit;
            }
            $bot_error = $this->formatVkSyncError($bot_info, 'vk_token_failed');
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'webhook_synced_at' => null,
                'last_error' => $bot_error,
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'VK не подтвердил токен: ' . $bot_error,
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $group_id = preg_replace('/\D+/', '', (string)($bot_info['id'] ?? ''));
        if ($group_id === '') {
            echo json_encode(['success' => false, 'error' => 'VK не вернул id сообщества. Проверьте Group Access Token.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $vk_bot_username = trim((string)($bot_info['username'] ?? ''));
        $credentials['vk_group_id'] = $group_id;
        $credentials['api_version'] = VkProvider::API_VERSION;

        $community_settings_group_id = preg_replace('/\D+/', '', (string)($credentials['community_settings_group_id'] ?? ''));
        $community_settings_ready = (!$token_changed && trim((string)($channel['webhook_synced_at'] ?? '')) !== '')
            || ($community_settings_group_id === $group_id
                && trim((string)($credentials['community_settings_enabled_at'] ?? '')) !== '');
        if (!$community_settings_ready) {
            $this->pauseVkSetupStep();
            $permissions = $provider->getTokenPermissions($token);
            if (empty($permissions['success'])) {
                if (!empty($permissions['rate_limited'])) {
                    $model_messenger->updateChannel((int)$channel['channel_id'], [
                        'status' => 'active',
                        'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                        'webhook_url' => $webhook_url,
                        'webhook_synced_at' => null,
                        'bot_user_id' => (string)($bot_info['id'] ?? ''),
                        'bot_name' => (string)($bot_info['name'] ?? ''),
                        'bot_username' => $vk_bot_username,
                        'last_error' => '',
                    ]);
                    echo json_encode($this->vkRateLimitResponse($permissions, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($permissions['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                    exit;
                }

                $permissions_error = $this->formatVkSyncError($permissions, 'token_permissions_failed');
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'error',
                    'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                    'webhook_url' => $webhook_url,
                    'webhook_synced_at' => null,
                    'bot_user_id' => (string)($bot_info['id'] ?? ''),
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'bot_username' => $vk_bot_username,
                    'last_error' => $permissions_error,
                ]);
                echo json_encode([
                    'success' => false,
                    'error' => 'VK не вернул права Group Access Token: ' . $permissions_error . '. Создайте ключ сообщества с правами «Сообщения сообщества» и «Управление сообществом».',
                    'detail' => 'EasyChat не смог проверить права VK-ключа. Без прав messages and groups автоматическая настройка Callback API невозможна.',
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'webhook_url' => $webhook_url,
                    'api_version' => VkProvider::API_VERSION,
                    'token_mask' => SecretBox::mask($token),
                    'setup_steps' => $this->buildVkSetupSteps($channel, $credentials, $token, (string)($permissions['vk_step'] ?? 'token_permissions')),
                ], JSON_UNESCAPED_UNICODE);
                exit;
            }

            $missing_permissions = [];
            if (!$provider->hasTokenPermission($permissions, 'messages')) {
                $missing_permissions[] = 'messages';
            }
            if (!$provider->hasTokenPermission($permissions, 'groups')) {
                $missing_permissions[] = 'groups';
            }
            if (!$provider->hasTokenPermission($permissions, 'photos')) {
                $missing_permissions[] = 'photos';
            }
            if (!$provider->hasTokenPermission($permissions, 'docs')) {
                $missing_permissions[] = 'docs';
            }
            if ($missing_permissions) {
                $missing_text = implode(', ', $missing_permissions);
                $permissions_error = 'vk_token_missing_scope: ' . $missing_text;
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'error',
                    'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                    'webhook_url' => $webhook_url,
                    'webhook_synced_at' => null,
                    'bot_user_id' => (string)($bot_info['id'] ?? ''),
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'bot_username' => $vk_bot_username,
                    'last_error' => $permissions_error,
                ]);
                echo json_encode([
                    'success' => false,
                    'error' => 'В VK-ключе не хватает прав: ' . $missing_text . '. Создайте новый Group Access Token с правами «Сообщения сообщества», «Управление сообществом», «Фотографии» и «Документы».',
                    'detail' => 'Текущий VK-ключ не может полноценно настроить канал и отправлять изображения или документы обратно во ВКонтакте. Создайте ключ заново и отметьте права messages, groups, photos и docs.',
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'webhook_url' => $webhook_url,
                    'api_version' => VkProvider::API_VERSION,
                    'token_mask' => SecretBox::mask($token),
                    'missing_permissions' => $missing_permissions,
                    'setup_steps' => $this->buildVkSetupSteps($channel, $credentials, $token, 'token_permissions'),
                ], JSON_UNESCAPED_UNICODE);
                exit;
            }

            $this->pauseVkSetupStep();
            $community_settings = $provider->ensureCommunityMessagesEnabled($token, $group_id);
            if (!empty($community_settings['success'])) {
                $credentials['community_settings_group_id'] = $group_id;
                $credentials['community_settings_enabled_at'] = date('Y-m-d H:i:s');
            } elseif (!empty($community_settings['rate_limited'])) {
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'active',
                    'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                    'webhook_url' => $webhook_url,
                    'webhook_synced_at' => null,
                    'bot_user_id' => (string)($bot_info['id'] ?? ''),
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'bot_username' => $vk_bot_username,
                    'last_error' => '',
                ]);
                echo json_encode($this->vkRateLimitResponse($community_settings, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($community_settings['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                exit;
            } else {
                $community_error = $this->formatVkSyncError($community_settings, 'community_settings_failed');
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'error',
                    'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                    'webhook_url' => $webhook_url,
                    'webhook_synced_at' => null,
                    'bot_user_id' => (string)($bot_info['id'] ?? ''),
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'bot_username' => $vk_bot_username,
                    'last_error' => $community_error,
                ]);
                echo json_encode([
                    'success' => false,
                    'error' => 'VK не включил сообщения сообщества и возможности ботов автоматически: ' . $community_error . '. Проверьте, что Group Access Token создан для этого сообщества и имеет право «Управление сообществом».',
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'webhook_url' => $webhook_url,
                    'api_version' => VkProvider::API_VERSION,
                    'token_mask' => SecretBox::mask($token),
                    'setup_steps' => $this->buildVkSetupSteps($channel, $credentials, $token, (string)($community_settings['vk_step'] ?? 'community_settings')),
                ], JSON_UNESCAPED_UNICODE);
                exit;
            }
        }

        $confirmation = [];
         
         
         
         
        $this->pauseVkSetupStep();
        $confirmation = $provider->getCallbackConfirmationCode($token, $group_id);
        if (!empty($confirmation['success'])) {
            $confirmation_code = (string)$confirmation['code'];
            $credentials['confirmation_code'] = $this->encryptChannelSecret($confirmation_code);
        } elseif (!empty($confirmation['rate_limited'])) {
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'active',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'bot_user_id' => (string)($bot_info['id'] ?? ''),
                'bot_name' => (string)($bot_info['name'] ?? ''),
                'bot_username' => $vk_bot_username,
                'last_error' => '',
            ]);
            echo json_encode($this->vkRateLimitResponse($confirmation, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($confirmation['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
            exit;
        }

        if ($confirmation_code === '') {
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
                'webhook_url' => $webhook_url,
                'webhook_synced_at' => null,
                'bot_user_id' => (string)($bot_info['id'] ?? ''),
                'bot_name' => (string)($bot_info['name'] ?? ''),
                'bot_username' => $vk_bot_username,
                'last_error' => $this->formatVkSyncError($confirmation, 'confirmation_code_missing'),
            ]);
            echo json_encode([
                'success' => false,
                'error' => 'VK не выдал Confirmation Code автоматически. Проверьте, что Group Access Token создан для сообщества и имеет право «Управление сообществом».',
                'bot_name' => (string)($bot_info['name'] ?? ''),
                'webhook_url' => $webhook_url,
                'api_version' => VkProvider::API_VERSION,
                'token_mask' => SecretBox::mask($token),
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $had_saved_valid_secret = $secret_key !== '' && $this->isValidVkSecretKey($secret_key);
        if (!$had_saved_valid_secret) {
            $secret_key = $this->generateVkSecretKey();
        }
        $secret_key = $this->normalizeVkSecretKey($secret_key);
        $credentials['secret_key'] = $this->encryptChannelSecret($secret_key);
        $credentials['vk_group_id'] = $group_id;
        $credentials['api_version'] = VkProvider::API_VERSION;
        $this->clearVkRateLimit($credentials);

        $channel['credentials_json'] = json_encode($credentials, JSON_UNESCAPED_UNICODE);
        $channel['webhook_url'] = $webhook_url;

        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => $channel['credentials_json'],
            'webhook_url' => $webhook_url,
            'bot_user_id' => (string)($bot_info['id'] ?? ''),
            'bot_name' => (string)($bot_info['name'] ?? ''),
            'bot_username' => $vk_bot_username,
            'last_error' => '',
        ]);

        $sync = null;
        $stored_callback_server_id = preg_replace('/\D+/', '', (string)($credentials['callback_server_id'] ?? ''));
        if ($saved_bot_info !== null && $had_saved_valid_secret && $stored_callback_server_id !== '' && $stored_webhook_url === $webhook_url) {
            $channel_already_synced = trim((string)($channel['webhook_synced_at'] ?? '')) !== '';
            $callback_server_failed = false;
            if ($channel_already_synced) {
                $this->pauseVkSetupStep();
                $servers_check = $provider->getCallbackServers($token, $group_id);
                if (!empty($servers_check['rate_limited'])) {
                    echo json_encode($this->vkRateLimitResponse($servers_check, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($servers_check['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                    exit;
                }
                $callback_server_failed = !empty($servers_check['success'])
                    && $provider->callbackServerHasFailed($servers_check['items'], $stored_callback_server_id);

                if (!$callback_server_failed) {
                    $this->pauseVkSetupStep();
                    $settings_check = $provider->getCallbackSettings($token, $group_id, $stored_callback_server_id);
                    if (!empty($settings_check['success']) && $provider->hasIncomingMessageEvent($settings_check)) {
                        $sync = [
                            'success' => true,
                            'bot' => [
                                'user_id' => (string)($bot_info['id'] ?? ''),
                                'name' => (string)($bot_info['name'] ?? ''),
                                'username' => $vk_bot_username,
                                'profile_url' => (string)($bot_info['profile_url'] ?? ''),
                            ],
                            'server_id' => $stored_callback_server_id,
                            'api_version' => VkProvider::API_VERSION,
                            'response' => $settings_check['json'] ?? [],
                            'http_code' => $settings_check['http_code'] ?? 200,
                        ];
                    } elseif (!empty($settings_check['rate_limited'])) {
                        echo json_encode($this->vkRateLimitResponse($settings_check, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($settings_check['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                        exit;
                    }
                }
            }

            if ($sync === null && !$callback_server_failed) {
                $this->pauseVkSetupStep();
                $settings = $provider->setCallbackSettings($token, $group_id, $stored_callback_server_id);
                if (!empty($settings['success'])) {
                    $sync = [
                        'success' => true,
                        'bot' => [
                            'user_id' => (string)($bot_info['id'] ?? ''),
                            'name' => (string)($bot_info['name'] ?? ''),
                            'username' => $vk_bot_username,
                            'profile_url' => (string)($bot_info['profile_url'] ?? ''),
                        ],
                        'server_id' => $stored_callback_server_id,
                        'api_version' => VkProvider::API_VERSION,
                        'response' => $settings['json'] ?? [],
                        'http_code' => $settings['http_code'] ?? 200,
                    ];
                } elseif (!empty($settings['rate_limited'])) {
                    echo json_encode($this->vkRateLimitResponse($settings, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($settings['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                    exit;
                }
            }
        }

        if ($sync === null) {
            $this->pauseVkSetupStep();
            $sync = $provider->syncWebhook($channel, $token, $webhook_url, $secret_key, $bot_info);
        }
        if (empty($sync['success'])) {
            if (!empty($sync['rate_limited'])) {
                $sync_server_id = preg_replace('/\D+/', '', (string)($sync['server_id'] ?? ''));
                if ((string)($sync['vk_step'] ?? '') === 'callback_settings' && $sync_server_id !== '') {
                    $credentials['callback_server_id'] = $sync_server_id;
                }
                $channel['credentials_json'] = json_encode($credentials, JSON_UNESCAPED_UNICODE);
                $model_messenger->updateChannel((int)$channel['channel_id'], [
                    'status' => 'active',
                    'credentials_json' => $channel['credentials_json'],
                    'webhook_url' => $webhook_url,
                    'bot_user_id' => (string)($bot_info['id'] ?? ''),
                    'bot_name' => (string)($bot_info['name'] ?? ''),
                    'bot_username' => $vk_bot_username,
                    'last_error' => '',
                ]);
                echo json_encode($this->vkRateLimitResponse($sync, $webhook_url, $token, (string)($bot_info['name'] ?? ''), $this->buildVkSetupSteps($channel, $credentials, $token, (string)($sync['vk_step'] ?? ''))), JSON_UNESCAPED_UNICODE);
                exit;
            }
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'status' => 'error',
                'credentials_json' => $channel['credentials_json'],
                'webhook_url' => $webhook_url,
                'webhook_synced_at' => null,
                'bot_user_id' => (string)($bot_info['id'] ?? ''),
                'bot_name' => (string)($bot_info['name'] ?? ''),
                'bot_username' => $vk_bot_username,
                'last_error' => (string)($sync['error'] ?? 'vk_sync_failed'),
            ]);

            echo json_encode([
                'success' => false,
                'error' => 'Автоматическая настройка VK не завершилась: ' . $this->formatVkSyncError($sync, 'ошибка API') . '. Проверьте права Group Access Token и лимит Callback servers в сообществе VK.',
            ], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $bot = is_array($sync['bot'] ?? null) ? $sync['bot'] : [];
        $synced_at = date('Y-m-d H:i:s');
        $credentials['callback_server_id'] = (string)($sync['server_id'] ?? '');
        $channel_credentials = json_encode($credentials, JSON_UNESCAPED_UNICODE);
        $synced_channel = $channel;
        $synced_channel['webhook_url'] = $webhook_url;
        $synced_channel['webhook_synced_at'] = $synced_at;
        $synced_channel['status'] = 'active';
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => 'active',
            'credentials_json' => $channel_credentials,
            'webhook_url' => $webhook_url,
            'webhook_synced_at' => $synced_at,
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'last_error' => '',
        ]);

        echo json_encode([
            'success' => true,
            'message' => 'Webhook VK подключен автоматически.',
            'bot_user_id' => (string)($bot['user_id'] ?? ''),
            'bot_name' => (string)($bot['name'] ?? ''),
            'bot_username' => (string)($bot['username'] ?? ''),
            'bot_profile_url' => (string)($bot['profile_url'] ?? ''),
            'synced_at' => $synced_at,
            'webhook_url' => $webhook_url,
            'callback_server_id' => (string)($sync['server_id'] ?? ''),
            'api_version' => VkProvider::API_VERSION,
            'token_mask' => SecretBox::mask($token),
            'secret_key_mask' => SecretBox::mask($secret_key),
            'setup_steps' => $this->buildVkSetupSteps($synced_channel, $credentials, $token),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    public function tlsDiagnostics() {
        $is_htmx = $this->isHtmxSettingsRequest();
        header('Content-Type: application/json; charset=utf-8');
        if ((\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] ?? '') !== 'POST') {
            if ($is_htmx) {
                $this->sendTlsDiagnosticsHtmx(['success' => false, 'error' => 'Метод не поддерживается.'], 405);
            }
            http_response_code(405);
            echo json_encode(['success' => false, 'error' => 'Метод не поддерживается.'], JSON_UNESCAPED_UNICODE);
            exit;
        }
        if (!$this->verifySettingsNonce()) {
            if ($is_htmx) {
                $this->sendTlsDiagnosticsHtmx(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], 403);
            }
            http_response_code(403);
            echo json_encode(['success' => false, 'error' => 'Сессия настроек устарела. Обновите страницу.'], JSON_UNESCAPED_UNICODE);
            exit;
        }

        $client = $this->externalHttp();
        $trust = $client->getTrustManager();
        $proxy_policy = new ProxyPolicy($this->registry);
        $refresh = $trust->refreshRussianCaBundle(!empty(\EasyChat\Library\RequestContext::post()['force_ca']));
        $checks = [
            ['label' => 'Telegram API', 'url' => 'https://api.telegram.org/', 'profile' => 'telegram', 'proxy_profile' => 'messenger_telegram'],
            ['label' => 'MAX API', 'url' => 'https://platform-api2.max.ru/', 'profile' => 'max', 'proxy_profile' => 'messenger_max'],
            ['label' => 'VK API', 'url' => 'https://api.vk.com/', 'profile' => 'vk', 'proxy_profile' => 'messenger_vk'],
            ['label' => 'Avito API', 'url' => 'https://api.avito.ru/', 'profile' => 'avito', 'proxy_profile' => 'messenger_avito'],
            ['label' => 'Youla Partner API', 'url' => 'https://partner-api.youla.ru/', 'profile' => 'youla', 'proxy_profile' => 'messenger_youla'],
        ];

        $results = [];
        foreach ($checks as $check) {
            $proxy_state = $proxy_policy->routeState($check['proxy_profile']);
            $result = $client->request('HEAD', $check['url'], [
                'connect_timeout' => 4,
                'timeout' => 7,
                'trust_profile' => $check['profile'],
                'proxy_profile' => $check['proxy_profile'],
            ]);
            $error_description = TransportErrorCatalog::describe(
                (int)($result['curl_errno'] ?? 0),
                (string)($proxy_state['code'] ?? '')
            );
            $results[] = [
                'label' => $check['label'],
                'host' => parse_url($check['url'], PHP_URL_HOST),
                'proxy_profile' => $check['proxy_profile'],
                'proxy_mode' => $proxy_state['code'],
                'proxy_label' => $proxy_state['label'],
                'http_code' => (int)($result['http_code'] ?? 0),
                'transport_ok' => !empty($result['transport_ok']),
                'tls_fallback_used' => !empty($result['tls_fallback_used']),
                'trust_bundle' => (string)($result['trust_bundle'] ?? ''),
                'curl_errno' => (int)($result['curl_errno'] ?? 0),
                'error_label' => !empty($result['transport_ok']) ? '' : (string)$error_description['label'],
                'error_hint' => !empty($result['transport_ok']) ? '' : (string)$error_description['hint'],
                'error' => !empty($result['transport_ok']) ? '' : (string)($result['curl_error'] ?? ''),
            ];
        }

        $payload = [
            'success' => true,
            'ca_refresh' => $refresh,
            'snapshot' => $trust->diagnosticsSnapshot(),
            'checks' => $results,
        ];

        if ($is_htmx) {
            $this->sendTlsDiagnosticsHtmx($payload);
        }

        echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        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 sendAvitoHealthHtmx(array $payload, $status = 200) {
        (new HtmxFragmentRenderer())->sendHtml(
            IntegrationFragments::avitoHealthResult($payload, true),
            (int)$status,
            [
                'HX-Retarget' => '#avito-health-result',
                'HX-Reswap' => HtmxFragmentRenderer::integrationDiagnosticsSwap(),
            ]
        );
    }

    private function sendTlsDiagnosticsHtmx(array $payload, $status = 200) {
        (new HtmxFragmentRenderer())->sendHtml(
            IntegrationFragments::tlsDiagnosticsResult($payload, true),
            (int)$status,
            [
                'HX-Retarget' => '#tls-diagnostics-result',
                'HX-Reswap' => HtmxFragmentRenderer::integrationDiagnosticsSwap(),
            ]
        );
    }

    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'] : 'max';
        $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'] : 'max');
        $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;
    }

     
    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 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 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 jsonMaxAuth(callable $handler, bool $include_health = true) {
        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();
        }

        $channel = $this->ensureMaxChannel();
        $service = new MaxAuthService($this->registry);
        if (session_status() === PHP_SESSION_ACTIVE) {
             
             
             
            session_write_close();
        }
        try {
            $result = call_user_func($handler, $service, $channel);
        } catch (MaxSessionLifecycleBusyException $e) {
            http_response_code(409);
            $result = [
                'success' => false,
                'error' => 'max_session_busy',
                'message' => 'Сессия MAX сейчас обновляется. Повторите действие через несколько секунд.',
                'state' => $service->authState($channel),
            ];
        } catch (\Throwable $e) {
            http_response_code(500);
            $result = [
                'success' => false,
                'error' => 'max_auth_failed',
                'message' => 'Не удалось обновить вход MAX. Повторите действие или откройте диагностику.',
            ];
            if (!$include_health) {
                $result['state'] = $service->authState($channel);
            }
        }
        if (!is_array($result)) {
            $result = ['success' => false, 'error' => 'max_auth_invalid_result'];
        }
        $auth_status = is_array($result['state'] ?? null)
            ? (string)($result['state']['status'] ?? '')
            : '';
        $terminal_auth_state = in_array($auth_status, ['connected', 'error', 'expired', 'password_required'], true);
        $refresh_health = false;
        if ($include_health) {
            $refresh_health = true;
        }
        if ($terminal_auth_state) {
            $refresh_health = true;
        }
        if ($refresh_health) {
            $fresh_channel = $this->ensureMaxChannel();
            try {
                $health = (new MaxHealthPresenter($this->registry))->build($fresh_channel);
                $result['health'] = $this->publicMaxHealth($health);
                $result['personal_max'] = $this->publicMaxPersonal(
                    (new MaxPersonalPresenter($this->registry))->build(
                        is_array($health['channel'] ?? null) ? $health['channel'] : $fresh_channel,
                        $health,
                        is_array($result['state'] ?? null) ? $result['state'] : []
                    )
                );
            } catch (\Throwable $e) {
                 
                 
                 
            }
        }
        echo json_encode($result, JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function jsonWhatsAppAuth(callable $handler) {
        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();
        }
        $channel = $this->ensureWhatsAppPersonalChannel();
        $service = new WhatsAppPersonalAuthService($this->registry);
        if (session_status() === PHP_SESSION_ACTIVE) {
            session_write_close();
        }
        try {
            $result = call_user_func($handler, $service, $channel);
        } catch (\Throwable $e) {
            http_response_code(500);
            $result = [
                'success' => false,
                'error' => 'whatsapp_action_failed',
                'message' => 'Не удалось выполнить действие. Повторите попытку.',
            ];
        }
        if (!is_array($result)) {
            $result = ['success' => false, 'error' => 'whatsapp_invalid_result'];
        }
        try {
            $fresh = $this->ensureWhatsAppPersonalChannel();
            $result['whatsapp_personal'] = (new WhatsAppPersonalPresenter($this->registry))->build($fresh);
        } catch (\Throwable $ignored) {
             
        }
        echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

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

    private function resolveLogFilename($type) {
        $map = [
            'max-bot' => 'max-bot.log',
            'max' => 'max.log',
            'avito' => 'avito.log',
            'youla' => 'youla.log',
            'telegram' => 'telegram.log',
        ];
        $type = strtolower(trim((string)$type));
        return $map[$type] ?? '';
    }

    private function ensureMaxBotChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel(MaxBotProvider::PROVIDER, 'MAX-бот');
    }

    private function ensureMaxChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel(MaxProvider::PROVIDER, 'MAX');
    }

    private function ensureWhatsAppPersonalChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel(WhatsAppPersonalProvider::PROVIDER, 'Личный WhatsApp');
    }

    private function buildMaxBotConnectionState(array $channel, $token) {
        $has_token = trim((string)$token) !== '';
        $status = (string)($channel['status'] ?? 'disabled');
        $last_error = trim((string)($channel['last_error'] ?? ''));
        $synced_at = trim((string)($channel['webhook_synced_at'] ?? ''));

        if (!$has_token) {
            return [
                'code' => 'empty',
                'badge_class' => 'bg-light text-secondary border',
                'icon' => 'fa-circle-o',
                'title' => 'Токен не добавлен',
                'detail' => 'Вставьте токен MAX-бота, сохраните настройки и выполните проверку подключения.',
            ];
        }
        if ($last_error !== '') {
            return [
                'code' => 'error',
                'badge_class' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'icon' => 'fa-exclamation-triangle',
                'title' => 'Ошибка подключения',
                'detail' => 'MAX не подтвердил подключение. Проверьте токен и повторите проверку.',
            ];
        }
        if ($status === 'active' && $synced_at !== '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-success-subtle text-success border border-success-subtle',
                'icon' => 'fa-check-circle',
                'title' => 'Подключено',
                'detail' => 'Webhook MAX-бота настроен автоматически. Входящие сообщения будут приходить в EasyChat.',
            ];
        }
        if ($status === 'active') {
            return [
                'code' => 'pending',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Нужна проверка',
                'detail' => 'Токен сохранен, но webhook еще не подтвержден MAX.',
            ];
        }
        return [
            'code' => 'disabled',
            'badge_class' => 'bg-light text-secondary border',
            'icon' => 'fa-pause-circle',
            'title' => 'Отключено',
            'detail' => 'Токен сохранен, но канал выключен переключателем MAX.',
        ];
    }

    private function buildMaxConnectionState(array $channel, array $health = []) {
        if (!$health) {
            $health = (new MaxHealthPresenter($this->registry))->build($channel);
        }
        return is_array($health['summary'] ?? null)
            ? $health['summary']
            : [
                'code' => 'pending',
                'badge_class' => 'bg-info-subtle text-info border border-info-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Проверяем состояние',
                'detail' => 'EasyChat обновляет сведения о личном MAX.',
            ];
    }

    private function buildMaxDedicatedCronState(array $channel, array $health = []): array {
        if (!$health) {
            $health = (new MaxHealthPresenter($this->registry))->build($channel);
        }
        foreach ((array)($health['items'] ?? []) as $item) {
            if ((string)($item['key'] ?? '') !== 'cron') {
                continue;
            }
            $state = (string)($item['state'] ?? 'pending');
            $badges = [
                'ok' => 'bg-success-subtle text-success border border-success-subtle',
                'warning' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'error' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'pending' => 'bg-info-subtle text-info border border-info-subtle',
                'disabled' => 'bg-light text-secondary border',
            ];
            $detail = (string)($item['detail'] ?? '');
            if (!empty($item['time'])) {
                $detail .= ($detail !== '' ? ' ' : '') . 'Последняя проверка: ' . (string)$item['time'] . '.';
            }
            return [
                'code' => $state,
                'badge_class' => $badges[$state] ?? $badges['pending'],
                'icon' => (string)($item['icon'] ?? 'fa-clock-o'),
                'title' => (string)($item['title'] ?? 'Проверяем Cron'),
                'detail' => $detail,
            ];
        }
        return [
            'code' => 'pending',
            'badge_class' => 'bg-info-subtle text-info border border-info-subtle',
            'icon' => 'fa-clock-o',
            'title' => 'Проверяем Cron',
            'detail' => 'EasyChat обновляет сведения о планировщике.',
        ];
    }

    private function saveMaxBotChannelConfig() {
        $channel = $this->ensureMaxBotChannel();
        $credentials = $this->decodeChannelCredentials($channel);
        $status = isset(\EasyChat\Library\RequestContext::post()['chat_max_bot_status']) ? 'active' : 'disabled';
        $token_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_max_bot_token'] ?? ''));

        if ($token_input !== '') {
            $credentials['bot_token'] = $this->encryptChannelSecret($token_input);
        }
        if (empty($credentials['webhook_secret'])) {
            $credentials['webhook_secret'] = $this->encryptChannelSecret($this->generateMaxWebhookSecret());
        }

        $token = $this->decryptChannelSecret((string)($credentials['bot_token'] ?? ''));
        if ($status === 'active' && $token === '') {
            return ['success' => false, 'error' => 'Для включения MAX-бота укажите токен бота и сохраните настройки.'];
        }

        $model_messenger = new MessengerModel($this->registry);
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => $status,
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $this->buildMaxBotWebhookUrl($channel),
        ]);

        return ['success' => true];
    }

    private function publicMaxHealth(array $health): array {
        $summary = is_array($health['summary'] ?? null) ? $health['summary'] : [];
        $items = [];
        foreach ((array)($health['items'] ?? []) as $item) {
            if (!is_array($item)) {
                continue;
            }
            $items[] = [
                'key' => (string)($item['key'] ?? ''),
                'label' => (string)($item['label'] ?? ''),
                'state' => (string)($item['state'] ?? 'pending'),
                'title' => (string)($item['title'] ?? ''),
                'detail' => (string)($item['detail'] ?? ''),
                'time' => (string)($item['time'] ?? ''),
                'icon' => (string)($item['icon'] ?? 'fa-clock-o'),
            ];
        }
        return [
            'generation' => max(1, (int)($health['generation'] ?? 1)),
            'summary' => [
                'code' => (string)($summary['code'] ?? 'pending'),
                'title' => (string)($summary['title'] ?? 'Проверяем состояние'),
                'detail' => (string)($summary['detail'] ?? ''),
                'icon' => (string)($summary['icon'] ?? 'fa-clock-o'),
            ],
            'items' => $items,
            'session' => [
                'status' => (string)($health['session']['status'] ?? 'unavailable'),
                'single' => (int)($health['session']['session_count'] ?? 0) <= 1,
            ],
        ];
    }

    private function buildMaxPersonalPublic(): array {
        $channel = $this->ensureMaxChannel();
        $health = (new MaxHealthPresenter($this->registry))->build($channel);
        return $this->publicMaxPersonal(
            (new MaxPersonalPresenter($this->registry))->build(
                is_array($health['channel'] ?? null) ? $health['channel'] : $channel,
                $health
            )
        );
    }

    private function publicMaxPersonal(array $personal): array {
        $account = is_array($personal['account'] ?? null) ? $personal['account'] : [];
        $cron = is_array($personal['cron'] ?? null) ? $personal['cron'] : [];
        $attention = is_array($personal['attention'] ?? null) ? $personal['attention'] : [];
        $diagnostics = is_array($personal['diagnostics'] ?? null) ? $personal['diagnostics'] : [];
        $summary = is_array($personal['summary'] ?? null) ? $personal['summary'] : [];
        return [
            'ui_state' => (string)($personal['ui_state'] ?? 'wizard'),
            'step' => max(0, (int)($personal['step'] ?? 1)),
            'connected' => !empty($personal['connected']),
            'enabled' => !empty($personal['enabled']),
            'revision' => max(0, (int)($personal['revision'] ?? 0)),
            'account' => [
                'display_name' => (string)($account['display_name'] ?? 'Личный MAX'),
                'initials' => (string)($account['initials'] ?? 'M'),
                'phone_mask' => (string)($account['phone_mask'] ?? ''),
            ],
            'scope' => array_values(array_intersect(
                ['dialogs', 'channels', 'groups'],
                is_array($personal['scope'] ?? null) ? $personal['scope'] : []
            )),
            'dedicated_cron_mode' => (string)($personal['dedicated_cron_mode'] ?? 'conservative'),
            'cron' => [
                'state' => (string)($cron['state'] ?? 'missing'),
                'confirmed' => !empty($cron['confirmed']),
                'observed_runs' => max(0, (int)($cron['observed_runs'] ?? 0)),
                'required_runs' => 3,
                'last_started_at' => max(0, (int)($cron['last_started_at'] ?? 0)),
                'requires_attention' => !empty($cron['requires_attention']),
                'severity' => (string)($cron['severity'] ?? 'warning'),
                'diagnosis' => (string)($cron['diagnosis'] ?? ''),
                'observed_interval_seconds' => max(0, (int)($cron['observed_interval_seconds'] ?? 0)),
                'coverage_percent' => max(0, min(100, (int)($cron['coverage_percent'] ?? 100))),
                'title' => (string)($cron['title'] ?? ''),
                'detail' => (string)($cron['detail'] ?? ''),
            ],
            'attention' => [
                'visible' => !empty($attention['visible']),
                'level' => (string)($attention['level'] ?? 'warning') === 'error' ? 'error' : 'warning',
                'title' => (string)($attention['title'] ?? ''),
                'detail' => (string)($attention['detail'] ?? ''),
                'source' => (string)($attention['source'] ?? ''),
                'action' => in_array((string)($attention['action'] ?? ''), ['reconnect', 'review'], true)
                    ? (string)$attention['action']
                    : 'review',
            ],
            'diagnostics' => [
                'active' => !empty($diagnostics['active']),
                'until_ts' => max(0, (int)($diagnostics['until_ts'] ?? 0)),
                'remaining_seconds' => max(0, (int)($diagnostics['remaining_seconds'] ?? 0)),
            ],
            'summary' => [
                'code' => (string)($summary['code'] ?? 'pending'),
                'title' => (string)($summary['title'] ?? 'Проверяем состояние'),
                'detail' => (string)($summary['detail'] ?? ''),
            ],
        ];
    }

    private function setMaxDiagnosticsLease(bool $enabled): void {
        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();
        }
        if (session_status() === PHP_SESSION_ACTIVE) {
             
             
            session_write_close();
        }
        $model_setting = new SettingModel($this->registry);
        $model_setting->setManyAtomic([
            'chat_max_logging' => '0',
            'chat_max_logging_until_ts' => $enabled ? (string)(time() + 900) : '0',
        ]);
        echo json_encode([
            'success' => true,
            'message' => $enabled ? 'Диагностика включена на 15 минут.' : 'Диагностика выключена.',
            'personal_max' => $this->buildMaxPersonalPublic(),
        ], JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function decodeChannelCredentials(array $channel) {
        $credentials = json_decode((string)($channel['credentials_json'] ?? ''), true);
        return is_array($credentials) ? $credentials : [];
    }

    private function decodeChannelSettings(array $channel) {
        $settings = json_decode((string)($channel['settings_json'] ?? ''), true);
        return is_array($settings) ? $settings : [];
    }

    private function encryptChannelSecret($secret) {
        $model_setting = new SettingModel($this->registry);
        return SecretBox::encrypt((string)$secret, $model_setting->get('chat_install_secret', ''));
    }

    private function decryptChannelSecret($secret) {
        $model_setting = new SettingModel($this->registry);
        return SecretBox::decrypt((string)$secret, $model_setting->get('chat_install_secret', ''));
    }

    private function decryptChannelSecretOrPlain($secret) {
        $secret = trim((string)$secret);
        if ($secret === '') {
            return '';
        }
        if (strpos($secret, 'enc:v1:') !== 0) {
            return $secret;
        }
        return $this->decryptChannelSecret($secret);
    }

    private function generateMaxWebhookSecret() {
        return 'mx-' . bin2hex(random_bytes(24));
    }

    private function generateVkSecretKey() {
        return 'vk' . bin2hex(random_bytes(20));
    }

    private function buildMaxBotWebhookUrl(array $channel) {
        return HTTP_SERVER . 'index.php?route=messenger/webhook&provider=max-bot&id=' . urlencode((string)$channel['public_id']);
    }

    private function ensureAvitoChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel('avito', 'Avito');
    }

    private function buildAvitoWebhookUrl(array $channel) {
        return HTTP_SERVER . 'index.php?route=messenger/webhook&provider=avito&id=' . urlencode((string)$channel['public_id']);
    }

    private function buildAvitoConnectionState(array $channel, $client_id) {
        $has_client_id = trim((string)$client_id) !== '';
        $status = (string)($channel['status'] ?? 'disabled');
        $last_error = trim((string)($channel['last_error'] ?? ''));
        $synced_at = trim((string)($channel['webhook_synced_at'] ?? ''));

        if (!$has_client_id) {
            return [
                'code' => 'empty',
                'badge_class' => 'bg-light text-secondary border',
                'icon' => 'fa-circle-o',
                'title' => 'Интеграция не настроена',
                'detail' => 'Введите Client ID и Client Secret из кабинета Авито: EasyChat сам сохранит их и подключит webhook.',
            ];
        }
        if ($last_error !== '') {
            return [
                'code' => 'error',
                'badge_class' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'icon' => 'fa-exclamation-triangle',
                'title' => 'Ошибка подключения',
                'detail' => 'Авито не подтвердил подключение. Ошибка: ' . $last_error,
            ];
        }
        if ($status === 'active' && $synced_at !== '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-success-subtle text-success border border-success-subtle',
                'icon' => 'fa-check-circle',
                'title' => 'Подключено',
                'detail' => 'Интеграция настроена. Сообщения из Авито будут приходить в EasyChat.',
            ];
        }
        if ($status === 'active') {
            return [
                'code' => 'pending',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Нужна проверка',
                'detail' => 'Учетные данные сохранены, но webhook еще не синхронизирован с Авито.',
            ];
        }
        return [
            'code' => 'disabled',
            'badge_class' => 'bg-light text-secondary border',
            'icon' => 'fa-pause-circle',
            'title' => 'Отключено',
            'detail' => 'Канал отключен переключателем.',
        ];
    }

    private function saveAvitoChannelConfig() {
        $channel = $this->ensureAvitoChannel();
        $config = $this->buildAvitoChannelConfigFromPost($channel, false);
        if (!empty($config['validation_error'])) {
            return ['success' => false, 'error' => $config['validation_error']];
        }
        $credentials = $config['credentials'];
        $status = $config['status'];
        $client_id = $config['client_id'];
        $client_secret = $config['client_secret'];

        if ($status === 'active' && ($client_id === '' || $client_secret === '')) {
            return ['success' => false, 'error' => 'Для включения интеграции Авито укажите Client ID и Client Secret.'];
        }

        $model_messenger = new MessengerModel($this->registry);
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => $status,
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $this->buildAvitoWebhookUrl($channel),
        ]);

        return ['success' => true];
    }

    private function buildAvitoChannelConfigFromPost(array $channel, $force_active) {
        $credentials = $this->decodeChannelCredentials($channel);
        $status = $force_active || isset(\EasyChat\Library\RequestContext::post()['chat_avito_status']) ? 'active' : 'disabled';
        $client_id_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_avito_client_id'] ?? ''));
        $client_secret_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_avito_client_secret'] ?? ''));
        $validation_error = $this->avitoClientIdLooksLikeUrl($client_id_input)
            ? 'В поле Client ID Авито попала ссылка. Удалите ее и вставьте Client ID из кабинета Авито.'
            : '';

        $token_payload = json_decode($this->decryptChannelSecret((string)($credentials['bot_token'] ?? '')), true);
        $stored_client_id = $this->decryptChannelSecretOrPlain((string)($credentials['client_id'] ?? ''));
        $stored_client_secret = $this->decryptChannelSecretOrPlain((string)($credentials['client_secret'] ?? ''));
        if ($this->avitoClientIdLooksLikeUrl($stored_client_id)) {
            $stored_client_id = '';
            unset($credentials['client_id'], $credentials['bot_token'], $credentials['access_token'], $credentials['access_token_expires']);
        }
        if (is_array($token_payload)) {
            if ($stored_client_id === '') {
                $stored_client_id = trim((string)($token_payload['client_id'] ?? ''));
            }
            if ($stored_client_secret === '') {
                $stored_client_secret = trim((string)($token_payload['client_secret'] ?? ''));
            }
        }

        $client_id = $client_id_input !== '' ? $client_id_input : $stored_client_id;
        $client_secret = $client_secret_input !== '' ? $client_secret_input : $stored_client_secret;
        $credentials_changed = ($client_id_input !== '' && !hash_equals($stored_client_id, $client_id_input))
            || ($client_secret_input !== '' && !hash_equals($stored_client_secret, $client_secret_input));

        if ($client_id !== '' && $client_secret !== '') {
            $credentials['client_id'] = $this->encryptChannelSecret($client_id);
            $credentials['client_secret'] = $this->encryptChannelSecret($client_secret);
            $credentials['bot_token'] = $this->encryptChannelSecret(json_encode([
                'client_id' => $client_id,
                'client_secret' => $client_secret,
            ], JSON_UNESCAPED_UNICODE));
            if ($credentials_changed) {
                unset($credentials['access_token'], $credentials['access_token_expires']);
            }
        }

        return [
            'status' => $status,
            'credentials' => $credentials,
            'client_id' => $client_id,
            'client_secret' => $client_secret,
            'token' => $client_id !== '' && $client_secret !== ''
                ? json_encode(['client_id' => $client_id, 'client_secret' => $client_secret], JSON_UNESCAPED_UNICODE)
                : '',
            'credentials_changed' => $credentials_changed,
            'validation_error' => $validation_error,
        ];
    }

    private function avitoClientIdLooksLikeUrl($client_id) {
        return preg_match('~^(?:[a-z][a-z0-9+.-]*:)?//~i', trim((string)$client_id)) === 1;
    }

    private function quarantineAutofilledAvitoCredentials(array $channel, array $credentials) {
        $stored_client_id = $this->decryptChannelSecretOrPlain((string)($credentials['client_id'] ?? ''));
        if (!$this->avitoClientIdLooksLikeUrl($stored_client_id)) {
            return $credentials;
        }

         
         
         
        unset($credentials['client_id'], $credentials['bot_token'], $credentials['access_token'], $credentials['access_token_expires']);
        (new MessengerModel($this->registry))->updateChannel((int)$channel['channel_id'], [
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'last_error' => 'invalid_client_id_autofill',
        ]);
        return $credentials;
    }

    private function ensureYoulaChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel('youla', 'Юла');
    }

    private function buildYoulaWebhookUrl(array $channel) {
        return HTTP_SERVER . 'index.php?route=messenger/webhook&provider=youla&id=' . urlencode((string)$channel['public_id']);
    }

    private function buildYoulaConnectionState(array $channel, $token) {
        $has_token = trim((string)$token) !== '';
        $status = (string)($channel['status'] ?? 'disabled');
        $last_error = trim((string)($channel['last_error'] ?? ''));
        $synced_at = trim((string)($channel['webhook_synced_at'] ?? ''));

        if (!$has_token) {
            return [
                'code' => 'empty',
                'badge_class' => 'bg-light text-secondary border',
                'icon' => 'fa-circle-o',
                'title' => 'Интеграция не настроена',
                'detail' => 'Вставьте Bearer token Юлы. EasyChat автоматически проверит доступ и подготовит webhook URL.',
            ];
        }
        if ($last_error !== '') {
            return [
                'code' => 'error',
                'badge_class' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'icon' => 'fa-exclamation-triangle',
                'title' => 'Ошибка подключения',
                'detail' => 'Юла не подтвердила доступ. ' . $this->formatYoulaSyncError(['error' => $last_error], $last_error),
            ];
        }
        if ($status === 'active' && $synced_at !== '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-success-subtle text-success border border-success-subtle',
                'icon' => 'fa-check-circle',
                'title' => 'Готово к приему webhook',
                'detail' => 'Токен проверен. Укажите webhook URL в партнерском кабинете или передайте его менеджеру Юлы.',
            ];
        }
        if ($status === 'active') {
            return [
                'code' => 'pending',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Нужна проверка',
                'detail' => 'Вставьте новый Bearer token Юлы, чтобы EasyChat автоматически проверил доступ.',
            ];
        }
        return [
            'code' => 'disabled',
            'badge_class' => 'bg-light text-secondary border',
            'icon' => 'fa-pause-circle',
            'title' => 'Отключено',
            'detail' => 'Канал Юлы отключен переключателем.',
        ];
    }

    private function saveYoulaChannelConfig() {
        $channel = $this->ensureYoulaChannel();
        $config = $this->buildYoulaChannelConfigFromPost($channel, false);
        $credentials = $config['credentials'];
        $status = $config['status'];
        $token = $config['token_value'];

        if (!empty($config['token_invalid'])) {
            return ['success' => false, 'error' => 'Bearer token Юлы содержит недопустимые символы или пуст после префикса Bearer. Скопируйте токен заново.'];
        }
        if ($status === 'active' && $token === '') {
            return ['success' => false, 'error' => 'Для включения интеграции Юлы укажите Bearer token.'];
        }

        (new MessengerModel($this->registry))->updateChannel((int)$channel['channel_id'], [
            'status' => $status,
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $this->buildYoulaWebhookUrl($channel),
        ]);

        return ['success' => true];
    }

    private function buildYoulaChannelConfigFromPost(array $channel, $force_active) {
        $credentials = $this->decodeChannelCredentials($channel);
        unset($credentials['webhook_secret'], $credentials['confirmation_code']);
        $status = $force_active || isset(\EasyChat\Library\RequestContext::post()['chat_youla_status']) ? 'active' : 'disabled';
        $token_raw = (string)(\EasyChat\Library\RequestContext::post()['chat_youla_token'] ?? '');
        $token_input = $this->normalizeYoulaBearerToken($token_raw);
        $token_invalid = trim($token_raw) !== '' && $token_input === '';

        $stored_token = $this->youlaBearerFromCredentials($credentials);
        $token = $token_input !== '' ? $token_input : $stored_token;
        $credentials_changed = $token_input !== '' && !hash_equals($stored_token, $token_input);

        if ($token !== '') {
            $credentials['token'] = $this->encryptChannelSecret($token);
            unset($credentials['user_id']);
            $credentials['bot_token'] = $this->encryptChannelSecret($token);
        }

        return [
            'status' => $status,
            'credentials' => $credentials,
            'token_value' => $token,
            'token' => $token,
            'credentials_changed' => $credentials_changed,
            'token_invalid' => $token_invalid,
        ];
    }

    private function youlaBearerFromCredentials(array $credentials) {
        $stored_token = $this->normalizeYoulaBearerToken($this->decryptChannelSecretOrPlain((string)($credentials['token'] ?? '')));
        if ($stored_token !== '') {
            return $stored_token;
        }
        $legacy_payload = json_decode($this->decryptChannelSecretOrPlain((string)($credentials['bot_token'] ?? '')), true);
        if (is_array($legacy_payload)) {
            return $this->normalizeYoulaBearerToken((string)($legacy_payload['token'] ?? $legacy_payload['access_token'] ?? ''));
        }
        return $this->normalizeYoulaBearerToken($this->decryptChannelSecretOrPlain((string)($credentials['bot_token'] ?? '')));
    }

    private function normalizeYoulaBearerToken($value) {
        $value = trim((string)$value);
        $value = preg_replace('/^Bearer\s+/i', '', $value);
        if ($value === '' || preg_match('/[\x00-\x1f\x7f]/', $value) === 1) {
            return '';
        }
        return $value;
    }

    private function formatYoulaSyncError($result, $fallback) {
        $result = is_array($result) ? $result : [];
        $http_code = (int)($result['http_code'] ?? 0);
        $error = trim((string)($result['error'] ?? $fallback));
        $normalized = strtolower($error);

        if ($error === 'auth_required' || $error === 'auth_failed') {
            return 'Введите Bearer token Юлы. Он обязателен для проверки Partner API.';
        }
        if ($http_code === 401 || strpos($normalized, 'unauthorized') !== false) {
            return 'Bearer token Юлы не принят. Проверьте, что токен скопирован полностью и не был отозван.';
        }
        if ($http_code === 403 || strpos($normalized, 'forbidden') !== false) {
            return 'Юла вернула 403 Forbidden. Проверьте Bearer token и права доступа этого токена к Partner API.';
        }
        if ($http_code === 404 || strpos($normalized, 'not_found') !== false || strpos($normalized, 'not found') !== false) {
            return 'Проверочный endpoint Юлы вернул 404. Повторите проверку позже или уточните у Юлы, активирован ли Partner API для токена.';
        }
        if ($error === 'api_request_failed') {
            return 'Сервер EasyChat не смог подключиться к partner-api.youla.ru. Проверьте HTTPS, DNS, proxy и доступ сервера к внешней сети.';
        }
        if ($error === 'api_bad_json') {
            return 'Partner API Юлы ответил не JSON-данными. Повторите проверку позже или проверьте proxy/TLS на сервере.';
        }

        $error = preg_replace('/https?:\/\/\S+/i', '[url]', $error);
        $error = preg_replace('/Bearer\s+[A-Za-z0-9._~+\/=-]+/i', 'Bearer [redacted]', (string)$error);
        $error = preg_replace('/[A-Za-z0-9._~+\/=-]{24,}/', '[redacted]', (string)$error);
        $error = trim(preg_replace('/\s+/u', ' ', strip_tags((string)$error)));
        return $error !== '' ? $error : (string)$fallback;
    }

    private function ensureTelegramChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel('telegram', 'Telegram');
    }

    private function buildTelegramConnectionState(array $channel, $token) {
        $has_token = trim((string)$token) !== '';
        $status = (string)($channel['status'] ?? 'disabled');
        $last_error = trim((string)($channel['last_error'] ?? ''));
        $synced_at = trim((string)($channel['webhook_synced_at'] ?? ''));
        $settings = $this->decodeChannelSettings($channel);
        $delivery_mode = (string)($settings['delivery_mode'] ?? 'webhook');

        if (!$has_token) {
            return [
                'code' => 'empty',
                'badge_class' => 'bg-light text-secondary border',
                'icon' => 'fa-circle-o',
                'title' => 'Токен не добавлен',
                'detail' => 'Вставьте токен Telegram бота, сохраните настройки и выполните проверку подключения.',
            ];
        }
        if ($status === 'active' && $delivery_mode === 'polling' && $last_error === '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-refresh',
                'title' => 'Polling через Cron',
                'detail' => 'Webhook Telegram недоступен снаружи, поэтому EasyChat забирает сообщения исходящими запросами через Cron.',
            ];
        }
        if ($last_error !== '') {
            return [
                'code' => 'error',
                'badge_class' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'icon' => 'fa-exclamation-triangle',
                'title' => 'Ошибка подключения',
                'detail' => 'Telegram не подтвердил подключение. Проверьте токен и повторите проверку.',
            ];
        }
        if ($status === 'active' && $synced_at !== '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-success-subtle text-success border border-success-subtle',
                'icon' => 'fa-check-circle',
                'title' => 'Подключено',
                'detail' => 'Webhook настроен автоматически. Входящие сообщения Telegram будут приходить в EasyChat.',
            ];
        }
        if ($status === 'active') {
            return [
                'code' => 'pending',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Нужна проверка',
                'detail' => 'Токен сохранен, но webhook еще не подтвержден Telegram.',
            ];
        }
        return [
            'code' => 'disabled',
            'badge_class' => 'bg-light text-secondary border',
            'icon' => 'fa-pause-circle',
            'title' => 'Отключено',
            'detail' => 'Токен сохранен, но канал выключен переключателем Telegram.',
        ];
    }

    private function buildTelegramWebhookUrl(array $channel) {
        return HTTP_SERVER . 'index.php?route=messenger/webhook&provider=telegram&id=' . urlencode((string)$channel['public_id']);
    }

    private function saveTelegramChannelConfig() {
        $channel = $this->ensureTelegramChannel();
        $credentials = $this->decodeChannelCredentials($channel);
        $status = isset(\EasyChat\Library\RequestContext::post()['chat_telegram_status']) ? 'active' : 'disabled';
        $token_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_telegram_bot_token'] ?? ''));

        if ($token_input !== '') {
            $credentials['bot_token'] = $this->encryptChannelSecret($token_input);
        }
        if (empty($credentials['webhook_secret'])) {
            $credentials['webhook_secret'] = $this->encryptChannelSecret($this->generateMaxWebhookSecret());
        }

        $token = $this->decryptChannelSecret((string)($credentials['bot_token'] ?? ''));
        if ($status === 'active' && $token === '') {
            return ['success' => false, 'error' => 'Для включения Telegram укажите токен бота и сохраните настройки.'];
        }

        $model_messenger = new MessengerModel($this->registry);
        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'status' => $status,
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $this->buildTelegramWebhookUrl($channel),
        ]);

        return ['success' => true];
    }

    private function ensureVkChannel() {
        $model_messenger = new MessengerModel($this->registry);
        return $model_messenger->ensureChannel('vk', 'VK');
    }

    private function buildVkWebhookUrl(array $channel) {
        return HTTP_SERVER . 'index.php?route=messenger/webhook&provider=vk&id=' . urlencode((string)$channel['public_id']);
    }

    private function buildVkConnectionState(array $channel, $token) {
        $has_token = trim((string)$token) !== '';
        $status = (string)($channel['status'] ?? 'disabled');
        $last_error = trim((string)($channel['last_error'] ?? ''));
        $synced_at = trim((string)($channel['webhook_synced_at'] ?? ''));

        if (!$has_token) {
            return [
                'code' => 'empty',
                'badge_class' => 'bg-light text-secondary border',
                'icon' => 'fa-circle-o',
                'title' => 'Интеграция не настроена',
                'detail' => 'Вставьте Токен сообщества (VK Group Token): EasyChat сам сохранит его и запустит проверку. Кнопка ниже нужна для повторной проверки.',
            ];
        }
        if ($last_error !== '') {
            return [
                'code' => 'error',
                'badge_class' => 'bg-danger-subtle text-danger border border-danger-subtle',
                'icon' => 'fa-exclamation-triangle',
                'title' => 'Ошибка подключения',
                'detail' => 'VK API вернул ошибку при проверке токена: ' . $last_error,
            ];
        }
        if ($status === 'active' && $synced_at !== '') {
            return [
                'code' => 'connected',
                'badge_class' => 'bg-success-subtle text-success border border-success-subtle',
                'icon' => 'fa-check-circle',
                'title' => 'Подключено',
                'detail' => 'Callback API VK настроен автоматически. Входящие сообщения и ответы сообщества VK будут приходить в EasyChat.',
            ];
        }
        if ($status === 'active') {
            return [
                'code' => 'pending',
                'badge_class' => 'bg-warning-subtle text-warning border border-warning-subtle',
                'icon' => 'fa-clock-o',
                'title' => 'Нужна проверка',
                'detail' => 'Токен сохранен. Нажмите «Проверить и подключить», чтобы EasyChat автоматически настроил Callback API VK.',
            ];
        }
        return [
            'code' => 'disabled',
            'badge_class' => 'bg-light text-secondary border',
            'icon' => 'fa-pause-circle',
            'title' => 'Отключено',
            'detail' => 'Канал отключен переключателем.',
        ];
    }

    private function buildVkSetupSteps(array $channel, array $credentials, string $token, string $blocked_step = ''): array {
        $has_token = trim($token) !== '' || !empty($credentials['bot_token']);
        $has_confirmation = !empty($credentials['confirmation_code']);
        $has_secret = !empty($credentials['secret_key']);
        $has_group = preg_replace('/\D+/', '', (string)($credentials['vk_group_id'] ?? '')) !== '';
        $has_server = preg_replace('/\D+/', '', (string)($credentials['callback_server_id'] ?? '')) !== '';
        $has_webhook = trim((string)($channel['webhook_url'] ?? '')) !== '';
        $is_connected = trim((string)($channel['webhook_synced_at'] ?? '')) !== '';
        $has_community_settings = $is_connected || (
            preg_replace('/\D+/', '', (string)($credentials['community_settings_group_id'] ?? '')) !== ''
            && trim((string)($credentials['community_settings_enabled_at'] ?? '')) !== ''
        );
        $is_active = (string)($channel['status'] ?? 'disabled') === 'active';

        $steps = [
            [
                'key' => 'token',
                'label' => 'Токен сообщества',
                'status' => $has_token ? 'done' : 'waiting',
                'status_text' => $has_token ? 'сохранен' : 'ожидает',
            ],
            [
                'key' => 'community_settings',
                'label' => 'Сообщения сообщества',
                'status' => $has_community_settings ? 'done' : 'waiting',
                'status_text' => $has_community_settings ? 'включены' : 'ожидает VK',
            ],
            [
                'key' => 'confirmation',
                'label' => 'Confirmation Code',
                'status' => $has_confirmation ? 'done' : 'waiting',
                'status_text' => $has_confirmation ? 'получен' : 'ожидает VK',
            ],
            [
                'key' => 'callback_server',
                'label' => 'Callback server',
                'status' => ($has_secret && $has_group && $has_server && $has_webhook) ? 'done' : 'waiting',
                'status_text' => ($has_secret && $has_group && $has_server && $has_webhook) ? 'сохранен' : 'ожидает VK',
            ],
            [
                'key' => 'callback_settings',
                'label' => 'Входящие и ответы сообщества',
                'status' => $is_connected ? 'done' : 'waiting',
                'status_text' => $is_connected ? 'включены' : 'ждет подтверждения VK',
            ],
        ];

        if (($is_active || $blocked_step !== '') && !$is_connected) {
            $current_key = $this->vkSetupStepKey($blocked_step);
            if ($current_key === '') {
                foreach ($steps as $step) {
                    if ($step['status'] !== 'done') {
                        $current_key = $step['key'];
                        break;
                    }
                }
            }
            foreach ($steps as &$step) {
                if ($step['key'] === $current_key && $step['status'] !== 'done') {
                    $step['status'] = 'current';
                    $current_step = $blocked_step !== '' ? $blocked_step : $this->vkSetupStepDefaultAction($current_key);
                    $step['status_text'] = $this->vkSetupStepCurrentText($current_step);
                }
            }
            unset($step);
        }

        return $steps;
    }

    private function vkSetupStepDefaultAction(string $step_key): string {
        $map = [
            'token' => 'token_check',
            'community_settings' => 'community_settings',
            'confirmation' => 'confirmation_code',
            'callback_server' => 'callback_servers',
            'callback_settings' => 'callback_settings',
        ];
        return (string)($map[$step_key] ?? '');
    }

    private function vkSetupStepCurrentText(string $vk_step): string {
        $texts = [
            'token_check' => 'проверяет',
            'token_permissions' => 'проверяет права',
            'community_settings' => 'включает в VK',
            'confirmation_code' => 'получает',
            'callback_server_add' => 'создает',
            'callback_server_edit' => 'обновляет',
            'callback_servers' => 'ищет',
            'callback_settings_check' => 'проверяет',
            'callback_settings' => 'включает в VK',
        ];
        return (string)($texts[$vk_step] ?? 'ждет VK');
    }

    private function vkSetupStepKey(string $vk_step): string {
        $map = [
            'token_check' => 'token',
            'token_permissions' => 'token',
            'community_settings' => 'community_settings',
            'confirmation_code' => 'confirmation',
            'callback_server_add' => 'callback_server',
            'callback_server_edit' => 'callback_server',
            'callback_servers' => 'callback_server',
            'callback_settings_check' => 'callback_settings',
            'callback_settings' => 'callback_settings',
        ];
        return (string)($map[$vk_step] ?? '');
    }

    private function saveVkChannelConfig() {
        $channel = $this->ensureVkChannel();
        $config = $this->buildVkChannelConfigFromPost($channel, false);
        $credentials = $config['credentials'];
        $status = $config['status'];
        $token = $config['token'];

        if ($status === 'active' && $token === '') {
            return ['success' => false, 'error' => 'Для включения интеграции VK укажите Токен сообщества.'];
        }

        $model_messenger = new MessengerModel($this->registry);
        $update = [
            'status' => $status,
            'credentials_json' => json_encode($credentials, JSON_UNESCAPED_UNICODE),
            'webhook_url' => $this->buildVkWebhookUrl($channel),
        ];
        if (!empty($config['token_changed'])) {
            $update['webhook_synced_at'] = null;
        }

        $model_messenger->updateChannel((int)$channel['channel_id'], $update);

        return ['success' => true];
    }

    private function buildVkChannelConfigFromPost(array $channel, $force_active) {
        $credentials = $this->decodeChannelCredentials($channel);
        $status = isset(\EasyChat\Library\RequestContext::post()['chat_vk_status']) ? 'active' : 'disabled';
        if ($force_active) {
            $status = 'active';
        }
        $token_input = trim((string)(\EasyChat\Library\RequestContext::post()['chat_vk_token'] ?? ''));
        $stored_token = $this->decryptChannelSecret((string)($credentials['bot_token'] ?? ''));
        $token_matches_saved = $token_input !== '' && $stored_token !== '' && hash_equals($stored_token, $token_input);
        $token_changed = $token_input !== '' && !$token_matches_saved;

        if ($token_changed) {
            $credentials['bot_token'] = $this->encryptChannelSecret($token_input);
            unset(
                $credentials['vk_group_id'],
                $credentials['confirmation_code'],
                $credentials['callback_server_id'],
                $credentials['community_settings_group_id'],
                $credentials['community_settings_enabled_at']
            );
        }

        $token = $token_input !== '' ? $token_input : $stored_token;
        $secret_key = $this->decryptChannelSecret((string)($credentials['secret_key'] ?? ''));
        $confirmation_code = $this->decryptChannelSecret((string)($credentials['confirmation_code'] ?? ''));

        if ($status === 'active' && ($secret_key === '' || !$this->isValidVkSecretKey($secret_key))) {
            $secret_key = $this->generateVkSecretKey();
            $credentials['secret_key'] = $this->encryptChannelSecret($secret_key);
        }

        return [
            'status' => $status,
            'credentials' => $credentials,
            'token' => $token,
            'secret_key' => $secret_key,
            'confirmation_code' => $confirmation_code,
            'token_input' => $token_input,
            'token_matches_saved' => $token_matches_saved,
            'token_changed' => $token_changed,
        ];
    }

    private function normalizeVkSecretKey($secret) {
        $secret = trim((string)$secret);
        return function_exists('mb_substr') ? mb_substr($secret, 0, 50, 'UTF-8') : substr($secret, 0, 50);
    }

    private function buildVkSavedBotInfo(array $config, array $channel) {
        if ((string)($config['token_input'] ?? '') !== '' && empty($config['token_matches_saved'])) {
            return null;
        }

        $credentials = is_array($config['credentials'] ?? null) ? $config['credentials'] : [];
        $group_id = preg_replace('/\D+/', '', (string)($credentials['vk_group_id'] ?? ''));
        if ($group_id === '') {
            return null;
        }

        $bot_name = trim((string)($channel['bot_name'] ?? ''));
        return [
            'success' => true,
            'id' => $group_id,
            'name' => $bot_name,
            'username' => trim((string)($channel['bot_username'] ?? '')),
            'profile_url' => 'https://vk.com/club' . $group_id,
        ];
    }

    private function isValidVkSecretKey($secret) {
        $secret = trim((string)$secret);
        return $secret !== '' && preg_match('/^[A-Za-z0-9]{1,50}$/', $secret) === 1;
    }

    private function formatVkSyncError($result, $fallback) {
        $result = is_array($result) ? $result : [];
        $code = trim((string)($result['error'] ?? ''));
        $message = trim((string)($result['error_message'] ?? ''));
        if ($code !== '' && $message !== '') {
            return $code . ': ' . $message;
        }
        if ($code !== '') {
            return $code;
        }
        return (string)$fallback;
    }

    private function clearVkRateLimit(array &$credentials): void {
        unset($credentials['rate_limited_until'], $credentials['rate_limit_token_hash'], $credentials['rate_limit_error']);
    }

    private function pauseVkSetupStep(): void {
        usleep(1000000);
    }

    private function vkRateLimitResponse(array $result, string $webhook_url, string $token, string $bot_name = '', array $setup_steps = []): array {
        $vk_step = trim((string)($result['vk_step'] ?? ''));
        $vk_step_title = $this->formatVkSetupStepTitle($vk_step);
        $detail_prefix = $vk_step_title !== '' ? 'Ограничен этап: ' . $vk_step_title . '. ' : '';
        $response = [
            'success' => true,
            'rate_limited' => true,
            'message' => 'VK временно ограничил частоту проверки. Настройки сохранены, кнопку можно нажать снова или сразу вставить другой ключ.',
            'detail' => $detail_prefix . 'VK временно ограничил этот API-запрос. EasyChat не блокирует повторные попытки: можно сразу вставить другой Group Access Token или повторить проверку позже.',
            'bot_name' => $bot_name,
            'webhook_url' => $webhook_url,
            'api_version' => VkProvider::API_VERSION,
            'token_mask' => SecretBox::mask($token),
        ];
        if ($vk_step !== '') {
            $response['vk_step'] = $vk_step;
        }
        if ($vk_step_title !== '') {
            $response['vk_step_title'] = $vk_step_title;
        }
        if ($setup_steps) {
            $response['setup_steps'] = $setup_steps;
        }
        return $response;
    }

    private function formatVkSetupStepTitle(string $vk_step): string {
        $titles = [
            'token_check' => 'проверка Group Access Token',
            'token_permissions' => 'проверка прав Group Access Token',
            'community_settings' => 'включение сообщений сообщества и возможностей ботов',
            'confirmation_code' => 'получение Confirmation Code',
            'callback_server_add' => 'создание Callback server',
            'callback_server_edit' => 'обновление Callback server',
            'callback_servers' => 'поиск Callback servers',
            'callback_settings_check' => 'проверка включенных событий Callback API',
            'callback_settings' => 'включение событий «Входящие сообщения» и «Ответы на сообщения»',
        ];
        return (string)($titles[$vk_step] ?? '');
    }
}
