<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Messenger\Marketplace\AbstractMarketplaceProvider;
use EasyChat\Library\Messenger\Marketplace\MarketplaceHttpTransport;
use EasyChat\Library\Messenger\Marketplace\MarketplaceStream;








class YandexMarketProvider extends AbstractMarketplaceProvider {
    public const PROVIDER = 'yandex_market';
    public const API_BASE = 'https://api.partner.market.yandex.ru';

    private const WRITABLE_CHAT_STATUSES = [
        'NEW', 'WAITING_FOR_CUSTOMER', 'WAITING_FOR_PARTNER',
    ];

    public function __construct($registry = null, ?MarketplaceHttpTransport $transport = null) {
        parent::__construct(
            $registry,
            $transport ?: new MarketplaceHttpTransport(
                new ExternalHttpClient($registry),
                ['api.partner.market.yandex.ru'],
                self::PROVIDER
            )
        );
    }

    public function providerCode(): string {
        return self::PROVIDER;
    }

    public function marketplaceCapabilities(): array {
        return [
            'provider' => self::PROVIDER,
            'auth_type' => 'api_key',
            'secret_fields' => ['api_key'],
            'polling_required' => true,
             
            'webhook_supported' => true,
            'webhook_registration' => 'manual',
            'account_actions' => ['start_conversation'],
            'streams' => [
                MarketplaceStream::BUYER_CHAT => [
                    'read' => true,
                    'reply' => true,
                    'attachments_in' => true,
                    'attachments_out' => true,
                    'formats' => AttachmentPolicy::allowedAttachmentExtensions(true),
                ],
                MarketplaceStream::QUESTION => [
                    'read' => true,
                    'reply' => false,
                    'draft' => true,
                    'actions' => [
                        'yandex_question_answer',
                        'yandex_question_comment',
                        'yandex_question_edit',
                        'yandex_question_delete',
                    ],
                ],
                MarketplaceStream::REVIEW => [
                    'read' => true,
                    'reply' => false,
                    'draft' => true,
                    'actions' => [
                        'yandex_review_answer',
                        'yandex_review_comment',
                        'yandex_review_edit',
                        'yandex_review_delete',
                        'yandex_review_skip',
                    ],
                ],
            ],
        ];
    }

    public function getBotInfo(string $token): array {
        $result = $this->validateMarketplaceCredentials($this->credentials($token));
        if (empty($result['success'])) {
            return $result;
        }
        return [
            'success' => true,
            'id' => (string)($result['account']['id'] ?? ''),
            'name' => (string)($result['account']['name'] ?? 'Яндекс Маркет'),
            'username' => '',
            'capabilities' => $result['capabilities'] ?? [],
        ];
    }

    public function validateMarketplaceCredentials(array $credentials): array {
        if ($this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'api_key_required'];
        }

        $token = $this->api('POST', '/v2/auth/token', $credentials, []);
        if (empty($token['success'])) {
            return $this->failure($token, 'auth_failed');
        }
        $token_json = is_array($token['json'] ?? null) ? $token['json'] : [];
        $api_key = is_array($token_json['result']['apiKey'] ?? null)
            ? $token_json['result']['apiKey']
            : [];
        $scopes = [];
        foreach ((array)($api_key['authScopes'] ?? []) as $scope) {
             
             
             
             
            $scope = strtolower(trim((string)$scope));
            $scope = preg_replace('/[^a-z0-9]+/', '_', $scope);
            $scope = trim((string)$scope, '_');
            if ($scope !== '') {
                $scopes[$scope] = $scope;
            }
        }
        $scopes = array_values($scopes);
        $can_read = in_array('communication', $scopes, true)
            || in_array('all_methods', $scopes, true)
            || in_array('all_methods_read_only', $scopes, true);
        $can_write = in_array('communication', $scopes, true)
            || in_array('all_methods', $scopes, true);
        if (!$can_read) {
            return ['success' => false, 'error' => 'permission_denied'];
        }

        $campaigns = $this->api('GET', '/v2/campaigns', $credentials, null, ['limit' => 100]);
        if (empty($campaigns['success'])) {
            return $this->failure($campaigns, 'marketplace_account_identity_missing');
        }
        $campaign_json = is_array($campaigns['json'] ?? null) ? $campaigns['json'] : [];
        $rows = is_array($campaign_json['campaigns'] ?? null)
            ? $campaign_json['campaigns']
            : (array)($campaign_json['result']['campaigns'] ?? []);
        $businesses = [];
        $campaign_ids = [];
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $business = is_array($row['business'] ?? null) ? $row['business'] : [];
            $business_id = (int)($business['id'] ?? 0);
            if ($business_id <= 0) continue;
            $businesses[$business_id] = $this->safeString($business['name'] ?? '', 128);
            if ((int)($row['id'] ?? 0) > 0) {
                $campaign_ids[] = (int)$row['id'];
            }
        }
        if (count($businesses) !== 1) {
             
             
            return ['success' => false, 'error' => 'marketplace_account_identity_missing'];
        }
        $business_id = (int)array_key_first($businesses);
        $business_name = trim((string)$businesses[$business_id]);
        if ($business_name === '') {
            $business_name = 'Кабинет ' . $business_id;
        }

        $stream_access = [];
        foreach (array_keys($this->marketplaceCapabilities()['streams']) as $stream) {
            $stream_access[$stream] = [
                'available' => true,
                'missing_read_methods' => [],
                'action_access' => $this->streamActionAccess($stream, $can_write),
            ];
        }

        return [
            'success' => true,
            'account' => [
                'id' => (string)$business_id,
                'name' => 'Яндекс Маркет · ' . $business_name,
            ],
            'credential_values' => ['business_id' => (string)$business_id],
            'capabilities' => array_merge($this->marketplaceCapabilities(), [
                'business_id' => $business_id,
                'campaign_ids' => array_values(array_unique($campaign_ids)),
                'stream_access' => $stream_access,
                'account_action_access' => ['start_conversation' => $can_write],
                 
                 
                'token_read_only' => !$can_write,
            ]),
            'diagnostics' => [
                'token_name' => $this->safeString($api_key['name'] ?? '', 100),
                'auth_scopes' => $scopes,
                'stream_access' => $stream_access,
                'campaign_count' => count(array_unique($campaign_ids)),
            ],
        ];
    }

    public function syncWebhook(array $channel, string $token, string $webhook_url, string $secret): array {
        $credentials = $this->credentials($token);
        if ($this->apiKey($credentials) === '' || !$this->validBusinessId((string)($channel['external_account_id'] ?? ''))) {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        if (!$this->safeWebhookUrl($webhook_url)) {
            return ['success' => false, 'error' => 'webhook_url_invalid'];
        }
        return [
            'success' => true,
            'mode' => 'push_with_polling_reconciliation',
            'webhook_supported' => true,
            'registration' => 'manual',
            'webhook_url' => $webhook_url,
        ];
    }

    public function syncMarketplaceStream(string $stream, array $credentials, array $cursor, int $limit = 50): array {
        if ($this->apiKey($credentials) === '' || $this->businessId($credentials) <= 0) {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        $stream = MarketplaceStream::normalize($stream);
        $limit = max(1, min(50, $limit));
        $hints = is_array($cursor['notification_hints'] ?? null) ? $cursor['notification_hints'] : [];
        unset($cursor['notification_hints']);
        if ($hints) {
             
             
             
            $hint_result = $this->syncNotificationHints($stream, $credentials, $hints);
            if (empty($hint_result['success'])) {
                return $hint_result;
            }
            return [
                'success' => true,
                'events' => (array)($hint_result['events'] ?? []),
                'cursor' => $cursor,
                'consumed_hint_ids' => array_values(array_unique(array_map(
                    'intval',
                    (array)($hint_result['consumed_hint_ids'] ?? [])
                ))),
                'has_more' => true,
                'delay_seconds' => 1,
            ];
        }
        switch ($stream) {
            case MarketplaceStream::BUYER_CHAT:
                $result = $this->syncChats($credentials, $cursor, min(20, $limit));
                break;
            case MarketplaceStream::QUESTION:
                $result = $this->syncQuestions($credentials, $cursor, min(4, $limit));
                break;
            case MarketplaceStream::REVIEW:
                $result = $this->syncReviews($credentials, $cursor, min(4, $limit));
                break;
            default:
                return ['success' => false, 'error' => 'stream_unknown'];
        }
        return $result;
    }

    public function sendMessage(string $token, string $external_user_id, string $text, array $attachments = [], array $options = []): array {
        $credentials = $this->credentials($token);
        $chat_id = $this->positiveId($options['external_chat_id'] ?? $options['external_thread_id'] ?? '');
        if ($this->apiKey($credentials) === '' || $this->businessId($credentials) <= 0) {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        if ($chat_id <= 0) {
            return ['success' => false, 'error' => 'chat_id_missing'];
        }
        if (strtoupper((string)($options['chat_type'] ?? 'CHAT')) !== 'CHAT') {
            return ['success' => false, 'error' => 'chat_read_only'];
        }
        if (count($attachments) > 10) {
            return ['success' => false, 'error' => 'attachment_limit_exceeded'];
        }

        $parts_sent = 0;
        $text = trim($text);
        if ($text !== '') {
            if ($this->utf8Length($text) > 5000) {
                return ['success' => false, 'error' => 'message_text_too_long'];
            }
            $sent = $this->api(
                'POST',
                $this->businessPath('/v2/businesses/%d/chats/message', $credentials),
                $credentials,
                ['message' => $text],
                ['chatId' => $chat_id]
            );
            if (empty($sent['success'])) {
                return $this->writeFailure($sent, 'send_failed');
            }
            $parts_sent++;
        }

        foreach ($attachments as $attachment) {
            $path = (string)($attachment['path'] ?? '');
            if (!is_file($path)) {
                return $this->partialFailure('attachment_missing', $parts_sent);
            }
            $size = (int)filesize($path);
            if ($size <= 0 || $size > 5 * 1024 * 1024) {
                return $this->partialFailure('attachment_size_exceeded', $parts_sent);
            }
            $name = $this->safeString($attachment['original_name'] ?? basename($path), 180);
            $validation = AttachmentPolicy::validateAttachmentFile($path, $name, $size, 5, true);
            if (empty($validation['success'])) {
                return $this->partialFailure('attachment_format_not_supported', $parts_sent);
            }
            $mime = (string)($validation['mime_type'] ?? mime_content_type($path) ?: 'application/octet-stream');
            $file = $this->transport->multipart(
                $this->url(
                    $this->businessPath('/v2/businesses/%d/chats/file/send', $credentials),
                    ['chatId' => $chat_id]
                ),
                $this->headers($credentials, false),
                ['file' => new \CURLFile($path, $mime, $name)],
                25
            );
            if (empty($file['success'])) {
                $failure = $this->writeFailure($file, 'file_send_failed');
                return $parts_sent > 0
                    ? $failure + ['partial_delivery' => true, 'delivered_parts' => $parts_sent, 'retryable' => false]
                    : $failure;
            }
            $parts_sent++;
        }

        if ($parts_sent <= 0) {
            return ['success' => false, 'error' => 'message_empty'];
        }
         
         
        return [
            'success' => true,
            'external_message_id' => '',
            'external_message_ids' => [],
            'external_chat_id' => (string)$chat_id,
            'provider_confirmation' => 'http_200_without_message_id',
        ];
    }

    public function startMarketplaceConversation(array $credentials, array $context): array {
        $type = strtoupper($this->safeString($context['context_type'] ?? '', 16));
        $id = $this->positiveId($context['context_id'] ?? '');
        if (!in_array($type, ['ORDER', 'RETURN'], true) || $id <= 0) {
            return ['success' => false, 'error' => 'yandex_market_context_invalid'];
        }
        $result = $this->api(
            'POST',
            $this->businessPath('/v2/businesses/%d/chats/new', $credentials),
            $credentials,
            ['context' => ['type' => $type, 'id' => $id]]
        );
        if (empty($result['success'])) {
            return $this->writeFailure($result, 'marketplace_start_failed');
        }
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $chat_id = $this->positiveId($source['chatId'] ?? 0);
        if ($chat_id <= 0) {
            return [
                'success' => false,
                'error' => 'marketplace_start_response_invalid',
                'outcome_unknown' => true,
                'http_code' => (int)($result['http_code'] ?? 200),
            ];
        }
        return [
            'success' => true,
            'external_chat_id' => (string)$chat_id,
            'external_thread_id' => (string)$chat_id,
            'context_type' => $type,
            'context_id' => (string)$id,
        ];
    }

    public function performMarketplaceAction(string $action, array $credentials, array $context): array {
        $text = trim((string)($context['text'] ?? ''));
        $target = is_array($context['action_target'] ?? null) ? $context['action_target'] : [];
        $thread_id = $this->positiveId($context['external_thread_id'] ?? 0);
        $result = [];

        switch ($action) {
            case 'yandex_question_answer':
            case 'yandex_question_comment':
                if ($text === '' || $this->utf8Length($text) > 5000) {
                    return ['success' => false, 'error' => 'answer_text_invalid'];
                }
                $parent_id = $this->positiveId($target['parent_id'] ?? ($action === 'yandex_question_answer' ? $thread_id : 0));
                $parent_type = strtoupper((string)($target['parent_type'] ?? ($action === 'yandex_question_answer' ? 'QUESTION' : 'ANSWER')));
                if ($parent_id <= 0 || !in_array($parent_type, ['QUESTION', 'ANSWER'], true)) {
                    return ['success' => false, 'error' => 'marketplace_action_not_allowed'];
                }
                $result = $this->api(
                    'POST',
                    $this->businessPath('/v1/businesses/%d/goods-questions/update', $credentials),
                    $credentials,
                    [
                        'operationType' => 'CREATE',
                        'parentEntityId' => ['id' => $parent_id, 'type' => $parent_type],
                        'text' => $text,
                    ]
                );
                break;

            case 'yandex_question_edit':
            case 'yandex_question_delete':
                $entity_id = $this->positiveId($target['entity_id'] ?? 0);
                $entity_type = strtoupper((string)($target['entity_type'] ?? ''));
                if ($entity_id <= 0 || !in_array($entity_type, ['ANSWER', 'COMMENT'], true)) {
                    return ['success' => false, 'error' => 'marketplace_action_not_allowed'];
                }
                if ($action === 'yandex_question_edit' && ($text === '' || $this->utf8Length($text) > 5000)) {
                    return ['success' => false, 'error' => 'answer_text_invalid'];
                }
                $body = [
                    'operationType' => $action === 'yandex_question_edit' ? 'UPDATE' : 'DELETE',
                    'entityId' => ['id' => $entity_id, 'type' => $entity_type],
                ];
                if ($action === 'yandex_question_edit') $body['text'] = $text;
                $result = $this->api(
                    'POST',
                    $this->businessPath('/v1/businesses/%d/goods-questions/update', $credentials),
                    $credentials,
                    $body
                );
                break;

            case 'yandex_review_answer':
            case 'yandex_review_comment':
            case 'yandex_review_edit':
                if ($text === '' || $this->utf8Length($text) > 4096) {
                    return ['success' => false, 'error' => 'answer_text_invalid'];
                }
                $feedback_id = $this->positiveId($target['feedback_id'] ?? $thread_id);
                if ($feedback_id <= 0) {
                    return ['success' => false, 'error' => 'marketplace_action_not_allowed'];
                }
                $comment = ['text' => $text];
                if ($action === 'yandex_review_comment') {
                    $comment['parentId'] = $this->positiveId($target['parent_id'] ?? 0);
                } elseif ($action === 'yandex_review_edit') {
                    $comment['id'] = $this->positiveId($target['comment_id'] ?? 0);
                }
                $required_comment_id = $action === 'yandex_review_comment'
                    ? (int)($comment['parentId'] ?? 0)
                    : ($action === 'yandex_review_edit' ? (int)($comment['id'] ?? 0) : 1);
                if ($required_comment_id <= 0) {
                    return ['success' => false, 'error' => 'marketplace_action_not_allowed'];
                }
                $result = $this->api(
                    'POST',
                    $this->businessPath('/v2/businesses/%d/goods-feedback/comments/update', $credentials),
                    $credentials,
                    ['feedbackId' => $feedback_id, 'comment' => $comment],
                    ['sourceType' => 'SELLER']
                );
                break;

            case 'yandex_review_delete':
                $feedback_id = $this->positiveId($target['feedback_id'] ?? $thread_id);
                $comment_id = $this->positiveId($target['comment_id'] ?? 0);
                if ($feedback_id <= 0 || $comment_id <= 0) {
                    return ['success' => false, 'error' => 'marketplace_action_not_allowed'];
                }
                $result = $this->api(
                    'POST',
                    $this->businessPath('/v2/businesses/%d/goods-feedback/comments/delete', $credentials),
                    $credentials,
                    ['id' => $comment_id],
                    ['sourceType' => 'SELLER']
                );
                break;

            case 'yandex_review_skip':
                if ($thread_id <= 0) {
                    return ['success' => false, 'error' => 'thread_id_missing'];
                }
                $result = $this->api(
                    'POST',
                    $this->businessPath('/v2/businesses/%d/goods-feedback/skip-reaction', $credentials),
                    $credentials,
                    ['feedbackIds' => [$thread_id]],
                    ['sourceType' => 'SELLER']
                );
                break;

            default:
                return ['success' => false, 'error' => 'action_unknown'];
        }

        return !empty($result['success'])
            ? ['success' => true, 'http_code' => (int)($result['http_code'] ?? 200), 'response' => $result['json'] ?? []]
            : $this->writeFailure($result, 'action_failed');
    }

    




    private function syncNotificationHints(string $stream, array $credentials, array $hints): array {
         
         
         
        $hint_limit = $stream === MarketplaceStream::BUYER_CHAT ? 50 : 4;
        $hints = array_slice(array_values(array_filter($hints, 'is_array')), 0, $hint_limit);
        if (!$hints) {
            return ['success' => true, 'events' => [], 'consumed_hint_ids' => []];
        }
        if ($stream === MarketplaceStream::BUYER_CHAT) {
            return $this->syncChatHints($credentials, $hints);
        }
        if ($stream === MarketplaceStream::QUESTION) {
            return $this->syncQuestionHints($credentials, $hints);
        }
        if ($stream === MarketplaceStream::REVIEW) {
            return $this->syncReviewHints($credentials, $hints);
        }
        return ['success' => false, 'error' => 'notification_hint_stream_unknown'];
    }

    private function syncChatHints(array $credentials, array $hints): array {
        $events = [];
        $consumed = [];
        $chats = [];
        foreach ($hints as $hint) {
            $hint_id = (int)($hint['hint_id'] ?? 0);
            $type = (string)($hint['entity_type'] ?? '');
            $payload = is_array($hint['payload'] ?? null) ? $hint['payload'] : [];
            $chat_id = $this->positiveId($payload['chat_id'] ?? 0);
            $message_id = $this->positiveId($payload['message_id'] ?? 0);
            if ($hint_id <= 0 || $chat_id <= 0
                || !in_array($type, ['chat', 'chat_message'], true)
                || ($type === 'chat_message' && $message_id <= 0)) {
                if ($hint_id > 0) $consumed[] = $hint_id;
                continue;
            }

            if (!array_key_exists($chat_id, $chats)) {
                $chat_result = $this->api(
                    'GET',
                    $this->businessPath('/v2/businesses/%d/chat', $credentials),
                    $credentials,
                    null,
                    ['chatId' => $chat_id]
                );
                if (empty($chat_result['success'])) {
                    if ($this->hintEntityGone($chat_result)) {
                        $chats[$chat_id] = null;
                    } else {
                        return $this->failure($chat_result, 'notification_chat_read_failed');
                    }
                } else {
                    $chat_json = is_array($chat_result['json'] ?? null) ? $chat_result['json'] : [];
                    $chats[$chat_id] = is_array($chat_json['result'] ?? null)
                        ? $chat_json['result']
                        : null;
                }
            }
            $chat = $chats[$chat_id];
            if (!is_array($chat)) {
                $consumed[] = $hint_id;
                continue;
            }

            if ($type === 'chat') {
                $events[] = $this->normalizeChatState($chat, false);
                $consumed[] = $hint_id;
                continue;
            }
            $message_result = $this->api(
                'GET',
                $this->businessPath('/v2/businesses/%d/chats/message', $credentials),
                $credentials,
                null,
                ['chatId' => $chat_id, 'messageId' => $message_id]
            );
            if (empty($message_result['success'])) {
                if ($this->hintEntityGone($message_result)) {
                    $consumed[] = $hint_id;
                    continue;
                }
                return $this->failure($message_result, 'notification_chat_message_read_failed');
            }
            $message_json = is_array($message_result['json'] ?? null) ? $message_result['json'] : [];
            $message = is_array($message_json['result'] ?? null) ? $message_json['result'] : [];
            if ($this->positiveId($message['messageId'] ?? 0) > 0) {
                $events[] = $this->normalizeChatMessage($chat, $message, false);
            }
            $consumed[] = $hint_id;
        }
        return ['success' => true, 'events' => $events, 'consumed_hint_ids' => $consumed];
    }

    private function syncQuestionHints(array $credentials, array $hints): array {
        $question_ids = [];
        $answer_ids = [];
        $normalized = [];
        $consumed = [];
        foreach ($hints as $hint) {
            $hint_id = (int)($hint['hint_id'] ?? 0);
            $type = (string)($hint['entity_type'] ?? '');
            $payload = is_array($hint['payload'] ?? null) ? $hint['payload'] : [];
            if ($hint_id <= 0) continue;
            if ($type === 'question') {
                $id = $this->positiveId($payload['question_id'] ?? 0);
                if ($id > 0) {
                    $question_ids[$id] = $id;
                    $normalized[] = ['hint_id' => $hint_id, 'type' => $type, 'question_id' => $id];
                    continue;
                }
            } elseif (in_array($type, ['question_answer', 'question_comment'], true)) {
                $id = $this->positiveId($payload['answer_id'] ?? 0);
                if ($id > 0) {
                    $answer_ids[$id] = $id;
                    $normalized[] = ['hint_id' => $hint_id, 'type' => $type, 'answer_id' => $id];
                    continue;
                }
            }
            $consumed[] = $hint_id;
        }

        $answers_by_id = [];
        if ($answer_ids) {
            $answer_result = $this->api(
                'POST',
                $this->businessPath('/v1/businesses/%d/goods-questions/answers', $credentials),
                $credentials,
                ['answerIds' => array_values($answer_ids)],
                ['limit' => 50]
            );
            if (empty($answer_result['success'])) {
                if (!$this->hintEntityGone($answer_result)) {
                    return $this->failure($answer_result, 'notification_question_answer_read_failed');
                }
            } else {
                $json = is_array($answer_result['json'] ?? null) ? $answer_result['json'] : [];
                $source = is_array($json['result'] ?? null) ? $json['result'] : [];
                foreach ((array)($source['answers'] ?? []) as $answer) {
                    if (!is_array($answer)) continue;
                    $answer_id = $this->positiveId($answer['id'] ?? 0);
                    $question_id = $this->positiveId($answer['questionId'] ?? 0);
                    if ($answer_id > 0 && $question_id > 0) {
                        $answers_by_id[$answer_id] = $answer;
                        $question_ids[$question_id] = $question_id;
                    }
                }
            }
        }

        $questions_by_id = [];
        if ($question_ids) {
            $question_result = $this->api(
                'POST',
                $this->businessPath('/v1/businesses/%d/goods-questions', $credentials),
                $credentials,
                ['questionIds' => array_values($question_ids)],
                ['limit' => 50]
            );
            if (empty($question_result['success'])) {
                if (!$this->hintEntityGone($question_result)) {
                    return $this->failure($question_result, 'notification_question_read_failed');
                }
            } else {
                $json = is_array($question_result['json'] ?? null) ? $question_result['json'] : [];
                $source = is_array($json['result'] ?? null) ? $json['result'] : [];
                foreach ((array)($source['questions'] ?? []) as $question) {
                    if (!is_array($question)) continue;
                    $ids = is_array($question['questionIdentifiers'] ?? null)
                        ? $question['questionIdentifiers']
                        : [];
                    $question_id = $this->positiveId($ids['id'] ?? 0);
                    if ($question_id > 0) $questions_by_id[$question_id] = $question;
                }
            }
        }

        $events = [];
        foreach ($normalized as $hint) {
            if ($hint['type'] === 'question') {
                $question = $questions_by_id[$hint['question_id']] ?? null;
                if (is_array($question)) {
                    foreach ($this->questionEvents($question, false, $credentials) as $event) {
                        $events[] = $event;
                    }
                }
                $consumed[] = $hint['hint_id'];
                continue;
            }
            $answer = $answers_by_id[$hint['answer_id']] ?? null;
            $question_id = is_array($answer) ? $this->positiveId($answer['questionId'] ?? 0) : 0;
            $question = $questions_by_id[$question_id] ?? null;
            if (is_array($answer) && is_array($question)) {
                foreach ($this->questionEvents($question, false, $credentials, [$answer]) as $event) {
                    $events[] = $event;
                }
            }
            $consumed[] = $hint['hint_id'];
        }
        return ['success' => true, 'events' => $events, 'consumed_hint_ids' => $consumed];
    }

    private function syncReviewHints(array $credentials, array $hints): array {
        $feedback_ids = [];
        $comment_ids = [];
        $normalized = [];
        $consumed = [];
        foreach ($hints as $hint) {
            $hint_id = (int)($hint['hint_id'] ?? 0);
            $type = (string)($hint['entity_type'] ?? '');
            $payload = is_array($hint['payload'] ?? null) ? $hint['payload'] : [];
            if ($hint_id <= 0) continue;
            if ($type === 'feedback') {
                $id = $this->positiveId($payload['feedback_id'] ?? 0);
                if ($id > 0) {
                    $feedback_ids[$id] = $id;
                    $normalized[] = ['hint_id' => $hint_id, 'type' => $type, 'feedback_id' => $id];
                    continue;
                }
            } elseif ($type === 'feedback_comment') {
                $id = $this->positiveId($payload['comment_id'] ?? 0);
                if ($id > 0) {
                    $comment_ids[$id] = $id;
                    $normalized[] = ['hint_id' => $hint_id, 'type' => $type, 'comment_id' => $id];
                    continue;
                }
            }
            $consumed[] = $hint_id;
        }

        $comments_by_id = [];
        if ($comment_ids) {
            $comment_result = $this->api(
                'POST',
                $this->businessPath('/v2/businesses/%d/goods-feedback/comments', $credentials),
                $credentials,
                ['commentIds' => array_values($comment_ids)],
                ['sourceType' => 'SELLER', 'limit' => 50]
            );
            if (empty($comment_result['success'])) {
                if (!$this->hintEntityGone($comment_result)) {
                    return $this->failure($comment_result, 'notification_feedback_comment_read_failed');
                }
            } else {
                $json = is_array($comment_result['json'] ?? null) ? $comment_result['json'] : [];
                $source = is_array($json['result'] ?? null) ? $json['result'] : [];
                foreach ((array)($source['comments'] ?? []) as $comment) {
                    if (!is_array($comment)) continue;
                    $comment_id = $this->positiveId($comment['id'] ?? 0);
                    $feedback_id = $this->positiveId($comment['feedbackId'] ?? 0);
                    if ($comment_id > 0 && $feedback_id > 0) {
                        $comments_by_id[$comment_id] = $comment;
                        $feedback_ids[$feedback_id] = $feedback_id;
                    }
                }
            }
        }

        $feedback_by_id = [];
        if ($feedback_ids) {
            $feedback_result = $this->api(
                'POST',
                $this->businessPath('/v2/businesses/%d/goods-feedback', $credentials),
                $credentials,
                ['feedbackIds' => array_values($feedback_ids)],
                ['limit' => 50]
            );
            if (empty($feedback_result['success'])) {
                if (!$this->hintEntityGone($feedback_result)) {
                    return $this->failure($feedback_result, 'notification_feedback_read_failed');
                }
            } else {
                $json = is_array($feedback_result['json'] ?? null) ? $feedback_result['json'] : [];
                $source = is_array($json['result'] ?? null) ? $json['result'] : [];
                foreach ((array)($source['feedbacks'] ?? []) as $feedback) {
                    if (!is_array($feedback)) continue;
                    $feedback_id = $this->positiveId($feedback['feedbackId'] ?? 0);
                    if ($feedback_id > 0) $feedback_by_id[$feedback_id] = $feedback;
                }
            }
        }

        $events = [];
        foreach ($normalized as $hint) {
            if ($hint['type'] === 'feedback') {
                $feedback = $feedback_by_id[$hint['feedback_id']] ?? null;
                if (is_array($feedback)) {
                    foreach ($this->reviewEvents($feedback, false, $credentials) as $event) {
                        $events[] = $event;
                    }
                }
                $consumed[] = $hint['hint_id'];
                continue;
            }
            $comment = $comments_by_id[$hint['comment_id']] ?? null;
            $feedback_id = is_array($comment) ? $this->positiveId($comment['feedbackId'] ?? 0) : 0;
            $feedback = $feedback_by_id[$feedback_id] ?? null;
            if (is_array($comment) && is_array($feedback)) {
                foreach ($this->reviewEvents($feedback, false, $credentials, [$comment]) as $event) {
                    $events[] = $event;
                }
            }
            $consumed[] = $hint['hint_id'];
        }
        return ['success' => true, 'events' => $events, 'consumed_hint_ids' => $consumed];
    }

    private function hintEntityGone(array $result): bool {
        return in_array((int)($result['http_code'] ?? 0), [404, 410], true);
    }

    private function syncChats(array $credentials, array $cursor, int $limit): array {
        $page_token = $this->safeString($cursor['page_token'] ?? '', 2000);
        $result = $this->api(
            'POST',
            $this->businessPath('/v2/businesses/%d/chats', $credentials),
            $credentials,
            [],
            array_filter(['limit' => $limit, 'pageToken' => $page_token])
        );
        if (empty($result['success'])) return $this->failure($result, 'chat_list_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : [];
        $chats = is_array($source['chats'] ?? null) ? $source['chats'] : [];
        $watermarks = is_array($cursor['chat_watermarks'] ?? null) ? $cursor['chat_watermarks'] : [];
        $quiet = empty($cursor['initialized']);
        $events = [];
         
         
         
        $event_budget = 200;
        $pending = is_array($cursor['pending_history'] ?? null)
            ? $cursor['pending_history']
            : [];

        if ($pending) {
            $chat_id = $this->positiveId($pending['chat_id'] ?? 0);
            $known = $this->positiveId($pending['message_id_from'] ?? 0);
            $max_id = max($known, $this->positiveId($pending['max_id'] ?? 0));
            $history_page = $this->safeString($pending['page_token'] ?? '', 2000);
            if ($chat_id <= 0 || $history_page === '') {
                $pending = [];
            } else {
                $chat_result = $this->api(
                    'GET',
                    $this->businessPath('/v2/businesses/%d/chat', $credentials),
                    $credentials,
                    null,
                    ['chatId' => $chat_id]
                );
                if (empty($chat_result['success'])) {
                    if ($this->hintEntityGone($chat_result)) {
                        $pending = [];
                    } else {
                        return $this->failure($chat_result, 'chat_context_failed');
                    }
                } else {
                    $chat_json = is_array($chat_result['json'] ?? null) ? $chat_result['json'] : [];
                    $chat = is_array($chat_json['result'] ?? null) ? $chat_json['result'] : [];
                    $history = $this->chatHistoryPage(
                        $credentials,
                        $chat_id,
                        $known,
                        $history_page,
                        $event_budget
                    );
                    if (empty($history['success'])) {
                        return $this->failure($history, 'chat_history_failed');
                    }
                    $history_source = is_array($history['source'] ?? null) ? $history['source'] : [];
                    if (is_array($history_source['context'] ?? null)) {
                        $chat['context'] = $history_source['context'];
                    }
                    foreach ($this->normalizeChatHistory(
                        $chat,
                        (array)($history_source['messages'] ?? []),
                        $known,
                        !empty($pending['quiet']),
                        $max_id
                    ) as $event) {
                        $events[] = $event;
                    }
                    $next_history = $this->safeString($history_source['paging']['nextPageToken'] ?? '', 2000);
                    if ($next_history !== '' && $next_history === $history_page) {
                        return ['success' => false, 'error' => 'marketplace_cursor_stalled', 'retryable' => true];
                    }
                    if ($next_history !== '') {
                        $pending['page_token'] = $next_history;
                        $pending['max_id'] = $max_id;
                        return [
                            'success' => true,
                            'events' => $events,
                            'cursor' => [
                                'page_token' => $page_token,
                                'chat_watermarks' => $this->trimChatWatermarks($watermarks),
                                'pending_history' => $pending,
                                'initialized' => !empty($cursor['initialized']),
                            ],
                            'has_more' => true,
                            'delay_seconds' => 1,
                        ];
                    }
                    if ($max_id > 0) {
                        unset($watermarks[(string)$chat_id]);
                        $watermarks[(string)$chat_id] = $max_id;
                    }
                    $pending = [];
                }
            }
        }

        foreach ($chats as $chat) {
            if (!is_array($chat)) continue;
            if (count($events) >= $event_budget) {
                return [
                    'success' => true,
                    'events' => $events,
                    'cursor' => [
                        'page_token' => $page_token,
                        'chat_watermarks' => $this->trimChatWatermarks($watermarks),
                        'initialized' => !empty($cursor['initialized']),
                    ],
                    'has_more' => true,
                    'delay_seconds' => 1,
                ];
            }
            $chat_id = $this->positiveId($chat['chatId'] ?? 0);
            if ($chat_id <= 0) continue;
            $known = $this->positiveId($watermarks[(string)$chat_id] ?? 0);
            $history = $this->chatHistoryPage(
                $credentials,
                $chat_id,
                $known,
                '',
                $event_budget - count($events)
            );
            if (empty($history['success'])) {
                if (!empty($history['retryable'])) return $this->failure($history, 'chat_history_failed');
                continue;
            }
            $history_source = is_array($history['source'] ?? null) ? $history['source'] : [];
            if (is_array($history_source['context'] ?? null)) {
                $chat['context'] = $history_source['context'];
            }
            $max_id = $known;
            $normalized = $this->normalizeChatHistory(
                $chat,
                (array)($history_source['messages'] ?? []),
                $known,
                $quiet,
                $max_id
            );
            foreach ($normalized as $event) $events[] = $event;
            $next_history = $this->safeString($history_source['paging']['nextPageToken'] ?? '', 2000);
            if ($next_history !== '') {
                return [
                    'success' => true,
                    'events' => $events,
                    'cursor' => [
                        'page_token' => $page_token,
                        'chat_watermarks' => $this->trimChatWatermarks($watermarks),
                        'pending_history' => [
                            'chat_id' => $chat_id,
                            'message_id_from' => $known,
                            'page_token' => $next_history,
                            'max_id' => $max_id,
                            'quiet' => $quiet,
                        ],
                        'initialized' => !empty($cursor['initialized']),
                    ],
                    'has_more' => true,
                    'delay_seconds' => 1,
                ];
            }
            if (!$normalized) {
                $events[] = $this->normalizeChatState($chat, $quiet);
            }
            if ($max_id > 0) {
                unset($watermarks[(string)$chat_id]);
                $watermarks[(string)$chat_id] = $max_id;
            }
        }
        $watermarks = $this->trimChatWatermarks($watermarks);

        $next = $this->safeString($source['paging']['nextPageToken'] ?? '', 2000);
        if ($next !== '' && $next === $page_token) {
            return ['success' => false, 'error' => 'marketplace_cursor_stalled', 'retryable' => true];
        }
        $has_more = $next !== '';
        return [
            'success' => true,
            'events' => $events,
            'cursor' => [
                'page_token' => $has_more ? $next : '',
                'chat_watermarks' => $watermarks,
                'initialized' => $has_more ? !empty($cursor['initialized']) : true,
            ],
            'has_more' => $has_more,
            'delay_seconds' => $has_more ? 1 : 15,
        ];
    }

    private function chatHistoryPage(
        array $credentials,
        int $chat_id,
        int $known,
        string $page_token,
        int $limit
    ): array {
        $history = $this->api(
            'POST',
            $this->businessPath('/v2/businesses/%d/chats/history', $credentials),
            $credentials,
            $known > 0 ? ['messageIdFrom' => $known] : [],
            array_filter([
                'chatId' => $chat_id,
                'limit' => max(1, min(100, $limit)),
                'pageToken' => $page_token,
            ])
        );
        if (empty($history['success'])) {
            return $history;
        }
        $json = is_array($history['json'] ?? null) ? $history['json'] : [];
        $history['source'] = is_array($json['result'] ?? null) ? $json['result'] : [];
        return $history;
    }

    



    private function normalizeChatHistory(
        array $chat,
        array $messages,
        int $known,
        bool $quiet,
        int &$max_id
    ): array {
        $normalized = [];
        foreach ($messages as $message) {
            if (!is_array($message)) continue;
            $message_id = $this->positiveId($message['messageId'] ?? 0);
            if ($message_id <= 0 || ($known > 0 && $message_id <= $known)) continue;
            $normalized[] = $this->normalizeChatMessage($chat, $message, $quiet);
            $max_id = max($max_id, $message_id);
        }
        usort($normalized, static function (array $a, array $b): int {
            return strcmp((string)($a['published_at'] ?? ''), (string)($b['published_at'] ?? ''));
        });
        return $normalized;
    }

    private function trimChatWatermarks(array $watermarks): array {
        return count($watermarks) > 500
            ? array_slice($watermarks, -500, null, true)
            : $watermarks;
    }

    private function syncQuestions(array $credentials, array $cursor, int $limit): array {
        $body = [
             
             
            'dateFrom' => gmdate('Y-m-d', time() - 30 * 86400),
            'dateTo' => gmdate('Y-m-d'),
            'needAnswer' => false,
            'sort' => 'CREATED_AT_DESC',
        ];
        return $this->syncPublicThreads(
            $credentials,
            $cursor,
            $limit,
            '/v1/businesses/%d/goods-questions',
            $body,
            'questions',
            'questionIdentifiers',
            'id',
            120,
            function (array $row, bool $quiet, array $credentials): array {
                return $this->questionEvents($row, $quiet, $credentials);
            }
        );
    }

    private function syncReviews(array $credentials, array $cursor, int $limit): array {
        $body = [
            'dateTimeFrom' => gmdate('c', time() - 180 * 86400),
            'dateTimeTo' => gmdate('c', time() + 60),
            'reactionStatus' => 'ALL',
        ];
        return $this->syncPublicThreads(
            $credentials,
            $cursor,
            $limit,
            '/v2/businesses/%d/goods-feedback',
            $body,
            'feedbacks',
            '',
            'feedbackId',
            120,
            function (array $row, bool $quiet, array $credentials): array {
                return $this->reviewEvents($row, $quiet, $credentials);
            }
        );
    }

    private function syncPublicThreads(
        array $credentials,
        array $cursor,
        int $limit,
        string $path,
        array $body,
        string $rows_key,
        string $identifiers_key,
        string $id_key,
        int $delay,
        callable $normalizer
    ): array {
        $page_token = $this->safeString($cursor['page_token'] ?? '', 2000);
        $initialized = !empty($cursor['initialized']);
        $last_full_sweep_at = max(0, (int)($cursor['last_full_sweep_at'] ?? 0));
        $full_sweep = !$initialized
            || !empty($cursor['full_sweep'])
            || ($page_token === '' && ($last_full_sweep_at <= 0 || time() - $last_full_sweep_at >= 6 * 3600));
        $result = $this->api(
            'POST',
            $this->businessPath($path, $credentials),
            $credentials,
            $body,
            array_filter(['limit' => min(50, $limit), 'pageToken' => $page_token])
        );
        if (empty($result['success'])) return $this->failure($result, 'marketplace_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : [];
        $quiet = !$initialized;
        $seen = is_array($cursor['seen'] ?? null) ? $cursor['seen'] : [];
        $events = [];
        foreach ((array)($source[$rows_key] ?? []) as $row) {
            if (!is_array($row)) continue;
            $identifiers = $identifiers_key !== '' && is_array($row[$identifiers_key] ?? null)
                ? $row[$identifiers_key]
                : $row;
            $id = $this->positiveId($identifiers[$id_key] ?? $row[$id_key] ?? 0);
            if ($id <= 0) continue;
            $fingerprint = substr(hash('sha256', json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)), 0, 32);
             
             
             
             
            $row_quiet = $quiet;
            foreach ($normalizer($row, $row_quiet, $credentials) as $event) {
                $events[] = $event;
            }
            unset($seen[(string)$id]);
            $seen[(string)$id] = $fingerprint;
        }
        if (count($seen) > 1000) $seen = array_slice($seen, -1000, null, true);
        $api_next = $this->safeString($source['paging']['nextPageToken'] ?? '', 2000);
        if ($api_next !== '' && $api_next === $page_token) {
            return ['success' => false, 'error' => 'marketplace_cursor_stalled', 'retryable' => true];
        }
        





        $has_more = $full_sweep && $api_next !== '';
        if ($full_sweep && !$has_more) {
            $last_full_sweep_at = time();
        }
        return [
            'success' => true,
            'events' => $events,
            'cursor' => [
                'page_token' => $has_more ? $api_next : '',
                'seen' => $seen,
                'initialized' => $has_more ? $initialized : true,
                'full_sweep' => $has_more,
                'last_full_sweep_at' => $last_full_sweep_at,
            ],
            'has_more' => $has_more,
            'delay_seconds' => $has_more ? 1 : $delay,
        ];
    }

    private function questionEvents(
        array $question,
        bool $quiet,
        array $credentials,
        ?array $preloaded_answers = null
    ): array {
        $identifiers = is_array($question['questionIdentifiers'] ?? null) ? $question['questionIdentifiers'] : [];
        $question_id = $this->positiveId($identifiers['id'] ?? $question['questionId'] ?? 0);
        if ($question_id <= 0) return [];
        $offer_id = $this->safeString($identifiers['offerId'] ?? $question['offerId'] ?? '', 128);
        $author = is_array($question['author'] ?? null) ? $question['author'] : [];
        $events = [$this->publicThreadEvent(
            MarketplaceStream::QUESTION,
            $question_id,
            'question:' . $question_id,
            (string)($question['text'] ?? ''),
            'in',
            (string)($author['name'] ?? 'Покупатель Маркета'),
            (string)($question['createdAt'] ?? ''),
            $offer_id,
            '',
            ['yandex_question_answer'],
            ['yandex_question_answer' => ['parent_id' => $question_id, 'parent_type' => 'QUESTION']],
            $question,
            $quiet
        )];

        if ($preloaded_answers === null) {
            $answers = $this->api(
                'POST',
                $this->businessPath('/v1/businesses/%d/goods-questions/answers', $credentials),
                $credentials,
                ['questionId' => $question_id],
                ['limit' => 50]
            );
            if (empty($answers['success'])) return $events;
            $answer_source = is_array($answers['json']['result'] ?? null) ? $answers['json']['result'] : [];
            $answer_rows = (array)($answer_source['answers'] ?? []);
        } else {
            $answer_rows = $preloaded_answers;
        }
        foreach ($answer_rows as $answer) {
            if (!is_array($answer)) continue;
            $answer_id = $this->positiveId($answer['id'] ?? 0);
            $answer_author = is_array($answer['author'] ?? null) ? $answer['author'] : [];
            $author_type = strtoupper((string)($answer_author['type'] ?? 'USER'));
            $actions = ['yandex_question_comment'];
            $targets = ['yandex_question_comment' => ['parent_id' => $answer_id, 'parent_type' => 'ANSWER']];
            if (!empty($answer['canModify'])) {
                $actions[] = 'yandex_question_edit';
                $actions[] = 'yandex_question_delete';
                $targets['yandex_question_edit'] = ['entity_id' => $answer_id, 'entity_type' => 'ANSWER'];
                $targets['yandex_question_delete'] = ['entity_id' => $answer_id, 'entity_type' => 'ANSWER'];
            }
            $events[] = $this->publicThreadEvent(
                MarketplaceStream::QUESTION,
                $question_id,
                'answer:' . $answer_id,
                (string)($answer['text'] ?? ''),
                $author_type === 'BUSINESS' ? 'out' : 'in',
                (string)($answer_author['name'] ?? ($author_type === 'BUSINESS' ? 'Магазин' : 'Покупатель Маркета')),
                (string)($answer['createdAt'] ?? ''),
                $offer_id,
                (string)($answer['status'] ?? ''),
                $actions,
                $targets,
                ['question' => $question, 'answer' => $answer],
                $quiet || !empty($answer['canModify'])
            );
            foreach ((array)($answer['comments'] ?? []) as $comment) {
                if (!is_array($comment)) continue;
                $comment_id = $this->positiveId($comment['id'] ?? 0);
                $comment_author = is_array($comment['author'] ?? null) ? $comment['author'] : [];
                $comment_type = strtoupper((string)($comment_author['type'] ?? 'USER'));
                $comment_actions = [];
                $comment_targets = [];
                if (!empty($comment['canModify'])) {
                    $comment_actions = ['yandex_question_edit', 'yandex_question_delete'];
                    $comment_targets = [
                        'yandex_question_edit' => ['entity_id' => $comment_id, 'entity_type' => 'COMMENT'],
                        'yandex_question_delete' => ['entity_id' => $comment_id, 'entity_type' => 'COMMENT'],
                    ];
                }
                $events[] = $this->publicThreadEvent(
                    MarketplaceStream::QUESTION,
                    $question_id,
                    'comment:' . $comment_id,
                    (string)($comment['text'] ?? ''),
                    $comment_type === 'BUSINESS' ? 'out' : 'in',
                    (string)($comment_author['name'] ?? ($comment_type === 'BUSINESS' ? 'Магазин' : 'Покупатель Маркета')),
                    (string)($comment['createdAt'] ?? ''),
                    $offer_id,
                    (string)($comment['status'] ?? ''),
                    $comment_actions,
                    $comment_targets,
                    ['question' => $question, 'answer' => $answer, 'comment' => $comment],
                    $quiet || !empty($comment['canModify'])
                );
            }
        }
        return $events;
    }

    private function reviewEvents(
        array $review,
        bool $quiet,
        array $credentials,
        ?array $preloaded_comments = null
    ): array {
        $feedback_id = $this->positiveId($review['feedbackId'] ?? 0);
        if ($feedback_id <= 0) return [];
        $identifiers = is_array($review['identifiers'] ?? null) ? $review['identifiers'] : [];
        $offer_id = $this->safeString($identifiers['offerId'] ?? '', 128);
        $description = is_array($review['description'] ?? null) ? $review['description'] : [];
        $statistics = is_array($review['statistics'] ?? null) ? $review['statistics'] : [];
        $parts = [];
        if ((int)($statistics['rating'] ?? 0) > 0) $parts[] = 'Оценка: ' . (int)$statistics['rating'] . '/5';
        foreach (['advantages' => 'Достоинства', 'disadvantages' => 'Недостатки', 'comment' => 'Комментарий'] as $key => $label) {
            $value = trim((string)($description[$key] ?? ''));
            if ($value !== '') $parts[] = $label . ': ' . $value;
        }
        $attachments = [];
        $media = is_array($review['media'] ?? null) ? $review['media'] : [];
        foreach ((array)($media['photos'] ?? []) as $url) {
            if (is_string($url) && strpos($url, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $url,
                    'payload_hash' => hash('sha256', self::PROVIDER . '|review|' . $url),
                ];
            }
        }
        foreach ((array)($media['videos'] ?? []) as $url) {
            if (is_string($url) && strpos($url, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'video',
                    'url' => $url,
                    'payload_hash' => hash('sha256', self::PROVIDER . '|review-video|' . $url),
                ];
            }
        }
        $root_actions = ['yandex_review_answer'];
        if (!empty($review['needReaction'])) $root_actions[] = 'yandex_review_skip';
        $root_targets = [
            'yandex_review_answer' => ['feedback_id' => $feedback_id],
            'yandex_review_skip' => ['feedback_id' => $feedback_id],
        ];
        $events = [$this->publicThreadEvent(
            MarketplaceStream::REVIEW,
            $feedback_id,
            'review:' . $feedback_id,
            implode("\n", $parts),
            'in',
            (string)($review['author'] ?? 'Покупатель Маркета'),
            (string)($review['createdAt'] ?? ''),
            $offer_id,
            !empty($review['needReaction']) ? 'need_reaction' : 'seen',
            $root_actions,
            $root_targets,
            $review,
            $quiet,
            $attachments,
            (string)($identifiers['orderId'] ?? '')
        )];

        if ($preloaded_comments === null) {
            $comments = $this->api(
                'POST',
                $this->businessPath('/v2/businesses/%d/goods-feedback/comments', $credentials),
                $credentials,
                ['feedbackId' => $feedback_id],
                ['sourceType' => 'SELLER', 'limit' => 50]
            );
            if (empty($comments['success'])) return $events;
            $comment_source = is_array($comments['json']['result'] ?? null) ? $comments['json']['result'] : [];
            $comment_rows = (array)($comment_source['comments'] ?? []);
        } else {
            $comment_rows = $preloaded_comments;
        }
        foreach ($comment_rows as $comment) {
            if (!is_array($comment)) continue;
            $comment_id = $this->positiveId($comment['id'] ?? 0);
            $author = is_array($comment['author'] ?? null) ? $comment['author'] : [];
            $author_type = strtoupper((string)($author['type'] ?? 'USER'));
            $actions = [];
            $targets = [];
            if ($author_type === 'USER') {
                $actions[] = 'yandex_review_comment';
                $targets['yandex_review_comment'] = ['feedback_id' => $feedback_id, 'parent_id' => $comment_id];
            }
            if (!empty($comment['canModify'])) {
                $actions[] = 'yandex_review_edit';
                $actions[] = 'yandex_review_delete';
                $targets['yandex_review_edit'] = ['feedback_id' => $feedback_id, 'comment_id' => $comment_id];
                $targets['yandex_review_delete'] = ['feedback_id' => $feedback_id, 'comment_id' => $comment_id];
            }
            $events[] = $this->publicThreadEvent(
                MarketplaceStream::REVIEW,
                $feedback_id,
                'review-comment:' . $comment_id,
                (string)($comment['text'] ?? ''),
                $author_type === 'BUSINESS' ? 'out' : 'in',
                (string)($author['name'] ?? ($author_type === 'BUSINESS' ? 'Магазин' : 'Покупатель Маркета')),
                (string)($comment['createdAt'] ?? $review['createdAt'] ?? ''),
                $offer_id,
                (string)($comment['status'] ?? ''),
                $actions,
                $targets,
                ['review' => $review, 'comment' => $comment],
                $quiet || !empty($comment['canModify']),
                [],
                (string)($identifiers['orderId'] ?? '')
            );
        }
        return $events;
    }

    private function normalizeChatMessage(array $chat, array $message, bool $quiet): array {
        $chat_id = $this->positiveId($chat['chatId'] ?? 0);
        $message_id = $this->positiveId($message['messageId'] ?? 0);
        $sender = strtoupper($this->safeString($message['sender'] ?? '', 16));
        $out = $sender === 'PARTNER';
        $status = strtoupper($this->safeString($chat['status'] ?? 'NEW', 40));
        $chat_type = strtoupper($this->safeString($chat['type'] ?? 'CHAT', 16));
        $context = is_array($chat['context'] ?? null) ? $chat['context'] : [];
        $customer = is_array($context['customer'] ?? null) ? $context['customer'] : [];
        [$context_type, $context_id] = $this->chatContextResource($chat);
        $attachments = [];
        foreach ((array)($message['payload'] ?? []) as $file) {
            if (!is_array($file)) continue;
            $url = trim((string)($file['url'] ?? ''));
            if (strpos($url, 'https://') !== 0) continue;
            $name = $this->safeString($file['name'] ?? '', 180);
            $extension = strtolower(pathinfo((string)(parse_url($url, PHP_URL_PATH) ?: $name), PATHINFO_EXTENSION));
            $attachments[] = [
                'type' => in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true) ? 'image' : 'document',
                'url' => $url,
                'file_name' => $name,
                'size' => max(0, (int)($file['size'] ?? 0)),
                'payload_hash' => hash('sha256', self::PROVIDER . '|chat|' . $chat_id . '|' . $message_id . '|' . $url),
            ];
        }
        $service_notice = in_array($sender, ['MARKET', 'SUPPORT'], true);
        $writable = $chat_type === 'CHAT' && in_array($status, self::WRITABLE_CHAT_STATUSES, true);
        return [
            'update_type' => 'message',
            'text' => $this->safeString($message['message'] ?? '', 20000),
            'external_user_id' => (string)$chat_id,
            'external_chat_id' => (string)$chat_id,
            'external_message_id' => (string)$message_id,
            'dedupe_key' => 'yandex-market:chat:' . $chat_id . ':' . $message_id,
            'direction' => $out ? 'out' : 'in',
            'display_name' => $sender === 'SUPPORT'
                ? 'Поддержка Яндекс Маркета'
                : ($sender === 'MARKET' ? 'Яндекс Маркет' : $this->safeString($customer['name'] ?? 'Покупатель Маркета', 255)),
            'attachments' => $attachments,
            'conversation_kind' => MarketplaceStream::BUYER_CHAT,
            'external_thread_id' => (string)$chat_id,
            'thread_state' => strtolower($status),
             
             
             
            'order_id' => $context_id,
            'item_context' => [],
            'capabilities' => [
                'can_reply' => $writable,
                'read_only' => !$writable,
                'attachments_out' => $writable,
                'chat_type' => $chat_type,
            ],
            'published_at' => $this->isoDate($message['createdAt'] ?? ''),
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'yandex_market_chat_message',
                'chat_type' => $chat_type,
                'context_type' => $context_type,
                'sender_type' => $sender,
                'service_notice' => $service_notice,
                'ai_auto_reply_allowed' => !$service_notice && $chat_type === 'CHAT',
            ],
            'snapshot' => $this->sanitizedSnapshot(['chat' => $chat, 'message' => $message]),
        ];
    }

    private function normalizeChatState(array $chat, bool $quiet): array {
        $chat_id = $this->positiveId($chat['chatId'] ?? 0);
        $status = strtoupper($this->safeString($chat['status'] ?? 'NEW', 40));
        $chat_type = strtoupper($this->safeString($chat['type'] ?? 'CHAT', 16));
        [$context_type, $context_id] = $this->chatContextResource($chat);
        $writable = $chat_type === 'CHAT' && in_array($status, self::WRITABLE_CHAT_STATUSES, true);
        return [
            'update_type' => 'thread_state',
            'text' => '',
            'external_user_id' => (string)$chat_id,
            'external_chat_id' => (string)$chat_id,
            'external_message_id' => 'state:' . substr(hash('sha256', $chat_id . '|' . $status . '|' . $chat_type), 0, 40),
            'dedupe_key' => 'yandex-market:state:' . $chat_id . ':' . $status . ':' . $chat_type,
            'direction' => 'in',
            'display_name' => 'Покупатель Маркета',
            'attachments' => [],
            'conversation_kind' => MarketplaceStream::BUYER_CHAT,
            'external_thread_id' => (string)$chat_id,
            'thread_state' => strtolower($status),
            'order_id' => $context_id,
            'item_context' => [],
            'capabilities' => [
                'can_reply' => $writable,
                'read_only' => !$writable,
                'attachments_out' => $writable,
                'chat_type' => $chat_type,
            ],
            'published_at' => '',
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'thread_only' => true,
                'raw_kind' => 'yandex_market_chat_state',
                'chat_type' => $chat_type,
                'context_type' => $context_type,
            ],
            'snapshot' => $this->sanitizedSnapshot(['chat' => $chat]),
        ];
    }

    private function publicThreadEvent(
        string $kind,
        int $thread_id,
        string $entity_key,
        string $text,
        string $direction,
        string $display_name,
        string $published_at,
        string $offer_id,
        string $state,
        array $actions,
        array $targets,
        array $snapshot,
        bool $quiet,
        array $attachments = [],
        string $order_id = ''
    ): array {
        return [
            'update_type' => $kind,
            'text' => $this->safeString($text, 20000),
            'external_user_id' => $kind . ':' . $thread_id,
            'external_chat_id' => $kind . ':' . $thread_id,
            'external_message_id' => $entity_key,
            'dedupe_key' => 'yandex-market:' . $kind . ':' . $thread_id . ':' . $entity_key,
            'direction' => $direction === 'out' ? 'out' : 'in',
            'display_name' => $this->safeString($display_name, 255),
            'attachments' => $attachments,
            'conversation_kind' => $kind,
            'external_thread_id' => (string)$thread_id,
            'thread_state' => strtolower($this->safeString($state !== '' ? $state : 'active', 40)),
            'item_context' => $offer_id !== '' ? [
                'provider' => self::PROVIDER,
                'item_id' => $offer_id,
                'sku' => $offer_id,
                'offer_id' => $offer_id,
                'url' => 'https://market.yandex.ru/search?text=' . rawurlencode($offer_id),
            ] : [],
            'order_id' => $this->safeString($order_id, 128),
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => true,
                'actions' => array_values(array_unique($actions)),
                'action_targets' => $targets,
            ],
            'published_at' => $this->isoDate($published_at),
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'yandex_market_' . $kind,
                'offer_id' => $offer_id,
                'ai_auto_reply_allowed' => false,
            ],
            'snapshot' => $this->sanitizedSnapshot($snapshot),
        ];
    }

    private function api(
        string $method,
        string $path,
        array $credentials,
        $body = null,
        array $query = [],
        int $timeout = 12
    ): array {
        return $this->transport->json(
            strtoupper($method),
            $this->url($path, $query),
            $this->headers($credentials),
            $body,
            $timeout
        );
    }

    private function url(string $path, array $query = []): string {
        $url = self::API_BASE . $path;
        if ($query) {
            $url .= '?' . http_build_query($query, '', '&', PHP_QUERY_RFC3986);
        }
        return $url;
    }

    private function headers(array $credentials, bool $json = true): array {
        $headers = ['Api-Key: ' . $this->apiKey($credentials), 'Accept: application/json'];
        if ($json) $headers[] = 'Content-Type: application/json';
        return $headers;
    }

    private function businessPath(string $template, array $credentials): string {
        return sprintf($template, $this->businessId($credentials));
    }

    private function businessId(array $credentials): int {
        return $this->positiveId($credentials['business_id'] ?? 0);
    }

    private function apiKey(array $credentials): string {
        $key = trim((string)($credentials['api_key'] ?? ''));
        return preg_match('/^[^\s\x00-\x1F\x7F]{16,512}$/', $key) === 1 ? $key : '';
    }

    private function positiveId($value): int {
        $value = trim((string)$value);
        if (preg_match('/^[1-9][0-9]{0,18}$/', $value) !== 1) {
            return 0;
        }
         
         
        $max = (string)PHP_INT_MAX;
        if (strlen($value) > strlen($max)
            || (strlen($value) === strlen($max) && strcmp($value, $max) > 0)) {
            return 0;
        }
        return (int)$value;
    }

    private function validBusinessId(string $value): bool {
        return $this->positiveId($value) > 0;
    }

     
    private function chatContextResource(array $chat): array {
        $context = is_array($chat['context'] ?? null) ? $chat['context'] : [];
        $type = strtoupper($this->safeString($context['type'] ?? 'DIRECT', 16));
        if ($type === 'RETURN') {
            $id = $this->positiveId($context['returnId'] ?? 0);
            return [$type, $id > 0 ? (string)$id : ''];
        }
        if ($type === 'ORDER') {
            $id = $this->positiveId($context['orderId'] ?? $chat['orderId'] ?? 0);
            return [$type, $id > 0 ? (string)$id : ''];
        }
        return ['DIRECT', ''];
    }

    private function streamActionAccess(string $stream, bool $can_write): array {
        $actions = (array)($this->marketplaceCapabilities()['streams'][$stream]['actions'] ?? []);
        return array_fill_keys($actions, $can_write);
    }

    private function partialFailure(string $error, int $delivered_parts): array {
        return $delivered_parts > 0
            ? ['success' => false, 'error' => $error, 'partial_delivery' => true, 'delivered_parts' => $delivered_parts, 'retryable' => false]
            : ['success' => false, 'error' => $error];
    }

    private function safeWebhookUrl(string $url): bool {
        $parts = parse_url($url);
        if (!is_array($parts)
            || strtolower((string)($parts['scheme'] ?? '')) !== 'https'
            || isset($parts['user']) || isset($parts['pass'])
            || (isset($parts['port']) && (int)$parts['port'] !== 443)
        ) {
            return false;
        }
        $host = strtolower((string)($parts['host'] ?? ''));
        if ($host === '' || in_array($host, ['localhost', '127.0.0.1', '::1'], true)) return false;
        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
        }
        return true;
    }
}
