<?php
namespace EasyChat\Controller;

use EasyChat\Library\Messenger\InboundService;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\Marketplace as MarketplaceModel;
use EasyChat\Library\Messenger\Marketplace\MarketplaceStream;
use EasyChat\Library\Messenger\Marketplace\MarketplaceText;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Model\Setting;

class Messenger extends Base {

    protected $layouts = '';

    public function index() {
        $this->jsonOut(['success' => true, 'service' => 'EasyChat messenger webhook']);
    }

    public function webhook() {
        $provider = strtolower(trim((string)(\EasyChat\Library\RequestContext::get()['provider'] ?? 'max')));
        $public_id = trim((string)(\EasyChat\Library\RequestContext::get()['id'] ?? ''));

        if (\EasyChat\Library\RequestContext::server()['REQUEST_METHOD'] !== 'POST') {
            $this->jsonOut(['success' => false, 'error' => 'method_not_allowed'], 405);
            return;
        }

        if ($provider === '' || $public_id === '') {
            $this->jsonOut(['success' => false, 'error' => 'channel_required'], 404);
            return;
        }
        if ($provider === 'max') {
             
             
            $this->jsonOut(['success' => false, 'error' => 'webhook_not_supported'], 404);
            return;
        }
        if ($provider === 'whatsapp-personal') {
             
             
            $this->jsonOut(['success' => false, 'error' => 'webhook_not_supported'], 404);
            return;
        }

        $model_messenger = new MessengerModel($this->registry);
        $channel = $model_messenger->getChannelByPublicId($provider, $public_id);
        if (!$channel) {
            $this->jsonOut(['success' => false, 'error' => 'channel_not_found'], 404);
            return;
        }

        if ($provider === 'ozon' && !$this->isValidOzonWebhookSignature($channel)) {
             
             
             
            $this->jsonOut(['success' => false, 'error' => 'webhook_signature_invalid'], 403);
            return;
        }

        $declared_length = (int)(\EasyChat\Library\RequestContext::server()['CONTENT_LENGTH'] ?? 0);
        if ($declared_length > 1024 * 1024) {
            $this->jsonOut(['success' => false, 'error' => 'payload_too_large'], 413);
            return;
        }
        $raw = (string)file_get_contents('php://input');
        if (strlen($raw) > 1024 * 1024) {
            $this->jsonOut(['success' => false, 'error' => 'payload_too_large'], 413);
            return;
        }
        $payload = json_decode($raw, true);
        if (!is_array($payload)) {
            if ($provider === 'ozon') {
                $this->ozonError('ERROR_PARAMETER_VALUE_MISSED', 'Некорректное тело уведомления.', 400);
                return;
            }
            $this->jsonOut(['success' => false, 'error' => 'bad_json'], 400);
            return;
        }

        if ($provider === 'ozon') {
            $this->handleOzonNotification($channel, $payload);
            return;
        }
        if ($provider === 'yandex_market') {
            $this->handleYandexMarketNotification($channel, $payload);
            return;
        }

        $secret_header = '';
        if ($provider === 'telegram') {
            $secret_header = (string)(\EasyChat\Library\RequestContext::server()['HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN'] ?? '');
        } elseif ($provider === 'max-bot') {
            $secret_header = (string)(\EasyChat\Library\RequestContext::server()['HTTP_X_MAX_BOT_API_SECRET'] ?? '');
        } elseif ($provider === 'youla') {
            $server = \EasyChat\Library\RequestContext::server();
            $payload['_headers'] = [
                'ce_id' => (string)($server['HTTP_CE_ID'] ?? ''),
                'ce_source' => (string)($server['HTTP_CE_SOURCE'] ?? ''),
                'ce_specversion' => (string)($server['HTTP_CE_SPECVERSION'] ?? ''),
                'ce_time' => (string)($server['HTTP_CE_TIME'] ?? ''),
                'ce_type' => (string)($server['HTTP_CE_TYPE'] ?? ''),
            ];
        }
        $service = new InboundService($this->registry);
        if ($provider === 'vk' && (string)($payload['type'] ?? '') !== 'confirmation') {
            $result = $service->acceptVkWebhook($channel, $payload, $raw);
            if (empty($result['success'])) {
                $this->jsonOut(['success' => false, 'error' => $result['error'] ?? 'webhook_failed'], (int)($result['http_code'] ?? 500));
                return;
            }
            http_response_code(200);
            header('Content-Type: text/plain; charset=utf-8');
            echo 'ok';
            $can_process_inline = function_exists('fastcgi_finish_request');
            if ($can_process_inline) {
                fastcgi_finish_request();
            } else {
                if (ob_get_level() > 0) {
                    ob_flush();
                }
                flush();
            }
            if ($can_process_inline && !empty($result['queued']) && !empty($result['job_id'])) {
                $queue = new \EasyChat\Library\Queue\QueueManager($this->registry);
                $claimed = $queue->claimById((int)$result['job_id']);
                if ($claimed) {
                    $lock_token = (string)($claimed['lock_token'] ?? '');
                    $job_payload = json_decode((string)($claimed['payload'] ?? ''), true);
                    $processed = $service->processAcceptedVkWebhook(is_array($job_payload) ? $job_payload : [
                        'channel_id' => (int)$channel['channel_id'],
                        'payload' => array_diff_key($payload, ['secret' => true]),
                    ]);
                    if (!empty($processed['success'])) {
                        $queue->markDone((int)$result['job_id'], $lock_token);
                    } else {
                        $queue->reschedule((int)$result['job_id'], 15, 'vk_inline_retry', $lock_token);
                    }
                }
            }
            exit;
        }
        $result = $service->handleWebhook($provider, $channel, $payload, $raw, $secret_header);

        if (empty($result['success'])) {
            $this->jsonOut(['success' => false, 'error' => $result['error'] ?? 'webhook_failed'], (int)($result['http_code'] ?? 500));
            return;
        }

        if ($provider === 'vk') {
            http_response_code(200);
            header('Content-Type: text/plain; charset=utf-8');
            echo $result['response_body'] ?? 'ok';
            exit;
        }

        $this->jsonOut(['success' => true] + $result);
    }

    private function jsonOut(array $data, int $status = 200): void {
        http_response_code($status);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE);
        exit;
    }

    private function isValidOzonWebhookSignature(array $channel): bool {
        $provided = trim((string)(\EasyChat\Library\RequestContext::get()['signature'] ?? ''));
        $public_id = trim((string)($channel['public_id'] ?? ''));
        $install_secret = trim((string)(new Setting($this->registry))->get('chat_install_secret', ''));
        if ($provided === '' || $public_id === '' || $install_secret === '') {
            return false;
        }
        $expected = hash_hmac('sha256', "easychat:ozon-webhook:v1\0" . $public_id, $install_secret);
        return hash_equals($expected, $provided);
    }

    private function handleOzonNotification(array $channel, array $payload): void {
         
         
         
        $started_at = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
            ->format('Y-m-d\TH:i:s.v\Z');
        if ((string)($channel['status'] ?? '') !== 'active') {
             
             
            $this->ozonOk();
            return;
        }

        $recognized_types = [
            'PING', 'TYPE_PING', 'TEST',
            'TYPE_NEW_MESSAGE', 'TYPE_UPDATE_MESSAGE', 'TYPE_MESSAGE_READ', 'TYPE_CHAT_CLOSED',
        ];
        $type = '';
        foreach ($this->notificationValues($payload, ['message_type', 'notification_type', 'type']) as $candidate) {
            $candidate = strtoupper($candidate);
            if (in_array($candidate, $recognized_types, true)) {
                $type = $candidate;
                break;
            }
        }
        if (in_array($type, ['PING', 'TYPE_PING', 'TEST'], true)) {
             
             
            $this->jsonOut([
                 
                 
                'result' => true,
                'version' => '1.0',
                'name' => 'EasyChat',
                'time' => $started_at,
            ]);
            return;
        }
        $allowed = ['TYPE_NEW_MESSAGE', 'TYPE_UPDATE_MESSAGE', 'TYPE_MESSAGE_READ', 'TYPE_CHAT_CLOSED'];
        if (!in_array($type, $allowed, true)) {
            $this->ozonError('ERROR_PARAMETER_VALUE_MISSED', 'Неизвестный тип уведомления.', 400);
            return;
        }

        $payload_account = $this->notificationValue($payload, ['client_id', 'seller_id']);
        $expected_account = trim((string)($channel['external_account_id'] ?? ''));
        if ($payload_account !== '' && $expected_account !== '' && !hash_equals($expected_account, $payload_account)) {
             
             
            $this->ozonOk();
            return;
        }

        $channel_id = (int)($channel['channel_id'] ?? 0);
        (new MarketplaceModel($this->registry))->wakeStream($channel_id, MarketplaceStream::BUYER_CHAT);
        $stream_id = $this->buyerStreamId($channel_id);
        if ($stream_id <= 0) {
            $this->ozonOk();
            return;
        }
        $job_id = (new QueueManager($this->registry))->pushUnique(
            'messenger_marketplace_sync',
            ['stream_id' => $stream_id],
            'marketplace_stream_' . $stream_id,
            0,
            10
        );
        $this->ozonOk();
    }

    private function ozonOk(): void {
         
        $this->jsonOut(['result' => true]);
    }

    private function ozonError(string $code, string $message, int $status): void {
        $this->jsonOut([
            'error' => [
                'code' => $code,
                'message' => $message,
                'details' => null,
            ],
        ], $status);
    }

    private function handleYandexMarketNotification(array $channel, array $payload): void {
        $started_at = gmdate('Y-m-d\TH:i:s\Z');
        if (!$this->isYandexMarketSourceIp((string)(\EasyChat\Library\RequestContext::server()['REMOTE_ADDR'] ?? ''))) {
            $this->jsonOut(['error' => ['type' => 'UNKNOWN', 'message' => 'source_not_allowed']], 403);
            return;
        }

        $type = strtoupper(MarketplaceText::truncate(trim((string)($payload['notificationType'] ?? '')), 64));
        if ($type === '') {
            $this->jsonOut(['error' => ['type' => 'WRONG_EVENT_FORMAT', 'message' => 'notificationType required']], 400);
            return;
        }
        if ($type === 'PING') {
            $this->yandexMarketAck($started_at);
            return;
        }
        if ((string)($channel['status'] ?? '') !== 'active') {
             
             
            $this->yandexMarketAck($started_at);
            return;
        }

        $business_id = trim((string)($payload['businessId'] ?? ''));
        $expected = trim((string)($channel['external_account_id'] ?? ''));
        if ($business_id === '' || $expected === '' || !hash_equals($expected, $business_id)) {
             
             
            $this->yandexMarketAck($started_at);
            return;
        }

        $stream = '';
        if (in_array($type, ['CHAT_CREATED', 'CHAT_MESSAGE_SENT', 'CHAT_ARBITRAGE_STARTED', 'CHAT_ARBITRAGE_FINISHED'], true)) {
            $stream = MarketplaceStream::BUYER_CHAT;
        } elseif (in_array($type, ['QUESTION_CREATED', 'QUESTION_ANSWER_CREATED', 'QUESTION_COMMENT_CREATED'], true)) {
            $stream = MarketplaceStream::QUESTION;
        } elseif (in_array($type, ['GOODS_FEEDBACK_CREATED', 'GOODS_FEEDBACK_COMMENT_CREATED'], true)) {
            $stream = MarketplaceStream::REVIEW;
        }
        if ($stream === '') {
             
             
             
            $this->yandexMarketAck($started_at);
            return;
        }

        $channel_id = (int)($channel['channel_id'] ?? 0);
        $model = new MarketplaceModel($this->registry);
        $model->wakeStream($channel_id, $stream);
        $stream_id = $this->enabledStreamId($channel_id, $stream);
        if ($stream_id > 0) {
            $hint = $this->yandexMarketNotificationHint($type, $payload);
            if ($hint) {
                 
                 
                 
                $model->rememberNotificationHint(
                    $channel_id,
                    $stream,
                    (string)$hint['entity_type'],
                    (string)$hint['entity_id'],
                    (array)$hint['payload']
                );
            }
            (new QueueManager($this->registry))->pushUnique(
                'messenger_marketplace_sync',
                ['stream_id' => $stream_id],
                'marketplace_stream_' . $stream_id,
                0,
                10
            );
        }
        $this->yandexMarketAck($started_at);
    }

    




    private function yandexMarketNotificationHint(string $type, array $payload): array {
        $chat_id = $this->positiveNotificationId($payload['chatId'] ?? '');
        $message_id = $this->positiveNotificationId($payload['messageId'] ?? '');
        $question_id = $this->positiveNotificationId($payload['questionId'] ?? '');
        $answer_id = $this->positiveNotificationId($payload['answerId'] ?? '');
        $comment_id = $this->positiveNotificationId($payload['commentId'] ?? '');
        $feedback_id = $this->positiveNotificationId($payload['feedbackId'] ?? '');

        if (in_array($type, ['CHAT_CREATED', 'CHAT_ARBITRAGE_STARTED', 'CHAT_ARBITRAGE_FINISHED'], true)
            && $chat_id !== '') {
            return [
                'entity_type' => 'chat',
                 
                 
                 
                'entity_id' => $chat_id . ':' . strtolower($type),
                'payload' => ['chat_id' => $chat_id, 'notification_type' => $type],
            ];
        }
        if ($type === 'CHAT_MESSAGE_SENT' && $chat_id !== '' && $message_id !== '') {
            return [
                'entity_type' => 'chat_message',
                'entity_id' => $chat_id . ':' . $message_id,
                'payload' => [
                    'chat_id' => $chat_id,
                    'message_id' => $message_id,
                    'notification_type' => $type,
                ],
            ];
        }
        if ($type === 'QUESTION_CREATED' && $question_id !== '') {
            return [
                'entity_type' => 'question',
                'entity_id' => $question_id,
                'payload' => ['question_id' => $question_id, 'notification_type' => $type],
            ];
        }
        if ($type === 'QUESTION_ANSWER_CREATED' && $answer_id !== '') {
            return [
                'entity_type' => 'question_answer',
                'entity_id' => $answer_id,
                'payload' => ['answer_id' => $answer_id, 'notification_type' => $type],
            ];
        }
        if ($type === 'QUESTION_COMMENT_CREATED' && $answer_id !== '' && $comment_id !== '') {
            return [
                'entity_type' => 'question_comment',
                'entity_id' => $answer_id . ':' . $comment_id,
                'payload' => [
                    'answer_id' => $answer_id,
                    'comment_id' => $comment_id,
                    'notification_type' => $type,
                ],
            ];
        }
        if ($type === 'GOODS_FEEDBACK_CREATED' && $feedback_id !== '') {
            return [
                'entity_type' => 'feedback',
                'entity_id' => $feedback_id,
                'payload' => ['feedback_id' => $feedback_id, 'notification_type' => $type],
            ];
        }
        if ($type === 'GOODS_FEEDBACK_COMMENT_CREATED' && $comment_id !== '') {
            return [
                'entity_type' => 'feedback_comment',
                'entity_id' => $comment_id,
                'payload' => ['comment_id' => $comment_id, 'notification_type' => $type],
            ];
        }
        return [];
    }

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

    private function enabledStreamId(int $channel_id, string $wanted): int {
        foreach ((new MarketplaceModel($this->registry))->listStreams($channel_id) as $stream) {
            if ((string)($stream['stream'] ?? '') === $wanted
                && (string)($stream['status'] ?? '') === 'enabled') {
                return (int)($stream['stream_id'] ?? 0);
            }
        }
        return 0;
    }

    private function yandexMarketAck(string $started_at): void {
        $this->jsonOut([
            'version' => defined('EASYCHAT_VERSION') ? (string)EASYCHAT_VERSION : '1.0.0',
            'name' => 'EasyChat',
            'time' => $started_at,
        ]);
    }

    private function isYandexMarketSourceIp(string $ip): bool {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
            return false;
        }
        foreach (['5.45.207.0/25', '141.8.142.0/25', '5.255.253.0/25'] as $cidr) {
            [$network, $bits] = explode('/', $cidr, 2);
            $mask = -1 << (32 - (int)$bits);
            $ip_number = ip2long($ip);
            $network_number = ip2long($network);
            if ($ip_number !== false && $network_number !== false
                && (($ip_number & $mask) === ($network_number & $mask))) {
                return true;
            }
        }
        return false;
    }

    private function buyerStreamId(int $channel_id): int {
        foreach ((new MarketplaceModel($this->registry))->listStreams($channel_id) as $stream) {
            if ((string)($stream['stream'] ?? '') === MarketplaceStream::BUYER_CHAT
                && (string)($stream['status'] ?? '') === 'enabled') {
                return (int)($stream['stream_id'] ?? 0);
            }
        }
        return 0;
    }

    private function notificationValue(array $payload, array $keys): string {
        $values = $this->notificationValues($payload, $keys);
        return $values ? (string)$values[0] : '';
    }

    private function notificationValues(array $payload, array $keys, int $depth = 0): array {
        if ($depth > 5) {
            return [];
        }
        $values = [];
        foreach ($keys as $key) {
            if (isset($payload[$key]) && !is_array($payload[$key]) && !is_object($payload[$key])) {
                $value = MarketplaceText::truncate(trim((string)$payload[$key]), 128);
                if ($value !== '') {
                    $values[] = $value;
                }
            }
        }
        foreach ($payload as $value) {
            if (!is_array($value)) {
                continue;
            }
            $values = array_merge($values, $this->notificationValues($value, $keys, $depth + 1));
        }
        return array_values(array_unique($values));
    }
}
