<?php

declare(strict_types=1);

require_once __DIR__ . '/prompts.php';

function syncDealFieldsFromConversation(string $latestUserMessage, array $context): array
{
    $extracted = extractDealContactFields($latestUserMessage, $context);

    return syncDealFieldsFromContactData($extracted, $context);
}

function enrichAiContextWithDealAdvertisement(array $context): array
{
    if (trim((string) ($context['open_line_id'] ?? '')) === '') {
        return $context;
    }

    if (function_exists('getChatStoredContext')) {
        $storedContext = getChatStoredContext($context);
        if (($storedContext['deal_advertisement_context_loaded'] ?? false) === true) {
            foreach (['deal_id', 'deal_advertisement_context', 'deal_advertisement_context_loaded'] as $key) {
                if (array_key_exists($key, $storedContext)) {
                    $context[$key] = $storedContext[$key];
                }
            }

            return $context;
        }
    }

    $scope = managedOpenLineBotScope($context);
    if (($scope['allowed'] ?? false) !== true) {
        return $context;
    }

    try {
        $dealId = normalizeCrmId($context['deal_id'] ?? '');
        if ($dealId === '') {
            $dealId = resolveDealIdFromOpenLineDialog($context);
        }
        if ($dealId === '') {
            return $context;
        }

        $response = bitrixCall('crm.deal.get', ['id' => $dealId]);
        $deal = (array) ($response['result'] ?? []);
        $field = trim((string) config(
            'DEAL_ADVERTISEMENT_CONTEXT_FIELD',
            'UF_CRM_1716203519003'
        ));
        $advertisement = $field !== ''
            ? normalizeDealAdvertisementContextValue($deal[$field] ?? '')
            : '';

        $context['deal_id'] = $dealId;
        $context['deal_advertisement_context'] = $advertisement;
        $context['deal_advertisement_context_loaded'] = $advertisement !== '';

        logMessage('info', 'Deal advertisement context loaded', [
            'deal_id' => $dealId,
            'field' => $field,
            'has_advertisement_context' => $advertisement !== '',
            'advertisement_context' => $advertisement,
        ]);
    } catch (Throwable $exception) {
        logMessage('warning', 'Deal advertisement context load failed', [
            'context' => $context,
            'error' => $exception->getMessage(),
        ]);
    }

    return $context;
}

function normalizeDealAdvertisementContextValue($value): string
{
    if (is_array($value)) {
        foreach (['VALUE', 'TEXT'] as $key) {
            if (array_key_exists($key, $value)) {
                return normalizeDealAdvertisementContextValue($value[$key]);
            }
        }

        $parts = [];
        foreach ($value as $item) {
            $normalized = normalizeDealAdvertisementContextValue($item);
            if ($normalized !== '') {
                $parts[] = $normalized;
            }
        }
        $value = implode("\n", array_unique($parts));
    }

    $value = trim(html_entity_decode(strip_tags((string) $value), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
    if (function_exists('mb_substr')) {
        return mb_substr($value, 0, 4096, 'UTF-8');
    }

    return substr($value, 0, 4096);
}

function syncDealFieldsFromContactData(array $extracted, array $context): array
{
    if (!(bool) config('CRM_SYNC_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'CRM sync disabled.'];
    }

    $scope = managedOpenLineBotScope($context);
    if (($scope['allowed'] ?? false) !== true) {
        return ['ok' => true, 'skipped' => (string) ($scope['reason'] ?? 'Open line scope rejected.'), 'scope' => $scope];
    }

    $phone = normalizePhoneForDeal($extracted['phone'] ?? '');
    $city = trim((string) ($extracted['city'] ?? ''));
    $fullName = normalizeClientFullName($extracted['full_name'] ?? '');
    $interestGroup = normalizeInterestGroup($extracted['interest_group'] ?? '');
    $branch = normalizeBranchName($extracted['branch'] ?? '');

    if ($phone === '' && $city === '' && $fullName === '' && $interestGroup === '' && $branch === '') {
        return [
            'ok' => true,
            'skipped' => 'Phone, city, full name and interest group not found.',
            'extracted' => [
                'phone' => $phone,
                'city' => $city,
                'full_name' => $fullName,
                'interest_group' => $interestGroup,
                'branch' => $branch,
            ],
        ];
    }

    $dealId = normalizeCrmId($context['deal_id'] ?? '');
    if ($dealId === '') {
        $dealId = resolveDealIdFromOpenLineDialog($context);
    }

    if ($dealId === '') {
        logMessage('warning', 'CRM sync skipped: deal id not found', [
            'context' => $context,
        ]);

        return ['ok' => true, 'skipped' => 'Deal id not found.'];
    }

    try {
        $fields = [];
        $cityMatch = null;
        $branchMatch = null;
        $branchNotification = null;
        $dealResponse = bitrixCall('crm.deal.get', [
            'id' => $dealId,
        ]);
        $deal = (array) ($dealResponse['result'] ?? []);

        if ($phone !== '') {
            $fields[(string) config('DEAL_PHONE_FIELD', 'UF_CRM_1783332070432')] = $phone;
        }

        if ($fullName !== '') {
            $fields[(string) config('DEAL_FULL_NAME_FIELD', 'UF_CRM_1784807336296')] = $fullName;
        }

        if ($interestGroup !== '') {
            $fields[(string) config('DEAL_INTEREST_GROUP_FIELD', 'UF_CRM_1784809347681')] = $interestGroup;
        }

        if ($city !== '') {
            $cityField = (string) config('DEAL_CITY_FIELD', 'UF_CRM_1693995123245');
            $cityItems = getDealUserFieldEnumItems($cityField);
            $cityMatch = matchDealCityEnumValue($city, $cityItems);

            if ($cityMatch === null) {
                logMessage('warning', 'CRM sync skipped city field: city enum value not found', [
                    'deal_id' => $dealId,
                    'city' => $city,
                    'available_cities' => array_map(static fn (array $item): string => $item['value'], $cityItems),
                ]);
            } else {
                $fields[$cityField] = $cityMatch['id'];
            }
        }

        if ($branch === '' && $city !== '') {
            $branch = normalizeBranchName($city);
        }
        $branchField = (string) config('DEAL_BRANCH_FIELD', 'UF_CRM_1764919997');
        $branchItems = [];
        $currentBranch = $deal[$branchField] ?? '';
        $currentBranchItem = null;
        if ($branch !== '' || $city !== '') {
            $branchItems = getDealUserFieldEnumItems($branchField);
            $currentBranchItem = resolveRoutableDealBranchItem($currentBranch, $branchItems);
        }

        if ($branch !== '') {
            $branchMatch = matchDealBranchEnumValue($branch, $branchItems);

            if ($branchMatch === null) {
                logMessage('warning', 'CRM sync skipped branch field: branch enum value not found', [
                    'deal_id' => $dealId,
                    'branch' => $branch,
                ]);
            } elseif ($currentBranchItem === null) {
                $fields[$branchField] = $branchMatch['id'];
            }
        }

        if ($city !== '' && $branchMatch === null && $currentBranchItem === null) {
            $branchNotification = notifyUndefinedBranchDeal($dealId, $city, $branch, $context);
        }

        $interestRouting = routeDealByInterest($deal, $fields, $interestGroup);
        if (($interestRouting['should_update'] ?? false) === true) {
            $fields['CATEGORY_ID'] = (string) $interestRouting['category_id'];
            $fields['STAGE_ID'] = (string) $interestRouting['stage_id'];
        }

        $qualification = qualifyRetailDealForTechnique($deal, $fields, $interestGroup);
        if (($interestRouting['should_update'] ?? false) !== true
            && ($qualification['should_update'] ?? false) === true
        ) {
            $fields['STAGE_ID'] = (string) $qualification['stage_id'];
        }

        if ($fields === []) {
            return [
                'ok' => true,
                'skipped' => 'No deal fields can be updated.',
                'extracted' => [
                    'phone' => $phone,
                    'city' => $city,
                    'full_name' => $fullName,
                    'interest_group' => $interestGroup,
                    'branch' => $branch,
                ],
                'city_match' => $cityMatch,
                'branch_match' => $branchMatch,
                'branch_notification' => $branchNotification,
            ];
        }

        $update = updateDealFieldsWithStageRouting($dealId, $fields);

        $qualifiedChatSummary = null;
        if (($interestRouting['should_update'] ?? false) !== true
            && ($qualification['should_update'] ?? false) === true
            && (string) ($fields['STAGE_ID'] ?? '') === (string) ($qualification['stage_id'] ?? '')
        ) {
            $qualifiedChatSummary = writeQualifiedDealChatSummary($dealId, $context, [
                'phone' => $phone,
                'city' => $city,
                'full_name' => $fullName,
                'interest_group' => $interestGroup,
                'branch' => $branch,
            ]);
        }

        $nonTargetNotification = null;
        if (($interestRouting['should_update'] ?? false) === true
            && ($interestRouting['interest_group'] ?? '') === 'другое'
            && (string) ($interestRouting['stage_id'] ?? '') === (string) config('OTHER_DEAL_STAGE_ID', '26')
        ) {
            $nonTargetNotification = notifyNonTargetDeal($dealId, $context);
        }

        $phoneField = (string) config('DEAL_PHONE_FIELD', 'UF_CRM_1783332070432');
        $phoneWorkflow = null;
        if ($phone !== '' && array_key_exists($phoneField, $fields)) {
            $phoneWorkflow = startDealPhoneBusinessProcessOnce($dealId, $phone, $context);
        }

        $qualificationDistributionWorkflow = null;
        if (($interestRouting['should_update'] ?? false) !== true
            && ($qualification['should_update'] ?? false) === true
            && (string) ($fields['STAGE_ID'] ?? '') === (string) ($qualification['stage_id'] ?? '')
        ) {
            $qualificationDistributionWorkflow = startDealQualificationDistributionWorkflow(
                $dealId,
                $deal,
                $fields,
                $context
            );
        }

        logMessage('info', 'CRM deal fields updated from AI extraction', [
            'deal_id' => $dealId,
            'fields' => $fields,
            'extracted' => [
                'phone' => $phone,
                'city' => $city,
                'full_name' => $fullName,
                'interest_group' => $interestGroup,
                'branch' => $branch,
            ],
            'city_match' => $cityMatch,
            'branch_match' => $branchMatch,
            'branch_notification' => $branchNotification,
            'interest_routing' => $interestRouting,
            'qualification' => $qualification,
            'non_target_notification' => $nonTargetNotification,
            'phone_workflow' => $phoneWorkflow,
            'qualification_distribution_workflow' => $qualificationDistributionWorkflow,
            'qualified_chat_summary' => $qualifiedChatSummary,
            'update' => $update,
        ]);

        return [
            'ok' => true,
            'deal_id' => $dealId,
            'fields' => $fields,
            'extracted' => [
                'phone' => $phone,
                'city' => $city,
                'full_name' => $fullName,
                'interest_group' => $interestGroup,
                'branch' => $branch,
            ],
            'city_match' => $cityMatch,
            'branch_match' => $branchMatch,
            'branch_notification' => $branchNotification,
            'interest_routing' => $interestRouting,
            'qualification' => $qualification,
            'non_target_notification' => $nonTargetNotification,
            'phone_workflow' => $phoneWorkflow,
            'qualification_distribution_workflow' => $qualificationDistributionWorkflow,
            'qualified_chat_summary' => $qualifiedChatSummary,
            'bitrix' => $update,
        ];
    } catch (Throwable $exception) {
        logMessage('error', 'CRM sync failed', [
            'error' => $exception->getMessage(),
            'context' => $context,
        ]);

        return [
            'ok' => false,
            'error' => $exception->getMessage(),
        ];
    }
}

function writeQualifiedDealChatSummary(string $dealId, array $context, array $contact): array
{
    if (!(bool) config('QUALIFIED_DEAL_CHAT_SUMMARY_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'Qualified deal chat summary disabled.'];
    }

    $field = trim((string) config('DEAL_CHAT_SUMMARY_FIELD', 'COMMENTS'));
    if ($field === '') {
        return ['ok' => true, 'skipped' => 'DEAL_CHAT_SUMMARY_FIELD is not configured.'];
    }

    try {
        if (!function_exists('generateQualifiedDealChatSummary')
            || !function_exists('formatQualifiedDealComment')
        ) {
            throw new RuntimeException('Qualified deal summary functions are not loaded.');
        }

        $summary = generateQualifiedDealChatSummary($context, $contact);
        $comment = formatQualifiedDealComment($contact, $summary);
        $response = bitrixCall('crm.deal.update', [
            'id' => $dealId,
            'fields' => [$field => $comment],
        ]);

        $result = [
            'ok' => true,
            'deal_id' => $dealId,
            'field' => $field,
            'comment' => $comment,
            'response' => $response,
        ];
        logMessage('info', 'Qualified deal chat summary saved', $result);

        return $result;
    } catch (Throwable $exception) {
        $result = [
            'ok' => false,
            'deal_id' => $dealId,
            'field' => $field,
            'error' => $exception->getMessage(),
        ];
        logMessage('error', 'Qualified deal chat summary failed', $result);

        return $result;
    }
}

function notifyUndefinedBranchDeal(
    string $dealId,
    string $city,
    string $requestedBranch,
    array $context
): array {
    if (!(bool) config('UNDEFINED_BRANCH_NOTIFICATION_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'Undefined branch notification disabled.'];
    }

    $notificationKey = $dealId . ':' . sha1(normalizeCityName($city));
    $stateFile = (string) config('STATE_FILE');
    $state = readJsonFile($stateFile, []);
    if (isset($state['undefined_branch_notifications'][$notificationKey])) {
        return ['ok' => true, 'skipped' => 'Undefined branch notification already sent.'];
    }

    $notificationDialogId = trim((string) config(
        'UNDEFINED_BRANCH_NOTIFICATION_DIALOG_ID',
        config('NON_TARGET_NOTIFICATION_DIALOG_ID', 'chat985965')
    ));
    if ($notificationDialogId === '') {
        return ['ok' => true, 'skipped' => 'Undefined branch notification dialog is not configured.'];
    }

    if (!function_exists('sendBitrixNotificationMessage')) {
        return ['ok' => false, 'error' => 'Bitrix notification messaging is not loaded.'];
    }

    $portalUrl = rtrim((string) config('BITRIX_PORTAL_URL', 'https://darex.bitrix24.ru'), '/');
    $dealUrl = $portalUrl . '/crm/deal/details/' . rawurlencode($dealId) . '/';
    $clientDialogId = normalizeNotificationDialogId($context);
    $clientDialogUrl = $clientDialogId !== ''
        ? $portalUrl . '/online/?IM_DIALOG=' . rawurlencode($clientDialogId)
        : '';

    $messageLines = [
        'Не удалось определить филиал для сделки №' . $dealId . '.',
        'Город клиента: ' . $city . '.',
    ];
    if ($requestedBranch !== '') {
        $messageLines[] = 'Ответ ИИ по филиалу: ' . $requestedBranch . '.';
    }
    $messageLines[] = 'Необходимо проверить и указать филиал вручную.';
    $messageLines[] = '';
    $messageLines[] = '[URL=' . $dealUrl . ']Открыть сделку[/URL]';
    if ($clientDialogUrl !== '') {
        $messageLines[] = '[URL=' . $clientDialogUrl . ']Открыть диалог с клиентом[/URL]';
    }

    $botId = (int) config('UNDEFINED_BRANCH_NOTIFICATION_BOT_ID', 0);

    try {
        $delivery = sendBitrixNotificationMessage(
            $notificationDialogId,
            implode("\n", $messageLines),
            $botId
        );

        $state['undefined_branch_notifications'][$notificationKey] = [
            'deal_id' => $dealId,
            'city' => $city,
            'sent_at' => date(DATE_ATOM),
        ];
        if (count($state['undefined_branch_notifications']) > 500) {
            $state['undefined_branch_notifications'] = array_slice(
                $state['undefined_branch_notifications'],
                -500,
                null,
                true
            );
        }
        writeJsonFile($stateFile, $state);

        $result = [
            'ok' => true,
            'dialog_id' => $notificationDialogId,
            'client_dialog_id' => $clientDialogId,
            'sender' => $delivery['sender'],
            'bot_id' => $delivery['bot_id'],
            'response' => $delivery['response'],
        ];

        logMessage('warning', 'Undefined branch notification sent', [
            'deal_id' => $dealId,
            'city' => $city,
            'requested_branch' => $requestedBranch,
            'result' => $result,
        ]);

        return $result;
    } catch (Throwable $exception) {
        $result = [
            'ok' => false,
            'dialog_id' => $notificationDialogId,
            'client_dialog_id' => $clientDialogId,
            'error' => $exception->getMessage(),
        ];

        logMessage('error', 'Undefined branch notification failed', [
            'deal_id' => $dealId,
            'city' => $city,
            'requested_branch' => $requestedBranch,
            'result' => $result,
        ]);

        return $result;
    }
}

function notifyNonTargetDeal(string $dealId, array $context): array
{
    if (!(bool) config('NON_TARGET_NOTIFICATION_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'Non-target notification disabled.'];
    }

    $notificationDialogId = trim((string) config('NON_TARGET_NOTIFICATION_DIALOG_ID', 'chat985965'));
    if ($notificationDialogId === '') {
        return ['ok' => true, 'skipped' => 'Notification dialog is not configured.'];
    }

    if (!function_exists('sendBitrixNotificationMessage')) {
        return ['ok' => false, 'error' => 'Bitrix notification messaging is not loaded.'];
    }

    $portalUrl = rtrim((string) config('BITRIX_PORTAL_URL', 'https://darex.bitrix24.ru'), '/');
    $dealUrl = $portalUrl . '/crm/deal/details/' . rawurlencode($dealId) . '/';
    $clientDialogId = normalizeNotificationDialogId($context);
    $clientDialogUrl = $clientDialogId !== ''
        ? $portalUrl . '/online/?IM_DIALOG=' . rawurlencode($clientDialogId)
        : '';

    $messageLines = [
        'Сделка №' . $dealId . ' перемещена в стадию «НЕЦЕЛЕВОЙ». Необходимо проверить.',
        '',
        '[URL=' . $dealUrl . ']Открыть сделку[/URL]',
    ];
    if ($clientDialogUrl !== '') {
        $messageLines[] = '[URL=' . $clientDialogUrl . ']Открыть диалог с клиентом[/URL]';
    }

    $botId = (int) config('NON_TARGET_NOTIFICATION_BOT_ID', 0);

    try {
        $delivery = sendBitrixNotificationMessage($notificationDialogId, implode("\n", $messageLines), $botId);
        $result = [
            'ok' => true,
            'dialog_id' => $notificationDialogId,
            'client_dialog_id' => $clientDialogId,
            'sender' => $delivery['sender'],
            'bot_id' => $delivery['bot_id'],
            'response' => $delivery['response'],
        ];

        logMessage('info', 'Non-target deal notification sent', [
            'deal_id' => $dealId,
            'result' => $result,
        ]);

        return $result;
    } catch (Throwable $exception) {
        $result = [
            'ok' => false,
            'dialog_id' => $notificationDialogId,
            'client_dialog_id' => $clientDialogId,
            'sender' => $botId > 0 ? 'bot' : 'application_user',
            'bot_id' => $botId > 0 ? $botId : null,
            'error' => $exception->getMessage(),
        ];

        logMessage('error', 'Non-target deal notification failed', [
            'deal_id' => $dealId,
            'result' => $result,
        ]);

        return $result;
    }
}

function normalizeNotificationDialogId(array $context): string
{
    $dialogId = trim((string) ($context['dialog_id'] ?? ''));
    if (str_starts_with($dialogId, 'chat')) {
        return $dialogId;
    }

    $chatId = normalizeCrmId($context['chat_id'] ?? '');
    if ($chatId !== '') {
        return 'chat' . $chatId;
    }

    $dialogChatId = normalizeCrmId($dialogId);

    return $dialogChatId !== '' ? 'chat' . $dialogChatId : '';
}

function routeDealByInterest(array $deal, array $pendingFields, string $interestGroup): array
{
    if (!(bool) config('INTEREST_ROUTING_ENABLED', true)) {
        return ['should_update' => false, 'reason' => 'Interest routing disabled.'];
    }

    $interestField = (string) config('DEAL_INTEREST_GROUP_FIELD', 'UF_CRM_1784809347681');
    $effectiveInterestSource = $pendingFields[$interestField] ?? '';
    if (!dealFieldHasValue($effectiveInterestSource)) {
        $effectiveInterestSource = $interestGroup;
    }
    if (!dealFieldHasValue($effectiveInterestSource)) {
        $effectiveInterestSource = $deal[$interestField] ?? '';
    }
    $effectiveInterest = normalizeInterestGroup($effectiveInterestSource);

    $routes = [
        'запчасти' => [
            'category_id' => (string) config('PARTS_DEAL_CATEGORY_ID', '26'),
            'stage_id' => (string) config('PARTS_INITIAL_STAGE_ID', 'C26:NEW'),
        ],
        'сервис' => [
            'category_id' => (string) config('SERVICE_DEAL_CATEGORY_ID', '30'),
            'stage_id' => (string) config('SERVICE_INITIAL_STAGE_ID', 'C30:NEW'),
        ],
        'другое' => [
            'category_id' => (string) config('OTHER_DEAL_CATEGORY_ID', '0'),
            'stage_id' => (string) config('OTHER_DEAL_STAGE_ID', '26'),
            'force_stage' => true,
        ],
    ];

    if (!isset($routes[$effectiveInterest])) {
        return [
            'should_update' => false,
            'reason' => 'Interest does not require category routing.',
            'interest_group' => $effectiveInterest,
        ];
    }

    $route = $routes[$effectiveInterest];
    if ($route['category_id'] === '' || $route['stage_id'] === '') {
        return [
            'should_update' => false,
            'reason' => 'Target category or stage is not configured.',
            'interest_group' => $effectiveInterest,
        ];
    }

    $currentCategoryId = (string) ($deal['CATEGORY_ID'] ?? '0');
    if ($currentCategoryId === $route['category_id']) {
        $forceStage = ($route['force_stage'] ?? false) === true;
        $currentStageId = (string) ($deal['STAGE_ID'] ?? '');
        if ($forceStage && $currentStageId !== $route['stage_id']) {
            return [
                'should_update' => true,
                'reason' => 'Deal is in the target category but must be moved to the configured stage.',
                'interest_group' => $effectiveInterest,
                'category_id' => $route['category_id'],
                'stage_id' => $route['stage_id'],
            ];
        }

        return [
            'should_update' => false,
            'reason' => $forceStage
                ? 'Deal is already in the target category and stage.'
                : 'Deal is already in the target category.',
            'interest_group' => $effectiveInterest,
            'category_id' => $currentCategoryId,
        ];
    }

    return [
        'should_update' => true,
        'reason' => 'Deal must be moved to the category matching client interest.',
        'interest_group' => $effectiveInterest,
        'category_id' => $route['category_id'],
        'stage_id' => $route['stage_id'],
    ];
}

function qualifyRetailDealForTechnique(array $deal, array $pendingFields, string $interestGroup): array
{
    if (!(bool) config('TECHNIQUE_QUALIFICATION_ENABLED', true)) {
        return ['should_update' => false, 'reason' => 'Technique qualification disabled.'];
    }

    $interestField = (string) config('DEAL_INTEREST_GROUP_FIELD', 'UF_CRM_1784809347681');
    $effectiveInterestSource = $pendingFields[$interestField] ?? '';
    if (!dealFieldHasValue($effectiveInterestSource)) {
        $effectiveInterestSource = $interestGroup;
    }
    if (!dealFieldHasValue($effectiveInterestSource)) {
        $effectiveInterestSource = $deal[$interestField] ?? '';
    }
    $effectiveInterest = normalizeInterestGroup($effectiveInterestSource);
    if ($effectiveInterest !== '' && $effectiveInterest !== 'техника') {
        return ['should_update' => false, 'reason' => 'Interest group is neither technique nor empty.'];
    }

    $retailCategoryId = (string) config('RETAIL_DEAL_CATEGORY_ID', '0');
    $dealCategoryId = (string) ($deal['CATEGORY_ID'] ?? '0');
    if ($dealCategoryId !== $retailCategoryId) {
        return [
            'should_update' => false,
            'reason' => 'Deal is outside the retail category.',
            'category_id' => $dealCategoryId,
        ];
    }

    $phoneField = (string) config('DEAL_PHONE_FIELD', 'UF_CRM_1783332070432');
    $cityField = (string) config('DEAL_CITY_FIELD', 'UF_CRM_1693995123245');
    $phone = $pendingFields[$phoneField] ?? ($deal[$phoneField] ?? '');
    $city = $pendingFields[$cityField] ?? ($deal[$cityField] ?? '');

    if (!dealFieldHasValue($phone) || !dealFieldHasValue($city)) {
        return [
            'should_update' => false,
            'reason' => 'Phone or city is missing.',
            'has_phone' => dealFieldHasValue($phone),
            'has_city' => dealFieldHasValue($city),
        ];
    }

    $stageId = trim((string) config('TECHNIQUE_QUALIFIED_STAGE_ID', '37'));
    if ($stageId === '') {
        return ['should_update' => false, 'reason' => 'Qualified stage is not configured.'];
    }

    if ((string) ($deal['STAGE_ID'] ?? '') === $stageId) {
        return [
            'should_update' => false,
            'reason' => 'Deal is already qualified.',
            'stage_id' => $stageId,
        ];
    }

    return [
        'should_update' => true,
        'reason' => $effectiveInterest === 'техника'
            ? 'Technique interest, phone and city are present in retail deal.'
            : 'Interest is empty, phone and city are present in retail deal.',
        'category_id' => $dealCategoryId,
        'stage_id' => $stageId,
    ];
}

function dealFieldHasValue($value): bool
{
    if (is_array($value)) {
        foreach ($value as $item) {
            if (dealFieldHasValue($item)) {
                return true;
            }
        }

        return false;
    }

    return trim((string) $value) !== '';
}

function managedOpenLineBotScope(array $context): array
{
    $lineId = normalizeCrmId($context['open_line_id'] ?? '');
    $botId = normalizeCrmId($context['bot_id'] ?? '');
    $allowedLineIds = managedOpenLineIdsFromPrompts();
    $allowedBotIds = normalizedConfiguredIds(
        config('MANAGED_OPEN_LINE_BOT_IDS', [config('BITRIX_OPENLINE_BOT_ID', '')])
    );

    if ($lineId === '') {
        return ['allowed' => false, 'reason' => 'Open line id is missing.', 'line_id' => '', 'bot_id' => $botId];
    }
    if ($botId === '') {
        return ['allowed' => false, 'reason' => 'Open line bot id is missing.', 'line_id' => $lineId, 'bot_id' => ''];
    }
    if (!in_array($lineId, $allowedLineIds, true)) {
        return [
            'allowed' => false,
            'reason' => 'Open line is not managed by this application.',
            'line_id' => $lineId,
            'bot_id' => $botId,
            'allowed_line_ids' => $allowedLineIds,
        ];
    }
    if (!in_array($botId, $allowedBotIds, true)) {
        return [
            'allowed' => false,
            'reason' => 'Bot is not allowed to manage CRM deals.',
            'line_id' => $lineId,
            'bot_id' => $botId,
            'allowed_bot_ids' => $allowedBotIds,
        ];
    }

    return [
        'allowed' => true,
        'reason' => 'Managed open line bot context.',
        'line_id' => $lineId,
        'bot_id' => $botId,
    ];
}

function managedOpenLineIdsFromPrompts(): array
{
    return managedOpenLineIdsFromPromptData(loadPromptsFromBitrixLists());
}

function managedOpenLineIdsFromPromptData(array $prompts): array
{
    $lineIds = [];
    foreach (['system', 'corrections'] as $group) {
        foreach ((array) ($prompts[$group] ?? []) as $prompt) {
            if (!is_array($prompt)) {
                continue;
            }

            foreach (normalizePromptOpenLineIds($prompt['open_line_ids'] ?? []) as $lineId) {
                $lineIds[] = $lineId;
            }
        }
    }

    return array_values(array_unique($lineIds));
}

function normalizedConfiguredIds($values): array
{
    if (is_string($values)) {
        $values = preg_split('/[\s,;]+/', $values) ?: [];
    } elseif (!is_array($values)) {
        $values = [$values];
    }

    $result = [];
    foreach ($values as $value) {
        $id = normalizeCrmId($value);
        if ($id !== '') {
            $result[] = $id;
        }
    }

    return array_values(array_unique($result));
}

function updateDealFieldsWithStageRouting(string $dealId, array $fields): array
{
    $categoryId = array_key_exists('CATEGORY_ID', $fields)
        ? trim((string) $fields['CATEGORY_ID'])
        : null;
    $stageId = array_key_exists('STAGE_ID', $fields)
        ? trim((string) $fields['STAGE_ID'])
        : null;

    $regularFields = $fields;
    unset($regularFields['CATEGORY_ID'], $regularFields['STAGE_ID']);

    $regularUpdate = null;
    if ($regularFields !== []) {
        $regularUpdate = bitrixCall('crm.deal.update', [
            'id' => $dealId,
            'fields' => $regularFields,
        ]);
    }

    $routingUpdate = null;
    if ($categoryId !== null || $stageId !== null) {
        $routingUpdate = updateDealCategoryAndStage($dealId, $categoryId, $stageId);
    }

    return [
        'result' => true,
        'regular_update' => $regularUpdate,
        'routing_update' => $routingUpdate,
    ];
}

function updateDealCategoryAndStage(
    string $dealId,
    ?string $categoryId,
    ?string $stageId
): array {
    $routingFields = [];
    if ($categoryId !== null && $categoryId !== '') {
        $routingFields['categoryId'] = (int) $categoryId;
    }
    if ($stageId !== null && $stageId !== '') {
        $routingFields['stageId'] = $stageId;
    }

    if ($routingFields === []) {
        return ['result' => true, 'skipped' => 'No category or stage fields supplied.'];
    }

    bitrixCall('crm.item.update', [
        'entityTypeId' => 2,
        'id' => $dealId,
        'fields' => $routingFields,
    ]);

    $dealResponse = bitrixCall('crm.deal.get', ['id' => $dealId]);
    $deal = (array) ($dealResponse['result'] ?? []);
    $actualCategoryId = (string) ($deal['CATEGORY_ID'] ?? '');
    $actualStageId = (string) ($deal['STAGE_ID'] ?? '');
    $categoryMatches = $categoryId === null || $categoryId === '' || $actualCategoryId === $categoryId;
    $stageMatches = $stageId === null || $stageId === '' || $actualStageId === $stageId;

    if (!$categoryMatches || !$stageMatches) {
        throw new RuntimeException(sprintf(
            'Deal routing verification failed for deal %s: expected category=%s stage=%s, got category=%s stage=%s',
            $dealId,
            $categoryId ?? '',
            $stageId ?? '',
            $actualCategoryId,
            $actualStageId
        ));
    }

    return [
        'result' => true,
        'method' => 'crm.item.update',
        'category_id' => $actualCategoryId,
        'stage_id' => $actualStageId,
        'verified' => true,
    ];
}

function startDealPhoneBusinessProcessOnce(string $dealId, string $phone, array $context): array
{
    if (!(bool) config('PHONE_WORKFLOW_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'Phone workflow disabled.'];
    }

    $templateId = (int) config('PHONE_WORKFLOW_TEMPLATE_ID', 1905);
    if ($templateId <= 0) {
        return ['ok' => true, 'skipped' => 'PHONE_WORKFLOW_TEMPLATE_ID is not configured.'];
    }

    if (function_exists('hasPhoneWorkflowStarted') && hasPhoneWorkflowStarted($context, $phone)) {
        return ['ok' => true, 'skipped' => 'Phone workflow already started for this phone.'];
    }

    $params = [
        'TEMPLATE_ID' => $templateId,
        'DOCUMENT_ID' => [
            'crm',
            'CCrmDocumentDeal',
            'DEAL_' . $dealId,
        ],
    ];

    $parameterName = trim((string) config('PHONE_WORKFLOW_PARAMETER_NAME', ''));
    if ($parameterName !== '') {
        $params['PARAMETERS'] = [
            $parameterName => $phone,
        ];
    }

    $response = bitrixCallRaw('bizproc.workflow.start', $params);
    $result = [
        'ok' => !isset($response['error']),
        'template_id' => $templateId,
        'deal_id' => $dealId,
        'phone' => $phone,
        'response' => $response,
    ];

    if (isset($response['error'])) {
        logMessage('error', 'Phone business process start failed', [
            'deal_id' => $dealId,
            'phone' => $phone,
            'params' => $params,
            'response' => $response,
        ]);

        return $result;
    }

    if (function_exists('markPhoneWorkflowStarted')) {
        markPhoneWorkflowStarted($context, $phone, $result);
    }

    logMessage('info', 'Phone business process started', [
        'deal_id' => $dealId,
        'phone' => $phone,
        'params' => $params,
        'response' => $response,
    ]);

    return $result;
}

function startDealQualificationDistributionWorkflow(
    string $dealId,
    array $deal,
    array $pendingFields,
    array $context = []
): array
{
    if (!(bool) config('QUALIFICATION_DISTRIBUTION_WORKFLOW_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'Qualification distribution workflow disabled.'];
    }

    $templateId = (int) config('QUALIFICATION_DISTRIBUTION_WORKFLOW_TEMPLATE_ID', 1907);
    if ($templateId <= 0) {
        return [
            'ok' => true,
            'skipped' => 'QUALIFICATION_DISTRIBUTION_WORKFLOW_TEMPLATE_ID is not configured.',
        ];
    }

    $lock = acquireQualificationWorkflowLock($dealId);
    try {
        if ($context !== [] && function_exists('getQualificationWorkflowState')) {
            $workflowState = getQualificationWorkflowState($context, $dealId);
            if ((string) ($workflowState['status'] ?? '') === 'started'
                && normalizeCrmId($workflowState['deal_id'] ?? '') === normalizeCrmId($dealId)
            ) {
                $result = [
                    'ok' => true,
                    'skipped' => 'Qualification distribution workflow already started for this deal.',
                    'deal_id' => $dealId,
                    'started_at' => (string) ($workflowState['started_at'] ?? ''),
                ];

                if (function_exists('saveDealQualificationWorkflowState')) {
                    saveDealQualificationWorkflowState($dealId, $workflowState);
                }

                logMessage('info', 'Qualification distribution workflow duplicate prevented', $result);

                return $result;
            }
        }

        return startDealQualificationDistributionWorkflowUnlocked(
            $dealId,
            $deal,
            $pendingFields,
            $context,
            $templateId
        );
    } finally {
        releaseQualificationWorkflowLock($lock);
    }
}

function startDealQualificationDistributionWorkflowUnlocked(
    string $dealId,
    array $deal,
    array $pendingFields,
    array $context,
    int $templateId
): array {

    $branchField = (string) config('DEAL_BRANCH_FIELD', 'UF_CRM_1764919997');
    $branchValue = $pendingFields[$branchField] ?? ($deal[$branchField] ?? '');
    $branchItem = resolveRoutableDealBranchItem($branchValue, getDealUserFieldEnumItems($branchField));
    if ($branchItem === null) {
        $result = [
            'ok' => true,
            'skipped' => 'Qualified deal does not have a routable branch.',
            'deal_id' => $dealId,
            'branch_value' => $branchValue,
        ];

        logMessage('warning', 'Qualification distribution workflow skipped', $result);

        if ($context !== [] && function_exists('markQualificationWorkflowPending')) {
            markQualificationWorkflowPending($context, $dealId, $result);
        }

        return $result;
    }

    $params = [
        'TEMPLATE_ID' => $templateId,
        'DOCUMENT_ID' => [
            'crm',
            'CCrmDocumentDeal',
            'DEAL_' . $dealId,
        ],
    ];

    $response = bitrixCallRaw('bizproc.workflow.start', $params);
    $result = [
        'ok' => !isset($response['error']),
        'template_id' => $templateId,
        'deal_id' => $dealId,
        'branch' => $branchItem,
        'response' => $response,
    ];

    logMessage(
        isset($response['error']) ? 'error' : 'info',
        isset($response['error'])
            ? 'Qualification distribution workflow start failed'
            : 'Qualification distribution workflow started',
        $result
    );

    if ($context !== []) {
        if (isset($response['error']) && function_exists('markQualificationWorkflowPending')) {
            markQualificationWorkflowPending($context, $dealId, $result);
        } elseif (!isset($response['error']) && function_exists('markQualificationWorkflowStarted')) {
            markQualificationWorkflowStarted($context, $dealId, $result);
        }
    }

    return $result;
}

function acquireQualificationWorkflowLock(string $dealId)
{
    $lockDir = dirname((string) config('STATE_FILE')) . '/locks';
    if (!is_dir($lockDir) && !mkdir($lockDir, 0775, true) && !is_dir($lockDir)) {
        throw new RuntimeException('Cannot create workflow lock directory: ' . $lockDir);
    }

    $lockPath = $lockDir . '/qualification_workflow_' . normalizeCrmId($dealId) . '.lock';
    $handle = @fopen($lockPath, 'c');
    if ($handle === false && is_file($lockPath)) {
        // A lock created by PHP-FPM may predate the shared-runtime permission fix.
        // flock works with a read-only descriptor and does not modify file content.
        $handle = @fopen($lockPath, 'r');
    }

    if ($handle === false) {
        throw new RuntimeException('Cannot open qualification workflow lock: ' . $lockPath);
    }

    @chmod($lockPath, 0664);

    if (!flock($handle, LOCK_EX)) {
        if (is_resource($handle)) {
            fclose($handle);
        }
        throw new RuntimeException('Cannot acquire qualification workflow lock: ' . $lockPath);
    }

    return $handle;
}

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

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

function retryPendingQualificationDistributionWorkflow(array $context): array
{
    if (!function_exists('getQualificationWorkflowState')) {
        return ['ok' => true, 'skipped' => 'Qualification workflow state storage is unavailable.'];
    }

    $state = getQualificationWorkflowState($context);
    if ((string) ($state['status'] ?? '') !== 'pending') {
        return ['ok' => true, 'skipped' => 'No pending qualification workflow.'];
    }

    $dealId = normalizeCrmId($state['deal_id'] ?? ($context['deal_id'] ?? ''));
    if ($dealId === '') {
        return ['ok' => false, 'error' => 'Pending qualification workflow has no deal id.'];
    }

    $dealResponse = bitrixCall('crm.deal.get', ['id' => $dealId]);
    $deal = (array) ($dealResponse['result'] ?? []);
    $expectedCategoryId = (string) config('RETAIL_DEAL_CATEGORY_ID', '0');
    $expectedStageId = (string) config('TECHNIQUE_QUALIFIED_STAGE_ID', '37');
    if ((string) ($deal['CATEGORY_ID'] ?? '') !== $expectedCategoryId
        || (string) ($deal['STAGE_ID'] ?? '') !== $expectedStageId
    ) {
        return [
            'ok' => true,
            'skipped' => 'Deal is no longer in the qualified retail stage.',
            'deal_id' => $dealId,
            'category_id' => (string) ($deal['CATEGORY_ID'] ?? ''),
            'stage_id' => (string) ($deal['STAGE_ID'] ?? ''),
        ];
    }

    return startDealQualificationDistributionWorkflow($dealId, $deal, [], $context);
}

function resolveRoutableDealBranchItem($branchValue, array $items): ?array
{
    $branchIds = is_array($branchValue) ? $branchValue : [$branchValue];
    foreach ($branchIds as $branchId) {
        $branchId = trim((string) $branchId);
        if ($branchId === '') {
            continue;
        }

        foreach ($items as $item) {
            if ((string) ($item['id'] ?? '') !== $branchId) {
                continue;
            }

            return isRoutableDealBranchEnumItem((array) $item) ? $item : null;
        }
    }

    return null;
}

function markDealClientNotResponding(array $context, array $contact = []): array
{
    if (!(bool) config('CRM_SYNC_ENABLED', true)) {
        return ['ok' => true, 'skipped' => 'CRM sync disabled.'];
    }

    $scope = managedOpenLineBotScope($context);
    if (($scope['allowed'] ?? false) !== true) {
        return ['ok' => true, 'skipped' => (string) ($scope['reason'] ?? 'Open line scope rejected.'), 'scope' => $scope];
    }

    $dealId = normalizeCrmId($context['deal_id'] ?? '');
    if ($dealId === '') {
        $dealId = resolveDealIdFromOpenLineDialog($context);
    }

    if ($dealId === '') {
        logMessage('warning', 'No-response mark skipped: deal id not found', [
            'context' => $context,
        ]);

        return ['ok' => true, 'skipped' => 'Deal id not found.'];
    }

    $fields = [];
    $fieldName = trim((string) config('DEAL_NO_RESPONSE_FIELD', ''));
    $value = config('DEAL_NO_RESPONSE_VALUE', 'Y');
    if ($fieldName !== '') {
        $fields[$fieldName] = $value;
    }

    $movedToNoPhoneStage = false;
    $phone = normalizePhoneForDeal((string) ($contact['phone'] ?? ''));
    if ($phone === '' && (bool) config('NO_PHONE_FINAL_STAGE_ENABLED', true)) {
        $dealResponse = bitrixCall('crm.deal.get', ['id' => $dealId]);
        $deal = (array) ($dealResponse['result'] ?? []);
        $phoneField = trim((string) config('DEAL_PHONE_FIELD', 'UF_CRM_1783332070432'));
        $dealPhone = $phoneField !== '' ? ($deal[$phoneField] ?? '') : '';

        if (!dealFieldHasValue($dealPhone)) {
            $stageId = trim((string) config('NO_PHONE_FINAL_STAGE_ID', '28'));
            if ($stageId !== '') {
                $fields['STAGE_ID'] = $stageId;
                $movedToNoPhoneStage = true;
            }
        }
    }

    if ($fields === []) {
        return ['ok' => true, 'skipped' => 'No no-response CRM fields are configured.'];
    }

    $update = updateDealFieldsWithStageRouting($dealId, $fields);

    logMessage('info', 'CRM deal marked as client not responding', [
        'deal_id' => $dealId,
        'field' => $fieldName,
        'value' => $value,
        'fields' => $fields,
        'moved_to_no_phone_stage' => $movedToNoPhoneStage,
        'update' => $update,
    ]);

    return [
        'ok' => true,
        'deal_id' => $dealId,
        'field' => $fieldName,
        'value' => $value,
        'fields' => $fields,
        'moved_to_no_phone_stage' => $movedToNoPhoneStage,
        'bitrix' => $update,
    ];
}

function resolveDealIdFromOpenLineDialog(array $context): string
{
    $params = [];
    $dialogId = trim((string) ($context['dialog_id'] ?? ''));
    $chatId = normalizeCrmId($context['chat_id'] ?? '');

    if ($dialogId !== '' && str_starts_with($dialogId, 'chat')) {
        $params['DIALOG_ID'] = $dialogId;
    } elseif ($chatId !== '') {
        $params['CHAT_ID'] = $chatId;
    } elseif ($dialogId !== '') {
        $dialogChatId = normalizeCrmId($dialogId);
        if ($dialogChatId !== '') {
            $params['CHAT_ID'] = $dialogChatId;
        }
    }

    if ($params === []) {
        return '';
    }

    $response = bitrixCallRaw('imopenlines.dialog.get', $params);
    if (isset($response['error'])) {
        logMessage('warning', 'Cannot resolve deal id from open line dialog', [
            'params' => $params,
            'response' => $response,
        ]);

        return '';
    }

    $result = (array) ($response['result'] ?? []);
    $dealId = extractDealIdFromDialogData($result);

    logMessage('info', 'Open line dialog CRM entity resolved', [
        'params' => $params,
        'deal_id' => $dealId,
        'entity_data_1' => findArrayValueByKey($result, ['entity_data_1', 'ENTITY_DATA_1']),
        'entity_data_2' => findArrayValueByKey($result, ['entity_data_2', 'ENTITY_DATA_2']),
    ]);

    return $dealId;
}

function extractDealIdFromDialogData(array $dialog): string
{
    $direct = normalizeCrmId(findArrayValueByKey($dialog, ['deal_id', 'DEAL_ID', 'crm_deal_id', 'CRM_DEAL_ID']));
    if ($direct !== '') {
        return $direct;
    }

    foreach (['entity_data_1', 'ENTITY_DATA_1', 'entity_data_2', 'ENTITY_DATA_2'] as $key) {
        $entityData = (string) findArrayValueByKey($dialog, [$key]);
        $entityId = extractDealIdFromEntityDataString($entityData);
        if ($entityId !== '') {
            return $entityId;
        }
    }

    $entityType = strtoupper((string) findArrayValueByKey($dialog, ['crm_entity_type', 'CRM_ENTITY_TYPE', 'entity_type', 'ENTITY_TYPE']));
    if (in_array($entityType, ['DEAL', 'D'], true)) {
        return normalizeCrmId(findArrayValueByKey($dialog, ['crm_entity_id', 'CRM_ENTITY_ID', 'entity_id', 'ENTITY_ID']));
    }

    return '';
}

function extractDealIdFromEntityDataString(string $entityData): string
{
    $entityData = trim($entityData);
    if ($entityData === '') {
        return '';
    }

    $parts = explode('|', $entityData);
    foreach ($parts as $index => $part) {
        if (strtoupper(trim($part)) !== 'DEAL') {
            continue;
        }

        $entityId = normalizeCrmId($parts[$index + 1] ?? '');
        if ($entityId !== '') {
            return $entityId;
        }
    }

    return '';
}

function findArrayValueByKey(array $data, array $keys)
{
    $normalizedKeys = array_map('strtoupper', $keys);
    foreach ($data as $key => $value) {
        if (in_array(strtoupper((string) $key), $normalizedKeys, true) && !is_array($value) && $value !== '') {
            return $value;
        }

        if (is_array($value)) {
            $found = findArrayValueByKey($value, $keys);
            if ($found !== null && $found !== '') {
                return $found;
            }
        }
    }

    return null;
}

function extractDealContactFields(string $latestUserMessage, array $context): array
{
    $apiKey = trim((string) config('OPENAI_API_KEY', ''));
    if ($apiKey === '') {
        return ['phone' => '', 'city' => '', 'full_name' => '', 'interest_group' => '', 'branch' => ''];
    }

    $branchNames = getRoutableDealBranchNamesSafely();
    $branchInstruction = buildDealBranchAiInstruction($branchNames, true);
    $historyLines = [];
    if (function_exists('getChatHistory')) {
        foreach (getChatHistory($context) as $message) {
            $role = (string) ($message['role'] ?? '');
            $content = trim((string) ($message['content'] ?? ''));
            if ($content !== '' && $role === 'user') {
                $historyLines[] = $role . ': ' . $content;
            }
        }
    }

    $historyText = implode("\n", array_slice($historyLines, -10));
    $requestId = 'crm-extract-' . date('YmdHis') . '-' . bin2hex(random_bytes(4));
    $payload = [
        'model' => (string) config('OPENAI_EXTRACTION_MODEL', config('OPENAI_MODEL', 'gpt-4.1-mini')),
        'messages' => [
            [
                'role' => 'system',
                'content' => implode("\n", [
                    'Extract the client phone number, city, full name, interest group and Darex branch from the conversation.',
                    'Return only JSON without explanations: {"phone":"","city":"","full_name":"","interest_group":"","branch":""}.',
                    'Fill phone, city and full_name only if the client explicitly provided them.',
                    'If a value is missing or uncertain, return an empty string for it.',
                    'If the city is written in Russian, keep the normal Russian city name.',
                    'For full_name, use only the client name, not manager, bot, product or company names.',
                    'Classify interest_group as exactly one of: "техника", "запчасти", "сервис", "другое".',
                    'Use "техника" for vehicles and machinery, "запчасти" for parts and accessories, "сервис" for repair, maintenance or diagnostics.',
                    'Use an empty interest_group if it cannot be determined from the client messages.',
                    $branchInstruction,
                    'When city is not empty, branch must also be non-empty: always choose the geographically nearest branch from the allowed list, even if the client did not explicitly name a branch.',
                    'Examples: Сальск maps to Ростов на Дону; Березники maps to Пермь. Return an empty branch only when neither the city nor an explicitly selected branch is known.',
                ]),
            ],
            [
                'role' => 'user',
                'content' => "Conversation history:\n" . $historyText . "\n\nLatest client message:\n" . $latestUserMessage,
            ],
        ],
        'temperature' => 0,
        'response_format' => [
            'type' => 'json_object',
        ],
    ];

    logMessage('info', 'OpenAI CRM extraction request prepared', [
        'request_id' => $requestId,
        'context' => $context,
        'payload' => $payload,
    ]);

    $response = openAiChatCompletions($payload, $apiKey, $requestId);
    $content = trim((string) ($response['choices'][0]['message']['content'] ?? ''));
    $decoded = json_decode($content, true);

    if (!is_array($decoded)) {
        logMessage('warning', 'OpenAI CRM extraction returned invalid JSON', [
            'request_id' => $requestId,
            'content' => $content,
        ]);

        return ['phone' => '', 'city' => '', 'full_name' => '', 'interest_group' => '', 'branch' => ''];
    }

    $result = [
        'phone' => trim((string) ($decoded['phone'] ?? '')),
        'city' => trim((string) ($decoded['city'] ?? '')),
        'full_name' => trim((string) ($decoded['full_name'] ?? '')),
        'interest_group' => normalizeInterestGroup($decoded['interest_group'] ?? ''),
        'branch' => normalizeBranchName($decoded['branch'] ?? ''),
    ];

    logMessage('info', 'OpenAI CRM extraction response received', [
        'request_id' => $requestId,
        'result' => $result,
        'usage' => $response['usage'] ?? null,
    ]);

    return $result;
}

function getDealUserFieldEnumItems(string $fieldName): array
{
    static $cache = [];

    if (isset($cache[$fieldName])) {
        return $cache[$fieldName];
    }

    $field = findDealUserFieldByName($fieldName);
    if ($field === null) {
        throw new RuntimeException('Deal user field not found: ' . $fieldName);
    }

    $cache[$fieldName] = normalizeUserFieldEnumItems((array) ($field['LIST'] ?? []));

    return $cache[$fieldName];
}

function getRoutableDealBranchEnumItems(): array
{
    $fieldName = (string) config('DEAL_BRANCH_FIELD', 'UF_CRM_1764919997');

    return array_values(array_filter(
        getDealUserFieldEnumItems($fieldName),
        static fn (array $item): bool => isRoutableDealBranchEnumItem($item)
    ));
}

function getRoutableDealBranchNamesSafely(): array
{
    try {
        return array_values(array_map(
            static fn (array $item): string => trim((string) ($item['value'] ?? '')),
            getRoutableDealBranchEnumItems()
        ));
    } catch (Throwable $exception) {
        logMessage('warning', 'Cannot load deal branches for AI instruction', [
            'field' => (string) config('DEAL_BRANCH_FIELD', 'UF_CRM_1764919997'),
            'error' => $exception->getMessage(),
        ]);

        return [];
    }
}

function buildDealBranchAiInstruction(array $branchNames, bool $cityRequiresBranch): string
{
    $branchNames = array_values(array_filter(array_map(
        static fn ($value): string => trim((string) $value),
        $branchNames
    )));

    if ($branchNames === []) {
        return 'Return an empty branch because the current Bitrix24 branch list is unavailable.';
    }

    $quotedNames = array_map(
        static fn (string $name): string => '"' . str_replace('"', '\\"', $name) . '"',
        $branchNames
    );
    $instruction = 'branch is the Darex branch that should handle the client. Return exactly one of the current Bitrix24 field values: '
        . implode(', ', $quotedNames) . '.';

    if ($cityRequiresBranch) {
        $instruction .= ' When city is not empty, branch must also be non-empty: choose the geographically nearest branch from this list.';
    }

    return $instruction;
}

function isRoutableDealBranchEnumItem(array $item): bool
{
    $name = trim((string) ($item['value'] ?? ''));
    if ($name === '') {
        return false;
    }

    $undefinedValue = trim((string) config('DEAL_UNDEFINED_BRANCH_VALUE', 'Не определено'));

    return $undefinedValue === '' || normalizeCityName($name) !== normalizeCityName($undefinedValue);
}

function normalizeBranchName($value): string
{
    $value = trim((string) $value);
    if ($value === '') {
        return '';
    }

    try {
        return normalizeBranchNameFromItems($value, getRoutableDealBranchEnumItems());
    } catch (Throwable $exception) {
        logMessage('warning', 'Cannot normalize deal branch from Bitrix24 field', [
            'branch' => $value,
            'error' => $exception->getMessage(),
        ]);

        return '';
    }
}

function normalizeBranchNameFromItems($value, array $items): string
{
    $needle = normalizeCityName((string) $value);
    if ($needle === '') {
        return '';
    }

    foreach ($items as $item) {
        if (!isRoutableDealBranchEnumItem((array) $item)) {
            continue;
        }

        $name = trim((string) ($item['value'] ?? ''));
        if (normalizeCityName($name) === $needle) {
            return $name;
        }
    }

    return '';
}

function findDealUserFieldByName(string $fieldName): ?array
{
    $start = 0;

    do {
        $response = bitrixCall('crm.deal.userfield.list', [
            'filter' => [
                'FIELD_NAME' => $fieldName,
            ],
            'start' => $start,
        ]);

        foreach ((array) ($response['result'] ?? []) as $field) {
            $field = (array) $field;
            if ((string) ($field['FIELD_NAME'] ?? '') === $fieldName) {
                return $field;
            }
        }

        $next = $response['next'] ?? null;
        $start = is_numeric($next) ? (int) $next : 0;
    } while ($start > 0);

    return null;
}

function normalizeUserFieldEnumItems(array $items): array
{
    $result = [];

    foreach ($items as $item) {
        $item = (array) $item;
        $result[] = [
            'id' => isset($item['ID']) ? (string) $item['ID'] : '',
            'value' => (string) ($item['VALUE'] ?? ''),
            'sort' => isset($item['SORT']) ? (int) $item['SORT'] : null,
            'def' => (string) ($item['DEF'] ?? 'N'),
            'xml_id' => (string) ($item['XML_ID'] ?? ''),
        ];
    }

    usort($result, static function (array $a, array $b): int {
        return [$a['sort'] ?? 500, $a['value'], $a['id']] <=> [$b['sort'] ?? 500, $b['value'], $b['id']];
    });

    return $result;
}

function matchDealCityEnumValue(string $city, array $items): ?array
{
    $needle = normalizeCityName($city);
    if ($needle === '') {
        return null;
    }

    $best = null;
    foreach ($items as $item) {
        $value = (string) ($item['value'] ?? '');
        $normalized = normalizeCityName($value);
        if ($normalized === '') {
            continue;
        }

        if ($normalized === $needle) {
            return $item + ['match_type' => 'exact', 'score' => 100];
        }

        if (str_contains($normalized, $needle) || str_contains($needle, $normalized)) {
            $score = 90 - abs(strlen($normalized) - strlen($needle));
            if ($best === null || $score > $best['score']) {
                $best = $item + ['match_type' => 'partial', 'score' => $score];
            }
            continue;
        }

        $distance = levenshtein($needle, $normalized);
        $maxLength = max(strlen($needle), strlen($normalized), 1);
        $score = (int) round(100 - min(100, ($distance / $maxLength) * 100));
        if ($score >= 70 && ($best === null || $score > $best['score'])) {
            $best = $item + ['match_type' => 'nearest', 'score' => $score];
        }
    }

    return $best;
}

function matchDealBranchEnumValue(string $branch, array $items): ?array
{
    $needle = normalizeCityName($branch);
    if ($needle === '') {
        return null;
    }

    foreach ($items as $item) {
        if (!isRoutableDealBranchEnumItem((array) $item)) {
            continue;
        }

        if (normalizeCityName((string) ($item['value'] ?? '')) === $needle) {
            return $item + ['match_type' => 'exact', 'score' => 100];
        }
    }

    return null;
}

function normalizeCityName(string $city): string
{
    $city = trim($city);
    if ($city === '') {
        return '';
    }

    $city = str_replace('ё', 'е', function_exists('mb_strtolower') ? mb_strtolower($city, 'UTF-8') : strtolower($city));
    $city = preg_replace('/\b(город|гор|г)\.?\b/u', ' ', $city);
    $city = preg_replace('/[^\p{L}\p{N}\s-]+/u', ' ', $city);
    $city = preg_replace('/[\s-]+/u', ' ', (string) $city);

    return trim((string) $city);
}

function normalizePhoneForDeal(string $phone): string
{
    $phone = trim($phone);
    if ($phone === '') {
        return '';
    }

    $digits = preg_replace('/\D+/', '', $phone);
    if ($digits === null || strlen($digits) < 10) {
        return '';
    }

    if (strlen($digits) === 11 && str_starts_with($digits, '8')) {
        $digits = '7' . substr($digits, 1);
    }

    if (strlen($digits) === 10) {
        $digits = '7' . $digits;
    }

    if (strlen($digits) !== 11 || !str_starts_with($digits, '7')) {
        return '';
    }

    return sprintf(
        '+7 (%s) %s-%s-%s',
        substr($digits, 1, 3),
        substr($digits, 4, 3),
        substr($digits, 7, 2),
        substr($digits, 9, 2)
    );
}

function normalizeClientFullName($fullName): string
{
    $fullName = trim((string) $fullName);
    if ($fullName === '') {
        return '';
    }

    $fullName = html_entity_decode(strip_tags($fullName), ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $fullName = preg_replace('/\s+/u', ' ', (string) $fullName);
    $fullName = trim((string) $fullName);

    $length = function_exists('mb_strlen') ? mb_strlen($fullName, 'UTF-8') : strlen($fullName);
    if ($fullName === '' || $length < 2) {
        return '';
    }

    return $fullName;
}

function normalizeCrmId($value): string
{
    $value = trim((string) $value);
    if ($value === '') {
        return '';
    }

    if (preg_match('/\d+/', $value, $matches)) {
        return $matches[0];
    }

    return '';
}
