<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\AttachmentStorage;
use EasyChat\Library\AndroidPushNotifier;
use EasyChat\Library\AppClock;
use EasyChat\Library\Ai\AiReplyEligibilityGuard;
use EasyChat\Library\Ai\AiResponseTimingPolicy;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Library\Voice\VoiceMediaService;
use EasyChat\Library\Messenger\Marketplace\MarketplaceCredentialsVault;
use EasyChat\Library\Messenger\Marketplace\MarketplaceStream;
use EasyChat\Model\Message;
use EasyChat\Model\MessageContext;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\Session;
use EasyChat\Model\Setting;







class InboundService {

    private const REMOTE_DOCUMENT_MAX_MB = 32;

    private $registry;
    private $logger;

    public function __construct($registry) {
        $this->registry = $registry;
        $this->logger = new MessengerLogger($registry);
    }

    public function acceptVkWebhook(array $channel, array $payload, string $raw_body): array {
        $provider = ProviderRegistry::get('vk', $this->registry);
        if (!$provider) {
            return ['success' => false, 'error' => 'provider_unknown', 'http_code' => 404];
        }
        $event = $provider->parseWebhook($payload);
        if (($event['update_type'] ?? '') === 'confirmation') {
            return $this->handleWebhook('vk', $channel, $payload, $raw_body);
        }

        $credentials = json_decode((string)($channel['credentials_json'] ?? ''), true);
        $credentials = is_array($credentials) ? $credentials : [];
        $expected_secret = $this->decryptSecret((string)($credentials['secret_key'] ?? ''));
        if ($expected_secret === '' || !hash_equals($expected_secret, (string)($payload['secret'] ?? ''))) {
            $this->logger->warning('inbound_bad_secret', ['provider' => 'vk', 'channel_id' => (int)($channel['channel_id'] ?? 0)]);
            return ['success' => true, 'ignored' => $expected_secret === '' ? 'secret_missing' : 'bad_secret'];
        }
        if ((string)($channel['status'] ?? '') !== 'active') {
            return ['success' => true, 'ignored' => 'channel_inactive'];
        }

        unset($payload['secret']);
        $channel_id = (int)($channel['channel_id'] ?? 0);
        $dedupe_source = (string)($payload['event_id'] ?? '');
        if ($dedupe_source === '') {
            $dedupe_source = hash('sha256', json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        }
        $queue = new QueueManager($this->registry);
        $job_id = $queue->pushUnique('messenger_vk_inbound', [
            'channel_id' => $channel_id,
            'payload' => $payload,
        ], 'vk_inbound_' . $channel_id . '_' . substr(hash('sha256', $dedupe_source), 0, 32), 0, 20);

        return ['success' => true, 'queued' => true, 'job_id' => $job_id];
    }

    public function processAcceptedVkWebhook(array $job_payload): array {
        $channel_id = (int)($job_payload['channel_id'] ?? 0);
        $payload = is_array($job_payload['payload'] ?? null) ? $job_payload['payload'] : [];
        if ($channel_id <= 0 || !$payload) {
            return ['success' => false, 'error' => 'vk_inbound_payload_invalid'];
        }
        $model_messenger = new MessengerModel($this->registry);
        $channel = $model_messenger->getChannelById($channel_id);
        if (!$channel) {
            return ['success' => false, 'error' => 'channel_not_found'];
        }
        $credentials = json_decode((string)$channel['credentials_json'], true);
        $credentials = is_array($credentials) ? $credentials : [];
        $payload['secret'] = $this->decryptSecret((string)($credentials['secret_key'] ?? ''));
        return $this->handleWebhook(
            'vk',
            $channel,
            $payload,
            json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
        );
    }

    



    public function acceptMarketplaceEvent(array $channel, array $event): array {
        $provider = strtolower((string)($channel['provider'] ?? ''));
        if (!in_array($provider, [WildberriesProvider::PROVIDER, OzonProvider::PROVIDER, YandexMarketProvider::PROVIDER], true)) {
            return ['success' => false, 'error' => 'marketplace_provider_unknown'];
        }
        $raw = json_encode($event, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if (!is_string($raw) || strlen($raw) > 1024 * 1024) {
            return ['success' => false, 'error' => 'marketplace_event_invalid'];
        }
        return $this->handleWebhook($provider, $channel, ['_marketplace_event' => $event], $raw, '', false, true);
    }

    public function handleWebhook(
        string $provider_code,
        array $channel,
        array $payload,
        string $raw_body,
        string $secret_header = '',
        bool $defer_notifications = false,
        bool $trusted_marketplace_event = false
    ): array {
        $provider_code = strtolower($provider_code);
        $this->logger = new MessengerLogger($this->registry, $provider_code);

        $provider = ProviderRegistry::get($provider_code, $this->registry);
        if (!$provider) {
            return ['success' => false, 'error' => 'provider_unknown', 'http_code' => 404];
        }

        $credentials = json_decode((string)$channel['credentials_json'], true);
        if (!is_array($credentials)) {
            $credentials = [];
        }

        $event = $provider->parseWebhook($payload);
        if (($event['update_type'] ?? '') === 'confirmation') {
            $confirmation_key = $provider_code === 'vk' ? 'confirmation_code' : 'webhook_secret';
            $expected_confirmation = $this->decryptSecret((string)($credentials[$confirmation_key] ?? ''));
            if ($expected_confirmation === '' && $provider_code === 'vk') {
                $expected_confirmation = $this->decryptSecret((string)($channel['webhook_secret'] ?? ''));
            }
            if ($expected_confirmation === '') {
                $this->logger->warning('inbound_confirmation_missing', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                ]);
                return ['success' => false, 'error' => 'confirmation_missing', 'http_code' => 500];
            }
            $this->logger->info('inbound_confirmation_served', [
                'provider' => $provider_code,
                'channel_id' => (int)($channel['channel_id'] ?? 0),
            ]);
            return [
                'success' => true,
                'response_body' => $expected_confirmation,
            ];
        }

        if ($provider_code === 'vk') {
            $expected_secret = $this->decryptSecret((string)($credentials['secret_key'] ?? ''));
            if ($expected_secret === '') {
                $this->logger->warning('inbound_secret_missing', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                ]);
                return ['success' => true, 'ignored' => 'secret_missing'];
            }
            if (!hash_equals($expected_secret, (string)($payload['secret'] ?? ''))) {
                $this->logger->warning('inbound_bad_secret', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                ]);
                return ['success' => true, 'ignored' => 'bad_secret'];
            }
        } else {
            $expected_secret = $this->decryptSecret((string)($credentials['webhook_secret'] ?? ''));
            if ($expected_secret === '' && $provider_code === 'telegram') {
                $this->logger->warning('inbound_secret_missing', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                ]);
                return ['success' => false, 'error' => 'secret_missing', 'http_code' => 403];
            }
            if ($expected_secret !== '' && !hash_equals($expected_secret, (string)$secret_header)) {
                $this->logger->warning('inbound_bad_secret', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                ]);
                return ['success' => false, 'error' => 'bad_secret', 'http_code' => 403];
            }
        }

        $channel_status = (string)($channel['status'] ?? '');
        $is_youla_system_event = $provider_code === 'youla' && !empty($event['system_event']);
         
         
         
        if ($channel_status !== 'active' && !($channel_status === 'error' && $is_youla_system_event)) {
            $this->logger->warning('inbound_ignored_channel_inactive', [
                'provider' => $provider_code,
                'channel_id' => (int)($channel['channel_id'] ?? 0),
            ]);
            return ['success' => true, 'ignored' => 'channel_inactive'];
        }
        $external_user_id = (string)($event['external_user_id'] ?? '');

        $bot_user_id = (string)($channel['bot_user_id'] ?? '');
        if ($provider_code === 'youla' && $bot_user_id === '') {
            $youla_account_user_id = $this->normalizeYoulaId((string)($event['recipient_user_id'] ?? ''));
            if ($youla_account_user_id !== '' && (string)($event['update_type'] ?? '') === 'message.incom') {
                $bot_user_id = $youla_account_user_id;
                $channel['bot_user_id'] = $bot_user_id;
                $this->rememberChannelBotUserId($channel, $bot_user_id);
            }
        }
        if (in_array($provider_code, ['avito', 'youla'], true) && $bot_user_id !== '') {
            $recipient_user_id = (string)($event['recipient_user_id'] ?? '');
            $sender_is_account = $external_user_id === $bot_user_id || $external_user_id === '-' . $bot_user_id;
            if ($recipient_user_id !== '' && !$sender_is_account && !hash_equals($bot_user_id, $recipient_user_id)) {
                $this->logger->warning('inbound_ignored_wrong_recipient', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                    'recipient_hash' => MessengerLogger::hashExternalId($recipient_user_id),
                ]);
                return ['success' => true, 'ignored' => 'wrong_recipient'];
            }
        }
        $is_external_out = (string)($event['direction'] ?? '') === 'out';
        $is_messenger_system_event = $this->isMessengerSystemEvent($provider_code, $event);
        $initial_event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $is_quiet_marketplace_import_hint = !empty($initial_event_metadata['marketplace'])
            && !empty($initial_event_metadata['quiet_import']);
        $is_quiet_history_reconciliation_hint =
            (string)($initial_event_metadata['sync_origin'] ?? '') === 'history_reconciliation'
            && (in_array($provider_code, ['max', 'whatsapp-personal'], true) || !empty($initial_event_metadata['marketplace']));
        if ($bot_user_id !== '' && ($external_user_id === $bot_user_id || $external_user_id === '-' . $bot_user_id)) {
            if (in_array($provider_code, ['avito', 'youla'], true) || $is_external_out) {
                 
                 
                 
                 
                 
                $is_external_out = true;
                $event['direction'] = 'out';
            } else {
                return ['success' => true, 'ignored' => 'self_message'];
            }
        }

        if ($external_user_id === '') {
            if ($provider_code === 'youla' && !empty($event['system_event'])) {
                $model_messenger = new MessengerModel($this->registry);
                $dedupe_key = $this->eventDedupeKey($event, $raw_body);
                $channel_id = (int)($channel['channel_id'] ?? 0);
                if ($channel_id > 0 && $dedupe_key !== '' && $model_messenger->findDeliveryByDedupe($channel_id, $dedupe_key)) {
                    $this->logger->info('inbound_duplicate', [
                        'provider' => $provider_code,
                        'channel_id' => $channel_id,
                        'event_type' => (string)($event['update_type'] ?? ''),
                    ]);
                    return ['success' => true, 'duplicate' => true];
                }
                $this->handleYoulaSystemEvent((int)($channel['channel_id'] ?? 0), $event);
                if ($channel_id > 0 && $dedupe_key !== '') {
                    $model_messenger->createDelivery([
                        'channel_id' => $channel_id,
                        'direction' => 'in',
                        'external_message_id' => (string)($event['external_message_id'] ?? ''),
                        'dedupe_key' => $dedupe_key,
                        'payload_hash' => hash('sha256', $raw_body),
                        'status' => 'ignored',
                    ]);
                }
                return ['success' => true, 'ignored' => 'system_event'];
            }
            $this->logger->warning('inbound_missing_user', [
                'provider' => $provider_code,
                'event_type' => (string)($event['update_type'] ?? ''),
            ]);
            return ['success' => true, 'ignored' => 'user_missing'];
        }

         
         
         
        if ($provider_code === 'max-bot') {
            $event = $this->enrichMaxBotEvent($provider, $credentials, $event, (int)($channel['channel_id'] ?? 0));
        }

        $dedupe_key = $this->eventDedupeKey($event, $raw_body);

        $model_messenger = new MessengerModel($this->registry);
        $channel_id = (int)$channel['channel_id'];
        $external_message_id = trim((string)($event['external_message_id'] ?? ''));
        if ($provider_code === WildberriesProvider::PROVIDER
            && $is_external_out
            && $trusted_marketplace_event) {
            $reconciled = $this->reconcileWildberriesOutboundEcho(
                $model_messenger,
                $channel_id,
                $event,
                $dedupe_key
            );
            if ($reconciled) {
                return $reconciled;
            }
        }
        if ($external_message_id !== '') {
            $provider_delivery = $model_messenger->findDeliveryByExternalMessageId($channel_id, $external_message_id);
            if ($provider_delivery) {
                 
                 
                 
                $model_message = new Message($this->registry);
                $existing_message = $model_message->findByExternalMessageId(
                    $provider_code,
                    $channel_id,
                    $external_message_id
                );
                $system_event_repaired = $existing_message && $is_messenger_system_event
                    ? $model_message->promoteExternalMessageToSystemEvent(
                        (int)$existing_message['message_id'],
                        [
                            'text' => urlencode(trim((string)($event['text'] ?? ''))),
                            'source_provider' => $provider_code,
                            'source_channel_id' => $channel_id,
                            'date_added' => $this->eventDate($event),
                        ]
                    )
                    : false;
                $mutable_reconciled = $existing_message
                    && $this->reconcileMutableMarketplaceMessage(
                        $provider_code,
                        $channel_id,
                        $event,
                        $existing_message,
                        $model_message,
                        $trusted_marketplace_event
                    );
                $attachment_repaired = !$is_messenger_system_event && $existing_message && $this->repairDuplicateAttachments(
                    $provider_code,
                    $channel,
                    $event,
                    $existing_message,
                    $model_message
                );
                if ($attachment_repaired
                    && !$is_quiet_marketplace_import_hint
                    && !$is_quiet_history_reconciliation_hint) {
                    (new Session($this->registry))->updateModifiedDate((int)$existing_message['chat_id']);
                }
                if (in_array($provider_code, ['max', 'whatsapp-personal', 'vk', 'telegram', 'max-bot'], true) && $existing_message) {
                     
                     
                    $this->saveProviderContext(
                        $provider_code,
                        $channel_id,
                        [],
                        $event,
                        (int)$existing_message['chat_id'],
                        (int)$existing_message['message_id']
                    );
                }
                $attachment_pending = $existing_message
                    && !$is_messenger_system_event
                    && $this->areEventAttachmentsPending($event, $existing_message);
                return [
                    'success' => true,
                    'duplicate' => true,
                    'updated' => (bool)($system_event_repaired || $mutable_reconciled || $attachment_repaired),
                    'attachment_pending' => (bool)$attachment_pending,
                    'chat_id' => (int)($existing_message['chat_id'] ?? 0),
                    'message_id' => (int)($existing_message['message_id'] ?? $provider_delivery['message_id'] ?? 0),
                ];
            }
        }
        if ($provider_code === YandexMarketProvider::PROVIDER && $is_external_out && $external_message_id !== '') {
            $reconciled = $this->reconcileYandexMarketOutboundEcho(
                $model_messenger,
                $channel_id,
                $event,
                $external_message_id
            );
            if ($reconciled) {
                return $reconciled;
            }
        }
        if ($model_messenger->findDeliveryByDedupe($channel_id, $dedupe_key)) {
            $existing_message = [];
            $system_event_repaired = false;
            $mutable_reconciled = false;
            $attachment_repaired = false;
            $external_message_id = (string)($event['external_message_id'] ?? '');
            if ($external_message_id !== '') {
                $model_message = new Message($this->registry);
                $existing_message = $model_message->findByExternalMessageId($provider_code, $channel_id, $external_message_id);
                $system_event_repaired = $existing_message && $is_messenger_system_event
                    ? $model_message->promoteExternalMessageToSystemEvent(
                        (int)$existing_message['message_id'],
                        [
                            'text' => urlencode(trim((string)($event['text'] ?? ''))),
                            'source_provider' => $provider_code,
                            'source_channel_id' => $channel_id,
                            'date_added' => $this->eventDate($event),
                        ]
                    )
                    : false;
                $mutable_reconciled = $existing_message
                    && $this->reconcileMutableMarketplaceMessage(
                        $provider_code,
                        $channel_id,
                        $event,
                        $existing_message,
                        $model_message,
                        $trusted_marketplace_event
                    );
                $attachment_repaired = !$is_messenger_system_event && $existing_message && $this->repairDuplicateAttachments(
                    $provider_code,
                    $channel,
                    $event,
                    $existing_message,
                    $model_message
                );
                if ($attachment_repaired
                    && !$is_quiet_marketplace_import_hint
                    && !$is_quiet_history_reconciliation_hint) {
                    (new Session($this->registry))->updateModifiedDate((int)$existing_message['chat_id']);
                }
                if (in_array($provider_code, ['max', 'whatsapp-personal', 'vk', 'telegram', 'max-bot'], true) && $existing_message) {
                    $this->saveProviderContext(
                        $provider_code,
                        $channel_id,
                        [],
                        $event,
                        (int)$existing_message['chat_id'],
                        (int)$existing_message['message_id']
                    );
                }
            }
            $this->logger->info('inbound_duplicate', [
                'provider' => $provider_code,
                'channel_id' => $channel_id,
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
            ]);
            return [
                'success' => true,
                'duplicate' => true,
                'updated' => (bool)($system_event_repaired || $mutable_reconciled || $attachment_repaired),
                'attachment_pending' => $existing_message
                    && !$is_messenger_system_event
                    && $this->areEventAttachmentsPending($event, $existing_message),
                'chat_id' => (int)($existing_message['chat_id'] ?? 0),
                'message_id' => (int)($existing_message['message_id'] ?? 0),
            ];
        }

        if ($this->suppressDeletedMaxHistory($provider_code, $channel_id, $event, $dedupe_key, $raw_body, $model_messenger)) {
            return ['success' => true, 'ignored' => 'deleted_chat_history'];
        }

        $contact_external_user_id = $this->contactExternalUserId($provider_code, $event, $external_user_id);
        $existing_contact = $model_messenger->findContact($channel_id, $contact_external_user_id);
        if (!$existing_contact && ($is_external_out || in_array($provider_code, ['avito', 'youla', 'max', 'whatsapp-personal', 'wildberries', 'ozon', 'yandex_market'], true))) {
            $existing_contact = $model_messenger->findContactByExternalChat($channel_id, (string)($event['external_chat_id'] ?? ''));
        }
        $event = $this->enrichProviderEvent($provider_code, $provider, $credentials, $event, $channel, $existing_contact);
        $event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $is_messenger_system_event = $is_messenger_system_event
            || $this->isMessengerSystemEvent($provider_code, $event);
        $is_quiet_marketplace_import = !empty($event_metadata['marketplace'])
            && !empty($event_metadata['quiet_import']);
        $is_quiet_history_reconciliation =
            (string)($event_metadata['sync_origin'] ?? '') === 'history_reconciliation'
            && (in_array($provider_code, ['max', 'whatsapp-personal'], true) || !empty($event_metadata['marketplace']));
        $should_reopen_chat = !$is_external_out
            && !$is_messenger_system_event
            && !$is_quiet_marketplace_import
            && !$is_quiet_history_reconciliation
            && !$this->isProfileOnlyEvent($provider_code, $event);
        $display_name = $this->resolveContactDisplayName($provider_code, $event, $existing_contact);
        $username = trim((string)($event['username'] ?? ''));
        $profile_url = trim((string)($event['profile_url'] ?? ''));
        if ($existing_contact) {
            if ($username === '') {
                $username = trim((string)($existing_contact['username'] ?? ''));
            }
            $stored_profile_url = trim((string)($existing_contact['profile_url'] ?? ''));
            if ($profile_url === '' || $this->shouldKeepStoredProfileUrl($provider_code, $profile_url, $stored_profile_url)) {
                $profile_url = $stored_profile_url;
            }
        }

        $contact = $model_messenger->upsertContact(
            $channel_id,
            $provider_code,
            $contact_external_user_id,
            (string)($event['external_chat_id'] ?? $external_user_id),
            $display_name,
            $username,
            $profile_url,
            0,
            $this->contactMetadata($provider_code, $event, $external_user_id, $existing_contact ?: [], $bot_user_id),
            in_array($provider_code, ['avito', 'max'], true)
                || $provider_code === 'whatsapp-personal'
        );

        $model_session = new Session($this->registry);
        $chat_id = (int)($contact['chat_id'] ?? 0);
        $session_info = null;

        if ($chat_id > 0) {
            $session_info = $model_session->getSessionById($chat_id);
            if (!$session_info) {
                $this->logger->warning('inbound_recovered_stale_contact', [
                    'provider' => $provider_code,
                    'channel_id' => $channel_id,
                    'chat_id' => $chat_id,
                    'contact_id' => (int)($contact['contact_id'] ?? 0),
                    'user_hash' => MessengerLogger::hashExternalId($contact_external_user_id),
                ]);
                $model_messenger->clearContactChat((int)($contact['contact_id'] ?? 0));
                $contact['chat_id'] = null;
                $chat_id = 0;
            } else {
                $blacklist = new \EasyChat\Library\BlacklistService($this->registry);
                if ($blacklist->isBlocked('', $chat_id)) {
                    try {
                        if ($provider_code === 'avito') {
                            $token = $this->avitoTokenFromCredentials($credentials);
                        } elseif ($provider_code === 'youla') {
                            $token = $this->youlaTokenFromCredentials($credentials);
                        } else {
                            $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
                        }
                        if ($token !== '') {
                            $model_setting = new Setting($this->registry);
                            $widget_locale = $model_setting->get('chat_widget_locale', 'ru');
                            $msg = \EasyChat\Library\I18n::translate('widget.blocked_placeholder', [], $widget_locale);
                            $provider->sendMessage($token, (string)$contact['external_user_id'], $msg, [], [
                                'external_chat_id' => (string)($contact['external_chat_id'] ?? ''),
                                'channel_id' => (int)$channel_id,
                            ]);
                        }
                    } catch (\Throwable $e) {
                        $this->logger->warning('inbound_blocked_notification_failed', [
                            'chat_id' => $chat_id,
                            'error' => $e->getMessage()
                        ]);
                    }

                    $this->logger->info('inbound_ignored_blocked_chat', [
                        'provider' => $provider_code,
                        'channel_id' => $channel_id,
                        'chat_id' => $chat_id,
                    ]);
                    return [
                        'success' => true,
                        'ignored' => 'chat_blocked',
                        'chat_id' => $chat_id,
                    ];
                }
            }
        }

        if ($chat_id <= 0 && $is_messenger_system_event) {
             
             
            $model_messenger->createDelivery([
                'channel_id' => $channel_id,
                'contact_id' => (int)($contact['contact_id'] ?? 0),
                'direction' => 'in',
                'external_message_id' => $external_message_id,
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', $raw_body),
                'status' => 'ignored',
            ]);
            $model_messenger->updateChannel($channel_id, [
                'last_inbound_at' => (new AppClock($this->registry))->nowSql(),
                'last_error' => '',
            ]);
            return ['success' => true, 'ignored' => 'messenger_system_event_without_chat'];
        }

        if ($chat_id <= 0) {
             
             
            if ($this->suppressDeletedMaxHistory($provider_code, $channel_id, $event, $dedupe_key, $raw_body, $model_messenger)) {
                return ['success' => true, 'ignored' => 'deleted_chat_history'];
            }
            $prefix = substr($provider_code, 0, 3);
            $session_id = $prefix . substr(hash('sha256', $channel_id . ':' . $contact_external_user_id), 0, 32 - strlen($prefix));
            $name = trim($display_name);
            if ($name === '') {
                $name = strtoupper($provider_code) . ' ' . substr(MessengerLogger::hashExternalId($contact_external_user_id), 0, 6);
            }
            $display_name = $name;

            $chat_id = $model_session->createSession($session_id, $name, [
                'source_provider' => $provider_code,
                'source_channel_id' => $channel_id,
                'external_contact_id' => (int)$contact['contact_id'],
                'conversation_kind' => (string)($event['conversation_kind'] ?? 'chat'),
                'external_thread_id' => (string)($event['external_thread_id'] ?? ''),
                'read_receipt_capability' => MessageReadReceiptService::capability(
                    $provider_code,
                    (string)($event['conversation_kind'] ?? ''),
                    is_array($event['metadata'] ?? null) ? $event['metadata'] : []
                ),
            ]);
            $model_messenger->linkContactToChat((int)$contact['contact_id'], $chat_id);
        } else {
            $session_update = [
                'source_provider' => $provider_code,
                'source_channel_id' => $channel_id,
                'external_contact_id' => (int)$contact['contact_id'],
                'read_receipt_capability' => MessageReadReceiptService::capability(
                    $provider_code,
                    (string)($event['conversation_kind'] ?? ''),
                    is_array($event['metadata'] ?? null) ? $event['metadata'] : []
                ),
            ];
            if (!empty($event['conversation_kind'])) {
                $session_update['conversation_kind'] = (string)$event['conversation_kind'];
            }
            if (!empty($event['external_thread_id'])) {
                $session_update['external_thread_id'] = (string)$event['external_thread_id'];
            }
            if ($this->shouldUpdateSessionName($provider_code, (string)($session_info['customer_name'] ?? ''), $display_name, $event)) {
                $session_update['customer_name'] = $display_name;
            }
            if (!$is_messenger_system_event) {
                $model_session->updateSession(
                    $chat_id,
                    $session_update,
                    !$is_quiet_marketplace_import && !$is_quiet_history_reconciliation
                );
            }
        }

        if (!$is_messenger_system_event) {
            (new ContactAvatarService($this->registry))->enqueueFromEvent($channel, $contact, $event);
        }

        if (($event['event_kind'] ?? '') === 'conversation_start') {
            return $this->handleConversationStart($provider_code, $channel, $contact, $event, $chat_id, $dedupe_key);
        }

        if ($this->isProfileOnlyEvent($provider_code, $event)) {
            $model_session->updateModifiedDate($chat_id);
            $model_messenger->updateChannel($channel_id, [
                'last_inbound_at' => (new AppClock($this->registry))->nowSql(),
                'last_error' => '',
            ]);
            $this->logger->info('inbound_profile_event_saved', [
                'provider' => $provider_code,
                'channel_id' => $channel_id,
                'chat_id' => $chat_id,
                'event_type' => (string)($event['update_type'] ?? ''),
                'user_hash' => MessengerLogger::hashExternalId($contact_external_user_id),
            ]);
            return [
                'success' => true,
                'ignored' => 'profile_event',
                'chat_id' => $chat_id,
            ];
        }

        $source_text = trim((string)($event['text'] ?? ''));
        $text = $source_text;
        $event_attachments = is_array($event['attachments'] ?? null) ? $event['attachments'] : [];
        $event_has_attachments = count($event_attachments) > 0;
        $event_has_voice = false;
        $event_has_image = false;
        foreach ($event_attachments as $event_attachment) {
            if (!is_array($event_attachment)) {
                continue;
            }

            $event_attachment_kind = strtolower((string)($event_attachment['type'] ?? $event_attachment['kind'] ?? ''));
            if ($event_attachment_kind === 'audio') {
                $event_has_voice = true;
            } elseif (in_array($event_attachment_kind, ['image', 'photo'], true)) {
                $event_has_image = true;
            }
        }
        $model_message = new Message($this->registry);
        $external_message_id = (string)($event['external_message_id'] ?? '');
        $existing_message = $external_message_id !== ''
            ? $model_message->findByExternalMessageId($provider_code, $channel_id, $external_message_id)
            : [];
        if ($existing_message) {
            $this->saveProviderContext($provider_code, $channel_id, $contact, $event, (int)$existing_message['chat_id'], (int)$existing_message['message_id']);
            if ($is_messenger_system_event) {
                 
                 
                 
                $system_event_repaired = $model_message->promoteExternalMessageToSystemEvent(
                    (int)$existing_message['message_id'],
                    [
                        'text' => urlencode($text),
                        'source_provider' => $provider_code,
                        'source_channel_id' => $channel_id,
                        'date_added' => $this->eventDate($event),
                    ]
                );
                return [
                    'success' => true,
                    'duplicate' => true,
                    'updated' => $system_event_repaired,
                    'attachment_pending' => false,
                    'chat_id' => (int)$existing_message['chat_id'],
                    'message_id' => (int)$existing_message['message_id'],
                ];
            }
            $attachment_repaired = $this->repairDuplicateAttachments(
                $provider_code,
                $channel,
                $event,
                $existing_message,
                $model_message
            );
            $mutable_reconciled = $this->reconcileMutableMarketplaceMessage(
                $provider_code,
                $channel_id,
                $event,
                $existing_message,
                $model_message,
                $trusted_marketplace_event
            );
            if ($attachment_repaired
                && !$is_quiet_marketplace_import
                && !$is_quiet_history_reconciliation) {
                $model_session->updateModifiedDate((int)$existing_message['chat_id']);
            }
            $attachment_pending = $this->areEventAttachmentsPending($event, $existing_message);
            return [
                'success' => true,
                'duplicate' => true,
                'updated' => (bool)($mutable_reconciled || $attachment_repaired),
                'attachment_pending' => $attachment_pending,
                'chat_id' => (int)$existing_message['chat_id'],
                'message_id' => (int)$existing_message['message_id'],
            ];
        }

        $revision_message = $is_messenger_system_event
            ? []
            : $this->findMaxChannelRevisionMessage($provider_code, $channel_id, $chat_id, $event, $model_message);

        $attachments = $this->downloadAttachments($provider_code, $channel, $chat_id, $event_attachments);
        $first_image = '';
        foreach ($attachments as $attachment) {
            if (($attachment['kind'] ?? 'image') === 'image' && !empty($attachment['filename'])) {
                $first_image = (string)$attachment['filename'];
                break;
            }
        }
        if ($text === '' && !$attachments && $event_has_attachments) {
            $text = $is_external_out
                ? 'Оператор отправил вложение в ' . $this->providerLabel($provider_code) . ', но EasyChat пока не смог получить файл.'
                : 'Пользователь отправил вложение в ' . $this->providerLabel($provider_code) . ', но EasyChat пока не смог получить файл.';
        }

        if ($revision_message && !$is_external_out) {
            $message_id = (int)$revision_message['message_id'];
            $revision_image = ($event_has_attachments && !$attachments)
                ? (string)($revision_message['image'] ?? '')
                : $first_image;
            $model_message->updateIncomingExternalMessageRevision($message_id, [
                'text' => urlencode($text),
                'image' => $revision_image,
                'source_provider' => $provider_code,
                'source_channel_id' => $channel_id,
                'external_message_id' => $external_message_id,
                'date_added' => $this->eventDate($event),
            ]);

            $storage = new AttachmentStorage($this->registry);
            if (!$event_has_attachments || $attachments) {
                $storage->replaceMessageAttachments($message_id, $chat_id, $attachments, $provider_code);
            }

            if ($is_quiet_marketplace_import || $is_quiet_history_reconciliation) {
                $model_session->updateModifiedDateFromMessages($chat_id);
            } else {
                $model_session->updateModifiedDate($chat_id);
            }
            $this->saveProviderContext($provider_code, $channel_id, $contact, $event, $chat_id, $message_id);
            $model_messenger->createDelivery([
                'channel_id' => $channel_id,
                'contact_id' => (int)$contact['contact_id'],
                'message_id' => $message_id,
                'direction' => 'in',
                'external_message_id' => $external_message_id,
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', $raw_body),
                'status' => 'updated',
                'provider_occurred_at' => $this->eventDate($event),
                'provider_cursor' => $provider_code === MaxProvider::PROVIDER ? max(0, (int)($event['created'] ?? 0)) : 0,
                'ingest_mode' => $is_quiet_marketplace_import ? 'initial_history' : 'live',
            ]);
            $model_messenger->updateChannel($channel_id, [
                'last_inbound_at' => (new AppClock($this->registry))->nowSql(),
                'last_error' => '',
            ]);

            $this->logger->info('inbound_max_channel_revision_collapsed', [
                'provider' => $provider_code,
                'channel_id' => $channel_id,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'previous_message_hash' => MessengerLogger::hashExternalId((string)($revision_message['external_message_id'] ?? '')),
                'message_hash' => MessengerLogger::hashExternalId($external_message_id),
                'attachment_count' => count($attachments),
            ]);

            return [
                'success' => true,
                'updated' => true,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
            ];
        }

        if ($should_reopen_chat) {
             
             
             
            $model_session->updateSession($chat_id, ['is_archived' => 0]);
        }

        $projected_manager_id = $this->projectedMarketplaceManagerId(
            $provider_code,
            $event,
            $is_external_out,
            $trusted_marketplace_event
        );
        $message_id = $model_message->addMessage([
            'chat_id' => $chat_id,
            'text' => urlencode($text),
            'image' => $first_image,
            'author' => $is_messenger_system_event ? 4 : ($is_external_out ? 2 : 1),
            'manager_id' => $projected_manager_id,
             
             
             
            'manager_name' => $is_external_out
                ? ($projected_manager_id > 0 ? '' : ('Оператор ' . $this->providerLabel($provider_code)))
                : '',
            'status_user' => $is_messenger_system_event ? 1 : ($is_external_out ? 0 : 1),
             
             
            'status_admin' => ($is_messenger_system_event || $is_external_out || $is_quiet_history_reconciliation || $is_quiet_marketplace_import) ? 1 : 0,
            'ip' => $provider_code,
            'source_provider' => $provider_code,
            'source_channel_id' => $channel_id,
            'external_message_id' => $external_message_id,
            'message_kind' => $is_messenger_system_event ? 'messenger_system_event' : 'normal',
            'date_added' => $this->eventDate($event),
            'touch_chat' => !$is_messenger_system_event
                && !$is_quiet_marketplace_import
                && !$is_quiet_history_reconciliation,
            'skip_metrics' => $is_messenger_system_event,
        ]);

        $storage = new AttachmentStorage($this->registry);
        foreach ($attachments as $attachment) {
            $storage->attachToMessage($message_id, $chat_id, $attachment, $provider_code, [
                'external_payload_hash' => $attachment['external_payload_hash'] ?? '',
            ]);
        }

        if (!$is_messenger_system_event) {
            if ($is_quiet_marketplace_import || $is_quiet_history_reconciliation) {
                $model_session->updateModifiedDateFromMessages($chat_id);
            } else {
                $model_session->updateModifiedDate($chat_id);
            }
        }
        $this->saveProviderContext($provider_code, $channel_id, $contact, $event, $chat_id, $message_id);

        $provider_cursor = $provider_code === MaxProvider::PROVIDER
            ? max(0, (int)($event['created'] ?? 0))
            : 0;
        $model_messenger->createDelivery([
            'channel_id' => $channel_id,
            'contact_id' => (int)$contact['contact_id'],
            'message_id' => $message_id,
            'direction' => $is_external_out ? 'out' : 'in',
            'external_message_id' => $external_message_id,
            'dedupe_key' => $dedupe_key,
            'payload_hash' => hash('sha256', $raw_body),
            'status' => $is_external_out ? 'sent' : 'received',
            'provider_occurred_at' => $this->eventDate($event),
            'provider_cursor' => $provider_cursor,
            'ingest_mode' => $is_quiet_history_reconciliation
                ? 'reconciliation'
                : ($is_quiet_marketplace_import ? 'initial_history' : 'live'),
        ]);
        if ($provider_code === MaxProvider::PROVIDER && $is_external_out && $provider_cursor > 0) {
            (new MessageReadReceiptService($this->registry))->projectMaxMessage(
                $channel_id,
                (string)($event['external_chat_id'] ?? ''),
                $message_id,
                $provider_cursor
            );
        }
        $model_messenger->updateChannel($channel_id, [
            'last_inbound_at' => (new AppClock($this->registry))->nowSql(),
            'last_error' => '',
        ]);

        $this->logger->info('inbound_message_saved', [
            'provider' => $provider_code,
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'user_hash' => MessengerLogger::hashExternalId($contact_external_user_id),
            'attachment_count' => count($attachments),
            'message_kind' => $is_messenger_system_event ? 'messenger_system_event' : 'normal',
        ]);

        if ($provider_code === 'avito') {
            (new AvitoHistorySyncService($this->registry))->enqueue($channel_id, $chat_id, 'webhook');
        }

        if (
            !$defer_notifications
            && !$is_messenger_system_event
            && !$is_external_out
            && !$is_quiet_history_reconciliation
            && !$is_quiet_marketplace_import
        ) {
             
             
            $push_notifier = new AndroidPushNotifier($this->registry);
            $push_notifier->notifyNewMessage(
                $chat_id,
                $provider_code,
                $display_name,
                $this->providerLabel($provider_code)
            );

             
             
             
             
            if ($provider_code !== SmsProvider::PROVIDER) {
                if ($event_has_voice && (new VoiceMediaService($this->registry))->isOperational()) {
                     
                     
                     
                    $this->logger->info('ai_defer_voice_transcription', [
                        'provider' => $provider_code,
                        'chat_id' => $chat_id,
                        'message_id' => (int)$message_id,
                        'layer' => 'inbound',
                    ]);
                } elseif ($event_has_image && $first_image === '') {
                     
                     
                     
                     
                    $this->logger->warning('ai_skip_visual_unavailable', [
                        'provider' => $provider_code,
                        'chat_id' => $chat_id,
                        'message_id' => (int)$message_id,
                        'layer' => 'inbound',
                    ]);
                } elseif ($source_text === '' && $event_has_attachments && $first_image === '') {
                     
                     
                     
                    $this->logger->info('ai_skip_attachment_only', [
                        'provider' => $provider_code,
                        'chat_id' => $chat_id,
                        'message_id' => (int)$message_id,
                        'layer' => 'inbound',
                    ]);
                } else {
                    $channel_settings = json_decode((string)($channel['settings_json'] ?? ''), true);
                    $channel_settings = is_array($channel_settings) ? $channel_settings : [];
                     
                     
                     
                     
                     
                    $auto_reply_allowed = $provider_code !== WhatsAppPersonalProvider::PROVIDER
                        && ($provider_code !== YandexMarketProvider::PROVIDER
                            || (!empty($channel_settings['ai_auto_reply'])
                                && !empty($event_metadata['ai_auto_reply_allowed'])));
                    if (!$auto_reply_allowed) {
                         
                         
                         
                        $this->logger->info('ai_auto_reply_skipped_channel_policy', [
                            'provider' => $provider_code,
                            'chat_id' => $chat_id,
                            'message_id' => (int)$message_id,
                            'layer' => 'inbound',
                        ]);
                        return [
                            'success' => true,
                            'chat_id' => $chat_id,
                            'message_id' => $message_id,
                            'attachment_count' => count($attachments),
                            'attachment_pending' => $event_has_attachments && count($attachments) < count($event_attachments),
                        ];
                    }
                    $service_notice = (new ServiceNoticeClassifier())->classify($provider_code, $text, [
                        'author' => $is_external_out ? 2 : 1,
                        'source_provider' => $provider_code,
                        'source_channel_id' => $channel_id,
                    ]);
                    if (!empty($service_notice['blocked'])) {
                        $this->logger->info('ai_skip_service_notice', [
                            'provider' => $provider_code,
                            'chat_id' => $chat_id,
                            'message_id' => (int)$message_id,
                            'reason' => (string)($service_notice['reason'] ?? 'service_notice'),
                            'layer' => 'inbound',
                        ]);
                    } else {
                        $this->triggerAiIfNeeded($chat_id, (int)$message_id, $provider_code);
                    }
                }
            }
        }

        return [
            'success' => true,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'attachment_count' => count($attachments),
            'attachment_pending' => $event_has_attachments && count($attachments) < count($event_attachments),
        ];
    }

    private function handleConversationStart(string $provider_code, array $channel, array $contact, array $event, int $chat_id, string $fallback_dedupe): array {
        if (!in_array($provider_code, ['max_bot', 'telegram', 'vk'], true) || $chat_id <= 0) {
            return ['success' => true, 'ignored' => 'conversation_start_unsupported'];
        }

        $model_messenger = new MessengerModel($this->registry);
        $dedupe_key = trim((string)($event['dedupe_key'] ?? '')) ?: $fallback_dedupe;
        $start_id = $model_messenger->claimConversationStart([
            'channel_id' => (int)$channel['channel_id'],
            'provider' => $provider_code,
            'dedupe_key' => $dedupe_key,
            'start_payload' => (string)($event['start_payload'] ?? ''),
            'event_at' => $this->eventDate($event),
        ]);
        if ($start_id <= 0) {
            return ['success' => true, 'duplicate' => true, 'chat_id' => $chat_id];
        }

        $model_message = new Message($this->registry);
        $last_message = $model_message->getLastMessage($chat_id);
        $boundary_message_id = (int)($last_message['message_id'] ?? 0);
        $model_session = new Session($this->registry);
        $model_session->beginConversation($chat_id, $boundary_message_id);
        $settings = new Setting($this->registry);
        $greeting_enabled = $settings->get('chat_bot_start_greeting_status', '0') === '1';
        $notify_enabled = $settings->get('chat_bot_start_notify_status', '0') === '1';
        $result = [
            'contact_id' => (int)($contact['contact_id'] ?? 0),
            'chat_id' => $chat_id,
            'boundary_message_id' => $boundary_message_id,
            'status' => 'silent',
        ];

        try {
            if ($greeting_enabled) {
                $override = trim((string)$settings->get('chat_bot_start_message_' . $provider_code, ''));
                $text = $override !== '' ? $override : trim((string)$settings->get('chat_bot_start_message', ''));
                if ($text !== '') {
                    $message_id = $model_message->addMessage([
                        'chat_id' => $chat_id, 'author' => 2, 'manager_name' => 'Автоприветствие',
                        'text' => urlencode($text), 'status_user' => 0, 'status_admin' => 1,
                        'source_provider' => $provider_code, 'source_channel_id' => (int)$channel['channel_id'],
                        'message_kind' => 'bot_greeting', 'skip_metrics' => true,
                    ]);
                    $delivery = (new OutboundService($this->registry))->sendMessageId($message_id);
                    if (empty($delivery['success'])) {
                        throw new \RuntimeException((string)($delivery['error'] ?? 'greeting_delivery_failed'));
                    }
                    $result['greeting_message_id'] = $message_id;
                    $result['status'] = 'greeted';
                }
            } elseif ($notify_enabled) {
                $notice_id = $model_message->addMessage([
                    'chat_id' => $chat_id, 'author' => 2, 'manager_name' => 'EasyChat',
                    'text' => urlencode('Пользователь запустил бота. Теперь можно первым начать диалог.'),
                    'status_user' => 1, 'status_admin' => 0,
                    'source_provider' => $provider_code, 'source_channel_id' => (int)$channel['channel_id'],
                    'message_kind' => 'conversation_start_notice', 'skip_metrics' => true,
                ]);
                (new AndroidPushNotifier($this->registry))->notifyNewMessage(
                    $chat_id, $provider_code, (string)($contact['display_name'] ?? ''), strtoupper($provider_code)
                );
                $result['notice_message_id'] = $notice_id;
                $result['status'] = 'notified';
            }
            $model_messenger->finishConversationStart($start_id, $result);
        } catch (\Throwable $e) {
            $result['status'] = 'failed';
            $result['last_error'] = mb_substr($e->getMessage(), 0, 512);
            $model_messenger->finishConversationStart($start_id, $result);
            $this->logger->warning('conversation_start_failed', ['provider' => $provider_code, 'chat_id' => $chat_id, 'error' => $e->getMessage()]);
            return ['success' => false, 'error' => 'conversation_start_failed', 'chat_id' => $chat_id];
        }

        $model_messenger->updateChannel((int)$channel['channel_id'], [
            'last_inbound_at' => (new AppClock($this->registry))->nowSql(), 'last_error' => '',
        ]);
        return ['success' => true, 'conversation_start' => true, 'status' => $result['status'], 'chat_id' => $chat_id];
    }

    private function contactExternalUserId(string $provider_code, array $event, string $fallback_user_id): string {
        if (in_array($provider_code, ['wildberries', 'ozon', 'yandex_market'], true)) {
            $external_thread_id = trim((string)($event['external_thread_id'] ?? $event['external_chat_id'] ?? ''));
            if ($external_thread_id !== '') {
                return $external_thread_id;
            }
        }
        if ($provider_code === 'avito') {
             
             
             
            $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
            if ($external_chat_id !== '') {
                return $external_chat_id;
            }
        }
        if ($provider_code === 'youla') {
             
             
             
            $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
            if ($external_chat_id !== '') {
                return $external_chat_id;
            }
        }
        if ($provider_code === 'max') {
             
             
             
             
            $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
            if ($external_chat_id !== '') {
                return $external_chat_id;
            }
        }
        return $fallback_user_id;
    }

    private function contactMetadata(string $provider_code, array $event, string $external_user_id, array $existing_contact = [], string $account_user_id = ''): array {
        $metadata = ['last_update_type' => (string)($event['update_type'] ?? '')];
        if ($provider_code === 'email') {
            $existing_metadata = $this->existingContactMetadata($existing_contact);
            $event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
             
             
            foreach (['customer_email', 'subject', 'reply_to', 'message_id'] as $key) {
                $value = trim((string)($event_metadata[$key] ?? $existing_metadata[$key] ?? ''));
                if ($value !== '') {
                    $metadata[$key] = mb_substr($value, 0, $key === 'subject' ? 998 : 254, 'UTF-8');
                }
            }
        }
        if ($provider_code === 'avito') {
            $existing_metadata = $this->existingContactMetadata($existing_contact);
            $event_author_id = preg_replace('/\D+/', '', (string)($event['external_user_id'] ?? $external_user_id));
            $account_user_id = preg_replace('/\D+/', '', $account_user_id);
            $counterparty_author_id = '';
            if ($event_author_id !== '' && ($account_user_id === '' || $event_author_id !== $account_user_id)) {
                $counterparty_author_id = $event_author_id;
            }
            if ($counterparty_author_id === '') {
                $stored_author_id = preg_replace('/\D+/', '', (string)($existing_metadata['counterparty_author_id'] ?? $existing_metadata['author_id'] ?? ''));
                if ($stored_author_id !== '' && ($account_user_id === '' || $stored_author_id !== $account_user_id)) {
                    $counterparty_author_id = $stored_author_id;
                }
            }

             
             
             
            $metadata['author_id'] = $counterparty_author_id !== '' ? $counterparty_author_id : $event_author_id;
            $metadata['counterparty_author_id'] = $counterparty_author_id;
            $metadata['last_event_author_id'] = $event_author_id;
            $metadata['recipient_user_id'] = (string)($event['recipient_user_id'] ?? '');
            $metadata['item_id'] = (string)($event['item_id'] ?? '');
            $metadata['chat_type'] = (string)($event['chat_type'] ?? '');
        }
        if ($provider_code === 'youla') {
            $existing_metadata = $this->existingContactMetadata($existing_contact);
            $sender_id = $this->normalizeYoulaId((string)($event['external_user_id'] ?? $external_user_id));
            $seller_user_id = $this->normalizeYoulaId((string)$account_user_id);
            $buyer_user_id = $sender_id;
            if ($seller_user_id !== '' && $sender_id === $seller_user_id) {
                $buyer_user_id = $this->normalizeYoulaId((string)($existing_metadata['buyer_user_id'] ?? ''));
            }
            $metadata['seller_user_id'] = $seller_user_id !== '' ? $seller_user_id : $this->normalizeYoulaId((string)($existing_metadata['seller_user_id'] ?? ''));
            $metadata['buyer_user_id'] = $buyer_user_id !== '' ? $buyer_user_id : $this->normalizeYoulaId((string)($existing_metadata['buyer_user_id'] ?? ''));
            $metadata['last_event_sender_id'] = $sender_id;
            $metadata['recipient_user_id'] = (string)($event['recipient_user_id'] ?? '');
            $metadata['product_id'] = (string)($event['item_id'] ?? $existing_metadata['product_id'] ?? '');
            $metadata['item_id'] = (string)($event['item_id'] ?? $existing_metadata['item_id'] ?? '');
            $metadata['chat_type'] = (string)($event['chat_type'] ?? 'product');
        }
        if ($provider_code === 'max') {
            $existing_metadata = $this->existingContactMetadata($existing_contact);
            $event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
            $chat_type = MaxConversationIdentity::firstKnownChatType([
                $event_metadata['chat_type'] ?? '',
                $existing_metadata['chat_type'] ?? '',
                $existing_metadata['max_chat_type'] ?? '',
            ]);
            $account_user_id = MaxConversationIdentity::normalizeUserId($account_user_id);
            $event_sender_id = MaxConversationIdentity::normalizeUserId(
                (string)($event_metadata['sender_id'] ?? $event['external_user_id'] ?? $external_user_id)
            );
            $is_outgoing = (string)($event['direction'] ?? '') === 'out';
            $stored_counterparty_id = MaxConversationIdentity::firstCounterpartyUserId([
                $existing_metadata['counterparty_sender_id'] ?? '',
                (string)($existing_metadata['identity_owner_type'] ?? '') === MaxConversationIdentity::OWNER_USER
                    ? ($existing_metadata['identity_owner_id'] ?? '')
                    : '',
                $existing_metadata['sender_id'] ?? '',
            ], $account_user_id, [(string)($event['external_chat_id'] ?? $existing_contact['external_chat_id'] ?? '')]);
            $event_counterparty_id = MaxConversationIdentity::firstCounterpartyUserId([
                $event_metadata['counterparty_sender_id'] ?? '',
                !$is_outgoing ? $event_sender_id : '',
            ], $account_user_id, [(string)($event['external_chat_id'] ?? '')]);
            $counterparty_id = $event_counterparty_id !== ''
                ? $event_counterparty_id
                : $stored_counterparty_id;
            if ($chat_type !== 'DIALOG') {
                $counterparty_id = '';
            }
            $metadata['sender_id'] = $counterparty_id;
            $metadata['counterparty_sender_id'] = $counterparty_id;
            $metadata['last_event_sender_id'] = $event_sender_id;
            $metadata['account_id'] = $account_user_id;
            $metadata['chat_id'] = (string)($event_metadata['chat_id'] ?? $event['external_chat_id'] ?? '');
            $metadata['chat_type'] = $chat_type;
            $metadata['chat_display_name'] = (string)($event_metadata['chat_display_name'] ?? $existing_metadata['chat_display_name'] ?? '');
            $metadata['display_name_authority'] = (string)($event_metadata['display_name_authority'] ?? $existing_metadata['display_name_authority'] ?? '');
            $metadata = MaxConversationIdentity::enrichMetadata(
                $metadata,
                (string)($event['external_chat_id'] ?? ''),
                (string)($existing_contact['external_user_id'] ?? ''),
                $account_user_id
            );
            $metadata = array_merge($metadata, ReplyCapability::metadataForMaxChatType($chat_type));
            $metadata['avatar_source'] = (string)($event_metadata['avatar_source'] ?? '');
        }
        if ($provider_code === 'whatsapp-personal') {
             
             
             
            $metadata = array_merge(
                $this->existingContactMetadata($existing_contact),
                $metadata,
                is_array($event['metadata'] ?? null) ? $event['metadata'] : []
            );
            $metadata['chat_type'] = strtoupper((string)($metadata['chat_type'] ?? 'DIALOG')) === 'GROUP'
                ? 'GROUP'
                : 'DIALOG';
        }
        if (in_array($provider_code, ['wildberries', 'ozon', 'yandex_market'], true)) {
            $event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
            $item_context = is_array($event['item_context'] ?? null) ? $event['item_context'] : [];
            $metadata['marketplace'] = $provider_code;
            $metadata['conversation_kind'] = (string)($event['conversation_kind'] ?? '');
            $metadata['external_thread_id'] = (string)($event['external_thread_id'] ?? '');
            $metadata['item_id'] = (string)(($item_context['item_id'] ?? '') ?: ($event_metadata['item_id'] ?? ''));
        }

        return $metadata;
    }

    private function normalizeYoulaId(string $value): string {
        $value = strtolower(trim($value));
        return preg_match('/^[a-f0-9]{24}$/', $value) === 1 ? $value : '';
    }

    private function isMessengerSystemEvent(string $provider_code, array $event): bool {
        if ($provider_code !== MaxProvider::PROVIDER) {
            return false;
        }
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        return !empty($metadata['system_event'])
            && trim((string)($metadata['system_event_type'] ?? '')) !== '';
    }

    private function existingContactMetadata(array $contact): array {
        if (empty($contact['metadata_json'])) {
            return [];
        }
        $metadata = json_decode((string)$contact['metadata_json'], true);
        return is_array($metadata) ? $metadata : [];
    }

    private function saveProviderContext(string $provider_code, int $channel_id, array $contact, array $event, int $chat_id, int $message_id): void {
        if (in_array($provider_code, ['max', 'whatsapp-personal', 'vk', 'telegram', 'max-bot'], true)) {
            $this->saveMessengerMessageContext($provider_code, $channel_id, $contact, $event, $chat_id, $message_id);
            return;
        }
        if (in_array($provider_code, ['wildberries', 'ozon', 'yandex_market'], true)) {
            $this->saveMarketplaceContext($provider_code, $channel_id, $contact, $event, $chat_id, $message_id);
            return;
        }
        if ($provider_code === 'youla') {
            $this->saveYoulaContext($channel_id, $contact, $event, $chat_id, $message_id);
            return;
        }
        $this->saveAvitoContext($provider_code, $channel_id, $contact, $event, $chat_id, $message_id);
    }

    private function saveMessengerMessageContext(string $provider_code, int $channel_id, array $contact, array $event, int $chat_id, int $message_id): void {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
         
         
        if (!in_array($provider_code, ['max', 'whatsapp-personal'], true) && empty($metadata['forwarded'])) {
            return;
        }
        (new MessageContext($this->registry))->upsertMessengerMessageContext($provider_code, [
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'external_chat_id' => (string)($event['external_chat_id'] ?? $contact['external_chat_id'] ?? ''),
            'external_user_id' => (string)($event['external_user_id'] ?? ''),
            'external_message_id' => (string)($event['external_message_id'] ?? ''),
            'chat_type' => (string)($metadata['chat_type'] ?? ''),
            'chat_display_name' => (string)($metadata['chat_display_name'] ?? ''),
            'sender_display_name' => (string)($metadata['sender_display_name'] ?? ''),
            'sender_resolution_status' => (string)($metadata['sender_resolution_status'] ?? ''),
            'forwarded' => !empty($metadata['forwarded']),
            'forward_source_name' => (string)($metadata['forward_source_name'] ?? ''),
            'forward_source_type' => (string)($metadata['forward_source_type'] ?? ''),
            'system_event_type' => (string)($metadata['system_event_type'] ?? ''),
            'system_event_actor_names' => is_array($metadata['system_event_actor_names'] ?? null)
                ? $metadata['system_event_actor_names']
                : [],
            'system_event_actor_count' => (int)($metadata['system_event_user_count'] ?? 0),
        ]);
    }

    private function saveMarketplaceContext(string $provider_code, int $channel_id, array $contact, array $event, int $chat_id, int $message_id): void {
        $item = is_array($event['item_context'] ?? null) ? $event['item_context'] : [];
        $item_id = trim((string)($item['item_id'] ?? $item['id'] ?? $item['sku'] ?? ''));
        if ($item_id === '') {
            return;
        }
        (new MessageContext($this->registry))->upsertMarketplaceItemContext(
            $provider_code,
            $provider_code . '_item',
            [
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'external_chat_id' => (string)($event['external_chat_id'] ?? $contact['external_chat_id'] ?? ''),
            'external_user_id' => (string)($event['external_user_id'] ?? ''),
            'external_message_id' => (string)($event['external_message_id'] ?? ''),
            'item_id' => $item_id,
            'chat_type' => (string)($event['conversation_kind'] ?? MarketplaceStream::BUYER_CHAT),
            'snapshot' => $item,
            ]
        );
    }

    private function saveAvitoContext(string $provider_code, int $channel_id, array $contact, array $event, int $chat_id, int $message_id): void {
        if ($provider_code !== 'avito') {
            return;
        }

        $item_context = is_array($event['item_context'] ?? null) ? $event['item_context'] : [];
        $item_id = trim((string)($event['item_id'] ?? ''));
        if ($item_id === '') {
            $item_id = trim((string)($item_context['id'] ?? $item_context['item_id'] ?? ''));
        }
        if ($item_id === '') {
            $chat_context = is_array($event['chat_context'] ?? null) ? $event['chat_context'] : [];
            $context_kind = (string)($chat_context['context_kind'] ?? '');
            $chat_type = trim((string)($event['chat_type'] ?? $chat_context['chat_type'] ?? ''));
            if ($context_kind === 'general' || ($chat_type !== '' && $chat_type !== 'u2i')) {
                (new MessageContext($this->registry))->upsertAvitoGeneralContext([
                    'channel_id' => $channel_id,
                    'chat_id' => $chat_id,
                    'message_id' => $message_id,
                    'external_chat_id' => (string)($event['external_chat_id'] ?? $contact['external_chat_id'] ?? ''),
                    'external_user_id' => (string)($event['external_user_id'] ?? ''),
                    'external_message_id' => (string)($event['external_message_id'] ?? ''),
                    'chat_type' => $chat_type !== '' ? $chat_type : 'u2u',
                    'title' => (string)($chat_context['title'] ?? 'Общий запрос Авито'),
                    'url' => (string)($chat_context['url'] ?? ''),
                    'snapshot' => $chat_context,
                ]);
            }
            return;
        }

        $context_payload = [
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'external_chat_id' => (string)($event['external_chat_id'] ?? $contact['external_chat_id'] ?? ''),
            'external_user_id' => (string)($event['external_user_id'] ?? ''),
            'external_message_id' => (string)($event['external_message_id'] ?? ''),
            'item_id' => $item_id,
            'chat_type' => (string)($event['chat_type'] ?? ''),
            'snapshot' => $item_context,
        ];
        (new MessageContext($this->registry))->upsertAvitoItemContext($context_payload);
        (new AvitoItemContextService($this->registry))->remember($channel_id, array_merge($item_context, [
            'item_id' => $item_id,
        ]), 'webhook_context');
    }

    private function saveYoulaContext(int $channel_id, array $contact, array $event, int $chat_id, int $message_id): void {
        $item_context = is_array($event['item_context'] ?? null) ? $event['item_context'] : [];
        $item_id = trim((string)($event['item_id'] ?? ''));
        if ($item_id === '') {
            $item_id = trim((string)($item_context['id'] ?? $item_context['item_id'] ?? ''));
        }
        if ($item_id === '') {
            return;
        }

        $context_payload = [
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'external_chat_id' => (string)($event['external_chat_id'] ?? $contact['external_chat_id'] ?? ''),
            'external_user_id' => (string)($event['external_user_id'] ?? ''),
            'external_message_id' => (string)($event['external_message_id'] ?? ''),
            'item_id' => $item_id,
            'chat_type' => (string)($event['chat_type'] ?? 'product'),
            'snapshot' => array_merge($item_context, [
                'provider' => 'youla',
                'item_id' => $item_id,
            ]),
        ];
        (new MessageContext($this->registry))->upsertYoulaItemContext($context_payload);
        (new YoulaItemContextService($this->registry))->remember($channel_id, array_merge($item_context, [
            'item_id' => $item_id,
        ]), 'webhook_context');
    }

    private function handleYoulaSystemEvent(int $channel_id, array $event): void {
        $event_type = trim((string)($event['update_type'] ?? ''));
        if (strpos($event_type, 'user.') === 0) {
            $this->handleYoulaUserSystemEvent($channel_id, $event);
            return;
        }

        $item_id = trim((string)($event['item_id'] ?? ''));
        if ($channel_id <= 0 || $item_id === '') {
            return;
        }
        $status = trim((string)($event['item_status'] ?? $event['update_type'] ?? ''));
        $item_context = is_array($event['item_context'] ?? null) ? $event['item_context'] : [];
        (new YoulaItemContextService($this->registry))->remember($channel_id, [
            'provider' => 'youla',
            'item_id' => $item_id,
            'status_id' => $status,
        ] + $item_context, 'system_event');
        $this->logger->info('youla_system_event_saved', [
            'channel_id' => $channel_id,
            'item_hash' => MessengerLogger::hashExternalId($item_id),
            'event_type' => $status,
        ]);
    }

    private function handleYoulaUserSystemEvent(int $channel_id, array $event): void {
        if ($channel_id <= 0) {
            return;
        }

        $event_type = trim((string)($event['update_type'] ?? ''));
        $user_id = $this->normalizeYoulaId((string)($event['system_user_id'] ?? ''));
        $model_messenger = new MessengerModel($this->registry);
        $channel = $model_messenger->getChannelById($channel_id);
        if (!$channel || (string)($channel['provider'] ?? '') !== 'youla') {
            return;
        }

        if ($user_id === '') {
            $this->logger->info('youla_user_system_event_ignored', [
                'channel_id' => $channel_id,
                'event_type' => $event_type,
                'reason' => 'user_missing',
            ]);
            return;
        }

        $seller_user_id = $this->normalizeYoulaId((string)($channel['bot_user_id'] ?? ''));
        if ($seller_user_id === '') {
            $this->logger->info('youla_user_system_event_ignored', [
                'channel_id' => $channel_id,
                'event_type' => $event_type,
                'user_hash' => MessengerLogger::hashExternalId($user_id),
                'reason' => 'account_unverified',
            ]);
            return;
        }

        if (!hash_equals($seller_user_id, $user_id)) {
            $this->logger->info('youla_user_system_event_ignored', [
                'channel_id' => $channel_id,
                'event_type' => $event_type,
                'user_hash' => MessengerLogger::hashExternalId($user_id),
                'reason' => 'unrelated_user',
            ]);
            return;
        }

        if ($event_type === 'user.block') {
            $block_type = trim((string)($event['system_block_type'] ?? ''));
            $model_messenger->updateChannel($channel_id, [
                'status' => 'error',
                'last_error' => $block_type !== '' ? ('youla_user_blocked:' . $this->safeError($block_type)) : 'youla_user_blocked',
            ]);
            $this->logger->warning('youla_seller_blocked', [
                'channel_id' => $channel_id,
                'user_hash' => MessengerLogger::hashExternalId($user_id),
            ]);
            return;
        }

        if ($event_type === 'user.unblock') {
            $current_status = (string)($channel['status'] ?? '');
            $current_error = (string)($channel['last_error'] ?? '');
            if (strpos($current_error, 'youla_user_blocked') === 0) {
                $model_messenger->updateChannel($channel_id, [
                    'status' => $current_status === 'error' ? 'active' : $current_status,
                    'last_error' => '',
                ]);
            }
            $this->logger->info('youla_seller_unblocked', [
                'channel_id' => $channel_id,
                'user_hash' => MessengerLogger::hashExternalId($user_id),
            ]);
        }
    }

    private function rememberChannelBotUserId(array $channel, string $bot_user_id): void {
        $channel_id = (int)($channel['channel_id'] ?? 0);
        if ($channel_id <= 0 || $bot_user_id === '') {
            return;
        }

        (new MessengerModel($this->registry))->updateChannel($channel_id, [
            'bot_user_id' => $bot_user_id,
        ]);
    }

    private function eventDedupeKey(array $event, string $raw_body): string {
        $dedupe_key = (string)($event['dedupe_key'] ?? '');
        if ($dedupe_key === '') {
            $dedupe_key = (string)($event['external_message_id'] ?? '');
        }
        if ($dedupe_key === '') {
            $dedupe_key = substr(hash('sha256', $raw_body), 0, 64);
        }
        return substr($dedupe_key, 0, 128);
    }

    





    private function reconcileMutableMarketplaceMessage(
        string $provider_code,
        int $channel_id,
        array $event,
        array $existing_message,
        Message $model_message,
        bool $trusted_marketplace_event
    ): bool {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $kind = MarketplaceStream::normalize((string)($event['conversation_kind'] ?? ''));
        $thread_id = trim((string)($event['external_thread_id'] ?? ''));
        $external_message_id = trim((string)($event['external_message_id'] ?? ''));
        if (
            !$trusted_marketplace_event
            || $provider_code !== WildberriesProvider::PROVIDER
            || $channel_id <= 0
            || empty($metadata['marketplace'])
            || empty($metadata['mutable_external_message'])
            || (string)($event['direction'] ?? '') !== 'out'
            || !in_array($kind, [MarketplaceStream::QUESTION, MarketplaceStream::REVIEW], true)
            || $thread_id === ''
            || !hash_equals($kind . ':' . $thread_id . ':answer', $external_message_id)
            || (int)($existing_message['author'] ?? 0) !== 2
        ) {
            return false;
        }

        $changed = $model_message->updateMutableMarketplaceMessage(
            (int)($existing_message['message_id'] ?? 0),
            [
                'text' => urlencode(trim((string)($event['text'] ?? ''))),
                'source_provider' => $provider_code,
                'source_channel_id' => $channel_id,
                'external_message_id' => $external_message_id,
                'date_added' => $this->eventDate($event),
                'update_event_date' => !empty($metadata['mutable_event_date']),
            ]
        );
        if ($changed) {
             
             
            (new Session($this->registry))->updateModifiedDateFromMessages(
                (int)($existing_message['chat_id'] ?? 0)
            );
        }
        return $changed;
    }

    private function projectedMarketplaceManagerId(
        string $provider_code,
        array $event,
        bool $is_external_out,
        bool $trusted_marketplace_event
    ): int {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        if (
            !$trusted_marketplace_event
            || !$is_external_out
            || $provider_code !== WildberriesProvider::PROVIDER
            || empty($metadata['marketplace'])
            || (string)($metadata['sync_origin'] ?? '') !== 'action_projection'
        ) {
            return 0;
        }
        return max(0, (int)($event['manager_id'] ?? 0));
    }

    private function downloadAttachments(string $provider_code, array $channel, int $chat_id, array $items): array {
        $downloaded = [];
        if (!$items) {
            return $downloaded;
        }

        $credentials = json_decode((string)$channel['credentials_json'], true);
        $credentials = is_array($credentials) ? $credentials : [];
        $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
        $allowed_hosts = [];
        if ($provider_code === WildberriesProvider::PROVIDER) {
            $opened = (new MarketplaceCredentialsVault($this->registry))->open($credentials, ['api_token']);
            $token = (string)($opened['api_token'] ?? '');
            $headers = $token !== '' ? ['Authorization: ' . preg_replace('/^Bearer\s+/i', '', trim($token))] : [];
            $allowed_hosts = ['buyer-chat-api.wildberries.ru', 'wbbasket.ru', 'wildberries.ru', 'wbstatic.net'];
        } elseif ($provider_code === OzonProvider::PROVIDER) {
            $headers = [];
            $allowed_hosts = ['ozon.ru', 'ozonusercontent.com', 'ozone.ru'];
        } elseif ($provider_code === YandexMarketProvider::PROVIDER) {
             
             
            $headers = [];
            $allowed_hosts = ['yandex.ru', 'yandex.net', 'yastatic.net'];
        }
         
         
         
         
        if (!isset($headers)) {
            $headers = ($provider_code === 'max-bot' && $token !== '') ? [$this->authorizationHeader($token)] : [];
        }

        $model_setting = new Setting($this->registry);
        $max_mb = max(1, (int)$model_setting->get('chat_max_file_size', '5'));
        $storage = new AttachmentStorage($this->registry);

        foreach ($items as $item_index => $item) {
            if (!is_array($item)) {
                $this->logger->warning('inbound_attachment_unsupported', [
                    'provider' => $provider_code,
                    'chat_id' => $chat_id,
                    'type' => 'unknown',
                ]);
                continue;
            }

             
             
             
             
             
            $item['payload_hash'] = $this->eventAttachmentHash($item, (int)$item_index);

             
             
             
             
            if (strtolower((string)($item['type'] ?? $item['kind'] ?? '')) === 'audio') {
                $downloaded[] = (new VoiceMediaService($this->registry))->placeholder(
                    $provider_code,
                    (int)($channel['channel_id'] ?? 0),
                    $item
                );
                continue;
            }

            if (!empty($item['unsupported'])) {
                $this->logger->warning('inbound_attachment_unsupported', [
                    'provider' => $provider_code,
                    'chat_id' => $chat_id,
                    'type' => (string)($item['type'] ?? 'unknown'),
                ]);
                continue;
            }

            $url = trim((string)($item['url'] ?? ''));
            if ($url === '' && !empty($item['telegram_file_id'])) {
                $provider = ProviderRegistry::get($provider_code, $this->registry);
                if ($provider && method_exists($provider, 'getFileUrl')) {
                    $url = $provider->getFileUrl($token, (string)$item['telegram_file_id']);
                }
            }

            if ($url === '') {
                $this->logger->warning('inbound_attachment_url_missing', [
                    'provider' => $provider_code,
                    'chat_id' => $chat_id,
                    'type' => (string)($item['type'] ?? 'unknown'),
                ]);
                continue;
            }

            $item_max_mb = $this->remoteAttachmentMaxMb($provider_code, $item, $max_mb);
            $item_headers = $headers;
            if ($provider_code === WildberriesProvider::PROVIDER) {
                $download_host = strtolower((string)(parse_url($url, PHP_URL_HOST) ?: ''));
                 
                 
                 
                if (!empty($item['auth_required'])) {
                    if ($download_host !== 'buyer-chat-api.wildberries.ru') {
                        $this->logger->warning('inbound_attachment_auth_host_denied', [
                            'provider' => $provider_code,
                            'chat_id' => $chat_id,
                        ]);
                        continue;
                    }
                } else {
                    $item_headers = [];
                }
            }
            $result = $storage->saveRemoteAttachment(
                $chat_id,
                $url,
                $item_headers,
                $item_max_mb,
                $provider_code,
                (string)($item['file_name'] ?? $item['filename'] ?? ''),
                $allowed_hosts
            );
            if (empty($result['success'])) {
                $this->logger->warning('inbound_attachment_download_failed', [
                    'provider' => $provider_code,
                    'chat_id' => $chat_id,
                    'error' => (string)($result['error'] ?? 'download_failed'),
                    'http_code' => (int)($result['http_code'] ?? 0),
                ]);
                continue;
            }
            $result['external_payload_hash'] = (string)$item['payload_hash'];
            $downloaded[] = $result;
        }

        return $downloaded;
    }

    private function repairDuplicateAttachments(
        string $provider_code,
        array $channel,
        array $event,
        array $existing_message,
        Message $model_message
    ): bool {
        $message_id = (int)($existing_message['message_id'] ?? 0);
        $chat_id = (int)($existing_message['chat_id'] ?? 0);
        $event_attachments = is_array($event['attachments'] ?? null) ? $event['attachments'] : [];
        if ($message_id <= 0 || $chat_id <= 0 || !$event_attachments) {
            return false;
        }

        $storage = new AttachmentStorage($this->registry);
        if (!$storage->acquireMessageAttachmentLock($message_id)) {
            return false;
        }
        try {
            $stored = $storage->getAttachmentsByMessageIds([$message_id]);
            $stored_rows = is_array($stored[$message_id] ?? null) ? $stored[$message_id] : [];
            $stored_hashes = $this->storedAttachmentHashes($stored_rows);
            $missing = [];
            foreach ($event_attachments as $index => $attachment) {
                if (!is_array($attachment)) {
                    continue;
                }
                $hash = $this->eventAttachmentHash($attachment, (int)$index);
                if (!isset($stored_hashes[$hash])) {
                    $attachment['payload_hash'] = $hash;
                    $missing[] = $attachment;
                }
            }
            if (!$missing) {
                return false;
            }

            $attachments = $this->downloadAttachments($provider_code, $channel, $chat_id, $missing);
            if (!$attachments) {
                $this->logger->warning('inbound_duplicate_attachment_repair_deferred', [
                    'provider' => $provider_code,
                    'channel_id' => (int)($channel['channel_id'] ?? 0),
                    'chat_id' => $chat_id,
                    'message_id' => $message_id,
                    'missing_count' => count($missing),
                ]);
                return false;
            }

            $first_image = (string)($existing_message['image'] ?? '');
            foreach ($attachments as $attachment) {
                $storage->attachToMessage($message_id, $chat_id, $attachment, $provider_code, [
                    'external_payload_hash' => $attachment['external_payload_hash'] ?? '',
                ]);
                if ($first_image === ''
                    && ($attachment['kind'] ?? 'image') === 'image'
                    && !empty($attachment['filename'])) {
                    $first_image = (string)$attachment['filename'];
                }
            }

            $classification = (new ServiceNoticeClassifier())->classify(
                $provider_code,
                urldecode((string)($existing_message['text'] ?? '')),
                ['author' => (int)($existing_message['author'] ?? 1)]
            );
            $text = (string)($existing_message['text'] ?? '');
            if ((string)($classification['reason'] ?? '') === 'attachment_download_placeholder') {
                $text = urlencode(trim((string)($event['text'] ?? '')));
            }
            $model_message->updateIncomingExternalMessageRevision($message_id, [
                'text' => $text,
                'image' => $first_image,
                'source_provider' => $provider_code,
                'source_channel_id' => (int)($channel['channel_id'] ?? 0),
                'external_message_id' => (string)($event['external_message_id'] ?? ''),
                'date_added' => (string)($existing_message['date_added'] ?? ''),
            ]);

            $this->logger->info('inbound_duplicate_attachment_repaired', [
                'provider' => $provider_code,
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'attachment_count' => count($attachments),
                'remaining_count' => max(0, count($missing) - count($attachments)),
            ]);
            return true;
        } finally {
            $storage->releaseMessageAttachmentLock($message_id);
        }
    }

    private function areEventAttachmentsPending(array $event, array $message): bool {
        $attachments = is_array($event['attachments'] ?? null) ? $event['attachments'] : [];
        $message_id = (int)($message['message_id'] ?? 0);
        if (!$attachments || $message_id <= 0) {
            return false;
        }

        $expected = [];
        foreach ($attachments as $index => $attachment) {
            if (is_array($attachment)) {
                $expected[$this->eventAttachmentHash($attachment, (int)$index)] = true;
            }
        }
        $stored = (new AttachmentStorage($this->registry))->getAttachmentsByMessageIds([$message_id]);
        $stored_rows = is_array($stored[$message_id] ?? null) ? $stored[$message_id] : [];
        if (count($stored_rows) < count($expected)) {
            return true;
        }
        $stored_hashes = $this->storedAttachmentHashes($stored_rows);
        return count(array_diff_key($expected, $stored_hashes)) > 0;
    }

    private function eventAttachmentHash(array $attachment, int $index): string {
        $provided = strtolower(trim((string)($attachment['payload_hash'] ?? '')));
        if (preg_match('/^[a-f0-9]{64}$/', $provided) === 1) {
            return $provided;
        }
        return hash('sha256', implode('|', [
            (string)($attachment['type'] ?? $attachment['kind'] ?? ''),
            (string)($attachment['url'] ?? ''),
            (string)($attachment['telegram_file_id'] ?? $attachment['file_id'] ?? ''),
            (string)($attachment['file_name'] ?? $attachment['filename'] ?? ''),
            (string)($attachment['duration'] ?? ''),
            (string)$index,
        ]));
    }

    private function storedAttachmentHashes(array $rows): array {
        $hashes = [];
        foreach ($rows as $row) {
            $meta = json_decode((string)($row['meta_json'] ?? ''), true);
            $hash = strtolower(trim((string)(is_array($meta) ? ($meta['external_payload_hash'] ?? '') : '')));
            if (preg_match('/^[a-f0-9]{64}$/', $hash) === 1) {
                $hashes[$hash] = true;
            }
        }
        return $hashes;
    }

    private function remoteAttachmentMaxMb(string $provider_code, array $item, int $configured_max_mb): int {
        $configured_max_mb = max(1, $configured_max_mb);
        $kind = strtolower((string)($item['kind'] ?? $item['type'] ?? ''));
        $name = strtolower((string)($item['file_name'] ?? $item['filename'] ?? ''));
        $mime = strtolower((string)($item['mime_type'] ?? $item['mime'] ?? ''));
        $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $is_document = $kind === 'document'
            || AttachmentPolicy::isDocumentExtension($extension)
            || AttachmentPolicy::isDocumentMime($mime);
        if (!$is_document) {
            return $configured_max_mb;
        }

         
         
         
         
        if (in_array($provider_code, ['max-bot', 'max', 'whatsapp-personal', 'telegram', 'vk', 'wildberries', 'ozon', 'yandex_market'], true)) {
            return max($configured_max_mb, self::REMOTE_DOCUMENT_MAX_MB);
        }

        return $configured_max_mb;
    }

    private function decryptSecret(string $encrypted): string {
        $model_setting = new Setting($this->registry);
        $install_secret = $model_setting->get('chat_install_secret', '');
        return SecretBox::decrypt($encrypted, $install_secret);
    }

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

    private function authorizationHeader(string $token): string {
        $token = trim($token);
        if (preg_match('/^Bearer\s+/i', $token)) {
            $token = trim((string)preg_replace('/^Bearer\s+/i', '', $token));
        }
        return 'Authorization: ' . $token;
    }

    private function enrichProviderEvent(string $provider_code, $provider, array $credentials, array $event, array $channel, ?array $existing_contact): array {
        if ($provider_code === 'vk') {
            return $this->enrichVkEvent($provider_code, $provider, $credentials, $event, (int)($channel['channel_id'] ?? 0), $existing_contact);
        }
        if ($provider_code === 'avito') {
            return $this->enrichAvitoEvent($provider, $credentials, $event, $channel, $existing_contact);
        }
        if ($provider_code === 'youla') {
            return $this->enrichYoulaEvent($provider, $credentials, $event, $channel, $existing_contact);
        }
        return $event;
    }

    private function enrichMaxBotEvent($provider, array $credentials, array $event, int $channel_id): array {
        if (!method_exists($provider, 'hydrateForwardedEvent')) {
            unset($event['_provider_internal']);
            return $event;
        }
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $provider_internal = is_array($event['_provider_internal'] ?? null) ? $event['_provider_internal'] : [];
        if (empty($metadata['forwarded']) || !$provider_internal) {
            unset($event['_provider_internal']);
            return $event;
        }
        $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
        if ($token === '') {
            unset($event['_provider_internal']);
            return $event;
        }

        try {
            $result = $provider->hydrateForwardedEvent($token, $event);
        } catch (\Throwable $e) {
            $this->logger->warning('max_bot_forward_lookup_failed', [
                'channel_id' => $channel_id,
                'error' => 'exception',
            ]);
            unset($event['_provider_internal']);
            return $event;
        }
        if (empty($result['success'])) {
            $this->logger->warning('max_bot_forward_lookup_failed', [
                'channel_id' => $channel_id,
                'error' => (string)($result['error'] ?? 'lookup_failed'),
                'http_code' => (int)($result['http_code'] ?? 0),
            ]);
            unset($event['_provider_internal']);
            return $event;
        }
        if (!empty($result['lookup'])) {
            $this->logger->info('max_bot_forward_lookup_completed', [
                'channel_id' => $channel_id,
                'attachment_count' => count((array)($result['event']['attachments'] ?? [])),
            ]);
        }
        $event = is_array($result['event'] ?? null) ? $result['event'] : $event;
        unset($event['_provider_internal']);
        return $event;
    }

    private function enrichVkEvent(string $provider_code, $provider, array $credentials, array $event, int $channel_id, ?array $existing_contact): array {
        $external_user_id = trim((string)($event['external_user_id'] ?? ''));
        if (!preg_match('/^\d+$/', $external_user_id)) {
            return $event;
        }

        $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
        if ($token === '') {
            return $event;
        }

        $event = $this->backfillVkMessageAttachments($provider, $token, $event, $channel_id);
        $event = $this->resolveVkForwardSource($provider, $token, $event, $channel_id);

        if (!method_exists($provider, 'getUserInfo') || !$this->shouldFetchProviderProfile($provider_code, $existing_contact)) {
            return $event;
        }

        try {
            $profile = $provider->getUserInfo($token, $external_user_id);
        } catch (\Throwable $e) {
            $this->logger->warning('vk_profile_lookup_failed', [
                'channel_id' => $channel_id,
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'error' => 'exception',
            ]);
            return $event;
        }

        if (empty($profile['success'])) {
            $this->logger->warning('vk_profile_lookup_failed', [
                'channel_id' => $channel_id,
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'error' => (string)($profile['error'] ?? 'profile_lookup_failed'),
                'http_code' => (int)($profile['http_code'] ?? 0),
            ]);
            return $event;
        }

        foreach (['display_name', 'username', 'profile_url', 'avatar_url'] as $field) {
            $value = trim((string)($profile[$field] ?? ''));
            if ($value !== '') {
                $event[$field] = $value;
            }
        }

        return $event;
    }

    private function resolveVkForwardSource($provider, string $token, array $event, int $channel_id): array {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $source_id = trim((string)($metadata['forward_source_id'] ?? ''));
        if (empty($metadata['forwarded'])
            || trim((string)($metadata['forward_source_name'] ?? '')) !== ''
            || $source_id === ''
            || !method_exists($provider, 'getForwardSourceInfo')) {
            return $event;
        }

        try {
            $source = $provider->getForwardSourceInfo(
                $token,
                $source_id,
                (string)($metadata['forward_source_type'] ?? '')
            );
        } catch (\Throwable $e) {
            $source = ['success' => false, 'error' => 'exception'];
        }

        if (empty($source['success'])) {
            $this->logger->warning('vk_forward_source_lookup_failed', [
                'channel_id' => $channel_id,
                'source_hash' => MessengerLogger::hashExternalId($source_id),
                'error' => (string)($source['error'] ?? 'source_lookup_failed'),
                'http_code' => (int)($source['http_code'] ?? 0),
            ]);
            return $event;
        }

        $source_name = trim((string)($source['display_name'] ?? ''));
        if ($source_name !== '') {
            $metadata['forward_source_name'] = $source_name;
            $event['metadata'] = $metadata;
        }
        return $event;
    }

    private function backfillVkMessageAttachments($provider, string $token, array $event, int $channel_id): array {
        if ((!method_exists($provider, 'getMessageForEvent') && !method_exists($provider, 'getMessageById'))
            || !$this->vkMessageNeedsAttachmentBackfill($event)) {
            return $event;
        }

        $external_message_id = preg_replace('/\D+/', '', (string)($event['external_message_id'] ?? ''));
        if ($external_message_id === '') {
            return $event;
        }

        try {
            $message = method_exists($provider, 'getMessageForEvent')
                ? $provider->getMessageForEvent($token, $event)
                : $provider->getMessageById($token, $external_message_id);
        } catch (\Throwable $e) {
            $this->logger->warning('vk_message_attachment_backfill_failed', [
                'channel_id' => $channel_id,
                'message_id' => $external_message_id,
                'error' => 'exception',
            ]);
            return $event;
        }

        if (empty($message['success'])) {
            $this->logger->warning('vk_message_attachment_backfill_failed', [
                'channel_id' => $channel_id,
                'message_id' => $external_message_id,
                'error' => (string)($message['error'] ?? 'message_lookup_failed'),
                'http_code' => (int)($message['http_code'] ?? 0),
            ]);
            return $event;
        }

        $attachments = is_array($message['attachments'] ?? null) ? $message['attachments'] : [];
        $current_attachments = is_array($event['attachments'] ?? null) ? $event['attachments'] : [];
        $lookup_usable_count = $this->usableProviderAttachmentCount($attachments);
        $current_usable_count = $this->usableProviderAttachmentCount($current_attachments);
         
         
        if ($lookup_usable_count > 0 && $lookup_usable_count >= $current_usable_count) {
            $event['attachments'] = $attachments;
            $this->logger->info('vk_message_attachment_backfill_succeeded', [
                'channel_id' => $channel_id,
                'message_id' => $external_message_id,
                'attachment_count' => $lookup_usable_count,
                'callback_attachment_count' => $current_usable_count,
            ]);
        }

        $text = trim((string)($message['text'] ?? ''));
        if (trim((string)($event['text'] ?? '')) === '' && $text !== '') {
            $event['text'] = $text;
        }

        $lookup_metadata = is_array($message['metadata'] ?? null) ? $message['metadata'] : [];
        if ($lookup_metadata) {
            $event_metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
            foreach ($lookup_metadata as $key => $value) {
                if ($value !== '' && $value !== null && $value !== false) {
                    $event_metadata[$key] = $value;
                }
            }
            $event['metadata'] = $event_metadata;
        }

        return $event;
    }

    private function vkMessageNeedsAttachmentBackfill(array $event): bool {
        if (preg_match('/^\d+$/', (string)($event['external_message_id'] ?? '')) !== 1) {
            return false;
        }
        $attachments = is_array($event['attachments'] ?? null) ? $event['attachments'] : [];
        return count($attachments) > 0 || trim((string)($event['text'] ?? '')) === '';
    }

    private function hasUsableProviderAttachments(array $attachments): bool {
        return $this->usableProviderAttachmentCount($attachments) > 0;
    }

    private function usableProviderAttachmentCount(array $attachments): int {
        $count = 0;
        foreach ($attachments as $attachment) {
            if (!is_array($attachment)) {
                continue;
            }
            if (trim((string)($attachment['url'] ?? '')) !== '') {
                $count++;
                continue;
            }
            if (trim((string)($attachment['telegram_file_id'] ?? '')) !== '') {
                $count++;
            }
        }
        return $count;
    }

    private function enrichAvitoEvent($provider, array $credentials, array $event, array $channel, ?array $existing_contact): array {
        if (!method_exists($provider, 'getChatUserInfo')) {
            return $event;
        }

        $external_user_id = trim((string)($event['external_user_id'] ?? ''));
        $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
        $account_user_id = trim((string)($event['recipient_user_id'] ?? ''));
        if ($account_user_id === '') {
            $account_user_id = trim((string)($channel['bot_user_id'] ?? ''));
        }
        if ($external_chat_id === '' || $account_user_id === '') {
            return $event;
        }

        $token = $this->avitoTokenFromCredentials($credentials);
        if ($token === '') {
            return $event;
        }

        try {
            $lookup_user_id = (string)($event['direction'] ?? '') === 'out' ? '' : $external_user_id;
            $profile = $provider->getChatUserInfo($token, $account_user_id, $external_chat_id, $lookup_user_id);
        } catch (\Throwable $e) {
            $this->logger->warning('avito_profile_lookup_failed', [
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'chat_hash' => MessengerLogger::hashExternalId($external_chat_id),
                'error' => 'exception',
            ]);
            return $event;
        }

        if (empty($profile['success'])) {
            $this->logger->warning('avito_profile_lookup_failed', [
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'chat_hash' => MessengerLogger::hashExternalId($external_chat_id),
                'error' => (string)($profile['error'] ?? 'profile_lookup_failed'),
                'http_code' => (int)($profile['http_code'] ?? 0),
            ]);
            return $event;
        }

        foreach (['display_name', 'username', 'profile_url', 'avatar_url'] as $field) {
            $value = trim((string)($profile[$field] ?? ''));
            if ($value !== '' && ($this->shouldFetchProviderProfile('avito', $existing_contact) || $field === 'avatar_url')) {
                $event[$field] = $value;
            }
        }
        if (empty($event['item_context']) && !empty($profile['item_context']) && is_array($profile['item_context'])) {
            $event['item_context'] = $profile['item_context'];
        }
        if (empty($event['chat_context']) && !empty($profile['chat_context']) && is_array($profile['chat_context'])) {
            $event['chat_context'] = $profile['chat_context'];
        }
        if (empty($event['item_id']) && !empty($event['item_context']['id'])) {
            $event['item_id'] = (string)$event['item_context']['id'];
        }

        return $event;
    }

    private function enrichYoulaEvent($provider, array $credentials, array $event, array $channel, ?array $existing_contact): array {
        if (!method_exists($provider, 'getUserInfo')) {
            return $event;
        }
        if (!$this->shouldFetchProviderProfile('youla', $existing_contact)) {
            return $event;
        }

        $external_user_id = $this->normalizeYoulaId((string)($event['external_user_id'] ?? ''));
        if ($external_user_id === '') {
            return $event;
        }

        $token = $this->youlaTokenFromCredentials($credentials);
        if ($token === '') {
            return $event;
        }

        try {
            $profile = $provider->getUserInfo($token, $external_user_id, [
                'connect_timeout' => 2,
                'timeout' => 5,
            ]);
        } catch (\Throwable $e) {
            $this->logger->warning('youla_profile_lookup_failed', [
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'error' => 'exception',
            ]);
            return $event;
        }

        if (empty($profile['success'])) {
            $this->logger->warning('youla_profile_lookup_failed', [
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'user_hash' => MessengerLogger::hashExternalId($external_user_id),
                'error' => (string)($profile['error'] ?? 'profile_lookup_failed'),
                'http_code' => (int)($profile['http_code'] ?? 0),
            ]);
            return $event;
        }

        foreach (['display_name', 'username', 'profile_url', 'avatar_url'] as $field) {
            $value = trim((string)($profile[$field] ?? ''));
            if ($value !== '') {
                $event[$field] = $value;
            }
        }

        return $event;
    }

    private function findMaxChannelRevisionMessage(string $provider_code, int $channel_id, int $chat_id, array $event, Message $model_message): array {
        if (!$this->isMaxChannelLikeEvent($provider_code, $event)) {
            return [];
        }

        $normalized_text = $this->normalizeMessageForSimilarity((string)($event['text'] ?? ''));
        if (strlen($normalized_text) < 140) {
            return [];
        }

        $event_ts = $this->eventTimestamp($event);
        $event_attachments = $event['attachments'] ?? [];
        $has_attachment = is_array($event_attachments) && count($event_attachments) > 0;
        foreach ($model_message->getRecentIncomingExternalMessages($chat_id, $provider_code, $channel_id, 6) as $message) {
            $message_ts = $this->sqlTimestamp((string)($message['date_added'] ?? ''));
            if ($event_ts > 0 && $message_ts > 0 && abs($event_ts - $message_ts) > 900) {
                continue;
            }

            $previous_normalized = $this->normalizeMessageForSimilarity(urldecode((string)($message['text'] ?? '')));
            if (strlen($previous_normalized) < 140) {
                continue;
            }

            $threshold = ($has_attachment && trim((string)($message['image'] ?? '')) !== '') ? 0.82 : 0.9;
            if ($this->messageSimilarity($normalized_text, $previous_normalized) >= $threshold) {
                return $message;
            }
        }

        return [];
    }

    private function isMaxChannelLikeEvent(string $provider_code, array $event): bool {
        if ($provider_code !== 'max') {
            return false;
        }

        $metadata = $event['metadata'] ?? [];
        $chat_type = strtoupper(trim((string)(is_array($metadata) ? ($metadata['chat_type'] ?? '') : '')));
        if (in_array($chat_type, ['CHANNEL', 'CHAT'], true)) {
            return true;
        }

        $external_user_id = trim((string)($event['external_user_id'] ?? ''));
        $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
        $sender_id = trim((string)(is_array($metadata) ? ($metadata['sender_id'] ?? '') : ''));

         
         
        return $external_user_id !== ''
            && $external_user_id === $external_chat_id
            && strpos($external_user_id, '-') === 0
            && $sender_id === '';
    }

    private function normalizeMessageForSimilarity(string $text): string {
        $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
        $text = preg_replace('~https?://\S+~u', ' ', $text);
        $text = preg_replace('~(?:^|\s)(?:тг|tg|telegram|мах|max|rutube|youtube|дзен|vk|вк)(?:\s*\|\s*(?:тг|tg|telegram|мах|max|rutube|youtube|дзен|vk|вк))*\s*$~iu', ' ', (string)$text);
        if (function_exists('mb_strtolower')) {
            $text = mb_strtolower((string)$text, 'UTF-8');
        } else {
            $text = strtolower((string)$text);
        }
        $text = preg_replace('/[^\p{L}\p{N}]+/u', ' ', $text);
        $text = preg_replace('/\s+/u', ' ', (string)$text);
        return trim((string)$text);
    }

    private function messageSimilarity(string $a, string $b): float {
        $words_a = $this->uniqueSignificantWords($a);
        $words_b = $this->uniqueSignificantWords($b);
        $base = min(count($words_a), count($words_b));
        if ($base < 24) {
            return 0.0;
        }

        $overlap = 0;
        foreach ($words_a as $word => $_) {
            if (isset($words_b[$word])) {
                $overlap++;
            }
        }

        return $overlap / $base;
    }

    private function uniqueSignificantWords(string $text): array {
        $parts = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
        $words = [];
        foreach ($parts ?: [] as $word) {
            if ($this->utf8Length($word) < 3) {
                continue;
            }
            $words[$word] = true;
        }
        return $words;
    }

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

    private function eventTimestamp(array $event): int {
        return $this->sqlTimestamp($this->eventDate($event));
    }

    private function suppressDeletedMaxHistory(string $provider_code, int $channel_id, array $event, string $dedupe_key, string $raw_body, MessengerModel $model_messenger): bool {
        if ($provider_code !== MaxProvider::PROVIDER) {
            return false;
        }
        $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
        if ($external_chat_id === '') {
            return false;
        }
        $deletion = $model_messenger->getExternalChatDeletion($channel_id, $external_chat_id);
        if (!$deletion) {
            return false;
        }
        $event_ts = $this->eventTimestamp($event);
        $deleted_ts = $this->sqlTimestamp((string)($deletion['deleted_at'] ?? ''));
        if ($event_ts <= 0 || $deleted_ts <= 0 || $event_ts > $deleted_ts) {
            return false;
        }

        if ($dedupe_key !== '') {
            $model_messenger->createDelivery([
                'channel_id' => $channel_id,
                'direction' => (string)($event['direction'] ?? '') === 'out' ? 'out' : 'in',
                'external_message_id' => (string)($event['external_message_id'] ?? ''),
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', $raw_body),
                'status' => 'ignored',
            ]);
        }
        $this->logger->info('inbound_deleted_chat_history_ignored', [
            'provider' => $provider_code,
            'channel_id' => $channel_id,
            'chat_hash' => MessengerLogger::hashExternalId($external_chat_id),
            'message_hash' => MessengerLogger::hashExternalId((string)($event['external_message_id'] ?? '')),
        ]);
        return true;
    }

    







    private function reconcileYandexMarketOutboundEcho(
        MessengerModel $model_messenger,
        int $channel_id,
        array $event,
        string $external_message_id
    ): array {
        $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
        if ($external_chat_id === '') {
            return [];
        }
        $contact = $model_messenger->findContactByExternalChat($channel_id, $external_chat_id);
        $contact_id = (int)($contact['contact_id'] ?? 0);
        if ($contact_id <= 0) {
            return [];
        }

        $event_text = trim((string)($event['text'] ?? ''));
        $event_attachments = array_values(array_filter(
            is_array($event['attachments'] ?? null) ? $event['attachments'] : [],
            'is_array'
        ));
        if ($event_text === '' && !$event_attachments) {
            return [];
        }
        $event_ts = $this->sqlTimestamp($this->eventDate($event));

        $matched = [];
        $matched_by_text = false;
        $best_delta = PHP_INT_MAX;
        foreach ($model_messenger->findYandexMarketEchoCandidates($channel_id, $contact_id) as $candidate) {
            $candidate_ts = $this->sqlTimestamp((string)($candidate['message_date'] ?? $candidate['delivery_date'] ?? ''));
            if ($event_ts <= 0 || $candidate_ts <= 0 || abs($event_ts - $candidate_ts) > 300) {
                continue;
            }
            $candidate_text = trim(urldecode((string)($candidate['text'] ?? '')));
            $text_matches = $event_text !== '' && hash_equals($candidate_text, $event_text);
            $attachments_match = $event_attachments
                && $this->yandexMarketEchoAttachmentsMatch(
                    (array)($candidate['attachments'] ?? []),
                    $event_attachments
                );
            if (!$text_matches && !$attachments_match) {
                continue;
            }

            $message_id = (int)($candidate['message_id'] ?? 0);
            $chat_id = (int)($candidate['chat_id'] ?? 0);
            if ($message_id <= 0 || $chat_id <= 0) {
                continue;
            }
            $delta = abs($event_ts - $candidate_ts);
            $candidate_delivery_id = (int)($candidate['delivery_id'] ?? PHP_INT_MAX);
            $matched_delivery_id = (int)($matched['delivery_id'] ?? PHP_INT_MAX);
            if ($matched && ($delta > $best_delta
                || ($delta === $best_delta && $candidate_delivery_id >= $matched_delivery_id))) {
                continue;
            }
            $matched = $candidate;
            $matched_by_text = $text_matches;
            $best_delta = $delta;
        }
        if (!$matched) {
            return [];
        }

        $message_id = (int)$matched['message_id'];
        $chat_id = (int)$matched['chat_id'];
        $model_messenger->rememberSentProviderMessageId([
            'channel_id' => $channel_id,
            'contact_id' => $contact_id,
            'message_id' => $message_id,
            'external_message_id' => $external_message_id,
            'dedupe_key' => 'provider-message:' . substr(
                hash('sha256', $channel_id . ':' . $external_message_id),
                0,
                32
            ),
            'payload_hash' => hash('sha256', $message_id . ':' . $external_message_id),
            'status' => 'sent',
        ]);
        if (trim((string)($matched['external_message_id'] ?? '')) === '') {
            (new Message($this->registry))->updateExternalMessageId(
                $message_id,
                $external_message_id,
                YandexMarketProvider::PROVIDER,
                $channel_id
            );
        }

         
         
         
         
        $expected_parts = (trim(urldecode((string)($matched['text'] ?? ''))) !== '' ? 1 : 0)
            + count((array)($matched['attachments'] ?? []));
        $confirmed_parts = $model_messenger->countYandexMarketEchoParts($channel_id, $message_id);
        if ($expected_parts > 0 && $confirmed_parts >= $expected_parts) {
            $model_messenger->updateDelivery((int)$matched['delivery_id'], ['last_error' => '']);
        }

        $this->logger->info('yandex_market_outbound_echo_reconciled', [
            'provider' => YandexMarketProvider::PROVIDER,
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'message_hash' => MessengerLogger::hashExternalId($external_message_id),
            'match' => $matched_by_text ? 'text' : 'attachment',
            'confirmed_parts' => $confirmed_parts,
            'expected_parts' => $expected_parts,
        ]);
        return [
            'success' => true,
            'duplicate' => true,
            'reconciled' => true,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
        ];
    }

    






    private function reconcileWildberriesOutboundEcho(
        MessengerModel $model_messenger,
        int $channel_id,
        array $event,
        string $dedupe_key
    ): array {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        $correlation_id = trim((string)($metadata['outbound_correlation_id'] ?? ''));
        if ((string)($metadata['raw_kind'] ?? '') !== 'wb_chat_event'
            || (string)($metadata['provider_source'] ?? '') !== 'seller-public-api'
            || strpos($correlation_id, 'wb-send:') !== 0) {
            return [];
        }

        $external_chat_id = trim((string)($event['external_chat_id'] ?? ''));
        $contact = $model_messenger->findContactByExternalChat($channel_id, $external_chat_id);
        $contact_id = (int)($contact['contact_id'] ?? 0);
        if ($contact_id <= 0) {
            return [];
        }

         
         
        $legacy_timestamp = trim((string)($metadata['provider_add_timestamp'] ?? ''));
        $candidate = $model_messenger->findWildberriesDispatchedMessageByCorrelation(
            $channel_id,
            $contact_id,
            [$correlation_id, $legacy_timestamp]
        );
        $message_id = (int)($candidate['message_id'] ?? 0);
        $chat_id = (int)($candidate['chat_id'] ?? 0);
        if ($message_id <= 0 || $chat_id <= 0) {
            return [];
        }

        $model_message = new Message($this->registry);
        $event_delivery = $model_messenger->findDeliveryByDedupe($channel_id, $dedupe_key);
        $echo_message_id = (int)($event_delivery['message_id'] ?? 0);
        if ($echo_message_id > 0 && $echo_message_id !== $message_id) {
            $echo_message = $model_message->getMessageById($echo_message_id);
            if ((int)($echo_message['chat_id'] ?? 0) !== $chat_id) {
                 
                 
                return [];
            }
            $storage = new AttachmentStorage($this->registry);
            $storage->deleteMessageAttachments($echo_message_id, $chat_id);
            $model_message->mergeProviderEchoMessage($echo_message_id, $message_id);
        }

        $model_message->updateExternalMessageId(
            $message_id,
            $correlation_id,
            WildberriesProvider::PROVIDER,
            $channel_id
        );
        $model_messenger->updateDelivery((int)$candidate['delivery_id'], [
            'external_message_id' => $correlation_id,
            'status' => 'sent',
            'last_error' => '',
        ]);

        $this->logger->info('wildberries_outbound_echo_reconciled', [
            'provider' => WildberriesProvider::PROVIDER,
            'channel_id' => $channel_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'message_hash' => MessengerLogger::hashExternalId($correlation_id),
            'legacy_repair' => $legacy_timestamp !== ''
                && $legacy_timestamp === trim((string)($candidate['external_message_id'] ?? '')),
        ]);
        return [
            'success' => true,
            'duplicate' => true,
            'reconciled' => true,
            'updated' => $echo_message_id > 0 && $echo_message_id !== $message_id,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
        ];
    }

    private function yandexMarketEchoAttachmentsMatch(array $local, array $remote): bool {
        if (!$local || !$remote) {
            return false;
        }
        foreach ($remote as $remote_attachment) {
            $remote_name = $this->normalizedEchoFilename((string)($remote_attachment['file_name'] ?? ''));
            $remote_size = max(0, (int)($remote_attachment['size'] ?? 0));
            foreach ($local as $local_attachment) {
                $meta = json_decode((string)($local_attachment['meta_json'] ?? ''), true);
                $meta = is_array($meta) ? $meta : [];
                $local_name = $this->normalizedEchoFilename(
                    (string)($meta['display_name'] ?? $local_attachment['file_name'] ?? '')
                );
                $local_size = max(0, (int)($local_attachment['file_size'] ?? 0));
                $name_matches = $remote_name !== '' && $local_name !== '' && hash_equals($local_name, $remote_name);
                $size_matches = $remote_size > 0 && $local_size > 0 && $remote_size === $local_size;
                if (($name_matches && ($remote_size <= 0 || $local_size <= 0 || $size_matches))
                    || ($size_matches && $remote_name === '')) {
                    return true;
                }
            }
        }
        return false;
    }

    private function normalizedEchoFilename(string $name): string {
        $name = trim(basename(str_replace('\\', '/', $name)));
        return function_exists('mb_strtolower')
            ? mb_strtolower($name, 'UTF-8')
            : strtolower($name);
    }

    private function sqlTimestamp(string $sql): int {
        $timestamp = (new AppClock($this->registry))->sqlToTimestamp($sql);
        return $timestamp === false ? 0 : (int)$timestamp;
    }

    private function eventDate(array $event): string {
        $clock = new AppClock($this->registry);
        return $clock->fromProviderTimestampSql($event['created'] ?? 0, (string)($event['published_at'] ?? ''));
    }

    private function isProfileOnlyEvent(string $provider_code, array $event): bool {
        if ($provider_code !== 'max') {
            return false;
        }

        if ((string)($event['update_type'] ?? '') !== 'bot_started') {
            return false;
        }

        if (trim((string)($event['text'] ?? '')) !== '') {
            return false;
        }

        if (trim((string)($event['external_message_id'] ?? '')) !== '') {
            return false;
        }

        $attachments = $event['attachments'] ?? [];
        return !is_array($attachments) || count($attachments) === 0;
    }

    private function avitoTokenFromCredentials(array $credentials): string {
        $token = $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''));
        if ($token !== '') {
            return $token;
        }

        $client_id = $this->decryptSecretOrPlain((string)($credentials['client_id'] ?? ''));
        $client_secret = $this->decryptSecretOrPlain((string)($credentials['client_secret'] ?? ''));
        if ($client_id === '' || $client_secret === '') {
            return '';
        }

        return json_encode([
            'client_id' => $client_id,
            'client_secret' => $client_secret,
        ], JSON_UNESCAPED_UNICODE);
    }

    private function youlaTokenFromCredentials(array $credentials): string {
        $token = $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''));
        if ($token !== '') {
            return $token;
        }

        $bearer = $this->decryptSecretOrPlain((string)($credentials['token'] ?? ''));
        return $bearer;
    }

    private function shouldFetchProviderProfile(string $provider_code, ?array $existing_contact): bool {
        if (!in_array($provider_code, ['vk', 'avito', 'youla'], true)) {
            return false;
        }
        if (!$existing_contact) {
            return true;
        }

        $stored_name = trim((string)($existing_contact['display_name'] ?? ''));
        if ($this->isGenericProviderDisplayName($provider_code, $stored_name)) {
            return true;
        }

        if ($provider_code === 'avito') {
            return trim((string)($existing_contact['profile_url'] ?? '')) === '';
        }

        $stored_username = trim((string)($existing_contact['username'] ?? ''));
        $stored_profile_url = trim((string)($existing_contact['profile_url'] ?? ''));
        return $stored_username === '' || $stored_profile_url === '' || strpos($stored_profile_url, 'https://vk.com/id') === 0;
    }

    private function resolveContactDisplayName(string $provider_code, array $event, ?array $existing_contact): string {
        $display_name = trim((string)($event['display_name'] ?? ''));
        if ($provider_code === 'max'
            && $this->isAuthoritativeMaxIdentityEvent($event)
            && !$this->isGenericProviderDisplayName($provider_code, $display_name)) {
            return $display_name;
        }
        if ($existing_contact && $this->isGenericProviderDisplayName($provider_code, $display_name)) {
            $stored_name = trim((string)($existing_contact['display_name'] ?? ''));
            if (!$this->isGenericProviderDisplayName($provider_code, $stored_name)) {
                return $stored_name;
            }
        }
        return $display_name;
    }

    private function shouldUpdateSessionName(string $provider_code, string $current_name, string $incoming_name, array $event = []): bool {
        if ($this->isGenericProviderDisplayName($provider_code, $incoming_name)) {
            return false;
        }

        if ($provider_code === 'max'
            && $this->isAuthoritativeMaxIdentityEvent($event)
        ) {
            return trim($current_name) !== trim($incoming_name);
        }

        $current_name = trim($current_name);
        return $current_name === '' || $this->isGenericProviderDisplayName($provider_code, $current_name);
    }

    private function isAuthoritativeMaxIdentityEvent(array $event): bool {
        $metadata = is_array($event['metadata'] ?? null) ? $event['metadata'] : [];
        if ((int)($metadata['identity_version'] ?? 0) < MaxConversationIdentity::VERSION) {
            return false;
        }

        $expected = trim((string)($metadata['identity_fingerprint'] ?? ''));
        if (strlen($expected) !== 64) {
            return false;
        }
        $identity = MaxConversationIdentity::resolve(
            $metadata,
            (string)($event['external_chat_id'] ?? ''),
            (string)($event['external_user_id'] ?? ''),
            (string)($metadata['account_id'] ?? '')
        );
        $actual = (string)($identity['fingerprint'] ?? '');
        if (strlen($actual) !== 64 || !hash_equals($expected, $actual)) {
            return false;
        }

        $authority = (string)($metadata['display_name_authority'] ?? '');
        return ($identity['owner_type'] === MaxConversationIdentity::OWNER_CHAT && $authority === 'chat')
            || ($identity['owner_type'] === MaxConversationIdentity::OWNER_USER && $authority === 'user');
    }

    private function isGenericProviderDisplayName(string $provider_code, string $name): bool {
        $name = trim($name);
        if ($name === '') {
            return true;
        }

        if ($provider_code === 'vk') {
            return $name === 'Пользователь VK' || preg_match('/^VK [0-9a-f]{6}$/i', $name) === 1;
        }
        if ($provider_code === 'avito') {
            return $name === 'Пользователь Авито' || preg_match('/^AVITO [0-9a-f]{6}$/i', $name) === 1;
        }
        if ($provider_code === 'youla') {
            return $name === 'Покупатель Юлы' || preg_match('/^YOULA [0-9a-f]{6}$/i', $name) === 1;
        }
        if ($provider_code === 'max') {
            return MaxDisplayNamePolicy::isPlaceholder($name);
        }
        if ($provider_code === 'whatsapp-personal') {
            return $name === 'WhatsApp'
                || $name === 'Группа WhatsApp'
                || strpos($name, 'WhatsApp •••• ') === 0
                || preg_match('/^WHATSAPP-PERSONAL [0-9a-f]{6}$/i', $name) === 1;
        }

        return false;
    }

    private function shouldKeepStoredProfileUrl(string $provider_code, string $incoming_url, string $stored_url): bool {
        if ($stored_url === '' || $provider_code !== 'vk') {
            return false;
        }

        return strpos($incoming_url, 'https://vk.com/id') === 0 && strpos($stored_url, 'https://vk.com/id') !== 0;
    }

    private function providerLabel(string $provider_code): string {
        return ChannelCatalog::label($provider_code);
    }

    private function triggerAiIfNeeded(int $chat_id, int $message_id, string $provider_code): void {
        $model_setting = new Setting($this->registry);
        if ($model_setting->get('chat_ai_status', '0') !== '1') {
            return;
        }

        $ai_guard = new AiReplyEligibilityGuard($this->registry);
        $eligibility = $ai_guard->checkChatId($chat_id);
        if (empty($eligibility['allowed'])) {
            $ai_guard->logSkip($chat_id, $message_id, $provider_code, 'inbound_schedule', $eligibility);
            return;
        }

        $timing_policy = new AiResponseTimingPolicy($this->registry);
        $delay_seconds = $timing_policy->initialQueueDelaySeconds();

        $model_session = new Session($this->registry);
        if ($delay_seconds === 0) {
            $model_session->updateSession($chat_id, ['ai_typing_expire' => time() + 25]);
        }

        $queue = new QueueManager($this->registry);
        $job_id = $queue->push('messenger_ai_tick', [
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'source_provider' => $provider_code,
        ], $delay_seconds, 2);

        $this->logger->info('ai_tick_scheduled', [
            'provider' => $provider_code,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'delay_seconds' => $delay_seconds,
            'response_timing_mode' => $timing_policy->mode(),
            'job_id' => $job_id,
        ]);
    }

    private function safeError(string $error): string {
        $error = strip_tags($error);
        $error = preg_replace('/\s+/u', '_', $error);
        $error = preg_replace('/[^a-z0-9_:-]/i', '_', (string)$error);
        return substr(trim((string)$error, '_'), 0, 120);
    }
}
