<?php
namespace EasyChat\Controller;

use EasyChat\Library\AttachmentStorage;
use EasyChat\Library\Attachment\AttachmentBatchUploadService;
use EasyChat\Library\Messenger\AttachmentCapabilities;
use EasyChat\Library\AndroidPushNotifier;
use EasyChat\Library\Ai\AiResponseTimingPolicy;
use EasyChat\Library\AppClock;
use EasyChat\Library\CustomerContactService;
use EasyChat\Library\Cms\CmsVisitorIdentityService;
use EasyChat\Library\I18n;
use EasyChat\Library\MessageTextRenderer;
use EasyChat\Library\Messenger\OutboundAttachmentLink;
use EasyChat\Library\Messenger\MessageReadReceiptService;
use EasyChat\Library\PresenceService;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Library\ResponseTimeMetrics;
use EasyChat\Library\SafeRuntime;
use EasyChat\Library\Support\OfficialProductSupportProfile;
use EasyChat\Library\WidgetDomainPolicy;
use EasyChat\View\AccessibleColor;
use EasyChat\Model\Setting;
use EasyChat\Model\Manager;
use EasyChat\Model\Session;
use EasyChat\Model\Message;
use EasyChat\Model\MessageContext;
use EasyChat\Library\AiResponder;

class Widget extends Base {

    protected $layouts = '';  
     
    private $widget_domain_policy = null;
     
    private $deferred_ai_message_id = 0;

     
    public function index() {
        $this->sendWidgetScriptHeaders();

        $model_setting = new Setting($this->registry);
        $server = \EasyChat\Library\RequestContext::server();
        if (!$this->widgetDomainPolicy($model_setting)->isRequestAllowed($server)) {
            $has_origin_or_referer = WidgetDomainPolicy::normalizeHost((string)($server['HTTP_ORIGIN'] ?? '')) !== ''
                || WidgetDomainPolicy::normalizeHost((string)($server['HTTP_REFERER'] ?? '')) !== '';
            echo $has_origin_or_referer
                ? "console.warn('EasyChat: Unauthorized widget domain loading attempt.');"
                : "console.warn('EasyChat: Referer missing. Loading blocked.');";
            return;
        }

         
        $status = $model_setting->get('chat_status', '0');

        if ($status !== '1') {
            echo "// EasyChat is disabled in settings.";
            return;
        }

         
        $theme_color = $this->normalizeThemeColor($model_setting->get('chat_widget_color', '#6366f1'));
        $theme_foreground = AccessibleColor::foregroundFor(
            $theme_color,
            $model_setting->get('chat_on_accent_mode', AccessibleColor::MODE_AUTO)
        );
        $theme_style = $this->normalizeThemeStyle($model_setting->get('chat_widget_theme', 'light'));
        $widget_position = $this->normalizeWidgetPosition($model_setting->get('chat_widget_position', 'right'));
        $widget_offset_x = $this->normalizeWidgetOffset($model_setting->get('chat_widget_offset_x', '20'));
        $widget_offset_y = $this->normalizeWidgetOffset($model_setting->get('chat_widget_offset_y', '20'));
        $official_support = new OfficialProductSupportProfile($this->registry);
        $official_support_ready = $official_support->isEnabledForCurrentInstallation()
            && $official_support->hasServiceCredential();
        $ai_live_typing = $model_setting->get('chat_ai_live_typing', '1') === '1' || $official_support_ready ? '1' : '0';
        $immediate_ai_reply = ($official_support_ready || (
            $model_setting->get('chat_ai_status', '0') === '1'
            && (new AiResponseTimingPolicy($this->registry))->initialQueueDelaySeconds() === 0
        )) ? '1' : '0';
        $widget_show_operators = $model_setting->get('chat_widget_show_operators', '1') === '1' ? '1' : '0';
        $widget_social_hover = $model_setting->get('chat_widget_social_hover', '1') === '1' ? '1' : '0';
        $cms_page_context = $this->cmsPageContextEnabled($model_setting) ? '1' : '0';
        $widget_locale = $this->resolveWidgetLocale($model_setting);
        $general_title = $this->resolveLocalizedWidgetSetting($model_setting, 'chat_widget_title', 'widget.title', $widget_locale);

         
         
        $avg_sec = (new ResponseTimeMetrics($this->registry))->getGlobalAverageSeconds(15);

         
        $minutes = (int)round($avg_sec / 60);
        if ($minutes < 1) {
            $minutes = 1;
        }

        if ($minutes <= 10) {
            $header_status = I18n::translate('widget.avg_response_n', ['minutes' => $minutes], $widget_locale);
        } else {
            $header_status = I18n::translate('widget.avg_response_n', ['minutes' => 10], $widget_locale);
        }

		$widget_text  = '<div style="font-size: 11px; line-height: 1.4; opacity: 0.95; margin-top: 8px; font-family: inherit; text-align: center; white-space: pre-line;">';
        $widget_text .= htmlspecialchars($this->resolveLocalizedWidgetSetting($model_setting, 'chat_widget_text', 'widget.footer_text', $widget_locale), ENT_QUOTES, 'UTF-8');
		$widget_text .= '</div>';
        $welcome_msg = $this->resolveLocalizedWidgetSetting($model_setting, 'chat_widget_welcome', 'widget.welcome', $widget_locale);


        $phone_url = $model_setting->get('chat_widget_phone');
        $whatsapp_url = $model_setting->get('chat_widget_whatsapp');
        $email_url = $model_setting->get('chat_widget_email');
        $telegram_url = $model_setting->get('chat_widget_telegram');
        $max_url = $model_setting->get('chat_widget_max');
        $vk_url = $model_setting->get('chat_widget_vk');

        $operators_html = '';
        if ($widget_show_operators === '1') {
             
            $model_manager = new Manager($this->registry);
            $managers = $model_manager->getActiveManagersForWidget();

            foreach ($managers as $manager) {
                 
                $presence = new PresenceService($this->registry);
                $is_online = false;
                if (!empty($manager['last_active'])) {
                    if ($presence->isLastActiveOnline($manager['last_active'])) {
                        $is_online = true;
                    }
                }

                $status_color = $is_online ? '#10B981' : '#9CA3AF';
                $avatar_opacity = $is_online ? '1' : '0.55';

                 
                $parts = preg_split('/\s+/', trim($manager['name']));
                $first_name = $parts[0] ?? $manager['name'];

                 
                if (count($parts) >= 2) {
                    $initials = mb_strtoupper(mb_substr($parts[0], 0, 1, 'UTF-8') . mb_substr($parts[1], 0, 1, 'UTF-8'), 'UTF-8');
                } else {
                    $initials = mb_strtoupper(mb_substr($first_name, 0, 1, 'UTF-8'), 'UTF-8');
                }

                 
                $avatar_content = '';
                if (!empty($manager['image']) && is_file(DIR_AVATARS . $manager['image'])) {
                    $avatar_url = HTTP_AVATARS . $manager['image'];
                    $avatar_content = '<img src="' . $avatar_url . '" style="width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block;" />';
                } else {
                    $avatar_content = htmlspecialchars($initials);
                }

                 
                $avatar_bg = $this->getAvatarColor($manager['name']);


                $operators_html .= '
                <div class="easychat-operator-item" data-manager-id="' . (int)$manager['id'] . '">
                    <div style="position: relative; width: 44px; height: 44px; margin-bottom: 5px;">
                        <div class="easychat-operator-avatar" style="width: 44px; height: 44px; border-radius: 50%; font-size: 14px; font-weight: bold; border: 2px solid rgba(255,255,255,0.85); background-color: ' . $avatar_bg . '; color: #ffffff; display: flex; align-items: center; justify-content: center; opacity: ' . $avatar_opacity . '; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.06);">
                            ' . $avatar_content . '
                        </div>
                        <span class="easychat-operator-status" style="position: absolute; top: 0; right: 0; width: 11px; height: 11px; border-radius: 50%; background-color: ' . $status_color . '; border: 1.8px solid #ffffff; box-shadow: 0 1px 3px rgba(0,0,0,0.15);"></span>
                    </div>
                    <div class="easychat-operator-name">' . htmlspecialchars($manager['name']) . '</div>
                </div>';
            }
        }

         
        $social_buttons = [];
        if ($phone_url) {
            $phone_href = (strpos($phone_url, 'tel:') === 0) ? $phone_url : 'tel:' . $phone_url;
            $social_buttons[] = '<a href="' . htmlspecialchars($phone_href) . '" class="easychat-quick-social-btn" style="background: #10B981;"><svg viewBox="0 0 24 24" style="width: 22px; height: 22px; fill: #ffffff;"><path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.01-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"/></svg></a>';
        }
        if ($email_url) {
            $email_href = (strpos($email_url, 'mailto:') === 0) ? $email_url : 'mailto:' . $email_url;
            $social_buttons[] = '<a href="' . htmlspecialchars($email_href) . '" class="easychat-quick-social-btn" style="background: #06B6D4;"><svg viewBox="0 0 24 24" style="width: 22px; height: 22px; fill: #ffffff;"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg></a>';
        }
        if ($max_url) {
            $social_buttons[] = '<a href="' . htmlspecialchars($max_url) . '" target="_blank" rel="noopener noreferrer nofollow ugc" referrerpolicy="no-referrer" class="easychat-quick-social-btn" style="background: linear-gradient(135deg, #3B82F6, #EC4899);"><svg xmlns="http://www.w3.org/2000/svg" style="width: 22px; height: 22px; fill: #ffffff;" viewBox="0 0 720 720"><path fill="#fff" d="M350.4,9.6C141.8,20.5,4.1,184.1,12.8,390.4c3.8,90.3,40.1,168,48.7,253.7,2.2,22.2-4.2,49.6,21.4,59.3,31.5,11.9,79.8-8.1,106.2-26.4,9-6.1,17.6-13.2,24.2-22,27.3,18.1,53.2,35.6,85.7,43.4,143.1,34.3,299.9-44.2,369.6-170.3C799.6,291.2,622.5-4.6,350.4,9.6h0ZM269.4,504c-11.3,8.8-22.2,20.8-34.7,27.7-18.1,9.7-23.7-.4-30.5-16.4-21.4-50.9-24-137.6-11.5-190.9,16.8-72.5,72.9-136.3,150-143.1,78-6.9,150.4,32.7,183.1,104.2,72.4,159.1-112.9,316.2-256.4,218.6h0Z"/></svg></a>';
        }
        if ($vk_url) {
            $social_buttons[] = '<a href="' . htmlspecialchars($vk_url) . '" target="_blank" rel="noopener noreferrer nofollow ugc" referrerpolicy="no-referrer" class="easychat-quick-social-btn" style="background: #2787F5;"><svg viewBox="1.76 3.28 24 24" style="width: 22px; height: 22px; fill: #ffffff;"><path d="M15.66 22c-7.36 0-11.56-5.04-11.74-13.44H7.5c.12 6.16 2.84 8.76 5 9.3V8.56H16v5.3c1.33-.14 3.55-2.58 4.02-5.3h3.58a11.19 11.19 0 0 1-3.69 7.02c1.37 1.28 3.12 3.5 3.12 6.42h-3.58c0-2.82-2.28-5.06-3.23-6.02v6.02h-4.25z"/></svg></a>';
        }
        if ($telegram_url) {
            $social_buttons[] = '<a href="' . htmlspecialchars($telegram_url) . '" target="_blank" rel="noopener noreferrer nofollow ugc" referrerpolicy="no-referrer" class="easychat-quick-social-btn" style="background: #3B82F6;"><svg viewBox="0 0 24 24" style="width: 22px; height: 22px; fill: #ffffff;"><path d="M9.78 18.65l.28-4.28 7.68-7.05c.34-.31-.07-.48-.52-.17L7.69 12.1 3.51 10.8c-.91-.29-.93-.91.19-1.35l16.32-6.4c.76-.28 1.42.19 1.17 1.34l-2.77 13.25c-.21 1.05-.83 1.31-1.7 1l-4.22-3.15-2.04 1.99c-.23.23-.42.42-.87.42z"/></svg></a>';
        }
        if ($whatsapp_url) {
            $social_buttons[] = '<a href="' . htmlspecialchars($whatsapp_url) . '" target="_blank" rel="noopener noreferrer nofollow ugc" referrerpolicy="no-referrer" class="easychat-quick-social-btn" style="background: #8B5CF6;"><svg viewBox="0 0 24 24" style="width: 22px; height: 22px; fill: #ffffff;"><path d="M12.04 2c-5.46 0-9.91 4.45-9.91 9.91 0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21 5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.9-7.01A9.816 9.816 0 0 0 12.04 2zm5.8 14.2c-.24.69-1.23 1.27-1.69 1.32-.46.05-.9.23-2.94-.57-2.62-1.03-4.32-3.71-4.45-3.89-.13-.18-1.07-1.42-1.07-2.72 0-1.3.68-1.94.92-2.2.24-.26.52-.33.7-.33h.49c.16 0 .37-.06.57.43.2.49.69 1.69.75 1.82.06.13.1.28.02.44-.08.16-.18.33-.28.46-.11.13-.23.27-.33.39-.1.11-.21.24-.09.45.12.21.52.86 1.11 1.39.77.68 1.41.89 1.62.99.21.11.33.09.45-.05.12-.14.52-.6.66-.81.14-.21.28-.18.47-.11.2.07 1.25.59 1.47.7.22.11.37.16.42.25.05.09.05.53-.19 1.22z"/></svg></a>';
        }

        $socials_html = '';
        if ($social_buttons) {
            $socials_html = '<div style="display: flex; justify-content: center; gap: 10px; margin-top: 8px;">' . implode('', $social_buttons) . '</div>';
        }
        $quick_socials_html = ($widget_social_hover === '1' && $social_buttons) ? implode('', $social_buttons) : '';

         
        header('Access-Control-Allow-Origin: *');

         
        $js_path = SITE_PATH . DS . 'views' . DS . 'widget_js.php';
        if (is_file($js_path)) {
            ob_start();
            include($js_path);
            $js_content = ob_get_clean();

             
            $max_file_size = (int)$model_setting->get('chat_max_file_size', '5');
            $i18n_json = json_encode(I18n::dictionary($widget_locale, [
                'widget.input_placeholder',
                'widget.blocked_placeholder',
                'widget.theme_toggle',
                'widget.toggle_contacts',
                'widget.consultant',
                'widget.operator',
                'widget.manager',
                'widget.guest',
                'widget.cms_identity_as',
                'widget.cms_identity_verified',
                'widget.cms_identity_required',
                'widget.ai_typing',
                'widget.ai_consultant',
                'widget.support_docs',
                'widget.support_more',
                'widget.support_handoff',
                'widget.support_telegram',
                'widget.support_max',
                'widget.typing_suffix',
                'widget.upload_images_only',
                'widget.max_file_size',
                'widget.attachment_image',
                'widget.attachment_file',
                'widget.attachment_remove',
                'widget.attachments_selected',
                'widget.attachments_limit',
                'widget.attachments_too_many',
                'widget.documents_too_many',
                'widget.push_enable',
                'widget.push_disable',
                'widget.push_enabled',
                'widget.push_disabled',
                'widget.push_disable_failed',
                'widget.push_unavailable',
                'widget.push_site_disabled',
                'widget.push_denied',
                'widget.push_loading',
                'widget.push_after_message_hint',
                'widget.push_wait_message',
                'widget.push_ios_install_hint',
                'widget.push_ios_after_message_hint',
            ]), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            $js_content = str_replace(
                [
                    '{{CHAT_URL}}',
                    '{{ASSET_URL}}',
                    '{{THEME_COLOR}}',
                    '{{THEME_FOREGROUND}}',
                    '{{THEME_STYLE_JSON}}',
                    '{{WIDGET_TITLE_JSON}}',
                    '{{WELCOME_MSG_JSON}}',
                    '{{MAX_FILE_SIZE}}',
                    '{{WIDGET_TEXT_JSON}}',
                    '{{OPERATORS_LIST}}',
                    '{{SOCIALS_LIST}}',
                    '{{QUICK_SOCIALS_LIST}}',
                    '{{WIDGET_SHOW_OPERATORS}}',
                    '{{WIDGET_SOCIAL_HOVER}}',
                    '{{HEADER_STATUS_JSON}}',
                    '{{I18N_JSON}}',
                    '{{WIDGET_LOCALE_JSON}}',
                    '{{WIDGET_POSITION}}',
                    '{{WIDGET_OFFSET_X}}',
                    '{{WIDGET_OFFSET_Y}}',
                    '{{AI_LIVE_TYPING}}',
                    '{{AI_IMMEDIATE_REPLY}}',
                    '{{CMS_PAGE_CONTEXT}}',
                    '{{ASSET_VERSION}}',
                    '{{PDF_JS_URL}}',
                    '{{PDF_WORKER_URL}}',
                    '{{WIDGET_SIDE_CSS}}',
                    '{{WIDGET_OPPOSITE_SIDE_CSS}}',
                    '{{WIDGET_ALIGN_ITEMS}}',
                    '{{WIDGET_TRANSFORM_ORIGIN}}'
                ],
                [
                    HTTP_SERVER,
                    easychat_asset_base_url(),
                    $theme_color,
                    $theme_foreground,
                    $this->jsonForScript($theme_style),
                    $this->jsonForScript($general_title),
                    $this->jsonForScript($welcome_msg),
                    $max_file_size,
                    $this->jsonForScript($widget_text),
                    $operators_html,
                    $socials_html,
                    $quick_socials_html,
                    $widget_show_operators,
                    $widget_social_hover,
                    $this->jsonForScript($header_status),
                    $i18n_json ?: '{}',
                    $this->jsonForScript($widget_locale),
                    $widget_position,
                    $widget_offset_x,
                    $widget_offset_y,
                    $ai_live_typing,
                    $immediate_ai_reply,
                    $cms_page_context,
                    easychat_emoji_asset_version(),
                    easychat_asset_versioned_url('js/vendor/pdfjs/pdf.min.js'),
                    easychat_asset_versioned_url('js/vendor/pdfjs/pdf.worker.min.js'),
                    $widget_position,
                    ($widget_position === 'left') ? 'right' : 'left',
                    ($widget_position === 'left') ? 'flex-start' : 'flex-end',
                    ($widget_position === 'left') ? 'bottom left' : 'bottom right'
                ],
                $js_content
            );

            echo $js_content;
        } else {
            echo "console.error('EasyChat: widget template not found.');";
        }
    }

    private function sendWidgetScriptHeaders() {
        if (headers_sent()) {
            return;
        }

        if (function_exists('header_remove')) {
            header_remove('X-Powered-By');
        }

        header('Content-Type: application/javascript; charset=utf-8');
        header('X-Content-Type-Options: nosniff');
        header('Cache-Control: private, max-age=0, must-revalidate');
        header('Vary: Referer, Accept-Language');
    }

    private function jsonForScript($value) {
        $json = json_encode((string)$value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
        return is_string($json) ? $json : '""';
    }

    private function resolveWidgetLocale($model_setting) {
        $explicit = $model_setting->get('chat_widget_locale', 'ru');
        $auto = $model_setting->get('chat_widget_locale_auto', '1') === '1';
        if (!$auto) {
            return I18n::normalizeLocale($explicit);
        }
        $request_locale = trim(\EasyChat\Library\RequestContext::get()['locale'] ?? \EasyChat\Library\RequestContext::post()['locale'] ?? '');
        if ($request_locale !== '' && strtolower($request_locale) !== 'auto') {
            return I18n::normalizeLocale($request_locale);
        }
        return I18n::resolveFromAcceptLanguage(\EasyChat\Library\RequestContext::server()['HTTP_ACCEPT_LANGUAGE'] ?? $explicit);
    }

    private function resolveLocalizedWidgetSetting($model_setting, $setting_key, $translation_key, $locale) {
        $value = (string)$model_setting->get($setting_key, '');
        if ($this->isDefaultWidgetTranslation($value, $translation_key)) {
            return I18n::translate($translation_key, [], $locale);
        }
        return $value;
    }

    private function isDefaultWidgetTranslation($value, $translation_key) {
        $normalized_value = $this->normalizeWidgetText($value);
        if ($normalized_value === '') {
            return true;
        }
        foreach (I18n::supportedLocales() as $locale) {
            $default = I18n::translate($translation_key, [], $locale);
            if ($normalized_value === $this->normalizeWidgetText($default)) {
                return true;
            }
        }
        return false;
    }

    private function normalizeWidgetText($value) {
        $value = str_replace(["\r\n", "\r"], "\n", (string)$value);
        $value = preg_replace('/[ \t]+/u', ' ', $value);
        return trim($value);
    }

    private function normalizeThemeColor($value) {
        return AccessibleColor::normalizeHex($value);
    }

    private function normalizeThemeStyle($value) {
        return ((string)$value === 'dark') ? 'dark' : 'light';
    }

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

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

    private function resolveWidgetRequestLocale($model_setting, array $json = []) {
        $explicit = $model_setting->get('chat_widget_locale', 'ru');
        $auto = $model_setting->get('chat_widget_locale_auto', '1') === '1';
        if (!$auto) {
            return I18n::normalizeLocale($explicit);
        }
        $request_locale = trim((string)($json['locale'] ?? \EasyChat\Library\RequestContext::get()['locale'] ?? \EasyChat\Library\RequestContext::post()['locale'] ?? ''));
        if ($request_locale !== '' && strtolower($request_locale) !== 'auto') {
            return I18n::normalizeLocale($request_locale);
        }
        return I18n::resolveFromAcceptLanguage(\EasyChat\Library\RequestContext::server()['HTTP_ACCEPT_LANGUAGE'] ?? $explicit);
    }

     
    public function init() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            http_response_code(403);
            $this->jsonOut(['success' => false, 'error' => 'unauthorized_widget_domain']);
            return;
        }

        $json = $this->readJsonInput();
        $session_id = trim($json['session_id'] ?? '');

        $model_session = new Session($this->registry);
        $model_message = new Message($this->registry);
        $widget_locale = $this->resolveWidgetRequestLocale($model_setting, $json);

        $chat_session = null;

        if ($session_id) {
            \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;
            $chat_session = $model_session->getSessionBySid($session_id);
        }

        $identity_state = $this->authorizeCmsVisitor($json, $session_id, $chat_session);
        if (empty($identity_state['allowed'])) {
            http_response_code(401);
            $this->jsonOut([
                'success' => false,
                'error' => (string)$identity_state['error'],
                'reset_session' => !empty($identity_state['reset_session']),
            ]);
            return;
        }
        if (!empty($identity_state['chat'])) {
            $chat_session = $identity_state['chat'];
        }

        $blacklist = new \EasyChat\Library\BlacklistService($this->registry);
        $ip = \EasyChat\Library\RequestContext::server()['REMOTE_ADDR'] ?? '';
        $chat_id_check = ($chat_session && isset($chat_session['chat_id'])) ? (int)$chat_session['chat_id'] : 0;
        $is_blocked = $blacklist->isBlocked($ip, $chat_id_check);

         
        $is_virtual = false;
        if (!$chat_session) {
            $is_virtual = true;
            if (!$session_id || strlen($session_id) !== 32) {
                $session_id = bin2hex(random_bytes(16));
            }
            \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;
            $customer_name = $this->generateVisitorName($session_id, $widget_locale);

            $chat_session = [
                'chat_id'       => 0,
                'session_id'    => $session_id,
                'customer_name' => $customer_name,
            ];
        }

        $chat_id = (int)$chat_session['chat_id'];
        $messages = [];
        $typing = 'none';
        $typing_name = '';
        $typing_image = '';
        $typing_message_id = 0;
        $hide_welcome_card = false;
        $badge_only = !empty($json['badge_only']);
        $stream_capable = !empty($json['stream_capable']);
        $stream_suppressed_message_id = (int)($json['stream_suppressed_message_id'] ?? 0);
        $ai_stream_available = false;
        $ai_stream_message_id = 0;
        $last_message_id = 0;
        $next_message_id = 0;

        if (!$is_virtual) {
            $delayed_stream = $this->resolveDelayedAiStreamDecision(
                $chat_id,
                $badge_only,
                $stream_capable,
                $stream_suppressed_message_id,
                $model_setting,
                $model_session,
                $model_message
            );
            $ai_stream_available = !empty($delayed_stream['available']);
            $ai_stream_message_id = (int)($delayed_stream['message_id'] ?? 0);
        }

        if ($badge_only) {
            $messages_count = 0;
            $operator_messages_count = 0;
            $unread_count = 0;
            if (!$is_virtual) {
                $db = $this->registry->get('db');
                $prefix = easychat_db_prefix();
                $counts_query = $db->query("SELECT
                    COUNT(*) AS total,
                    SUM(CASE WHEN author != 1 THEN 1 ELSE 0 END) AS operator_total
                    FROM `{$prefix}messages`
                    WHERE `chat_id` = " . (int)$chat_id);
                if ($counts_query->row) {
                    $messages_count = (int)$counts_query->row['total'];
                    $operator_messages_count = (int)$counts_query->row['operator_total'];
                }
                $unread_count = $model_message->getUnreadCountForUser($chat_id);
            }
            $this->jsonOut([
                'success'                 => true,
                'session_id'              => $session_id,
                'chat_id'                 => $chat_id,
                'customer_name'           => $chat_session['customer_name'],
                'email'                   => '',
                'unread_count'            => $unread_count,
                'messages_count'          => $messages_count,
                'operator_messages_count' => $operator_messages_count,
                'blocked'                 => $is_blocked
                ,'cms_identity'            => $this->publicCmsIdentity($identity_state)
            ]);
            return;
        }

        if (!$is_virtual) {
            $last_message_id = max(0, (int)($json['last_message_id'] ?? 0));
            $next_message_id = $last_message_id;
             
            $messages_rows = $model_message->getMessagesForUser($chat_id, 50, $last_message_id);

            $storage = new AttachmentStorage($this->registry);
            $sync_variant_count = 0;
            $image_session_id = (string)($chat_session['session_id'] ?? $session_id);
            $message_ids = [];
            foreach ($messages_rows as $row) {
                $message_ids[] = (int)$row['message_id'];
            }
            $attachments_by_message = $storage->getAttachmentsByMessageIds($message_ids);
            $support_by_message = (new MessageContext($this->registry))->getOfficialSupportContextsByMessageIds($message_ids);
            $clock = new AppClock($this->registry);
            $message_text_renderer = new MessageTextRenderer();
            foreach ($messages_rows as $row) {
                $next_message_id = max($next_message_id, (int)$row['message_id']);
                $image_urls = ['url' => '', 'full_url' => '', 'preview_url' => '', 'thumb_url' => ''];
                if ($row['image']) {
                    $image_urls = $storage->imageUrlSet($chat_id, (string)$row['image'], 'widget/image', [], $sync_variant_count < 2);
                    $image_urls = $this->protectWidgetImageUrls($chat_id, $image_urls, $image_session_id);
                    if ($sync_variant_count < 2) {
                        $sync_variant_count++;
                    }
                }
                $attachments = [];
                foreach ($attachments_by_message[(int)$row['message_id']] ?? [] as $attachment) {
                    $sync_variants = $sync_variant_count < 2;
                    $attachment_payload = $storage->attachmentPayload($chat_id, $attachment, 'widget/image', $sync_variants);
                    $attachments[] = $this->protectWidgetAttachmentUrls($chat_id, $attachment_payload, $image_session_id);
                    if ($sync_variants && (($attachment_payload['kind'] ?? '') === 'image')) {
                        $sync_variant_count++;
                    }
                }

                $m_name = '';
                if ((int)$row['author'] === 2) {
                    $m_name = $row['manager_name'] ? $row['manager_name'] : I18n::translate('widget.operator', [], $this->resolveWidgetLocale(new Setting($this->registry)));
                } elseif ((int)$row['author'] === 3) {
                    $m_name = I18n::translate('widget.ai_consultant', [], $this->resolveWidgetLocale(new Setting($this->registry)));
                }

                $plain_text = urldecode($row['text']);
                if ((int)$row['author'] === 3) {
                    $plain_text = MessageTextRenderer::withoutCitationMarkers($plain_text);
                }
                $text_html = $message_text_renderer->toHtml($plain_text);

                $messages[] = [
                    'id'            => (int)$row['message_id'],
                    'author'        => (int)$row['author'],
                    'manager_name'  => $m_name,
                    'manager_image' => (!empty($row['manager_image']) && (int)$row['author'] === 2) ? HTTP_AVATARS . $row['manager_image'] : '',
                    'text'          => $plain_text,
                    'text_html'     => $text_html,
                    'image'         => $image_urls['url'],
                    'full_url'      => $image_urls['full_url'],
                    'preview_url'   => $image_urls['preview_url'],
                    'thumb_url'     => $image_urls['thumb_url'],
                    'attachments'   => $attachments,
                    'support'       => $support_by_message[(int)$row['message_id']] ?? null,
                    'date_added'    => !empty($row['date_added']) ? $clock->sqlToIsoUtc((string)$row['date_added']) : $clock->nowIsoUtc()
                ];
            }

            $chat_info = $model_session->getSessionById($chat_id);
            if ($chat_info) {
                $now = time();
                if (isset($chat_info['operator_typing_expire']) && (int)$chat_info['operator_typing_expire'] > $now) {
                    $typing = 'operator';
                    $typing_name = !empty($chat_info['operator_typing_name']) ? $chat_info['operator_typing_name'] : I18n::translate('widget.manager', [], $this->resolveWidgetLocale(new Setting($this->registry)));
                    $typing_image = !empty($chat_info['operator_typing_image']) ? HTTP_AVATARS . $chat_info['operator_typing_image'] : '';
                } else {
                    $ai_typing_active = (
                        (isset($chat_info['ai_typing_expire']) && (int)$chat_info['ai_typing_expire'] > $now)
                        || (isset($chat_info['ai_processing_until']) && (int)$chat_info['ai_processing_until'] > $now)
                    );
                    if ($ai_typing_active) {
                        $last_for_typing = $model_message->getLastMessage($chat_id);
                        $processing_message_id = (int)($chat_info['ai_processing_message_id'] ?? 0);
                        $last_message_id_for_typing = (int)($last_for_typing['message_id'] ?? 0);

                         
                         
                         
                        if (
                            $last_for_typing
                            && (int)($last_for_typing['author'] ?? 0) === 1
                            && ($processing_message_id <= 0 || $processing_message_id === $last_message_id_for_typing)
                        ) {
                            $typing = 'ai';
                            $typing_name = I18n::translate('widget.ai_consultant', [], $this->resolveWidgetLocale(new Setting($this->registry)));
                            $typing_message_id = $processing_message_id > 0 ? $processing_message_id : $last_message_id_for_typing;
                        }
                    }
                }
            }

             
             
            $db = $this->registry->get('db');
            $prefix = easychat_db_prefix();
            $query_user = $db->query("SELECT message_id FROM `{$prefix}messages` WHERE `chat_id` = " . (int)$chat_id . " AND `author` = 1 LIMIT 1");
            if ($query_user->num_rows > 0) {
                $hide_welcome_card = true;
            }

             
            if (!$hide_welcome_card) {
                $query_mgr = $db->query("SELECT date_added FROM `{$prefix}messages` WHERE `chat_id` = " . (int)$chat_id . " AND `author` IN (2, 3) ORDER BY `date_added` DESC LIMIT 1");
                if ($query_mgr->num_rows > 0) {
                    if ((new AppClock($this->registry))->secondsSinceSql((string)$query_mgr->row['date_added']) <= 86400) {
                        $hide_welcome_card = true;
                    }
                }
            }
        }

        $operators = [];
        $widget_show_operators = $model_setting->get('chat_widget_show_operators', '1') === '1' ? '1' : '0';
        if ($widget_show_operators === '1') {
            $model_manager = new Manager($this->registry);
            $managers = $model_manager->getActiveManagersForWidget();
            $presence = new PresenceService($this->registry);

            foreach ($managers as $manager) {
                $is_online = false;
                if (!empty($manager['last_active'])) {
                    if ($presence->isLastActiveOnline($manager['last_active'])) {
                        $is_online = true;
                    }
                }

                 
                $parts = preg_split('/\s+/', trim($manager['name']));
                $first_name = $parts[0] ?? $manager['name'];

                 
                if (count($parts) >= 2) {
                    $initials = mb_strtoupper(mb_substr($parts[0], 0, 1, 'UTF-8') . mb_substr($parts[1], 0, 1, 'UTF-8'), 'UTF-8');
                } else {
                    $initials = mb_strtoupper(mb_substr($first_name, 0, 1, 'UTF-8'), 'UTF-8');
                }

                 
                $avatar_bg = $this->getAvatarColor($manager['name']);


                $avatar_url = '';
                if (!empty($manager['image']) && is_file(DIR_AVATARS . $manager['image'])) {
                    $avatar_url = HTTP_AVATARS . $manager['image'];
                }

                $operators[] = [
                    'manager_id' => (int)$manager['id'],
                    'name'       => $manager['name'],
                    'first_name' => $first_name,
                    'initials'   => $initials,
                    'avatar_bg'  => $avatar_bg,
                    'avatar_url' => $avatar_url,
                    'is_online'  => $is_online
                ];
            }
        }

        $this->jsonOut([
            'success'           => true,
            'session_id'        => $session_id,
            'chat_id'           => $chat_id,
            'customer_name'     => $chat_session['customer_name'],
            'email'             => '',
            'typing'            => $typing,
            'typing_name'       => $typing_name,
            'typing_image'      => $typing_image,
            'typing_message_id' => $typing_message_id,
            'messages'          => $messages,
            'next_message_id'   => $next_message_id,
            'hide_welcome_card' => $hide_welcome_card,
            'operators'         => $operators,
            'ai_stream_available' => $ai_stream_available,
            'ai_stream_message_id' => $ai_stream_available ? $ai_stream_message_id : 0,
            'blocked'           => $is_blocked
            ,'cms_identity'      => $this->publicCmsIdentity($identity_state)
        ]);
        $this->runDeferredAiReplyAfterResponse($chat_id);
    }

     
    public function send() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            http_response_code(403);
            $this->jsonOut(['success' => false, 'error' => 'unauthorized_widget_domain']);
            return;
        }

        $json = $this->readJsonInput();
        $session_id = trim($json['session_id'] ?? '');
        $text       = trim($json['text']       ?? '');
        $name       = trim($json['name']       ?? '');
        $email      = trim($json['email']      ?? '');
        $stream_capable = !empty($json['stream_capable']);

         
        if (mb_strlen($text, 'UTF-8') > 2000) {
            $text = mb_substr($text, 0, 2000, 'UTF-8');
        }

        $request_files = \EasyChat\Library\RequestContext::files();
        $normalized_files = AttachmentBatchUploadService::normalizeFiles([
            $request_files['files'] ?? [],
            $request_files['file'] ?? [],
        ]);
        if (!$session_id || (!$text && !$normalized_files)) {
            $this->jsonOut(['success' => false, 'error' => 'Invalid parameters']);
            return;
        }

        $model_session = new Session($this->registry);
        $model_message = new Message($this->registry);
        $widget_locale = $this->resolveWidgetRequestLocale($model_setting, $json);
        $page_context = $this->sanitizePageContext(
            $this->pageContextFromRequest($json),
            $this->cmsPageContextEnabled($model_setting),
            $widget_locale
        );

         
        $blacklist = new \EasyChat\Library\BlacklistService($this->registry);
        $ip = \EasyChat\Library\RequestContext::server()['REMOTE_ADDR'] ?? '';
        $chat_pre = $model_session->getSessionBySid($session_id);
        $chat_id_pre = $chat_pre ? (int)$chat_pre['chat_id'] : 0;

        if ($chat_pre) {
            $identity_state = $this->authorizeCmsVisitor($json, $session_id, $chat_pre);
            if (empty($identity_state['allowed'])) {
                http_response_code(401);
                $this->jsonOut(['success' => false, 'error' => $identity_state['error'], 'reset_session' => !empty($identity_state['reset_session'])]);
                return;
            }
            if (!empty($identity_state['chat'])) $chat_pre = $identity_state['chat'];
        } else {
            $identity_state = $this->authorizeCmsVisitor($json, $session_id, null);
            if (empty($identity_state['allowed'])) {
                http_response_code(401);
                $this->jsonOut(['success' => false, 'error' => $identity_state['error']]);
                return;
            }
        }

        if ($blacklist->checkFlood($ip, $chat_id_pre)) {
            $this->jsonOut([
                'success' => false,
                'blocked' => true,
                'error' => I18n::translate('widget.blocked_placeholder', [], $widget_locale)
            ]);
            return;
        }

         
        $chat = $chat_pre;
        if (!$chat) {
            $customer_name = $name ? $name : $this->generateVisitorName($session_id, $widget_locale);
            $chat_id = $model_session->createSession($session_id, $customer_name);

            $chat = [
                'chat_id'       => $chat_id,
                'session_id'    => $session_id,
                'customer_name' => $customer_name,
            ];

            $identity_state = $this->authorizeCmsVisitor($json, $session_id, $model_session->getSessionById($chat_id));
            if (empty($identity_state['allowed'])) {
                http_response_code(401);
                $this->jsonOut(['success' => false, 'error' => $identity_state['error'], 'reset_session' => !empty($identity_state['reset_session'])]);
                return;
            }
            $chat = $model_session->getSessionById($chat_id) ?: $chat;
        }

        \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;

        $chat_id = (int)$chat['chat_id'];

         
        $update_data = [];
        if ($name && $name !== $chat['customer_name']) {
            $update_data['customer_name'] = $name;
            $chat['customer_name'] = $name;
        }
        if ($update_data) {
            $model_session->updateSession($chat_id, $update_data);
        }

        if ($email !== '') {
            (new CustomerContactService($this->registry))->recordDetectedContactsForChat($chat_id, $email, '', 'widget');
        }

         
        $image = '';
        $uploaded_attachments = [];
        $storage = new AttachmentStorage($this->registry);
        if ($normalized_files) {
            $max_mb = (int)$model_setting->get('chat_max_file_size', '5');
            $batch = (new AttachmentBatchUploadService($storage))->save(
                $chat_id,
                [$request_files['files'] ?? [], $request_files['file'] ?? []],
                $max_mb,
                'c',
                'widget'
            );
            if (!empty($batch['success'])) {
                $uploaded_attachments = (array)$batch['uploads'];
                foreach ($uploaded_attachments as $upload) {
                    if (($upload['kind'] ?? 'image') === 'image') {
                        $image = (string)$upload['filename'];
                        break;
                    }
                }
            } else {
                $this->jsonOut(['success' => false, 'error' => $batch['error'] ?? 'file_denied', 'limit' => (int)($batch['limit'] ?? 0)]);
                return;
            }
        }

         
         
         
        if (!$text && !$uploaded_attachments) {
            $this->jsonOut(['success' => false, 'error' => 'Empty message']);
            return;
        }

         
        $encoded_text = urlencode($text);
        $ip = \EasyChat\Library\RequestContext::server()['REMOTE_ADDR'] ?? '';

        $message_id = $model_message->addMessage([
            'chat_id'      => $chat_id,
            'text'         => $encoded_text,
            'image'        => $image,
            'author'       => 1,
            'manager_id'   => 0,
            'status_user'  => 1,
            'status_admin' => 0,
            'ip'           => $ip
        ]);
        $saved_message = $model_message->getMessageById((int)$message_id);

        if ($page_context) {
            try {
                (new MessageContext($this->registry))->upsertSitePageContext(array_merge($page_context, [
                    'chat_id' => $chat_id,
                    'message_id' => (int)$message_id,
                ]));
            } catch (\Throwable $e) {
                error_log('[EasyChat] widget page context save failed: ' . $e->getMessage());
            }
        }

        $attachment_rows = [];
        foreach ($uploaded_attachments as $uploaded_attachment) {
            $attachment_rows[] = [
                'upload' => $uploaded_attachment,
                'id' => $storage->attachToMessage((int)$message_id, $chat_id, $uploaded_attachment, 'widget'),
            ];
        }

         
        $model_session->updateSession($chat_id, ['is_archived' => 0]);
        $model_session->updateModifiedDate($chat_id);

         
         
         
        $official_support = new OfficialProductSupportProfile($this->registry);
        $official_support_profile = $official_support->resolve($chat_id, (int)$message_id);
        $tenant_ai_enabled = $model_setting->get('chat_ai_status', '0') === '1';
        $ai_status = ($tenant_ai_enabled || $official_support_profile !== null) ? '1' : '0';
        $ai_stream_available = false;
        if ($ai_status === '1' && ($official_support_profile === null || $official_support->hasServiceCredential())) {
             
             
             
            $delay_seconds = $official_support_profile !== null
                ? 0
                : (new AiResponseTimingPolicy($this->registry))->initialQueueDelaySeconds();

             
             
             
            $official_queue_claim = $official_support_profile !== null
                ? $official_support->createQueueClaim($chat_id, (int)$message_id)
                : '';
            $this->enqueueAiTick($chat_id, (int)$message_id, $delay_seconds, $official_queue_claim);

            if ($delay_seconds === 0) {
                 
                 
                 
                if ($official_support_profile !== null) {
                    $model_session->updateSession($chat_id, [
                        'ai_typing_expire' => time() + 25
                    ]);
                    $this->deferred_ai_message_id = (int)$message_id;
                    $ai_stream_available = false;
                } else {
                    $model_session->updateSession($chat_id, [
                        'ai_typing_expire' => time() + 25
                    ]);
                    if (
                        !$uploaded_attachments
                        && $stream_capable
                        && (
                            $model_setting->get('chat_ai_live_typing', '1') === '1'
                            && trim((string)$model_setting->get('chat_ai_key', '')) !== ''
                        )
                    ) {
                        $ai_stream_available = true;
                    } else {
                        $this->deferred_ai_message_id = (int)$message_id;
                    }
                }
            }
        }

         
         
        $push_notifier = new AndroidPushNotifier($this->registry);
        $push_notifier->notifyNewMessage($chat_id, 'widget', (string)$chat['customer_name']);

        $image_urls = ['url' => '', 'full_url' => '', 'preview_url' => '', 'thumb_url' => ''];
        if ($image) {
            $image_upload = null;
            foreach ($uploaded_attachments as $candidate) {
                if ((string)($candidate['filename'] ?? '') === $image) { $image_upload = $candidate; break; }
            }
            $image_urls = $storage->imageUrlSet($chat_id, $image, 'widget/image', ['variants' => $image_upload['variants'] ?? []], true);
            $image_session_id = (string)($chat['session_id'] ?? $session_id);
            $image_urls = $this->protectWidgetImageUrls($chat_id, $image_urls, $image_session_id);
        }
        $attachment_payloads = [];
        foreach ($attachment_rows as $row) {
            $uploaded_attachment = $row['upload'];
            $attachment_payload = $storage->attachmentPayload($chat_id, [
                'chat_id' => $chat_id,
                'id' => (int)$row['id'],
                'message_id' => (int)$message_id,
                'file_name' => (string)$uploaded_attachment['filename'],
                'mime_type' => (string)($uploaded_attachment['mime'] ?? 'application/octet-stream'),
                'file_size' => (int)($uploaded_attachment['size'] ?? 0),
                'meta_json' => json_encode([
                    'kind' => (string)($uploaded_attachment['kind'] ?? 'image'),
                    'display_name' => (string)($uploaded_attachment['display_name'] ?? $uploaded_attachment['original_name'] ?? $uploaded_attachment['filename']),
                    'variants' => $uploaded_attachment['variants'] ?? [],
                ], JSON_UNESCAPED_UNICODE),
            ], 'widget/image', true);
            $image_session_id = (string)($chat['session_id'] ?? $session_id);
            $attachment_payloads[] = $this->protectWidgetAttachmentUrls($chat_id, $attachment_payload, $image_session_id);
        }

        $this->jsonOut(array_merge([
            'success'    => true,
            'id'         => (int)$message_id,
            'chat_id'    => $chat_id,
            'text'       => $text,
            'image'      => $image_urls['url'],
            'date_added' => (new AppClock($this->registry))->sqlToIsoUtc((string)($saved_message['date_added'] ?? '')),
            'ai_stream_available' => $ai_stream_available,
            'ai_stream_message_id' => $ai_stream_available ? (int)$message_id : 0,
            'attachment' => $attachment_payloads[0] ?? null,
            'attachments' => $attachment_payloads,
            'cms_identity' => $this->publicCmsIdentity($identity_state),
        ], [
            'full_url' => $image_urls['full_url'],
            'preview_url' => $image_urls['preview_url'],
            'thumb_url' => $image_urls['thumb_url'],
        ]));
        $this->runDeferredAiReplyAfterResponse($chat_id);
    }

    private function authorizeCmsVisitor(array $json, string $session_id, $chat): array {
        $assertion = trim((string)($json['cms_identity_assertion'] ?? ''));
        $bound_identity_id = is_array($chat) ? (int)($chat['cms_identity_id'] ?? 0) : 0;
        if ($assertion !== '' || $bound_identity_id > 0) {
            header('Cache-Control: no-store, private');
            header('Vary: Origin', false);
        }
        if ($assertion === '') {
            return $bound_identity_id > 0
                ? ['allowed' => false, 'error' => 'cms_identity_required']
                : ['allowed' => true, 'authenticated' => false];
        }
        $chat_id = is_array($chat) ? (int)($chat['chat_id'] ?? 0) : 0;
        $result = (new CmsVisitorIdentityService($this->registry))->bindAssertionToChat($assertion, $session_id, $chat_id);
        if (empty($result['authenticated'])) {
            $changed = ($result['error'] ?? '') === 'identity_changed';
            return ['allowed' => false, 'error' => $changed ? 'cms_identity_changed' : 'cms_identity_invalid', 'reset_session' => $changed];
        }
        return [
            'allowed' => true,
            'authenticated' => true,
            'identity' => (array)($result['identity'] ?? []),
            'chat' => $chat_id > 0 ? (new Session($this->registry))->getSessionById($chat_id) : $chat,
        ];
    }

    private function publicCmsIdentity(array $state): array {
        $identity = (array)($state['identity'] ?? []);
        return [
            'authenticated' => !empty($state['authenticated']),
            'provider' => !empty($state['authenticated']) ? (string)($identity['provider'] ?? '') : '',
            'name' => !empty($state['authenticated']) ? (string)($identity['name'] ?? '') : '',
        ];
    }

    public function saveAttachmentPreview() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            http_response_code(403);
            $this->jsonOut(['success' => false, 'error' => 'unauthorized_widget_domain']);
            return;
        }

        $post = \EasyChat\Library\RequestContext::post();
        $chat_id = (int)($post['chat_id'] ?? 0);
        $attachment_id = (int)($post['attachment_id'] ?? 0);
        $image_token = trim((string)($post['image_token'] ?? ''));
        $files = \EasyChat\Library\RequestContext::files();
        if ($chat_id <= 0 || $attachment_id <= 0 || $image_token === '' || empty($files['thumbnail'])) {
            $this->jsonOut(['success' => false, 'error' => 'preview_required']);
            return;
        }

        $storage = new AttachmentStorage($this->registry);
        $attachment = $storage->getAttachmentById($attachment_id);
        if (!$attachment || (int)($attachment['chat_id'] ?? 0) !== $chat_id) {
            $this->jsonOut(['success' => false, 'error' => 'attachment_not_found']);
            return;
        }

        $chat = (new Session($this->registry))->getSessionById($chat_id);
        $session_id = (string)($chat['session_id'] ?? '');
        $file = basename((string)($attachment['file_name'] ?? ''));
        if ($session_id === '' || $file === '' || !hash_equals($this->widgetImageToken($chat_id, $file, $session_id), $image_token)) {
            $this->jsonOut(['success' => false, 'error' => 'forbidden']);
            return;
        }

        $result = $storage->savePdfPreviewUpload($attachment_id, $chat_id, $files['thumbnail'], 'widget/image');
        if (!empty($result['attachment'])) {
            $result['attachment'] = $this->protectWidgetAttachmentUrls($chat_id, $result['attachment'], $session_id);
        }
        $this->jsonOut($result);
    }

    




    public function acknowledgeRead() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            http_response_code(403);
            $this->jsonOut(['success' => false, 'error' => 'unauthorized_widget_domain']);
            return;
        }

        $json = $this->readJsonInput();
        $session_id = trim((string)($json['session_id'] ?? ''));
        $message_id = max(0, (int)($json['through_message_id'] ?? 0));
        if ($session_id === '' || $message_id <= 0) {
            http_response_code(422);
            $this->jsonOut(['success' => false, 'error' => 'invalid_parameters']);
            return;
        }

        $chat = (new Session($this->registry))->getSessionBySid($session_id);
        if (!$chat) {
            http_response_code(404);
            $this->jsonOut(['success' => false, 'error' => 'session_not_found']);
            return;
        }
        \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;

        $server = \EasyChat\Library\RequestContext::server();
        $reader_fingerprint = hash(
            'sha256',
            (string)($server['HTTP_USER_AGENT'] ?? '') . '|' . (string)($server['HTTP_ACCEPT_LANGUAGE'] ?? '')
        );
        $result = (new MessageReadReceiptService($this->registry))->acknowledgeWidget(
            (int)$chat['chat_id'],
            $message_id,
            $session_id,
            $reader_fingerprint
        );
        if (empty($result['success'])) {
            http_response_code(($result['error'] ?? '') === 'message_not_acknowledgeable' ? 409 : 422);
        }
        $this->jsonOut($result);
    }

    public function aiStream() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting)) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting)) {
            http_response_code(403);
            $this->sendSseHeaders();
            $this->emitSse('error', ['message' => 'unauthorized_widget_domain']);
            return;
        }

        $json = $this->readJsonInput();
        $session_id = trim((string)($json['session_id'] ?? ''));
        $message_id = (int)($json['message_id'] ?? 0);

        if ($session_id !== '' && session_status() === PHP_SESSION_ACTIVE) {
            \EasyChat\Library\RequestContext::session()['chat_session_id'] = $session_id;
             
             
            session_write_close();
        }

        $this->sendSseHeaders();

        if ($session_id === '' || $message_id <= 0) {
            $this->emitSse('error', ['message' => 'invalid_parameters']);
            return;
        }

        $model_session = new Session($this->registry);
        $chat = $model_session->getSessionBySid($session_id);
        if (!$chat) {
            $this->emitSse('error', ['message' => 'session_not_found']);
            return;
        }

        $ai = new AiResponder($this->registry);
        $result = $ai->streamForWidget((int)$chat['chat_id'], $message_id, function ($event, array $payload = []) {
            $this->emitSse($event, $payload);
        });

        if (!empty($result['success'])) {
            $message_text_renderer = new MessageTextRenderer();
            $this->emitSse('done', [
                'message_id' => (int)$result['message_id'],
                'date_added' => $result['date_added'] ?? (new AppClock($this->registry))->nowIsoUtc(),
                'text'       => (string)($result['text'] ?? ''),
                'text_html'  => $message_text_renderer->toHtml((string)($result['text'] ?? '')),
                'support'    => [
                    'sources' => (array)($result['support_sources'] ?? []),
                    'handoff' => (array)($result['support_handoff'] ?? ['show' => false]),
                ],
            ]);
            return;
        }

        $this->emitSse('error', [
            'message' => $result['error'] ?? 'ai_stream_failed',
        ]);
    }

     
     
     

    private function sendSseHeaders() {
        SafeRuntime::call(function () {
            set_time_limit(130);
            return true;
        }, false);
         
         
         
         
        SafeRuntime::call(function () {
            ignore_user_abort(true);
            return true;
        }, false);
        SafeRuntime::call(function () {
            ini_set('zlib.output_compression', '0');
            return true;
        }, false);
        header('Content-Type: text/event-stream; charset=utf-8');
        header('Cache-Control: no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('X-Accel-Buffering: no');
    }

    private function emitSse($event, array $payload = []) {
        if (connection_aborted()) {
            return;
        }
        echo 'event: ' . preg_replace('/[^a-zA-Z0-9_\-]/', '', (string)$event) . "\n";
        echo 'data: ' . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n\n";
        SafeRuntime::call(function () {
            ob_flush();
            return true;
        }, false);
        flush();
    }

    private function resolveDelayedAiStreamDecision($chat_id, $badge_only, $stream_capable, $stream_suppressed_message_id, Setting $model_setting, Session $model_session, Message $model_message) {
        $chat_id = (int)$chat_id;
        $stream_suppressed_message_id = (int)$stream_suppressed_message_id;
        $decision = ['available' => false, 'message_id' => 0];

        if ($chat_id <= 0) {
            return $decision;
        }

        $last_msg = $model_message->getLastMessage($chat_id);
        if (!$last_msg || (int)$last_msg['author'] !== 1 || !empty($last_msg['image'])) {
            return $decision;
        }

        $official_support = new OfficialProductSupportProfile($this->registry);
        $official_support_profile = $official_support->resolve($chat_id, (int)$last_msg['message_id']);
        if ($official_support_profile === null && $model_setting->get('chat_ai_status', '0') !== '1') {
            return $decision;
        }
        if ($official_support_profile !== null && !$official_support->hasServiceCredential()) {
            return $decision;
        }

        if ($official_support_profile === null) {
            $timing = (new AiResponseTimingPolicy($this->registry))->evaluateForMessageDate((string)$last_msg['date_added']);
            if (empty($timing['ready'])) {
                return $decision;
            }
        }

        $chat_info = $model_session->getSessionById($chat_id);
        if (!$chat_info) {
            return $decision;
        }

        $now = time();
        $last_message_id = (int)($last_msg['message_id'] ?? 0);
        $already_attempted = !empty($chat_info['ai_last_attempt_message_id'])
            && (int)$chat_info['ai_last_attempt_message_id'] === $last_message_id
            && in_array((string)($chat_info['ai_last_attempt_status'] ?? ''), ['success', 'empty', 'error'], true);
        if ($already_attempted) {
            return $decision;
        }

        $processing_message_id = (int)($chat_info['ai_processing_message_id'] ?? 0);
        $processing_until = (int)($chat_info['ai_processing_until'] ?? 0);
        if ($processing_message_id > 0 && $processing_until > $now) {
            return $decision;
        }

        $live_typing_enabled = $official_support_profile !== null
            || $model_setting->get('chat_ai_live_typing', '1') === '1';
        $has_api_key = $official_support_profile !== null
            ? $official_support->hasServiceCredential()
            : trim((string)$model_setting->get('chat_ai_key', '')) !== '';
        $previous_stream_failed = !empty($chat_info['ai_last_attempt_message_id'])
            && (int)$chat_info['ai_last_attempt_message_id'] === $last_message_id
            && in_array((string)($chat_info['ai_last_attempt_status'] ?? ''), ['stream_aborted', 'stream_error'], true);
        $can_stream = !$badge_only
            && $stream_capable
            && $live_typing_enabled
            && $has_api_key
             
             
             
            && $official_support_profile === null
            && !$previous_stream_failed
            && $stream_suppressed_message_id !== $last_message_id;

        if ($can_stream) {
             
             
             
            return ['available' => true, 'message_id' => $last_message_id];
        }

        $typing_active = isset($chat_info['ai_typing_expire']) && (int)$chat_info['ai_typing_expire'] > $now;
        $stream_fallback_requested = $stream_suppressed_message_id === $last_message_id;
        if ($stream_fallback_requested || $previous_stream_failed || !$typing_active) {
            $model_session->updateSession($chat_id, [
                'ai_typing_expire' => $now + 25
            ]);
            $this->deferred_ai_message_id = $last_message_id;
        }

        return $decision;
    }

    private function getAvatarColor($name) {
        $colors = [
            '#3B82F6', '#EF4444', '#10B981', '#F59E0B',
            '#8B5CF6', '#EC4899', '#14B8A6', '#6366F1'
        ];
        $hash = 0;
        $len = mb_strlen($name, 'UTF-8');
        for ($i = 0; $i < $len; $i++) {
            $char = mb_substr($name, $i, 1, 'UTF-8');
            $code = mb_ord($char, 'UTF-8');
            $hash = ($hash * 31 + $code) & 0xFFFFFFFF;
        }
        if ($hash & 0x80000000) {
            $hash = $hash - 0x100000000;
        }
        $index = abs($hash) % count($colors);
        return $colors[$index];
    }


    private function generateVisitorName($session_id = '', $locale = 'ru') {
        $adjectives = $this->splitVisitorNamePart(
            I18n::translate('widget.visitor_name_adjectives', [], $locale)
        );
        $nouns = $this->splitVisitorNamePart(
            I18n::translate('widget.visitor_name_nouns', [], $locale)
        );
        if (!$adjectives || !$nouns) {
            $adjectives = $this->splitVisitorNamePart(I18n::translate('widget.visitor_name_adjectives', [], 'ru'));
            $nouns = $this->splitVisitorNamePart(I18n::translate('widget.visitor_name_nouns', [], 'ru'));
        }
        if ($session_id) {
            $hash = hexdec(substr(md5($session_id), 0, 8));
            $adj_idx = $hash % count($adjectives);
            $noun_idx = ($hash >> 2) % count($nouns);
            return $adjectives[$adj_idx] . ' ' . $nouns[$noun_idx];
        }

        return $adjectives[array_rand($adjectives)] . ' ' . $nouns[array_rand($nouns)];
    }

    private function splitVisitorNamePart($value) {
        $parts = array_map('trim', explode('|', (string)$value));
        return array_values(array_filter($parts, static function ($part) {
            return $part !== '';
        }));
    }

    private function readJsonInput(): array {
        $data = \EasyChat\Library\RequestContext::post();
        if (empty($data)) {
            $raw = file_get_contents('php://input');
            $data = json_decode($raw, true) ?? [];
        }
        return $data;
    }

    private function cmsPageContextEnabled(Setting $model_setting): bool {
        return $model_setting->get('chat_ai_status', '0') === '1'
            || $model_setting->get('chat_ai_suggestions_status', '0') === '1'
            || (new OfficialProductSupportProfile($this->registry))->isEnabledForCurrentInstallation();
    }

    private function pageContextFromRequest(array $json): array {
        $context = $json['page_context'] ?? null;
        if (is_string($context)) {
            $decoded = json_decode($context, true);
            $context = is_array($decoded) ? $decoded : [];
        }
        if (is_array($context) && $context) {
            return $context;
        }

        $post = \EasyChat\Library\RequestContext::post();
        if (isset($post['page_context'])) {
            $decoded = json_decode((string)$post['page_context'], true);
            return is_array($decoded) ? $decoded : [];
        }

        return [];
    }

    private function sanitizePageContext(array $context, bool $allow_cms_context, string $locale = 'ru'): array {
        $title = $this->limitPlainText((string)($context['title'] ?? ''), 255);
        $url = $this->sanitizePageUrl((string)($context['url'] ?? ''));
        $canonical_url = $this->sanitizePageUrl((string)($context['canonical_url'] ?? ''));

        if ($url === '') {
            return [];
        }

        $cms_provider = '';
        $page_type = '';
        $page_id = '';
        $schema_version = '';
        if ($allow_cms_context) {
            $cms_provider = $this->sanitizeIdentifier((string)($context['cms_provider'] ?? ''), 32);
            $page_type = $this->sanitizePageType((string)($context['page_type'] ?? ''));
            $page_id = $this->limitPlainText((string)($context['page_id'] ?? ''), 64);
            $page_id = preg_replace('/[^a-zA-Z0-9:_-]/', '', $page_id);
            $schema_version = $this->sanitizeIdentifier((string)($context['schema_version'] ?? ''), 16);
        }
        $captured_at = $this->limitPlainText((string)($context['captured_at'] ?? ''), 64);

        return [
            'locale' => $locale === 'en' ? 'en' : 'ru',
            'title' => $title,
            'url' => $url,
            'canonical_url' => $canonical_url,
            'cms_provider' => $cms_provider,
            'page_type' => $page_type,
            'page_id' => $page_id,
            'schema_version' => $schema_version,
            'captured_at' => $captured_at,
            'snapshot' => [
                'locale' => $locale === 'en' ? 'en' : 'ru',
                'page' => [
                    'title' => $title,
                    'url' => $url,
                    'canonical_url' => $canonical_url,
                    'type' => $page_type,
                    'id' => $page_id,
                ],
                'cms' => [
                    'provider' => $cms_provider,
                    'schema_version' => $schema_version,
                ],
                'captured_at' => $captured_at,
            ],
        ];
    }

    private function sanitizePageUrl(string $url): string {
        $url = trim($url);
        if ($url === '' || strlen($url) > 2048 || !preg_match('/^https?:\/\//i', $url)) {
            return '';
        }
        $parts = parse_url($url);
        if (!is_array($parts) || empty($parts['host']) || !empty($parts['user']) || !empty($parts['pass'])) {
            return '';
        }

        $scheme = strtolower((string)($parts['scheme'] ?? 'https'));
        if (!in_array($scheme, ['http', 'https'], true)) {
            return '';
        }
        $host = WidgetDomainPolicy::normalizeHost((string)$parts['host']);
        if (!$this->widgetDomainPolicy()->isHostAllowed($host)) {
            return '';
        }

        $query = [];
        if (!empty($parts['query'])) {
            parse_str((string)$parts['query'], $query);
            $deny = ['token', 'session', 'sid', 'sessid', 'auth', 'key', 'password', 'pass', 'email', 'phone', 'tel', 'jwt', 'hash', 'signature', 'code'];
            foreach (array_keys($query) as $key) {
                $normalized = strtolower((string)$key);
                foreach ($deny as $needle) {
                    if (strpos($normalized, $needle) !== false) {
                        unset($query[$key]);
                        break;
                    }
                }
            }
        }

        $clean = $scheme . '://' . $host;
        if (!empty($parts['port'])) {
            $clean .= ':' . (int)$parts['port'];
        }
        $clean .= (string)($parts['path'] ?? '/');
        if ($query) {
            $query_string = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
             
             
            $clean .= '?' . str_ireplace('%2F', '/', $query_string);
        }
        if (!empty($parts['fragment'])) {
            $fragment = preg_replace('/[^a-zA-Z0-9_:-]/', '', (string)$parts['fragment']);
            if ($fragment !== '') {
                $clean .= '#' . $fragment;
            }
        }

        return $this->limitPlainText($clean, 768);
    }

    private function widgetDomainPolicy(?Setting $model_setting = null): WidgetDomainPolicy {
        if ($this->widget_domain_policy === null) {
            $this->widget_domain_policy = WidgetDomainPolicy::fromSettings($model_setting ?: new Setting($this->registry));
        }

        return $this->widget_domain_policy;
    }

    private function sanitizePageType(string $value): string {
        $value = $this->sanitizeIdentifier($value, 32);
        return in_array($value, ['product', 'category', 'information', 'page'], true) ? $value : '';
    }

    private function sanitizeIdentifier(string $value, int $limit): string {
        $value = strtolower(preg_replace('/[^a-z0-9_-]/i', '', trim($value)));
        return $this->limitPlainText($value, $limit);
    }

    private function limitPlainText(string $value, int $limit): string {
        $value = trim(strip_tags(str_replace(["\r\n", "\r"], "\n", $value)));
        $value = preg_replace('/[ \t]+/u', ' ', $value);
        return mb_strlen($value, 'UTF-8') > $limit ? mb_substr($value, 0, $limit, 'UTF-8') : $value;
    }

    private function jsonOut(array $data) {
        $body = json_encode($data, JSON_UNESCAPED_UNICODE);
        if (!is_string($body)) {
            $body = '{}';
        }
         
         
        SafeRuntime::call(function () {
            ini_set('zlib.output_compression', '0');
            return true;
        }, false);
        header('Content-Type: application/json; charset=utf-8');
        header('Content-Length: ' . strlen($body));
        if ($this->deferred_ai_message_id > 0) {
            header('Connection: close');
        }
        echo $body;
    }
    public function image() {
        $model_setting = new Setting($this->registry);
        if ($this->handleWidgetCorsPreflight($model_setting, 'GET, HEAD, OPTIONS', 'Range')) {
            return;
        }
        if (!$this->sendWidgetCorsHeaders($model_setting, 'GET, HEAD, OPTIONS', 'Range')) {
            header("HTTP/1.1 403 Forbidden");
            exit;
        }
        header('Access-Control-Expose-Headers: Accept-Ranges, Content-Range, Content-Length, ETag, Last-Modified');
        if (\EasyChat\Library\RequestContext::method() === 'OPTIONS') {
            exit;
        }

        $chat_id = isset(\EasyChat\Library\RequestContext::get()['chat_id']) ? (int)\EasyChat\Library\RequestContext::get()['chat_id'] : 0;
        $file = isset(\EasyChat\Library\RequestContext::get()['file']) ? basename(trim(\EasyChat\Library\RequestContext::get()['file'])) : '';
        $download_name = isset(\EasyChat\Library\RequestContext::get()['name']) ? basename(trim((string)\EasyChat\Library\RequestContext::get()['name'])) : '';
        $image_token = isset(\EasyChat\Library\RequestContext::get()['image_token']) ? trim((string)\EasyChat\Library\RequestContext::get()['image_token']) : '';

        if (!$chat_id || !$file) {
            header("HTTP/1.1 400 Bad Request");
            exit;
        }

        $model_session = new Session($this->registry);
        $chat = $model_session->getSessionById($chat_id);
        if (!$chat) {
            header("HTTP/1.1 403 Forbidden");
            exit;
        }

        $session_id = (string)($chat['session_id'] ?? '');
        $session_ok = isset(\EasyChat\Library\RequestContext::session()['chat_session_id']) && hash_equals($session_id, (string)\EasyChat\Library\RequestContext::session()['chat_session_id']);
        $token_ok = $image_token !== '' && hash_equals($this->widgetImageToken($chat_id, $file, $session_id), $image_token);
        if (!$session_ok && !$token_ok) {
            header("HTTP/1.1 403 Forbidden");
            exit;
        }
        $this->releaseSessionLockForReadOnlyBinaryResponse();

        $file_path = DIR_IMAGE . 'chat_id_' . $chat_id . DS . $file;
        if (is_file($file_path)) {
            $response = $this->localAttachmentResponse($file_path, $file, $download_name);
            $this->sendLocalBinaryFile($file_path, (string)$response['mime'], (string)$response['download_name'], !empty($response['force_attachment']), [
                'Access-Control-Expose-Headers: Accept-Ranges, Content-Range, Content-Length, ETag, Last-Modified',
            ]);
        } else {
            header("HTTP/1.1 404 Not Found");
            exit;
        }
    }

    public function outboundImage() {
        $chat_id = isset(\EasyChat\Library\RequestContext::get()['chat_id']) ? (int)\EasyChat\Library\RequestContext::get()['chat_id'] : 0;
        $file = isset(\EasyChat\Library\RequestContext::get()['file']) ? basename(trim((string)\EasyChat\Library\RequestContext::get()['file'])) : '';
        $expires = isset(\EasyChat\Library\RequestContext::get()['expires']) ? (int)\EasyChat\Library\RequestContext::get()['expires'] : 0;
        $signature = isset(\EasyChat\Library\RequestContext::get()['sig']) ? trim((string)\EasyChat\Library\RequestContext::get()['sig']) : '';

        $model_setting = new Setting($this->registry);
        $secret = (string)$model_setting->get('chat_install_secret', '');
        if (!OutboundAttachmentLink::verify($chat_id, $file, $expires, $signature, $secret)) {
            header('HTTP/1.1 403 Forbidden');
            exit;
        }

        $file_path = DIR_IMAGE . 'chat_id_' . $chat_id . DS . $file;
        if (!OutboundAttachmentLink::isAllowedImagePath($file_path)) {
            header('HTTP/1.1 404 Not Found');
            exit;
        }

        $mime = function_exists('mime_content_type') ? (string)mime_content_type($file_path) : 'image/jpeg';
        header('X-Content-Type-Options: nosniff');
        header('Cache-Control: private, max-age=300');
        header('Content-Type: ' . $mime);
        header('Content-Length: ' . filesize($file_path));
        readfile($file_path);
        exit;
    }

    public function outboundFile() {
        $request = \EasyChat\Library\RequestContext::get();
        $chat_id = isset($request['chat_id']) ? (int)$request['chat_id'] : 0;
        $file = isset($request['file']) ? OutboundAttachmentLink::safeDownloadName((string)$request['file']) : '';
        $display_name = isset($request['name']) ? OutboundAttachmentLink::safeDownloadName((string)$request['name']) : $file;
        $expires = isset($request['expires']) ? (int)$request['expires'] : 0;
        $signature = isset($request['sig']) ? trim((string)$request['sig']) : '';

        $model_setting = new Setting($this->registry);
        $secret = (string)$model_setting->get('chat_install_secret', '');
        if (!OutboundAttachmentLink::verifyDocument($chat_id, $file, $display_name, $expires, $signature, $secret)) {
            header('HTTP/1.1 403 Forbidden');
            exit;
        }

        $file_path = DIR_IMAGE . 'chat_id_' . $chat_id . DS . $file;
        if (!OutboundAttachmentLink::isAllowedDocumentPath($file_path)) {
            header('HTTP/1.1 404 Not Found');
            exit;
        }

        $response = $this->localAttachmentResponse($file_path, $file, $display_name);
        $this->sendLocalBinaryFile(
            $file_path,
            (string)$response['mime'],
            (string)$response['download_name'],
            true
        );
    }

    public function avatar() {
        $file = isset(\EasyChat\Library\RequestContext::get()['file']) ? basename(trim(\EasyChat\Library\RequestContext::get()['file'])) : '';
        if (!$file) {
            header("HTTP/1.1 400 Bad Request");
            exit;
        }

        $file_path = DIR_AVATARS . $file;
        if (is_file($file_path)) {
            $mime = 'image/png';
            if (function_exists('mime_content_type')) {
                $mime = mime_content_type($file_path);
            } else {
                $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
                $mimes = [
                    'jpg'  => 'image/jpeg',
                    'jpeg' => 'image/jpeg',
                    'png'  => 'image/png',
                    'gif'  => 'image/gif',
                    'webp' => 'image/webp'
                ];
                if (isset($mimes[$ext])) {
                    $mime = $mimes[$ext];
                }
            }
            header("Content-Type: " . $mime);
            header("Content-Length: " . filesize($file_path));
            readfile($file_path);
            exit;
        } else {
            header("HTTP/1.1 404 Not Found");
            exit;
        }
    }

    private function protectWidgetImageUrls(int $chat_id, array $urls, string $session_id): array {
        foreach (['url', 'full_url', 'preview_url', 'thumb_url'] as $key) {
            if (!empty($urls[$key])) {
                $urls[$key] = $this->appendWidgetImageToken((string)$urls[$key], $chat_id, $session_id);
            }
        }
        return $urls;
    }

    private function protectWidgetAttachmentUrls(int $chat_id, array $attachment, string $session_id): array {
        foreach (['url', 'full_url', 'preview_url', 'thumb_url', 'download_url'] as $key) {
            if (!empty($attachment[$key])) {
                $attachment[$key] = $this->appendWidgetImageToken((string)$attachment[$key], $chat_id, $session_id);
            }
        }
        return $attachment;
    }

    private function appendWidgetImageToken(string $url, int $chat_id, string $session_id): string {
        if ($url === '' || $session_id === '') {
            return $url;
        }

        $parts = parse_url($url);
        if (empty($parts['query'])) {
            return $url;
        }

        parse_str($parts['query'], $query);
        $file = isset($query['file']) ? basename((string)$query['file']) : '';
        if ($file === '') {
            return $url;
        }

        $separator = (strpos($url, '?') === false) ? '?' : '&';
        return $url . $separator . 'image_token=' . rawurlencode($this->widgetImageToken($chat_id, $file, $session_id));
    }

    private function widgetImageToken(int $chat_id, string $file, string $session_id): string {
        return hash_hmac('sha256', (int)$chat_id . '|' . basename($file), $session_id);
    }

    private function runDeferredAiReplyAfterResponse(int $chat_id): void {
        $message_id = (int)$this->deferred_ai_message_id;
        $this->deferred_ai_message_id = 0;
        if ($chat_id <= 0 || $message_id <= 0) {
            return;
        }

         
         
         
        ignore_user_abort(true);
        set_time_limit(130);
        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        } else {
            while (ob_get_level() > 0) {
                if (!ob_end_flush()) {
                    break;
                }
            }
            flush();
        }

        $ai = new AiResponder($this->registry);
        $ai->answerCustomerMessage($chat_id, $message_id, true);
    }

    private function enqueueAiTick(int $chat_id, int $message_id, int $delay_seconds, string $official_queue_claim = ''): void {
        if ($chat_id <= 0 || $message_id <= 0 || $delay_seconds < 0) {
            return;
        }

        try {
            $queue = new QueueManager($this->registry);
             
             
             
             
            $payload = [
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'source' => 'widget',
            ];
            if ($official_queue_claim !== '') {
                $payload['official_support_claim'] = $official_queue_claim;
            }
            $queue->pushUnique('messenger_ai_tick', $payload, 'widget_ai_tick_' . $chat_id . '_' . $message_id, $delay_seconds, 5);
        } catch (\Throwable $e) {
            error_log('[EasyChat] widget AI queue failed: ' . $e->getMessage());
        }
    }

}
