<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AppClock;
use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\AttachmentStorage;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Model\Message;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\Session;
use EasyChat\Model\Setting;
use EasyChat\Model\Marketplace as MarketplaceModel;
use EasyChat\Model\OutboundContact;
use EasyChat\Model\SmsJob;
use EasyChat\Library\Messenger\Marketplace\MarketplaceCredentialsVault;








class OutboundService {

    private $registry;
    private $logger;

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

    public function sendMessageId(int $message_id, bool $from_queue = false): array {
        $model_message = new Message($this->registry);
        $message = $model_message->getMessageById($message_id);
        if (!$message) {
            return ['success' => false, 'error' => 'message_not_found'];
        }

        return $this->sendStoredMessage($message, $from_queue, $model_message);
    }

    public function sendStoredMessage(array $message, bool $from_queue = false, ?Message $model_message = null): array {
        $chat_id = (int)($message['chat_id'] ?? 0);
        $message_id = (int)($message['message_id'] ?? 0);
        if ($chat_id <= 0 || $message_id <= 0) {
            return ['success' => false, 'error' => 'message_invalid'];
        }

        $model_message = $model_message ?: new Message($this->registry);
        $model_session = new Session($this->registry);
        $chat = $model_session->getSessionById($chat_id);
        if (!$chat || ($chat['source_provider'] ?? 'widget') === 'widget') {
            return ['success' => true, 'skipped' => 'widget_chat'];
        }

        $provider_code = strtolower((string)$chat['source_provider']);
        $provider = ProviderRegistry::get($provider_code, $this->registry);
        if (!$provider) {
            return ['success' => false, 'error' => 'provider_unknown'];
        }
        $this->logger = new MessengerLogger($this->registry, $provider_code);

        $model_messenger = new MessengerModel($this->registry);
        $channel_id = (int)($chat['source_channel_id'] ?? 0);
        $channel = $channel_id > 0 ? $model_messenger->getChannelById($channel_id) : $model_messenger->getChannelByProvider($provider_code);
        if (!$channel) {
            return ['success' => false, 'error' => 'channel_inactive'];
        }

         
         
         
         
        $model_message->updateExternalMessageSource($message_id, $provider_code, (int)$channel['channel_id']);

        $contact = $model_messenger->getContactByChat((int)$channel['channel_id'], $chat_id);
        if (!is_array($contact)) {
            $contact = [];
        }
        $dedupe_key = 'out_' . $message_id;
        $delivery = $model_messenger->findDeliveryByDedupe((int)$channel['channel_id'], $dedupe_key);
        $sms_job = [];
        $direct_sms_deadline = 0;
        if ($provider_code === SmsProvider::PROVIDER) {
            $sms_job = (new SmsJob($this->registry))->getByMessageOrDelivery(
                $message_id,
                (int)($delivery['delivery_id'] ?? 0)
            );
            $direct_sms_deadline = (new DirectSmsDispatchOutbox($this->registry))->deadlineForMessage($message_id);

             
             
             
            if (!$sms_job && $delivery
                && (string)($delivery['status'] ?? '') === 'failed'
                && (string)($delivery['last_error'] ?? '') === 'deadline_expired') {
                return [
                    'success' => false,
                    'terminal' => true,
                    'retryable' => false,
                    'error' => 'deadline_expired',
                    'delivery_id' => (int)$delivery['delivery_id'],
                ];
            }
            if (!$sms_job && $direct_sms_deadline > 0 && $direct_sms_deadline <= time()) {
                $job_model = new SmsJob($this->registry);
                $dispatch_lock = '';
                $phone = SmsPhoneNumber::normalize((string)($contact['external_user_id'] ?? ''));
                try {
                    if ($phone !== '') {
                        $dispatch_lock = $job_model->acquirePhoneDispatchLock($phone, 2);
                        if ($dispatch_lock === '') {
                            return [
                                'success' => false,
                                'terminal' => false,
                                'retryable' => true,
                                'error' => 'sms_dispatch_busy',
                            ];
                        }
                    }
                     
                     
                     
                    $sms_job = $job_model->getByMessageOrDelivery(
                        $message_id,
                        (int)($delivery['delivery_id'] ?? 0)
                    );
                    if (!$sms_job) {
                        $delivery_id = $this->projectExpiredDirectSms(
                            $model_messenger,
                            $delivery,
                            $channel,
                            $contact,
                            $message,
                            $message_id,
                            $dedupe_key
                        );
                        return [
                            'success' => false,
                            'terminal' => true,
                            'retryable' => false,
                            'error' => 'deadline_expired',
                            'delivery_id' => $delivery_id,
                        ];
                    }
                } catch (\Throwable $e) {
                    return [
                        'success' => false,
                        'terminal' => false,
                        'retryable' => true,
                        'error' => 'sms_dispatch_storage_failed',
                    ];
                } finally {
                    if ($dispatch_lock !== '') {
                        $job_model->releaseRateLock($dispatch_lock);
                    }
                }
            }
        }

        if (($channel['status'] ?? '') !== 'active') {
            return ['success' => false, 'error' => 'channel_inactive'];
        }
        if (!$contact || empty($contact['external_user_id'])) {
            return ['success' => false, 'error' => 'contact_missing'];
        }
        $contact_metadata = json_decode((string)($contact['metadata_json'] ?? ''), true);
        if (!is_array($contact_metadata)) {
            $contact_metadata = [];
        }
        $reply_capability = (new ReplyCapability($this->registry))->forChatRow(array_merge($chat, [
            'contact_metadata_json' => (string)($contact['metadata_json'] ?? ''),
        ]));
        if (empty($reply_capability['can_reply'])) {
            $this->logger->warning('outbound_send_blocked_read_only', [
                'provider' => $provider_code,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'reason' => (string)($reply_capability['reason'] ?? ''),
                'chat_type' => (string)($reply_capability['chat_type'] ?? ''),
            ]);
            return ['success' => false, 'error' => 'reply_not_allowed', 'reply_capability' => $reply_capability];
        }

        if ($delivery && $this->isAlreadyDelivered($delivery, $provider_code)) {
            if (($delivery['status'] ?? '') !== 'sent') {
                $model_messenger->updateDelivery((int)$delivery['delivery_id'], [
                    'status' => 'sent',
                    'last_error' => '',
                ]);
            }
            return ['success' => true, 'skipped' => 'already_sent'];
        }
        if ($delivery && ($delivery['status'] ?? '') === 'pending' && !$from_queue) {
            if (!$this->shouldResumePendingSmsDelivery($provider_code, $delivery, $sms_job)) {
                $result = ['success' => true, 'skipped' => 'already_scheduled'];
                if ($sms_job) {
                    $job_id = (int)$sms_job['job_id'];
                    $result['job_id'] = $job_id;
                    try {
                         
                         
                         
                        $wake = (new SmsOutboundService($this->registry))->notifyExistingJob($job_id);
                        $result['push_sent'] = (int)($wake['push_sent'] ?? 0);
                        if (!empty($wake['deferred'])) $result['push_deferred'] = 1;
                        if (!empty($wake['retry_after'])) $result['push_retry_after'] = (int)$wake['retry_after'];
                        if (!empty($wake['terminal'])) {
                            $fresh_job = (new SmsJob($this->registry))->getById($job_id);
                            if ($fresh_job) {
                                (new SmsDeliveryProjector($this->registry))->syncFromSmsJob($fresh_job);
                            }
                            if ((string)($wake['error'] ?? '') === 'deadline_expired') {
                                return [
                                    'success' => false,
                                    'terminal' => true,
                                    'retryable' => false,
                                    'error' => 'deadline_expired',
                                    'job_id' => $job_id,
                                    'delivery_id' => (int)($delivery['delivery_id'] ?? 0),
                                ];
                            }
                        }
                    } catch (\Throwable $e) {
                         
                         
                        $result['push_deferred'] = 1;
                        $this->logger->warning('sms_existing_job_wakeup_failed', [
                            'chat_id' => $chat_id,
                            'message_id' => $message_id,
                            'job_id' => $job_id,
                        ]);
                    }
                }
                return $result;
            }
        }

        if (!$delivery) {
            $delivery_id = $model_messenger->createDelivery([
                'channel_id' => (int)$channel['channel_id'],
                'contact_id' => (int)$contact['contact_id'],
                'message_id' => $message_id,
                'direction' => 'out',
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', (string)$message_id . ':' . (string)$message['text'] . ':' . (string)$message['image']),
                'status' => 'pending',
            ]);
        } else {
            $delivery_id = (int)$delivery['delivery_id'];
        }

         
         
         
        if ($provider_code === EmailProvider::PROVIDER && !$from_queue) {
            $job_id = (new QueueManager($this->registry))->pushUnique(
                'messenger_outbound',
                ['message_id'=>$message_id],
                'email_outbound_' . $message_id,
                0,
                5
            );
            return ['success'=>true,'status'=>'queued','queued'=>true,'job_id'=>$job_id,'delivery_id'=>$delivery_id];
        }

        $credentials = json_decode((string)$channel['credentials_json'], true);
        if (!is_array($credentials)) {
            $credentials = [];
        }
        $marketplace_thread = [];
        if (in_array($provider_code, [WildberriesProvider::PROVIDER, OzonProvider::PROVIDER, YandexMarketProvider::PROVIDER], true)) {
            $marketplace_thread = (new MarketplaceModel($this->registry))->getThreadByChat($chat_id);
            if (!$marketplace_thread || (string)($marketplace_thread['conversation_kind'] ?? '') !== 'buyer_chat') {
                $model_messenger->updateDelivery($delivery_id, ['status' => 'failed', 'last_error' => 'marketplace_thread_missing']);
                return ['success' => false, 'error' => 'marketplace_thread_missing'];
            }
        }
        $token = $this->resolveProviderToken($provider_code, $credentials, $channel);
        if ($token === '') {
            $model_messenger->updateDelivery($delivery_id, ['status' => 'failed', 'last_error' => 'token_missing']);
            return ['success' => false, 'error' => 'token_missing'];
        }

        $storage = new AttachmentStorage($this->registry);
        $attachments = [];
        $grouped = $storage->getAttachmentsByMessageIds([$message_id]);
        foreach ($grouped[$message_id] ?? [] as $attachment) {
            $attachment['path'] = $storage->chatFilePath($chat_id, (string)$attachment['file_name']);
            $attachments[] = $attachment;
        }
        if (!$attachments && !empty($message['image'])) {
            $attachments[] = [
                'file_name' => $message['image'],
                'path' => $storage->chatFilePath($chat_id, (string)$message['image']),
            ];
        }
        if ($attachments && array_key_exists('can_send_attachments', $reply_capability)
            && empty($reply_capability['can_send_attachments'])) {
            $model_messenger->updateDelivery($delivery_id, [
                'status' => 'failed',
                'last_error' => 'marketplace_attachment_permission_missing',
            ]);
            return ['success' => false, 'error' => 'marketplace_attachment_permission_missing'];
        }

        $text = trim(urldecode((string)$message['text']));
        $attachment_plan = $this->attachmentsForProvider($provider_code, $attachments);
        if (in_array($provider_code, ['avito', 'youla'], true) && !empty($attachment_plan['sendable'])) {
            $attachment_plan['sendable'] = $this->withMarketplacePublicAttachmentUrls($chat_id, $attachment_plan['sendable']);
        }
        if (!empty($attachment_plan['unsupported']) && empty($attachment_plan['sendable']) && $text === '') {
            $model_messenger->updateDelivery($delivery_id, [
                'status' => 'failed',
                'last_error' => 'attachment_type_unsupported',
                'attempts' => $from_queue ? 1 : 0,
            ]);
            $this->logger->warning('outbound_attachment_unsupported', [
                'provider' => $provider_code,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'unsupported_count' => count($attachment_plan['unsupported']),
            ]);
            return [
                'success' => false,
                'error' => 'attachment_type_unsupported',
                'unsupported_attachments' => count($attachment_plan['unsupported']),
            ];
        }
        if (!empty($attachment_plan['unsupported'])) {
            $this->logger->warning('outbound_attachment_skipped', [
                'provider' => $provider_code,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'unsupported_count' => count($attachment_plan['unsupported']),
            ]);
        }

        $this->logger->info('outbound_send_attempt', [
            'provider' => $provider_code,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'attachment_count' => count($attachment_plan['sendable']),
            'unsupported_attachment_count' => count($attachment_plan['unsupported']),
        ]);

        $provider_options = [
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'manager_id' => (int)($message['manager_id'] ?? 0),
            'delivery_id' => $delivery_id,
            'dedupe_key' => $dedupe_key,
            'external_chat_id' => (string)($contact['external_chat_id'] ?? ''),
            'whatsapp_phone_jid' => (string)($contact_metadata['whatsapp_phone_jid'] ?? $contact_metadata['provider_phone_jid'] ?? $contact_metadata['provider_confirmed_phone'] ?? ''),
            'counterparty_user_id' => (string)($contact_metadata['counterparty_sender_id'] ?? $contact_metadata['sender_id'] ?? ''),
            'channel_id' => (int)$channel['channel_id'],
            'vk_group_id' => (string)($credentials['vk_group_id'] ?? ''),
            'whatsapp_account_hash' => (string)($contact_metadata['whatsapp_account_hash'] ?? ''),
        ];
        if ($provider_code === EmailProvider::PROVIDER) {
            $provider_options['subject'] = (string)($contact_metadata['subject'] ?? '');
            $provider_options['in_reply_to'] = (string)($contact_metadata['message_id'] ?? '');
            $references = $contact_metadata['references'] ?? array();
            if (is_string($references)) {
                preg_match_all('/<([^<>\r\n]{1,512})>/', $references, $matches);
                $references = (array)($matches[1] ?? array());
            }
            $provider_options['references'] = is_array($references) ? $references : array();
        }
        if ($provider_code === SmsProvider::PROVIDER && $direct_sms_deadline > 0) {
            $provider_options['expires_at'] = gmdate('Y-m-d H:i:s', $direct_sms_deadline);
        }
        if ($marketplace_thread) {
            $thread_capabilities = json_decode((string)($marketplace_thread['capabilities_json'] ?? ''), true);
            $thread_capabilities = is_array($thread_capabilities) ? $thread_capabilities : [];
            $provider_options['external_thread_id'] = (string)$marketplace_thread['external_thread_id'];
            $provider_options['external_chat_id'] = (string)($marketplace_thread['external_chat_id'] ?? $marketplace_thread['external_thread_id']);
            $provider_options['reply_secret'] = (new MarketplaceCredentialsVault($this->registry))->openThreadSecret(
                (string)($marketplace_thread['reply_secret'] ?? '')
            );
            $provider_options['chat_type'] = (string)($thread_capabilities['chat_type'] ?? 'buyer_seller');
        }
        $formatted_text = OutboundTextFormatter::forProvider($provider_code, $text);
        $text = (string)$formatted_text['text'];
        $provider_options['text_format'] = (string)$formatted_text['format'];
        $result = $provider->sendMessage(
            $token,
            (string)$contact['external_user_id'],
            $text,
            $attachment_plan['sendable'],
            $provider_options
        );
        if ($provider_code === AvitoProvider::PROVIDER) {
            $avito_incidents = new AvitoAccessIncidentService($this->registry);
            if (empty($result['success'])) {
                $avito_incidents->observeFailure(
                    (int)$channel['channel_id'],
                    (int)($contact['contact_id'] ?? 0),
                    $chat_id,
                    (string)($contact['external_chat_id'] ?? ''),
                    $result
                );
            } else {
                $avito_incidents->observeSuccess(
                    (int)$channel['channel_id'],
                    (int)($contact['contact_id'] ?? 0),
                    $chat_id,
                    (string)($contact['external_chat_id'] ?? '')
                );
            }
        }
        if (empty($result['success'])) {
            $base_error = trim((string)($result['error'] ?? 'send_failed'));
            if ($base_error === '') {
                $base_error = 'send_failed';
            }
            $error = $base_error;
            $provider_error = $this->safeProviderError($result);
            $partial_delivery = !empty($result['partial_delivery']);
            $outcome_unknown = !$partial_delivery && !empty($result['outcome_unknown']);
            $safe_error = $outcome_unknown
                ? (in_array($provider_code, [WhatsAppPersonalProvider::PROVIDER, EmailProvider::PROVIDER], true)
                    ? 'delivery_outcome_unknown'
                    : 'marketplace_delivery_outcome_unknown')
                : $provider_error;
            $partial_external_message_id = $partial_delivery
                ? trim((string)($result['external_message_id'] ?? ''))
                : '';
            $model_messenger->updateDelivery($delivery_id, [
                'status' => 'failed',
                'external_message_id' => $partial_external_message_id,
                'last_error' => $safe_error,
                'attempts' => $from_queue ? 1 : 0,
            ]);
            if ($provider_code === SmsProvider::PROVIDER && $base_error === 'deadline_expired') {
                 
                 
                 
                (new OutboundContact($this->registry))->updateSmsStatusByMessageId(
                    $message_id,
                    'failed',
                    'deadline_expired'
                );
            }
            if ($partial_delivery) {
                $this->rememberProviderMessageIds(
                    $model_messenger,
                    (int)$channel['channel_id'],
                    (int)($contact['contact_id'] ?? 0),
                    $message_id,
                    $delivery_id,
                    $result['external_message_ids'] ?? []
                );
            }
            if ($partial_external_message_id !== '') {
                 
                 
                $model_message->updateExternalMessageId($message_id, $partial_external_message_id, $provider_code, (int)$channel['channel_id']);
            }
            $retryable = !$partial_delivery
                && !$outcome_unknown
                && (!empty($result['retryable']) || $this->isRetryableError($error, $provider_code));
             
             
             
             
            $retry_scheduled = $partial_delivery
                ? false
                : ($retryable
                    ? $this->scheduleRetry(
                        $message_id,
                        $from_queue,
                        max(5, min(3600, (int)($result['retry_after_seconds'] ?? 60)))
                    )
                    : false);
            $retry_without_attempt = $from_queue
                && $retryable
                && ($base_error === 'rate_limited' || !empty($result['retry_without_attempt']));
            if ($retry_scheduled || $retry_without_attempt) {
                 
                 
                 
                $model_messenger->updateDelivery($delivery_id, [
                    'status' => 'pending',
                    'last_error' => $safe_error,
                ]);
            }
            $this->logger->error('outbound_send_failed', [
                'provider' => $provider_code,
                'chat_id' => $chat_id,
                'message_id' => $message_id,
                'http_code' => (int)($result['http_code'] ?? 0),
                'error' => $safe_error,
                'provider_error' => $provider_error,
                'provider_step' => (string)($result['vk_step'] ?? ''),
                'provider_details' => substr((string)($result['details'] ?? ''), 0, 180),
                'exception_class' => (string)($result['exception_class'] ?? ''),
                'provider_chat_hash' => (string)($result['chat_hash'] ?? ''),
                'partial_delivery' => $partial_delivery ? 1 : 0,
                'outcome_unknown' => $outcome_unknown ? 1 : 0,
                'sent_attachment_count' => (int)($result['sent_attachment_count'] ?? 0),
            ]);
            if ($outcome_unknown) {
                $result['error'] = $safe_error;
                $result['provider_error'] = $provider_error;
            }
            $result['outcome_unknown'] = $outcome_unknown;
            $result['retryable'] = $retryable;
            $result['retry_scheduled'] = $retry_scheduled;
            $result['retry_without_attempt'] = $retry_without_attempt;
            return $result;
        }

        $external_message_id = (string)($result['external_message_id'] ?? '');
        if ($provider_code === SmsProvider::PROVIDER && (string)($result['status'] ?? '') === 'queued') {
            $model_messenger->updateDelivery($delivery_id, [
                'status' => 'pending',
                'external_message_id' => $external_message_id,
                'last_error' => '',
            ]);
            if ($external_message_id !== '') {
                $model_message->updateExternalMessageId($message_id, $external_message_id, $provider_code, (int)$channel['channel_id']);
            }
            $model_messenger->updateChannel((int)$channel['channel_id'], [
                'last_outbound_at' => (new AppClock($this->registry))->nowSql(),
                'last_error' => '',
            ]);
            return $result;
        }
        $model_messenger->updateDelivery($delivery_id, [
            'status' => 'sent',
            'external_message_id' => $external_message_id,
            'provider_cursor' => max(0, (int)($result['provider_cursor'] ?? 0)),
             
             
             
            'last_error' => $provider_code === YandexMarketProvider::PROVIDER
                && $external_message_id === ''
                ? 'awaiting_provider_echo'
                : '',
        ]);
        if ($provider_code === 'max' && (int)($result['provider_cursor'] ?? 0) > 0) {
            (new MessageReadReceiptService($this->registry))->projectMaxMessage(
                (int)$channel['channel_id'],
                (string)($contact['external_chat_id'] ?? ''),
                $message_id,
                (int)$result['provider_cursor']
            );
        }
        if ($provider_code === WhatsAppPersonalProvider::PROVIDER && $external_message_id !== '') {
            (new MessageReadReceiptService($this->registry))->projectWhatsAppMessage(
                (int)$channel['channel_id'],
                $external_message_id,
                $message_id
            );
        }
        $this->rememberProviderMessageIds(
            $model_messenger,
            (int)$channel['channel_id'],
            (int)($contact['contact_id'] ?? 0),
            $message_id,
            $delivery_id,
            $result['external_message_ids'] ?? []
        );
        $this->reconcileProviderEchoMessages(
            $model_message,
            $provider_code,
            (int)$channel['channel_id'],
            $message_id,
            $chat_id,
            $result['external_message_ids'] ?? []
        );
        if ($external_message_id !== '') {
            $model_message->updateExternalMessageId($message_id, $external_message_id, $provider_code, (int)$channel['channel_id']);
        }
        $channel_update = [
            'last_outbound_at' => (new AppClock($this->registry))->nowSql(),
        ];
         
         
        if (!in_array($provider_code, ['max', WhatsAppPersonalProvider::PROVIDER], true)) {
            $channel_update['last_error'] = '';
        }
        $model_messenger->updateChannel((int)$channel['channel_id'], $channel_update);

        $this->logger->info('outbound_send_ok', [
            'provider' => $provider_code,
            'chat_id' => $chat_id,
            'message_id' => $message_id,
            'http_code' => (int)($result['http_code'] ?? 0),
            'skipped_attachment_count' => count($attachment_plan['unsupported']),
        ]);

        if (!empty($attachment_plan['unsupported'])) {
            $result['skipped_attachments'] = count($attachment_plan['unsupported']);
            $result['skipped_attachment_reason'] = 'attachment_type_unsupported';
        }

        return $result;
    }

     
    private function projectExpiredDirectSms(
        MessengerModel $model_messenger,
        array $delivery,
        array $channel,
        array $contact,
        array $message,
        int $message_id,
        string $dedupe_key
    ): int {
        $delivery_id = (int)($delivery['delivery_id'] ?? 0);
        if ($delivery_id <= 0) {
            $delivery_id = $model_messenger->createDelivery([
                'channel_id' => (int)$channel['channel_id'],
                'contact_id' => (int)($contact['contact_id'] ?? 0),
                'message_id' => $message_id,
                'direction' => 'out',
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', (string)$message_id . ':' . (string)($message['text'] ?? '') . ':' . (string)($message['image'] ?? '')),
                'status' => 'failed',
                'last_error' => 'deadline_expired',
            ]);
        } else {
            $model_messenger->updateSmsDeliveryMonotonic($delivery_id, 'failed', 'deadline_expired');
        }
        (new OutboundContact($this->registry))->updateSmsStatusByMessageId(
            $message_id,
            'failed',
            'deadline_expired'
        );
        $this->logger->warning('sms_dispatch_deadline_expired', [
            'chat_id' => (int)($message['chat_id'] ?? 0),
            'message_id' => $message_id,
            'delivery_id' => $delivery_id,
        ]);
        return $delivery_id;
    }

     
    private function shouldResumePendingSmsDelivery(string $provider_code, array $delivery, array $sms_job): bool {
        return $provider_code === SmsProvider::PROVIDER
            && (string)($delivery['status'] ?? '') === 'pending'
            && empty($sms_job);
    }

    private function rememberProviderMessageIds(
        MessengerModel $model_messenger,
        int $channel_id,
        int $contact_id,
        int $message_id,
        int $delivery_id,
        array $external_message_ids
    ): void {
        foreach (array_values(array_unique(array_filter(array_map('strval', $external_message_ids)))) as $external_message_id) {
            $external_message_id = trim($external_message_id);
            if ($external_message_id === '' || $model_messenger->findDeliveryByExternalMessageId($channel_id, $external_message_id)) {
                continue;
            }
            $dedupe_key = 'provider-message:' . substr(hash('sha256', $channel_id . ':' . $external_message_id), 0, 32);
            $model_messenger->rememberSentProviderMessageId([
                'channel_id' => $channel_id,
                'contact_id' => $contact_id,
                'message_id' => $message_id,
                'direction' => 'out',
                'external_message_id' => $external_message_id,
                'dedupe_key' => $dedupe_key,
                'payload_hash' => hash('sha256', $message_id . ':' . $delivery_id . ':' . $external_message_id),
                'status' => 'sent',
            ]);
        }
    }

    private function reconcileProviderEchoMessages(
        Message $model_message,
        string $provider_code,
        int $channel_id,
        int $message_id,
        int $chat_id,
        array $external_message_ids
    ): void {
        $storage = new AttachmentStorage($this->registry);
        foreach (array_values(array_unique(array_filter(array_map('strval', $external_message_ids)))) as $external_message_id) {
            foreach ($model_message->findProviderEchoMessages($provider_code, $channel_id, $external_message_id, $message_id) as $echo) {
                $echo_message_id = (int)($echo['message_id'] ?? 0);
                $echo_chat_id = (int)($echo['chat_id'] ?? $chat_id);
                if ($echo_message_id <= 0 || $echo_chat_id !== $chat_id) {
                    continue;
                }
                $storage->deleteMessageAttachments($echo_message_id, $echo_chat_id);
                $model_message->mergeProviderEchoMessage($echo_message_id, $message_id);
            }
        }
    }

    private function attachmentsForProvider(string $provider_code, array $attachments): array {
        $capabilities = AttachmentCapabilities::forProvider($provider_code);
        $sendable = [];
        $unsupported = [];
        $has_document = false;
        $has_image = false;

        foreach ($attachments as $attachment) {
            $kind = $this->attachmentKind($attachment);
            if ($kind === 'document') {
                if (!empty($capabilities['documents']) && $this->attachmentFormatAllowed($attachment, $capabilities, 'document')) {
                    $sendable[] = $attachment;
                    $has_document = true;
                } else {
                    $unsupported[] = $attachment;
                }
                continue;
            }

            if (!empty($capabilities['images']) && $this->attachmentFormatAllowed($attachment, $capabilities, 'image')) {
                $sendable[] = $attachment;
                $has_image = true;
            } else {
                $unsupported[] = $attachment;
            }
        }

        if ($has_document && $has_image && array_key_exists('mixed_image_document', $capabilities) && empty($capabilities['mixed_image_document'])) {
            $documents = [];
            $images = [];
            foreach ($sendable as $attachment) {
                if ($this->attachmentKind($attachment) === 'document') {
                    $documents[] = $attachment;
                } else {
                    $images[] = $attachment;
                }
            }
             
             
             
             
            $unsupported = array_merge($unsupported, $images);
            $sendable = $documents;
        }

        if (!empty($capabilities['max_documents'])) {
            $limited = [];
            $documents_seen = 0;
            foreach ($sendable as $attachment) {
                if ($this->attachmentKind($attachment) === 'document') {
                    $documents_seen++;
                    if ($documents_seen > (int)$capabilities['max_documents']) {
                        $unsupported[] = $attachment;
                        continue;
                    }
                }
                $limited[] = $attachment;
            }
            $sendable = $limited;
        }

        if (!empty($capabilities['max_attachments']) && count($sendable) > (int)$capabilities['max_attachments']) {
            $unsupported = array_merge($unsupported, array_slice($sendable, (int)$capabilities['max_attachments']));
            $sendable = array_slice($sendable, 0, (int)$capabilities['max_attachments']);
        }

        return ['sendable' => $sendable, 'unsupported' => $unsupported];
    }

    private function attachmentKind(array $attachment): string {
        $meta = json_decode((string)($attachment['meta_json'] ?? ''), true);
        if (is_array($meta) && (string)($meta['kind'] ?? '') === 'document') {
            return 'document';
        }

        $filename = (string)($attachment['file_name'] ?? $attachment['filename'] ?? '');
        $mime = (string)($attachment['mime_type'] ?? $attachment['mime'] ?? '');
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        if ($extension === '' && $mime !== '') {
            $extension = AttachmentPolicy::extensionForMime($mime);
        }

        return AttachmentPolicy::isDocumentExtension($extension) || AttachmentPolicy::isDocumentMime($mime)
            ? 'document'
            : 'image';
    }

    private function attachmentFormatAllowed(array $attachment, array $capabilities, string $kind): bool {
        $filename = (string)($attachment['original_name'] ?? $attachment['display_name'] ?? $attachment['file_name'] ?? $attachment['filename'] ?? '');
        $mime = (string)($attachment['mime_type'] ?? $attachment['mime'] ?? '');
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
        if ($extension === '') {
            $extension = AttachmentPolicy::extensionForMime($mime);
        }
        $key = $kind === 'document' ? 'document_extensions' : 'image_extensions';
        $fallback = $kind === 'document'
            ? AttachmentPolicy::allowedDocumentExtensions()
            : AttachmentPolicy::allowedImageExtensions();
        $allowed = is_array($capabilities[$key] ?? null) ? $capabilities[$key] : $fallback;
        $allowed = array_map(function ($value): string {
            return strtolower(ltrim(trim((string)$value), '.'));
        }, $allowed);

        return $extension !== '' && in_array($extension, $allowed, true);
    }

    private function withMarketplacePublicAttachmentUrls(int $chat_id, array $attachments): array {
        $model_setting = new Setting($this->registry);
        $install_secret = (string)$model_setting->get('chat_install_secret', '');
        if (trim($install_secret) === '') {
            return array_map(function ($attachment) {
                $attachment['public_url'] = '';
                return $attachment;
            }, $attachments);
        }

        return array_map(function ($attachment) use ($chat_id, $install_secret) {
            $filename = (string)($attachment['file_name'] ?? $attachment['filename'] ?? '');
            if ($this->attachmentKind($attachment) === 'document') {
                $meta = json_decode((string)($attachment['meta_json'] ?? ''), true);
                if (!is_array($meta)) {
                    $meta = [];
                }
                $display_name = (string)($meta['display_name'] ?? $filename);
                $attachment['public_url'] = OutboundAttachmentLink::documentUrl($chat_id, $filename, $display_name, $install_secret);
                $attachment['public_url_kind'] = 'document';
                $attachment = $this->withPdfPreviewImage($chat_id, $attachment, $meta);
                return $attachment;
            }

            $attachment['public_url'] = OutboundAttachmentLink::imageUrl($chat_id, $filename, $install_secret);
            $attachment['public_url_kind'] = 'image';
            return $attachment;
        }, $attachments);
    }

    private function withPdfPreviewImage(int $chat_id, array $attachment, array $meta): array {
        $preview = isset($meta['pdf_preview']) && is_array($meta['pdf_preview']) ? $meta['pdf_preview'] : [];
        $preview_file = basename((string)($preview['file'] ?? ''));
        if ($preview_file === '') {
            return $attachment;
        }

        $storage = new AttachmentStorage($this->registry);
        $preview_path = $storage->chatFilePath($chat_id, $preview_file);
        if (!OutboundAttachmentLink::isAllowedImagePath($preview_path)) {
            return $attachment;
        }

        $attachment['pdf_preview_image'] = [
            'file_name' => $preview_file,
            'filename' => $preview_file,
            'path' => $preview_path,
            'mime_type' => (string)($preview['mime'] ?? AttachmentPolicy::detectMime($preview_path, strtolower(pathinfo($preview_file, PATHINFO_EXTENSION)))),
        ];
        return $attachment;
    }

    private function isAlreadyDelivered(array $delivery, string $provider_code = ''): bool {
        if (($delivery['status'] ?? '') === 'sent') {
            return true;
        }

         
         
        if ($provider_code === SmsProvider::PROVIDER) return false;

         
         
         
         
        return trim((string)($delivery['external_message_id'] ?? '')) !== '';
    }

    private function safeProviderError(array $result): string {
        $error = trim((string)($result['error'] ?? 'send_failed'));
        if ($error === '') {
            $error = 'send_failed';
        }

        $step = trim((string)($result['vk_step'] ?? ''));
        if ($step !== '') {
            $error .= ':' . preg_replace('/[^a-z0-9_\\-]/i', '', $step);
        }

         
         
         
        $details = trim((string)($result['details'] ?? ''));
        if (preg_match('/\\Awhatsapp_ack_[a-z0-9._-]{1,64}\\z/i', $details) === 1) {
            $error .= ':' . substr($details, strlen('whatsapp_ack_'));
        }

        return substr($error, 0, 120);
    }

    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 resolveProviderToken(string $provider_code, array $credentials, array $channel = []): string {
        if ($provider_code === SmsProvider::PROVIDER) {
            return 'local_sms';
        }
        if ($provider_code === EmailProvider::PROVIDER) {
            $settings = json_decode((string)($channel['settings_json'] ?? '{}'), true);
            $settings = is_array($settings) ? $settings : [];
            $username = $this->decryptSecret((string)($credentials['username'] ?? ''));
            $password = $this->decryptSecret((string)($credentials['password'] ?? ''));
            if ($username === '' || $password === '') return '';
            $smtp_bz_api_key = $this->decryptSecret((string)($credentials['smtp_bz_api_key'] ?? ''));
            return (string)json_encode(array_merge($settings, ['username'=>$username,'password'=>$password,'smtp_bz_api_key'=>$smtp_bz_api_key]), JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        }

        if ($provider_code === 'youla') {
            $token = $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''));
            if ($token !== '') {
                return $token;
            }

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

        if ($provider_code === 'max') return '';

        if ($provider_code === WhatsAppPersonalProvider::PROVIDER) {
            $settings = json_decode((string)($channel['settings_json'] ?? '{}'), true);
            $settings = is_array($settings) ? $settings : [];
            $generation = max(0, (int)($settings['session_generation'] ?? 0));
            return $generation > 0 ? (string)json_encode([
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'generation' => $generation,
            ], JSON_UNESCAPED_SLASHES) : '';
        }

        if (in_array($provider_code, [WildberriesProvider::PROVIDER, OzonProvider::PROVIDER, YandexMarketProvider::PROVIDER], true)) {
            $provider = ProviderRegistry::get($provider_code, $this->registry);
            if (!$provider || !method_exists($provider, 'marketplaceCapabilities')) {
                return '';
            }
            $capabilities = $provider->marketplaceCapabilities();
            $opened = (new MarketplaceCredentialsVault($this->registry))->open(
                $credentials,
                is_array($capabilities['secret_fields'] ?? null) ? $capabilities['secret_fields'] : []
            );
            $encoded = json_encode($opened, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            return is_string($encoded) ? $encoded : '';
        }

        $token = $provider_code === 'avito'
            ? $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''))
            : $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
        if ($token !== '' || $provider_code !== 'avito') {
            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 scheduleRetry(int $message_id, bool $from_queue, int $delay = 60): bool {
         
         
         
        if ($from_queue) {
            return false;
        }

        $queue = new QueueManager($this->registry);
        $queue->push(
            'messenger_outbound',
            ['message_id' => $message_id],
            max(5, min(3600, $delay)),
            5
        );
        return true;
    }

    private function isRetryableError(string $error, string $provider_code): bool {
        if ($provider_code === TelegramProvider::PROVIDER) {
             
             
             
             
            return stripos($error, 'Too Many Requests') !== false
                || stripos($error, 'retry after') !== false;
        }

        if (in_array($error, [
            'attachment.not.ready',
            'attachment_not_ready',
            'api_http_error',
            'api_request_failed',
            'upload_failed',
            'vk_api_rate_limited',
            'vk_bulk_upload_payload_unsupported',
            'rate_limited',
            'marketplace_unavailable',
            'transport_failed',
        ], true)) {
            return true;
        }

        return in_array($error, ['vk_api_error_6', 'vk_api_error_29'], true);
    }
}
