<?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 OzonProvider extends AbstractMarketplaceProvider {
    public const PROVIDER = 'ozon';
    public const API_BASE = 'https://api-seller.ozon.ru';

    private const WRITABLE_CHAT_TYPES = ['buyer_seller'];
    private const NOTIFICATION_TYPES = [
        'TYPE_NEW_MESSAGE',
        'TYPE_UPDATE_MESSAGE',
        'TYPE_MESSAGE_READ',
        'TYPE_CHAT_CLOSED',
    ];
    private const QUESTION_STATUSES = ['NEW', 'VIEWED'];

    public function __construct($registry = null, ?MarketplaceHttpTransport $transport = null) {
        parent::__construct(
            $registry,
            $transport ?: new MarketplaceHttpTransport(new ExternalHttpClient($registry), ['api-seller.ozon.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,
            'account_actions' => ['start_conversation'],
            'streams' => [
                MarketplaceStream::BUYER_CHAT => [
                    'read' => true, 'reply' => true, 'attachments_in' => true,
                    'attachments_out' => true, 'read_receipts' => true,
                    'formats' => AttachmentPolicy::allowedAttachmentExtensions(true),
                ],
                MarketplaceStream::QUESTION => [
                    'read' => true, 'reply' => false, 'draft' => true,
                    'actions' => ['question_answer', 'question_mark_viewed', 'question_mark_processed'],
                ],
                MarketplaceStream::REVIEW => [
                    'read' => true, 'reply' => false, 'draft' => true,
                    'actions' => ['review_answer', 'review_mark_processed'],
                ],
                MarketplaceStream::RETURN_CLAIM => [
                    'read' => true, 'reply' => false, 'draft' => false,
                    'actions_from_payload' => true,
                ],
            ],
        ];
    }

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

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

        $result = $this->api('/v1/roles', $credentials, []);
        if (empty($result['success'])) return $this->failure($result, 'auth_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $methods = [];
        foreach ((array)($source['roles'] ?? []) as $role) {
            if (!is_array($role)) continue;
            foreach ((array)($role['methods'] ?? []) as $method) {
                $method = '/' . ltrim(strtolower(trim((string)$method)), '/');
                if ($method !== '/') $methods[$method] = true;
            }
        }

        $requirements = [
            MarketplaceStream::BUYER_CHAT => [
                'read' => ['/v3/chat/list', '/v3/chat/history'],
                'actions' => [
                    'buyer_chat_reply' => ['/v1/chat/send/message'],
                    'buyer_chat_attachment' => ['/v1/chat/send/file'],
                    'buyer_chat_read' => ['/v2/chat/read'],
                ],
            ],
            MarketplaceStream::QUESTION => [
                'read' => ['/v1/question/list'],
                'actions' => [
                    'question_answer' => ['/v1/question/answer/create'],
                    'question_mark_viewed' => ['/v1/question/change-status'],
                    'question_mark_processed' => ['/v1/question/change-status'],
                ],
            ],
            MarketplaceStream::REVIEW => [
                'read' => ['/v2/review/list', '/v2/review/info'],
                'actions' => [
                    'review_answer' => ['/v1/review/comment/create'],
                    'review_mark_processed' => ['/v2/review/change-status'],
                ],
            ],
            MarketplaceStream::RETURN_CLAIM => [
                'read' => ['/v2/returns/rfbs/list', '/v2/returns/rfbs/get'],
                'actions' => ['return_action' => ['/v1/returns/rfbs/action/set']],
            ],
        ];
        $access = [];
        foreach ($requirements as $stream => $requirement) {
            $missing_read = [];
            foreach ($requirement['read'] as $path) {
                if ($methods && !isset($methods[$path])) $missing_read[] = $path;
            }
            $action_access = [];
            foreach ($requirement['actions'] as $action => $paths) {
                $missing = [];
                foreach ($paths as $path) {
                    if ($methods && !isset($methods[$path])) $missing[] = $path;
                }
                $action_access[$action] = $methods ? !$missing : null;
            }
            $access[$stream] = [
                 
                 
                'available' => $methods ? !$missing_read : null,
                'missing_read_methods' => $missing_read,
                'action_access' => $action_access,
            ];
        }
        $account_action_access = [
            'start_conversation' => $methods ? isset($methods['/v1/chat/start']) : null,
        ];

        return [
            'success' => true,
            'account' => ['id' => $client_id, 'name' => 'Ozon · кабинет ' . $client_id],
            'capabilities' => array_merge($this->marketplaceCapabilities(), [
                'stream_access' => $access,
                'account_action_access' => $account_action_access,
                'key_expires_at' => $this->safeString($source['expires_at'] ?? '', 64),
                'notification_api' => isset($methods['/v1/notification/set']) || !$methods,
            ]),
            'diagnostics' => [
                'roles' => array_values(array_filter(array_map(function ($role) {
                    return is_array($role) ? $this->safeString($role['name'] ?? '', 128) : '';
                }, (array)($source['roles'] ?? [])))),
                'stream_access' => $access,
                'account_action_access' => $account_action_access,
            ],
        ];
    }

    public function syncWebhook(array $channel, string $token, string $webhook_url, string $secret): array {
        $credentials = $this->credentials($token);
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        if (!$this->safeWebhookUrl($webhook_url)) {
            return ['success' => false, 'error' => 'webhook_url_invalid'];
        }

        $check = $this->api('/v1/notification/check', $credentials, ['url' => $webhook_url]);
        $check_json = is_array($check['json'] ?? null) ? $check['json'] : [];
        $check_data = is_array($check_json['result'] ?? null) ? $check_json['result'] : $check_json;
        $check_active = array_key_exists('is_active', $check_data) && !empty($check_data['is_active']);
         
         
         
        if (empty($check['success']) && !$check_active) {
            return $this->failure($check, 'notification_check_failed');
        }
        if (array_key_exists('is_active', $check_data) && empty($check_data['is_active'])) {
            return ['success' => false, 'error' => 'notification_url_inactive', 'details' => $check_data['errors'] ?? []];
        }

        $existing = $this->notificationByUrl($credentials, $webhook_url);
        if (empty($existing['success'])) {
            return $existing;
        }
        $notification = is_array($existing['notification'] ?? null) ? $existing['notification'] : [];
        $notification_id = (int)($notification['id'] ?? 0);
        if ($notification_id > 0) {
            $updated = $this->api('/v1/notification/update', $credentials, [
                'id' => $notification_id,
                'types' => self::NOTIFICATION_TYPES,
                'url' => $webhook_url,
            ]);
            if (empty($updated['success'])) return $this->failure($updated, 'notification_update_failed');
            if (empty($notification['enable'])) {
                $enabled = $this->api('/v1/notification/enable', $credentials, [
                    'id' => $notification_id,
                    'enabled' => true,
                ]);
                if (empty($enabled['success'])) return $this->failure($enabled, 'notification_enable_failed');
            }
        } else {
            $set = $this->api('/v1/notification/set', $credentials, [
                'types' => self::NOTIFICATION_TYPES,
                'url' => $webhook_url,
            ]);
            if (empty($set['success'])) return $this->failure($set, 'notification_set_failed');
        }
        return [
            'success' => true,
            'mode' => 'push_with_polling_reconciliation',
            'webhook_supported' => true,
            'types' => self::NOTIFICATION_TYPES,
            'notification_id' => $notification_id,
            'registration' => $notification_id > 0 ? 'updated' : 'created',
            'http_code' => 200,
        ];
    }

     
    public function removeWebhook(array $credentials, string $webhook_url): array {
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        if (!$this->safeWebhookUrl($webhook_url)) {
            return ['success' => false, 'error' => 'webhook_url_invalid'];
        }
        $list = $this->api('/v1/notification/list', $credentials, []);
        if (empty($list['success'])) return $this->failure($list, 'notification_list_failed');
        $matches = $this->notificationEntries($list);
        $removed = 0;
        foreach ($matches as $notification) {
            if ((string)($notification['url'] ?? '') !== $webhook_url || (int)($notification['id'] ?? 0) <= 0) continue;
            $deleted = $this->api('/v1/notification/delete', $credentials, ['id' => (int)$notification['id']]);
            if (empty($deleted['success'])) {
                return $this->failure($deleted, 'notification_delete_failed') + ['removed' => $removed];
            }
            $removed++;
        }
        return ['success' => true, 'removed' => $removed];
    }

    public function syncMarketplaceStream(string $stream, array $credentials, array $cursor, int $limit = 50): array {
        $stream = MarketplaceStream::normalize($stream);
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        $limit = max(1, min(100, $limit));
        switch ($stream) {
            case MarketplaceStream::BUYER_CHAT:
                return $this->syncChats($credentials, $cursor, min($limit, 5));
            case MarketplaceStream::QUESTION:
                return $this->syncQuestions($credentials, $cursor, $limit);
            case MarketplaceStream::REVIEW:
                 
                 
                 
                 
                return $this->syncReviews($credentials, $cursor, 20);
            case MarketplaceStream::RETURN_CLAIM:
                return $this->syncReturns($credentials, $cursor, min($limit, 4));
        }
        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);
        $chat_id = $this->safeString($options['external_chat_id'] ?? $options['external_thread_id'] ?? '', 191);
        $chat_type = strtolower($this->safeString($options['chat_type'] ?? 'buyer_seller', 64));
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') return ['success' => false, 'error' => 'auth_failed'];
        if ($chat_id === '') return ['success' => false, 'error' => 'chat_id_missing'];
        if (!in_array($chat_type, self::WRITABLE_CHAT_TYPES, true)) return ['success' => false, 'error' => 'chat_read_only'];

        $ids = [];
        $text = trim($text);
        if (count($attachments) > 10) {
            return ['success' => false, 'error' => 'attachment_limit_exceeded'];
        }
        if ($text !== '') {
            $sent = $this->api('/v1/chat/send/message', $credentials, ['chat_id' => $chat_id, 'text' => $text]);
            if (empty($sent['success'])) return $this->writeFailure($sent, 'send_failed');
            $sent_json = is_array($sent['json'] ?? null) ? $sent['json'] : [];
            $ids[] = $this->safeString($sent_json['result'] ?? '', 128);
        }

        foreach ($attachments as $attachment) {
            $path = (string)($attachment['path'] ?? '');
            if (!is_file($path)) return $this->partialSendFailure('attachment_missing', $ids);
            $size = (int)filesize($path);
             
             
            if ($size <= 0 || $size > 10 * 1024 * 1024) return $this->partialSendFailure('attachment_size_exceeded', $ids);
            $name = $this->safeString($attachment['original_name'] ?? basename($path), 180);
            $validation = AttachmentPolicy::validateAttachmentFile($path, $name, $size, 10, true);
            if (empty($validation['success'])) {
                return $this->partialSendFailure('attachment_format_not_supported', $ids);
            }
            $raw = file_get_contents($path);
            if (!is_string($raw)) return $this->partialSendFailure('attachment_read_failed', $ids);
            $file = $this->api('/v1/chat/send/file', $credentials, [
                'base64_content' => base64_encode($raw),
                'chat_id' => $chat_id,
                'name' => $name,
            ], 25);
            unset($raw);
            if (empty($file['success'])) {
                $failure = $this->writeFailure($file, 'file_send_failed');
                return $ids ? array_merge($failure, $this->partialSendMetadata($ids)) : $failure;
            }
            $file_json = is_array($file['json'] ?? null) ? $file['json'] : [];
            $ids[] = $this->safeString($file_json['result'] ?? '', 128);
        }

        if (!$ids) return ['success' => false, 'error' => 'message_empty'];
        return [
            'success' => true,
            'external_message_id' => (string)end($ids),
            'external_message_ids' => array_values(array_filter($ids)),
            'external_chat_id' => $chat_id,
        ];
    }

    private function partialSendFailure(string $error, array $ids): array {
        $failure = ['success' => false, 'error' => $error];
        return $ids ? array_merge($failure, $this->partialSendMetadata($ids)) : $failure;
    }

    private function partialSendMetadata(array $ids): array {
        $ids = array_values(array_filter(array_map('strval', $ids)));
        return [
            'partial_delivery' => true,
            'external_message_id' => $ids ? (string)end($ids) : '',
            'external_message_ids' => $ids,
             
            'retryable' => false,
        ];
    }

    public function startMarketplaceConversation(array $credentials, array $context): array {
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        $posting_number = $this->safeString($context['posting_number'] ?? '', 80);
        if ($posting_number === '' || preg_match('/^[a-z0-9._\-\/]{3,80}$/i', $posting_number) !== 1) {
            return ['success' => false, 'error' => 'ozon_posting_number_invalid'];
        }
        $result = $this->api('/v1/chat/start', $credentials, ['posting_number' => $posting_number]);
        if (empty($result['success'])) {
            return $this->failure($result, 'marketplace_start_failed');
        }
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $chat_id = $this->safeString($source['chat_id'] ?? '', 191);
        if ($chat_id === '') {
             
             
            return [
                'success' => false,
                'error' => 'marketplace_start_response_invalid',
                'http_code' => (int)($result['http_code'] ?? 200),
                'outcome_unknown' => true,
            ];
        }
        return [
            'success' => true,
            'external_chat_id' => $chat_id,
            'external_thread_id' => $chat_id,
            'posting_number' => $posting_number,
            'http_code' => (int)($result['http_code'] ?? 200),
        ];
    }

    public function performMarketplaceAction(string $action, array $credentials, array $context): array {
        $id = $this->safeString($context['external_thread_id'] ?? $context['id'] ?? '', 191);
        if ($id === '') return ['success' => false, 'error' => 'thread_id_missing'];
        $text = $this->safeString($context['text'] ?? '', 5000);
        $result = [];
        switch ($action) {
            case 'question_answer':
                $sku = (int)($context['sku'] ?? 0);
                if ($sku <= 0 || $this->utf8Length($text) < 2 || $this->utf8Length($text) > 3000) {
                    return ['success' => false, 'error' => $sku <= 0 ? 'question_sku_required' : 'answer_text_invalid'];
                }
                $result = $this->api('/v1/question/answer/create', $credentials, ['question_id' => $id, 'sku' => $sku, 'text' => $text]);
                break;
            case 'question_mark_viewed':
            case 'question_mark_processed':
                $result = $this->api('/v1/question/change-status', $credentials, [
                    'question_ids' => [$id],
                    'status' => $action === 'question_mark_viewed' ? 'VIEWED' : 'PROCESSED',
                ]);
                break;
            case 'review_answer':
                if ($text === '') return ['success' => false, 'error' => 'answer_text_required'];
                $body = ['review_id' => $id, 'text' => $text, 'mark_review_as_processed' => true];
                if (!empty($context['parent_comment_id'])) $body['parent_comment_id'] = (string)$context['parent_comment_id'];
                $result = $this->api('/v1/review/comment/create', $credentials, $body);
                break;
            case 'review_mark_processed':
                $result = $this->api('/v2/review/change-status', $credentials, ['review_ids' => [$id], 'status' => 'PROCESSED']);
                break;
            case 'return_action':
                $return_id = (int)$id;
                $action_id = (int)($context['action_id'] ?? 0);
                $allowed = array_map('intval', (array)($context['allowed_action_ids'] ?? []));
                if ($return_id <= 0 || $action_id <= 0 || !in_array($action_id, $allowed, true)) {
                    return ['success' => false, 'error' => 'return_action_not_allowed'];
                }
                $body = ['return_id' => $return_id, 'id' => $action_id];
                foreach (['compensation_amount', 'rejection_reason_id', 'return_for_back_way'] as $field) {
                    if (isset($context[$field]) && $context[$field] !== '') $body[$field] = $context[$field] + 0;
                }
                if ($text !== '') $body['comment'] = $text;
                $result = $this->api('/v1/returns/rfbs/action/set', $credentials, $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');
    }

    public function markMarketplaceThreadRead(array $credentials, array $context): array {
        $chat_id = $this->safeString($context['external_chat_id'] ?? $context['external_thread_id'] ?? '', 191);
        $from_message_id = (int)($context['from_message_id'] ?? 0);
        if ($this->clientId($credentials) === '' || $this->apiKey($credentials) === '') {
            return ['success' => false, 'error' => 'auth_failed'];
        }
        if ($chat_id === '' || $from_message_id <= 0) {
            return ['success' => false, 'error' => 'read_receipt_context_invalid'];
        }
        $result = $this->api('/v2/chat/read', $credentials, [
            'chat_id' => $chat_id,
            'from_message_id' => $from_message_id,
        ]);
        return !empty($result['success'])
            ? ['success' => true]
            : $this->failure($result, 'read_receipt_failed');
    }

    public function notificationTypes(array $credentials): array {
        $result = $this->api('/v1/notification/push-type/list', $credentials, []);
        return !empty($result['success'])
            ? ['success' => true, 'types' => (array)(($result['json']['types'] ?? $result['json']['result']['types'] ?? []))]
            : $this->failure($result, 'notification_types_failed');
    }

    private function notificationByUrl(array $credentials, string $webhook_url): array {
        $list = $this->api('/v1/notification/list', $credentials, []);
        if (empty($list['success'])) return $this->failure($list, 'notification_list_failed');
        foreach ($this->notificationEntries($list) as $notification) {
            if ((string)($notification['url'] ?? '') === $webhook_url && (int)($notification['id'] ?? 0) > 0) {
                return ['success' => true, 'notification' => $notification];
            }
        }
        return ['success' => true, 'notification' => []];
    }

    private function notificationEntries(array $result): array {
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        return is_array($source['urls'] ?? null) ? $source['urls'] : [];
    }

    private function syncChats(array $credentials, array $cursor, int $limit): array {
        $list_cursor = $this->safeString($cursor['list_cursor'] ?? '', 2000);
        $body = ['filter' => ['chat_status' => 'All', 'unread_only' => false], 'limit' => $limit];
        if ($list_cursor !== '') $body['cursor'] = $list_cursor;
        $list = $this->api('/v3/chat/list', $credentials, $body);
        if (empty($list['success'])) return $this->failure($list, 'chat_list_failed');

        $json = is_array($list['json'] ?? null) ? $list['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $chats = is_array($source['chats'] ?? null) ? $source['chats'] : [];
        $watermarks = is_array($cursor['chat_watermarks'] ?? null) ? $cursor['chat_watermarks'] : [];
        $quiet = empty($cursor['initialized']);
        $events = [];

        foreach ($chats as $entry) {
            if (!is_array($entry)) continue;
            $chat = is_array($entry['chat'] ?? null) ? $entry['chat'] : [];
            $chat_id = $this->safeString($chat['chat_id'] ?? '', 191);
            if ($chat_id === '') continue;
            $first_unread_message_id = (int)($entry['first_unread_message_id'] ?? 0);
            $unread_count = max(0, (int)($entry['unread_count'] ?? 0));
            $last_message_id = $this->safeString($entry['last_message_id'] ?? '', 128);
            $chat['_first_unread_message_id'] = $first_unread_message_id > 0 ? $first_unread_message_id : 0;
            $known = (string)($watermarks[$chat_id] ?? '');
             
             
             
             
            $chat_quiet = $quiet || ($known === '' && $unread_count === 0);
            if ($known !== '' && $last_message_id !== '' && $this->compareMessageIds($last_message_id, $known) <= 0) {
                 
                 
                 
                 
                $events[] = $this->normalizeChatState($chat, $chat_quiet);
                continue;
            }
            $history_body = [
                'chat_id' => $chat_id,
                'direction' => ($known !== '' || $first_unread_message_id > 0) ? 'Forward' : 'Backward',
                'limit' => 50,
            ];
            if ($known !== '' && ctype_digit($known)) {
                 
                 
                 
                $history_body['from_message_id'] = (int)$known;
            } elseif ($first_unread_message_id > 0) {
                 
                 
                 
                $history_body['from_message_id'] = $first_unread_message_id;
            }
            $history = $this->api('/v3/chat/history', $credentials, $history_body);
            if (empty($history['success'])) {
                if (!empty($history['retryable'])) return $this->failure($history, 'chat_history_failed');
                continue;
            }
            $history_json = is_array($history['json'] ?? null) ? $history['json'] : [];
            $history_source = is_array($history_json['result'] ?? null) ? $history_json['result'] : $history_json;
            $messages = is_array($history_source['messages'] ?? null) ? $history_source['messages'] : [];
            $max_id = $known;
             
             
            $normalized = [];
            foreach ($messages as $message) {
                if (!is_array($message)) continue;
                $message_id = $this->safeString($message['message_id'] ?? '', 128);
                if ($message_id !== '' && $known !== '' && $this->compareMessageIds($message_id, $known) <= 0) continue;
                $normalized[] = $this->normalizeChatMessage($chat, $message, $chat_quiet);
                if ($this->compareMessageIds($message_id, $max_id) > 0) $max_id = $message_id;
            }
            usort($normalized, function (array $a, array $b) {
                return strcmp((string)($a['published_at'] ?? ''), (string)($b['published_at'] ?? ''));
            });
            foreach ($normalized as $event) $events[] = $event;
            if (!$normalized && $known !== '') {
                $events[] = $this->normalizeChatState($chat, $chat_quiet);
            }
            if ($max_id !== '') {
                 
                 
                unset($watermarks[$chat_id]);
                $watermarks[$chat_id] = $max_id;
            }
        }

        if (count($watermarks) > 500) $watermarks = array_slice($watermarks, -500, null, true);
        $next_list_cursor = $this->safeString($source['cursor'] ?? '', 2000);
        if (!empty($source['has_next']) && ($next_list_cursor === '' || $next_list_cursor === $list_cursor)) {
            return ['success' => false, 'error' => 'marketplace_cursor_stalled', 'retryable' => true];
        }
        $has_next = !empty($source['has_next']) && $next_list_cursor !== '';
        return [
            'success' => true,
            'events' => $events,
            'cursor' => [
                'list_cursor' => $has_next ? $next_list_cursor : '',
                'chat_watermarks' => $watermarks,
                 
                 
                 
                'initialized' => $has_next ? !empty($cursor['initialized']) : true,
            ],
            'has_more' => $has_next,
            'delay_seconds' => $has_next ? 2 : 15,
        ];
    }

    private function syncQuestions(array $credentials, array $cursor, int $limit): array {
        $status_cursors = is_array($cursor['status_cursors'] ?? null)
            ? $cursor['status_cursors']
            : [];
        if (!$status_cursors && $cursor) {
             
             
            $status_cursors['NEW'] = $cursor;
        }
        $status_index = max(0, min(
            count(self::QUESTION_STATUSES) - 1,
            (int)($cursor['status_index'] ?? 0)
        ));
        $status = self::QUESTION_STATUSES[$status_index];
        $status_cursor = is_array($status_cursors[$status] ?? null)
            ? $status_cursors[$status]
            : [];

        $body = ['limit' => $limit, 'sort_dir' => 'DESC', 'filter' => ['status' => $status]];
        $page_cursor = $this->safeString($status_cursor['page_cursor'] ?? '', 191);
        if ($page_cursor !== '') $body['last_id'] = $page_cursor;
        $result = $this->api('/v1/question/list', $credentials, $body);
        if (empty($result['success'])) return $this->failure($result, 'question_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $page = $this->newestPage((array)($source['questions'] ?? []), $status_cursor, 'published_at', function (array $row, bool $quiet) {
            return $this->normalizeQuestion($row, $quiet);
        }, 30, !empty($source['has_next']), (string)($source['last_id'] ?? ''));
        if (empty($page['success'])) {
            return $page;
        }

        $status_cursors[$status] = is_array($page['cursor'] ?? null) ? $page['cursor'] : $status_cursor;
        $status_has_more = !empty($page['has_more']);
        $next_status_index = $status_has_more ? $status_index : $status_index + 1;
        $cycle_has_more = $status_has_more || $next_status_index < count(self::QUESTION_STATUSES);
        if (!$cycle_has_more) {
            $next_status_index = 0;
        }

        return [
            'success' => true,
            'events' => is_array($page['events'] ?? null) ? $page['events'] : [],
            'cursor' => [
                'status_index' => $next_status_index,
                'status_cursors' => $status_cursors,
            ],
             
             
             
            'has_more' => $cycle_has_more,
            'delay_seconds' => $cycle_has_more ? 1 : 30,
        ];
    }

    private function syncReviews(array $credentials, array $cursor, int $limit): array {
        $body = ['limit' => $limit, 'sort_dir' => 'DESC', 'filters' => ['status' => 'UNPROCESSED']];
        $page_cursor = $this->safeString($cursor['page_cursor'] ?? '', 191);
        if ($page_cursor !== '') $body['last_id'] = $page_cursor;
        $result = $this->api('/v2/review/list', $credentials, $body);
        if (empty($result['success'])) return $this->failure($result, 'review_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $rows = is_array($source['reviews'] ?? null) ? $source['reviews'] : [];
        foreach ($rows as $index => $row) {
            if (!is_array($row)) continue;
            $media_count = max(0, (int)($row['photos_amount'] ?? 0)) + max(0, (int)($row['videos_amount'] ?? 0));
            $review_id = $this->safeString($row['id'] ?? '', 191);
            if ($media_count <= 0 || $review_id === '') continue;
            $detail = $this->api('/v2/review/info', $credentials, ['review_id' => $review_id]);
            if (empty($detail['success'])) {
                 
                 
                if (!empty($detail['retryable'])) return $this->failure($detail, 'review_info_failed');
                continue;
            }
            $detail_json = is_array($detail['json'] ?? null) ? $detail['json'] : [];
            $detail_source = is_array($detail_json['result'] ?? null) ? $detail_json['result'] : $detail_json;
            if ($detail_source) $rows[$index] = array_replace($row, $detail_source);
        }
        return $this->newestPage($rows, $cursor, 'published_at', function (array $row, bool $quiet) {
            return $this->normalizeReview($row, $quiet);
        }, 30, !empty($source['has_next']), (string)($source['last_id'] ?? ''));
    }

    private function syncReturns(array $credentials, array $cursor, int $limit): array {
        $body = ['limit' => $limit];
        $page_cursor = max(0, (int)($cursor['page_cursor'] ?? 0));
        if ($page_cursor > 0) $body['last_id'] = $page_cursor;
        $result = $this->api('/v2/returns/rfbs/list', $credentials, $body);
        if (empty($result['success'])) return $this->failure($result, 'return_sync_failed');
        $json = is_array($result['json'] ?? null) ? $result['json'] : [];
        $source = is_array($json['result'] ?? null) ? $json['result'] : $json;
        $rows = is_array($source['returns'] ?? null) ? $source['returns'] : [];
        $details = [];
        foreach ($rows as $row) {
            if (!is_array($row) || (int)($row['return_id'] ?? 0) <= 0) continue;
            $detail = $this->api('/v2/returns/rfbs/get', $credentials, ['return_id' => (int)$row['return_id']]);
            if (!empty($detail['success'])) {
                $detail_json = is_array($detail['json'] ?? null) ? $detail['json'] : [];
                $detail_source = $detail_json['returns'] ?? $detail_json['result']['returns'] ?? $detail_json['result'] ?? [];
                if (is_array($detail_source)) $row = array_replace_recursive($row, $detail_source);
            } elseif (!empty($detail['retryable'])) {
                 
                 
                 
                return $this->failure($detail, 'return_info_failed');
            }
            $details[] = $row;
        }
        $last = end($rows);
        $next_id = is_array($last) ? (string)max(0, (int)($last['return_id'] ?? 0)) : '';
        return $this->newestPage($details, $cursor, 'created_at', function (array $row, bool $quiet) {
            return $this->normalizeReturn($row, $quiet);
        }, 60, count($rows) >= $limit && $next_id !== '', $next_id);
    }

    private function newestPage(array $rows, array $cursor, string $date_key, callable $normalizer, int $delay, bool $provider_has_more, string $next_page_cursor): 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] ?? 0);
            $max_seen = max($max_seen, $timestamp);
            if (!$quiet && $watermark > 0 && $timestamp > 0 && $timestamp < $watermark) {
                $reached_old_data = true;
                continue;
            }
            $events[] = $normalizer($row, $quiet);
        }
        $current_page_cursor = $this->safeString($cursor['page_cursor'] ?? '', 191);
        if ($provider_has_more && !$reached_old_data
            && ($next_page_cursor === '' || $next_page_cursor === $current_page_cursor)) {
            return ['success' => false, 'error' => 'marketplace_cursor_stalled', 'retryable' => true];
        }
        $has_more = $provider_has_more && !$reached_old_data && $next_page_cursor !== '';
        $next_cursor = $has_more
            ? [
                'watermark' => max(0, (int)($cursor['watermark'] ?? 0)),
                'scan_watermark' => $watermark,
                'scan_max_watermark' => $max_seen,
                'page_cursor' => $next_page_cursor,
                'initialized' => !empty($cursor['initialized']),
            ]
            : ['watermark' => $max_seen, 'page_cursor' => '', 'initialized' => true];
        return [
            'success' => true, 'events' => $events,
            'cursor' => $next_cursor,
            'has_more' => $has_more, 'delay_seconds' => $has_more ? 1 : $delay,
        ];
    }

    private function normalizeChatMessage(array $chat, array $message, bool $quiet): array {
        $chat_id = $this->safeString($chat['chat_id'] ?? '', 191);
        $message_id = $this->safeString($message['message_id'] ?? '', 128);
        $user = is_array($message['user'] ?? null) ? $message['user'] : [];
        $user_type = strtolower($this->safeString($user['type'] ?? '', 64));
        $chat_type = strtolower($this->safeString($chat['chat_type'] ?? '', 64));
        $out = $user_type === 'seller';
        $platform_author = $this->isOzonPlatformAuthor($user_type);
        $writable = !$platform_author
            && in_array($chat_type, self::WRITABLE_CHAT_TYPES, true)
            && strtolower((string)($chat['chat_status'] ?? 'opened')) === 'opened';
        $data = is_array($message['data'] ?? null) ? $message['data'] : [];
        $context = is_array($message['context'] ?? null) ? $message['context'] : [];
        $attachments = $this->ozonAttachments($data, $message);
        $text = $this->ozonMessageText($data, $attachments);
        $empty_content = $text === '' && !$attachments;
        $is_chatbot = $user_type === 'chatbot';
        if ($empty_content) {
             
             
             
             
             
            $text = !empty($message['is_image'])
                ? 'Ozon прислал изображение, но его содержимое не удалось получить.'
                : ($is_chatbot
                    ? 'Ozon: служебный шаг бота без отображаемого содержимого.'
                    : 'Ozon прислал сообщение без текста или вложения.');
        }
        return [
            'update_type' => 'message',
            'text' => $text,
             
             
             
            'external_user_id' => $chat_id,
            'external_chat_id' => $chat_id,
            'external_message_id' => $message_id,
            'dedupe_key' => 'ozon:chat:' . $chat_id . ':' . $message_id,
            'direction' => $out ? 'out' : 'in',
            'display_name' => $this->ozonAuthorDisplayName($user_type),
            'attachments' => $attachments,
            'conversation_kind' => MarketplaceStream::BUYER_CHAT,
            'external_thread_id' => $chat_id,
            'thread_state' => strtolower($this->safeString($chat['chat_status'] ?? 'unknown', 32)),
            'item_context' => $this->ozonItemContext($context),
            'order_id' => $this->safeString($context['order_number'] ?? '', 128),
            'capabilities' => [
                'can_reply' => $writable,
                'read_only' => !$writable,
                'chat_type' => $chat_type,
                'read_ack_message_id' => max(0, (int)($chat['_first_unread_message_id'] ?? 0)),
            ],
            'published_at' => $this->isoDate($message['created_at'] ?? ''),
            'metadata' => [
                'marketplace' => true, 'quiet_import' => $quiet, 'raw_kind' => 'ozon_chat_message',
                'chat_type' => $chat_type, 'empty_content' => $empty_content,
                'empty_content_kind' => $empty_content ? ($is_chatbot ? 'chatbot_step' : 'unknown') : '',
                 
                 
                'suppress_ui' => $empty_content && $is_chatbot,
                'image_payload_missing' => $empty_content && !empty($message['is_image']),
            ],
            'snapshot' => $this->sanitizedSnapshot(['chat' => $chat, 'message' => $message]),
        ];
    }

     
    private function normalizeChatState(array $chat, bool $quiet): array {
        $chat_id = $this->safeString($chat['chat_id'] ?? '', 191);
        $chat_type = strtolower($this->safeString($chat['chat_type'] ?? '', 64));
        $state = strtolower($this->safeString($chat['chat_status'] ?? 'unknown', 32));
        $writable = in_array($chat_type, self::WRITABLE_CHAT_TYPES, true) && $state === 'opened';
        return [
            'update_type' => 'thread_state',
            'text' => '',
            'external_user_id' => $chat_id,
            'external_chat_id' => $chat_id,
            'external_message_id' => 'state:' . substr(hash('sha256', $chat_id . '|' . $chat_type . '|' . $state), 0, 40),
            'dedupe_key' => 'ozon:state:' . $chat_id . ':' . $state,
            'direction' => 'in',
            'display_name' => 'Покупатель Ozon',
            'attachments' => [],
            'conversation_kind' => MarketplaceStream::BUYER_CHAT,
            'external_thread_id' => $chat_id,
            'thread_state' => $state,
            'item_context' => [],
            'capabilities' => [
                'can_reply' => $writable,
                'read_only' => !$writable,
                'chat_type' => $chat_type,
                'read_ack_message_id' => max(0, (int)($chat['_first_unread_message_id'] ?? 0)),
            ],
            'published_at' => '',
            'metadata' => [
                'marketplace' => true,
                'quiet_import' => $quiet,
                'thread_only' => true,
                'raw_kind' => 'ozon_chat_state',
                'chat_type' => $chat_type,
            ],
            'snapshot' => $this->sanitizedSnapshot(['chat' => $chat]),
        ];
    }

    




    private function isOzonPlatformAuthor(string $user_type): bool {
        return in_array(strtolower(trim($user_type)), ['notificationuser', 'chatbot'], true);
    }

    private function ozonAuthorDisplayName(string $user_type): string {
        switch (strtolower(trim($user_type))) {
            case 'notificationuser':
                return 'Уведомления Ozon';
            case 'chatbot':
                return 'Бот Ozon';
            case 'support':
                return 'Поддержка Ozon';
        }
        return 'Покупатель Ozon';
    }

    private function normalizeQuestion(array $row, bool $quiet): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $sku = $this->safeString($row['sku'] ?? '', 128);
        $status = strtoupper($this->safeString($row['status'] ?? 'NEW', 32));
        $actions = ['question_answer', 'question_mark_processed'];
        if ($status === 'NEW') {
            $actions[] = 'question_mark_viewed';
        }
        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' => 'ozon:question:' . $id,
            'direction' => 'in', 'display_name' => $this->safeString($row['author_name'] ?? 'Покупатель Ozon', 255),
            'attachments' => [], 'conversation_kind' => MarketplaceStream::QUESTION,
            'external_thread_id' => $id, 'thread_state' => strtolower($status),
            'item_context' => $this->ozonItemContext(['sku' => $sku, 'product_url' => $row['product_url'] ?? '']),
            'capabilities' => ['can_reply' => false, 'draft_only' => true, 'actions' => $actions],
            'published_at' => $this->isoDate($row['published_at'] ?? ''),
            'metadata' => ['marketplace' => true, 'quiet_import' => $quiet, 'raw_kind' => 'ozon_question', 'sku' => $sku],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function normalizeReview(array $row, bool $quiet): array {
        $id = $this->safeString($row['id'] ?? '', 191);
        $sku = $this->safeString($row['sku'] ?? '', 128);
        $review_text = trim((string)($row['text'] ?? ''));
        $photo_count = max(0, (int)($row['photos_amount'] ?? 0));
        $video_count = max(0, (int)($row['videos_amount'] ?? 0));
        $has_media = $photo_count > 0 || $video_count > 0 || !empty($row['photos']) || !empty($row['videos']);
        $commentable = $review_text !== '' || $has_media;
        $text = (isset($row['rating']) ? 'Оценка: ' . (int)$row['rating'] . "/5\n" : '') . $review_text;
        if ($video_count > 0) {
            $text = trim($text . "\nВидео в отзыве: " . $video_count);
        }
        $attachments = $this->ozonReviewAttachments($row);
        return [
            'update_type' => 'review', 'text' => trim($text),
            'external_user_id' => 'review:' . $id, 'external_chat_id' => 'review:' . $id,
            'external_message_id' => 'review:' . $id, 'dedupe_key' => 'ozon:review:' . $id,
            'direction' => 'in', 'display_name' => 'Покупатель Ozon', 'attachments' => $attachments,
            'conversation_kind' => MarketplaceStream::REVIEW, 'external_thread_id' => $id,
            'thread_state' => strtolower($this->safeString($row['status'] ?? 'unprocessed', 32)),
            'item_context' => $this->ozonItemContext(['sku' => $sku]),
             
             
             
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => $commentable,
                'review_commentable' => $commentable,
                'actions' => $commentable ? ['review_answer', 'review_mark_processed'] : ['review_mark_processed'],
            ],
            'published_at' => $this->isoDate($row['published_at'] ?? ''),
            'metadata' => [
                'marketplace' => true, 'quiet_import' => $quiet, 'raw_kind' => 'ozon_review', 'sku' => $sku,
                'photos_amount' => $photo_count, 'videos_amount' => $video_count,
            ],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function normalizeReturn(array $row, bool $quiet): array {
        $id = (int)($row['return_id'] ?? 0);
        $product = is_array($row['product'] ?? null) ? $row['product'] : [];
        $state = is_array($row['state'] ?? null) ? $row['state'] : [];
        $actions = [];
        foreach ((array)($row['available_actions'] ?? []) as $action) {
            if (!is_array($action) || (int)($action['id'] ?? 0) <= 0) continue;
            $actions[] = ['id' => (int)$action['id'], 'name' => $this->safeString($action['name'] ?? '', 255)];
        }
        $rejection_reasons = [];
        foreach ((array)($row['rejection_reason'] ?? []) as $reason) {
            if (!is_array($reason) || (int)($reason['id'] ?? 0) <= 0) continue;
            $rejection_reasons[] = [
                'id' => (int)$reason['id'],
                'name' => $this->safeString($reason['name'] ?? '', 255),
                'hint' => $this->safeString($reason['hint'] ?? '', 500),
                'requires_comment' => !empty($reason['is_comment_required']),
            ];
        }
        $attachments = [];
        foreach ((array)($row['client_photo'] ?? []) as $url) {
            if (is_string($url) && strpos($url, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $url,
                    'payload_hash' => hash('sha256', self::PROVIDER . '|return|' . $url),
                ];
            }
        }
        return [
            'update_type' => 'return_claim', 'text' => $this->safeString($row['comment'] ?? 'Заявка покупателя на возврат', 10000),
            'external_user_id' => 'return:' . $id, 'external_chat_id' => 'return:' . $id,
            'external_message_id' => 'return:' . $id . ':' . $this->timestamp($row['created_at'] ?? 0),
            'dedupe_key' => 'ozon:return:' . $id . ':' . $this->timestamp($row['created_at'] ?? 0),
            'direction' => 'in', 'display_name' => $this->safeString($row['client_name'] ?? 'Покупатель Ozon', 255),
            'attachments' => $attachments, 'conversation_kind' => MarketplaceStream::RETURN_CLAIM,
            'external_thread_id' => (string)$id, 'thread_state' => strtolower($this->safeString($state['state'] ?? 'unknown', 32)),
            'item_context' => $this->ozonItemContext(['sku' => $product['sku'] ?? '', 'title' => $product['name'] ?? '', 'offer_id' => $product['offer_id'] ?? '']),
            'order_id' => $this->safeString($row['order_number'] ?? $row['posting_number'] ?? '', 128),
            'capabilities' => [
                'can_reply' => false,
                'draft_only' => false,
                'actions' => $actions ? ['return_action'] : [],
                'workflow_actions' => $actions,
                'return_rejection_reasons' => $rejection_reasons,
            ],
            'workflow_actions' => $actions,
            'published_at' => $this->isoDate($row['created_at'] ?? ''),
            'metadata' => ['marketplace' => true, 'quiet_import' => $quiet, 'raw_kind' => 'ozon_return'],
            'snapshot' => $this->sanitizedSnapshot($row),
        ];
    }

    private function ozonAttachments(array $data, array $message): array {
        $result = [];
        $is_image = !empty($message['is_image']);
        foreach ($data as $part) {
            if (!is_string($part)) continue;
            if (!preg_match_all('#https://[^\s)<>]+#u', $part, $matches)) continue;
            foreach ($matches[0] as $url) {
                $path = (string)(parse_url(rtrim($url, '.,'), PHP_URL_PATH) ?: '');
                $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
                if (!$is_image && !in_array($extension, AttachmentPolicy::allowedAttachmentExtensions(true), true)) continue;
                $result[] = [
                    'type' => $is_image || in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true) ? 'image' : 'document',
                    'url' => rtrim($url, '.,'),
                    'file_name' => basename($path),
                    'payload_hash' => hash('sha256', self::PROVIDER . '|chat|' . rtrim($url, '.,')),
                ];
            }
        }
        return $result;
    }

     
    private function ozonMessageText(array $data, array $attachments): string {
        $text = $this->textFromData($data);
        foreach ($attachments as $attachment) {
            $url = trim((string)($attachment['url'] ?? ''));
            if ($url !== '') {
                $text = str_replace($url, '', $text);
            }
        }
        $lines = [];
        foreach (preg_split('/\R/u', $text) ?: [] as $line) {
            $line = trim($line);
            if ($line !== '') $lines[] = $line;
        }
        return trim(implode("\n", $lines));
    }

    private function ozonReviewAttachments(array $row): array {
        $attachments = [];
        foreach ((array)($row['photos'] ?? []) as $photo) {
            $url = is_array($photo) ? (string)($photo['url'] ?? '') : (string)$photo;
            if (strpos($url, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $url,
                    'payload_hash' => hash('sha256', self::PROVIDER . '|review|' . $url),
                ];
            }
        }
        foreach ((array)($row['videos'] ?? []) as $video) {
            if (!is_array($video)) continue;
             
             
            $preview = (string)($video['preview_url'] ?? '');
            if (strpos($preview, 'https://') === 0) {
                $attachments[] = [
                    'type' => 'image',
                    'url' => $preview,
                    'file_name' => 'ozon-review-video-preview.jpg',
                    'payload_hash' => hash('sha256', self::PROVIDER . '|review-preview|' . $preview),
                ];
            }
        }
        return $attachments;
    }

    private function ozonItemContext(array $source): array {
        $sku = $this->safeString($source['sku'] ?? '', 128);
        if ($sku === '') return [];
        return [
            'provider' => self::PROVIDER, 'item_id' => $sku, 'sku' => $sku,
            'title' => $this->safeString($source['title'] ?? '', 500),
            'offer_id' => $this->safeString($source['offer_id'] ?? '', 128),
            'url' => $this->safeString($source['product_url'] ?? '', 2000) ?: 'https://www.ozon.ru/product/' . rawurlencode($sku) . '/',
        ];
    }

    private function api(string $path, array $credentials, array $body, int $timeout = 12): array {
         
         
         
        $payload = $body === [] ? '{}' : $body;
        return $this->transport->json('POST', self::API_BASE . $path, $this->headers($credentials), $payload, $timeout);
    }

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

    private function clientId(array $credentials): string {
        $value = trim((string)($credentials['client_id'] ?? ''));
        return preg_match('/^[0-9]{1,20}$/', $value) ? $value : '';
    }

    private function apiKey(array $credentials): string {
        return trim((string)($credentials['api_key'] ?? ''));
    }

    private function compareMessageIds(string $a, string $b): int {
        $a = ltrim($a, '0');
        $b = ltrim($b, '0');
        if (strlen($a) !== strlen($b)) return strlen($a) <=> strlen($b);
        return strcmp($a, $b);
    }

    private function safeWebhookUrl(string $url): bool {
        $parts = parse_url($url);
        if (!is_array($parts) || strtolower((string)($parts['scheme'] ?? '')) !== 'https') return false;
        if (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;
    }
}
