<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);

mb_internal_encoding('UTF-8');
date_default_timezone_set('Asia/Tehran');

$config = require __DIR__ . '/config.php';

$config['runtime_dir'] = $config['runtime_dir'] ?? (__DIR__ . '/runtime');
$config['output_dir']  = $config['output_dir'] ?? (__DIR__ . '/exports');

ensureDir($config['runtime_dir']);
ensureDir($config['output_dir']);

$lockFile = $config['runtime_dir'] . '/polling.lock';
$lock = fopen($lockFile, 'c+');
if (!$lock) {
    exit('lock error');
}

if (!flock($lock, LOCK_EX | LOCK_NB)) {
    exit('already running');
}

try {
    $offsetFile = $config['runtime_dir'] . '/offset.txt';
    $offset = is_file($offsetFile) ? (int)trim((string)file_get_contents($offsetFile)) : 0;

    $updatesResp = baleApi($config, 'getUpdates', [
        'offset'  => $offset,
        'limit'   => 20,
        'timeout' => 0,
    ]);

    $updates = $updatesResp['result'] ?? [];

    if (!$updates) {
        logLine($config, 'no updates');
        echo 'no updates';
    } else {
        foreach ($updates as $update) {
            $updateId = (int)($update['update_id'] ?? 0);
            if ($updateId > 0) {
                file_put_contents($offsetFile, (string)($updateId + 1));
            }

            $message = $update['message'] ?? null;
            if (!is_array($message)) {
                continue;
            }

            $chatId   = (string)($message['chat']['id'] ?? '');
            $text     = trim((string)($message['text'] ?? ''));
            $document = $message['document'] ?? null;

            if ($chatId === '') {
                continue;
            }

            logLine($config, "UPDATE | chat_id={$chatId} | text={$text}");

            $allowed = array_map(static fn($x) => trim((string)$x), $config['allowed_chat_ids'] ?? []);
            $allowed = array_values(array_filter($allowed, static fn($x) => $x !== ''));

            if (!empty($allowed) && !in_array($chatId, $allowed, true)) {
                baleSendMessage($config, $chatId, 'شما مجاز به استفاده از این ربات نیستید.');
                continue;
            }

            if (is_array($document)) {
                handleIncomingDocument($config, $chatId, $document);
                continue;
            }
            
            if ($text === '/start' || $text === '/menu' || $text === 'منو') {
                sendMainMenu($config, $chatId, "سلام\nیکی از گزینه‌های زیر را انتخاب کن:");
                continue;
            }
            
            if ($text === 'استخراج همه فایل‌ها') {
                handleExportAllAction($config, $chatId);
                continue;
            }
            
            // if ($text === 'ایجاد تغییرات در سایت اهواز فون') {
            //     handleAhvazPhoneChangeTemplateRequest($config, $chatId);
            //     continue;
            // }
            
            if ($text === 'تنظیم مپ قیمت هوشمند') {
                handleSmartMapTemplateRequest($config, $chatId);
                continue;
            }
            
            if ($text === 'بررسی قیمت هوشمند') {
                handleSmartReviewRequest($config, $chatId);
                continue;
            }
            
            if ($text === 'کالاهای مپ نشده') {
                handleUnmappedProductsAction($config, $chatId);
                continue;
            }
            
            if ($text === 'تولید لیست متنی') {
                handleGenerateTextListAction($config, $chatId);
                continue;
            }
            
            if ($text === 'تایید' || $text === 'همین تاییده') {
                applyStoredSmartReviewApproval($config, $chatId);
                continue;
            }
            
            sendMainMenu($config, $chatId, 'دستور نامعتبر است. یکی از دکمه‌ها را بزن.');
        }

        echo 'done';
    }
} catch (Throwable $e) {
    logLine($config, 'ERROR: ' . $e->getMessage());
    echo 'error: ' . $e->getMessage();
} finally {
    flock($lock, LOCK_UN);
    fclose($lock);
}
function handleUnmappedProductsAction(array $config, string $chatId): void
{
    handleExporterAction(
        $config,
        $chatId,
        'در حال بررسی کالاهای مپ نشده...',
        fn() => runUnmappedProductsExporter($config),
        '✅ فایل کالاهای مپ نشده ارسال شد.'
    );
}
function ahvazPhoneTokenCacheFile(array $config): string
{
    return $config['runtime_dir'] . '/ahvazphone_token.json';
}

function loadCachedAhvazPhoneToken(array $config): array
{
    $file = ahvazPhoneTokenCacheFile($config);
    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function saveCachedAhvazPhoneToken(array $config, string $token): void
{
    $payload = [
        'token'      => $token,
        'created_at' => time(),
    ];

    file_put_contents(
        ahvazPhoneTokenCacheFile($config),
        json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );
}

function extractAhvazPhoneTokenFromLoginResponse(array $json): string
{
    $candidates = [
        $json['token'] ?? null,
        $json['access_token'] ?? null,
        $json['data']['token'] ?? null,
        $json['data']['access_token'] ?? null,
        $json['result']['token'] ?? null,
        $json['result']['access_token'] ?? null,
        $json['session']['token'] ?? null,
        $json['session']['access_token'] ?? null,
    ];

    foreach ($candidates as $candidate) {
        if (is_string($candidate) && trim($candidate) !== '') {
            return trim($candidate);
        }
    }

    return '';
}

function refreshAhvazPhoneToken(array $config): string
{
    $c = $config['ahvazphone'] ?? [];

    $authUrl  = trim((string)($c['auth_url'] ?? ''));
    $username = trim((string)($c['username'] ?? ''));
    $password = (string)($c['password'] ?? '');

    if ($authUrl === '') {
        throw new RuntimeException('ahvazphone.auth_url تنظیم نشده است.');
    }

    if ($username === '' || $password === '') {
        throw new RuntimeException('نام کاربری یا رمز عبور اهوازفون تنظیم نشده است.');
    }

    $payload = json_encode([
        'username' => $username,
        'password' => $password,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if ($payload === false) {
        throw new RuntimeException('ساخت JSON لاگین اهوازفون شکست خورد.');
    }

    $ch = curl_init($authUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Accept: application/json, text/plain, */*',
        ],
        CURLOPT_TIMEOUT        => 60,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException('خطای cURL در لاگین اهوازفون: ' . $error);
    }

    if ($httpCode < 200 || $httpCode >= 300) {
        throw new RuntimeException('لاگین اهوازفون خطا داد. HTTP=' . $httpCode . ' | ' . mb_substr((string)$response, 0, 500));
    }

    $json = json_decode((string)$response, true);
    if (!is_array($json)) {
        throw new RuntimeException('پاسخ لاگین اهوازفون معتبر نیست.');
    }

    $token = extractAhvazPhoneTokenFromLoginResponse($json);
    if ($token === '') {
        throw new RuntimeException('توکن از پاسخ لاگین اهوازفون پیدا نشد. پاسخ: ' . mb_substr((string)$response, 0, 500));
    }

    saveCachedAhvazPhoneToken($config, $token);
    logLine($config, 'AHVAZ TOKEN REFRESHED');

    return $token;
}

function getAhvazPhoneToken(array $config, bool $forceRefresh = false): string
{
    $c = $config['ahvazphone'] ?? [];
    $refreshSeconds = (int)($c['token_refresh_seconds'] ?? 172800);

    if ($refreshSeconds <= 0) {
        $refreshSeconds = 172800;
    }

    if (!$forceRefresh) {
        $cached = loadCachedAhvazPhoneToken($config);
        $token = trim((string)($cached['token'] ?? ''));
        $createdAt = (int)($cached['created_at'] ?? 0);

        if ($token !== '' && $createdAt > 0 && (time() - $createdAt) < $refreshSeconds) {
            return $token;
        }
    }

    return refreshAhvazPhoneToken($config);
}

function apiGetJsonAhvaz(array $config, string $url): array
{
    $token = getAhvazPhoneToken($config, false);
    [$httpCode, $response, $error] = apiGetJson($url, $token);

    if ($httpCode === 401 || $httpCode === 403) {
        $token = getAhvazPhoneToken($config, true);
        [$httpCode, $response, $error] = apiGetJson($url, $token);
    }

    return [$httpCode, $response, $error];
}

function apiPutJsonAhvaz(array $config, string $url, array $payload): array
{
    $token = getAhvazPhoneToken($config, false);
    [$httpCode, $response, $error] = apiPutJson($url, $token, $payload);

    if ($httpCode === 401 || $httpCode === 403) {
        $token = getAhvazPhoneToken($config, true);
        [$httpCode, $response, $error] = apiPutJson($url, $token, $payload);
    }

    return [$httpCode, $response, $error];
}
function safeCollectFarnaaRawRows(array $config): array
{
    try {
        return collectFarnaaRawRows($config);
    } catch (Throwable $e) {
        logLine($config, 'FARNAA OPTIONAL ERROR: ' . $e->getMessage());
        return [];
    }
}
function normalizeText(string $text): string
{
    return normalizeSpaces($text);
}
function buildPagedUrl(string $url, int $page): string
{
    $parts = parse_url($url);
    $query = [];

    if (!empty($parts['query'])) {
        parse_str($parts['query'], $query);
    }

    $query['page'] = $page;
    $parts['query'] = http_build_query($query);

    return unparseUrl($parts);
}
function makeAbsoluteUrl(string $href, string $baseUrl): string
{
    return absoluteUrl($href, $baseUrl);
}
function unparseUrl(array $parts): string
{
    $scheme   = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
    $host     = $parts['host'] ?? '';
    $port     = isset($parts['port']) ? ':' . $parts['port'] : '';
    $user     = $parts['user'] ?? '';
    $pass     = isset($parts['pass']) ? ':' . $parts['pass'] : '';
    $pass     = ($user || $pass) ? "$pass@" : '';
    $path     = $parts['path'] ?? '';
    $query    = isset($parts['query']) && $parts['query'] !== '' ? '?' . $parts['query'] : '';
    $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';

    return $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
}
function safeCollectHamrahtelRawRows(array $config): array
{
    try {
        return collectHamrahtelRawRows($config);
    } catch (Throwable $e) {
        logLine($config, 'HAMRAHTEL OPTIONAL ERROR: ' . $e->getMessage());
        return [];
    }
}
function runUnmappedProductsExporter(array $config): string
{
    $map = loadSmartPriceMap($config);

    if (!$map) {
        throw new RuntimeException('فایل مپ قیمت هوشمند پیدا نشد. اول «تنظیم مپ قیمت هوشمند» را کامل کن.');
    }

    $rows = buildUnmappedProductsRows($config, $map);

    $file = $config['output_dir'] . '/unmapped_products_' . date('Y-m-d_H-i-s') . '.xls';

    writeExcelLikeXls(
        $rows,
        $file,
        ['اسم', 'کد کالا', 'نام سایت', 'لینک'],
        'کالاهای مپ نشده'
    );

    return $file;
}
function buildUnmappedProductsRows(array $config, array $map): array
{
    $mappedSets = extractMappedSourceKeySets($map);

    $rows = [];
    $seen = [];

    // حسابرسی
    $hesabanRows = fetchHesabanwebRows($config);
    foreach ($hesabanRows as $row) {
        $code = trim((string)($row['code'] ?? ''));
        $name = trim((string)($row['name'] ?? ''));

        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($code),
            normalizeKeyText($name),
        ], fn($x) => $x !== '')));

        if (!$candidates) {
            continue;
        }

        if (sourceCandidatesMapped($candidates, $mappedSets['hesabanweb'])) {
            continue;
        }

        if ($name === '') {
            $name = $code !== '' ? $code : '-';
        }

        $itemCode = $code !== '' ? $code : '-';

        $key = 'hesabanweb|' . normalizeKeyText($name) . '|' . normalizeKeyText($itemCode);
        if (isset($seen[$key])) {
            continue;
        }
        $seen[$key] = true;

        $rows[] = [
            'اسم'      => $name,
            'کد کالا'  => $itemCode,
            'نام سایت' => 'حسابرسی',
            'لینک'     => '-',
        ];
    }
    // فرنا
    $farnaaRows = safeCollectFarnaaRawRows($config);
    foreach ($farnaaRows as $row) {
        $title = normalizeSpaces((string)($row['title'] ?? ''));
        $color = normalizeSpaces((string)($row['color'] ?? ''));
        $link  = trim((string)($row['url'] ?? ''));

        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($title),
            normalizeKeyText($title . ' | ' . $color),
        ], fn($x) => $x !== '')));

        if (!$candidates) {
            continue;
        }

        if (sourceCandidatesMapped($candidates, $mappedSets['farnaa'])) {
            continue;
        }

        $displayName = $title !== '' ? $title : '-';

        $itemCode = extractProductCodeFromUrl($link);
        if ($itemCode === '') {
            $itemCode = '-';
        }

        if ($link === '') {
            $link = '-';
        }

        $key = 'farnaa|' . normalizeKeyText($displayName) . '|' . normalizeKeyText($itemCode) . '|' . $link;
        if (isset($seen[$key])) {
            continue;
        }
        $seen[$key] = true;

        $rows[] = [
            'اسم'      => $displayName,
            'کد کالا'  => $itemCode,
            'نام سایت' => 'فرنا',
            'لینک'     => $link,
        ];
    }
    // همراه تل
    $hamrahtelRows = safeCollectHamrahtelRawRows($config);
    foreach ($hamrahtelRows as $row) {
        $title = normalizeSpaces((string)($row['title'] ?? ''));
        $slug  = normalizeSpaces((string)($row['slug'] ?? ''));
        $link  = trim((string)($row['url'] ?? ''));

        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($title),
            normalizeKeyText($slug),
        ], fn($x) => $x !== '')));

        if (!$candidates) {
            continue;
        }

        if (sourceCandidatesMapped($candidates, $mappedSets['hamrahtel'])) {
            continue;
        }

        $itemCode = $slug !== '' ? $slug : '-';

        if ($link === '') {
            $link = '-';
        }

        $key = 'hamrahtel|' . normalizeKeyText($title) . '|' . normalizeKeyText($itemCode) . '|' . $link;
        if (isset($seen[$key])) {
            continue;
        }
        $seen[$key] = true;

        $rows[] = [
            'اسم'      => $title !== '' ? $title : '-',
            'کد کالا'  => $itemCode,
            'نام سایت' => 'همراه تل',
            'لینک'     => $link,
        ];
    }
    // دیجی‌کالا
    $digikalaRows = safeCollectDigikalaRawRows($config);
    foreach ($digikalaRows as $row) {
        $title = normalizeSpaces((string)($row['title'] ?? ''));
        $slug  = normalizeSpaces((string)($row['slug'] ?? ''));
        $link  = trim((string)($row['url'] ?? ''));
    
        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($title),
            normalizeKeyText($slug),
        ], fn($x) => $x !== '')));
    
        if (!$candidates) continue;
        if (sourceCandidatesMapped($candidates, $mappedSets['digikala'])) continue;
    
        $itemCode = $slug !== '' ? $slug : extractProductCodeFromUrl($link);
        if ($itemCode === '') $itemCode = '-';
        if ($link === '') $link = '-';
    
        $key = 'digikala|' . normalizeKeyText($title) . '|' . normalizeKeyText($itemCode) . '|' . $link;
        if (isset($seen[$key])) continue;
        $seen[$key] = true;
    
        $rows[] = [
            'اسم'      => $title !== '' ? $title : '-',
            'کد کالا'  => $itemCode,
            'نام سایت' => 'دیجی‌کالا',
            'لینک'     => $link,
        ];
    }
    
    // mobile140
    $mobile140Rows = safeCollectMobile140RawRows($config);
    foreach ($mobile140Rows as $row) {
        $title = normalizeSpaces((string)($row['title'] ?? ''));
        $slug  = normalizeSpaces((string)($row['slug'] ?? ''));
        $link  = trim((string)($row['url'] ?? ''));
    
        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($title),
            normalizeKeyText($slug),
        ], fn($x) => $x !== '')));
    
        if (!$candidates) continue;
        if (sourceCandidatesMapped($candidates, $mappedSets['mobile140'])) continue;
    
        $itemCode = $slug !== '' ? $slug : extractProductCodeFromUrl($link);
        if ($itemCode === '') $itemCode = '-';
        if ($link === '') $link = '-';
    
        $key = 'mobile140|' . normalizeKeyText($title) . '|' . normalizeKeyText($itemCode) . '|' . $link;
        if (isset($seen[$key])) continue;
        $seen[$key] = true;
    
        $rows[] = [
            'اسم'      => $title !== '' ? $title : '-',
            'کد کالا'  => $itemCode,
            'نام سایت' => 'mobile140',
            'لینک'     => $link,
        ];
    }
    
    // تکنولایف
    $technolifeRows = safeCollectTechnolifeRawRows($config);
    foreach ($technolifeRows as $row) {
        $title = normalizeSpaces((string)($row['title'] ?? ''));
        $slug  = normalizeSpaces((string)($row['slug'] ?? ''));
        $link  = trim((string)($row['url'] ?? ''));
    
        $candidates = array_values(array_unique(array_filter([
            normalizeKeyText($title),
            normalizeKeyText($slug),
        ], fn($x) => $x !== '')));
    
        if (!$candidates) continue;
        if (sourceCandidatesMapped($candidates, $mappedSets['technolife'])) continue;
    
        $itemCode = $slug !== '' ? $slug : extractProductCodeFromUrl($link);
        if ($itemCode === '') $itemCode = '-';
        if ($link === '') $link = '-';
    
        $key = 'technolife|' . normalizeKeyText($title) . '|' . normalizeKeyText($itemCode) . '|' . $link;
        if (isset($seen[$key])) continue;
        $seen[$key] = true;
    
        $rows[] = [
            'اسم'      => $title !== '' ? $title : '-',
            'کد کالا'  => $itemCode,
            'نام سایت' => 'تکنولایف',
            'لینک'     => $link,
        ];
    }
    usort($rows, function ($a, $b) {
        $siteCmp = strcmp(
            normalizeKeyText((string)($a['نام سایت'] ?? '')),
            normalizeKeyText((string)($b['نام سایت'] ?? ''))
        );

        if ($siteCmp !== 0) {
            return $siteCmp;
        }

        return strcmp(
            normalizeKeyText((string)($a['اسم'] ?? '')),
            normalizeKeyText((string)($b['اسم'] ?? ''))
        );
    });

    return $rows;
}
function roundTextListDisplayPriceUpTo500(int $price): int
{
    if ($price <= 0) {
        return 0;
    }

    // قیمت نمایشی در لیست متنی بر اساس هزار تومان نمایش داده می‌شود
    $displayValue = (int)ceil($price / 1000);

    // گرد به بالای 500
    return (int)(ceil($displayValue / 500) * 500);
}

function isBlackOrWhiteColor(string $colorText): bool
{
    $base = detectBaseColorKey($colorText);
    return in_array($base, ['مشکی', 'سفید'], true);
}

function buildInStockMappedVariantCountsByTitle(array $ahvazRows, array $map, array $hesabanIndex): array
{
    $counts = [];

    foreach ($ahvazRows as $ahvazRow) {
        $productId = (int)($ahvazRow['product_id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }

        $variant = trim((string)($ahvazRow['color'] ?? '-'));
        if ($variant === '') {
            $variant = '-';
        }

        $mapKey = buildAhvazMapRowKey($productId, $variant);
        $mapRow = $map[$mapKey] ?? null;
        if (!$mapRow) {
            continue;
        }

        $hesabanKey = normalizeKeyText((string)($mapRow['hesabanweb_key'] ?? ''));
        if ($hesabanKey === '') {
            continue;
        }

        $hesabanRow = $hesabanIndex[$hesabanKey] ?? null;
        if (!$hesabanRow) {
            continue;
        }

        $buyCount = (int)($hesabanRow['count'] ?? 0);
        if ($buyCount <= 0) {
            continue;
        }

        $titleKey = normalizeKeyText((string)($ahvazRow['title'] ?? ''));
        if ($titleKey === '') {
            continue;
        }

        $variantKey = normalizeVariantMatchKey($variant);
        $counts[$titleKey][$variantKey] = true;
    }

    return $counts;
}
function extractProductCodeFromUrl(string $url): string
{
    $url = trim($url);
    if ($url === '') {
        return '';
    }

    $path = parse_url($url, PHP_URL_PATH);
    if (!is_string($path) || trim($path) === '') {
        return '';
    }

    $path = trim($path, '/');
    if ($path === '') {
        return '';
    }

    $parts = explode('/', $path);
    $last = trim((string)end($parts));

    return $last !== '' ? $last : '';
}
function extractMappedSourceKeySets(array $map): array
{
    $sets = [
        'hesabanweb' => [],
        'farnaa'     => [],
        'hamrahtel'  => [],
        'digikala'   => [],
        'mobile140'  => [],
        'technolife' => [],
    ];

    foreach ($map as $row) {
        $hesabanKey   = normalizeKeyText((string)($row['hesabanweb_key'] ?? ''));
        $farnaaKey    = normalizeKeyText((string)($row['farnaa_key'] ?? ''));
        $hamrahtelKey = normalizeKeyText((string)($row['hamrahtel_key'] ?? ''));
        $digikalaKey  = normalizeKeyText((string)($row['digikala_key'] ?? ''));
        $mobile140Key = normalizeKeyText((string)($row['mobile140_key'] ?? ''));
        $technolifeKey= normalizeKeyText((string)($row['technolife_key'] ?? ''));

        if ($hesabanKey !== '')   $sets['hesabanweb'][$hesabanKey] = true;
        if ($farnaaKey !== '')    $sets['farnaa'][$farnaaKey] = true;
        if ($hamrahtelKey !== '') $sets['hamrahtel'][$hamrahtelKey] = true;
        if ($digikalaKey !== '')  $sets['digikala'][$digikalaKey] = true;
        if ($mobile140Key !== '') $sets['mobile140'][$mobile140Key] = true;
        if ($technolifeKey !== '')$sets['technolife'][$technolifeKey] = true;
    }

    return $sets;
}

function sourceCandidatesMapped(array $candidates, array $mappedSet): bool
{
    foreach ($candidates as $candidate) {
        if ($candidate !== '' && isset($mappedSet[$candidate])) {
            return true;
        }
    }

    return false;
}
function sendMainMenu(array $config, string $chatId, string $text): void
{
    $replyMarkup = [
        'keyboard' => [
            // [['text' => 'ایجاد تغییرات در سایت اهواز فون']]
            [['text' => 'کالاهای مپ نشده']],
            [['text' => 'تنظیم مپ قیمت هوشمند']],
            [['text' => 'بررسی قیمت هوشمند']],
            [['text' => 'تولید لیست متنی']],
            [['text' => 'استخراج همه فایل‌ها']],
            [['text' => 'تایید']],
        ],
        'resize_keyboard'   => true,
        'one_time_keyboard' => false,
    ];

    baleSendMessage($config, $chatId, $text, $replyMarkup);
}
function handleExporterAction(
    array $config,
    string $chatId,
    string $progressText,
    callable $runner,
    string $successText
): void {
    try {
        baleSendMessage($config, $chatId, $progressText);

        $filePath = $runner();

        if (!is_file($filePath)) {
            throw new RuntimeException('فایل خروجی ساخته نشد.');
        }

        baleSendDocument($config, $chatId, $filePath, basename($filePath));
        sendMainMenu($config, $chatId, $successText);
    } catch (Throwable $e) {
        logLine($config, 'ACTION ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا: ' . $e->getMessage());
    }
}

function handleIncomingDocument(array $config, string $chatId, array $document): void
{
    $state = getChatWorkflowState($config, $chatId);

    try {
        if ($state === 'awaiting_ahvaz_change_file') {
            handleIncomingAhvazPhoneChangeFile($config, $chatId, $document);
            return;
        }

        if ($state === 'awaiting_smart_map_file') {
            handleIncomingSmartMapFile($config, $chatId, $document);
            return;
        }

        if ($state === 'awaiting_smart_review_file') {
            handleIncomingSmartReviewFile($config, $chatId, $document);
            return;
        }

        sendMainMenu($config, $chatId, 'فعلاً منتظر فایلی از شما نیستم. اول یکی از دکمه‌های مربوطه را بزن.');
    } catch (Throwable $e) {
        logLine($config, 'DOC ROUTER ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در پردازش فایل: ' . $e->getMessage());
    }
}
function finalSmartAppliedRowsFile(array $config): string
{
    return $config['runtime_dir'] . '/final_smart_applied_rows.json';
}

function saveFinalSmartAppliedRows(array $config, array $rows): void
{
    $normalized = normalizeFinalSmartRowsForState($rows);

    file_put_contents(
        finalSmartAppliedRowsFile($config),
        json_encode($normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );
}

function loadFinalSmartAppliedRows(array $config): array
{
    $file = finalSmartAppliedRowsFile($config);

    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function normalizeFinalSmartRowsForState(array $rows): array
{
    $out = [];

    foreach ($rows as $row) {
        $productId = (int)($row['product_id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }

        $variant = trim((string)($row['variant'] ?? $row['color'] ?? '-'));
        if ($variant === '') {
            $variant = '-';
        }

        $proposedPrice = (int)($row['proposed_price'] ?? 0);
        $proposedStock = (int)($row['proposed_stock'] ?? 0);

        $finalPriceForText = $proposedPrice;
        $finalStockForText = $proposedStock;

        if (array_key_exists('final_price_to_apply', $row) && $row['final_price_to_apply'] !== null) {
            $finalPriceForText = max(0, (int)$row['final_price_to_apply']);
        } elseif (csvCellHasUserValue($row['proposed_price_raw'] ?? '')) {
            $finalPriceForText = max(0, (int)parsePriceValue((string)($row['proposed_price_raw'] ?? '0')));
        }

        if (array_key_exists('final_stock_to_apply', $row) && $row['final_stock_to_apply'] !== null) {
            $finalStockForText = max(0, (int)$row['final_stock_to_apply']);
        } elseif (csvCellHasUserValue($row['proposed_stock_raw'] ?? '')) {
            $finalStockForText = max(0, (int)parsePriceValue((string)($row['proposed_stock_raw'] ?? '0')));
        }

        $out[] = [
            'product_id'           => $productId,
            'title'                => trim((string)($row['title'] ?? '')),
            'variant'              => $variant,
            'hesabanweb_key'       => trim((string)($row['hesabanweb_key'] ?? '')),
            'farnaa_key'           => trim((string)($row['farnaa_key'] ?? '')),
            'hamrahtel_key'        => trim((string)($row['hamrahtel_key'] ?? '')),
            'status'               => trim((string)($row['status'] ?? '')),
            'current_price'        => (int)($row['current_price'] ?? 0),
            'buy_price'            => (int)($row['buy_price'] ?? 0),
            'buy_count'            => (int)($row['buy_count'] ?? 0),
            'market_mean'          => (int)($row['market_mean'] ?? 0),
            'proposed_price'       => $proposedPrice,
            'proposed_stock'       => $proposedStock,
            'final_price_for_text' => $finalPriceForText,
            'final_stock_for_text' => $finalStockForText,
        ];
    }

    return $out;
}

function stripPersianWordsFromModel(string $text): string
{
    $text = normalizeSpaces($text);

    // حذف هر چیزی که فارسی است
    $text = preg_replace('/[\x{0600}-\x{06FF}]+/u', ' ', $text);

    // نگه داشتن فقط حروف و اعداد لاتین و چند کاراکتر لازم
    $text = preg_replace('/[^A-Za-z0-9\-\+\.\s]/u', ' ', $text);

    $text = preg_replace('/\s+/u', ' ', trim($text));

    return $text;
}
function runHesabanwebExporter(array $config): string
{
    $rows = fetchHesabanwebRows($config);

    if (!$rows) {
        throw new RuntimeException('هیچ ردیفی برای حسابرسی پیدا نشد.');
    }

    $out = [];
    foreach ($rows as $row) {
        $out[] = [
            'کد کالا'   => (string)($row['code'] ?? ''),
            'نام کالا'  => (string)($row['name'] ?? ''),
            'قیمت واحد' => normalizePriceForSheet((string)($row['buy_price'] ?? 0)),
            'تعداد'     => (string)($row['count'] ?? 0),
        ];
    }

    $filename = $config['output_dir'] . '/hesabanweb_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($out, $filename, ['کد کالا', 'نام کالا', 'قیمت واحد', 'تعداد'], 'خروجی حسابرسی');

    return $filename;
}

function fetchHesabanwebRows(array $config): array
{
    $c = $config['hesabanweb'] ?? [];

    $token       = trim((string)($c['token'] ?? ''));
    $baseUrl     = rtrim((string)($c['base_url'] ?? 'https://app.hesabanweb.com'), '/');
    $onlyStock   = (string)($c['only_stock'] ?? 'false');
    $perPage     = (int)($c['per_page'] ?? 1000);
    $page        = (int)($c['page'] ?? 1);
    $targetGroup = trim((string)($c['target_group'] ?? 'موبایل'));

    if ($token === '' || str_contains($token, 'PUT_')) {
        throw new RuntimeException('توکن حسابان‌وب تنظیم نشده است.');
    }

    $url = "{$baseUrl}/Product/PRODUCTS/{$onlyStock}/{$perPage}/{$page}";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'accept: text/plain',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException('Curl error: ' . $error);
    }

    $data = json_decode((string)$response, true);
    if ($httpCode !== 200 || !is_array($data)) {
        throw new RuntimeException('API حسابان‌وب خطا داد. HTTP=' . $httpCode);
    }

    $items = [];
    if (isset($data['items']) && is_array($data['items'])) {
        $items = $data['items'];
    } elseif (array_is_list($data)) {
        $items = $data;
    }

    if (!$items) {
        throw new RuntimeException('هیچ آیتمی از حسابان‌وب دریافت نشد.');
    }

    $rows = [];
    foreach ($items as $item) {
        $groupName = $item['group']['name'] ?? ($item['group'] ?? '');

        if ($targetGroup !== '' && $groupName !== $targetGroup) {
            continue;
        }

        $rows[] = [
            'code'      => (string)($item['code'] ?? ''),
            'name'      => (string)($item['name'] ?? ''),
            'buy_price' => rialToTomanInt($item['oneAmount'] ?? 0),
            'count'     => (int)($item['count'] ?? 0),
        ];
    }

    if (!$rows) {
        throw new RuntimeException('هیچ ردیفی برای گروه ' . $targetGroup . ' پیدا نشد.');
    }

    return $rows;
}

function runAhvazPhoneExporter(array $config): string
{
    $rawRows = fetchAhvazPhoneRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای اهوازفون هیچ ردیفی ساخته نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'شناسه محصول' => (string)($row['product_id'] ?? ''),
            'عنوان'       => (string)($row['title'] ?? ''),
            'واریانت'     => (string)($row['color'] ?? ''),
            'قیمت'        => normalizePriceForSheet((string)($row['price'] ?? '')),
            'موجودی'      => (string)($row['stock'] ?? ''),
        ];
    }

    $outFile = $config['output_dir'] . '/ahvazphone_products_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $outFile, ['شناسه محصول', 'عنوان', 'واریانت', 'قیمت', 'موجودی'], 'خروجی اهوازفون');

    return $outFile;
}
function fetchAhvazPhoneRows(array $config): array
{
    $c = $config['ahvazphone'] ?? [];

    $baseUrl    = rtrim((string)($c['base_url'] ?? 'https://ahvazphone.ir'), '/');
    $size       = (int)($c['page_size'] ?? 100);
    $delayMicro = (int)($c['delay_microseconds'] ?? 150000);

    $products = [];
    $page = 1;

    while (true) {
        $url = $baseUrl . '/site/api/v1/manage/store/products?page=' . $page . '&size=' . $size;
        [$httpCode, $response, $error] = apiGetJsonAhvaz($config, $url);

        if ($error) {
            throw new RuntimeException('خطا در دریافت لیست محصولات اهوازفون: ' . $error);
        }

        if ($httpCode !== 200) {
            throw new RuntimeException('HTTP ' . $httpCode . ' در لیست محصولات اهوازفون');
        }

        $data = json_decode((string)$response, true);
        if (!is_array($data)) {
            throw new RuntimeException('پاسخ لیست محصولات اهوازفون JSON معتبر نیست.');
        }

        $items = $data['data'] ?? $data['items'] ?? $data['products'] ?? [];
        if (!is_array($items) || empty($items)) {
            break;
        }

        foreach ($items as $item) {
            $products[] = $item;
        }

        $lastPage = $data['meta']['last_page']
            ?? $data['pagination']['last_page']
            ?? $data['last_page']
            ?? $page;

        if ($page >= (int)$lastPage) {
            break;
        }

        $page++;
    }

    if (!$products) {
        throw new RuntimeException('هیچ محصولی از اهوازفون دریافت نشد.');
    }

    $rows = [];
    $seen = [];

    foreach ($products as $item) {
        $productId = (int)($item['id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }

        $url = $baseUrl . '/site/api/v1/manage/store/products/' . $productId;
        [$httpCode, $response, $error] = apiGetJsonAhvaz($config, $url);

        if ($error || $httpCode !== 200) {
            usleep($delayMicro);
            continue;
        }

        $data = json_decode((string)$response, true);
        $detail = $data['product'] ?? null;

        if (!is_array($detail)) {
            usleep($delayMicro);
            continue;
        }

        $title = trim((string)($detail['title'] ?? '-'));

        if (!empty($detail['variants']) && is_array($detail['variants'])) {
            foreach ($detail['variants'] as $variant) {
                $variantTitle = (string)($variant['title'] ?? '');
                $color        = extractColorFromVariantTitle($variantTitle);
                $price        = (int)($variant['price'] ?? 0);
                $stock        = normalizeStock($variant['stock'] ?? '');

                $key = $productId . '|' . $title . '|' . $color . '|' . $price . '|' . $stock;
                if (isset($seen[$key])) {
                    continue;
                }
                $seen[$key] = true;

                $rows[] = [
                    'product_id' => $productId,
                    'title'      => $title,
                    'color'      => $color,
                    'price'      => $price,
                    'stock'      => $stock,
                ];
            }
        } else {
            $basePrice = (int)($detail['price'] ?? 0);
            $baseStock = normalizeStock($detail['stock'] ?? '');

            $key = $productId . '|' . $title . '|-|' . $basePrice . '|' . $baseStock;
            if (!isset($seen[$key])) {
                $seen[$key] = true;

                $rows[] = [
                    'product_id' => $productId,
                    'title'      => $title,
                    'color'      => '-',
                    'price'      => $basePrice,
                    'stock'      => $baseStock,
                ];
            }
        }

        usleep($delayMicro);
    }

    return $rows;
}
function runFarnaaExporter(array $config): string
{
    $rawRows = collectFarnaaRawRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای فرنا هیچ ردیفی تولید نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'نام محصول' => $row['title'],
            'قیمت'      => normalizePriceForSheet((string)$row['price']),
            'رنگ'       => $row['color'],
            'دسته'      => $row['category'],
            'لینک'      => $row['url'],
            'وضعیت'     => $row['status'],
        ];
    }

    usort($rows, function ($a, $b) {
        return comparePricesDesc((string)($a['قیمت'] ?? ''), (string)($b['قیمت'] ?? ''));
    });

    $rows = array_values(array_filter($rows, function ($row) {
        return rowHasPrice($row);
    }));

    if (!$rows) {
        throw new RuntimeException('بعد از حذف ردیف‌های بدون قیمت، خروجی فرنا خالی شد.');
    }

    $file = $config['output_dir'] . '/farnaa_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $file, ['نام محصول', 'قیمت', 'رنگ', 'دسته', 'لینک', 'وضعیت'], 'خروجی فرنا');

    return $file;
}

function collectFarnaaRawRows(array $config): array
{
    $fc = $config['farnaa'] ?? [];
    $urls = $fc['category_urls'] ?? [];

    if (!is_array($urls) || !$urls) {
        throw new RuntimeException('آدرس دسته‌های فرنا تنظیم نشده است.');
    }

    $allItems = [];

    foreach ($urls as $categoryUrl) {
        $categoryUrl = trim((string)$categoryUrl);
        if ($categoryUrl === '') {
            continue;
        }

        $categoryHtml = fetchUrl($categoryUrl, $config);
        $items = parseCategoryProducts($categoryHtml, $categoryUrl);

        foreach ($items as $item) {
            $item['دسته'] = guessCategoryNameFromUrl($categoryUrl);
            $allItems[] = $item;
        }
    }

    $allItems = uniqueByUrl($allItems);

    if (!$allItems) {
        throw new RuntimeException('هیچ محصولی از دسته‌های فرنا استخراج نشد.');
    }

    $rows = [];
    $seen = [];

    foreach ($allItems as $item) {
        $url = $item['url'] ?? '';
        if ($url === '' || isset($seen[$url])) {
            continue;
        }
        $seen[$url] = true;

        try {
            $productHtml = fetchUrl($url, $config);
            $productRows = parseProductPage($productHtml, $url, $item, $config);

            foreach ($productRows as $row) {
                $rows[] = [
                    'title'    => normalizeSpaces((string)($row['نام محصول'] ?? '')),
                    'price'    => (int)parsePriceValue((string)($row['قیمت'] ?? '')),
                    'color'    => normalizeSpaces((string)($row['رنگ'] ?? '')),
                    'category' => normalizeSpaces((string)($row['دسته'] ?? '')),
                    'url'      => (string)($row['لینک'] ?? ''),
                    'status'   => (string)($row['وضعیت'] ?? ''),
                ];
            }
        } catch (Throwable $e) {
            $rows[] = [
                'title'    => $item['name'] ?: 'نامشخص',
                'price'    => (int)parsePriceValue((string)($item['price'] ?? '')),
                'color'    => implode(' | ', $item['colors'] ?? []),
                'category' => (string)($item['دسته'] ?? ''),
                'url'      => $url,
                'status'   => 'خطا در خواندن صفحه محصول',
            ];
        }
    }

    return array_values(array_filter($rows, fn($row) => $row['price'] > 0));
}

function runHamrahtelExporter(array $config): string
{
    $rawRows = collectHamrahtelRawRows($config);

    if (!$rawRows) {
        throw new RuntimeException('هیچ محصول موجودی‌دار با قیمت معتبر از همراه‌تل استخراج نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'نام محصول' => $row['title'],
            'قیمت'      => normalizePriceForSheet((string)$row['price']),
            'رنگ'       => '',
            'دسته'      => 'hamrahtel',
            'لینک'      => $row['url'],
            'وضعیت'     => $row['qty'] !== null ? ('موجود | تعداد: ' . $row['qty']) : 'موجود',
        ];
    }

    usort($rows, function ($a, $b) {
        return comparePricesDesc((string)($a['قیمت'] ?? ''), (string)($b['قیمت'] ?? ''));
    });

    $file = $config['output_dir'] . '/hamrahtel_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $file, ['نام محصول', 'قیمت', 'رنگ', 'دسته', 'لینک', 'وضعیت'], 'خروجی همراه‌تل');

    return $file;
}

function collectHamrahtelRawRows(array $config): array
{
    $hc = $config['hamrahtel'] ?? [];

    $baseUrl = rtrim((string)($hc['base_url'] ?? 'https://hamrahtel.com'), '/');
    $graphqlEndpoint = trim((string)($hc['graphql_url'] ?? 'https://core-api.hamrahtel.com/graphql/'));
    $mobileCategoryId = trim((string)($hc['category_id'] ?? 'Q2F0ZWdvcnk6Mg=='));
    $pageSize = (int)($hc['page_size'] ?? 100);

    if ($pageSize <= 0) {
        $pageSize = 100;
    }

    $categoryUrls = $hc['category_urls'] ?? [];
    if (!is_array($categoryUrls) || !$categoryUrls) {
        throw new RuntimeException('آدرس دسته‌های همراه تل تنظیم نشده است.');
    }

    $categoryUrl = trim((string)$categoryUrls[0]);
    if ($categoryUrl === '') {
        throw new RuntimeException('لینک دسته همراه تل خالی است.');
    }

    $brands = parseHamrahtelBrandValuesFromUrlForButton($categoryUrl);

    $filter = [
        'categories' => [$mobileCategoryId],
        'attributes' => [
            [
                'slug'   => 'brand',
                'values' => $brands,
            ],
        ],
    ];

    $rows = [];
    $seen = [];
    $after = null;

    $query = <<<'GQL'
query CategoryBrandProductsStock($first: Int = 100, $after: String, $filter: ProductFilterInput) {
  publicProducts(
    channel: "customer"
    first: $first
    after: $after
    filter: $filter
    sortBy: {channel: "customer", direction: ASC, field: CREATED_AT}
  ) {
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        name
        slug
        isAvailable
        isAvailableForPurchase
        pricing {
          priceRange {
            start {
              gross {
                amount
              }
            }
          }
          priceRangeUndiscounted {
            start {
              gross {
                amount
              }
            }
          }
        }
        defaultVariant {
          id
          quantityAvailable
          pricing {
            price {
              gross {
                amount
              }
            }
            priceUndiscounted {
              gross {
                amount
              }
            }
          }
        }
        variants {
          id
          name
          quantityAvailable
          pricing {
            price {
              gross {
                amount
              }
            }
            priceUndiscounted {
              gross {
                amount
              }
            }
          }
        }
      }
    }
  }
}
GQL;

    while (true) {
        $variables = [
            'first'  => $pageSize,
            'after'  => $after,
            'filter' => $filter,
        ];

        $json = hamrahtelGraphqlRequestForButton(
            $config,
            $graphqlEndpoint,
            $categoryUrl,
            $query,
            $variables,
            'CategoryBrandProductsStock'
        );

        $conn = $json['data']['publicProducts'] ?? null;
        if (!is_array($conn)) {
            throw new RuntimeException('ساختار publicProducts در پاسخ پیدا نشد.');
        }

        $edges = $conn['edges'] ?? [];

        foreach ($edges as $edge) {
            $node = $edge['node'] ?? null;
            if (!is_array($node)) {
                continue;
            }

            $slug = trim((string)($node['slug'] ?? ''));
            $title = trim((string)($node['name'] ?? ''));
            if ($slug === '' || $title === '') {
                continue;
            }

            $price = (int)round(pickBestPriceHamrahtelForButton($node));
            $qty   = pickQtyHamrahtelForButton($node);

            if (!isInStockNodeHamrahtelForButton($node, (float)$price)) {
                continue;
            }

            $key = strtolower($slug);
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;

            $rows[] = [
                'title' => normalizeSpaces($title),
                'price' => $price,
                'qty'   => $qty,
                'slug'  => $slug,
                'url'   => $baseUrl . '/products/' . ltrim($slug, '/'),
            ];
        }

        $pageInfo = $conn['pageInfo'] ?? [];
        $hasNextPage = (bool)($pageInfo['hasNextPage'] ?? false);
        $endCursor = $pageInfo['endCursor'] ?? null;

        if (!$hasNextPage || !$endCursor) {
            break;
        }

        $after = (string)$endCursor;
    }

    return $rows;
}

function handleAhvazPhoneChangeTemplateRequest(array $config, string $chatId): void
{
    try {
        baleSendMessage($config, $chatId, 'در حال ساخت فایل تغییرات اهوازفون...');

        $filePath = runAhvazPhoneChangeTemplateExporter($config);

        baleSendDocument($config, $chatId, $filePath, basename($filePath));
        setChatWorkflowState($config, $chatId, 'awaiting_ahvaz_change_file');

        sendMainMenu(
            $config,
            $chatId,
            "فایل ارسال شد.\nقیمت جدید و تعداد جدید را پر کن و همان فایل را برگردان."
        );
    } catch (Throwable $e) {
        logLine($config, 'AHVAZ TEMPLATE ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا: ' . $e->getMessage());
    }
}

function runAhvazPhoneChangeTemplateExporter(array $config): string
{
    $rawRows = fetchAhvazPhoneRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای قالب تغییرات اهوازفون هیچ ردیفی ساخته نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'شناسه محصول' => (string)($row['product_id'] ?? ''),
            'عنوان'       => (string)($row['title'] ?? ''),
            'واریانت'     => (string)($row['color'] ?? ''),
            'قیمت فعلی'   => normalizePriceForSheet((string)($row['price'] ?? 0)),
            'تعداد فعلی'  => (string)($row['stock'] ?? ''),
            'قیمت جدید'   => '',
            'تعداد جدید'  => '',
        ];
    }

    $outFile = $config['output_dir'] . '/ahvazphone_change_template_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls(
        $rows,
        $outFile,
        ['شناسه محصول', 'عنوان', 'واریانت', 'قیمت فعلی', 'تعداد فعلی', 'قیمت جدید', 'تعداد جدید'],
        'فایل تغییرات اهوازفون'
    );

    return $outFile;
}

function handleIncomingAhvazPhoneChangeFile(array $config, string $chatId, array $document): void
{
    try {
        baleSendMessage($config, $chatId, 'فایل دریافت شد. در حال اعمال تغییرات...');

        $uploadDir = $config['runtime_dir'] . '/ahvazphone_uploaded_changes';
        ensureDir($uploadDir);

        $localFile = downloadBaleDocument($config, $document, $uploadDir);
        $rows = readAhvazPhoneChangeFile($localFile);

        if (!$rows) {
            throw new RuntimeException('فایل ارسالی خالی است یا ردیف معتبری ندارد.');
        }

        $result = applyAhvazPhoneChanges($config, $rows);

        clearChatWorkflowState($config, $chatId);

        $msg = "✅ اعمال شد\n"
             . "محصولات به‌روزشده: " . $result['updated_products'] . "\n"
             . "واریانت‌های به‌روزشده: " . $result['updated_variants'];

        if (!empty($result['failed'])) {
            $msg .= "\n\nخطاها:\n" . implode("\n", array_slice($result['failed'], 0, 10));
        }

        sendMainMenu($config, $chatId, $msg);
    } catch (Throwable $e) {
        logLine($config, 'AHVAZ APPLY ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در اعمال تغییرات: ' . $e->getMessage());
    }
}

function readAhvazPhoneChangeFile(string $filePath): array
{
    $table = readTableFile($filePath);

    if (!$table) {
        throw new RuntimeException('فایل ارسالی خوانده نشد.');
    }

    $rows = [];
    $rowIndex = 0;

    foreach ($table as $cells) {
        $rowIndex++;
        if ($rowIndex === 1) {
            continue;
        }

        $productId = (int)parsePriceValue((string)($cells[0] ?? ''));
        if ($productId <= 0) {
            continue;
        }

        $rows[] = [
            'product_id' => $productId,
            'title'      => trim((string)($cells[1] ?? '')),
            'color'      => trim((string)($cells[2] ?? '-')) !== '' ? trim((string)($cells[2] ?? '-')) : '-',
            'price'      => trim((string)($cells[3] ?? '')),
            'stock'      => trim((string)($cells[4] ?? '')),
            'new_price'  => trim((string)($cells[5] ?? '')),
            'new_count'  => trim((string)($cells[6] ?? '')),
        ];
    }

    if (!$rows) {
        throw new RuntimeException('فایل ارسالی خوانده شد ولی هیچ product_id معتبری داخلش پیدا نشد.');
    }

    return $rows;
}
function applyAhvazPhoneChanges(array $config, array $rows): array
{
    $c = $config['ahvazphone'] ?? [];

    $baseUrl    = rtrim((string)($c['base_url'] ?? 'https://ahvazphone.ir'), '/');
    $delayMicro = (int)($c['delay_microseconds'] ?? 150000);

    $grouped = [];
    foreach ($rows as $row) {
        $productId = (int)($row['product_id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }
        $grouped[$productId][] = $row;
    }

    if (!$grouped) {
        throw new RuntimeException('هیچ product_id معتبری در فایل پیدا نشد.');
    }

    $updatedProducts = 0;
    $updatedVariants = 0;
    $failed = [];

    foreach ($grouped as $productId => $productRows) {
        try {
            $getUrl = $baseUrl . '/site/api/v1/manage/store/products/' . $productId;
            [$httpCode, $response, $error] = apiGetJsonAhvaz($config, $getUrl);

            if ($error) {
                throw new RuntimeException('GET خطا داد: ' . $error);
            }
            if ($httpCode !== 200) {
                throw new RuntimeException('GET HTTP=' . $httpCode);
            }

            $data = json_decode((string)$response, true);
            $detail = $data['product'] ?? null;
            if (!is_array($detail)) {
                throw new RuntimeException('جزئیات محصول معتبر نیست.');
            }

            [$payload, $changedCount] = buildAhvazPhoneUpdatedPayload($detail, $productRows);

            if ($changedCount <= 0) {
                throw new RuntimeException('هیچ واریانتی برای اعمال تغییر پیدا نشد.');
            }

            [$putHttp, $putResp, $putErr] = apiPutJsonAhvaz($config, $getUrl, $payload);

            if ($putErr) {
                throw new RuntimeException('PUT خطا داد: ' . $putErr);
            }
            if ($putHttp < 200 || $putHttp >= 300) {
                throw new RuntimeException('PUT HTTP=' . $putHttp . ' | ' . mb_substr((string)$putResp, 0, 500));
            }

            $updatedProducts++;
            $updatedVariants += $changedCount;
        } catch (Throwable $e) {
            $failed[] = 'ID ' . $productId . ': ' . $e->getMessage();
        }

        usleep($delayMicro);
    }

    return [
        'updated_products' => $updatedProducts,
        'updated_variants' => $updatedVariants,
        'failed'           => $failed,
    ];
}
function buildAhvazPhoneUpdatedPayload(array $detail, array $productRows): array
{
    $variants = $detail['variants'] ?? [];
    $changedCount = 0;

    if (is_array($variants) && !empty($variants)) {
        $remainingRows = array_values($productRows);
        $newVariants = [];

        foreach ($variants as $variant) {
            $variantPut = normalizeAhvazPhoneVariantForPut($variant);

            $match = takeMatchingAhvazRowForVariant(
                $remainingRows,
                (string)($variantPut['title'] ?? '')
            );

            if ($match !== null) {
                $variantPut['price'] = csvIntOrZero($match['new_price'] ?? '');
                $variantPut['stock'] = csvIntOrZero($match['new_count'] ?? '');
                $changedCount++;
            }

            $newVariants[] = $variantPut;
        }

        $payload = buildAhvazPhonePutPayload($detail, $newVariants);
        return [$payload, $changedCount];
    }

    $first = $productRows[0] ?? null;
    if (!$first) {
        throw new RuntimeException('ردیف معتبری برای محصول پیدا نشد.');
    }

    $newVariants = [[
        'status'        => ['approved', 'bank_payment', 'online_payment', 'cash_on_delivery', 'shipping_required'],
        'price'         => csvIntOrZero($first['new_price'] ?? ''),
        'compare_price' => null,
        'stock'         => csvIntOrZero($first['new_count'] ?? ''),
        'sku'           => '',
        'minimum'       => null,
        'maximum'       => null,
        'weight'        => null,
        'width'         => null,
        'length'        => null,
        'height'        => null,
        'title'         => 'primary',
        'type'          => 'commodity',
    ]];

    $payload = buildAhvazPhonePutPayload($detail, $newVariants);
    return [$payload, 1];
}

function takeMatchingAhvazRowForVariant(array &$rows, string $variantTitle): ?array
{
    $variantKeys = array_unique([
        normalizeVariantMatchKey($variantTitle),
        normalizeVariantMatchKey(extractColorFromVariantTitle($variantTitle)),
    ]);

    foreach ($rows as $idx => $row) {
        $rowKey = normalizeVariantMatchKey((string)($row['color'] ?? ''));
        if ($rowKey !== '-' && in_array($rowKey, $variantKeys, true)) {
            $match = $row;
            unset($rows[$idx]);
            $rows = array_values($rows);
            return $match;
        }
    }

    if (count($rows) === 1) {
        $idx = array_key_first($rows);
        $match = $rows[$idx];
        unset($rows[$idx]);
        $rows = array_values($rows);
        return $match;
    }

    foreach ($rows as $idx => $row) {
        $rowKey = normalizeVariantMatchKey((string)($row['color'] ?? ''));
        if ($rowKey === '-') {
            $match = $row;
            unset($rows[$idx]);
            $rows = array_values($rows);
            return $match;
        }
    }

    return null;
}
function handleExportAllAction(array $config, string $chatId): void
{
    $files = [];
    $warnings = [];

    try {
        baleSendMessage($config, $chatId, 'در حال استخراج همه فایل‌ها...');

        baleSendMessage($config, $chatId, '1 از 7: استخراج حسابرسی...');
        try {
            $files[] = runHesabanwebExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'حسابرسی: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | HESABANWEB ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '2 از 7: استخراج اهوازفون...');
        try {
            $files[] = runAhvazPhoneExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'اهوازفون: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | AHVAZPHONE ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '3 از 7: استخراج فرنا...');
        try {
            $files[] = runFarnaaExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'فرنا: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | FARNAA ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '4 از 7: استخراج همراه تل...');
        try {
            $files[] = runHamrahtelExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'همراه تل: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | HAMRAHTEL ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '5 از 7: استخراج دیجی‌کالا...');
        try {
            $files[] = runDigikalaExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'دیجی‌کالا: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | DIGIKALA ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '6 از 7: استخراج موبایل 140...');
        try {
            $files[] = runMobile140Exporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'موبایل 140: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | MOBILE140 ERROR: ' . $e->getMessage());
        }

        baleSendMessage($config, $chatId, '7 از 7: استخراج تکنولایف...');
        try {
            $files[] = runTechnolifeExporter($config);
        } catch (Throwable $e) {
            $warnings[] = 'تکنولایف: ' . $e->getMessage();
            logLine($config, 'EXPORT ALL | TECHNOLIFE ERROR: ' . $e->getMessage());
        }

        $sentCount = 0;
        foreach ($files as $filePath) {
            if (is_file($filePath)) {
                baleSendDocument($config, $chatId, $filePath, basename($filePath));
                $sentCount++;
            }
        }

        if ($sentCount <= 0) {
            throw new RuntimeException("هیچ فایلی استخراج نشد.\n" . implode("\n", $warnings));
        }

        $msg = '✅ فایل‌های قابل استخراج ارسال شدند.';
        if ($warnings) {
            $msg .= "\n\nمنابعی که خطا داشتند:\n" . implode("\n", $warnings);
        }

        sendMainMenu($config, $chatId, $msg);
    } catch (Throwable $e) {
        logLine($config, 'EXPORT ALL ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در استخراج همه فایل‌ها: ' . $e->getMessage());
    }
}
function safeCollectDigikalaRawRows(array $config): array
{
    try {
        return collectDigikalaRawRows($config);
    } catch (Throwable $e) {
        logLine($config, 'DIGIKALA OPTIONAL ERROR: ' . $e->getMessage());
        return [];
    }
}

function safeCollectMobile140RawRows(array $config): array
{
    try {
        return collectMobile140RawRows($config);
    } catch (Throwable $e) {
        logLine($config, 'MOBILE140 OPTIONAL ERROR: ' . $e->getMessage());
        return [];
    }
}

function safeCollectTechnolifeRawRows(array $config): array
{
    try {
        return collectTechnolifeRawRows($config);
    } catch (Throwable $e) {
        logLine($config, 'TECHNOLIFE OPTIONAL ERROR: ' . $e->getMessage());
        return [];
    }
}
function latestSmartReviewStateFile(array $config): string
{
    return $config['runtime_dir'] . '/latest_smart_review_rows.json';
}

function saveLatestSmartReviewRows(array $config, array $rows): void
{
    file_put_contents(
        latestSmartReviewStateFile($config),
        json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );
}

function loadLatestSmartReviewRows(array $config): array
{
    $file = latestSmartReviewStateFile($config);

    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function handleGenerateTextListAction(array $config, string $chatId): void
{
    try {
        baleSendMessage($config, $chatId, 'در حال ساخت لیست متنی...');

        $text = buildMobilePriceTextList($config);

        if (trim($text) === '') {
            throw new RuntimeException('متن لیست ساخته نشد.');
        }

        $parts = splitTextForBale($text, 3500);
        foreach ($parts as $part) {
            baleSendMessage($config, $chatId, $part);
        }

        sendMainMenu($config, $chatId, '✅ لیست متنی ارسال شد.');
    } catch (Throwable $e) {
        logLine($config, 'TEXT LIST ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در تولید لیست متنی: ' . $e->getMessage());
    }
}
function normalizePriceTextForList(int $price): string
{
    $roundedDisplay = roundTextListDisplayPriceUpTo100($price);
    return number_format($roundedDisplay, 0, '.', ',');
}
function buildMobilePriceTextList(array $config): string
{
    $finalRows   = loadFinalSmartAppliedRows($config);
    $hesabanRows = fetchHesabanwebRows($config);

    if (!$finalRows) {
        throw new RuntimeException('لیست نهایی ذخیره‌شده پیدا نشد. اول فایل بررسی قیمت را تایید یا اعمال کن.');
    }

    if (!$hesabanRows) {
        throw new RuntimeException('فایل حسابرسی خالی است.');
    }

    $hesabanIndex = buildHesabanIndex($hesabanRows);
    $grouped = [];

    foreach ($finalRows as $row) {
        $status = trim((string)($row['status'] ?? ''));

        // فقط این دو وضعیت باید وارد لیست متنی شوند
        if (!in_array($status, ['قابل اعمال', 'بدون مرجع بازار'], true)) {
            continue;
        }

        $hesabanKey = normalizeKeyText((string)($row['hesabanweb_key'] ?? ''));
        $hesabanRow = $hesabanKey !== '' ? ($hesabanIndex[$hesabanKey] ?? null) : null;

        $sourceName = trim((string)($hesabanRow['name'] ?? ''));
        if ($sourceName === '') {
            $sourceName = trim((string)($row['title'] ?? ''));
        }

        [$brandTitle, $modelTitle, $memoryTitle] = formatHesabanNameForTextList(
            $sourceName,
            (string)($row['title'] ?? '')
        );

        if ($modelTitle === '') {
            continue;
        }

        $emoji = detectColorEmojiFromText(
            $sourceName . ' ' . (string)($row['variant'] ?? '')
        );

        if ($status === 'بدون مرجع بازار') {
            $displayPrice = 'Call';
        } else {
            $finalPrice = (int)($row['final_price_for_text'] ?? 0);
            if ($finalPrice <= 0) {
                continue;
            }
            $displayPrice = normalizePriceTextForList($finalPrice);
        }

        $groupKey = $memoryTitle !== '-' ? ($modelTitle . ' | ' . $memoryTitle) : $modelTitle;

        if (!isset($grouped[$brandTitle])) {
            $grouped[$brandTitle] = [];
        }

        if (!isset($grouped[$brandTitle][$groupKey])) {
            $grouped[$brandTitle][$groupKey] = [];
        }

        $itemText = $emoji . $displayPrice;

        if (!in_array($itemText, $grouped[$brandTitle][$groupKey], true)) {
            $grouped[$brandTitle][$groupKey][] = $itemText;
        }
    }

    return renderTextPriceList($grouped);
}

function buildLatestSmartReviewIndexForText(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $productId = (int)($row['product_id'] ?? 0);
        $variant   = (string)($row['variant'] ?? '-');

        if ($productId <= 0) {
            continue;
        }

        $key = buildAhvazMapRowKey($productId, $variant);
        $index[$key] = $row;
    }

    return $index;
}
function formatHesabanNameForTextList(string $hesabName, string $ahvazTitle = ''): array
{
    $brand = detectBrandTitle($ahvazTitle . ' ' . $hesabName);

    $raw = trim(normalizeSpaces($hesabName !== '' ? $hesabName : $ahvazTitle));
    if ($raw === '') {
        return [$brand, '', '-'];
    }

    $memory = extractStorageKey($raw);
    if ($memory === '') {
        $memory = extractStorageKey($ahvazTitle);
    }
    if ($memory === '') {
        $memory = '-';
    }

    $model = $raw;

    // حذف حافظه
    $model = preg_replace('/^\d+\s*\/\s*\d+\s*/u', '', $model);
    $model = preg_replace('/\b\d+\s*\/\s*\d+\b/u', '', $model);
    $model = preg_replace('/\b\d+\s*(?:gb|گیگ|گیگابایت|g)\s+(\d+)\s*(?:gb|گیگ|گیگابایت|g)\b/iu', ' ', $model);

    // حذف رنگ‌های متداول از انتهای نام
    $model = preg_replace('/\s+(مشکی|سفید|آبی|ابي|سبز|صورتی|طلایی|نقره.?ای|بنفش|ارغوانی|فیروزه.?ای|زرد|خاکستری|گرافیت|تیتانیوم|زیتونی|gold|black|white|blue|green|pink|purple|silver|gray|grey|graphite|titanium|olive)\s*$/iu', '', $model);

    // حذف برند از اول نام
    $model = preg_replace('/^(iphone|آیفون|apple|اپل)\s+/iu', '', $model);
    $model = preg_replace('/^(samsung|سامسونگ|galaxy)\s+/iu', '', $model);
    $model = preg_replace('/^(redmi|ردمی)\s+/iu', '', $model);
    $model = preg_replace('/^(poco|پوکو)\s+/iu', '', $model);
    $model = preg_replace('/^(xiaomi|شیائومی)\s+/iu', '', $model);

    // حذف کامل واژه‌ها و حروف فارسی از مدل
    $model = stripPersianWordsFromModel($model);

    // اگر خالی شد، از عنوان اهوازفون به عنوان بکاپ استفاده کن
    if ($model === '') {
        $fallback = trim(normalizeSpaces($ahvazTitle));

        $fallback = preg_replace('/^\d+\s*\/\s*\d+\s*/u', '', $fallback);
        $fallback = preg_replace('/\b\d+\s*\/\s*\d+\b/u', '', $fallback);
        $fallback = preg_replace('/^(iphone|آیفون|apple|اپل)\s+/iu', '', $fallback);
        $fallback = preg_replace('/^(samsung|سامسونگ|galaxy)\s+/iu', '', $fallback);
        $fallback = preg_replace('/^(redmi|ردمی)\s+/iu', '', $fallback);
        $fallback = preg_replace('/^(poco|پوکو)\s+/iu', '', $fallback);
        $fallback = preg_replace('/^(xiaomi|شیائومی)\s+/iu', '', $fallback);

        $model = stripPersianWordsFromModel($fallback);
    }

    $model = strtoupperLatinWords($model);
    $model = trim($model);

    if ($model === '') {
        return [$brand, '', $memory];
    }

    if ($brand === 'Apple (iPhone)') {
        if (!preg_match('/^iphone\b/i', $model)) {
            $model = 'iPhone ' . $model;
        }
    } elseif ($brand === 'Samsung') {
        if (!preg_match('/^samsung\b/i', $model)) {
            $model = 'Samsung ' . $model;
        }
    } elseif ($brand === 'Redmi') {
        if (!preg_match('/^redmi\b/i', $model)) {
            $model = 'Redmi ' . $model;
        }
    } elseif ($brand === 'Poco') {
        if (!preg_match('/^poco\b/i', $model)) {
            $model = 'Poco ' . $model;
        }
    } elseif ($brand === 'Xiaomi') {
        if (!preg_match('/^xiaomi\b/i', $model)) {
            $model = 'Xiaomi ' . $model;
        }
    }

    return [$brand, trim($model), $memory];
}

function strtoupperLatinWords(string $text): string
{
    return preg_replace_callback('/[A-Za-z0-9\-\+]+/', function ($m) {
        return strtoupper($m[0]);
    }, $text);
}

function extractStorageKey(string $text): string
{
    $t = normalizeSpaces(faToEn($text));

    if (preg_match('/(\d+)\s*\/\s*(\d+)/u', $t, $m)) {
        return $m[1] . '/' . $m[2];
    }

    if (preg_match('/(\d+)\s*(?:gb|گیگ|گیگابایت|g)\s+(\d+)\s*(?:gb|گیگ|گیگابایت|g)/iu', $t, $m)) {
        return $m[1] . '/' . $m[2];
    }

    return '';
}

function detectBrandTitle(string $text): string
{
    $t = normalizeKeyText($text);

    if (str_contains($t, 'iphone') || str_contains($t, 'آیفون') || str_contains($t, 'apple') || str_contains($t, 'اپل')) {
        return 'Apple (iPhone)';
    }

    if (str_contains($t, 'samsung') || str_contains($t, 'سامسونگ') || str_contains($t, 'galaxy')) {
        return 'Samsung';
    }

    if (str_contains($t, 'redmi') || str_contains($t, 'ردمی')) {
        return 'Redmi';
    }

    if (str_contains($t, 'poco') || str_contains($t, 'پوکو')) {
        return 'Poco';
    }

    if (str_contains($t, 'xiaomi') || str_contains($t, 'شیائومی')) {
        return 'Xiaomi';
    }

    return 'سایر';
}

function detectBaseColorKey(string $text): string
{
    $t = normalizeKeyText($text);

    $map = [
        'مشکی'      => ['مشکی', 'black'],
        'سفید'      => ['سفید', 'white'],
        'آبی'       => ['آبی', 'ابي', 'blue'],
        'سبز'       => ['سبز', 'green'],
        'زرد'       => ['زرد', 'yellow', 'طلایی', 'gold'],
        'صورتی'     => ['صورتی', 'pink'],
        'بنفش'      => ['بنفش', 'ارغوانی', 'purple'],
        'نقره‌ای'   => ['نقره ای', 'نقره‌ای', 'silver'],
        'فیروزه‌ای' => ['فیروزه', 'turquoise'],
        'خاکستری'   => ['خاکستری', 'graphite', 'gray', 'grey'],
        'تیتانیوم'  => ['تیتانیوم', 'titanium'],
        'زیتونی'    => ['زیتونی', 'olive'],
    ];

    foreach ($map as $base => $variants) {
        foreach ($variants as $variant) {
            if (str_contains($t, normalizeKeyText($variant))) {
                return $base;
            }
        }
    }

    return '';
}

function detectColorEmojiFromText(string $text): string
{
    $base = detectBaseColorKey($text);

    return match ($base) {
        'مشکی'      => '⚫️',
        'سفید'      => '⚪️',
        'آبی'       => '🔵',
        'سبز'       => '🟢',
        'زرد'       => '🟡',
        'صورتی'     => '🌸',
        'بنفش'      => '🟣',
        'نقره‌ای'   => '⚪️',
        'فیروزه‌ای' => '🩵',
        'خاکستری'   => '🔘',
        'تیتانیوم'  => '🔘',
        'زیتونی'    => '🫒',
        default     => '▪️',
    };
}

function roundTextListDisplayPriceUpTo100(int $price): int
{
    if ($price <= 0) {
        return 0;
    }

    // تبدیل قیمت اصلی به عدد نمایشی لیست
    $displayValue = (int)ceil($price / 1000);

    // رند همیشه رو به بالا به مضرب بعدی 100
    return (int)(ceil($displayValue / 100) * 100);
}

function renderTextPriceList(array $grouped): string
{
    $dateText = jdateNow();

    $text = "﷽\n";
    $text .= "وَما مِن دابَّةٍ فِی الأَرضِ إِلّا عَلَی اللَّهِ رِزقُها 🌱\n\n";
    $text .= "📱 لیست قیمت موبایل – فروشگاه اهوازفون | 📅 {$dateText}\n";
    $text .= "📍 اهواز – بازار موبایل آریا – زیرزمین – اهوازفون | 📞 09165000780 – 09127938100\n";
    $text .= "🔗 کانال واتساپ: https://chat.whatsapp.com/D6mhWm0vbdnCAa9VR0B52K\n\n";
    $text .= "---\n\n";

    $order = ['Apple (iPhone)', 'Samsung', 'Redmi', 'Poco', 'Xiaomi', 'سایر'];

    foreach ($order as $brand) {
        if (empty($grouped[$brand])) {
            continue;
        }

        $text .= "## ✅ {$brand}\n\n";

        krsort($grouped[$brand]);

        foreach ($grouped[$brand] as $model => $items) {
            $text .= $model . ' ' . implode(' ', $items) . "\n\n";
        }

        $text .= "---\n\n";
    }

    return trim($text);
}

function splitTextForBale(string $text, int $maxLen = 3500): array
{
    $text = str_replace("\r\n", "\n", $text);

    if (mb_strlen($text, 'UTF-8') <= $maxLen) {
        return [$text];
    }

    $blocks = explode("\n\n", $text);
    $parts = [];
    $current = '';

    foreach ($blocks as $block) {
        $candidate = $current === '' ? $block : $current . "\n\n" . $block;

        if (mb_strlen($candidate, 'UTF-8') <= $maxLen) {
            $current = $candidate;
        } else {
            if ($current !== '') {
                $parts[] = $current;
            }
            $current = $block;
        }
    }

    if ($current !== '') {
        $parts[] = $current;
    }

    return $parts;
}

function jdateNow(): string
{
    [$gy, $gm, $gd] = array_map('intval', explode('-', date('Y-m-d')));
    [$jy, $jm, $jd] = gregorian_to_jalali($gy, $gm, $gd);

    return $jy . '/' . str_pad((string)$jm, 2, '0', STR_PAD_LEFT) . '/' . str_pad((string)$jd, 2, '0', STR_PAD_LEFT);
}

function gregorian_to_jalali(int $gy, int $gm, int $gd): array
{
    $g_d_m = [0,31,59,90,120,151,181,212,243,273,304,334];
    if ($gy > 1600) {
        $jy = 979;
        $gy -= 1600;
    } else {
        $jy = 0;
        $gy -= 621;
    }

    $gy2 = ($gm > 2) ? ($gy + 1) : $gy;
    $days = 365 * $gy
        + intdiv($gy2 + 3, 4)
        - intdiv($gy2 + 99, 100)
        + intdiv($gy2 + 399, 400)
        - 80
        + $gd
        + $g_d_m[$gm - 1];

    $jy += 33 * intdiv($days, 12053);
    $days %= 12053;

    $jy += 4 * intdiv($days, 1461);
    $days %= 1461;

    if ($days > 365) {
        $jy += intdiv($days - 1, 365);
        $days = ($days - 1) % 365;
    }

    if ($days < 186) {
        $jm = 1 + intdiv($days, 31);
        $jd = 1 + ($days % 31);
    } else {
        $jm = 7 + intdiv($days - 186, 30);
        $jd = 1 + (($days - 186) % 30);
    }

    return [$jy, $jm, $jd];
}
function runSmartMapTemplateExporter(array $config): string
{
    $ahvazRows = fetchAhvazPhoneRows($config);
    $existingMap = loadSmartPriceMap($config);

    if (!$ahvazRows) {
        throw new RuntimeException('هیچ ردیفی از اهوازفون برای ساخت مپ پیدا نشد.');
    }

    $rows = [];
    foreach ($ahvazRows as $row) {
        $mapKey = buildAhvazMapRowKey((int)$row['product_id'], (string)$row['color']);
        $saved  = $existingMap[$mapKey] ?? [];

        $rows[] = [
            'شناسه محصول اهوازفون' => (string)($row['product_id'] ?? ''),
            'عنوان اهوازفون'       => (string)($row['title'] ?? ''),
            'واریانت اهوازفون'     => (string)($row['color'] ?? ''),
            'کلید حسابرسی'         => (string)($saved['hesabanweb_key'] ?? ''),
            'کلید فرنا'            => (string)($saved['farnaa_key'] ?? ''),
            'کلید همراه تل'        => (string)($saved['hamrahtel_key'] ?? ''),
            'کلید دیجی‌کالا'       => (string)($saved['digikala_key'] ?? ''),
            'کلید موبایل140'       => (string)($saved['mobile140_key'] ?? ''),
            'کلید تکنولایف'        => (string)($saved['technolife_key'] ?? ''),
        ];
    }

    $file = $config['output_dir'] . '/smart_price_map_' . date('Y-m-d_H-i-s') . '.csv';
    writeCsvUtf8Bom(
        $rows,
        $file,
        [
            'شناسه محصول اهوازفون',
            'عنوان اهوازفون',
            'واریانت اهوازفون',
            'کلید حسابرسی',
            'کلید فرنا',
            'کلید همراه تل',
            'کلید دیجی‌کالا',
            'کلید موبایل140',
            'کلید تکنولایف',
        ]
    );

    saveLatestSmartMapCsvCopy($config, $file);
    return $file;
}

// function getSmartMapTemplateFileForSend(array $config): string
// {
//     $latest = smartPriceMapLatestCsvFile($config);

//     if (is_file($latest) && filesize($latest) > 0) {
//         return $latest;
//     }

//     return runSmartMapTemplateExporter($config);
// }
function getSmartMapTemplateFileForSend(array $config): string
{
    return runSmartMapTemplateExporter($config);
}
function handleIncomingSmartMapFile(array $config, string $chatId, array $document): void
{
    try {
        baleSendMessage($config, $chatId, 'فایل مپ دریافت شد. در حال ذخیره...');

        $uploadDir = $config['runtime_dir'] . '/smart_map_uploaded';
        ensureDir($uploadDir);

        $localFile = downloadBaleDocument($config, $document, $uploadDir);
        $mapRows = readSmartMapFile($localFile);

        if (!$mapRows) {
            throw new RuntimeException('هیچ ردیف معتبری در فایل مپ پیدا نشد.');
        }

        saveSmartPriceMap($config, $mapRows);
        clearChatWorkflowState($config, $chatId);

        sendMainMenu($config, $chatId, '✅ مپ قیمت هوشمند ذخیره شد.');
    } catch (Throwable $e) {
        logLine($config, 'SMART MAP APPLY ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در ذخیره مپ: ' . $e->getMessage());
    }
}
function readSmartMapFile(string $filePath): array
{
    $table = readTableFile($filePath);

    if (!$table) {
        throw new RuntimeException('فایل مپ خوانده نشد.');
    }

    $rows = [];
    $rowIndex = 0;

    foreach ($table as $cells) {
        $rowIndex++;
        if ($rowIndex === 1) {
            continue;
        }

        $productId = (int)parsePriceValue((string)($cells[0] ?? ''));
        if ($productId <= 0) {
            continue;
        }

        $variant = trim((string)($cells[2] ?? '-'));
        if ($variant === '') {
            $variant = '-';
        }

        $rows[] = [
            'product_id'      => $productId,
            'title'           => trim((string)($cells[1] ?? '')),
            'variant'         => $variant,
            'hesabanweb_key'  => trim((string)($cells[3] ?? '')),
            'farnaa_key'      => trim((string)($cells[4] ?? '')),
            'hamrahtel_key'   => trim((string)($cells[5] ?? '')),
            'digikala_key'    => trim((string)($cells[6] ?? '')),
            'mobile140_key'   => trim((string)($cells[7] ?? '')),
            'technolife_key'  => trim((string)($cells[8] ?? '')),
        ];
    }

    return $rows;
}
function saveSmartPriceMap(array $config, array $rows): void
{
    $map = [];

    foreach ($rows as $row) {
        $key = buildAhvazMapRowKey((int)$row['product_id'], (string)$row['variant']);
        $map[$key] = [
            'product_id'      => (int)$row['product_id'],
            'title'           => (string)$row['title'],
            'variant'         => (string)$row['variant'],
            'hesabanweb_key'  => (string)$row['hesabanweb_key'],
            'farnaa_key'      => (string)$row['farnaa_key'],
            'hamrahtel_key'   => (string)$row['hamrahtel_key'],
            'digikala_key'    => (string)($row['digikala_key'] ?? ''),
            'mobile140_key'   => (string)($row['mobile140_key'] ?? ''),
            'technolife_key'  => (string)($row['technolife_key'] ?? ''),
        ];
    }

    file_put_contents(
        smartPriceMapFile($config),
        json_encode($map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );

    saveSmartPriceMapCsvState($config, $rows);
}
function saveSmartPriceMapCsvState(array $config, array $rows): void
{
    $csvRows = [];

    foreach ($rows as $row) {
        $csvRows[] = [
            'شناسه محصول اهوازفون' => (string)($row['product_id'] ?? ''),
            'عنوان اهوازفون'       => (string)($row['title'] ?? ''),
            'واریانت اهوازفون'     => (string)($row['variant'] ?? ''),
            'کلید حسابرسی'         => (string)($row['hesabanweb_key'] ?? ''),
            'کلید فرنا'            => (string)($row['farnaa_key'] ?? ''),
            'کلید همراه تل'        => (string)($row['hamrahtel_key'] ?? ''),
            'کلید دیجی‌کالا'       => (string)($row['digikala_key'] ?? ''),
            'کلید موبایل140'       => (string)($row['mobile140_key'] ?? ''),
            'کلید تکنولایف'        => (string)($row['technolife_key'] ?? ''),
        ];
    }

    writeCsvUtf8Bom(
        $csvRows,
        smartPriceMapLatestCsvFile($config),
        [
            'شناسه محصول اهوازفون',
            'عنوان اهوازفون',
            'واریانت اهوازفون',
            'کلید حسابرسی',
            'کلید فرنا',
            'کلید همراه تل',
            'کلید دیجی‌کالا',
            'کلید موبایل140',
            'کلید تکنولایف',
        ]
    );
}

function loadSmartPriceMap(array $config): array
{
    $file = smartPriceMapFile($config);
    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function smartPriceMapFile(array $config): string
{
    return $config['runtime_dir'] . '/smart_price_map.json';
}

function smartPriceMapLatestCsvFile(array $config): string
{
    return $config['runtime_dir'] . '/smart_price_map_latest.csv';
}

function saveLatestSmartMapCsvCopy(array $config, string $sourceFile): void
{
    $dest = smartPriceMapLatestCsvFile($config);
    ensureDir(dirname($dest));

    if (!@copy($sourceFile, $dest)) {
        throw new RuntimeException('ذخیره نسخه آخر فایل مپ ممکن نشد.');
    }
}

function buildAhvazMapRowKey(int $productId, string $variant): string
{
    return $productId . '|' . normalizeVariantMatchKey($variant);
}
function handleSmartReviewRequest(array $config, string $chatId): void
{
    try {
        baleSendMessage($config, $chatId, 'در حال ساخت فایل بررسی قیمت هوشمند...');

        $map = loadSmartPriceMap($config);
        if (!$map) {
            throw new RuntimeException('اول باید مپ قیمت هوشمند را تنظیم کنی.');
        }

        $reviewRows = buildSmartPricingReviewRows($config, $map);
        if (!$reviewRows) {
            throw new RuntimeException('هیچ ردیفی برای بررسی قیمت هوشمند ساخته نشد.');
        }

        saveLatestSmartReviewRows($config, $reviewRows);

        $filePath = writeSmartPriceReviewCsv($config, $reviewRows);
        saveSmartReviewSnapshot($config, $chatId, $reviewRows);
        setChatWorkflowState($config, $chatId, 'awaiting_smart_review_file');

        baleSendDocument($config, $chatId, $filePath, basename($filePath));
        sendMainMenu(
            $config,
            $chatId,
            "فایل مپ CSV ارسال شد.\nجلوی هر واریانت، کلید متناظر حسابرسی، فرنا، همراه‌تل، دیجی‌کالا، موبایل140 و تکنولایف را پر کن و همان فایل را برگردان."
        );
    } catch (Throwable $e) {
        logLine($config, 'SMART REVIEW ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا: ' . $e->getMessage());
    }
}
function buildDigikalaIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $keys = [
            normalizeKeyText((string)($row['title'] ?? '')),
            normalizeKeyText((string)($row['slug'] ?? '')),
        ];

        foreach ($keys as $key) {
            if ($key !== '') {
                $index[$key] = $row;
            }
        }
    }

    return $index;
}

function buildMobile140Index(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $keys = [
            normalizeKeyText((string)($row['title'] ?? '')),
            normalizeKeyText((string)($row['slug'] ?? '')),
        ];

        foreach ($keys as $key) {
            if ($key !== '') {
                $index[$key] = $row;
            }
        }
    }

    return $index;
}

function buildTechnolifeIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $keys = [
            normalizeKeyText((string)($row['title'] ?? '')),
            normalizeKeyText((string)($row['slug'] ?? '')),
        ];

        foreach ($keys as $key) {
            if ($key !== '') {
                $index[$key] = $row;
            }
        }
    }

    return $index;
}
function handleSmartMapTemplateRequest(array $config, string $chatId): void
{
    try {
        baleSendMessage($config, $chatId, 'در حال ساخت فایل مپ قیمت هوشمند...');

        $filePath = getSmartMapTemplateFileForSend($config);

        baleSendDocument($config, $chatId, $filePath, basename($filePath));
        setChatWorkflowState($config, $chatId, 'awaiting_smart_map_file');

        sendMainMenu(
            $config,
            $chatId,
            "فایل مپ CSV ارسال شد.\nجلوی هر واریانت، کلید متناظر حسابرسی و فرنا و همراه‌تل را پر کن و همان فایل را برگردان."
        );
    } catch (Throwable $e) {
        logLine($config, 'SMART MAP TEMPLATE ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا: ' . $e->getMessage());
    }
}

function buildSmartPricingReviewRows(array $config, array $map): array
{
    $hesabanRows    = fetchHesabanwebRows($config);
    $farnaaRows     = safeCollectFarnaaRawRows($config);
    $hamrahtelRows  = safeCollectHamrahtelRawRows($config);
    $digikalaRows   = safeCollectDigikalaRawRows($config);
    $mobile140Rows  = safeCollectMobile140RawRows($config);
    $technolifeRows = safeCollectTechnolifeRawRows($config);
    $ahvazRows      = fetchAhvazPhoneRows($config);

    $hesabanIndex    = buildHesabanIndex($hesabanRows);
    $farnaaIndex     = buildFarnaaIndex($farnaaRows);
    $hamrahtelIndex  = buildHamrahtelIndex($hamrahtelRows);
    $digikalaIndex   = buildDigikalaIndex($digikalaRows);
    $mobile140Index  = buildMobile140Index($mobile140Rows);
    $technolifeIndex = buildTechnolifeIndex($technolifeRows);

    $inStockVariantCountsByTitle = buildInStockMappedVariantCountsByTitle($ahvazRows, $map, $hesabanIndex);

    $reviewRows = [];

    foreach ($ahvazRows as $ahvazRow) {
        $productId = (int)($ahvazRow['product_id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }

        $variant = trim((string)($ahvazRow['color'] ?? '-'));
        if ($variant === '') {
            $variant = '-';
        }

        $mapKey = buildAhvazMapRowKey($productId, $variant);

        $mapRow = $map[$mapKey] ?? [
            'product_id'      => $productId,
            'title'           => (string)($ahvazRow['title'] ?? ''),
            'variant'         => $variant,
            'hesabanweb_key'  => '',
            'farnaa_key'      => '',
            'hamrahtel_key'   => '',
            'digikala_key'    => '',
            'mobile140_key'   => '',
            'technolife_key'  => '',
        ];

        $hesabanKey    = normalizeKeyText((string)($mapRow['hesabanweb_key'] ?? ''));
        $farnaaKey     = normalizeKeyText((string)($mapRow['farnaa_key'] ?? ''));
        $hamrahtelKey  = normalizeKeyText((string)($mapRow['hamrahtel_key'] ?? ''));
        $digikalaKey   = normalizeKeyText((string)($mapRow['digikala_key'] ?? ''));
        $mobile140Key  = normalizeKeyText((string)($mapRow['mobile140_key'] ?? ''));
        $technolifeKey = normalizeKeyText((string)($mapRow['technolife_key'] ?? ''));

        $hesabanRow = $hesabanKey !== '' ? ($hesabanIndex[$hesabanKey] ?? null) : null;

        $buyCount = $hesabanRow ? (int)($hesabanRow['count'] ?? 0) : 0;
        $buyPrice = $hesabanRow ? (int)($hesabanRow['buy_price'] ?? 0) : 0;

        $farnaaPrice = ($farnaaKey !== '' && isset($farnaaIndex[$farnaaKey]))
            ? (int)($farnaaIndex[$farnaaKey]['price'] ?? 0)
            : 0;

        $hamrahtelPrice = ($hamrahtelKey !== '' && isset($hamrahtelIndex[$hamrahtelKey]))
            ? (int)($hamrahtelIndex[$hamrahtelKey]['price'] ?? 0)
            : 0;

        $digikalaPrice = ($digikalaKey !== '' && isset($digikalaIndex[$digikalaKey]))
            ? (int)($digikalaIndex[$digikalaKey]['price'] ?? 0)
            : 0;

        $mobile140Price = ($mobile140Key !== '' && isset($mobile140Index[$mobile140Key]))
            ? (int)($mobile140Index[$mobile140Key]['price'] ?? 0)
            : 0;

        $technolifePrice = ($technolifeKey !== '' && isset($technolifeIndex[$technolifeKey]))
            ? (int)($technolifeIndex[$technolifeKey]['price'] ?? 0)
            : 0;

        // اولویت اصلی: فرنا + موبایل140 + همراه‌تل
        $primaryMarketPrices = array_values(array_filter([
            $farnaaPrice,
            $mobile140Price,
            $hamrahtelPrice,
        ], fn($x) => (int)$x > 0));

        // فقط اگر هیچ‌کدام از این سه تا قیمت نداشتند
        $secondaryMarketPrices = array_values(array_filter([
            $digikalaPrice,
            $technolifePrice,
        ], fn($x) => (int)$x > 0));

        if (!empty($primaryMarketPrices)) {
            $marketPrices = $primaryMarketPrices;
            $marketSource = 'primary';
        } elseif (!empty($secondaryMarketPrices)) {
            $marketPrices = $secondaryMarketPrices;
            $marketSource = 'secondary';
        } else {
            $marketPrices = [];
            $marketSource = 'none';
        }

        $marketMean = !empty($marketPrices)
            ? (int)round(array_sum($marketPrices) / count($marketPrices))
            : 0;

        $titleKey = normalizeKeyText((string)($ahvazRow['title'] ?? ''));
        $mappedInStockColorCount = count($inStockVariantCountsByTitle[$titleKey] ?? []);
        $hasMultipleMappedInStockColors = $mappedInStockColorCount > 1;

        $markupRate = 1.02;
        if ($hasMultipleMappedInStockColors && isBlackOrWhiteColor($variant)) {
            $markupRate = 1.022;
        }

        $proposedPrice = $marketMean > 0 ? (int)ceil($marketMean * $markupRate) : 0;
        $proposedStock = $buyCount > 0 ? (int)ceil($buyCount / 2) : 0;

        // نکته اصلی:
        // همه محصولات اهوازفون باید بیایند، حتی اگر در حسابرسی نباشند
        if ($hesabanKey === '' || !$hesabanRow) {
            $status = 'بدون کلید حسابرسی';
            $proposedPrice = 0;
            $proposedStock = 0;
        } elseif ($buyCount <= 0) {
            $status = 'ناموجود در حسابرسی';
            $proposedPrice = 0;
            $proposedStock = 0;
        } elseif ($buyPrice <= 0) {
            $status = 'قیمت خرید نامعتبر';
            $proposedPrice = 0;
        } elseif (empty($marketPrices)) {
            $status = 'بدون مرجع بازار';
            $proposedPrice = 0;
        } elseif ($proposedPrice <= $buyPrice) {
            $status = 'کمتر یا مساوی قیمت خرید';
        } else {
            $status = 'قابل اعمال';
        }

        $reviewRows[] = [
            'product_id'       => $productId,
            'title'            => (string)($ahvazRow['title'] ?? ''),
            'variant'          => $variant,
            'hesabanweb_key'   => (string)($mapRow['hesabanweb_key'] ?? ''),
            'farnaa_key'       => (string)($mapRow['farnaa_key'] ?? ''),
            'hamrahtel_key'    => (string)($mapRow['hamrahtel_key'] ?? ''),
            'digikala_key'     => (string)($mapRow['digikala_key'] ?? ''),
            'mobile140_key'    => (string)($mapRow['mobile140_key'] ?? ''),
            'technolife_key'   => (string)($mapRow['technolife_key'] ?? ''),
            'current_price'    => (int)($ahvazRow['price'] ?? 0),
            'buy_price'        => $buyPrice,
            'buy_count'        => $buyCount,
            'farnaa_price'     => $farnaaPrice,
            'hamrahtel_price'  => $hamrahtelPrice,
            'digikala_price'   => $digikalaPrice,
            'mobile140_price'  => $mobile140Price,
            'technolife_price' => $technolifePrice,
            'market_source'    => $marketSource,
            'market_mean'      => $marketMean,
            'proposed_price'   => $proposedPrice,
            'proposed_stock'   => $proposedStock,
            'status'           => $status,
        ];
    }

    usort($reviewRows, function ($a, $b) {
        return strcmp(
            normalizeKeyText((string)($a['title'] ?? '') . '|' . (string)($a['variant'] ?? '')),
            normalizeKeyText((string)($b['title'] ?? '') . '|' . (string)($b['variant'] ?? ''))
        );
    });

    return $reviewRows;
}
function buildHesabanIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $keys = [
            normalizeKeyText((string)($row['code'] ?? '')),
            normalizeKeyText((string)($row['name'] ?? '')),
        ];

        foreach ($keys as $key) {
            if ($key === '') {
                continue;
            }
            $index[$key] = $row;
        }
    }

    return $index;
}

function buildFarnaaIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $title = (string)($row['title'] ?? '');
        $color = (string)($row['color'] ?? '');

        $keys = [
            normalizeKeyText($title),
            normalizeKeyText($title . ' | ' . $color),
        ];

        foreach ($keys as $key) {
            if ($key === '') {
                continue;
            }
            $index[$key] = $row;
        }
    }

    return $index;
}

function buildHamrahtelIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $keys = [
            normalizeKeyText((string)($row['title'] ?? '')),
            normalizeKeyText((string)($row['slug'] ?? '')),
        ];

        foreach ($keys as $key) {
            if ($key === '') {
                continue;
            }
            $index[$key] = $row;
        }
    }

    return $index;
}

function buildAhvazIndex(array $rows): array
{
    $index = [];

    foreach ($rows as $row) {
        $key = buildAhvazMapRowKey((int)($row['product_id'] ?? 0), (string)($row['color'] ?? '-'));
        $index[$key] = $row;
    }

    return $index;
}
function writeSmartPriceReviewCsv(array $config, array $rows): string
{
    $out = [];

    foreach ($rows as $row) {
        $out[] = [
            'شناسه محصول اهوازفون' => (string)($row['product_id'] ?? ''),
            'عنوان اهوازفون'       => (string)($row['title'] ?? ''),
            'واریانت اهوازفون'     => (string)($row['variant'] ?? ''),
            'کلید حسابرسی'         => (string)($row['hesabanweb_key'] ?? ''),
            'کلید فرنا'            => (string)($row['farnaa_key'] ?? ''),
            'کلید همراه تل'        => (string)($row['hamrahtel_key'] ?? ''),
            'کلید دیجی‌کالا'       => (string)($row['digikala_key'] ?? ''),
            'کلید موبایل140'       => (string)($row['mobile140_key'] ?? ''),
            'کلید تکنولایف'        => (string)($row['technolife_key'] ?? ''),
            'قیمت فعلی اهوازفون'   => normalizePriceForSheet((string)($row['current_price'] ?? 0)),
            'قیمت خرید حسابرسی'    => normalizePriceForSheet((string)($row['buy_price'] ?? 0)),
            'موجودی حسابرسی'       => (string)($row['buy_count'] ?? 0),
            'قیمت فرنا'            => normalizePriceForSheet((string)($row['farnaa_price'] ?? 0)),
            'قیمت همراه تل'        => normalizePriceForSheet((string)($row['hamrahtel_price'] ?? 0)),
            'قیمت دیجی‌کالا'       => normalizePriceForSheet((string)($row['digikala_price'] ?? 0)),
            'قیمت موبایل140'       => normalizePriceForSheet((string)($row['mobile140_price'] ?? 0)),
            'قیمت تکنولایف'        => normalizePriceForSheet((string)($row['technolife_price'] ?? 0)),
            'میانگین قیمت بازار'   => normalizePriceForSheet((string)($row['market_mean'] ?? 0)),
            'قیمت پیشنهادی اعمال'  => normalizePriceForSheet((string)($row['proposed_price'] ?? 0)),
            'موجودی پیشنهادی اعمال'=> (string)($row['proposed_stock'] ?? 0),
            'وضعیت'                => (string)($row['status'] ?? ''),
        ];
    }

    $file = $config['output_dir'] . '/smart_price_review_' . date('Y-m-d_H-i-s') . '.csv';

    writeCsvUtf8Bom(
        $out,
        $file,
        [
            'شناسه محصول اهوازفون',
            'عنوان اهوازفون',
            'واریانت اهوازفون',
            'کلید حسابرسی',
            'کلید فرنا',
            'کلید همراه تل',
            'کلید دیجی‌کالا',
            'کلید موبایل140',
            'کلید تکنولایف',
            'قیمت فعلی اهوازفون',
            'قیمت خرید حسابرسی',
            'موجودی حسابرسی',
            'قیمت فرنا',
            'قیمت همراه تل',
            'قیمت دیجی‌کالا',
            'قیمت موبایل140',
            'قیمت تکنولایف',
            'میانگین قیمت بازار',
            'قیمت پیشنهادی اعمال',
            'موجودی پیشنهادی اعمال',
            'وضعیت',
        ]
    );

    return $file;
}

function saveSmartReviewSnapshot(array $config, string $chatId, array $rows): void
{
    file_put_contents(
        smartReviewSnapshotFile($config, $chatId),
        json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );
}

function loadSmartReviewSnapshot(array $config, string $chatId): array
{
    $file = smartReviewSnapshotFile($config, $chatId);
    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function clearSmartReviewSnapshot(array $config, string $chatId): void
{
    $file = smartReviewSnapshotFile($config, $chatId);
    if (is_file($file)) {
        @unlink($file);
    }
}

function smartReviewSnapshotFile(array $config, string $chatId): string
{
    return $config['runtime_dir'] . '/smart_review_snapshot_' . preg_replace('/[^\w\-]+/u', '_', $chatId) . '.json';
}
function handleIncomingSmartReviewFile(array $config, string $chatId, array $document): void
{
    try {
        baleSendMessage($config, $chatId, 'فایل بررسی قیمت دریافت شد. در حال اعمال...');

        $uploadDir = $config['runtime_dir'] . '/smart_review_uploaded';
        ensureDir($uploadDir);

        $localFile = downloadBaleDocument($config, $document, $uploadDir);
        $rows = readSmartPriceReviewFile($localFile);

        if (!$rows) {
            throw new RuntimeException('هیچ ردیف معتبری در فایل تایید قیمت پیدا نشد.');
        }

        $approvedRows = normalizeApprovedSmartReviewRows($rows);
        if (!$approvedRows) {
            throw new RuntimeException('هیچ ردیف قابل اعمالی در فایل تایید پیدا نشد.');
        }

        $result = applySmartPricingRows($config, $approvedRows);

        // اینجا کل فایل نهایی کاربر ذخیره می‌شود تا لیست متنی فقط از همین ساخته شود
        saveFinalSmartAppliedRows($config, $rows);

        clearChatWorkflowState($config, $chatId);
        clearSmartReviewSnapshot($config, $chatId);

        sendMainMenu($config, $chatId, buildSmartApplyResultMessage($result));
    } catch (Throwable $e) {
        logLine($config, 'SMART REVIEW APPLY ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در اعمال فایل تایید: ' . $e->getMessage());
    }
}
function readSmartPriceReviewFile(string $filePath): array
{
    $table = readTableFile($filePath);

    if (!$table) {
        throw new RuntimeException('فایل تایید قیمت خوانده نشد.');
    }

    $rows = [];
    $rowIndex = 0;

    foreach ($table as $cells) {
        $rowIndex++;
        if ($rowIndex === 1) {
            continue;
        }

        $productId = (int)parsePriceValue((string)($cells[0] ?? ''));
        if ($productId <= 0) {
            continue;
        }

        $rows[] = [
            'product_id'          => $productId,
            'title'               => trim((string)($cells[1] ?? '')),
            'variant'             => trim((string)($cells[2] ?? '-')) !== '' ? trim((string)($cells[2] ?? '-')) : '-',
            'hesabanweb_key'      => trim((string)($cells[3] ?? '')),
            'farnaa_key'          => trim((string)($cells[4] ?? '')),
            'hamrahtel_key'       => trim((string)($cells[5] ?? '')),
            'digikala_key'        => trim((string)($cells[6] ?? '')),
            'mobile140_key'       => trim((string)($cells[7] ?? '')),
            'technolife_key'      => trim((string)($cells[8] ?? '')),
            'current_price'       => (int)parsePriceValue((string)($cells[9] ?? '')),
            'buy_price'           => (int)parsePriceValue((string)($cells[10] ?? '')),
            'buy_count'           => (int)parsePriceValue((string)($cells[11] ?? '')),
            'farnaa_price'        => (int)parsePriceValue((string)($cells[12] ?? '')),
            'hamrahtel_price'     => (int)parsePriceValue((string)($cells[13] ?? '')),
            'digikala_price'      => (int)parsePriceValue((string)($cells[14] ?? '')),
            'mobile140_price'     => (int)parsePriceValue((string)($cells[15] ?? '')),
            'technolife_price'    => (int)parsePriceValue((string)($cells[16] ?? '')),
            'market_mean'         => (int)parsePriceValue((string)($cells[17] ?? '')),

            'proposed_price_raw'  => trim((string)($cells[18] ?? '')),
            'proposed_stock_raw'  => trim((string)($cells[19] ?? '')),

            'proposed_price'      => (int)parsePriceValue((string)($cells[18] ?? '')),
            'proposed_stock'      => (int)parsePriceValue((string)($cells[19] ?? '')),
            'status'              => trim((string)($cells[20] ?? '')),
        ];
    }

    if (!$rows) {
        throw new RuntimeException('هیچ ردیف معتبری در فایل تایید قیمت پیدا نشد.');
    }

    return $rows;
}
function runDigikalaExporter(array $config): string
{
    $rawRows = collectDigikalaRawRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای دیجی‌کالا هیچ ردیفی تولید نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'نام محصول' => $row['title'],
            'قیمت'      => normalizePriceForSheet((string)$row['price']),
            'رنگ'       => '',
            'دسته'      => (string)($row['category'] ?? 'digikala'),
            'لینک'      => $row['url'],
            'وضعیت'     => 'موجود',
        ];
    }

    usort($rows, function ($a, $b) {
        return comparePricesDesc((string)($a['قیمت'] ?? ''), (string)($b['قیمت'] ?? ''));
    });

    $file = $config['output_dir'] . '/digikala_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $file, ['نام محصول', 'قیمت', 'رنگ', 'دسته', 'لینک', 'وضعیت'], 'خروجی دیجی‌کالا');

    return $file;
}
function fetchExactDigikalaPageForBot(string $categorySlug, array $brands, int $hasSellingStock, int $page): array
{
    $query = [
        'page' => $page,
        'has_selling_stock' => $hasSellingStock,
    ];

    $brands = array_values(array_filter($brands, static fn($v) => $v !== '' && $v !== null));

    foreach ($brands as $i => $brandId) {
        $query["brands[$i]"] = $brandId;
    }

    $url = 'https://api.digikala.com/v1/categories/' . rawurlencode($categorySlug) . '/search/?' . http_build_query($query);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING       => '',
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_TIMEOUT        => 45,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_HTTPHEADER     => [
            'accept: application/json, text/plain, */*',
            'accept-language: fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7',
            'cache-control: no-cache',
            'pragma: no-cache',
            'origin: https://www.digikala.com',
            'referer: https://www.digikala.com/',
            'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
        ],
    ]);

    $body = curl_exec($ch);
    $err  = curl_error($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $err) {
        throw new RuntimeException('cURL error: ' . $err);
    }

    if ($code < 200 || $code >= 300) {
        throw new RuntimeException('HTTP error: ' . $code . ' | body=' . mb_substr((string)$body, 0, 500));
    }

    $json = json_decode((string)$body, true);
    if (!is_array($json)) {
        throw new RuntimeException('JSON decode failed');
    }

    return extractExactProductsFromApiJsonForBot($json);
}
function fetchExactDigikalaListingForBot(string $categorySlug, array $brands, int $hasSellingStock = 1, int $maxPages = 100): array
{
    $all = [];
    $seen = [];
    $noNewRounds = 0;

    for ($page = 1; $page <= $maxPages; $page++) {
        $pageItems = fetchExactDigikalaPageForBot($categorySlug, $brands, $hasSellingStock, $page);
        $addedThisPage = 0;

        foreach ($pageItems as $item) {
            $key = md5(($item['name'] ?? '') . '|' . ($item['url'] ?? ''));
            if (isset($seen[$key])) {
                continue;
            }

            $seen[$key] = true;
            $all[] = $item;
            $addedThisPage++;
        }

        if ($addedThisPage === 0) {
            $noNewRounds++;
        } else {
            $noNewRounds = 0;
        }

        if ($noNewRounds >= 2) {
            break;
        }
    }

    usort($all, fn($a, $b) => ($b['price'] ?? 0) <=> ($a['price'] ?? 0));
    return $all;
}
function collectDigikalaRawRows(array $config): array
{
    $dc = $config['digikala'] ?? [];

    $categories = $dc['categories'] ?? [];
    $hasSellingStock = (int)($dc['has_selling_stock'] ?? 1);
    $maxPages = (int)($dc['max_pages'] ?? 100);

    if (!is_array($categories) || !$categories) {
        // برای سازگاری با ساختار قدیمی
        $categories = [
            [
                'slug'   => 'mobile-phone',
                'brands' => $dc['brands'] ?? [10, 1662, 18],
            ],
        ];
    }

    $rows = [];
    $seen = [];

    foreach ($categories as $category) {
        $slug = trim((string)($category['slug'] ?? ''));
        if ($slug === '') {
            continue;
        }

        $brands = $category['brands'] ?? [];
        if (!is_array($brands)) {
            $brands = [];
        }

        $items = fetchExactDigikalaListingForBot($slug, $brands, $hasSellingStock, $maxPages);

        foreach ($items as $item) {
            $url = trim((string)($item['url'] ?? ''));
            $title = normalizeSpaces((string)($item['name'] ?? ''));
            $price = (int)($item['price'] ?? 0);

            if ($url === '' || $title === '' || $price <= 0) {
                continue;
            }

            $key = md5($title . '|' . $url);
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;

            $rows[] = [
                'title'    => $title,
                'price'    => $price,
                'slug'     => extractProductCodeFromUrl($url),
                'url'      => $url,
                'category' => $slug,
            ];
        }
    }

    return $rows;
}

function fetchExactDigikalaMobileListingForBot(array $brands, int $hasSellingStock = 1, int $maxPages = 100): array
{
    $all = [];
    $seen = [];
    $noNewRounds = 0;

    for ($page = 1; $page <= $maxPages; $page++) {
        $pageItems = fetchExactDigikalaMobilePageForBot($brands, $hasSellingStock, $page);
        $addedThisPage = 0;

        foreach ($pageItems as $item) {
            $key = md5(($item['name'] ?? '') . '|' . ($item['url'] ?? ''));
            if (isset($seen[$key])) continue;

            $seen[$key] = true;
            $all[] = $item;
            $addedThisPage++;
        }

        if ($addedThisPage === 0) $noNewRounds++;
        else $noNewRounds = 0;

        if ($noNewRounds >= 2) break;
    }

    usort($all, fn($a, $b) => ($b['price'] ?? 0) <=> ($a['price'] ?? 0));
    return $all;
}

function fetchExactDigikalaMobilePageForBot(array $brands, int $hasSellingStock, int $page): array
{
    $query = [
        'page' => $page,
        'has_selling_stock' => $hasSellingStock,
    ];

    foreach ($brands as $i => $brandId) {
        $query["brands[$i]"] = $brandId;
    }

    $url = 'https://api.digikala.com/v1/categories/mobile-phone/search/?' . http_build_query($query);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING       => '',
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_TIMEOUT        => 45,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_HTTPHEADER     => [
            'accept: application/json, text/plain, */*',
            'accept-language: fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7',
            'cache-control: no-cache',
            'pragma: no-cache',
            'origin: https://www.digikala.com',
            'referer: https://www.digikala.com/',
            'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
        ],
    ]);

    $body = curl_exec($ch);
    $err  = curl_error($ch);
    $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $err) {
        throw new RuntimeException('cURL error: ' . $err);
    }

    if ($code < 200 || $code >= 300) {
        throw new RuntimeException('HTTP error: ' . $code . ' | body=' . mb_substr((string)$body, 0, 500));
    }

    $json = json_decode((string)$body, true);
    if (!is_array($json)) {
        throw new RuntimeException('JSON decode failed');
    }

    return extractExactProductsFromApiJsonForBot($json);
}

function extractExactProductsFromApiJsonForBot(array $json): array
{
    $rows = [];

    walkDigikalaJsonForBot($json, function (array $node) use (&$rows) {
        $name = findBestDigikalaNameForBot($node);
        $url  = findBestDigikalaUrlForBot($node);
        $price = findBestDigikalaPriceForBot($node);

        if ($name === '' || $url === '' || $price <= 0) return;
        if (strpos($url, '/product/') === false) return;

        $rows[] = [
            'name'  => $name,
            'price' => $price,
            'url'   => $url,
        ];
    });

    return dedupeDigikalaProductsForBot($rows);
}

function walkDigikalaJsonForBot($node, callable $fn): void
{
    if (!is_array($node)) return;

    $fn($node);

    foreach ($node as $value) {
        if (is_array($value)) {
            walkDigikalaJsonForBot($value, $fn);
        }
    }
}

function findBestDigikalaNameForBot(array $node): string
{
    foreach (['title_fa', 'title_en', 'title', 'name', 'product_name'] as $key) {
        if (!empty($node[$key]) && is_string($node[$key])) {
            $v = normalizeText($node[$key]);
            if ($v !== '' && mb_strlen($v) >= 4) {
                return $v;
            }
        }
    }
    return '';
}

function findBestDigikalaUrlForBot(array $node): string
{
    foreach ($node as $value) {
        if (!is_string($value)) continue;

        if (strpos($value, '/product/') === 0) {
            return 'https://www.digikala.com' . $value;
        }

        if (preg_match('~^https?://www\.digikala\.com/product/~', $value)) {
            return $value;
        }
    }

    foreach (['id', 'product_id'] as $idKey) {
        if (!empty($node[$idKey])) {
            $id = preg_replace('/\D+/', '', (string)$node[$idKey]);
            if ($id !== '') {
                return 'https://www.digikala.com/product/dkp-' . $id . '/';
            }
        }
    }

    return '';
}

function findBestDigikalaPriceForBot(array $node): int
{
    $priorityKeys = [
        'selling_price','final_price','current_price','price',
        'price_value','price_number','rrp_price','base_price','amount'
    ];

    foreach ($priorityKeys as $key) {
        if (array_key_exists($key, $node)) {
            $n = parsePriceValueRawForBot($node[$key]);
            if ($n > 1000) {
                return normalizeDigikalaPriceForBot($n);
            }
        }
    }

    $numbers = [];
    collectDigikalaPossiblePricesForBot($node, $numbers);
    $numbers = array_values(array_filter($numbers, fn($n) => $n > 1000));

    if (!empty($numbers)) {
        rsort($numbers, SORT_NUMERIC);
        return normalizeDigikalaPriceForBot($numbers[0]);
    }

    return 0;
}

function collectDigikalaPossiblePricesForBot($node, array &$numbers): void
{
    if (!is_array($node)) return;

    foreach ($node as $k => $v) {
        if (is_array($v)) {
            collectDigikalaPossiblePricesForBot($v, $numbers);
            continue;
        }

        if (!is_string($k)) continue;

        if (preg_match('/^(selling_price|final_price|current_price|price|price_value|price_number|rrp_price|base_price|amount)$/i', $k)) {
            $n = parsePriceValueRawForBot($v);
            if ($n > 1000) $numbers[] = $n;
        }
    }
}

function parsePriceValueRawForBot($value): int
{
    if (is_int($value) || is_float($value)) return (int)$value;
    if (is_string($value) && preg_match('/\d[\d,]*/u', $value, $m)) {
        return (int)str_replace(',', '', $m[0]);
    }
    return 0;
}

function normalizeDigikalaPriceForBot(int $n): int
{
    if ($n >= 100000000) {
        return (int)round($n / 10);
    }
    return $n;
}

function dedupeDigikalaProductsForBot(array $rows): array
{
    $seen = [];
    $out = [];

    foreach ($rows as $row) {
        $name = normalizeText((string)($row['name'] ?? ''));
        $url  = trim((string)($row['url'] ?? ''));
        $price= (int)($row['price'] ?? 0);

        if ($name === '' || $url === '' || $price <= 0) continue;

        $key = md5($name . '|' . $url);
        if (isset($seen[$key])) continue;

        $seen[$key] = true;
        $out[] = [
            'name'  => $name,
            'price' => $price,
            'url'   => $url,
        ];
    }

    return $out;
}
function runMobile140Exporter(array $config): string
{
    $rawRows = collectMobile140RawRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای mobile140 هیچ ردیفی تولید نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'نام محصول' => $row['title'],
            'قیمت'      => normalizePriceForSheet((string)$row['price']),
            'رنگ'       => '',
            'دسته'      => 'mobile140',
            'لینک'      => $row['url'],
            'وضعیت'     => 'موجود',
        ];
    }

    usort($rows, fn($a, $b) => comparePricesDesc((string)($a['قیمت'] ?? ''), (string)($b['قیمت'] ?? '')));

    $file = $config['output_dir'] . '/mobile140_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $file, ['نام محصول', 'قیمت', 'رنگ', 'دسته', 'لینک', 'وضعیت'], 'خروجی mobile140');

    return $file;
}

function collectMobile140RawRows(array $config): array
{
    $mc = $config['mobile140'] ?? [];
    $endpoint = trim((string)($mc['api_url'] ?? 'https://services.mobile140.com/client/ProductSearch/Category'));
    $brands = $mc['brands'] ?? ['xiaomi', 'samsung', 'apple'];
    $pageSize = (int)($mc['page_size'] ?? 12);
    $maxPages = (int)($mc['max_pages'] ?? 100);

    $rows = [];
    $seen = [];

    for ($page = 1; $page <= $maxPages; $page++) {
        $payload = [
            'category' => 'mobile',
            'filters'  => [
                'brands'   => array_values($brands),
                'hasStock' => true,
            ],
            'page'     => $page,
            'pageSize' => $pageSize,
        ];

        $body = mobile140PostJson($endpoint, $payload, $config);
        $json = json_decode($body, true);

        $items = $json['data']['products']['items'] ?? [];
        if (!is_array($items) || !$items) {
            break;
        }

        foreach ($items as $item) {
            $slug = trim((string)($item['slug'] ?? ''));
            $title = normalizeSpaces((string)($item['title'] ?? ''));
            if ($slug === '' || $title === '') continue;

            $variants = $item['variants'] ?? [];
            $bestPrice = 0;

            if (is_array($variants)) {
                foreach ($variants as $variant) {
                    $stock = (string)($variant['stock'] ?? '');
                    $amount = (int)($variant['amount'] ?? 0);
                    if ($stock === 'inStock' && $amount > 0) {
                        if ($bestPrice === 0 || $amount < $bestPrice) {
                            $bestPrice = $amount;
                        }
                    }
                }
            }

            if ($bestPrice <= 0) continue;

            $url = 'https://mobile140.com/product-single/' . ltrim($slug, '/');
            $key = md5($title . '|' . $url);
            if (isset($seen[$key])) continue;
            $seen[$key] = true;

            $rows[] = [
                'title' => $title,
                'price' => $bestPrice,
                'slug'  => $slug,
                'url'   => $url,
            ];
        }

        if (count($items) < $pageSize) {
            break;
        }
    }

    return $rows;
}

function mobile140PostJson(string $url, array $payload, array $config): string
{
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if ($json === false) {
        throw new RuntimeException('ساخت JSON برای mobile140 شکست خورد.');
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $json,
        CURLOPT_HTTPHEADER     => [
            'Accept: application/json, text/plain, */*',
            'Accept-Language: en-US,en;q=0.9,fa;q=0.8',
            'Authorization: Bearer undefined',
            'Content-Type: application/json',
            'Domain: mobile140.com',
            'Origin: https://mobile140.com',
            'Referer: https://mobile140.com/',
            'User-Agent: ' . (string)($config['user_agent'] ?? 'Mozilla/5.0'),
        ],
        CURLOPT_TIMEOUT        => (int)($config['request_timeout'] ?? 180),
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $error = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException('mobile140 cURL error: ' . $error);
    }
    if ($httpCode < 200 || $httpCode >= 300) {
        throw new RuntimeException('mobile140 HTTP=' . $httpCode . ' | ' . mb_substr((string)$response, 0, 500));
    }

    return (string)$response;
}
function runTechnolifeExporter(array $config): string
{
    $rawRows = collectTechnolifeRawRows($config);

    if (!$rawRows) {
        throw new RuntimeException('برای تکنولایف هیچ ردیفی تولید نشد.');
    }

    $rows = [];
    foreach ($rawRows as $row) {
        $rows[] = [
            'نام محصول' => $row['title'],
            'قیمت'      => normalizePriceForSheet((string)$row['price']),
            'رنگ'       => '',
            'دسته'      => 'technolife',
            'لینک'      => $row['url'],
            'وضعیت'     => 'موجود',
        ];
    }

    usort($rows, fn($a, $b) => comparePricesDesc((string)($a['قیمت'] ?? ''), (string)($b['قیمت'] ?? '')));

    $file = $config['output_dir'] . '/technolife_' . date('Y-m-d_H-i-s') . '.xls';
    writeExcelLikeXls($rows, $file, ['نام محصول', 'قیمت', 'رنگ', 'دسته', 'لینک', 'وضعیت'], 'خروجی تکنولایف');

    return $file;
}

function collectTechnolifeRawRows(array $config): array
{
    $tc = $config['technolife'] ?? [];

    $baseUrl = trim((string)($tc['base_url'] ?? 'https://www.technolife.com'));
    $listUrls = $tc['list_urls'] ?? [];
    $maxPages = (int)($tc['max_pages'] ?? 20);

    if (!$listUrls || !is_array($listUrls)) {
        throw new RuntimeException('technolife list_urls تنظیم نشده است.');
    }

    $all = [];
    $seen = [];

    foreach ($listUrls as $listUrl) {
        $listUrl = trim((string)$listUrl);
        if ($listUrl === '') continue;

        $items = crawlTechnolifeHybridForBot($listUrl, $baseUrl, $maxPages);

        foreach ($items as $item) {
            $title = normalizeSpaces((string)($item['name'] ?? ''));
            $price = (int)($item['price'] ?? 0);
            $url   = trim((string)($item['url'] ?? ''));

            if ($title === '' || $price <= 0 || $url === '') continue;

            $key = md5($title . '|' . $url);
            if (isset($seen[$key])) continue;
            $seen[$key] = true;

            $all[] = [
                'title' => $title,
                'price' => $price,
                'slug'  => extractProductCodeFromUrl($url),
                'url'   => $url,
            ];
        }
    }

    return $all;
}

function crawlTechnolifeHybridForBot(string $firstUrl, string $baseUrl, int $maxPages = 20): array
{
    $all = [];
    $seen = [];
    $emptyRounds = 0;
    $lastCount = 0;

    for ($page = 1; $page <= $maxPages; $page++) {
        $url = buildPagedUrl($firstUrl, $page);

        try {
            $html = fetchTechnolifeHtmlForBot($url);
        } catch (Throwable $e) {
            continue;
        }

        $htmlItems = parseTechnolifeHtmlCardsForBot($html, $baseUrl);
        foreach ($htmlItems as $row) {
            addUniqueTechnolifeProductForBot($all, $seen, $row);
        }

        $nextJson = extractTechnolifeNextDataForBot($html);
        if ($nextJson) {
            $data = json_decode($nextJson, true);
            if (is_array($data)) {
                $jsonItems = extractTechnolifeProductsFromAnyJsonForBot($data, $baseUrl);
                foreach ($jsonItems as $row) {
                    addUniqueTechnolifeProductForBot($all, $seen, $row);
                }
            }
        }

        $currentCount = count($all);
        if ($currentCount === $lastCount) $emptyRounds++;
        else $emptyRounds = 0;
        $lastCount = $currentCount;

        if ($emptyRounds >= 2) break;
    }

    usort($all, fn($a, $b) => ($b['price'] ?? 0) <=> ($a['price'] ?? 0));
    return $all;
}

function addUniqueTechnolifeProductForBot(array &$all, array &$seen, array $row): void
{
    $name = normalizeText($row['name'] ?? '');
    $url  = trim((string)($row['url'] ?? ''));
    $price = (int)($row['price'] ?? 0);

    if ($name === '' || $url === '' || $price <= 0) return;
    if (strpos($url, '/product-') === false) return;

    $key = md5($name . '|' . $url);
    if (isset($seen[$key])) return;

    $seen[$key] = true;
    $all[] = [
        'name'  => $name,
        'price' => $price,
        'url'   => $url,
    ];
}

function fetchTechnolifeHtmlForBot(string $url): string
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING       => '',
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_TIMEOUT        => 45,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_HTTPHEADER     => [
            'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'accept-language: fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7',
            'cache-control: no-cache',
            'pragma: no-cache',
            'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
        ],
    ]);

    $html = curl_exec($ch);
    $err  = curl_error($ch);
    $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($html === false || $err) throw new RuntimeException('cURL error: ' . $err);
    if ($code < 200 || $code >= 300) throw new RuntimeException('HTTP error: ' . $code);

    return (string)$html;
}

function parseTechnolifeHtmlCardsForBot(string $html, string $baseUrl): array
{
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $dom->loadHTML('<?xml encoding="utf-8" ?>' . $html);
    $xpath = new DOMXPath($dom);

    $rows = [];
    $sections = $xpath->query('//article//section');

    foreach ($sections as $section) {
        $nameNode = $xpath->query('.//h2', $section)->item(0);
        if (!$nameNode) continue;

        $name = normalizeText($nameNode->textContent);
        if ($name === '') continue;

        $linkNode = $xpath->query('.//a[@href]', $section)->item(0);
        if (!$linkNode) continue;

        $href = trim((string)$linkNode->getAttribute('href'));
        $url = makeAbsoluteUrl($href, $baseUrl);
        if (strpos($url, '/product-') === false) continue;

        $price = extractTechnolifeCurrentPriceFromSectionForBot($xpath, $section);
        if ($price <= 0) continue;

        $rows[] = [
            'name'  => $name,
            'price' => $price,
            'url'   => $url,
        ];
    }

    return $rows;
}

function extractTechnolifeCurrentPriceFromSectionForBot(DOMXPath $xpath, DOMNode $section): int
{
    $numbers = [];
    $pNodes = $xpath->query('.//p', $section);

    foreach ($pNodes as $p) {
        $class = ' ' . $p->getAttribute('class') . ' ';
        if (strpos($class, ' line-through ') !== false) continue;

        $text = normalizeText($p->textContent);
        if (preg_match('/\d[\d,]{4,}/u', $text, $m)) {
            $n = (int)str_replace(',', '', $m[0]);
            if ($n > 100000) $numbers[] = $n;
        }
    }

    return $numbers[0] ?? 0;
}

function extractTechnolifeNextDataForBot(string $html): ?string
{
    if (preg_match('~<script[^>]*id="__NEXT_DATA__"[^>]*>(.*?)</script>~su', $html, $m)) {
        return trim($m[1]);
    }
    return null;
}

function extractTechnolifeProductsFromAnyJsonForBot(array $data, string $baseUrl): array
{
    $rows = [];
    walkTechnolifeJsonTreeForBot($data, function (array $node) use (&$rows, $baseUrl) {
        $urlCandidates = findTechnolifeStringValuesMatchingForBot($node, '~^/product-\d+/.+~u');
        if (empty($urlCandidates)) return;

        $name = findBestTechnolifeNameForBot($node);
        $price = findBestTechnolifePriceForBot($node);
        $url = makeAbsoluteUrl($urlCandidates[0], $baseUrl);

        if ($name !== '' && $price > 0) {
            $rows[] = [
                'name'  => $name,
                'price' => $price,
                'url'   => $url,
            ];
        }
    });

    return $rows;
}

function walkTechnolifeJsonTreeForBot($node, callable $fn): void
{
    if (!is_array($node)) return;

    $fn($node);

    foreach ($node as $value) {
        if (is_array($value)) {
            walkTechnolifeJsonTreeForBot($value, $fn);
        }
    }
}

function findTechnolifeStringValuesMatchingForBot(array $node, string $pattern): array
{
    $out = [];

    foreach ($node as $value) {
        if (is_string($value) && preg_match($pattern, $value)) {
            $out[] = $value;
        } elseif (is_array($value)) {
            foreach (findTechnolifeStringValuesMatchingForBot($value, $pattern) as $v) {
                $out[] = $v;
            }
        }
    }

    return array_values(array_unique($out));
}

function findBestTechnolifeNameForBot(array $node): string
{
    foreach (['title', 'name', 'product_name'] as $key) {
        if (!empty($node[$key]) && is_string($node[$key])) {
            return normalizeText($node[$key]);
        }
    }

    foreach ($node as $value) {
        if (is_array($value)) {
            $name = findBestTechnolifeNameForBot($value);
            if ($name !== '') return $name;
        }
    }

    return '';
}

function findBestTechnolifePriceForBot(array $node): int
{
    $priorityKeys = [
        'price', 'sell_price', 'final_price', 'price_main', 'price_value',
        'price_number', 'last_price', 'current_price', 'sale_price',
        'priceWithDiscount', 'price_with_discount', 'amount'
    ];

    foreach ($priorityKeys as $key) {
        if (array_key_exists($key, $node)) {
            $n = parseTechnolifePriceValueForBot($node[$key]);
            if ($n > 100000) return $n;
        }
    }

    $numbers = [];
    collectTechnolifePossiblePricesForBot($node, $numbers);
    $numbers = array_values(array_filter($numbers, fn($x) => $x > 100000));

    if (!empty($numbers)) {
        sort($numbers, SORT_NUMERIC);
        return $numbers[0];
    }

    return 0;
}

function collectTechnolifePossiblePricesForBot($node, array &$numbers): void
{
    if (!is_array($node)) return;

    foreach ($node as $k => $v) {
        if (is_array($v)) {
            collectTechnolifePossiblePricesForBot($v, $numbers);
            continue;
        }

        if (is_string($k) && preg_match('/price|amount|final|sell|discount|current/i', $k)) {
            $n = parseTechnolifePriceValueForBot($v);
            if ($n > 0) $numbers[] = $n;
        }
    }
}

function parseTechnolifePriceValueForBot($value): int
{
    if (is_int($value) || is_float($value)) return (int)$value;
    if (is_string($value) && preg_match('/\d[\d,]*/u', $value, $m)) {
        return (int)str_replace(',', '', $m[0]);
    }
    return 0;
}
function normalizeApprovedSmartReviewRows(array $rows): array
{
    $approved = [];
    $rejected = [];

    foreach ($rows as $row) {
        $hasPriceInput = csvCellHasUserValue($row['proposed_price_raw'] ?? '');
        $hasStockInput = csvCellHasUserValue($row['proposed_stock_raw'] ?? '');

        if (!$hasPriceInput && !$hasStockInput) {
            continue;
        }

        $finalPrice = null;
        $finalStock = null;

        if ($hasPriceInput) {
            $finalPrice = (int)parsePriceValue((string)($row['proposed_price_raw'] ?? '0'));
            if ($finalPrice < 0) {
                $finalPrice = 0;
            }
        }

        if ($hasStockInput) {
            $finalStock = (int)parsePriceValue((string)($row['proposed_stock_raw'] ?? '0'));
            if ($finalStock < 0) {
                $finalStock = 0;
            }
        }

        if ($finalPrice === null && $finalStock === null) {
            $rejected[] =
                'شناسه ' . (string)($row['product_id'] ?? '') .
                ' | عنوان: ' . (string)($row['title'] ?? '') .
                ' | هیچ مقدار قابل اعمالی مشخص نشده';
            continue;
        }

        $approved[] = [
            'product_id'           => (int)($row['product_id'] ?? 0),
            'title'                => (string)($row['title'] ?? ''),
            'variant'              => (string)($row['variant'] ?? '-'),
            'final_price_to_apply' => $finalPrice,
            'final_stock_to_apply' => $finalStock,
        ];
    }

    if (!$approved) {
        $msg = "هیچ ردیف قابل اعمالی در فایل تایید پیدا نشد.";
        if ($rejected) {
            $msg .= "\n" . implode("\n", array_slice($rejected, 0, 20));
        }
        throw new RuntimeException($msg);
    }

    return $approved;
}
function applyStoredSmartReviewApproval(array $config, string $chatId): void
{
    try {
        baleSendMessage($config, $chatId, 'در حال اعمال همان قیمت‌های تاییدشده...');

        $snapshot = loadSmartReviewSnapshot($config, $chatId);
        if (!$snapshot) {
            throw new RuntimeException('اسنپ‌شات قیمت‌های پیشنهادی پیدا نشد.');
        }

        $approvedRows = [];
        $blockedRows = [];

        foreach ($snapshot as $row) {
            $status = trim((string)($row['status'] ?? ''));
            $proposedPrice = (int)($row['proposed_price'] ?? 0);
            $proposedStock = (int)($row['proposed_stock'] ?? 0);

            if ($status === 'قابل اعمال' && $proposedPrice > 0) {
                $approvedRows[] = [
                    'product_id'           => (int)($row['product_id'] ?? 0),
                    'title'                => (string)($row['title'] ?? ''),
                    'variant'              => (string)($row['variant'] ?? '-'),
                    'final_price_to_apply' => $proposedPrice,
                    'final_stock_to_apply' => max(0, $proposedStock),
                ];
            } else {
                $blockedRows[] = $row;
            }
        }

        if (!$approvedRows) {
            $reasons = [];
            foreach ($blockedRows as $row) {
                $reasons[] =
                    'شناسه ' . (string)($row['product_id'] ?? '') .
                    ' | وضعیت: ' . (string)($row['status'] ?? 'نامشخص') .
                    ' | قیمت خرید: ' . normalizePriceForSheet((string)($row['buy_price'] ?? 0)) .
                    ' | قیمت پیشنهادی: ' . normalizePriceForSheet((string)($row['proposed_price'] ?? 0));
            }

            throw new RuntimeException("هیچ ردیف قابل اعمالی در فایل تایید پیدا نشد.\n" . implode("\n", $reasons));
        }

        $result = applySmartPricingRows($config, $approvedRows);

        // اینجا کل اسنپ‌شات نهایی ذخیره می‌شود تا لیست متنی فقط از همین ساخته شود
        saveFinalSmartAppliedRows($config, $snapshot);

        clearChatWorkflowState($config, $chatId);
        clearSmartReviewSnapshot($config, $chatId);

        sendMainMenu($config, $chatId, buildSmartApplyResultMessage($result));
    } catch (Throwable $e) {
        logLine($config, 'SMART SNAPSHOT APPLY ERROR: ' . $e->getMessage());
        sendMainMenu($config, $chatId, 'خطا در اعمال تایید مستقیم: ' . $e->getMessage());
    }
}
function applySmartPricingRows(array $config, array $rows): array
{
    $c = $config['ahvazphone'] ?? [];

    $baseUrl    = rtrim((string)($c['base_url'] ?? 'https://ahvazphone.ir'), '/');
    $delayMicro = (int)($c['delay_microseconds'] ?? 150000);

    $grouped = [];
    foreach ($rows as $row) {
        $productId = (int)($row['product_id'] ?? 0);
        if ($productId <= 0) {
            continue;
        }
        $grouped[$productId][] = $row;
    }

    if (!$grouped) {
        throw new RuntimeException('هیچ ردیف معتبری برای اعمال قیمت پیدا نشد.');
    }

    $updatedProducts = 0;
    $updatedVariants = 0;
    $failed = [];

    foreach ($grouped as $productId => $productRows) {
        try {
            $getUrl = $baseUrl . '/site/api/v1/manage/store/products/' . $productId;
            [$httpCode, $response, $error] = apiGetJsonAhvaz($config, $getUrl);

            if ($error) {
                throw new RuntimeException('GET خطا داد: ' . $error);
            }
            if ($httpCode !== 200) {
                throw new RuntimeException('GET HTTP=' . $httpCode);
            }

            $data = json_decode((string)$response, true);
            $detail = $data['product'] ?? null;
            if (!is_array($detail)) {
                throw new RuntimeException('جزئیات محصول معتبر نیست.');
            }

            [$payload, $changedCount] = buildSmartPricingPayload($detail, $productRows);

            if ($changedCount <= 0) {
                throw new RuntimeException('هیچ واریانت منطبقی برای اعمال پیدا نشد.');
            }

            [$putHttp, $putResp, $putErr] = apiPutJsonAhvaz($config, $getUrl, $payload);

            if ($putErr) {
                throw new RuntimeException('PUT خطا داد: ' . $putErr);
            }
            if ($putHttp < 200 || $putHttp >= 300) {
                throw new RuntimeException('PUT HTTP=' . $putHttp . ' | ' . mb_substr((string)$putResp, 0, 500));
            }

            $updatedProducts++;
            $updatedVariants += $changedCount;
        } catch (Throwable $e) {
            $failed[] = 'ID ' . $productId . ': ' . $e->getMessage();
        }

        usleep($delayMicro);
    }

    return [
        'updated_products' => $updatedProducts,
        'updated_variants' => $updatedVariants,
        'failed'           => $failed,
    ];
}
function buildSmartPricingPayload(array $detail, array $productRows): array
{
    $variants = $detail['variants'] ?? [];
    $changedCount = 0;

    if (is_array($variants) && !empty($variants)) {
        $remainingRows = array_values($productRows);
        $newVariants = [];

        foreach ($variants as $variant) {
            $variantPut = normalizeAhvazPhoneVariantForPut($variant);

            $match = takeMatchingSmartReviewRowForVariant(
                $remainingRows,
                (string)($variantPut['title'] ?? '')
            );

            if ($match !== null) {
                $changed = false;

                if (array_key_exists('final_price_to_apply', $match) && $match['final_price_to_apply'] !== null) {
                    $variantPut['price'] = max(0, (int)$match['final_price_to_apply']);
                    $changed = true;
                }

                if (array_key_exists('final_stock_to_apply', $match) && $match['final_stock_to_apply'] !== null) {
                    $variantPut['stock'] = max(0, (int)$match['final_stock_to_apply']);
                    $changed = true;
                }

                if ($changed) {
                    $changedCount++;
                }
            }

            $newVariants[] = $variantPut;
        }

        $payload = buildAhvazPhonePutPayload($detail, $newVariants);
        return [$payload, $changedCount];
    }

    $first = $productRows[0] ?? null;
    if (!$first) {
        throw new RuntimeException('ردیف معتبری برای محصول پیدا نشد.');
    }

    $currentPrice = isset($detail['price']) ? (int)$detail['price'] : 0;
    $currentStock = isset($detail['stock']) ? (int)$detail['stock'] : 0;

    $priceToApply = array_key_exists('final_price_to_apply', $first) && $first['final_price_to_apply'] !== null
        ? max(0, (int)$first['final_price_to_apply'])
        : $currentPrice;

    $stockToApply = array_key_exists('final_stock_to_apply', $first) && $first['final_stock_to_apply'] !== null
        ? max(0, (int)$first['final_stock_to_apply'])
        : $currentStock;

    $newVariants = [[
        'status'        => ['approved', 'bank_payment', 'online_payment', 'cash_on_delivery', 'shipping_required'],
        'price'         => $priceToApply,
        'compare_price' => null,
        'stock'         => $stockToApply,
        'sku'           => '',
        'minimum'       => null,
        'maximum'       => null,
        'weight'        => null,
        'width'         => null,
        'length'        => null,
        'height'        => null,
        'title'         => 'primary',
        'type'          => 'commodity',
    ]];

    $payload = buildAhvazPhonePutPayload($detail, $newVariants);
    return [$payload, 1];
}
function takeMatchingSmartReviewRowForVariant(array &$rows, string $variantTitle): ?array
{
    $variantKeys = array_unique([
        normalizeVariantMatchKey($variantTitle),
        normalizeVariantMatchKey(extractColorFromVariantTitle($variantTitle)),
    ]);

    foreach ($rows as $idx => $row) {
        $rowKey = normalizeVariantMatchKey((string)($row['variant'] ?? ''));
        if ($rowKey !== '-' && in_array($rowKey, $variantKeys, true)) {
            $match = $row;
            unset($rows[$idx]);
            $rows = array_values($rows);
            return $match;
        }
    }

    if (count($rows) === 1) {
        $idx = array_key_first($rows);
        $match = $rows[$idx];
        unset($rows[$idx]);
        $rows = array_values($rows);
        return $match;
    }

    foreach ($rows as $idx => $row) {
        $rowKey = normalizeVariantMatchKey((string)($row['variant'] ?? ''));
        if ($rowKey === '-') {
            $match = $row;
            unset($rows[$idx]);
            $rows = array_values($rows);
            return $match;
        }
    }

    return null;
}

function buildSmartApplyResultMessage(array $result): string
{
    $msg = "✅ اعمال شد\n"
         . "محصولات به‌روزشده: " . (string)($result['updated_products'] ?? 0) . "\n"
         . "واریانت‌های به‌روزشده: " . (string)($result['updated_variants'] ?? 0);

    if (!empty($result['failed'])) {
        $msg .= "\n\nخطاها:\n" . implode("\n", array_slice($result['failed'], 0, 10));
    }

    return $msg;
}

function getChatWorkflowState(array $config, string $chatId): string
{
    $state = loadChatWorkflowState($config);
    return (string)($state[$chatId]['mode'] ?? '');
}

function setChatWorkflowState(array $config, string $chatId, string $mode): void
{
    $state = loadChatWorkflowState($config);
    $state[$chatId] = [
        'mode'       => $mode,
        'created_at' => time(),
    ];
    saveChatWorkflowState($config, $state);
}

function clearChatWorkflowState(array $config, string $chatId): void
{
    $state = loadChatWorkflowState($config);
    unset($state[$chatId]);
    saveChatWorkflowState($config, $state);
}

function loadChatWorkflowState(array $config): array
{
    $file = chatWorkflowStateFile($config);
    if (!is_file($file)) {
        return [];
    }

    $json = json_decode((string)file_get_contents($file), true);
    return is_array($json) ? $json : [];
}

function saveChatWorkflowState(array $config, array $state): void
{
    file_put_contents(
        chatWorkflowStateFile($config),
        json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
    );
}

function chatWorkflowStateFile(array $config): string
{
    return $config['runtime_dir'] . '/chat_workflow_state.json';
}

function normalizeVariantMatchKey(string $text): string
{
    $text = normalizeSpaces(faToEn($text));
    $text = preg_replace('/^رنگ\s*:\s*/u', '', $text);
    $text = mb_strtolower(trim($text), 'UTF-8');

    if ($text === '' || $text === '-') {
        return '-';
    }

    return $text;
}

function normalizeKeyText(string $text): string
{
    $text = normalizeSpaces(faToEn($text));
    $text = mb_strtolower($text, 'UTF-8');
    $text = str_replace(['|', '،', ',', 'ـ'], ' ', $text);
    $text = preg_replace('/\s+/u', ' ', trim($text));
    return $text;
}

function normalizeAhvazPhoneVariantForPut(array $variant): array
{
    return [
        'status'        => is_array($variant['status'] ?? null)
            ? array_values($variant['status'])
            : ['approved', 'online_payment', 'bank_payment', 'cash_on_delivery', 'shipping_required'],
        'price'         => (int)($variant['price'] ?? 0),
        'compare_price' => normalizeNullableNumber($variant['compare_price'] ?? null),
        'stock'         => (int)($variant['stock'] ?? 0),
        'sku'           => (string)($variant['sku'] ?? ''),
        'minimum'       => normalizeNullableNumber($variant['minimum'] ?? null),
        'maximum'       => normalizeNullableNumber($variant['maximum'] ?? null),
        'weight'        => normalizeNullableNumber($variant['weight'] ?? null),
        'width'         => normalizeNullableNumber($variant['width'] ?? null),
        'length'        => normalizeNullableNumber($variant['length'] ?? null),
        'height'        => normalizeNullableNumber($variant['height'] ?? null),
        'title'         => trim((string)($variant['title'] ?? 'primary')),
        'type'          => trim((string)($variant['type'] ?? 'commodity')),
    ];
}

function buildAhvazPhonePutPayload(array $detail, array $variants): array
{
    return [
        'title'              => trim((string)($detail['title'] ?? '')),
        'caption'            => trim((string)($detail['caption'] ?? '')),
        'description'        => (string)($detail['description'] ?? ''),
        'contents'           => normalizeAhvazNameValueList($detail['contents'] ?? []),
        'image'              => normalizeAhvazSingleImage($detail['image'] ?? null),
        'images'             => normalizeAhvazImages($detail['images'] ?? []),
        'commenting_enabled' => (bool)($detail['commenting_enabled'] ?? true),
        'fields'             => normalizeAhvazNameValueList($detail['fields'] ?? []),
        'variants'           => array_values($variants),
        'slug'               => trim((string)($detail['slug'] ?? '')),
        'published'          => null,
        'expiration'         => null,
        'password'           => normalizeNullableString($detail['password'] ?? null),
        'meta_title'         => normalizeNullableString($detail['meta_title'] ?? null),
        'meta_description'   => normalizeNullableString($detail['meta_description'] ?? null),
        'meta_robots'        => normalizeNullableString($detail['meta_robots'] ?? null),
        'redirect'           => normalizeNullableString($detail['redirect'] ?? null),
        'filters'            => extractIdListForPut($detail['filters'] ?? []),
        'categories'         => extractIdListForPut($detail['categories'] ?? []),
        'status'             => is_array($detail['status'] ?? null)
            ? array_values($detail['status'])
            : [],
    ];
}

function normalizeAhvazNameValueList($items): array
{
    $out = [];

    if (!is_array($items)) {
        return $out;
    }

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

        $name  = trim((string)($item['name'] ?? ''));
        $value = (string)($item['value'] ?? '');

        if ($name === '' && $value === '') {
            continue;
        }

        $out[] = [
            'name'  => $name,
            'value' => $value,
        ];
    }

    return $out;
}

function normalizeAhvazSingleImage($image): ?string
{
    if (is_string($image)) {
        $image = trim($image);
        return $image !== '' ? $image : null;
    }

    if (is_array($image)) {
        foreach (['path', 'url', 'src', 'image', 'file'] as $key) {
            if (!empty($image[$key]) && is_string($image[$key])) {
                $v = trim($image[$key]);
                if ($v !== '') {
                    return $v;
                }
            }
        }
    }

    return null;
}
function csvCellHasUserValue($value): bool
{
    $v = normalizeSpaces(faToEn((string)$value));
    return $v !== '' && $v !== '-';
}
function normalizeAhvazImages($images): array
{
    $out = [];

    if (!is_array($images)) {
        $one = normalizeAhvazSingleImage($images);
        return $one !== null ? [$one] : [];
    }

    foreach ($images as $img) {
        $path = normalizeAhvazSingleImage($img);
        if ($path !== null) {
            $out[] = $path;
        }
    }

    return array_values(array_unique($out));
}

function normalizeNullableString($value)
{
    if ($value === null) {
        return null;
    }

    $value = trim((string)$value);
    return $value === '' ? null : $value;
}

function normalizeNullableNumber($value)
{
    if ($value === null || $value === '') {
        return null;
    }
    if (is_numeric($value)) {
        return $value + 0;
    }
    return null;
}

function extractIdListForPut($items): array
{
    if (!is_array($items)) {
        return [];
    }

    $out = [];
    foreach ($items as $item) {
        if (is_scalar($item)) {
            $out[] = $item;
            continue;
        }

        if (is_array($item)) {
            if (isset($item['id'])) {
                $out[] = $item['id'];
                continue;
            }
            if (isset($item['value'])) {
                $out[] = $item['value'];
                continue;
            }
        }
    }

    return array_values($out);
}
function readTableFile(string $filePath): array
{
    if (!is_file($filePath)) {
        throw new RuntimeException('فایل پیدا نشد: ' . $filePath);
    }

    $raw = (string)file_get_contents($filePath);
    if (trim($raw) === '') {
        throw new RuntimeException('فایل خالی است.');
    }

    if (
        stripos($raw, 'WorksheetSource') !== false ||
        preg_match('~_files/sheet\d+\.htm~i', $raw)
    ) {
        throw new RuntimeException(
            'این فایل به صورت Web Page اکسل ذخیره شده و داده اصلی داخل فایل‌های جانبی مثل sheet001.htm است. لطفاً فایل را به صورت CSV ذخیره و ارسال کن.'
        );
    }

    if (stripos($raw, '<table') !== false) {
        return readTableRowsFromHtml($raw);
    }

    return readTableRowsFromCsv($filePath);
}
function readTableRowsFromHtml(string $html): array
{
    $xp = createXPath($html);
    $trNodes = $xp->query('//table//tr');

    $rows = [];
    if ($trNodes) {
        foreach ($trNodes as $tr) {
            $cellNodes = $xp->query('./th|./td', $tr);
            if (!$cellNodes || $cellNodes->length === 0) {
                continue;
            }

            $cells = [];
            foreach ($cellNodes as $cell) {
                $cells[] = normalizeSpaces($cell->textContent);
            }

            $rows[] = $cells;
        }
    }

    return $rows;
}

function readTableRowsFromCsv(string $filePath): array
{
    $delimiter = detectCsvDelimiter($filePath);
    $fp = fopen($filePath, 'r');
    if (!$fp) {
        throw new RuntimeException('فایل CSV باز نشد.');
    }

    $rows = [];
    while (($data = fgetcsv($fp, 0, $delimiter)) !== false) {
        if (!is_array($data)) {
            continue;
        }
        $rows[] = array_map(function ($v) {
            $v = str_replace("\xEF\xBB\xBF", '', (string)$v);
            return normalizeSpaces($v);
        }, $data);
    }

    fclose($fp);
    return $rows;
}

function detectCsvDelimiter(string $filePath): string
{
    $sample = (string)file_get_contents($filePath, false, null, 0, 2048);

    $delimiters = [',', ';', "\t"];
    $best = ',';
    $bestCount = -1;

    foreach ($delimiters as $delimiter) {
        $count = substr_count($sample, $delimiter);
        if ($count > $bestCount) {
            $bestCount = $count;
            $best = $delimiter;
        }
    }

    return $best;
}

function writeCsvUtf8Bom(array $rows, string $file, ?array $headers = null): void
{
    ensureDir(dirname($file));

    if ($headers === null) {
        $headers = !empty($rows) ? array_keys(reset($rows)) : [];
    }

    $fp = fopen($file, 'w');
    if (!$fp) {
        throw new RuntimeException('ساخت فایل CSV ممکن نشد: ' . $file);
    }

    fwrite($fp, "\xEF\xBB\xBF");

    if (!empty($headers)) {
        fputcsv($fp, $headers);
    }

    foreach ($rows as $row) {
        $line = [];
        foreach ($headers as $h) {
            $line[] = (string)($row[$h] ?? '');
        }
        fputcsv($fp, $line);
    }

    fclose($fp);
}

function csvIntOrZero($value): int
{
    $digits = parsePriceValue((string)$value);
    if ($digits === '') {
        return 0;
    }
    return (int)$digits;
}

function apiPutJson(string $url, string $token, array $payload): array
{
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if ($json === false) {
        throw new RuntimeException('ساخت JSON برای PUT شکست خورد.');
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => 'PUT',
        CURLOPT_POSTFIELDS     => $json,
        CURLOPT_HTTPHEADER     => [
            'Accept: application/json',
            'Content-Type: application/json',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    return [$httpCode, $response, $error];
}

function downloadBaleDocument(array $config, array $document, string $targetDir): string
{
    $fileId = trim((string)($document['file_id'] ?? ''));
    if ($fileId === '') {
        throw new RuntimeException('file_id فایل بله پیدا نشد.');
    }

    $fileResp = baleApi($config, 'getFile', [
        'file_id' => $fileId,
    ]);

    $fileInfo = $fileResp['result'] ?? null;
    if (!is_array($fileInfo)) {
        throw new RuntimeException('پاسخ getFile معتبر نیست.');
    }

    $filePath = trim((string)($fileInfo['file_path'] ?? ''));
    if ($filePath === '') {
        throw new RuntimeException('file_path در پاسخ getFile پیدا نشد.');
    }

    $token = trim((string)($config['bale_bot_token'] ?? ''));
    if ($token === '') {
        throw new RuntimeException('توکن بله تنظیم نشده است.');
    }

    ensureDir($targetDir);

    $origName = trim((string)($document['file_name'] ?? basename($filePath)));
    if ($origName === '') {
        $origName = 'uploaded_file.csv';
    }

    $localPath = rtrim($targetDir, '/\\') . '/' . time() . '_' . preg_replace('/[^\w\-.]+/u', '_', $origName);
    $downloadUrl = "https://tapi.bale.ai/file/bot{$token}/{$filePath}";

    $ch = curl_init($downloadUrl);
    $fp = fopen($localPath, 'w');
    if (!$fp) {
        throw new RuntimeException('فایل محلی برای ذخیره آپلود ساخته نشد.');
    }

    curl_setopt_array($ch, [
        CURLOPT_FILE           => $fp,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT        => (int)($config['request_timeout'] ?? 180),
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    curl_exec($ch);
    $error = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    fclose($fp);

    if ($error) {
        @unlink($localPath);
        throw new RuntimeException('خطا در دانلود فایل از بله: ' . $error);
    }

    if ($httpCode < 200 || $httpCode >= 300) {
        @unlink($localPath);
        throw new RuntimeException('دانلود فایل از بله با HTTP=' . $httpCode . ' شکست خورد.');
    }

    return $localPath;
}

function baleSendMessage(array $config, string $chatId, string $text, ?array $replyMarkup = null): array
{
    $params = [
        'chat_id' => $chatId,
        'text'    => $text,
    ];

    if ($replyMarkup !== null) {
        $params['reply_markup'] = json_encode($replyMarkup, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    return baleApi($config, 'sendMessage', $params);
}

function baleSendDocument(array $config, string $chatId, string $filePath, string $caption = ''): array
{
    if (!is_file($filePath)) {
        throw new RuntimeException('فایل برای ارسال وجود ندارد: ' . $filePath);
    }

    return baleApiMultipart($config, 'sendDocument', [
        'chat_id'  => $chatId,
        'caption'  => $caption,
        'document' => new CURLFile($filePath, mime_content_type_safe($filePath), basename($filePath)),
    ]);
}

function baleApi(array $config, string $method, array $params = []): array
{
    $token = trim((string)($config['bale_bot_token'] ?? ''));
    if ($token === '') {
        throw new RuntimeException('توکن بله تنظیم نشده است.');
    }

    $url = "https://tapi.bale.ai/bot{$token}/{$method}";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query($params),
        CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
        CURLOPT_TIMEOUT        => (int)($config['request_timeout'] ?? 180),
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    logLine($config, "BALE {$method} | HTTP={$httpCode} | ERR={$error} | RESP=" . mb_substr((string)$response, 0, 1000));

    if ($error) {
        throw new RuntimeException('خطای cURL بله: ' . $error);
    }

    $data = json_decode((string)$response, true);
    if ($httpCode < 200 || $httpCode >= 300 || !is_array($data) || empty($data['ok'])) {
        throw new RuntimeException('خطا در API بله: ' . mb_substr((string)$response, 0, 500));
    }

    return $data;
}

function baleApiMultipart(array $config, string $method, array $params = []): array
{
    $token = trim((string)($config['bale_bot_token'] ?? ''));
    if ($token === '') {
        throw new RuntimeException('توکن بله تنظیم نشده است.');
    }

    $url = "https://tapi.bale.ai/bot{$token}/{$method}";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $params,
        CURLOPT_TIMEOUT        => (int)($config['request_timeout'] ?? 180),
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    logLine($config, "BALE {$method} MULTIPART | HTTP={$httpCode} | ERR={$error} | RESP=" . mb_substr((string)$response, 0, 1000));

    if ($error) {
        throw new RuntimeException('خطای cURL بله: ' . $error);
    }

    $data = json_decode((string)$response, true);
    if ($httpCode < 200 || $httpCode >= 300 || !is_array($data) || empty($data['ok'])) {
        throw new RuntimeException('خطا در API بله: ' . mb_substr((string)$response, 0, 500));
    }

    return $data;
}

function apiGetJson(string $url, string $token): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPGET        => true,
        CURLOPT_HTTPHEADER     => [
            'Accept: application/json',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error    = curl_error($ch);
    curl_close($ch);

    return [$httpCode, $response, $error];
}

function fetchUrl(string $url, array $config): string
{
    $timeout = (int)($config['request_timeout'] ?? 180);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 8,
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_ENCODING       => '',
        CURLOPT_USERAGENT      => (string)($config['user_agent'] ?? 'Mozilla/5.0'),
        CURLOPT_HTTPHEADER     => [
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,application/json,text/plain,*/*;q=0.8',
            'Accept-Language: fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7',
            'X-Requested-With: XMLHttpRequest',
            'Cache-Control: no-cache',
            'Pragma: no-cache',
        ],
    ]);

    $body = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($errno) {
        throw new RuntimeException('خطای cURL در دریافت ' . $url . ' => ' . $error);
    }
    if ($httpCode < 200 || $httpCode >= 300) {
        throw new RuntimeException('HTTP ' . $httpCode . ' برای ' . $url);
    }
    if (!is_string($body) || trim($body) === '') {
        throw new RuntimeException('بدنه خالی برای ' . $url);
    }

    return $body;
}

function parseCategoryProducts(string $html, string $baseUrl): array
{
    $xp = createXPath($html);
    $nodes = $xp->query("//div[contains(concat(' ', normalize-space(@class), ' '), ' item ') and .//div[contains(@class,'product-info')]]");

    $items = [];

    if ($nodes) {
        foreach ($nodes as $node) {
            $name = trim(nodeText($xp, ".//span[contains(@class,'title-box')]//a", $node));
            $href = trim(nodeAttr($xp, ".//span[contains(@class,'title-box')]//a", 'href', $node));

            if ($href === '') {
                $href = trim(nodeAttr($xp, ".//span[contains(@class,'pic-box')]//a", 'href', $node));
            }

            $price = trim(nodeText($xp, ".//div[contains(@class,'pric')]//ins", $node));
            $colors = nodesText($xp, ".//div[contains(@class,'weaz-colors')]//*[contains(@class,'wz-matrix-tooltip-text')]", $node);

            $items[] = [
                'name'   => normalizeSpaces($name),
                'url'    => absoluteUrl($href, $baseUrl),
                'price'  => parsePriceValue($price),
                'colors' => cleanArray($colors),
            ];
        }
    }

    if (!$items) {
        preg_match_all('~<a[^>]+href="([^"]+/product/[^"]+)"[^>]*>(.*?)</a>~isu', $html, $m, PREG_SET_ORDER);
        foreach ($m as $row) {
            $items[] = [
                'name'   => normalizeSpaces(strip_tags($row[2])),
                'url'    => absoluteUrl(html_entity_decode($row[1], ENT_QUOTES | ENT_HTML5, 'UTF-8'), $baseUrl),
                'price'  => '',
                'colors' => [],
            ];
        }
        $items = uniqueByUrl($items);
    }

    return uniqueByUrl(array_values(array_filter($items, fn($x) => !empty($x['url']))));
}

function parseProductPage(string $html, string $url, array $fallback = [], array $config = []): array
{
    $xp = createXPath($html);

    $name = firstNonEmpty([
        nodeText($xp, "//div[contains(@class,'info-title')]//h1"),
        nodeAttr($xp, "//meta[@name='product_name']", 'content'),
        $fallback['name'] ?? '',
    ]);

    $basePrice = firstNonEmpty([
        parsePriceValue(nodeText($xp, "//*[contains(@class,'c-product_price-real')]//span[1]")),
        parsePriceValue(nodeText($xp, "//div[contains(@class,'price-product')]//ins//*[self::span or self::div][1]")),
        parsePriceValue(nodeAttr($xp, "//meta[@name='product_price']", 'content')),
        $fallback['price'] ?? '',
    ]);

    $variants = extractVariants($xp, $fallback);
    $rows = [];

    if ($variants) {
        foreach ($variants as $variant) {
            $variantDomId = trim((string)($variant['id'] ?? ''));
            $colorName = normalizeSpaces((string)($variant['color'] ?? ''));
            $variantPrice = '';
            $status = 'OK-قیمت‌پایه';

            if ($variantDomId !== '' && $colorName !== '') {
                try {
                    $variantData = fetchVariantSalesInfoByDomId($variantDomId, $colorName, $config);
                    if (!empty($variantData['price'])) {
                        $variantPrice = $variantData['price'];
                        $status = 'OK';
                    }
                } catch (Throwable $e) {
                }
            }

            if ($variantPrice === '') {
                $variantPrice = $basePrice;
            }

            $rows[] = [
                'نام محصول' => normalizeSpaces($name),
                'قیمت'      => normalizePriceForSheet($variantPrice),
                'رنگ'       => $colorName,
                'دسته'      => $fallback['دسته'] ?? '',
                'لینک'      => $url,
                'وضعیت'     => $status,
            ];
        }
    } else {
        $colors = cleanArray(array_merge(
            nodesText($xp, "//div[contains(@class,'weaz-colors')]//*[contains(@class,'wz-matrix-tooltip-text')]"),
            $fallback['colors'] ?? []
        ));

        $rows[] = [
            'نام محصول' => normalizeSpaces($name),
            'قیمت'      => normalizePriceForSheet($basePrice),
            'رنگ'       => implode(' | ', array_unique($colors)),
            'دسته'      => $fallback['دسته'] ?? '',
            'لینک'      => $url,
            'وضعیت'     => 'OK',
        ];
    }

    return $rows;
}

function extractVariants(DOMXPath $xp, array $fallback = []): array
{
    $out = [];
    $nodes = $xp->query("//div[contains(@class,'js-product-variants')]//label[contains(@class,'color-option')]");

    if ($nodes) {
        foreach ($nodes as $label) {
            $variantId = '';
            if ($label instanceof DOMElement && $label->hasAttribute('data')) {
                $variantId = trim($label->getAttribute('data'));
            }

            $input = $xp->query(".//input[contains(@class,'js-variant_selector')]", $label)->item(0);

            $color = '';
            if ($input instanceof DOMElement) {
                $color = trim($input->getAttribute('data-id'));
                if ($color === '') {
                    $color = trim($input->getAttribute('value'));
                }
            }

            if ($color === '') {
                $color = getDirectText($label);
            }

            $color = normalizeSpaces($color);

            if ($color !== '') {
                $out[] = [
                    'id'    => $variantId,
                    'color' => $color,
                ];
            }
        }
    }

    if (!$out && !empty($fallback['colors'])) {
        foreach ($fallback['colors'] as $color) {
            $color = normalizeSpaces((string)$color);
            if ($color !== '') {
                $out[] = [
                    'id'    => '',
                    'color' => $color,
                ];
            }
        }
    }

    $unique = [];
    $seen = [];
    foreach ($out as $v) {
        $key = ($v['id'] ?? '') . '|' . ($v['color'] ?? '');
        if (!isset($seen[$key])) {
            $seen[$key] = true;
            $unique[] = $v;
        }
    }

    return $unique;
}

function fetchVariantSalesInfoByDomId(string $domVariantId, string $wantedColor, array $config): array
{
    $domVariantId = trim($domVariantId);
    $wantedColor = normalizeSpaces($wantedColor);

    if ($domVariantId === '') {
        return [
            'price'         => '',
            'active_color'  => '',
            'active_dom_id' => '',
        ];
    }

    $url = 'https://farnaa.com/Product/GetVendorPrices?variantId=' . rawurlencode($domVariantId);
    $body = fetchUrl($url, $config);

    return extractSalesInfoFromVendorResponse($body, $wantedColor);
}

function extractSalesInfoFromVendorResponse(string $body, string $wantedColor = ''): array
{
    $body = trim($body);

    if ($body === '') {
        return [
            'price'         => '',
            'active_color'  => '',
            'active_dom_id' => '',
        ];
    }

    $json = json_decode($body, true);
    if (is_array($json)) {
        foreach (['html', 'data', 'result', 'content'] as $key) {
            if (!empty($json[$key]) && is_string($json[$key])) {
                $body = $json[$key];
                break;
            }
        }
    }

    $xp = createXPath($body);

    $activeLabel = $xp->query("//div[contains(@class,'js-product-variants')]//label[contains(@class,'active')]")->item(0);

    $activeColor = '';
    $activeDomId = '';

    if ($activeLabel instanceof DOMElement) {
        if ($activeLabel->hasAttribute('data')) {
            $activeDomId = trim($activeLabel->getAttribute('data'));
        }

        $input = $xp->query(".//input[contains(@class,'js-variant_selector')]", $activeLabel)->item(0);
        if ($input instanceof DOMElement) {
            $activeColor = trim($input->getAttribute('data-id'));
            if ($activeColor === '') {
                $activeColor = trim($input->getAttribute('value'));
            }
        }

        if ($activeColor === '') {
            $activeColor = getDirectText($activeLabel);
        }

        $activeColor = normalizeSpaces($activeColor);
    }

    $price = firstNonEmpty([
        parsePriceValue(nodeText($xp, "//*[contains(@class,'c-product_price-real')]//span[1]")),
        parsePriceValue(nodeText($xp, "//div[contains(@class,'price-product')]//ins//*[self::span or self::div][1]")),
        parsePriceValue(nodeText($xp, "//div[contains(@class,'sales-info')]//*[contains(@class,'c-product_price-real')]//span[1]")),
        parsePriceValue(strip_tags($body)),
    ]);

    if ($wantedColor !== '') {
        if (normalizeSpaces($activeColor) !== normalizeSpaces($wantedColor)) {
            return [
                'price'         => '',
                'active_color'  => $activeColor,
                'active_dom_id' => $activeDomId,
            ];
        }
    }

    return [
        'price'         => $price,
        'active_color'  => $activeColor,
        'active_dom_id' => $activeDomId,
    ];
}

function hamrahtelGraphqlRequestForButton(
    array $config,
    string $endpoint,
    string $refererUrl,
    string $query,
    array $variables,
    string $operationName
): array {
    $payload = json_encode([
        'operationName' => $operationName,
        'query'         => $query,
        'variables'     => $variables,
    ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if ($payload === false) {
        throw new RuntimeException('ساخت JSON درخواست GraphQL شکست خورد.');
    }

    $ch = curl_init($endpoint);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_HTTPHEADER     => [
            'Accept: application/json, text/plain, */*',
            'Accept-Language: fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7',
            'Content-Type: application/json',
            'Origin: https://hamrahtel.com',
            'Referer: ' . $refererUrl,
            'X-Requested-With: XMLHttpRequest',
            'User-Agent: ' . (string)($config['user_agent'] ?? 'Mozilla/5.0'),
        ],
        CURLOPT_TIMEOUT        => (int)($config['request_timeout'] ?? 180),
        CURLOPT_CONNECTTIMEOUT => 20,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $response = curl_exec($ch);
    $error    = curl_error($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException('خطای cURL گراف‌کیوال همراه تل: ' . $error);
    }

    $json = json_decode((string)$response, true);
    if (!is_array($json)) {
        throw new RuntimeException('پاسخ GraphQL معتبر نبود: ' . mb_substr((string)$response, 0, 1000));
    }

    if ($httpCode < 200 || $httpCode >= 300) {
        throw new RuntimeException('HTTP ' . $httpCode . ' از GraphQL همراه‌تل | پاسخ: ' . mb_substr((string)$response, 0, 1000));
    }

    if (!empty($json['errors'])) {
        throw new RuntimeException('GraphQL errors: ' . json_encode($json['errors'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    }

    return $json;
}

function parseHamrahtelBrandValuesFromUrlForButton(string $url): array
{
    $parts = parse_url($url);
    $query = $parts['query'] ?? '';
    parse_str($query, $params);

    $filter = (string)($params['filter'] ?? '');
    $brands = [];

    foreach (explode(',', $filter) as $part) {
        $part = trim((string)$part);
        if ($part === '') {
            continue;
        }

        if (str_starts_with($part, 'brand-')) {
            $brands[] = substr($part, 6);
        } else {
            $brands[] = $part;
        }
    }

    $brands = array_values(array_unique(array_filter($brands, fn($v) => $v !== '')));

    if (!$brands) {
        throw new RuntimeException('هیچ برند معتبری از لینک همراه‌تل استخراج نشد.');
    }

    return $brands;
}

function pickBestPriceHamrahtelForButton(array $node): float
{
    $candidates = [];

    $candidates[] = extractAmountHamrahtelForButton($node['defaultVariant']['pricing']['price']['gross']['amount'] ?? null);
    $candidates[] = extractAmountHamrahtelForButton($node['pricing']['priceRange']['start']['gross']['amount'] ?? null);
    $candidates[] = extractAmountHamrahtelForButton($node['pricing']['priceRangeUndiscounted']['start']['gross']['amount'] ?? null);

    foreach (($node['variants'] ?? []) as $variant) {
        $candidates[] = extractAmountHamrahtelForButton($variant['pricing']['price']['gross']['amount'] ?? null);
        $candidates[] = extractAmountHamrahtelForButton($variant['pricing']['priceUndiscounted']['gross']['amount'] ?? null);
    }

    $candidates = array_values(array_filter($candidates, fn($v) => $v > 0));

    if (!$candidates) {
        return 0.0;
    }

    return min($candidates);
}

function extractAmountHamrahtelForButton($value): float
{
    if (is_numeric($value)) {
        return (float)$value;
    }
    return 0.0;
}

function pickQtyHamrahtelForButton(array $node): ?int
{
    $qtys = [];

    $dvQty = normalizeQtyHamrahtelForButton($node['defaultVariant']['quantityAvailable'] ?? null);
    if ($dvQty !== null) {
        $qtys[] = $dvQty;
    }

    foreach (($node['variants'] ?? []) as $variant) {
        $q = normalizeQtyHamrahtelForButton($variant['quantityAvailable'] ?? null);
        if ($q !== null) {
            $qtys[] = $q;
        }
    }

    if (!$qtys) {
        return null;
    }

    return max($qtys);
}

function normalizeQtyHamrahtelForButton($value): ?int
{
    if ($value === null || $value === '') {
        return null;
    }
    if (is_numeric($value)) {
        return (int)$value;
    }
    return null;
}

function isInStockNodeHamrahtelForButton(array $node, float $price): bool
{
    if ($price <= 0) {
        return false;
    }

    $qty = pickQtyHamrahtelForButton($node);
    if ($qty !== null) {
        return $qty > 0;
    }

    $isAvailable = $node['isAvailable'] ?? null;
    $isAvailableForPurchase = $node['isAvailableForPurchase'] ?? null;

    return $isAvailable === true && $isAvailableForPurchase === true;
}

function rowHasPrice(array $row): bool
{
    $price = parsePriceValue((string)($row['قیمت'] ?? ''));
    return $price !== '' && $price !== '0';
}

function writeExcelLikeXls(array $rows, string $file, ?array $headers = null, string $title = ''): void
{
    if ($headers === null) {
        $headers = !empty($rows) ? array_keys(reset($rows)) : [];
    }

    $count = count($rows);

    $html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
    $html .= '<style>
        body{
            font-family: Tahoma, Arial, sans-serif;
            direction: rtl;
            margin: 20px;
            color: #1f2937;
            background: #ffffff;
        }
        .title{
            font-size: 20px;
            font-weight: bold;
            margin-bottom: 6px;
            color: #0f172a;
        }
        .meta{
            font-size: 12px;
            color: #64748b;
            margin-bottom: 14px;
        }
        table{
            border-collapse: collapse;
            width: 100%;
            table-layout: auto;
            font-size: 13px;
        }
        th, td{
            border: 1px solid #cbd5e1;
            padding: 8px 10px;
            vertical-align: top;
            text-align: right;
            white-space: normal;
        }
        th{
            background: #1d4ed8;
            color: #ffffff;
            font-weight: bold;
        }
        tr:nth-child(even) td{
            background: #f8fafc;
        }
        tr:hover td{
            background: #eff6ff;
        }
        a{
            color: #2563eb;
            text-decoration: none;
        }
        .num{
            direction: ltr;
            text-align: left;
            white-space: nowrap;
        }
    </style></head><body>';

    if ($title !== '') {
        $html .= '<div class="title">' . e($title) . '</div>';
        $html .= '<div class="meta">تعداد ردیف‌ها: ' . e((string)$count) . '</div>';
    }

    $html .= '<table><thead><tr>';
    foreach ($headers as $h) {
        $html .= '<th>' . e((string)$h) . '</th>';
    }
    $html .= '</tr></thead><tbody>';

    foreach ($rows as $row) {
        $html .= '<tr>';
        foreach ($headers as $h) {
            $value = (string)($row[$h] ?? '');
            $trimmed = trim($value);

            $isLink = ($h === 'لینک') || preg_match('~^https?://~i', $trimmed);
            $isNumericLike = in_array($h, [
                'قیمت', 'قیمت فعلی', 'قیمت جدید', 'قیمت واحد', 'تعداد', 'تعداد فعلی', 'تعداد جدید',
                'موجودی', 'شناسه محصول', 'کد کالا', 'قیمت فعلی اهوازفون', 'قیمت خرید حسابرسی',
                'موجودی حسابرسی', 'قیمت فرنا', 'قیمت همراه تل', 'میانگین قیمت بازار',
                'قیمت پیشنهادی اعمال', 'موجودی پیشنهادی اعمال', 'شناسه محصول اهوازفون'
            ], true);

            if ($isLink && $trimmed !== '') {
                $html .= '<td><a href="' . e($trimmed) . '">' . e($trimmed) . '</a></td>';
            } else {
                $class = $isNumericLike ? ' class="num"' : '';
                $html .= '<td' . $class . '>' . e($value) . '</td>';
            }
        }
        $html .= '</tr>';
    }

    $html .= '</tbody></table></body></html>';

    file_put_contents($file, "\xEF\xBB\xBF" . $html);
}

function createXPath(string $html): DOMXPath
{
    libxml_use_internal_errors(true);
    $dom = new DOMDocument();
    $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
    $dom->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);
    libxml_clear_errors();
    return new DOMXPath($dom);
}

function nodeText(DOMXPath $xp, string $query, ?DOMNode $context = null): string
{
    $node = $xp->query($query, $context)->item(0);
    return $node ? normalizeSpaces($node->textContent) : '';
}

function nodeAttr(DOMXPath $xp, string $query, string $attr, ?DOMNode $context = null): string
{
    $node = $xp->query($query, $context)->item(0);
    return ($node instanceof DOMElement && $node->hasAttribute($attr)) ? trim($node->getAttribute($attr)) : '';
}

function nodesText(DOMXPath $xp, string $query, ?DOMNode $context = null, bool $directText = false): array
{
    $list = $xp->query($query, $context);
    $out = [];

    if ($list) {
        foreach ($list as $node) {
            $text = $directText ? getDirectText($node) : $node->textContent;
            $text = normalizeSpaces($text);
            if ($text !== '') {
                $out[] = $text;
            }
        }
    }

    return $out;
}

function getDirectText(DOMNode $node): string
{
    $parts = [];
    foreach ($node->childNodes as $child) {
        if ($child->nodeType === XML_TEXT_NODE) {
            $parts[] = $child->nodeValue;
        }
    }

    $text = trim(implode(' ', $parts));
    if ($text !== '') {
        return $text;
    }

    return trim($node->textContent);
}

function absoluteUrl(string $url, string $base): string
{
    if ($url === '') return '';
    if (preg_match('~^https?://~i', $url)) return $url;

    $parts = parse_url($base);
    if (!$parts || empty($parts['scheme']) || empty($parts['host'])) return $url;

    $origin = $parts['scheme'] . '://' . $parts['host'];

    if (str_starts_with($url, '//')) return $parts['scheme'] . ':' . $url;
    if (str_starts_with($url, '/')) return $origin . $url;

    $path = $parts['path'] ?? '/';
    $dir = rtrim(str_replace('\\', '/', dirname($path)), '/');

    return $origin . ($dir ? $dir . '/' : '/') . ltrim($url, '/');
}

function guessCategoryNameFromUrl(string $url): string
{
    $path = parse_url($url, PHP_URL_PATH);
    if (!$path) {
        return '';
    }

    $parts = array_values(array_filter(explode('/', trim($path, '/'))));
    return $parts ? end($parts) : '';
}

function extractColorFromVariantTitle(string $variantTitle): string
{
    $variantTitle = trim($variantTitle);

    if ($variantTitle === '') {
        return '-';
    }

    if (preg_match('/رنگ\s*:\s*(.+)$/u', $variantTitle, $m)) {
        return trim($m[1]);
    }

    return $variantTitle;
}

function normalizeStock($stock): string
{
    if ($stock === null || $stock === '') {
        return '';
    }

    return (string)$stock;
}

function parsePriceValue(string $text): string
{
    $text = faToEn($text);
    preg_match_all('/\d+/', $text, $m);

    if (empty($m[0])) return '';

    $value = implode('', $m[0]);
    if ($value === '') return '';

    $trimmed = ltrim($value, '0');
    return $trimmed === '' ? '0' : $trimmed;
}

function normalizePriceForSheet(string $digits): string
{
    if ($digits === '') return '';
    return number_format((float)$digits, 0, '.', ',');
}

function comparePricesDesc(string $a, string $b): int
{
    $na = (int)str_replace(',', '', faToEn($a));
    $nb = (int)str_replace(',', '', faToEn($b));
    return $nb <=> $na;
}

function faToEn(string $text): string
{
    $fa = ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹','٬','،'];
    $en = ['0','1','2','3','4','5','6','7','8','9',',',','];
    return str_replace($fa, $en, $text);
}

function rialToTomanInt($value): int
{
    $rial = (int)parsePriceValue((string)$value);
    return (int)floor($rial / 10);
}

function normalizeSpaces(string $text): string
{
    $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    $text = str_replace(["\r", "\n", "\t", "\xC2\xA0"], ' ', $text);
    $text = preg_replace('/\s+/u', ' ', trim($text));
    return $text;
}

function cleanArray(array $items): array
{
    $out = [];
    foreach ($items as $item) {
        $item = normalizeSpaces((string)$item);
        if ($item !== '') {
            $out[] = $item;
        }
    }
    return array_values(array_unique($out));
}

function uniqueByUrl(array $items): array
{
    $seen = [];
    $out = [];

    foreach ($items as $item) {
        $url = $item['url'] ?? '';
        if ($url === '' || isset($seen[$url])) continue;
        $seen[$url] = true;
        $out[] = $item;
    }

    return $out;
}

function firstNonEmpty(array $values): string
{
    foreach ($values as $v) {
        $v = normalizeSpaces((string)$v);
        if ($v !== '') return $v;
    }
    return '';
}

function mime_content_type_safe(string $filePath): string
{
    if (function_exists('mime_content_type')) {
        $type = @mime_content_type($filePath);
        if ($type) {
            return $type;
        }
    }
    return 'application/octet-stream';
}

function logLine(array $config, string $text): void
{
    file_put_contents(
        $config['runtime_dir'] . '/polling_log.txt',
        '[' . date('Y-m-d H:i:s') . '] ' . $text . PHP_EOL,
        FILE_APPEND
    );
}

function ensureDir(string $dir): void
{
    if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
        throw new RuntimeException('ایجاد پوشه ممکن نشد: ' . $dir);
    }
}

function e(string $text): string
{
    return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}