<?php

declare(strict_types=1);

require_once __DIR__ . '/conversation.php';

function handleBitrixBotEvent(array $payload): array
{
    $message = normalizeBitrixBotPayload($payload);
    $photoRequest = isConnectorPhotoRequest($message['message']);
    if ($message['message'] === '' && firstPayloadValue($payload, ['data.PARAMS.FILES', 'data.PARAMS.FILE_ID', 'data.PARAMS.fileId', 'PARAMS.FILES', 'PARAMS.fileId'], []) !== []) {
        $message['message'] = 'Клиент прислал вложение без текста. Содержимое вложения недоступно. Попроси клиента описать его вопрос текстом; не делай предположений о содержимом.';
    }

    if ($message['event'] !== '' && !in_array($message['event'], ['ONIMBOTMESSAGEADD', 'ONIMBOTJOINCHAT', 'ONIMBOTDELETE'], true)) {
        return ['ok' => true, 'skipped' => 'Unsupported bot event: ' . $message['event'], 'normalized' => $message];
    }

    if ($message['event'] === 'ONIMBOTDELETE') {
        return ['ok' => true, 'skipped' => 'Bot delete event.', 'normalized' => $message];
    }

    if ($message['message'] === '') {
        return ['ok' => true, 'skipped' => 'Message is empty.', 'normalized' => $message];
    }

    if (trim((string) ($message['open_line_id'] ?? '')) !== ''
        && (isConnectorServiceText($message['message'])
            || (!$photoRequest && firstPayloadValue($payload, ['data.PARAMS.SYSTEM', 'PARAMS.SYSTEM'], '') === 'Y'))) {
        logMessage('info', 'Connector service event ignored', [
            'dialog_id' => $message['dialog_id'], 'message_id' => $message['message_id'],
        ]);
        if (function_exists('managedOpenLineBotScope') && $message['dialog_id'] !== ''
            && !in_array($message['author_id'], configuredBitrixBotIds(), true) && !shouldSkipOpenLineInternalSender($message)) {
            $context = array_intersect_key($message, array_flip(['dialog_id','chat_id','chat_title','open_line_id','deal_id','bot_id']));
            if ((managedOpenLineBotScope($context)['allowed'] ?? false) === true) registerPassiveBotChat($context);
        }
        return ['ok' => true, 'skipped' => 'Connector service event.'];
    }

    if ($message['author_id'] !== '' && in_array($message['author_id'], configuredBitrixBotIds(), true)) {
        return ['ok' => true, 'skipped' => 'Own bot message.', 'normalized' => $message];
    }

    if (isOwnFollowupText($message['message'])) {
        return ['ok' => true, 'skipped' => 'Own followup message.', 'normalized' => $message];
    }

    if (shouldSkipOpenLineInternalSender($message)) {
        logMessage('info', 'Open line internal sender ignored', [
            'dialog_id' => $message['dialog_id'],
            'chat_id' => $message['chat_id'],
            'open_line_id' => $message['open_line_id'],
            'message_id' => $message['message_id'],
            'author_id' => $message['author_id'],
            'is_connector' => $message['is_connector'],
            'is_bot' => $message['is_bot'],
        ]);

        return ['ok' => true, 'skipped' => 'Open line message is not from an external client.', 'normalized' => $message];
    }

    if ($photoRequest) $message['message'] = trim(preg_replace('/^\s*\[Системное сообщение\]\s*/u', '', $message['message']));

    if ($message['dialog_id'] === '') {
        return ['ok' => false, 'error' => 'DIALOG_ID not found.', 'normalized' => $message];
    }

    $eventId = 'bot:' . ($message['message_id'] ?: sha1(json_encode($payload)));
    if (!claimEventForProcessing($eventId)) {
        return ['ok' => true, 'skipped' => 'Event already processed.', 'event_id' => $eventId];
    }
    $dialogLock = null;
    $replyDelivered = false;

    try {
        $dialogLock = acquireBotDialogLock($message['dialog_id']);
        $chatContext = [
            'dialog_id' => $message['dialog_id'],
            'chat_id' => $message['chat_id'],
            'chat_title' => $message['chat_title'],
            'open_line_id' => $message['open_line_id'],
            'deal_id' => $message['deal_id'],
            'bot_id' => $message['bot_id'],
        ];
        $chatContext = enrichAiContextWithDealAdvertisement($chatContext);

        if (isRecentDuplicateClientMessage($chatContext, $message['message'])) {
            logMessage('info', 'Duplicate open line client message ignored', [
                'dialog_id' => $message['dialog_id'],
                'chat_id' => $message['chat_id'],
                'message_id' => $message['message_id'],
            ]);

            return [
                'ok' => true,
                'skipped' => 'Duplicate client message.',
                'event_id' => $eventId,
            ];
        }

        $aiResult = generateAiAnswerResult($message['message'], $chatContext);
        $answer = (string) ($aiResult['answer'] ?? '');
        $contactState = mergeChatContactData($chatContext, (array) ($aiResult['contact'] ?? []));
        $replyBotId = (int) ($message['bot_id'] ?: config('BITRIX_AI_BOT_ID', 0));
        $reply = sendBitrixBotMessage($message['dialog_id'], $answer, $replyBotId);
        if (empty($reply['result'])) {
            throw new RuntimeException('Bitrix did not confirm the bot reply.');
        }
        $replyDelivered = true;
        if (function_exists('recordBotInboxDelivery')) recordBotInboxDelivery($chatContext, $message['message'], $answer, $contactState, $reply);
        // Only a confirmed reply belongs in conversation history. Save both messages together.
        appendChatHistoryMessages($chatContext, [
            ['role' => 'user', 'content' => $message['message']],
            ['role' => 'assistant', 'content' => $answer],
        ]);
        // The reply is already delivered. A CRM failure must not replay the dialog event.
        try {
            $crmSync = syncDealFieldsFromContactData($contactState, $chatContext);
        } catch (Throwable $exception) {
            $crmSync = ['ok' => false, 'error' => $exception->getMessage()];
            logMessage('error', 'CRM sync failed after bot reply', [
                'context' => $chatContext, 'message_id' => $message['message_id'],
                'error' => $exception->getMessage(),
            ]);
        }
        recordChatCrmSyncResult($chatContext, $crmSync);

        if (isContactDataComplete($contactState)) {
            clearChatAwaitingClientReply($chatContext);
        } else {
            markChatAwaitingClientReply($chatContext);
        }

        logMessage('info', 'Bitrix bot event processed', [
            'dialog_id' => $message['dialog_id'],
            'chat_id' => $message['chat_id'],
            'open_line_id' => $message['open_line_id'],
            'deal_id' => $message['deal_id'],
            'bot_id' => $replyBotId,
            'message_id' => $message['message_id'],
            'reply' => $reply['result'] ?? null,
            'contact_state' => $contactState,
            'crm_sync' => $crmSync,
        ]);

        return [
            'ok' => true,
            'normalized' => $message,
            'message_to_client' => $answer,
            'ai_contact' => $aiResult['contact'] ?? [],
            'contact_state' => $contactState,
            'bitrix_reply' => $reply,
            'crm_sync' => $crmSync,
        ];
    } catch (Throwable $exception) {
        if (!$replyDelivered) {
            unmarkEventProcessed($eventId);
        } else {
            // Keep the claim even if history, CRM retry state, timers or logging failed after delivery.
            logMessage('error', 'Bot reply delivered but post-delivery processing failed', [
                'event_id' => $eventId, 'dialog_id' => $message['dialog_id'],
                'reply' => $reply['result'] ?? null, 'error' => $exception->getMessage(),
            ]);
        }
        throw $exception;
    } finally {
        releaseBotDialogLock($dialogLock);
    }
}

function isRecentDuplicateClientMessage(array $context, string $message): bool
{
    $windowSeconds = max(0, (int) config('DUPLICATE_CLIENT_MESSAGE_WINDOW_SECONDS', 900));
    $minimumLength = max(1, (int) config('DUPLICATE_CLIENT_MESSAGE_MIN_LENGTH', 25));
    $normalized = normalizeClientMessageForDuplicateCheck($message);
    $messageLength = function_exists('mb_strlen')
        ? mb_strlen($normalized, 'UTF-8')
        : strlen($normalized);
    if ($windowSeconds === 0 || $messageLength < $minimumLength) {
        return false;
    }

    $now = time();
    foreach (array_reverse(getChatHistory($context)) as $historyMessage) {
        if ((string) ($historyMessage['role'] ?? '') !== 'user') {
            continue;
        }

        $createdAt = strtotime((string) ($historyMessage['created_at'] ?? ''));
        if ($createdAt === false || $now - $createdAt > $windowSeconds) {
            continue;
        }

        if (normalizeClientMessageForDuplicateCheck((string) ($historyMessage['content'] ?? '')) === $normalized) {
            return true;
        }
    }

    return false;
}

function normalizeClientMessageForDuplicateCheck(string $message): string
{
    $message = trim(preg_replace('/\s+/u', ' ', $message) ?? $message);

    return function_exists('mb_strtolower')
        ? mb_strtolower($message, 'UTF-8')
        : strtr(strtolower($message), [
            'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ё' => 'ё',
            'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м',
            'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у',
            'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ',
            'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я',
        ]);
}

function shouldSkipOpenLineInternalSender(array $message): bool
{
    if (!(bool) config('OPEN_LINE_EXTERNAL_CLIENTS_ONLY', true)) {
        return false;
    }

    if (trim((string) ($message['open_line_id'] ?? '')) === '') {
        return false;
    }

    $isConnector = strtoupper(trim((string) ($message['is_connector'] ?? '')));
    $isBot = strtoupper(trim((string) ($message['is_bot'] ?? '')));

    return $isConnector === 'N' || $isBot === 'Y';
}

function acquireBotDialogLock(string $dialogId, bool $wait = true)
{
    $lockDir = dirname((string) config('STATE_FILE')) . '/locks';
    if (!is_dir($lockDir) && !mkdir($lockDir, 0775, true) && !is_dir($lockDir)) {
        throw new RuntimeException('Unable to create bot dialog lock directory.');
    }

    $lockPath = $lockDir . '/bot_dialog_' . sha1($dialogId) . '.lock';
    $handle = @fopen($lockPath, 'c');
    // Existing FPM-owned locks may be read-only for cron. flock does not write to the file.
    if ($handle === false && is_file($lockPath)) $handle = @fopen($lockPath, 'r');
    if ($handle === false) {
        throw new RuntimeException('Unable to open bot dialog lock.');
    }

    @chmod($lockPath, 0664);
    if (!flock($handle, LOCK_EX | ($wait ? 0 : LOCK_NB))) {
        fclose($handle);
        if (!$wait) return null;
        throw new RuntimeException('Unable to acquire bot dialog lock.');
    }

    return $handle;
}

function releaseBotDialogLock($handle): void
{
    if (!is_resource($handle)) {
        return;
    }

    flock($handle, LOCK_UN);
    fclose($handle);
}

function isOwnFollowupText(string $message): bool
{
    $message = trim($message);
    if ($message === '') {
        return false;
    }

    $texts = [];
    foreach ([
        'FOLLOWUP_MESSAGE',
        'FOLLOWUP_MESSAGES',
        'FOLLOWUP_MESSAGES_NEED_CITY',
        'FOLLOWUP_MESSAGES_NEED_PHONE',
    ] as $key) {
        $value = config($key, []);
        if (is_array($value)) {
            foreach ($value as $item) {
                $text = trim((string) $item);
                if ($text !== '') {
                    $texts[] = $text;
                }
            }
        } else {
            $text = trim((string) $value);
            if ($text !== '') {
                $texts[] = $text;
            }
        }
    }

    return in_array($message, array_unique($texts), true);
}
