<?php
namespace EasyChat\Library;

use EasyChat\Library\AiProvider\Gemini;
use EasyChat\Library\AiProvider\EasyTokensProvider;
use EasyChat\Library\AiProvider\OpenAI;
use EasyChat\Library\Ai\AiReplyEligibilityGuard;
use EasyChat\Library\Ai\AvitoReplyTextNormalizer;
use EasyChat\Library\Ai\AiResponseTimingPolicy;
use EasyChat\Library\Ai\Pipeline;
use EasyChat\Library\Ai\TestNoticeSanitizer;
use EasyChat\Library\Ai\VisualContextBuilder;
use EasyChat\Library\Http\ProxyPolicy;
use EasyChat\Library\Messenger\MessengerLogger;
use EasyChat\Library\Messenger\OutboundDispatchGuard;
use EasyChat\Library\Voice\VoiceMessageTextResolver;
use EasyChat\Library\Support\OfficialProductSupportProfile;
use EasyChat\Library\Support\ProductSupportService;
use EasyChat\Model\Setting;
use EasyChat\Model\Message;
use EasyChat\Model\MessageContext;
use EasyChat\Model\Session;

class AiResponder {

    




    private const AI_LOCK_LEASE_SECONDS = 180;

    private $registry;
    private $db;
    private $prefix;
    private $provider_name;
    private $api_key;
    private $model;
    private $system_prompt;
    private $cms_type;
    private $cms_prefix;
    private $api_host;
    private $temperature;
    private $last_tick_result = [
        'success' => false,
        'reason' => 'not_started',
        'retryable' => false,
        'retry_after' => 0,
    ];

     
    private $tools = [];

     
    private $tool_executor = null;

    


    public function __construct($registry) {
        $this->registry = $registry;
        $this->db = $registry->get('db');
        $this->prefix = easychat_db_prefix();

         
        $model_setting = new Setting($registry);
        $settings = $model_setting->getAll();

        $this->provider_name         = $settings['chat_ai_provider'] ?? 'easytokens';
        $this->api_key               = $settings['chat_ai_key']      ?? '';
        $this->model                 = $settings['chat_ai_model']    ?? 'easylite';
        $this->system_prompt         = $settings['chat_ai_prompt']   ?? 'Ты — вежливый онлайн-консультант. Не обращайся к клиенту по фамилии и не повторяй его полное имя.';
        $this->cms_type              = $settings['chat_cms_type']     ?? 'none';
        $this->cms_prefix            = $settings['chat_cms_prefix']   ?? '';
        $this->api_host              = $settings['chat_ai_host']     ?? '';
        $this->temperature           = isset($settings['chat_ai_temp']) ? (float)$settings['chat_ai_temp'] : 0.6;
    }

    





    public function tick($chat_id) {
        $this->log("[Чат #{$chat_id}] === НАЧАЛО ОБРАБОТКИ TICK ===");
        $this->setTickResult(false, 'started', false);

        $model_message = new Message($this->registry);
        $last_msg = $model_message->getLastMessage($chat_id);
        if (!$last_msg) {
            $this->log("[Чат #{$chat_id}] Пропуск: в диалоге еще нет ни одного сообщения");
            $this->setTickResult(false, 'message_missing', false);
            return false;
        }

        return $this->answerCustomerMessage($chat_id, (int)$last_msg['message_id'], true);
    }

    






    public function answerCustomerMessage($chat_id, $message_id, $enforce_delay = true, ?array $trusted_support_profile = null) {
        $chat_id = (int)$chat_id;
        $message_id = (int)$message_id;
        $this->setTickResult(false, 'started', false);

        $model_setting = new Setting($this->registry);
        $model_message = new Message($this->registry);
        $model_session = new Session($this->registry);
        $locked = false;
        $locked_message_id = $message_id;
        $lock_status = 'error';
        $lock_error = 'unexpected_ai_tick_stop';

        try {
            if ($chat_id <= 0 || $message_id <= 0) {
                $this->setTickResult(false, 'message_missing', false);
                return false;
            }

            $target_msg = $model_message->getMessageById($message_id);
            if (!$target_msg || (int)($target_msg['chat_id'] ?? 0) !== $chat_id) {
                $this->log("[Чат #{$chat_id}] Пропуск: сообщение #{$message_id} не найдено или принадлежит другому диалогу");
                $this->setTickResult(false, 'message_missing', false);
                return false;
            }

            $last_msg = $model_message->getLastMessage($chat_id);
            if (!$last_msg || (int)($last_msg['message_id'] ?? 0) !== $message_id) {
                $actual_id = $last_msg ? (int)($last_msg['message_id'] ?? 0) : 0;
                $this->log("[Чат #{$chat_id}] Пропуск: сообщение #{$message_id} уже не последнее (последнее #{$actual_id})");
                $this->setTickResult(false, 'message_is_not_last', false);
                return false;
            }

             
            if ((int)$last_msg['author'] !== 1) {
                $this->log("[Чат #{$chat_id}] Пропуск: последнее сообщение отправлено не клиентом (автор = " . $last_msg['author'] . ")");
                $this->setTickResult(false, 'last_message_not_customer', false);
                return false;
            }

            $support_profile = $trusted_support_profile;
            if ($support_profile !== null && (
                (int)($support_profile['chat_id'] ?? 0) !== $chat_id
                || (int)($support_profile['message_id'] ?? 0) !== $message_id
                || (string)($support_profile['profile'] ?? '') !== 'easychat_product_support'
                || (string)($support_profile['space'] ?? '') !== 'official_easychat_docs'
            )) {
                $this->setTickResult(false, 'official_support_profile_invalid', false);
                return false;
            }
            if ($support_profile === null) {
                $support_profile = (new OfficialProductSupportProfile($this->registry))->resolve($chat_id, $message_id);
            }
            if ($support_profile === null) {
                 
                 
                if ($model_setting->get('chat_ai_status', '0') !== '1') {
                    $this->log("[Чат #{$chat_id}] Пропуск: ИИ-Консультант отключен в настройках (chat_ai_status = 0)");
                    $this->setTickResult(false, 'ai_disabled', false);
                    return false;
                }
                if (!$this->api_key) {
                    $this->log("[Чат #{$chat_id}] Пропуск: отсутствует API Key провайдера");
                    $this->setTickResult(false, 'api_key_missing', false);
                    return false;
                }
            }

            if (!empty($last_msg['image']) && !$this->supportsVisualContext()) {
                $this->log("[Чат #{$chat_id}] Пропуск: распознавание изображений доступно через EasyTokens");
                $this->setTickResult(false, 'image_requires_easytokens', false);
                return false;
            }

            if (!$this->ensureAiReplyAllowed($chat_id, $message_id, (string)($last_msg['source_provider'] ?? ''), 'ai_responder_preflight')) {
                return false;
            }

            if ($enforce_delay && $support_profile === null) {
                $timing = (new AiResponseTimingPolicy($this->registry))->evaluateForMessageDate((string)$last_msg['date_added']);
                $this->log("[Чат #{$chat_id}] Проверка времени AI: режим {$timing['mode']}, оператор online: " . ($timing['operator_online'] ? 'да' : 'нет') . ", прошло секунд {$timing['elapsed_seconds']} из необходимых {$timing['required_seconds']} (дата сообщения: {$last_msg['date_added']}, текущее EasyChat время: {$timing['now_sql']})");

                if (empty($timing['ready'])) {
                    $this->log("[Чат #{$chat_id}] Пропуск: время задержки ответа еще не истекло");
                    $this->setTickResult(false, 'ai_delay_not_elapsed', true, (int)($timing['retry_after'] ?? 60));
                    return false;
                }
            } elseif (!$enforce_delay) {
                $this->log("[Чат #{$chat_id}] Delayed messenger job готов к обработке сообщения #{$message_id}: задержка уже отработана через очередь");
            } else {
                $this->log("[Чат #{$chat_id}] Официальная справка отвечает без tenant-задержки автоответа");
            }

            if (!$model_session->acquireAiProcessingLock($chat_id, $locked_message_id, self::AI_LOCK_LEASE_SECONDS)) {
                $this->log("[Чат #{$chat_id}] Пропуск: AI уже обрабатывает или уже обработал сообщение #{$locked_message_id}");
                $reason = $model_session->hasCompletedAiAttempt($chat_id, $locked_message_id)
                    ? 'ai_attempt_already_completed'
                    : 'ai_lock_busy';
                $this->setTickResult(false, $reason, $reason === 'ai_lock_busy');
                return false;
            }
            $locked = true;

             
             
             
            $this->log("[Чат #{$chat_id}] Собираем историю диалога...");
            $history_data = $this->buildHistoryContext($chat_id, $locked_message_id);
            $history_messages = $history_data['history_messages'];
            $last_customer_text = $history_data['last_customer_text'];
            if (
                (int)$history_data['target_image_expected'] > 0
                && (int)$history_data['target_image_count'] === 0
            ) {
                $lock_status = 'empty';
                $lock_error = 'visual_context_unavailable';
                $this->setTickResult(false, $lock_error, true, 30);
                return false;
            }
            if ($last_customer_text === '' && (int)$history_data['target_image_count'] === 0) {
                $lock_status = 'empty';
                $lock_error = 'message_context_empty';
                $this->setTickResult(false, $lock_error, false);
                return false;
            }

            if ($support_profile !== null) {
                $support_profile['message_date'] = (string)($last_msg['date_added'] ?? '');
                 
                 
                 
                $this->log("[Чат #{$chat_id}] История официальной справки собрана: " . count($history_messages) . " сообщений; содержимое не журналируется.");
            } else {
                $this->log("[Чат #{$chat_id}] Истории собрано сообщений: " . count($history_messages) . ". Последний текст клиента: \"" . $last_customer_text . "\"");
            }

            $ai_response = '';
            $support_result = null;

             
            $settings = $model_setting->getAll();

            if ($support_profile !== null) {
                $support_profile['message_date'] = (string)($last_msg['date_added'] ?? '');
                 
                 
                $support_result = (new ProductSupportService($this->registry))->answer(
                    $support_profile,
                    $history_messages,
                    $last_customer_text,
                    $this->productSupportRequestKey($chat_id, $locked_message_id)
                );
                $ai_response = (string)$support_result['answer'];
                $this->log("[Чат #{$chat_id}] Ответ подготовлен из изолированного official_easychat_docs.");
            } else {
                 
                $pipeline = new Pipeline($this->registry, $this);
                $this->log("[Чат #{$chat_id}] Запуск Pipeline...");
                $ai_response = $pipeline->run($chat_id, $last_customer_text, $history_messages, $settings, [
                    'target_message_id' => $locked_message_id,
                    'visual_image_count' => (int)$history_data['visual_image_count'],
                    'visual_skipped_image_count' => (int)$history_data['visual_skipped_image_count'],
                    'visual_only' => !empty($history_data['target_text_empty']),
                ]);
            }

            if ($ai_response) {
                $ai_response = $this->normalizeAiResponseForChannel($chat_id, $ai_response);
                if ($support_profile !== null) {
                    $this->log("[Чат #{$chat_id}] Проверенный официальный ответ получен; содержимое не журналируется.");
                } else {
                    $this->log("[Чат #{$chat_id}] Получен ответ от ИИ: \"" . $ai_response . "\"");
                }
                if (!$this->ensureAiReplyAllowed($chat_id, $locked_message_id, (string)($last_msg['source_provider'] ?? ''), 'ai_responder_final')) {
                    $lock_status = 'empty';
                    $lock_error = 'ai_reply_read_only';
                    return false;
                }
                $saved_message_id = $this->saveAiMessageAndDispatch($chat_id, $ai_response, $locked_message_id);
                if (!$saved_message_id) {
                    $save_reason = (string)($this->getLastTickResult()['reason'] ?? '');
                    if ($save_reason === 'ai_reply_read_only') {
                        $lock_status = 'empty';
                        $lock_error = 'ai_reply_read_only';
                        return false;
                    }
                    $lock_status = 'stale';
                    $lock_error = 'message_is_not_last';
                    $this->log("[Чат #{$chat_id}] Ответ ИИ не сохранен: сообщение #{$locked_message_id} уже не последнее");
                    $this->setTickResult(false, $lock_error, false);
                    return false;
                }
                if (is_array($support_result)) {
                    $this->storeOfficialSupportContext($chat_id, (int)$saved_message_id, $support_result);
                }

                $lock_status = 'success';
                $lock_error = '';
                $model_session->releaseAiProcessingLock($chat_id, $locked_message_id, $lock_status, $lock_error);
                $locked = false;

                $this->log("[Чат #{$chat_id}] === КОНЕЦ ОБРАБОТКИ TICK (УСПЕХ) ===");
                $this->setTickResult(true, 'success', false);
                return true;
            }

            $lock_status = 'empty';
            $lock_error = 'empty_ai_response';
            $this->log("[Чат #{$chat_id}] === КОНЕЦ ОБРАБОТКИ TICK (НЕТ ОТВЕТА) ===");
            $this->setTickResult(false, 'empty_ai_response', false);
            return false;
        } catch (\Throwable $e) {
            $lock_status = 'error';
            $lock_error = $this->safeAiError($e->getMessage());
            $this->log("[Чат #{$chat_id}] Ошибка AI tick: " . $lock_error);
            if ($locked && $locked_message_id > 0 && $this->saveAiFailureNotice($chat_id, $locked_message_id)) {
                $lock_status = 'success';
                $lock_error = 'service_unavailable_notice';
                $this->setTickResult(true, 'service_unavailable_notice', false);
                return true;
            }
            $this->setTickResult(false, $lock_error, false);
            return false;
        } finally {
            if ($locked && $locked_message_id > 0) {
                $model_session->releaseAiProcessingLock($chat_id, $locked_message_id, $lock_status, $lock_error);
            }
        }
    }

    public function getLastTickResult() {
        return $this->last_tick_result;
    }

    private function setTickResult($success, $reason, $retryable, $retry_after = 0) {
        $reason = preg_replace('/[^a-z0-9_:-]/i', '_', (string)$reason);
        $this->last_tick_result = [
            'success' => (bool)$success,
            'reason' => $reason !== '' ? $reason : 'unknown',
            'retryable' => (bool)$retryable,
            'retry_after' => max(0, (int)$retry_after),
        ];
    }

    







    public function streamForWidget($chat_id, $message_id, callable $emit) {
        $model_setting = new Setting($this->registry);
        $model_message = new Message($this->registry);
        $model_session = new Session($this->registry);

        $locked = false;
        $lock_status = 'error';
        $lock_error = 'unexpected_ai_stream_stop';
        $message_id = (int)$message_id;

        try {
            $last_msg = $model_message->getLastMessage($chat_id);
            if (!$last_msg || (int)$last_msg['message_id'] !== $message_id) {
                throw new \RuntimeException('message_is_not_last');
            }
            if ((int)$last_msg['author'] !== 1) {
                throw new \RuntimeException('message_is_not_customer');
            }
            $support_profile = (new OfficialProductSupportProfile($this->registry))->resolve((int)$chat_id, $message_id);
            if ($support_profile === null) {
                if ($model_setting->get('chat_ai_status', '0') !== '1') {
                    throw new \RuntimeException('ai_disabled');
                }
                if ($model_setting->get('chat_ai_live_typing', '1') !== '1') {
                    throw new \RuntimeException('live_typing_disabled');
                }
                if (!$this->api_key) {
                    throw new \RuntimeException('api_key_missing');
                }
            }
            if (!empty($last_msg['image']) && !$this->supportsVisualContext()) {
                throw new \RuntimeException('image_requires_easytokens');
            }
            if (!$this->ensureAiReplyAllowed((int)$chat_id, $message_id, (string)($last_msg['source_provider'] ?? ''), 'ai_stream_preflight')) {
                throw new \RuntimeException('ai_reply_read_only');
            }

            $clock = new AppClock($this->registry);
            if ($support_profile === null) {
                $timing = (new AiResponseTimingPolicy($this->registry))->evaluateForMessageDate((string)$last_msg['date_added']);
                if (empty($timing['ready'])) {
                    throw new \RuntimeException('ai_delay_not_elapsed');
                }
            }

            if (!$model_session->acquireAiProcessingLock($chat_id, $message_id, self::AI_LOCK_LEASE_SECONDS)) {
                throw new \RuntimeException('ai_lock_busy_or_completed');
            }
            $locked = true;

            $emit('start', ['message_id' => $message_id]);

            $history_data = $this->buildHistoryContext($chat_id, $message_id);
            if (
                (int)$history_data['target_image_expected'] > 0
                && (int)$history_data['target_image_count'] === 0
            ) {
                throw new \RuntimeException('visual_context_unavailable');
            }
            $settings = $model_setting->getAll();
            $support_result = null;
            if ($support_profile !== null) {
                $support_result = (new ProductSupportService($this->registry))->answer(
                    $support_profile,
                    $history_data['history_messages'],
                    $history_data['last_customer_text'],
                    $this->productSupportRequestKey((int)$chat_id, $message_id),
                    function ($delta) use ($emit) {
                        if ($delta !== '') $emit('delta', ['text' => $delta]);
                    }
                );
                $ai_response = (string)$support_result['answer'];
            } else {
                $pipeline = new Pipeline($this->registry, $this);
                $request = $pipeline->prepareFinalRequest(
                    $chat_id,
                    $history_data['last_customer_text'],
                    $history_data['history_messages'],
                    $settings,
                    [
                        'target_message_id' => $message_id,
                        'visual_image_count' => (int)$history_data['visual_image_count'],
                        'visual_skipped_image_count' => (int)$history_data['visual_skipped_image_count'],
                        'visual_only' => !empty($history_data['target_text_empty']),
                    ]
                );
                $ai_response = '';
                if (!$request || ($request['type'] ?? '') === 'skip') {
                    $lock_status = 'empty';
                    $lock_error = 'empty_ai_response';
                    return ['success' => false, 'error' => 'empty_ai_response'];
                }
                if (($request['type'] ?? '') === 'direct') {
                    $ai_response = trim((string)($request['text'] ?? ''));
                    if ($ai_response !== '') $emit('delta', ['text' => $ai_response]);
                } else {
                    $stream_options = array_merge([
                        'stream_timeout' => 120,
                        'connect_timeout' => 10,
                        'first_byte_timeout' => 25,
                    ], is_array($request['options'] ?? null) ? $request['options'] : []);
                    $ai_response = $this->callAPIStream($request['messages'], $request['temperature'], $stream_options, function ($delta) use ($emit) {
                        if ($delta !== '') $emit('delta', ['text' => $delta]);
                    });
                    if (trim((string)$ai_response) === '') {
                        $this->log("[streamForWidget] Stream-ответ пуст. Повторяем финальный LLM с компактным fallback prompt.");
                        $ai_response = $this->callAPI(
                            $this->buildCompactFallbackMessages($request['messages']),
                            $request['temperature'],
                            array_merge(is_array($request['options'] ?? null) ? $request['options'] : [], [
                                'timeout' => 25,
                                'connect_timeout' => 10,
                                'first_byte_timeout' => 15,
                                'max_tokens' => 700,
                            ])
                        );
                        if (trim((string)$ai_response) !== '') $emit('delta', ['text' => $ai_response]);
                    }
                }
            }

            $ai_response = trim((string)$ai_response);
            if ($ai_response === '') {
                $ai_response = $this->getEmergencyFallbackResponse();
                $emit('delta', ['text' => $ai_response]);
            }
            $ai_response = $this->normalizeAiResponseForChannel($chat_id, $ai_response);
            if (!$this->ensureAiReplyAllowed((int)$chat_id, $message_id, (string)($last_msg['source_provider'] ?? ''), 'ai_stream_final')) {
                $lock_status = 'empty';
                $lock_error = 'ai_reply_read_only';
                return ['success' => false, 'error' => $lock_error];
            }

            $current_last_msg = $model_message->getLastMessage($chat_id);
            if (
                !$current_last_msg
                || (int)($current_last_msg['message_id'] ?? 0) !== $message_id
                || (int)($current_last_msg['author'] ?? 0) !== 1
            ) {
                $lock_status = 'error';
                $lock_error = 'message_is_not_last';
                return ['success' => false, 'error' => $lock_error];
            }

            $saved_message_id = $this->saveAiMessageAndDispatch($chat_id, $ai_response, $message_id);
            if (!$saved_message_id) {
                $save_reason = (string)($this->getLastTickResult()['reason'] ?? '');
                if ($save_reason === 'ai_reply_read_only') {
                    $lock_status = 'empty';
                    $lock_error = 'ai_reply_read_only';
                    return ['success' => false, 'error' => $lock_error];
                }
                $lock_status = 'error';
                $lock_error = 'message_is_not_last';
                return ['success' => false, 'error' => $lock_error];
            }
            if (is_array($support_result)) {
                $this->storeOfficialSupportContext((int)$chat_id, (int)$saved_message_id, $support_result);
            }

            $lock_status = 'success';
            $lock_error = '';
            $model_session->releaseAiProcessingLock($chat_id, $message_id, $lock_status, $lock_error);
            $locked = false;
            $saved_message = $model_message->getMessageById((int)$saved_message_id);

            return [
                'success'    => true,
                'message_id' => (int)$saved_message_id,
                'text'       => $ai_response,
                'date_added' => $clock->sqlToIsoUtc((string)($saved_message['date_added'] ?? '')),
                'support_sources' => is_array($support_result) ? (array)($support_result['sources'] ?? []) : [],
                'support_handoff' => is_array($support_result) ? (array)($support_result['handoff'] ?? []) : ['show' => false],
            ];
        } catch (\Throwable $e) {
            $lock_error = $this->safeAiError($e->getMessage());
             
             
             
            $lock_status = ($lock_error === 'client_stream_aborted') ? 'stream_aborted' : 'stream_error';
            $this->log("[streamForWidget] Ошибка: " . $lock_error);
            if (
                $locked
                && $message_id > 0
                && !in_array($lock_error, ['client_stream_aborted', 'ai_reply_read_only', 'message_is_not_last'], true)
                && $this->saveAiFailureNotice((int)$chat_id, $message_id)
            ) {
                $lock_status = 'success';
                $lock_error = 'service_unavailable_notice';
                $saved_message = $model_message->getLastMessage((int)$chat_id);
                return [
                    'success' => true,
                    'message_id' => (int)($saved_message['message_id'] ?? 0),
                    'text' => $this->getEmergencyFallbackResponse(),
                    'date_added' => (new AppClock($this->registry))->sqlToIsoUtc((string)($saved_message['date_added'] ?? '')),
                    'support_sources' => [],
                    'support_handoff' => ['show' => false],
                ];
            }
            return ['success' => false, 'error' => $lock_error];
        } finally {
            if ($locked && $message_id > 0) {
                $model_session->releaseAiProcessingLock($chat_id, $message_id, $lock_status, $lock_error);
            }
        }
    }

    private function buildHistoryContext($chat_id, $target_message_id = 0) {
        $model_message = new Message($this->registry);
        $session = (new Session($this->registry))->getSessionById((int)$chat_id);
        $boundary_message_id = (int)($session['conversation_start_message_id'] ?? 0);
        $history_rows = $model_message->getRecentHistory($chat_id, 10, $boundary_message_id);
        $voice_resolver = new VoiceMessageTextResolver($this->registry);
        $voice_transcripts = $voice_resolver->transcriptsForRows($history_rows);
        $history_messages = [];
        $last_customer_text = '';
        $target_text_empty = true;
        $max_history_chars = 2400;
        $used_chars = 0;

         
         
        $selected = [];
        for ($i = count($history_rows) - 1; $i >= 0; $i--) {
            $row = $history_rows[$i];
            $text = $voice_resolver->compose($row, $voice_transcripts);
            $text = TestNoticeSanitizer::forPrompt($text);
            $text = $this->trimMessageForAiHistory($text);
            $length = $this->stringLength($text);
            if (!empty($selected) && ($used_chars + $length) > $max_history_chars) {
                continue;
            }
            $selected[] = [
                'chat_id' => (int)$chat_id,
                'message_id' => (int)($row['message_id'] ?? 0),
                'author' => (int)$row['author'],
                'text' => $text,
                'image' => (string)($row['image'] ?? ''),
            ];
            $used_chars += $length;
        }

        $selected = array_reverse($selected);
        $visual = [
            'images_by_message_id' => [],
            'image_count' => 0,
            'target_image_expected' => 0,
            'target_image_count' => 0,
            'skipped_image_count' => 0,
        ];
        if ($this->supportsVisualContext()) {
            $visual = (new VisualContextBuilder($this->registry))->buildForRows(
                (int)$chat_id,
                $selected,
                (int)$target_message_id
            );
        }
        foreach ($selected as $row) {
            $text = $row['text'];
            $role = ((int)$row['author'] === 1) ? 'user' : 'assistant';
            $message_id = (int)$row['message_id'];
            $images = isset($visual['images_by_message_id'][$message_id])
                ? (array)$visual['images_by_message_id'][$message_id]
                : [];
            $content = $text;
            if ($images) {
                $content = [[
                    'type' => 'text',
                    'text' => $text !== ''
                        ? $text
                        : 'Клиент отправил изображение. Ответьте на его вопрос по содержимому изображения.',
                ]];
                foreach ($images as $image) {
                    unset($image['fingerprint']);
                    $content[] = $image;
                }
            }
            if ($content === '') {
                continue;
            }
            $history_messages[] = ['role' => $role, 'content' => $content];
            if ((int)$row['author'] === 1) {
                $last_customer_text = $text;
                if ($message_id === (int)$target_message_id) {
                    $target_text_empty = $text === '';
                }
                if ($message_id === (int)$target_message_id && $last_customer_text === '' && $images) {
                    $last_customer_text = 'Клиент отправил изображение и ожидает ответ по его содержимому.';
                }
            }
        }

        return [
            'history_messages'  => $history_messages,
            'last_customer_text' => $last_customer_text,
            'visual_image_count' => (int)$visual['image_count'],
            'visual_skipped_image_count' => (int)$visual['skipped_image_count'],
            'target_image_expected' => (int)$visual['target_image_expected'],
            'target_image_count' => (int)$visual['target_image_count'],
            'target_text_empty' => $target_text_empty,
        ];
    }

    private function supportsVisualContext(): bool {
        return strtolower(trim((string)$this->provider_name)) === 'easytokens';
    }

    private function normalizeAiResponseForChannel($chat_id, string $text): string {
        $model_session = new Session($this->registry);
        $chat = $model_session->getSessionById((int)$chat_id);
        if ($chat && (string)($chat['source_provider'] ?? '') === 'avito') {
            return AvitoReplyTextNormalizer::normalize($text);
        }

        return $text;
    }

    private function ensureAiReplyAllowed($chat_id, $message_id, string $provider_code, string $layer): bool {
        $chat_id = (int)$chat_id;
        $message_id = (int)$message_id;
        $guard = new AiReplyEligibilityGuard($this->registry);
        $eligibility = $guard->checkChatId($chat_id);
        if (!empty($eligibility['allowed'])) {
            return true;
        }

        $guard->logSkip($chat_id, $message_id, $provider_code, $layer, $eligibility);
        $reason = (string)($eligibility['reason'] ?? 'ai_reply_not_allowed');
        $detail = (string)($eligibility['detail'] ?? '');
        $this->log("[Чат #{$chat_id}] Пропуск: автоматический ответ запрещен политикой канала" . ($detail !== '' ? " ({$detail})" : ''));
        $this->setTickResult(false, $reason !== '' ? $reason : 'ai_reply_not_allowed', false);
        return false;
    }

    private function trimMessageForAiHistory($text) {
        $text = trim((string)$text);
        if ($this->stringLength($text) <= 800) {
            return $text;
        }

        return $this->stringSubstring($text, 0, 760) . "\n...[сообщение сокращено]";
    }

    public function buildCompactFallbackMessages(array $messages) {
        $result = [];
        $system_added = false;
        $channel_context = '';

        foreach ($messages as $message) {
            $role = isset($message['role']) ? (string)$message['role'] : '';
            if ($role === 'system') {
                if (!$system_added) {
                    $channel_context = $this->extractMarketplaceContextBlock((string)($message['content'] ?? ''));
                    $fallback_system = $this->compactFallbackSystemPrompt($channel_context, (string)($message['content'] ?? ''));
                    $system_content = $channel_context !== ''
                        ? $fallback_system . "\n\nКОНТЕКСТ ВНЕШНЕГО КАНАЛА:\n" . $channel_context
                        : $fallback_system;
                    $result[] = ['role' => 'system', 'content' => $system_content];
                    $system_added = true;
                }
                continue;
            }

            if ($role === 'user' || $role === 'assistant') {
                $content = $this->compactFallbackContent($message['content'] ?? '');
                if ($this->messageContentHasValue($content)) {
                    $result[] = ['role' => $role, 'content' => $content];
                }
            }
        }

        if (!$system_added) {
            $fallback_system = $this->compactFallbackSystemPrompt('', '');
            array_unshift($result, ['role' => 'system', 'content' => $fallback_system]);
        }

        return $result;
    }

    private function compactFallbackContent($content) {
        if (!is_array($content)) {
            $text = TestNoticeSanitizer::forPrompt((string)$content);
            return $this->trimMessageForAiHistory($text);
        }

        $parts = [];
        foreach ($content as $part) {
            if (!is_array($part)) {
                continue;
            }
            $type = (string)($part['type'] ?? '');
            if ($type === 'text' || $type === 'input_text') {
                $text = TestNoticeSanitizer::forPrompt((string)($part['text'] ?? ''));
                $text = $this->trimMessageForAiHistory($text);
                if ($text !== '') {
                    $parts[] = ['type' => 'text', 'text' => $text];
                }
                continue;
            }
            if ($type === 'image_url' && isset($part['image_url']['url'])) {
                $parts[] = $part;
            }
        }
        return $parts;
    }

    private function messageContentHasValue($content): bool {
        return is_array($content) ? !empty($content) : trim((string)$content) !== '';
    }

    private function extractMarketplaceContextBlock(string $content): string {
        if (!preg_match('/\\[(AVITO_ITEM_CONTEXT|AVITO_GENERAL_CONTEXT|YOULA_ITEM_CONTEXT)\\].*?\\[\\/\\1\\]/su', $content, $matches)) {
            return '';
        }
        return $this->stringSubstring(trim($matches[0]), 0, 1200);
    }

    private function compactFallbackSystemPrompt(string $channel_context = '', string $original_system_prompt = '') {
        $customer_address_rule = 'Не обращайся к клиенту по фамилии и не повторяй его полное имя. По умолчанию отвечай нейтрально, без имени.';
        if (strpos($channel_context, '[AVITO_ITEM_CONTEXT]') !== false) {
            return implode("\n", [
                'Ты — вежливый ИИ-консультант продавца в Avito.',
                $customer_address_rule,
                'Отвечай кратко, понятно и по-русски.',
                'Если вопрос относится к объявлению Avito, используй факты из контекста объявления: название, цену, описание и параметры.',
                'Если характеристики явно указаны в названии объявления, считай их фактами карточки и можешь использовать в ответе.',
                'Запись 16/512Gb, 16/512 или 24gb/1tb в названии трактуй как ОЗУ/накопитель; в готовом ответе разворачивай ее как 16 ГБ ОЗУ и 512 ГБ накопитель, а не 16/512 ГБ памяти. Не называй накопитель SSD, если SSD прямо не указан.',
                'Вопросы вроде “цена, оперативка и накопитель” означают цену всего товара и характеристики ОЗУ/накопителя. Не заявляй, что комплектующие продаются или не продаются отдельно, если этого нет в карточке.',
                'Не говори, что консультируешь только по EasyChat, если вопрос относится к товару Avito.',
                'Не выдумывай характеристики, наличие, гарантию, доставку или скидки, если их нет в карточке, переписке или базе знаний.',
            ]);
        }
        if (strpos($channel_context, '[AVITO_GENERAL_CONTEXT]') !== false) {
            return implode("\n", [
                'Ты — вежливый ИИ-консультант продавца в Avito.',
                $customer_address_rule,
                'Клиент написал в общий чат Avito без выбранной карточки товара.',
                'Не угадывай объявление и не используй товары из других диалогов.',
                'Если вопрос про конкретный товар, цену, характеристики, наличие или доставку, попроси клиента уточнить объявление, название или ссылку.',
                'Если вопрос общий про магазин, оплату, график или условия, отвечай кратко по переписке и доступной базе знаний.',
            ]);
        }
        if (strpos($channel_context, '[YOULA_ITEM_CONTEXT]') !== false) {
            return implode("\n", [
                'Ты — вежливый ИИ-консультант продавца на Юле.',
                $customer_address_rule,
                'Отвечай кратко, понятно и по-русски.',
                'Если вопрос относится к товару или объявлению Юлы, используй факты из контекста карточки: название, цену, описание, статус и параметры.',
                'Не говори, что консультируешь только по EasyChat, если вопрос относится к товару Юлы.',
                'Не выдумывай характеристики, наличие, гарантию, доставку или скидки, если их нет в карточке, переписке или базе знаний.',
            ]);
        }

        $business_prompt = trim($this->system_prompt !== '' ? $this->system_prompt : $original_system_prompt);
        if ($business_prompt !== '') {
            $business_prompt = $this->stringSubstring($business_prompt, 0, 1800);
        } else {
            $business_prompt = 'Ты — вежливый онлайн-консультант. Отвечай по правилам сайта и не выдумывай факты.';
        }

        return implode("\n", [
            $business_prompt,
            $customer_address_rule,
            'Отвечай кратко, понятно и по-русски.',
            'Если точных данных нет, честно скажи, что лучше уточнить у оператора, и предложи оставить вопрос.',
            'Не называй себя EasyChat, если это прямо не указано в правилах выше.',
        ]);
    }

    public function getEmergencyFallbackResponse() {
        return 'Помощник сейчас временно недоступен. Попробуйте задать вопрос ещё раз через минуту или дождитесь оператора — мы обязательно поможем.';
    }

     
    private function saveAiFailureNotice($chat_id, $reply_to_message_id): bool {
        return $this->saveAiMessageAndDispatch(
            (int)$chat_id,
            $this->getEmergencyFallbackResponse(),
            (int)$reply_to_message_id
        ) > 0;
    }

    private function saveAiMessageAndDispatch($chat_id, $ai_response, $reply_to_message_id = 0) {
        $model_message = new Message($this->registry);
        $model_session = new Session($this->registry);
        $chat_info = $model_session->getSessionById($chat_id);

        if (!$this->ensureAiReplyAllowed($chat_id, $reply_to_message_id, (string)($chat_info['source_provider'] ?? ''), 'ai_responder_save')) {
            return 0;
        }

        $reply_to_message_id = (int)$reply_to_message_id;
        $current_last_msg = [];
        if ($reply_to_message_id > 0) {
            $current_last_msg = $model_message->getLastMessage($chat_id);
            if (
                !$current_last_msg
                || (int)($current_last_msg['message_id'] ?? 0) !== $reply_to_message_id
                || (int)($current_last_msg['author'] ?? 0) !== 1
            ) {
                 
                 
                 
                 
                $this->logStaleAiResponseForExternalChannel($chat_id, $reply_to_message_id, $current_last_msg, $chat_info ?: []);
                return 0;
            }
        }
        $date_added = $reply_to_message_id > 0
            ? $this->dateAfterCustomerMessage((string)($current_last_msg['date_added'] ?? ''))
            : '';

        $message_id = $model_message->addMessage([
            'chat_id'      => $chat_id,
            'author'       => 3,
            'manager_id'   => 0,
            'text'         => urlencode($ai_response),
            'image'        => '',
            'status_user'  => 0,
            'status_admin' => 1,
            'ip'           => '127.0.0.1',
            'source_provider' => $chat_info['source_provider'] ?? 'widget',
            'source_channel_id' => (int)($chat_info['source_channel_id'] ?? 0),
            'date_added' => $date_added,
        ]);

        $model_session->updateModifiedDate($chat_id);
        $this->log("[Чат #{$chat_id}] Ответ ИИ сохранен в БД.");

        if ($chat_info) {
             
             
             
            try {
                $push_notifier = new AndroidPushNotifier($this->registry);
                $push_notifier->notifySyncChat(
                    $chat_id,
                    $chat_info['source_provider'] ?? 'widget',
                    $chat_info['customer_name'] ?? '',
                    strtoupper($chat_info['source_provider'] ?? 'widget')
                );
            } catch (\Throwable $e) {
                $this->log("[Чат #{$chat_id}] Ошибка отправки sync_chat пуша: " . $e->getMessage());
            }

            try {
                $customer_push_notifier = new CustomerWebPushNotifier($this->registry);
                $customer_push_notifier->notifyReply($chat_id, 'ai');
            } catch (\Throwable $e) {
                $this->log("[Чат #{$chat_id}] Ошибка отправки customer WebPush: " . $e->getMessage());
            }
        }

        if ($chat_info) {
            $outbound_guard = new OutboundDispatchGuard($this->registry);
            $delivery_result = $outbound_guard->sendMessageId((int)$message_id);
            if (empty($delivery_result['success'])) {
                try {
                    (new MessengerLogger($this->registry, (string)($chat_info['source_provider'] ?? '')))->warning('ai_outbound_delivery_not_confirmed', [
                        'provider' => (string)($chat_info['source_provider'] ?? ''),
                        'chat_id' => (int)$chat_id,
                        'message_id' => (int)$message_id,
                        'retry_scheduled' => !empty($delivery_result['retry_scheduled']) ? 1 : 0,
                        'outcome_unknown' => !empty($delivery_result['outcome_unknown']) ? 1 : 0,
                        'error' => (string)($delivery_result['error'] ?? 'send_failed'),
                    ]);
                } catch (\Throwable $e) {
                     
                }
            }
        }

        return (int)$message_id;
    }

    private function logStaleAiResponseForExternalChannel(int $chat_id, int $target_message_id, array $current_last_msg, array $chat_info): void {
        $provider = strtolower((string)($chat_info['source_provider'] ?? 'widget'));
        if ($provider === '' || $provider === 'widget') {
            return;
        }

        try {
            (new MessengerLogger($this->registry, $provider))->info('ai_response_stale_last_message_changed', [
                'provider' => $provider,
                'chat_id' => $chat_id,
                'target_message_id' => $target_message_id,
                'current_message_id' => (int)($current_last_msg['message_id'] ?? 0),
                'current_author' => (int)($current_last_msg['author'] ?? 0),
                'current_source_provider' => (string)($current_last_msg['source_provider'] ?? ''),
                'current_source_channel_id' => (int)($current_last_msg['source_channel_id'] ?? 0),
            ]);
        } catch (\Throwable $e) {
             
        }
    }

    private function dateAfterCustomerMessage(string $customer_date): string {
        $customer_date = trim($customer_date);
        if ($customer_date === '' || !preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $customer_date)) {
            return '';
        }

        $clock = new AppClock($this->registry);
        $customer_timestamp = $clock->sqlToTimestamp($customer_date);
        if ($customer_timestamp === false) {
            return '';
        }

         
         
         
        $now_timestamp = $clock->sqlToTimestamp($clock->nowSql());
        $answer_timestamp = max($customer_timestamp + 1, $now_timestamp ?: 0);

        return $clock->timestampToSql($answer_timestamp);
    }

    private function productSupportRequestKey(int $chat_id, int $message_id): string {
        $secret = (string)(new Setting($this->registry))->get('chat_install_secret', 'easychat-local');
        return hash_hmac('sha256', 'official-product-support|' . $chat_id . '|' . $message_id, $secret);
    }

    private function storeOfficialSupportContext(int $chat_id, int $message_id, array $support_result): void {
        try {
            (new MessageContext($this->registry))->upsertOfficialSupportContext($chat_id, $message_id, [
                'state' => (string)($support_result['state'] ?? 'answer'),
                'sources' => (array)($support_result['sources'] ?? []),
                'handoff' => (array)($support_result['handoff'] ?? ['show' => false]),
            ]);
        } catch (\Throwable $e) {
             
             
            $this->log('[official-support] Не удалось сохранить публичные источники: ' . $this->safeAiError($e->getMessage()));
        }
    }

    private function safeAiError($message) {
        $message = trim((string)$message);
        if ($message === '') {
            return 'unknown_ai_error';
        }

         
         
        $message = preg_replace('/Bearer\s+[A-Za-z0-9._\-]+/i', 'Bearer ***', $message);
        $message = preg_replace('/api[_-]?key=([^&\s]+)/i', 'api_key=***', $message);
        $message = preg_replace('/token=([^&\s]+)/i', 'token=***', $message);

        return $this->stringSubstring($message, 0, 255);
    }

    private function stringLength(string $value): int {
        return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
    }

    private function stringSubstring(string $value, int $start, int $length): string {
        return function_exists('mb_substr') ? mb_substr($value, $start, $length, 'UTF-8') : substr($value, $start, $length);
    }

    




    public function setTools(array $toolsConfig) {
        $this->tools = $toolsConfig;
        return $this;
    }

    




    public function setToolExecutor($executor) {
        $this->tool_executor = $executor;
        return $this;
    }

    public function getToolExecutor() {
        return $this->tool_executor;
    }

    






    public function callAPI($messages, $temperature = 0.6, array $options = []) {
        $provider = $this->createProvider($temperature);

        if (!$provider) {
            $this->log("[callAPI] Ошибка: не удалось создать провайдера '{$this->provider_name}'");
            return null;
        }

         
        $system_text = '';
        $history = [];
        $last_user_text = '';

        foreach ($messages as $msg) {
            if ($msg['role'] === 'system') {
                $system_text .= ($system_text ? "\n" : '') . $msg['content'];
            } else {
                $history[] = $msg;
            }
        }

         
        if (count($history) > 1) {
            $provider->setHistory(array_slice($history, 0, -1));
        }

         
        if ($system_text) {
            $provider->setContext($system_text);
        }

         
        $last_user_text = !empty($history) ? end($history)['content'] : '';

        if (empty($last_user_text)) {
            $this->log("[callAPI] Ошибка: нет текста для запроса к ИИ");
            return null;
        }

         
        $has_tools = !empty($this->tools) && $this->tool_executor;
        if ($has_tools && method_exists($provider, 'setTools')) {
            $provider->setTools($this->tools);
            $this->log("[callAPI] Зарегистрированы tools: " . implode(', ', array_map(function ($t) {
                return is_string($t) ? $t : (isset($t['name']) ? $t['name'] : '?');
            }, $this->tools)));
        }

        $this->log("[callAPI] Вызов провайдера '{$this->provider_name}' | Модель: {$this->model} | Темп: {$temperature}" . ($has_tools ? ' | tools=on' : ''));

        $query_options = array_merge($options, [
            'temperature' => $temperature,
        ]);
        if ($this->supportsVisualContext() && empty($query_options['idempotency_key'])) {
            $query_options['idempotency_key'] = $this->idempotencyKeyForMessages(
                $messages,
                'chat',
                (string)($query_options['idempotency_scope'] ?? '')
            );
        }

        if ($has_tools && method_exists($provider, 'runWithTools')) {
            $result = $provider->runWithTools($last_user_text, $this->tool_executor, $query_options);
        } else {
            $result = $provider->query($last_user_text, $query_options);
        }

        $this->log("[callAPI] Ответ от провайдера (Raw): " . $result);

        $text = $this->extractTextFromProviderResponse($result);
        if ($text) {
            return $text;
        }

        if ($this->isToolCallPayload($result)) {
            $this->log("[callAPI] Подавлен служебный tool-call payload, вместо него возвращен безопасный fallback.");
            return $this->toolCallFallbackResponse();
        }

        $http_status = method_exists($provider, 'getLastHttpStatus') ? (int)$provider->getLastHttpStatus() : 0;
        $transport_error = method_exists($provider, 'getLastTransportError') ? (string)$provider->getLastTransportError() : '';
        if ($http_status || $transport_error !== '') {
            $this->log("[callAPI] Пустой ответ провайдера. http_status={$http_status}, transport_error=" . $this->safeAiError($transport_error));
        } else {
            $this->log("[callAPI] Пустой или нераспознанный ответ провайдера без transport error.");
        }

        return null;
    }

    public function callAPIStream($messages, $temperature = 0.6, array $options = [], ?callable $on_delta = null) {
        $provider = $this->createProvider($temperature);

        if (!$provider) {
            $this->log("[callAPIStream] Ошибка: не удалось создать провайдера '{$this->provider_name}'");
            return null;
        }

        $system_text = '';
        $history = [];
        foreach ($messages as $msg) {
            if ($msg['role'] === 'system') {
                $system_text .= ($system_text ? "\n" : '') . $msg['content'];
            } else {
                $history[] = $msg;
            }
        }

        if (count($history) > 1) {
            $provider->setHistory(array_slice($history, 0, -1));
        }
        if ($system_text) {
            $provider->setContext($system_text);
        }

        $last_user_text = !empty($history) ? end($history)['content'] : '';
        if (empty($last_user_text)) {
            $this->log("[callAPIStream] Ошибка: нет текста для запроса к ИИ");
            return null;
        }

        $has_tools = !empty($this->tools) && $this->tool_executor;
        if ($has_tools && method_exists($provider, 'setTools')) {
            $provider->setTools($this->tools);
            $this->log("[callAPIStream] Зарегистрированы tools: " . implode(', ', array_map(function ($t) {
                return is_string($t) ? $t : (isset($t['name']) ? $t['name'] : '?');
            }, $this->tools)));
        }

        $this->log("[callAPIStream] Stream вызов провайдера '{$this->provider_name}' | Модель: {$this->model} | Темп: {$temperature}" . ($has_tools ? ' | tools=on' : ''));

        $query_options = array_merge($options, [
            'temperature'    => $temperature,
            'stream_timeout' => 120,
        ]);
        if ($this->supportsVisualContext() && empty($query_options['idempotency_key'])) {
            $query_options['idempotency_key'] = $this->idempotencyKeyForMessages(
                $messages,
                'stream',
                (string)($query_options['idempotency_scope'] ?? '')
            );
        }

        if ($has_tools && method_exists($provider, 'runWithToolsStream')) {
            $result = $provider->runWithToolsStream($last_user_text, $this->tool_executor, $query_options, $on_delta);
        } elseif (method_exists($provider, 'streamChat')) {
            $result = $provider->streamChat($last_user_text, $query_options, $on_delta);
        } else {
            $result = $provider->query($last_user_text, $query_options);
        }

        $this->log("[callAPIStream] Финальный raw response: " . $result);
        $text = $this->extractTextFromProviderResponse($result);
        if (!$text && $this->isToolCallPayload($result)) {
            $this->log("[callAPIStream] Подавлен служебный tool-call payload, вместо него возвращен безопасный fallback.");
            $fallback = $this->toolCallFallbackResponse();
            if ($on_delta) {
                $on_delta($fallback);
            }
            return $fallback;
        }
        if (!$text) {
            $http_status = method_exists($provider, 'getLastHttpStatus') ? (int)$provider->getLastHttpStatus() : 0;
            $transport_error = method_exists($provider, 'getLastTransportError') ? (string)$provider->getLastTransportError() : '';
            $this->log("[callAPIStream] Пустой stream-ответ. http_status={$http_status}, transport_error=" . $this->safeAiError($transport_error));
        }
        return $text;
    }

    private function idempotencyKeyForMessages(array $messages, string $phase, string $scope): string {
        $payload_hash = hash(
            'sha256',
            (string)json_encode($messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
        );
        return substr(
            hash('sha256', 'easychat-ai|' . $phase . '|' . $scope . '|' . $this->model . '|' . $payload_hash),
            0,
            64
        );
    }

    private function extractTextFromProviderResponse($result) {
        if (!$result) {
            return null;
        }
        if ($this->isToolCallPayload($result)) {
            return null;
        }
        $json = is_array($result) ? $result : json_decode((string)$result, true);
        if (!is_array($json)) {
            return null;
        }
        if (isset($json['choices'][0]['text'])) {
            $text = trim((string)$json['choices'][0]['text']);
            return $this->isToolCallPayload($text) ? null : $text;
        }
        if (isset($json['choices'][0]['message']['content'])) {
            $text = trim((string)$json['choices'][0]['message']['content']);
            return $this->isToolCallPayload($text) ? null : $text;
        }
        if (isset($json['candidates'][0]['content']['parts'][0]['text'])) {
            $text = trim((string)$json['candidates'][0]['content']['parts'][0]['text']);
            return $this->isToolCallPayload($text) ? null : $text;
        }
        return null;
    }

    private function isToolCallPayload($value) {
        if ($value === null || $value === '') {
            return false;
        }

        $payload = is_array($value) ? $value : json_decode(trim((string)$value), true);
        if (!is_array($payload)) {
            return false;
        }

        if (isset($payload['choices'][0]['message']['tool_calls'])) {
            return true;
        }
        if (isset($payload['choices'][0]['finish_reason']) && in_array((string)$payload['choices'][0]['finish_reason'], ['tool_calls', 'tool_calls_exhausted'], true)) {
            return true;
        }

        if (isset($payload['candidates']) && is_array($payload['candidates'])) {
            foreach ($payload['candidates'] as $candidate) {
                if ((isset($candidate['finishReason']) && strtoupper((string)$candidate['finishReason']) === 'FUNCTION_CALL')) {
                    return true;
                }
                $parts = $candidate['content']['parts'] ?? [];
                if (is_array($parts)) {
                    foreach ($parts as $part) {
                        if (isset($part['functionCall'])) {
                            return true;
                        }
                    }
                }
            }
        }

        if (isset($payload['action']) && is_string($payload['action']) && preg_match('/^(search_products|search_pages|[a-z0-9_]+)$/i', $payload['action'])) {
            return true;
        }
        if (isset($payload['functionCall']) || isset($payload['tool_calls']) || isset($payload['tool_call_id'])) {
            return true;
        }

        return false;
    }

    private function toolCallFallbackResponse() {
        return 'Сейчас уточняю данные по каталогу. Оператор поможет проверить наличие и подобрать подходящий вариант.';
    }

    


    private function createProvider($temperature = 0.6) {
        $config = [
            'model'       => $this->model,
            'temperature' => $temperature,
        ];

        if ($this->api_host) {
            $config['host'] = rtrim($this->api_host, '/');
        }

        $proxy = (new ProxyPolicy($this->registry))->proxyForProfile($this->proxyProfileForAiProvider($this->provider_name));
        if ($proxy !== null) {
            $config['proxy'] = $proxy;
        }

        if ($this->provider_name === 'easytokens') {
            $config['host'] = 'https://api.easytokens.ru';
            if (empty($config['model']) || strpos($config['model'], 'gemini') !== false) {
                $config['model'] = 'easylite';
            }
            return new EasyTokensProvider($this->api_key, $config);
        } elseif ($this->provider_name === 'gemini') {
            $model = strtolower((string)$config['model']);
            if (strpos($model, 'models/') === 0) {
                $model = substr($model, 7);
                $config['model'] = substr((string)$config['model'], 7);
            }
            if ($model === '' || strpos($model, 'gemini-') !== 0) {
                $config['model'] = 'gemini-3.5-flash';
            }
            return new Gemini($this->api_key, $config);
        } else {
            $model = strtolower((string)$config['model']);
            if ($model === '' || $model === 'easylite' || strpos($model, 'gemini-') === 0 || strpos($model, 'models/gemini-') === 0) {
                $config['model'] = 'gpt-4o-mini';
            }
            return new OpenAI($this->api_key, $config);
        }
    }

    




    public function getProvider() {
        return $this->createProvider();
    }

    private function proxyProfileForAiProvider($provider_name) {
        $provider_name = strtolower(trim((string)$provider_name));
        if ($provider_name === 'gemini') {
            return 'ai_gemini';
        }
        if ($provider_name === 'easytokens') {
            return 'ai_easytokens';
        }
        return 'ai_openai';
    }

    


    public function log($message) {
        $model_setting = new Setting($this->registry);
        if ($model_setting->get('chat_ai_logging', '0') !== '1') {
            return;
        }

         
        $masked_message = $message;
        if (!empty($this->api_key)) {
            $masked_message = str_replace($this->api_key, substr($this->api_key, 0, 8) . '...', $masked_message);
        }

        Logger::write($masked_message, 'ai.log');
    }
}
