<?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 WildberriesProvider extends AbstractMarketplaceProvider {
    public const PROVIDER = 'wildberries';
    public const CHAT_BASE = 'https://buyer-chat-api.wildberries.ru';
    public const FEEDBACK_BASE = 'https://feedbacks-api.wildberries.ru';
    public const RETURNS_BASE = 'https://returns-api.wildberries.ru';
    public const COMMON_BASE = 'https://common-api.wildberries.ru';
    private const CHAT_CURSOR_VERSION = 3;
    private const FEEDBACK_CURSOR_VERSION = 2;

    private const HOSTS = [
        'buyer-chat-api.wildberries.ru',
        'feedbacks-api.wildberries.ru',
        'returns-api.wildberries.ru',
        'common-api.wildberries.ru',
    ];

    public function __construct($registry = null, ?MarketplaceHttpTransport $transport = null) {
        parent::__construct(
            $registry,
            $transport ?: new MarketplaceHttpTransport(new ExternalHttpClient($registry), self::HOSTS, self::PROVIDER)
        );
    }

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

    public function marketplaceCapabilities(): array {
        return [
            'provider' => self::PROVIDER,
            'auth_type' => 'api_token',
            'secret_fields' => ['api_token'],
            'polling_required' => true,
            'webhook_supported' => false,
            'streams' => [
                MarketplaceStream::BUYER_CHAT => [
                    'read' => true, 'reply' => true, 'attachments_in' => true,
                    'attachments_out' => true, 'max_files' => 10,
                    'formats' => ['jpg', 'jpeg', 'png', 'pdf'],
                ],
                MarketplaceStream::QUESTION => [
                    'read' => true, 'reply' => false, 'draft' => true,
                    'actions' => ['question_answer', 'question_reject', 'question_mark_viewed'],
                ],
                MarketplaceStream::REVIEW => [
                    'read' => true, 'reply' => false, 'draft' => true,
                    'actions' => ['review_answer', 'review_edit'],
                ],
                MarketplaceStream::RETURN_CLAIM => [
                    'read' => true, 'reply' => false, 'draft' => false,
                    'actions_from_payload' => true,
                ],
            ],
            'limits' => [
                'chat_text_chars' => 1000,
                'file_bytes' => 5 * 1024 * 1024,
                'files_total_bytes' => 30 * 1024 * 1024,
            ],
        ];
    }

    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'] ?? 'Wildberries'),
            'username' => '',
            'capabilities' => $result['capabilities'] ?? [],
        ];
    }

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

        $headers = $this->headers($token);
        $seller = $this->transport->json('GET', self::COMMON_BASE . '/api/v1/seller-info', $headers);
        $services = [
            MarketplaceStream::BUYER_CHAT => self::CHAT_BASE,
            MarketplaceStream::QUESTION => self::FEEDBACK_BASE,
            MarketplaceStream::REVIEW => self::FEEDBACK_BASE,
            MarketplaceStream::RETURN_CLAIM => self::RETURNS_BASE,
        ];
        $checks = [];
        foreach ($services as $stream => $base) {
            if (isset($checks[$base])) {
                $checks[$stream] = $checks[$base];
                continue;
            }
            $check = $this->transport->json('GET', $base . '/ping', $headers);
            $checks[$base] = $check;
            $checks[$stream] = $check;
        }

        $available = [];
        $at_least_one = false;
        foreach (MarketplaceStream::all() as $stream) {
            $check = $checks[$stream] ?? [];
            $ok = !empty($check['success']);
            $available[$stream] = [
                'available' => $ok,
                'error' => $ok ? '' : (string)($check['error'] ?? 'permission_unknown'),
                'http_code' => (int)($check['http_code'] ?? 0),
            ];
            $at_least_one = $at_least_one || $ok;
        }

        if (!$at_least_one && empty($seller['success'])) {
            return $this->failure($seller, 'auth_failed');
        }

        $seller_json = is_array($seller['json'] ?? null) ? $seller['json'] : [];
        $jwt = WildberriesTokenInspector::inspect($token);
        $account_id = $this->safeString($seller_json['sid'] ?? $jwt['seller_id'] ?? '', 128);
        if ($account_id === '') {
             
             
             
            return ['success' => false, 'error' => 'marketplace_account_identity_missing'];
        }

        return [
            'success' => true,
            'account' => [
                'id' => $account_id,
                'name' => $this->safeString($seller_json['tradeMark'] ?? $seller_json['name'] ?? 'Wildberries', 128),
                'tin' => $this->safeString($seller_json['tin'] ?? '', 32),
            ],
            'capabilities' => array_merge($this->marketplaceCapabilities(), [
                'stream_access' => $available,
                'token_expires_at' => (string)($jwt['expires_at'] ?? ''),
                'token_read_only' => !empty($jwt['read_only']),
                'token_type' => (string)($jwt['token_type'] ?? 'unknown'),
                'token_type_label' => (string)($jwt['token_type_label'] ?? 'Не удалось определить'),
                'token_permissions' => is_array($jwt['permissions'] ?? null) ? $jwt['permissions'] : [],
            ]),
            'diagnostics' => [
                'seller_info' => !empty($seller['success']) ? 'ok' : (string)($seller['error'] ?? 'unavailable'),
                'stream_access' => $available,
            ],
        ];
    }

    public function syncMarketplaceStream(string $stream, array $credentials, array $cursor, int $limit = 50): array {
        $stream = MarketplaceStream::normalize($stream);
        $token = $this->apiToken($credentials);
        if ($stream === '' || $token === '') {
            return ['success' => false, 'error' => $token === '' ? 'auth_failed' : 'stream_unknown'];
        }
        $limit = max(1, min(100, $limit));

        switch ($stream) {
            case MarketplaceStream::BUYER_CHAT:
                return $this->syncChat($token, $cursor, $this->tokenType($token));
            case MarketplaceStream::QUESTION:
                return $this->syncQuestions($token, $cursor, $limit);
            case MarketplaceStream::REVIEW:
                return $this->syncReviews($token, $cursor, $limit);
            case MarketplaceStream::RETURN_CLAIM:
                return $this->syncReturns($token, $cursor, $limit);
        }
        return ['success' => false, 'error' => 'stream_unknown'];
    }

    public function sendMessage(string $token, string $external_user_id, string $text, array $attachments = [], array $options = []): array {
        $credentials = $this->credentials($token);
        $api_token = $this->apiToken($credentials);
        $reply_sign = trim((string)($options['reply_secret'] ?? ''));
        if ($api_token === '' || $reply_sign === '') {
            return ['success' => false, 'error' => $api_token === '' ? 'auth_failed' : 'reply_sign_missing'];
        }

        $text = trim($text);
        if ($this->utf8Length($text) > 1000) {
            return ['success' => false, 'error' => 'message_text_too_long'];
        }
        if (count($attachments) > 10) {
            return ['success' => false, 'error' => 'attachment_limit_exceeded'];
        }
        $fields = ['replySign' => $reply_sign];
        if ($text !== '') {
            $fields['message'] = $text;
        }

        $total = 0;
        $file_index = 0;
        foreach ($attachments as $attachment) {
            $path = (string)($attachment['path'] ?? '');
            $name = $this->safeString($attachment['original_name'] ?? basename($path), 180);
            if (!is_file($path)) {
                return ['success' => false, 'error' => 'attachment_missing'];
            }
            $size = (int)filesize($path);
            $validation = AttachmentPolicy::validateAttachmentFile($path, $name, $size, 5, true);
            $mime = strtolower((string)($validation['mime'] ?? ''));
            if (empty($validation['success']) || !in_array($mime, ['image/jpeg', 'image/png', 'application/pdf'], true)) {
                return ['success' => false, 'error' => 'attachment_format_not_supported'];
            }
            if ($size <= 0 || $size > 5 * 1024 * 1024 || ($total + $size) > 30 * 1024 * 1024) {
                return ['success' => false, 'error' => 'attachment_size_exceeded'];
            }
            $total += $size;
            $fields['file[' . $file_index++ . ']'] = new \CURLFile($path, $mime, $name);
        }
        if ($text === '' && $file_index === 0) {
            return ['success' => false, 'error' => 'message_empty'];
        }

        $result = $this->transport->multipart(
            self::CHAT_BASE . '/api/v1/seller/message',
            $this->headers($api_token, false),
            $fields
        );
        if (empty($result['success'])) {
            return $this->writeFailure($result, 'send_failed');
        }
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $response = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $external_chat_id = $this->safeString(
            $response['chatID'] ?? $options['external_chat_id'] ?? '',
            191
        );
         
         
         
        $external_message_id = self::outboundCorrelationId(
            $external_chat_id,
            $response['addTime'] ?? ''
        );
        if ($external_message_id === '') {
            $external_message_id = $this->safeString($response['sign'] ?? '', 128);
        }
        return [
            'success' => true,
            'external_message_id' => $external_message_id,
            'external_message_ids' => $external_message_id !== '' ? [$external_message_id] : [],
            'external_chat_id' => $external_chat_id,
            'http_code' => (int)($result['http_code'] ?? 200),
        ];
    }

    public function performMarketplaceAction(string $action, array $credentials, array $context): array {
        $token = $this->apiToken($credentials);
        $id = $this->safeString($context['external_thread_id'] ?? $context['id'] ?? '', 191);
        if ($token === '' || $id === '') {
            return ['success' => false, 'error' => $token === '' ? 'auth_failed' : 'thread_id_missing'];
        }
        $headers = $this->headers($token);
        $text = $this->safeString($context['text'] ?? '', 5000);
        $result = [];

        switch ($action) {
            case 'question_answer':
                if ($text === '') return ['success' => false, 'error' => 'answer_text_required'];
                $result = $this->transport->json('PATCH', self::FEEDBACK_BASE . '/api/v1/questions', $headers, ['id' => $id, 'text' => $text, 'state' => 'wbRu']);
                break;
            case 'question_reject':
                if ($text === '') return ['success' => false, 'error' => 'answer_text_required'];
                $result = $this->transport->json('PATCH', self::FEEDBACK_BASE . '/api/v1/questions', $headers, ['id' => $id, 'text' => $text, 'state' => 'none']);
                break;
            case 'question_mark_viewed':
                $result = $this->transport->json('PATCH', self::FEEDBACK_BASE . '/api/v1/questions', $headers, ['id' => $id, 'wasViewed' => true]);
                break;
            case 'review_answer':
                if ($this->utf8Length($text) < 2) return ['success' => false, 'error' => 'answer_text_too_short'];
                $result = $this->transport->json('POST', self::FEEDBACK_BASE . '/api/v1/feedbacks/answer', $headers, ['id' => $id, 'text' => $this->safeString($text, 5000)]);
                break;
            case 'review_edit':
                if ($this->utf8Length($text) < 2) return ['success' => false, 'error' => 'answer_text_too_short'];
                $result = $this->transport->json('PATCH', self::FEEDBACK_BASE . '/api/v1/feedbacks/answer', $headers, ['id' => $id, 'text' => $this->safeString($text, 5000)]);
                break;
            case 'return_action':
                $allowed = array_values(array_filter(array_map('strval', (array)($context['allowed_actions'] ?? []))));
                $claim_action = $this->safeString($context['claim_action'] ?? '', 64);
                if ($claim_action === '' || !in_array($claim_action, $allowed, true)) {
                    return ['success' => false, 'error' => 'return_action_not_allowed'];
                }
                if (($claim_action === 'rejectcustom' && $this->utf8Length($text) < 10)
                    || ($claim_action === 'approvecc1' && $text !== '' && $this->utf8Length($text) < 10)) {
                    return ['success' => false, 'error' => 'return_comment_required'];
                }
                $body = ['id' => $id, 'action' => $claim_action];
                 
                 
                 
                if ($text !== '' && in_array($claim_action, ['rejectcustom', 'approvecc1'], true)) {
                    $body['comment'] = $this->safeString($text, 1000);
                }
                $result = $this->transport->json('PATCH', self::RETURNS_BASE . '/api/v1/claim', $headers, $body);
                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->failure($result, 'action_failed');
    }

    private function syncChat(string $token, array $cursor, string $token_type): array {
        $cursor = $this->upgradedChatCursor($cursor);
        $next = isset($cursor['next']) && is_numeric($cursor['next']) ? (string)$cursor['next'] : '';
        $url = self::CHAT_BASE . '/api/v1/seller/events' . ($next !== '' ? '?next=' . rawurlencode($next) : '');
        $result = $this->transport->json('GET', $url, $this->headers($token));
        if (empty($result['success'])) return $this->failure($result, 'chat_sync_failed');

        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $rows = is_array($source['events'] ?? null) ? $source['events'] : [];
        $chat_index = ['success' => true, 'chats' => []];
        if ($rows && $token_type !== 'base') {
             
             
             
            $chat_index = $this->chatIndex($token);
            if (empty($chat_index['success'])) {
                return $chat_index;
            }
        }
        $is_reconciliation = !empty($cursor['history_replay_active']);
        $quiet = empty($cursor['initialized']) || $is_reconciliation;
        $events = [];
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $chat_id = $this->safeString($row['chatID'] ?? '', 191);
            $chat = is_array($chat_index['chats'][$chat_id] ?? null) ? $chat_index['chats'][$chat_id] : [];
             
             
             
            if (empty($row['replySign']) && !empty($chat['replySign'])) $row['replySign'] = $chat['replySign'];
            if (empty($row['clientName']) && !empty($chat['clientName'])) $row['clientName'] = $chat['clientName'];
            $message = is_array($row['message'] ?? null) ? $row['message'] : [];
            $attachments = is_array($message['attachments'] ?? null) ? $message['attachments'] : [];
            if (empty($attachments['goodCard']) && !empty($chat['goodCard'])) {
                $attachments['goodCard'] = $chat['goodCard'];
                $message['attachments'] = $attachments;
                $row['message'] = $message;
            }
            $events[] = $this->normalizeChatEvent($row, $quiet, $is_reconciliation);
        }
        $new_next = $source['next'] ?? $json['next'] ?? $next;
        $total = (int)($source['totalEvents'] ?? $json['totalEvents'] ?? count($rows));
        if ($total > 0 && ((string)$new_next === '' || (string)$new_next === $next)) {
             
             
             
            return [
                'success' => false,
                'error' => 'marketplace_cursor_stalled',
                'retryable' => true,
            ];
        }
        $has_more = $total > 0 && (string)$new_next !== '' && (string)$new_next !== $next;
        $next_cursor = [
            'next' => (string)$new_next,
            'initialized' => $has_more ? !empty($cursor['initialized']) : true,
            'reliability_version' => self::CHAT_CURSOR_VERSION,
        ];
        if ($has_more && $is_reconciliation) {
            $next_cursor['history_replay_active'] = true;
        }
        return [
            'success' => true,
            'events' => $events,
             
             
            'cursor' => $next_cursor,
            'has_more' => $has_more,
            'delay_seconds' => 10,
        ];
    }

    private function syncQuestions(string $token, array $cursor, int $limit): array {
        $state = $this->upgradedFeedbackCursor($cursor, 'answered');
        $phase = (string)$state['phase'];
        $phase_cursor = is_array($state[$phase] ?? null) ? $state[$phase] : [];
        $offset = max(0, (int)($phase_cursor['page_offset'] ?? 0));
        $is_answered = $phase === 'answered';
        $url = self::FEEDBACK_BASE . '/api/v1/questions?isAnswered='
            . ($is_answered ? 'true' : 'false')
            . '&take=' . $limit . '&skip=' . $offset . '&order=dateDesc';
        $result = $this->transport->json('GET', $url, $this->headers($token));
        if (empty($result['success'])) return $this->failure($result, 'question_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $data = is_array($json['data'] ?? null) ? $json['data'] : $json;
        $rows = is_array($data['questions'] ?? null) ? $data['questions'] : [];
        $page = $is_answered
            ? $this->exhaustivePage($rows, $phase_cursor, function (array $row, bool $quiet): array {
                return $this->normalizeQuestionWithAnswer($row, $quiet);
            }, 30, $limit, $offset, 10000)
            : $this->newestPage($rows, $phase_cursor, 'createdDate', function (array $row, bool $quiet): array {
                return [$this->normalizeQuestion($row, $quiet, false)];
            }, 30, $limit, $offset, 10000);
        return $this->feedbackPhaseResult($state, $phase, $page, $is_answered ? 'open' : 'answered');
    }

    private function syncReviews(string $token, array $cursor, int $limit): array {
        $state = $this->upgradedFeedbackCursor($cursor, 'archive');
        $phase = (string)$state['phase'];
        $phase_cursor = is_array($state[$phase] ?? null) ? $state[$phase] : [];
        $offset = max(0, (int)($phase_cursor['page_offset'] ?? 0));
        $is_archive = $phase === 'archive';
        $url = self::FEEDBACK_BASE . ($is_archive ? '/api/v1/feedbacks/archive?' : '/api/v1/feedbacks?isAnswered=false&')
            . 'take=' . $limit . '&skip=' . $offset . '&order=dateDesc';
        $result = $this->transport->json('GET', $url, $this->headers($token));
        if (empty($result['success'])) return $this->failure($result, 'review_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $data = is_array($json['data'] ?? null) ? $json['data'] : $json;
        $rows = is_array($data['feedbacks'] ?? null) ? $data['feedbacks'] : [];
        $page = $is_archive
            ? $this->exhaustivePage($rows, $phase_cursor, function (array $row, bool $quiet): array {
                return $this->normalizeReviewWithAnswer($row, $quiet);
            }, 30, $limit, $offset, 200000)
            : $this->newestPage($rows, $phase_cursor, 'createdDate', function (array $row, bool $quiet): array {
                return [$this->normalizeReview($row, $quiet, false)];
            }, 30, $limit, $offset, 200000);
        return $this->feedbackPhaseResult($state, $phase, $page, $is_archive ? 'open' : 'archive');
    }

    private function syncReturns(string $token, array $cursor, int $limit): array {
        $offset = max(0, (int)($cursor['page_offset'] ?? 0));
        $url = self::RETURNS_BASE . '/api/v1/claims?is_archive=false&limit=' . $limit . '&offset=' . $offset;
        $result = $this->transport->json('GET', $url, $this->headers($token));
        if (empty($result['success'])) return $this->failure($result, 'return_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $rows = is_array($json['claims'] ?? null) ? $json['claims'] : [];
        return $this->newestPage($rows, $cursor, 'dt_update', function (array $row, bool $quiet) {
            return $this->normalizeReturn($row, $quiet);
        }, 60, $limit, $offset, 10000);
    }

    private function newestPage(array $rows, array $cursor, string $date_key, callable $normalizer, int $delay, int $limit, int $offset, int $max_offset): array {
        $watermark = max(0, (int)($cursor['scan_watermark'] ?? $cursor['watermark'] ?? 0));
        $quiet = empty($cursor['initialized']);
        $max_seen = max($watermark, (int)($cursor['scan_max_watermark'] ?? 0));
        $events = [];
        $reached_old_data = false;
         
         
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $timestamp = $this->timestamp($row[$date_key] ?? $row['dt'] ?? 0);
            $max_seen = max($max_seen, $timestamp);
            if (!$quiet && $watermark > 0 && $timestamp > 0 && $timestamp < $watermark) {
                $reached_old_data = true;
                continue;
            }
            $events = array_merge($events, $this->normalizedEvents($normalizer($row, $quiet)));
        }

        $next_offset = $offset + count($rows);
        $has_more = !$reached_old_data && count($rows) >= $limit && $next_offset < $max_offset;
        $next_cursor = $has_more
            ? [
                'watermark' => max(0, (int)($cursor['watermark'] ?? 0)),
                'scan_watermark' => $watermark,
                'scan_max_watermark' => $max_seen,
                'page_offset' => $next_offset,
                'initialized' => !empty($cursor['initialized']),
            ]
            : ['watermark' => $max_seen, 'page_offset' => 0, 'initialized' => true];
        return [
            'success' => true,
            'events' => $events,
            'cursor' => $next_cursor,
            'has_more' => $has_more,
            'delay_seconds' => $has_more ? 1 : $delay,
        ];
    }

    





    private function exhaustivePage(
        array $rows,
        array $cursor,
        callable $normalizer,
        int $delay,
        int $limit,
        int $offset,
        int $max_offset
    ): array {
        $quiet = empty($cursor['initialized']);
        $events = [];
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $events = array_merge($events, $this->normalizedEvents($normalizer($row, $quiet)));
        }
        $next_offset = $offset + count($rows);
        $has_more = count($rows) >= $limit && $next_offset < $max_offset;
        return [
            'success' => true,
            'events' => $events,
            'cursor' => $has_more
                ? ['page_offset' => $next_offset, 'initialized' => !empty($cursor['initialized'])]
                : ['page_offset' => 0, 'initialized' => true],
            'has_more' => $has_more,
            'delay_seconds' => $has_more ? 1 : $delay,
        ];
    }

    private function normalizedEvents($value): array {
        if (!is_array($value)) {
            return [];
        }
        if (isset($value['conversation_kind'])) {
            return [$value];
        }
        return array_values(array_filter($value, static function ($event): bool {
            return is_array($event) && isset($event['conversation_kind']);
        }));
    }

    private function upgradedFeedbackCursor(array $cursor, string $history_phase): array {
        if ((int)($cursor['feedback_cursor_version'] ?? 0) >= self::FEEDBACK_CURSOR_VERSION) {
            $phase = (string)($cursor['phase'] ?? 'open');
            if (!in_array($phase, ['open', $history_phase], true)) {
                $phase = 'open';
            }
            return [
                'feedback_cursor_version' => self::FEEDBACK_CURSOR_VERSION,
                'phase' => $phase,
                'open' => is_array($cursor['open'] ?? null) ? $cursor['open'] : [],
                $history_phase => is_array($cursor[$history_phase] ?? null) ? $cursor[$history_phase] : [],
            ];
        }

         
         
         
         
        return [
            'feedback_cursor_version' => self::FEEDBACK_CURSOR_VERSION,
            'phase' => 'open',
            'open' => ['page_offset' => 0, 'initialized' => false],
            $history_phase => ['page_offset' => 0, 'initialized' => false],
        ];
    }

    private function feedbackPhaseResult(array $state, string $phase, array $page, string $next_phase): array {
        $state[$phase] = is_array($page['cursor'] ?? null) ? $page['cursor'] : [];
        if (empty($page['has_more'])) {
            $state['phase'] = $next_phase;
        }
        return [
            'success' => true,
            'events' => is_array($page['events'] ?? null) ? $page['events'] : [],
            'cursor' => $state,
            'has_more' => !empty($page['has_more']),
            'delay_seconds' => max(1, (int)($page['delay_seconds'] ?? 30)),
        ];
    }

    private function upgradedChatCursor(array $cursor): array {
        if ((int)($cursor['reliability_version'] ?? 0) >= self::CHAT_CURSOR_VERSION) {
            return $cursor;
        }
        if ($cursor) {
             
             
             
             
            return [
                'next' => '',
                'initialized' => false,
                'history_replay_active' => true,
                'reliability_version' => self::CHAT_CURSOR_VERSION,
            ];
        }
        return [
            'next' => '',
            'initialized' => false,
            'reliability_version' => self::CHAT_CURSOR_VERSION,
        ];
    }

    private function chatIndex(string $token): array {
        $result = $this->transport->json('GET', self::CHAT_BASE . '/api/v1/seller/chats', $this->headers($token));
        if (empty($result['success'])) return $this->failure($result, 'chat_list_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $rows = is_array($json['result'] ?? null) ? $json['result'] : [];
        $chats = [];
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $chat_id = $this->safeString($row['chatID'] ?? '', 191);
            if ($chat_id !== '') $chats[$chat_id] = $row;
        }
        return ['success' => true, 'chats' => $chats];
    }

    private function normalizeChatEvent(array $row, bool $quiet, bool $reconciliation = false): array {
        $message = is_array($row['message'] ?? null) ? $row['message'] : [];
        $attachments = is_array($message['attachments'] ?? null) ? $message['attachments'] : [];
        $chat_id = $this->safeString($row['chatID'] ?? '', 191);
        $event_id = $this->safeString($row['eventID'] ?? '', 128);
        $out = strtolower((string)($row['sender'] ?? '')) !== 'client';
        $source = strtolower($this->safeString($row['source'] ?? '', 64));
        $add_timestamp = self::canonicalProviderTimestamp(
            $row['addTimestamp'] ?? $row['addTime'] ?? ''
        );
        $outbound_correlation_id = $out && $source === 'seller-public-api'
            ? self::outboundCorrelationId($chat_id, $add_timestamp)
            : '';
        $good = is_array($attachments['goodCard'] ?? null) ? $attachments['goodCard'] : [];
        return [
            'update_type' => 'message',
            'text' => $this->safeString($message['text'] ?? '', 10000),
             
             
             
            'external_user_id' => $chat_id,
            'external_chat_id' => $chat_id,
            'external_message_id' => $outbound_correlation_id !== ''
                ? $outbound_correlation_id
                : $event_id,
            'dedupe_key' => 'wb:chat:' . ($event_id !== '' ? $event_id : hash('sha256', json_encode($row))),
            'direction' => $out ? 'out' : 'in',
            'display_name' => $this->safeString($row['clientName'] ?? 'Покупатель Wildberries', 255),
            'attachments' => $this->wbAttachments($attachments),
            'conversation_kind' => MarketplaceStream::BUYER_CHAT,
            'external_thread_id' => $chat_id,
            'thread_state' => 'open',
            'reply_secret' => $this->safeString($row['replySign'] ?? '', 2000),
            'item_context' => $this->wbItemContext($good),
            'capabilities' => [
                'can_reply' => trim((string)($row['replySign'] ?? '')) !== '',
                'read_only' => trim((string)($row['replySign'] ?? '')) === '',
            ],
            'published_at' => $this->isoDate($row['addTimestamp'] ?? $row['addTime'] ?? ''),
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'wb_chat_event',
                'sync_origin' => $reconciliation ? 'history_reconciliation' : 'polling',
                'provider_source' => $source,
                'provider_event_id' => $event_id,
                'provider_add_timestamp' => $add_timestamp,
                'outbound_correlation_id' => $outbound_correlation_id,
            ],
            'snapshot' => $this->sanitizedSnapshot($row, ['replySign']),
        ];
    }

    





    private static function outboundCorrelationId(string $chat_id, $timestamp): string {
        $chat_id = trim($chat_id);
        $timestamp = self::canonicalProviderTimestamp($timestamp);
        if ($chat_id === '' || $timestamp === '') {
            return '';
        }
        return 'wb-send:' . hash('sha256', $chat_id . '|' . $timestamp);
    }

    private static function canonicalProviderTimestamp($timestamp): string {
        $timestamp = trim((string)$timestamp);
        if ($timestamp === '' || preg_match('/^\d{10,19}$/', $timestamp) !== 1) {
            return '';
        }
        $timestamp = ltrim($timestamp, '0');
        return $timestamp !== '' ? $timestamp : '0';
    }

    private function normalizeQuestion(array $row, bool $quiet, bool $answered): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $product = is_array($row['productDetails'] ?? null) ? $row['productDetails'] : [];
        return [
            'update_type' => 'question',
            'text' => $this->safeString($row['text'] ?? '', 10000),
            'external_user_id' => 'question:' . $id,
            'external_chat_id' => 'question:' . $id,
            'external_message_id' => 'question:' . $id,
            'dedupe_key' => 'wb:question:' . $id,
            'direction' => 'in',
            'display_name' => $this->safeString($row['userName'] ?? 'Покупатель Wildberries', 255),
            'attachments' => [],
            'conversation_kind' => MarketplaceStream::QUESTION,
            'external_thread_id' => $id,
            'thread_state' => $answered ? 'answered' : strtolower((string)($row['state'] ?? 'open')),
            'item_context' => $this->wbItemContext($product),
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => true,
                'actions' => $answered ? [] : ['question_answer', 'question_reject', 'question_mark_viewed'],
            ],
            'published_at' => $this->isoDate($row['createdDate'] ?? ''),
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'wb_question',
                'sync_origin' => $answered ? 'history_reconciliation' : 'polling',
            ],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function normalizeQuestionWithAnswer(array $row, bool $quiet): array {
        $events = [$this->normalizeQuestion($row, true, true)];
        $answer = is_array($row['answer'] ?? null) ? $row['answer'] : [];
        $text = $this->safeString($answer['text'] ?? '', 5000);
        $id = $this->safeString($row['id'] ?? '', 191);
        if ($id === '' || $text === '') {
            return $events;
        }
        $answer_date = $this->isoDate($answer['createDate'] ?? '');
        $events[] = $this->normalizeSellerAnswer(
            MarketplaceStream::QUESTION,
            $row,
            $text,
            $answer_date !== '' ? $answer_date : $this->isoDate($row['createdDate'] ?? ''),
            $answer_date !== '',
            $quiet,
            []
        );
        return $events;
    }

    private function normalizeReview(array $row, bool $quiet, bool $archived): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $product = is_array($row['productDetails'] ?? null) ? $row['productDetails'] : [];
        $parts = [];
        if (isset($row['productValuation'])) $parts[] = 'Оценка: ' . (int)$row['productValuation'] . '/5';
        if (trim((string)($row['text'] ?? '')) !== '') $parts[] = trim((string)$row['text']);
        if (trim((string)($row['pros'] ?? '')) !== '') $parts[] = 'Плюсы: ' . trim((string)$row['pros']);
        if (trim((string)($row['cons'] ?? '')) !== '') $parts[] = 'Минусы: ' . trim((string)$row['cons']);
        $attachments = [];
        foreach ((array)($row['photoLinks'] ?? []) as $photo) {
            $url = is_array($photo) ? ($photo['fullSize'] ?? $photo['miniSize'] ?? '') : $photo;
            if (is_string($url) && strpos($url, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $url,
                    'payload_hash' => $this->attachmentHash('review-image', $url, ''),
                ];
            }
        }
        $answer = is_array($row['answer'] ?? null) ? $row['answer'] : [];
        $has_answer = trim((string)($answer['text'] ?? '')) !== '';
        $actions = [];
        if (!$archived) {
            $actions = ['review_answer'];
        } elseif ($has_answer && !empty($answer['editable'])) {
            $actions = ['review_edit'];
        }
        return [
            'update_type' => 'review',
            'text' => $this->safeString(implode("\n", $parts), 10000),
            'external_user_id' => 'review:' . $id,
            'external_chat_id' => 'review:' . $id,
            'external_message_id' => 'review:' . $id,
            'dedupe_key' => 'wb:review:' . $id,
            'direction' => 'in',
            'display_name' => $this->safeString($row['userName'] ?? 'Покупатель Wildberries', 255),
            'attachments' => $attachments,
            'conversation_kind' => MarketplaceStream::REVIEW,
            'external_thread_id' => $id,
            'thread_state' => $has_answer ? 'answered' : ($archived ? 'archived' : 'open'),
            'item_context' => $this->wbItemContext($product),
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => true,
                'actions' => $actions,
            ],
            'published_at' => $this->isoDate($row['createdDate'] ?? ''),
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'wb_review',
                'sync_origin' => $archived ? 'history_reconciliation' : 'polling',
            ],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function normalizeReviewWithAnswer(array $row, bool $quiet): array {
        $events = [$this->normalizeReview($row, true, true)];
        $answer = is_array($row['answer'] ?? null) ? $row['answer'] : [];
        $text = $this->safeString($answer['text'] ?? '', 5000);
        $id = $this->safeString($row['id'] ?? '', 191);
        if ($id === '' || $text === '') {
            return $events;
        }
        $answer_date = $this->isoDate($answer['createDate'] ?? '');
        $events[] = $this->normalizeSellerAnswer(
            MarketplaceStream::REVIEW,
            $row,
            $text,
            $answer_date !== '' ? $answer_date : $this->isoDate($row['createdDate'] ?? ''),
            $answer_date !== '',
            $quiet,
            !empty($answer['editable']) ? ['review_edit'] : []
        );
        return $events;
    }

    private function normalizeSellerAnswer(
        string $kind,
        array $row,
        string $text,
        string $published_at,
        bool $trusted_date,
        bool $quiet,
        array $actions
    ): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $product = is_array($row['productDetails'] ?? null) ? $row['productDetails'] : [];
        return [
            'update_type' => $kind . '_answer',
            'text' => $text,
            'external_user_id' => $kind . ':' . $id,
            'external_chat_id' => $kind . ':' . $id,
            'external_message_id' => $kind . ':' . $id . ':answer',
            'dedupe_key' => 'wb:' . $kind . ':' . $id . ':answer',
            'direction' => 'out',
            'display_name' => $this->safeString($row['userName'] ?? 'Покупатель Wildberries', 255),
            'attachments' => [],
            'conversation_kind' => $kind,
            'external_thread_id' => $id,
            'thread_state' => 'answered',
            'item_context' => $this->wbItemContext($product),
            'capabilities' => ['can_reply' => false, 'draft_only' => true, 'actions' => $actions],
            'published_at' => $published_at,
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'raw_kind' => 'wb_' . $kind . '_answer',
                'sync_origin' => 'history_reconciliation',
                'mutable_external_message' => true,
                'mutable_event_date' => $trusted_date,
            ],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function normalizeReturn(array $row, bool $quiet): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $actions = array_values(array_filter(array_map('strval', (array)($row['actions'] ?? []))));
        $attachments = [];
        foreach ((array)($row['photos'] ?? []) as $photo) {
            $photo = $this->wbAssetUrl($photo);
            if ($photo !== '') {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $photo,
                    'payload_hash' => $this->attachmentHash('return-image', $photo, ''),
                ];
            }
        }
        foreach ((array)($row['video_paths'] ?? []) as $video) {
            $video = $this->wbAssetUrl($video);
            if ($video !== '') $attachments[] = ['type' => 'video', 'url' => $video, 'unsupported' => true];
        }
        $workflow_actions = [];
        foreach ($actions as $action) {
            $workflow_actions[] = [
                'code' => $action,
                'name' => $this->wbReturnActionLabel($action),
                 
                 
                'requires_comment' => $action === 'rejectcustom',
                'accepts_comment' => in_array($action, ['rejectcustom', 'approvecc1'], true),
            ];
        }
        return [
            'update_type' => 'return_claim',
            'text' => $this->safeString($row['user_comment'] ?? 'Заявка покупателя на возврат', 10000),
            'external_user_id' => 'return:' . $id,
            'external_chat_id' => 'return:' . $id,
            'external_message_id' => 'return:' . $id . ':' . $this->timestamp($row['dt_update'] ?? $row['dt'] ?? 0),
            'dedupe_key' => 'wb:return:' . $id . ':' . $this->timestamp($row['dt_update'] ?? $row['dt'] ?? 0),
            'direction' => 'in',
            'display_name' => 'Покупатель Wildberries',
            'attachments' => $attachments,
            'conversation_kind' => MarketplaceStream::RETURN_CLAIM,
            'external_thread_id' => $id,
            'thread_state' => $this->safeString($row['status'] ?? 'open', 32),
            'item_context' => $this->wbItemContext(['nmId' => $row['nm_id'] ?? '', 'productName' => $row['imt_name'] ?? '']),
            'order_id' => $this->safeString($row['srid'] ?? '', 128),
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => false,
                'actions' => $actions ? ['return_action'] : [],
                'allowed_claim_actions' => $actions,
                'workflow_actions' => $workflow_actions,
            ],
            'workflow_actions' => $workflow_actions,
            'published_at' => $this->isoDate($row['dt_update'] ?? $row['dt'] ?? ''),
            'metadata' => ['marketplace' => true, 'quiet_import' => $quiet, 'raw_kind' => 'wb_return'],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function wbAttachments(array $source): array {
        $result = [];
        foreach ((array)($source['images'] ?? []) as $image) {
            if (is_string($image)) {
                if (strpos($image, 'https://') === 0) {
                    $result[] = [
                        'type' => 'image',
                        'url' => $image,
                        'payload_hash' => $this->attachmentHash('image', $image, ''),
                    ];
                }
                continue;
            }
            if (!is_array($image)) continue;
            $url = (string)($image['url'] ?? $image['fullSize'] ?? '');
            $download_id = $this->safeString($image['downloadID'] ?? $image['downloadId'] ?? '', 128);
            if ($url === '' && $download_id !== '') {
                $url = self::CHAT_BASE . '/api/v1/seller/download/' . rawurlencode($download_id);
            }
            if (strpos($url, 'https://') !== 0) continue;
            $result[] = [
                'type' => 'image',
                'url' => $url,
                'file_name' => $this->safeString($image['name'] ?? $image['fileName'] ?? 'image.jpg', 180),
                'auth_required' => $download_id !== '',
                'payload_hash' => $this->attachmentHash('image', $download_id !== '' ? $download_id : $url, (string)($image['name'] ?? $image['fileName'] ?? '')),
            ];
        }
        foreach ((array)($source['files'] ?? []) as $file) {
            if (!is_array($file)) continue;
            $url = (string)($file['url'] ?? '');
            $download_id = $this->safeString($file['downloadID'] ?? $file['downloadId'] ?? '', 128);
            if ($url === '' && $download_id !== '') $url = self::CHAT_BASE . '/api/v1/seller/download/' . rawurlencode($download_id);
            if (strpos($url, 'https://') !== 0) continue;
            $result[] = [
                'type' => 'document', 'url' => $url,
                'file_name' => $this->safeString($file['name'] ?? $file['fileName'] ?? 'file', 180),
                'auth_required' => $download_id !== '',
                'payload_hash' => $this->attachmentHash('document', $download_id !== '' ? $download_id : $url, (string)($file['name'] ?? $file['fileName'] ?? '')),
            ];
        }
        return $result;
    }

    private function attachmentHash(string $type, string $identity, string $name): string {
        return hash('sha256', self::PROVIDER . '|' . $type . '|' . trim($identity) . '|' . trim($name));
    }

     
    private function wbAssetUrl($value): string {
        if (!is_string($value)) return '';
        $url = trim($value);
        if (strpos($url, '//') === 0) $url = 'https:' . $url;
        return strpos($url, 'https://') === 0 ? $url : '';
    }

    private function wbItemContext(array $source): array {
        $id = $this->safeString($source['nmId'] ?? $source['nmID'] ?? $source['nm_id'] ?? '', 128);
        if ($id === '') return [];
        return [
            'provider' => self::PROVIDER,
            'item_id' => $id,
            'title' => $this->safeString($source['productName'] ?? $source['imtName'] ?? '', 500),
            'sku' => $this->safeString($source['supplierArticle'] ?? '', 128),
            'brand' => $this->safeString($source['brandName'] ?? '', 255),
            'url' => 'https://www.wildberries.ru/catalog/' . rawurlencode($id) . '/detail.aspx',
            'image_url' => $this->safeString($source['photoLink'] ?? $source['photoUrl'] ?? '', 2000),
        ];
    }

    private function wbReturnActionLabel(string $action): string {
        $labels = [
            'approve1' => 'Одобрить возврат',
            'approvecc1' => 'Одобрить с комментарием',
            'reject1' => 'Отклонить заявку',
            'rejectcustom' => 'Отклонить с объяснением',
        ];
        return $labels[$action] ?? ('Выполнить действие: ' . $action);
    }

    private function apiToken(array $credentials): string {
        $token = trim((string)($credentials['api_token'] ?? ''));
        return preg_replace('/^Bearer\s+/i', '', $token);
    }

    private function headers(string $token, bool $json = true): array {
        $headers = ['Authorization: ' . $token, 'Accept: application/json'];
        if ($json) $headers[] = 'Content-Type: application/json';
        return $headers;
    }

    private function tokenType(string $token): string {
        $inspection = WildberriesTokenInspector::inspect($token);
        return !empty($inspection['success']) ? (string)$inspection['token_type'] : 'unknown';
    }

}
