<?php

namespace BaleBot;

class BotHandler
{
    public function __construct(private readonly BaleApi $api)
    {
    }

    public function handle(array $update): void
    {
        foreach ($this->expandUpdates($update) as $item) {
            $this->handleOne($item);
        }
    }

    /** @return list<array<string, mixed>> */
    private function expandUpdates(array $update): array
    {
        if (isset($update[0]) && is_array($update[0]) && isset($update[0]['update_id'])) {
            return array_values(array_filter($update, 'is_array'));
        }

        if (isset($update['result']) && is_array($update['result'])) {
            return $this->expandUpdates($update['result']);
        }

        if (isset($update['body']) && is_array($update['body'])) {
            return $this->expandUpdates($update['body']);
        }

        return [$update];
    }

    private function handleOne(array $update): void
    {
        $callback = $update['callback_query']
            ?? $update['callbackQuery']
            ?? (isset($update['callback']) && is_array($update['callback']) ? $update['callback'] : null);

        if (is_array($callback)) {
            $this->handleCallback($callback);
            return;
        }

        if (!isset($update['message']['text'])) {
            return;
        }

        $message = $update['message'];
        $chatId = $message['chat']['id'];
        $text = trim($message['text']);

        if (Config::isAdmin($chatId) && $text === '/cancel_edit') {
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, 'ویرایش لغو شد.');
            $this->sendAdminPanel($chatId);
            return;
        }

        if (Config::isAdmin($chatId) && $this->tryAdminShortcut($chatId, $text)) {
            return;
        }

        if (Config::isAdmin($chatId) && $this->handleAdminMenuText($chatId, $text)) {
            return;
        }

        if ($this->handleAdminMessage($chatId, $text)) {
            return;
        }

        if (Config::isAdmin($chatId) && $this->handleAdminPendingInput($chatId, $text)) {
            return;
        }

        if ($this->isStartCommand($text)) {
            $this->recordBotUserFromMessage($message, $text);
            if (!Lang::has($chatId)) {
                $this->sendLanguageSelection($chatId);
                return;
            }
            $this->sendWelcome($chatId);
            return;
        }

        if (!Lang::has($chatId)) {
            if ($this->handleLanguageSelection($chatId, $text)) {
                return;
            }
            $this->sendLanguageSelection($chatId);
            return;
        }

        if ($this->handleMenuText($chatId, $text)) {
            return;
        }

        if ($this->handleUserFlowInput($chatId, $text)) {
            return;
        }

        if ($this->handleLanguageSelection($chatId, $text)) {
            return;
        }

        $this->sendUnknown($chatId);
    }

    private function t(int|string $chatId, string $key, array $vars = []): string
    {
        return Lang::t($chatId, $key, $vars);
    }

    private function sendLanguageSelection(int|string $chatId): void
    {
        $prompt = Lang::has($chatId)
            ? $this->t($chatId, 'select_language')
            : "لطفاً زبان خود را انتخاب کنید:\nيرجى اختيار لغتك\nPlease select your language:";

        $this->api->sendMessage($chatId, $prompt, [
            'keyboard' => Lang::languageKeyboardRows(),
            'resize_keyboard' => true,
        ]);
    }

    private function handleLanguageSelection(int|string $chatId, string $text): bool
    {
        $lang = Lang::languageFromButton($text);
        if ($lang === null) {
            return false;
        }

        Lang::set($chatId, $lang);
        Database::clearUserState($chatId);
        $this->api->sendMessage($chatId, $this->t($chatId, 'language_saved'));
        $this->sendWelcome($chatId);

        return true;
    }

    private function handleCallback(array $callback): void
    {
        $data = $this->callbackData($callback);
        $chatId = $this->callbackChatId($callback);
        $messageId = $this->callbackMessageId($callback);
        if ($chatId === null || $data === '') {
            error_log('Invalid Bale callback payload: ' . json_encode($callback, JSON_UNESCAPED_UNICODE));
            return;
        }

        $this->answerCallback($callback);

        if (str_starts_with($data, 'admin:')) {
            if (!Config::isAdmin($chatId)) {
                $this->api->sendMessage($chatId, 'شما دسترسی مدیریت ندارید.');
                return;
            }

            $this->handleAdminCallback($chatId, $messageId, $data);
            return;
        }

        if (str_starts_with($data, 'order:')) {
            $this->handleOrderCallback($chatId, $data, $callback);
            return;
        }

        match ($data) {
            'menu:main' => $this->sendWelcome($chatId, $messageId),
            'menu:contact' => $this->sendContact($chatId, $messageId),
            'menu:website' => $this->sendWebsite($chatId, $messageId),
            'menu:products' => $this->sendProductsBySize($chatId, $messageId),
            'products:style' => $this->sendProductsByStyle($chatId, $messageId),
            'products:size' => $this->sendProductsBySize($chatId, $messageId),
            'products:search' => $this->startProductSearch($chatId, $messageId),
            default => $this->handleProductCallback($chatId, $messageId, $data),
        };
    }

    private function callbackChatId(array $callback): int|string|null
    {
        $candidates = [
            $callback['message']['chat']['id'] ?? null,
            $callback['message']['chat_id'] ?? null,
            $callback['chat']['id'] ?? null,
            $callback['chat_id'] ?? null,
            $callback['from']['id'] ?? null,
            $callback['from_user']['id'] ?? null,
            $callback['user']['id'] ?? null,
        ];

        foreach ($candidates as $chatId) {
            if (is_int($chatId) || is_string($chatId)) {
                return $chatId;
            }
        }

        return null;
    }

    private function callbackData(array $callback): string
    {
        $candidates = [
            $callback['data'] ?? null,
            $callback['callback_data'] ?? null,
            $callback['callbackData'] ?? null,
        ];

        foreach ($candidates as $value) {
            if (is_string($value) && $value !== '') {
                return $value;
            }
        }

        return '';
    }

    private function callbackMessageId(array $callback): int|string|null
    {
        $candidates = [
            $callback['message']['message_id'] ?? null,
            $callback['message']['messageId'] ?? null,
            $callback['message']['id'] ?? null,
            $callback['message_id'] ?? null,
            $callback['messageId'] ?? null,
        ];

        foreach ($candidates as $messageId) {
            if (is_int($messageId) || is_string($messageId)) {
                return $messageId;
            }
        }

        return null;
    }

    private function handleAdminMessage(int|string $chatId, string $text): bool
    {
        if (!str_starts_with($text, '/admin') && !str_starts_with($text, '/config_')) {
            if ($text === '/update_sizes' || $text === '/update_products') {
                if (!Config::isAdmin($chatId)) {
                    $this->api->sendMessage($chatId, 'شما دسترسی مدیریت ندارید.');
                    return true;
                }

                if ($text === '/update_sizes') {
                    $this->runUpdateSizes($chatId);
                    return true;
                }

                $this->runUpdateProducts($chatId);
                return true;
            }

            return false;
        }

        if (!Config::isAdmin($chatId)) {
            $this->api->sendMessage($chatId, 'شما دسترسی مدیریت ندارید.');
            return true;
        }

        if ($text === '/admin' || $text === '/admin help') {
            $this->sendAdminPanel($chatId);
            return true;
        }

        if ($text === '/admin reps' || $text === '/reps') {
            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        if ($text === '/admin locations' || $text === '/locations') {
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if (str_starts_with($text, '/config_get')) {
            $path = trim(substr($text, strlen('/config_get')));
            $value = $path === '' ? Config::all() : Config::get($path, '__missing__');
            if ($value === '__missing__') {
                $this->api->sendMessage($chatId, 'مسیر مورد نظر پیدا نشد.');
                return true;
            }

            $this->sendLongMessage($chatId, $this->formatAdminValue($value));
            return true;
        }

        if (str_starts_with($text, '/config_set')) {
            [$path, $rawValue] = $this->splitAdminArguments(substr($text, strlen('/config_set')));
            if ($path === null || $rawValue === null) {
                $this->api->sendMessage($chatId, "فرمت درست:\n/config_set مسیر مقدار");
                return true;
            }

            $saved = Config::set($path, $this->parseAdminValue($rawValue));
            $this->api->sendMessage($chatId, $saved ? 'تنظیمات ذخیره شد.' : 'ذخیره تنظیمات ناموفق بود.');
            return true;
        }

        if (str_starts_with($text, '/config_add')) {
            [$path, $rawValue] = $this->splitAdminArguments(substr($text, strlen('/config_add')));
            if ($path === null || $rawValue === null) {
                $this->api->sendMessage($chatId, "فرمت درست:\n/config_add مسیر مقدار");
                return true;
            }

            $saved = Config::append($path, $this->parseAdminValue($rawValue));
            $this->api->sendMessage($chatId, $saved ? 'آیتم جدید اضافه شد.' : 'اضافه کردن آیتم ناموفق بود.');
            return true;
        }

        if (str_starts_with($text, '/config_delete')) {
            [$path, $idOrIndex] = $this->splitAdminArguments(substr($text, strlen('/config_delete')));
            if ($path === null || $idOrIndex === null) {
                $this->api->sendMessage($chatId, "فرمت درست:\n/config_delete مسیر id-یا-index");
                return true;
            }

            $deleted = Config::delete($path, $idOrIndex);
            $this->api->sendMessage($chatId, $deleted ? 'آیتم حذف شد.' : 'آیتم مورد نظر پیدا نشد.');
            return true;
        }

        $this->sendAdminPanel($chatId);
        return true;
    }

    private function sendAdminPanel(int|string $chatId, int|string|null $messageId = null): void
    {
        $stats = $this->adminStats();
        $text = "پنل مدیریت\n\n";
        $text .= "وضعیت فعلی:\n";
        $text .= "• سایزها: {$stats['sizes']}\n";
        $text .= "• سبک‌ها: {$stats['styles']}\n";
        $text .= "• محصولات: {$stats['products']}\n";
        $text .= "• کاربران: {$stats['users']}\n";
        $text .= "• نمایندگان: {$stats['representatives']}\n";
        $text .= "• استان‌ها: {$stats['provinces']}\n";
        $text .= "• شهرها: {$stats['cities']}\n\n";
        $text .= "از دکمه‌های پایین صفحه استفاده کنید یا از دستورهای مدیریتی.";

        $this->clearUserFlowState($chatId);
        Database::setAdminState($chatId, 'browsing_menu');
        $this->api->sendMessage($chatId, $text, $this->adminPanelKeyboard());
    }

    private function handleAdminCallback(int|string $chatId, int|string|null $messageId, string $data): void
    {
        if ($data === 'admin:panel') {
            $this->sendAdminPanel($chatId, $messageId);
            return;
        }

        if ($data === 'admin:update_sizes') {
            $this->runUpdateSizes($chatId);
            $this->sendAdminPanel($chatId, $messageId);
            return;
        }

        if ($data === 'admin:update_products') {
            $this->runUpdateProducts($chatId);
            $this->sendAdminPanel($chatId, $messageId);
            return;
        }

        if ($data === 'admin:help') {
            $this->sendAdminHelp($chatId, $messageId);
            return;
        }

        if ($data === 'admin:admins') {
            $this->sendAdminAdminsPanel($chatId, $messageId);
            return;
        }

        if ($data === 'admin:users' || str_starts_with($data, 'admin:users:page:')) {
            $page = 0;
            if (str_starts_with($data, 'admin:users:page:')) {
                $pageValue = substr($data, strlen('admin:users:page:'));
                if (ctype_digit($pageValue)) {
                    $page = (int) $pageValue;
                }
            }

            $this->sendAdminUsersPanel($chatId, $messageId, $page);
            return;
        }

        if ($data === 'admin:admins:add') {
            Database::setAdminState($chatId, 'awaiting_admin_add');
            $this->respond(
                $chatId,
                $messageId,
                "شناسه چت (chat_id) ادمین جدید را ارسال کنید.\nبرای لغو: /cancel_edit",
                [
                    'inline_keyboard' => [
                        [['text' => '◀️ بازگشت به مدیریت ادمین‌ها', 'callback_data' => 'admin:admins']],
                    ],
                ]
            );
            return;
        }

        if (str_starts_with($data, 'admin:admins:remove:')) {
            $index = substr($data, strlen('admin:admins:remove:'));
            if (!ctype_digit($index)) {
                $this->sendAdminAdminsPanel($chatId, $messageId);
                return;
            }

            $admins = Config::listAdmins();
            if (!array_key_exists((int) $index, $admins)) {
                $this->respond($chatId, $messageId, 'ادمین مورد نظر پیدا نشد.', [
                    'inline_keyboard' => [
                        [['text' => '◀️ بازگشت به مدیریت ادمین‌ها', 'callback_data' => 'admin:admins']],
                    ],
                ]);
                return;
            }

            $result = Config::removeAdmin($admins[(int) $index]);
            $message = $result === true ? 'ادمین حذف شد.' : (string) $result;
            $this->respond($chatId, $messageId, $message, [
                'inline_keyboard' => [
                    [['text' => '◀️ بازگشت به مدیریت ادمین‌ها', 'callback_data' => 'admin:admins']],
                ],
            ]);
            return;
        }

        if (str_starts_with($data, 'admin:view:')) {
            $section = substr($data, strlen('admin:view:'));
            $this->sendAdminConfigSection($chatId, $messageId, $section);
            return;
        }

        if ($data === 'admin:edit:welcome') {
            Database::setAdminState($chatId, 'awaiting_welcome_text');
            $this->respond(
                $chatId,
                $messageId,
                "متن جدید خوش‌آمد را ارسال کنید.\nبرای لغو: /cancel_edit",
                [
                    'inline_keyboard' => [
                        [['text' => '◀️ بازگشت به پنل مدیریت', 'callback_data' => 'admin:panel']],
                    ],
                ]
            );
            return;
        }

        if ($data === 'admin:edit:contact') {
            $this->sendAdminContactEditPanel($chatId);
            return;
        }

        if ($data === 'admin:edit:contact:new') {
            Database::setAdminState($chatId, 'awaiting_contact_new_field');
            $this->respond(
                $chatId,
                $messageId,
                "نام فیلد جدید را ارسال کنید. مثال: instagram\n(فقط حروف انگلیسی، عدد و _)\nبرای لغو: /cancel_edit",
                [
                    'inline_keyboard' => [
                        [['text' => '◀️ بازگشت به ویرایش تماس', 'callback_data' => 'admin:edit:contact']],
                    ],
                ]
            );
            return;
        }

        if ($data === 'admin:reps' || str_starts_with($data, 'admin:reps:page:')) {
            $page = 0;
            if (str_starts_with($data, 'admin:reps:page:')) {
                $pageValue = substr($data, strlen('admin:reps:page:'));
                if (ctype_digit($pageValue)) {
                    $page = (int) $pageValue;
                }
            }
            $this->sendAdminRepsPanel($chatId, $messageId, $page);
            return;
        }

        if ($data === 'admin:reps:add') {
            $this->sendAdminRepProvincePicker($chatId, $messageId);
            return;
        }

        if (str_starts_with($data, 'admin:rep:provpage:')) {
            $page = (int) substr($data, strlen('admin:rep:provpage:'));
            $this->sendAdminRepProvincePicker($chatId, $messageId, max(0, $page));
            return;
        }

        if (str_starts_with($data, 'admin:rep:prov:')) {
            $provinceId = (int) substr($data, strlen('admin:rep:prov:'));
            $this->sendAdminRepScopeChoice($chatId, $provinceId);
            return;
        }

        if (str_starts_with($data, 'admin:rep:citypage:')) {
            $parts = explode(':', substr($data, strlen('admin:rep:citypage:')));
            $provinceId = (int) ($parts[0] ?? 0);
            $page = (int) ($parts[1] ?? 0);
            $this->sendAdminRepCityPicker($chatId, $messageId, $provinceId, $page);
            return;
        }

        if (str_starts_with($data, 'admin:rep:provonly:')) {
            $provinceId = (int) substr($data, strlen('admin:rep:provonly:'));
            $this->startAdminRepForm($chatId, $provinceId, null);
            return;
        }

        if (str_starts_with($data, 'admin:rep:city:')) {
            $parts = explode(':', substr($data, strlen('admin:rep:city:')));
            $provinceId = (int) ($parts[0] ?? 0);
            $cityId = (int) ($parts[1] ?? 0);
            $this->startAdminRepForm($chatId, $provinceId, $cityId > 0 ? $cityId : null);
            return;
        }

        if (str_starts_with($data, 'admin:rep:del:')) {
            $repId = (int) substr($data, strlen('admin:rep:del:'));
            $deleted = $repId > 0 && Database::deleteRepresentative($repId);
            $message = $deleted ? 'نماینده حذف شد.' : 'نماینده مورد نظر پیدا نشد.';
            $this->respond($chatId, $messageId, $message, [
                'inline_keyboard' => [
                    [['text' => '◀️ بازگشت به نمایندگان', 'callback_data' => 'admin:reps']],
                ],
            ]);
            return;
        }

        if (str_starts_with($data, 'admin:edit:contact:')) {
            $field = substr($data, strlen('admin:edit:contact:'));
            if (!$this->isValidContactFieldName($field)) {
                $this->sendAdminPanel($chatId, $messageId);
                return;
            }

            Database::setAdminState($chatId, 'awaiting_contact_value', ['path' => 'contact.' . $field]);
            $this->respond(
                $chatId,
                $messageId,
                $this->contactFieldPrompt("مقدار جدید برای «{$field}» را ارسال کنید."),
                [
                    'inline_keyboard' => [
                        [['text' => '◀️ بازگشت به ویرایش تماس', 'callback_data' => 'admin:edit:contact']],
                    ],
                ]
            );
            return;
        }
    }

    private function handleAdminPendingInput(int|string $chatId, string $text): bool
    {
        $stateRow = Database::getAdminState($chatId);
        if ($stateRow === null) {
            return false;
        }

        $state = (string) ($stateRow['state'] ?? '');

        if ($this->isKnownAdminMenuText($text)) {
            return false;
        }

        if ($this->isAdminBrowsingState($state)) {
            return false;
        }

        $payload = is_array($stateRow['payload'] ?? null) ? $stateRow['payload'] : [];
        $value = trim($text);

        if ($value === '') {
            $this->api->sendMessage($chatId, "ورودی خالی است. مقدار جدید را ارسال کنید یا /cancel_edit");
            return true;
        }

        if ($state === 'awaiting_welcome_text') {
            $saved = Config::set('bot.welcome', $value);
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, $saved ? 'متن خوش‌آمد بروزرسانی شد.' : 'ذخیره متن خوش‌آمد ناموفق بود.');
            $this->sendAdminPanel($chatId);
            return true;
        }

        if ($state === 'awaiting_contact_value') {
            $path = $this->resolveContactConfigPath($payload);
            if ($path === null) {
                Database::clearAdminState($chatId);
                return false;
            }

            $saved = Config::set($path, $this->normalizeRepFieldInput($value));
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, $saved ? 'فیلد بروزرسانی شد.' : 'ذخیره فیلد ناموفق بود.');
            $this->sendAdminContactEditPanel($chatId);
            return true;
        }

        if ($state === 'awaiting_contact_new_field') {
            if (!$this->isValidContactFieldName($value)) {
                $this->api->sendMessage($chatId, "نام فیلد نامعتبر است.\nفقط حروف انگلیسی، عدد و _ مجاز است.\nمثال: instagram");
                return true;
            }

            Database::setAdminState($chatId, 'awaiting_contact_value', ['path' => 'contact.' . $value]);
            $this->api->sendMessage($chatId, $this->contactFieldPrompt("مقدار فیلد {$value} را ارسال کنید."));
            return true;
        }

        if ($state === 'awaiting_admin_add') {
            $result = Config::addAdmin($value);
            Database::clearAdminState($chatId);
            $message = $result === true ? 'ادمین جدید اضافه شد.' : (string) $result;
            $this->api->sendMessage($chatId, $message);
            $this->sendAdminAdminsPanel($chatId);
            return true;
        }

        if ($state === 'admin_rep_first_name') {
            Database::setAdminState($chatId, 'admin_rep_last_name', array_merge($payload, [
                'first_name' => $this->normalizeRepFieldInput($value),
            ]));
            $this->api->sendMessage($chatId, $this->adminRepFieldPrompt('نام خانوادگی نماینده را بنویسید.'));
            return true;
        }

        if ($state === 'admin_rep_last_name') {
            Database::setAdminState($chatId, 'admin_rep_address', array_merge($payload, [
                'last_name' => $this->normalizeRepFieldInput($value),
            ]));
            $this->api->sendMessage($chatId, $this->adminRepFieldPrompt('آدرس را بنویسید.'));
            return true;
        }

        if ($state === 'admin_rep_address') {
            Database::setAdminState($chatId, 'admin_rep_shop_name', array_merge($payload, [
                'address' => $this->normalizeRepFieldInput($value),
            ]));
            $this->api->sendMessage($chatId, $this->adminRepFieldPrompt('نام مغازه یا فروشگاه را بنویسید.'));
            return true;
        }

        if ($state === 'admin_rep_shop_name') {
            Database::setAdminState($chatId, 'admin_rep_phone', array_merge($payload, [
                'shop_name' => $this->normalizeRepFieldInput($value),
            ]));
            $this->api->sendMessage($chatId, "شماره تماس را بنویسید.\nبرای لغو: /cancel_edit");
            return true;
        }

        if ($state === 'admin_rep_phone') {
            if ($this->isEmptyRepField($value)) {
                $this->api->sendMessage($chatId, "شماره تماس الزامی است. دوباره وارد کنید.\nبرای لغو: /cancel_edit");
                return true;
            }

            $provinceId = (int) ($payload['province_id'] ?? 0);
            if ($provinceId <= 0) {
                Database::clearAdminState($chatId);
                $this->api->sendMessage($chatId, 'استان نامعتبر است. دوباره تلاش کنید.');
                return true;
            }

            $cityId = $payload['city_id'] ?? null;
            if ($cityId !== null && (int) $cityId <= 0) {
                $cityId = null;
            }

            $repData = [
                'province_id' => $provinceId,
                'city_id' => $cityId,
                'first_name' => (string) ($payload['first_name'] ?? ''),
                'last_name' => (string) ($payload['last_name'] ?? ''),
                'address' => (string) ($payload['address'] ?? ''),
                'shop_name' => (string) ($payload['shop_name'] ?? ''),
                'phone' => $this->normalizeRepFieldInput($value),
            ];

            $repId = (int) ($payload['rep_id'] ?? 0);
            if ($repId > 0) {
                $saved = Database::updateRepresentative($repId, $repData);
                Database::clearAdminState($chatId);
                $this->api->sendMessage($chatId, $saved ? '✅ نماینده بروزرسانی شد.' : 'بروزرسانی نماینده ناموفق بود.');
            } else {
                Database::addRepresentative($repData);
                Database::clearAdminState($chatId);
                $this->api->sendMessage($chatId, '✅ نماینده با موفقیت ثبت شد.');
            }

            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        if ($state === 'admin_loc_province_name') {
            if (Database::findProvinceByName($value) !== null) {
                $this->api->sendMessage($chatId, 'استانی با این نام وجود دارد. نام دیگری وارد کنید یا /cancel_edit');
                return true;
            }

            $id = Database::addProvince($value);
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, "✅ استان «{$value}» با شناسه P{$id} اضافه شد.");
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if ($state === 'admin_loc_edit_province_name') {
            $provinceId = (int) ($payload['province_id'] ?? 0);
            if ($provinceId <= 0) {
                Database::clearAdminState($chatId);
                return false;
            }

            $existing = Database::findProvinceByName($value);
            if ($existing !== null && (int) ($existing['id'] ?? 0) !== $provinceId) {
                $this->api->sendMessage($chatId, 'استانی با این نام وجود دارد. نام دیگری وارد کنید.');
                return true;
            }

            $saved = Database::updateProvince($provinceId, $value);
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, $saved ? '✅ نام استان بروزرسانی شد.' : 'بروزرسانی استان ناموفق بود.');
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if ($state === 'admin_loc_city_name') {
            $provinceId = (int) ($payload['province_id'] ?? 0);
            $cityId = Database::addCity($provinceId, $value);
            if ($cityId === false) {
                $this->api->sendMessage($chatId, 'افزودن شهر ناموفق بود. نام تکراری است یا استان نامعتبر است.');
                return true;
            }

            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, "✅ شهر «{$value}» با شناسه C{$cityId} اضافه شد.");
            $this->sendAdminLocationCitiesPanel($chatId, $provinceId);
            return true;
        }

        if ($state === 'admin_loc_edit_city_name') {
            $cityId = (int) ($payload['city_id'] ?? 0);
            $provinceId = (int) ($payload['province_id'] ?? 0);
            if ($cityId <= 0) {
                Database::clearAdminState($chatId);
                return false;
            }

            $saved = Database::updateCity($cityId, $value);
            Database::clearAdminState($chatId);
            $this->api->sendMessage($chatId, $saved ? '✅ نام شهر بروزرسانی شد.' : 'بروزرسانی شهر ناموفق بود. نام تکراری است.');
            $this->sendAdminLocationCitiesPanel($chatId, $provinceId);
            return true;
        }

        return false;
    }

    private function isAdminBrowsingState(string $state): bool
    {
        return $state === 'browsing_menu' || str_starts_with($state, 'browsing_');
    }

    private function isValidContactFieldName(string $field): bool
    {
        return $field !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $field) === 1;
    }

    private function runUpdateSizes(int|string $chatId): void
    {
        $this->api->sendMessage($chatId, 'در حال دریافت سایزها از سایت...');
        $result = SizeScraper::scrapeAndSaveSizes();
        $this->api->sendMessage($chatId, $result['message']);
    }

    private function runUpdateProducts(int|string $chatId): void
    {
        $this->api->sendMessage($chatId, 'در حال دریافت سایزها، سبک‌ها و محصولات از سایت... این عملیات ممکن است چند دقیقه طول بکشد.');
        $result = SizeScraper::scrapeAll(function (string $msg) use ($chatId) {
            $this->api->sendMessage($chatId, $msg);
        });
        $this->api->sendMessage($chatId, $result['message']);
    }

    private function sendAdminHelp(int|string $chatId, int|string|null $messageId = null): void
    {
        $text = "راهنمای مدیریت\n\n";
        $text .= "/admin یا /admin help\n";
        $text .= "/config_get [path]\n";
        $text .= "/config_set path value\n";
        $text .= "/config_add path json\n";
        $text .= "/config_delete path id-or-index\n";
        $text .= "/update_sizes\n";
        $text .= "/update_products\n\n";
        $text .= "مدیریت ادمین‌ها از پنل: دکمه «👥 مدیریت ادمین‌ها»\n";
        $text .= "مدیریت نمایندگان از پنل: دکمه «🏪 نمایندگان»\n\n";
        $text .= "نمونه‌ها:\n";
        $text .= "/config_get products.styles\n";
        $text .= "/config_set website.url https://example.com\n";
        $text .= "/config_add products.styles {\"id\":\"99\",\"name\":\"مدرن\",\"description\":\"محصولات سبک مدرن\"}\n";
        $text .= "/config_delete products.styles 99";

        Database::setAdminState($chatId, 'browsing_help');
        $this->api->sendMessage($chatId, $text, $this->adminBackKeyboard());
    }

    private function sendAdminAdminsPanel(int|string $chatId, int|string|null $messageId = null): void
    {
        $admins = Config::listAdmins();
        $currentId = (string) $chatId;

        $text = "مدیریت ادمین‌ها\n\n";
        if ($admins === []) {
            $text .= "هیچ ادمینی ثبت نشده است.\n";
        } else {
            $text .= "ادمین‌های فعلی:\n";
            foreach ($admins as $index => $adminId) {
                $label = (string) $adminId;
                if ((string) $adminId === $currentId) {
                    $label .= ' (شما)';
                }
                $text .= "• {$label}\n";
            }
        }

        $text .= "\nبرای حذف، دکمه 🗑 کنار شناسه را بزنید.";

        $rows = [];
        foreach ($admins as $adminId) {
            $label = '🗑 ' . (string) $adminId;
            if ((string) $adminId === $currentId) {
                $label .= ' (شما)';
            }
            $rows[] = [['text' => $label]];
        }

        $rows[] = [['text' => '➕ افزودن ادمین']];
        $rows[] = [['text' => '◀️ بازگشت به پنل مدیریت']];

        Database::setAdminState($chatId, 'browsing_admins');
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function sendAdminConfigSection(int|string $chatId, int|string|null $messageId, string $section): void
    {
        $map = [
            'products' => 'products',
            'contact' => 'contact',
            'website' => 'website',
        ];

        if (!isset($map[$section])) {
            $this->sendAdminPanel($chatId, $messageId);
            return;
        }

        $value = Config::get($map[$section], []);
        $text = $this->formatAdminSection($section, $value);

        Database::setAdminState($chatId, 'browsing_view', ['section' => $section]);
        $this->api->sendMessage($chatId, $text, $this->adminBackKeyboard());
    }

    private function formatAdminSection(string $section, mixed $value): string
    {
        if (!is_array($value)) {
            return "تنظیمات {$section}:\n\n" . (string) $value;
        }

        if ($section === 'products') {
            $styles = isset($value['styles']) && is_array($value['styles']) ? count($value['styles']) : 0;
            $sizes = isset($value['sizes']) && is_array($value['sizes']) ? count($value['sizes']) : 0;
            $items = isset($value['items']) && is_array($value['items']) ? count($value['items']) : 0;

            return "تنظیمات products:\n\n"
                . "• عنوان لیست: " . (string) ($value['list_title'] ?? '-') . "\n"
                . "• سبک‌ها: {$styles}\n"
                . "• سایزها: {$sizes}\n"
                . "• آیتم‌های دستی: {$items}\n\n"
                . "برای نمایش کامل:\n/config_get products";
        }

        if ($section === 'contact') {
            return $this->formatAdminContactSection();
        }

        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        if ($json === false) {
            return "تنظیمات {$section}:\n\nخطا در نمایش تنظیمات";
        }

        if (strlen($json) > 3000) {
            return "تنظیمات {$section} بزرگ است.\n\nبرای نمایش کامل:\n/config_get {$section}";
        }

        return "تنظیمات {$section}:\n\n" . $json;
    }

    private function adminStats(): array
    {
        return [
            'sizes' => Database::hasSizes() ? count(Database::getSizes()) : 0,
            'styles' => Database::hasStyles() ? count(Database::getStyles()) : 0,
            'products' => Database::hasProducts() ? Database::countProducts() : 0,
            'users' => Database::countBotUsers(),
            'representatives' => Database::countRepresentatives(),
            'provinces' => Database::countProvinces(),
            'cities' => Database::countCities(),
        ];
    }

    private const ADMIN_REPS_PER_PAGE = 8;
    private const ADMIN_REP_CITIES_PER_PAGE = 20;
    private const ADMIN_REP_PROVINCES_PER_PAGE = 20;
    private const ADMIN_LOC_PROVINCES_PER_PAGE = 16;
    private const ADMIN_LOC_CITIES_PER_PAGE = 20;
    private const ORDER_PROVINCES_PER_PAGE = 16;

    private function sendAdminRepsPanel(int|string $chatId, int|string|null $messageId = null, int $page = 0): void
    {
        $perPage = self::ADMIN_REPS_PER_PAGE;
        $total = Database::countRepresentatives();
        $maxPage = max(0, (int) ceil(max(1, $total) / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $reps = Database::listRepresentatives($perPage, $page * $perPage);

        $text = "🏪 مدیریت نمایندگان\n\n";
        $text .= "تعداد کل: {$total}\n";
        if ($total > 0) {
            $text .= 'صفحه ' . ($page + 1) . ' از ' . ($maxPage + 1) . "\n";
        }

        if ($reps !== []) {
            $text .= "\n";
            foreach ($reps as $index => $rep) {
                $text .= ($index + 1 + $page * $perPage) . '. ' . $this->formatAdminRepButtonLabel($rep) . "\n";
            }
        }

        $rows = [];
        foreach ($reps as $rep) {
            $id = (int) ($rep['id'] ?? 0);
            if ($id <= 0) {
                continue;
            }

            $rows[] = [
                ['text' => '✏️ ' . $id],
                ['text' => '🗑 ' . $id],
            ];
        }

        if ($total > $perPage) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '➕ افزودن نماینده']];
        $rows[] = [['text' => '◀️ بازگشت به پنل مدیریت']];

        Database::setAdminState($chatId, 'browsing_reps', ['page' => $page]);

        if ($this->deliverAdminReplyPanel($chatId, $text, $rows)) {
            return;
        }

        $this->api->sendMessage(
            $chatId,
            $text . "\n\nخطا در نمایش دکمه‌ها. از دستور /admin reps استفاده کنید یا دوباره تلاش کنید."
        );
    }

    private function deliverAdminReplyPanel(int|string $chatId, string $text, array $rows): bool
    {
        if ($this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows)) !== null) {
            return true;
        }

        $fallbackRows = [
            [['text' => '➕ افزودن نماینده']],
            [['text' => '◀️ بازگشت به پنل مدیریت']],
        ];

        if ($this->api->sendMessage($chatId, $text, $this->replyKeyboard($fallbackRows)) !== null) {
            return true;
        }

        return $this->api->sendMessage($chatId, $text) !== null;
    }

    private function tryAdminShortcut(int|string $chatId, string $text): bool
    {
        if ($this->isRepsAdminButton($text)) {
            $this->clearUserFlowState($chatId);
            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '🗺 استان/شهر')
            || $this->matchesAdminButton($text, '🗺 استان و شهر')) {
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if ($text === '/admin reps' || $text === '/reps') {
            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        return false;
    }

    private function sendAdminRepProvincePicker(int|string $chatId, int|string|null $messageId = null, int $page = 0): void
    {
        if (!Database::hasProvinces()) {
            IranLocationsImporter::ensureImported();
        }

        $provinces = Database::getProvinces();
        if ($provinces === []) {
            $this->api->sendMessage($chatId, 'لیست استان‌ها خالی است. ابتدا فایل‌های provinces.json و cities.json را import کنید.', $this->replyKeyboard([
                [['text' => '◀️ بازگشت به نمایندگان']],
            ]));
            Database::setAdminState($chatId, 'browsing_rep_provinces', ['page' => 0]);
            return;
        }
        $total = count($provinces);
        $perPage = self::ADMIN_REP_PROVINCES_PER_PAGE;
        $maxPage = max(0, (int) ceil(max(1, $total) / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $slice = array_slice($provinces, $page * $perPage, $perPage);

        $text = "استان نماینده را انتخاب کنید:";
        if ($total > $perPage) {
            $text .= "\nصفحه " . ($page + 1) . ' از ' . ($maxPage + 1);
        }

        $labels = array_map(
            static fn(array $province): string => (string) ($province['name'] ?? ''),
            $slice
        );
        $rows = $this->buildLabelRows($labels, 2);

        if ($total > $perPage) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '◀️ بازگشت به نمایندگان']];

        Database::setAdminState($chatId, 'browsing_rep_provinces', ['page' => $page]);
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function sendAdminRepScopeChoice(int|string $chatId, int $provinceId): void
    {
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->sendAdminRepProvincePicker($chatId);
            return;
        }

        $provinceName = (string) ($province['name'] ?? '');
        Database::setAdminState($chatId, 'browsing_rep_scope', [
            'province_id' => $provinceId,
            'province' => $provinceName,
        ]);

        $text = "استان: {$provinceName}\n\n";
        $text .= "نماینده استانی برای کل استان ثبت می‌شود.\nدر صورت نیاز می‌توانید شهر مشخص انتخاب کنید.";

        $this->api->sendMessage($chatId, $text, $this->replyKeyboard([
            [['text' => '🏛 ثبت سطح استان']],
            [['text' => '📍 انتخاب شهر']],
            [['text' => '◀️ بازگشت به استان‌ها']],
        ]));
    }

    private function sendAdminRepCityPicker(int|string $chatId, int|string|null $messageId, int $provinceId, int $page = 0): void
    {
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->sendAdminRepProvincePicker($chatId, $messageId);
            return;
        }

        $total = Database::countCitiesByProvince($provinceId);
        $perPage = self::ADMIN_REP_CITIES_PER_PAGE;
        $maxPage = max(0, (int) ceil(max(1, $total) / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $cities = Database::getCitiesByProvincePaged($provinceId, $perPage, $page * $perPage);

        $text = 'شهر نماینده را انتخاب کنید (اختیاری):' . "\n📍 " . ($province['name'] ?? '');
        if ($total > $perPage) {
            $text .= "\nصفحه " . ($page + 1) . ' از ' . ($maxPage + 1);
        }

        $labels = array_map(
            static fn(array $city): string => (string) ($city['name'] ?? ''),
            $cities
        );
        $rows = $this->buildLabelRows($labels, 2);

        if ($total > $perPage) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '◀️ بازگشت']];

        Database::setAdminState($chatId, 'browsing_rep_cities', [
            'province_id' => $provinceId,
            'province' => (string) ($province['name'] ?? ''),
            'page' => $page,
        ]);
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function startAdminRepForm(int|string $chatId, int $provinceId, ?int $cityId): void
    {
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->api->sendMessage($chatId, 'استان نامعتبر است.');
            $this->sendAdminRepProvincePicker($chatId);
            return;
        }

        if ($cityId !== null) {
            $city = Database::getCityById($cityId);
            if ($city === null || (int) ($city['province_id'] ?? 0) !== $provinceId) {
                $this->api->sendMessage($chatId, 'شهر نامعتبر است.');
                $this->sendAdminRepCityPicker($chatId, null, $provinceId, 0);
                return;
            }
            $location = ($province['name'] ?? '') . ' - ' . ($city['name'] ?? '');
        } else {
            $location = (string) ($province['name'] ?? '') . ' (سطح استان)';
        }

        Database::setAdminState($chatId, 'admin_rep_first_name', [
            'province_id' => $provinceId,
            'city_id' => $cityId,
        ]);

        $this->api->sendMessage(
            $chatId,
            "ثبت نماینده برای: {$location}\n\n" . $this->adminRepFieldPrompt('نام نماینده را بنویسید.'),
            $this->adminBackKeyboard()
        );
    }

    private function adminRepFieldPrompt(string $label): string
    {
        return $label . "\nدر صورت عدم نیاز «-» بفرستید.\nبرای لغو: /cancel_edit";
    }

    private function normalizeRepFieldInput(string $value): string
    {
        $value = trim($value);

        return $this->isEmptyRepField($value) ? '' : $value;
    }

    private function isEmptyRepField(mixed $value): bool
    {
        $value = trim((string) $value);
        if ($value === '') {
            return true;
        }

        static $placeholders = ['-', '—', '–', '_', 'ندارد', 'بدون', 'خالی'];
        if (in_array($value, $placeholders, true)) {
            return true;
        }

        return preg_match('/^[\s\-—–_]+$/u', $value) === 1;
    }

    private function startAdminRepEdit(int|string $chatId, int $repId): void
    {
        $rep = Database::getRepresentative($repId);
        if ($rep === null) {
            $this->api->sendMessage($chatId, 'نماینده مورد نظر پیدا نشد.');
            $this->sendAdminRepsPanel($chatId);
            return;
        }

        $provinceId = (int) ($rep['province_id'] ?? 0);
        $cityId = $rep['city_id'] ?? null;
        if ($cityId !== null && (int) $cityId <= 0) {
            $cityId = null;
        }

        Database::setAdminState($chatId, 'admin_rep_first_name', [
            'rep_id' => $repId,
            'province_id' => $provinceId,
            'city_id' => $cityId,
        ]);

        $location = (string) ($rep['province_name'] ?? '');
        $cityName = trim((string) ($rep['city_name'] ?? ''));
        if ($cityName !== '') {
            $location .= ' - ' . $cityName;
        }

        $currentName = trim((string) ($rep['first_name'] ?? ''));
        $this->api->sendMessage(
            $chatId,
            "ویرایش نماینده #{$repId}\n📍 {$location}\n\nنام فعلی: {$currentName}\nنام جدید را بنویسید:\nبرای لغو: /cancel_edit",
            $this->adminBackKeyboard()
        );
    }

    private function formatAdminRepButtonLabel(array $rep): string
    {
        $shop = trim((string) ($rep['shop_name'] ?? ''));
        $name = trim(trim((string) ($rep['first_name'] ?? '')) . ' ' . trim((string) ($rep['last_name'] ?? '')));
        $province = (string) ($rep['province_name'] ?? '');
        $city = trim((string) ($rep['city_name'] ?? ''));

        $location = $city !== '' ? "{$province}/{$city}" : $province;
        $title = $shop !== '' ? $shop : ($name !== '' ? $name : 'نماینده');

        $label = $title . ' — ' . $location;
        if (function_exists('mb_strlen') && mb_strlen($label, 'UTF-8') > 48) {
            return mb_substr($label, 0, 45, 'UTF-8') . '...';
        }

        return strlen($label) > 48 ? substr($label, 0, 45) . '...' : $label;
    }

    private function sendAdminLocationsPanel(int|string $chatId, int $page = 0): void
    {
        if (!Database::hasProvinces()) {
            IranLocationsImporter::ensureImported();
        }

        $perPage = self::ADMIN_LOC_PROVINCES_PER_PAGE;
        $total = Database::countProvinces();
        $maxPage = max(0, (int) ceil(max(1, $total) / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $provinces = Database::listProvincesPaged($perPage, $page * $perPage);

        $text = "🗺 مدیریت استان و شهر\n\n";
        $text .= 'استان‌ها: ' . $total . ' | شهرها: ' . Database::countCities() . "\n";
        if ($total > 0) {
            $text .= 'صفحه ' . ($page + 1) . ' از ' . ($maxPage + 1) . "\n";
        }
        $text .= "\nبرای مشاهده شهرها، نام استان را بزنید.\n";
        $text .= "ویرایش: ✏️ Pشناسه | حذف: 🗑 Pشناسه";

        if ($provinces !== []) {
            $text .= "\n";
            foreach ($provinces as $index => $province) {
                $provinceId = (int) ($province['id'] ?? 0);
                $cityCount = Database::countCitiesByProvince($provinceId);
                $text .= ($index + 1 + $page * $perPage) . '. '
                    . ($province['name'] ?? '') . " ({$cityCount} شهر) [P{$provinceId}]\n";
            }
        }

        $rows = [];
        $labels = array_map(
            static fn(array $province): string => (string) ($province['name'] ?? ''),
            $provinces
        );
        if ($labels !== []) {
            $rows = array_merge($rows, $this->buildLabelRows($labels, 2));
        }

        foreach ($provinces as $province) {
            $id = (int) ($province['id'] ?? 0);
            $rows[] = [
                ['text' => '✏️ P' . $id],
                ['text' => '🗑 P' . $id],
            ];
        }

        if ($total > $perPage) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '➕ افزودن استان']];
        $rows[] = [['text' => '🔄 بازسازی از فایل']];
        $rows[] = [['text' => '◀️ بازگشت به پنل مدیریت']];

        Database::setAdminState($chatId, 'browsing_locations', ['page' => $page]);
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function sendAdminLocationCitiesPanel(int|string $chatId, int $provinceId, int $page = 0): void
    {
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->api->sendMessage($chatId, 'استان پیدا نشد.');
            $this->sendAdminLocationsPanel($chatId);
            return;
        }

        $perPage = self::ADMIN_LOC_CITIES_PER_PAGE;
        $total = Database::countCitiesByProvince($provinceId);
        $maxPage = max(0, (int) ceil(max(1, $total) / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $cities = Database::getCitiesByProvincePaged($provinceId, $perPage, $page * $perPage);

        $text = '🏙 شهرهای استان «' . ($province['name'] ?? '') . "»\n\n";
        $text .= "تعداد شهر: {$total}\n";
        if ($total > 0) {
            $text .= 'صفحه ' . ($page + 1) . ' از ' . ($maxPage + 1) . "\n";
        }
        $text .= "\nویرایش: ✏️ Cشناسه | حذف: 🗑 Cشناسه";

        if ($cities !== []) {
            $text .= "\n";
            foreach ($cities as $index => $city) {
                $cityId = (int) ($city['id'] ?? 0);
                $text .= ($index + 1 + $page * $perPage) . '. '
                    . ($city['name'] ?? '') . " [C{$cityId}]\n";
            }
        }

        $rows = [];
        foreach ($cities as $city) {
            $id = (int) ($city['id'] ?? 0);
            $rows[] = [
                ['text' => '✏️ C' . $id],
                ['text' => '🗑 C' . $id],
            ];
        }

        if ($total > $perPage) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '➕ افزودن شهر']];
        $rows[] = [['text' => '◀️ بازگشت به استان‌ها']];

        Database::setAdminState($chatId, 'browsing_location_cities', [
            'province_id' => $provinceId,
            'province' => (string) ($province['name'] ?? ''),
            'page' => $page,
        ]);
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function startAdminProvinceEdit(int|string $chatId, int $provinceId): void
    {
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->api->sendMessage($chatId, 'استان پیدا نشد.');
            $this->sendAdminLocationsPanel($chatId);
            return;
        }

        Database::setAdminState($chatId, 'admin_loc_edit_province_name', ['province_id' => $provinceId]);
        $this->api->sendMessage(
            $chatId,
            'ویرایش استان P' . $provinceId . "\nنام فعلی: " . ($province['name'] ?? '')
            . "\n\nنام جدید را بنویسید:\nبرای لغو: /cancel_edit",
            $this->adminBackKeyboard()
        );
    }

    private function startAdminCityEdit(int|string $chatId, int $cityId): void
    {
        $city = Database::getCityById($cityId);
        if ($city === null) {
            $this->api->sendMessage($chatId, 'شهر پیدا نشد.');
            $this->sendAdminLocationsPanel($chatId);
            return;
        }

        Database::setAdminState($chatId, 'admin_loc_edit_city_name', [
            'city_id' => $cityId,
            'province_id' => (int) ($city['province_id'] ?? 0),
        ]);
        $this->api->sendMessage(
            $chatId,
            'ویرایش شهر C' . $cityId . "\nنام فعلی: " . ($city['name'] ?? '')
            . "\n\nنام جدید را بنویسید:\nبرای لغو: /cancel_edit",
            $this->adminBackKeyboard()
        );
    }

    private function isStartCommand(string $text): bool
    {
        if ($text === '/start') {
            return true;
        }

        if (!str_starts_with($text, '/start')) {
            return false;
        }

        if (strlen($text) === 6) {
            return true;
        }

        $next = $text[6] ?? '';

        return $next === ' ' || $next === '@';
    }

    private function parseStartPayload(string $text): string
    {
        $text = trim($text);
        if (!$this->isStartCommand($text)) {
            return '';
        }

        $rest = trim(substr($text, 6));
        if ($rest === '') {
            return '';
        }

        if (str_starts_with($rest, '@')) {
            $parts = preg_split('/\s+/', $rest, 2);
            $rest = trim($parts[1] ?? '');
        }

        return $rest;
    }

    private function recordBotUserFromMessage(array $message, string $text): void
    {
        $chat = is_array($message['chat'] ?? null) ? $message['chat'] : [];
        $from = is_array($message['from'] ?? null)
            ? $message['from']
            : (is_array($message['from_user'] ?? null)
                ? $message['from_user']
                : (is_array($message['user'] ?? null) ? $message['user'] : []));

        $chatId = $chat['id'] ?? $from['id'] ?? null;
        if ($chatId === null) {
            return;
        }

        Database::recordBotStart([
            'chat_id' => $chatId,
            'user_id' => $from['id'] ?? $chatId,
            'first_name' => $from['first_name'] ?? $chat['first_name'] ?? '',
            'last_name' => $from['last_name'] ?? $chat['last_name'] ?? '',
            'username' => $from['username'] ?? $chat['username'] ?? '',
            'chat_type' => $chat['type'] ?? 'private',
            'language_code' => $from['language_code'] ?? $message['language_code'] ?? '',
            'start_payload' => $this->parseStartPayload($text),
        ]);
    }

    private function formatBotUserLabel(array $user): string
    {
        $name = trim((string) ($user['first_name'] ?? '') . ' ' . (string) ($user['last_name'] ?? ''));
        if ($name === '') {
            $name = 'بدون نام';
        }

        $username = trim((string) ($user['username'] ?? ''));
        if ($username !== '') {
            $name .= ' (@' . ltrim($username, '@') . ')';
        }

        $chatId = (string) ($user['chat_id'] ?? '');
        $starts = (int) ($user['start_count'] ?? 1);

        return "{$name} — {$chatId} — {$starts} بار";
    }

    private function sendAdminUsersPanel(int|string $chatId, int|string|null $messageId = null, int $page = 0): void
    {
        $perPage = 15;
        $total = Database::countBotUsers();
        $maxPage = max(0, (int) ceil($total / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $offset = $page * $perPage;
        $users = Database::listBotUsers($perPage, $offset);

        $text = "کاربران ربات\n\n";
        $text .= "تعداد کل: {$total}\n";
        if ($total === 0) {
            $text .= "\nهنوز کاربری /start نزده است.";
        } else {
            $text .= 'صفحه ' . ($page + 1) . ' از ' . ($maxPage + 1) . "\n\n";
            foreach ($users as $index => $user) {
                $text .= ($offset + $index + 1) . '. ' . $this->formatBotUserLabel($user) . "\n";
                $firstSeen = (string) ($user['first_seen_at'] ?? '');
                $lastSeen = (string) ($user['last_seen_at'] ?? '');
                if ($firstSeen !== '' || $lastSeen !== '') {
                    $text .= "   اولین: {$firstSeen} | آخرین: {$lastSeen}\n";
                }

                $payload = trim((string) ($user['start_payload'] ?? ''));
                if ($payload !== '') {
                    $text .= "   پارامتر start: {$payload}\n";
                }
            }
        }

        $rows = [];
        if ($maxPage > 0) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => '◀️ قبلی'];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => 'بعدی ▶️'];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => '◀️ بازگشت به پنل مدیریت']];

        Database::setAdminState($chatId, 'browsing_users', ['page' => $page]);
        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function splitAdminArguments(string $input): array
    {
        $input = trim($input);
        if ($input === '' || !str_contains($input, ' ')) {
            return [null, null];
        }

        [$path, $value] = explode(' ', $input, 2);

        return [trim($path), trim($value)];
    }

    private function parseAdminValue(string $value): mixed
    {
        $decoded = json_decode($value, true);
        if (json_last_error() === JSON_ERROR_NONE) {
            return $decoded;
        }

        return $value;
    }

    private function formatAdminValue(mixed $value): string
    {
        $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

        return $json === false ? var_export($value, true) : $json;
    }

    private function sendLongMessage(int|string $chatId, string $text): void
    {
        if (function_exists('mb_strcut')) {
            $chunks = [];
            for ($offset = 0, $length = strlen($text); $offset < $length; $offset += 3500) {
                $chunks[] = mb_strcut($text, $offset, 3500, 'UTF-8');
            }
        } else {
            $chunks = str_split($text, 3500);
        }

        foreach ($chunks as $chunk) {
            $this->api->sendMessage($chatId, $chunk);
        }
    }

    private function handleProductCallback(int|string $chatId, int|string|null $messageId, string $data): void
    {
        if (str_starts_with($data, 'product_style:')) {
            $styleId = substr($data, strlen('product_style:'));
            $this->sendStyleProducts($chatId, $messageId, $styleId);
            return;
        }

        if (str_starts_with($data, 'product_size:')) {
            $sizeId = substr($data, strlen('product_size:'));
            $this->sendSizeProducts($chatId, $messageId, $sizeId);
            return;
        }

        if (str_starts_with($data, 'dbproduct:')) {
            $parts = explode(':', $data, 3);
            $productId = $parts[1] ?? '';
            $backGroup = $parts[2] ?? '';
            $this->sendDbProductDetail($chatId, $messageId, $productId, $backGroup);
            return;
        }

        if (str_starts_with($data, 'sizepage:')) {
            $parts = explode(':', $data, 3);
            $sizeId = $parts[1] ?? '';
            $page = (int) ($parts[2] ?? 0);
            $this->sendSizeProducts($chatId, $messageId, $sizeId, $page);
            return;
        }

        if (str_starts_with($data, 'stylepage:')) {
            $parts = explode(':', $data, 3);
            $styleId = $parts[1] ?? '';
            $page = (int) ($parts[2] ?? 0);
            $this->sendStyleProducts($chatId, $messageId, $styleId, $page);
            return;
        }

        if ($data === 'search:back') {
            $stateRow = Database::getUserState($chatId);
            $query = is_array($stateRow['payload'] ?? null) ? (string) ($stateRow['payload']['query'] ?? '') : '';
            if ($query !== '') {
                $this->sendSearchResults($chatId, $query, 0);
            } else {
                $this->sendProductsBySize($chatId);
            }
            return;
        }

        if (str_starts_with($data, 'searchpage:')) {
            $page = (int) substr($data, strlen('searchpage:'));
            $stateRow = Database::getUserState($chatId);
            $query = is_array($stateRow['payload'] ?? null) ? (string) ($stateRow['payload']['query'] ?? '') : '';
            if ($query === '') {
                $this->sendProductsBySize($chatId);
                return;
            }
            $this->sendSearchResults($chatId, $query, $page);
            return;
        }

        if (str_starts_with($data, 'product:')) {
            $this->sendProductDetail($chatId, substr($data, 8), $messageId);
        }
    }

    private function handleUserFlowInput(int|string $chatId, string $text): bool
    {
        if ($text === '/cancel_search') {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'search_cancelled'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        if ($text === '/cancel_order') {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'order.cancelled'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        $stateRow = Database::getUserState($chatId);
        if ($stateRow === null) {
            return false;
        }

        $state = (string) ($stateRow['state'] ?? '');
        $payload = is_array($stateRow['payload'] ?? null) ? $stateRow['payload'] : [];

        return match ($state) {
            'awaiting_product_search' => $this->handleAwaitingSearchInput($chatId, $text),
            'browsing_sizes' => $this->handleSizeSelection($chatId, $text),
            'browsing_size_products' => $this->handleSizeProductSelection($chatId, $text, $payload),
            'search_results' => $this->handleSearchResultSelection($chatId, $text, $payload),
            'viewing_product' => $this->handleViewingProductInput($chatId, $text, $payload),
            'awaiting_order_province' => $this->handleOrderProvinceInput($chatId, $text, $payload),
            'awaiting_order_city' => $this->handleOrderCityInput($chatId, $text, $payload),
            default => false,
        };
    }

    private function handleViewingProductInput(int|string $chatId, string $text, array $payload): bool
    {
        if ($text === $this->t($chatId, 'order.register')) {
            $productId = (string) ($payload['productId'] ?? '');
            $product = Database::getProduct($productId);
            if ($product === null) {
                $this->api->sendMessage($chatId, $this->t($chatId, 'product_not_found'));
                return true;
            }

            $orderPayload = [
                'productId' => $productId,
                'productName' => (string) ($product['name'] ?? ''),
                'backGroup' => (string) ($payload['backGroup'] ?? ''),
                'page' => (int) ($payload['page'] ?? 0),
                'query' => (string) ($payload['query'] ?? ''),
            ];
            Database::setUserState($chatId, 'awaiting_order_province', $orderPayload);
            $this->sendOrderProvinceSelection($chatId, $orderPayload, 0);
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            $backGroup = (string) ($payload['backGroup'] ?? '');
            if ($backGroup === 'search') {
                $query = (string) ($payload['query'] ?? '');
                $page = (int) ($payload['page'] ?? 0);
                if ($query !== '') {
                    Database::setUserState($chatId, 'search_results', ['query' => $query, 'page' => $page]);
                    $this->sendSearchResults($chatId, $query, $page);
                    return true;
                }
            }

            if ($backGroup !== '' && $backGroup !== 'search') {
                $sizeId = $backGroup;
                $page = (int) ($payload['page'] ?? 0);
                $this->sendSizeProducts($chatId, null, $sizeId, $page);
                return true;
            }

            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'back_to_main'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        return false;
    }

    private function handleAwaitingSearchInput(int|string $chatId, string $text): bool
    {
        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'back_to_main'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        return $this->handleSearchQueryInput($chatId, $text);
    }

    private function handleSearchQueryInput(int|string $chatId, string $text): bool
    {
        $query = trim($text);
        if ($query === '') {
            $this->api->sendMessage($chatId, $this->t($chatId, 'search_empty_input'));
            return true;
        }
        if (function_exists('mb_strlen') ? mb_strlen($query, 'UTF-8') < 2 : strlen($query) < 2) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'search_min_chars'));
            return true;
        }

        Database::setUserState($chatId, 'search_results', ['query' => $query, 'page' => 0]);
        $this->sendSearchResults($chatId, $query, 0);

        return true;
    }

    private function handleSizeSelection(int|string $chatId, string $text): bool
    {
        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'back_to_main'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        $sizes = Database::hasSizes()
            ? Database::getSizes()
            : Config::get('products.sizes', []);
        $group = $this->findGroupByName($sizes, $text);
        if ($group === null) {
            return false;
        }

        $this->sendSizeProducts($chatId, null, (string) $group['id'], 0);

        return true;
    }

    private function handleSizeProductSelection(int|string $chatId, string $text, array $payload): bool
    {
        $sizeId = (string) ($payload['sizeId'] ?? '');
        $page = (int) ($payload['page'] ?? 0);

        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            $this->sendProductsBySize($chatId);
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.prev')) {
            if ($page > 0) {
                $this->sendSizeProducts($chatId, null, $sizeId, $page - 1);
            }
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.next')) {
            $this->sendSizeProducts($chatId, null, $sizeId, $page + 1);
            return true;
        }

        $products = Database::getProductsBySize($sizeId);
        $slice = array_slice($products, $page * self::PRODUCTS_PER_PAGE, self::PRODUCTS_PER_PAGE);
        $product = $this->findProductByButtonLabel($slice, $text, $chatId);
        if ($product === null) {
            return false;
        }

        $this->sendDbProductDetail($chatId, null, (string) $product['id'], $sizeId);

        return true;
    }

    private function handleSearchResultSelection(int|string $chatId, string $text, array $payload): bool
    {
        $query = trim((string) ($payload['query'] ?? ''));
        $page = (int) ($payload['page'] ?? 0);

        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'back_to_main'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.search_again')) {
            $this->startProductSearch($chatId);
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.prev')) {
            if ($page > 0 && $query !== '') {
                $this->sendSearchResults($chatId, $query, $page - 1);
            }
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.next')) {
            if ($query !== '') {
                $this->sendSearchResults($chatId, $query, $page + 1);
            }
            return true;
        }

        if ($query === '') {
            return false;
        }

        $total = Database::countProductsByName($query);
        $totalPages = (int) ceil($total / self::PRODUCTS_PER_PAGE);
        $page = max(0, min($page, max(0, $totalPages - 1)));
        $slice = Database::searchProductNamesPaged(
            $query,
            self::PRODUCTS_PER_PAGE,
            $page * self::PRODUCTS_PER_PAGE
        );
        $product = $this->findProductByButtonLabel($slice, $text, $chatId);
        if ($product === null) {
            return false;
        }

        $this->sendDbProductDetail($chatId, null, (string) $product['id'], 'search');

        return true;
    }

    private function clearUserFlowState(int|string $chatId): void
    {
        Database::clearUserState($chatId);
    }

    private function startProductSearch(int|string $chatId, int|string|null $messageId = null): void
    {
        Database::setUserState($chatId, 'awaiting_product_search');

        $this->api->sendMessage(
            $chatId,
            $this->t($chatId, 'search_prompt') . "\n\n" . $this->t($chatId, 'search_cancel_hint'),
            $this->replyKeyboard([
                [['text' => $this->t($chatId, 'menu.back')]],
            ])
        );
    }

    private function sendSearchResults(int|string $chatId, string $query, int $page = 0): void
    {
        $query = trim($query);
        Database::setUserState($chatId, 'search_results', ['query' => $query, 'page' => $page]);

        $total = Database::countProductsByName($query);

        if ($total <= 0) {
            $this->api->sendMessage(
                $chatId,
                $this->t($chatId, 'search_empty'),
                $this->replyKeyboard([
                    [['text' => $this->t($chatId, 'btn.search_again')]],
                    [['text' => $this->t($chatId, 'menu.back')]],
                ])
            );
            return;
        }

        $totalPages = (int) ceil($total / self::PRODUCTS_PER_PAGE);
        $page = max(0, min($page, max(0, $totalPages - 1)));
        $slice = Database::searchProductNamesPaged(
            $query,
            self::PRODUCTS_PER_PAGE,
            $page * self::PRODUCTS_PER_PAGE
        );

        $text = '🔍 ' . $this->t($chatId, 'search_results_title') . " «{$query}» ("
            . $this->t($chatId, 'products_count', ['count' => (string) $total]) . ')';
        if ($totalPages > 1) {
            $text .= "\n" . $this->t($chatId, 'page_info', [
                'page' => (string) ($page + 1),
                'total' => (string) $totalPages,
            ]);
        }

        $labels = array_map(
            fn(array $p): string => $this->formatProductButtonLabel($chatId, (string) ($p['name'] ?? '')),
            $slice
        );
        $rows = $this->buildLabelRows($labels, 1);

        if ($totalPages > 1) {
            $navRow = [];
            if ($page > 0) {
                $navRow[] = ['text' => $this->t($chatId, 'btn.prev')];
            }
            if ($page < $totalPages - 1) {
                $navRow[] = ['text' => $this->t($chatId, 'btn.next')];
            }
            if ($navRow !== []) {
                $rows[] = $navRow;
            }
        }

        $rows[] = [['text' => $this->t($chatId, 'btn.search_again')]];
        $rows[] = [['text' => $this->t($chatId, 'menu.back')]];

        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function sendWelcome(int|string $chatId, int|string|null $messageId = null): void
    {
        $this->clearUserFlowState($chatId);

        $botName = Config::get('bot.name', 'ربات');
        $text = $this->t($chatId, 'welcome_intro', ['bot' => $botName]) . "\n\n" . $this->t($chatId, 'welcome_body');

        $this->respond($chatId, $messageId, $text, $this->mainReplyKeyboard($chatId));
    }

    private function sendCatalog(int|string $chatId, int|string|null $messageId = null): void
    {
        $url = Config::get('links.catalog', '');
        $text = $this->t($chatId, 'catalog_title') . "\n\n";
        $text .= $url !== '' ? $url : $this->t($chatId, 'catalog_missing');

        $this->respond($chatId, $messageId, $text);
    }

    private function sendTelegram(int|string $chatId, int|string|null $messageId = null): void
    {
        $url = Config::get('links.telegram', '');
        $text = $this->t($chatId, 'telegram_title') . "\n\n";
        $text .= $this->isEmptyRepField($url)
            ? $this->t($chatId, 'telegram_missing')
            : trim((string) $url);

        $this->respond($chatId, $messageId, $text);
    }

    private function sendSupport(int|string $chatId, int|string|null $messageId = null): void
    {
        $text = $this->t($chatId, 'support_title') . "\n\n";
        $text .= $this->buildContactDetailsText($chatId, false, [
            'contact.office_number',
            'contact.mobile',
            'contact.phone',
            'contact.support_bale',
            'links.telegram',
        ]);

        $this->respond($chatId, $messageId, rtrim($text));
    }

    private function sendChangeLanguage(int|string $chatId, int|string|null $messageId = null): void
    {
        $this->clearUserFlowState($chatId);
        $this->api->sendMessage(
            $chatId,
            $this->t($chatId, 'select_language'),
            [
                'keyboard' => Lang::languageKeyboardRows(),
                'resize_keyboard' => true,
            ]
        );
    }

    private function sendContact(int|string $chatId, int|string|null $messageId = null): void
    {
        $text = $this->t($chatId, 'contact_title') . "\n\n";
        $text .= $this->buildContactDetailsText($chatId, true);

        $this->respond($chatId, $messageId, rtrim($text));
    }

    private function sendWebsite(int|string $chatId, int|string|null $messageId = null): void
    {
        $w = Config::get('website');
        $text = $this->t($chatId, 'website_title') . "\n\n" . $w['description'];
        if (!empty($w['url'])) {
            $text .= "\n\n🌐 " . $w['url'];
        }

        $this->respond($chatId, $messageId, $text);
    }

    private function sendProductsList(int|string $chatId, int|string|null $messageId = null): void
    {
        $this->clearUserFlowState($chatId);

        $title = Config::get('products.list_title');

        $this->respond($chatId, $messageId, $title . "\n\n" . Config::get('products.browse_message'), [
            'inline_keyboard' => [
                [
                    ['text' => 'براساس سبک', 'callback_data' => 'products:style'],
                    ['text' => 'براساس سایز', 'callback_data' => 'products:size'],
                ],
                [
                    ['text' => Config::get('products.search_button', '🔍 جستجو'), 'callback_data' => 'products:search'],
                ],
                [
                    ['text' => Config::get('menu.back'), 'callback_data' => 'menu:main'],
                ],
            ],
        ]);
    }

    private function sendProductsByStyle(int|string $chatId, int|string|null $messageId = null): void
    {
        $styles = Database::hasStyles()
            ? Database::getStyles()
            : Config::get('products.styles', []);

        $this->sendProductGroupList(
            $chatId,
            $messageId,
            Config::get('products.style_title'),
            Config::get('products.style_message'),
            $styles,
            'product_style:',
            'menu:products',
            true
        );
    }

    private function sendProductsBySize(int|string $chatId, int|string|null $messageId = null): void
    {
        $sizes = Database::hasSizes()
            ? Database::getSizes()
            : Config::get('products.sizes', []);

        if ($sizes === []) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'empty_products'), $this->mainReplyKeyboard($chatId));
            return;
        }

        Database::setUserState($chatId, 'browsing_sizes');

        $this->api->sendMessage(
            $chatId,
            $this->t($chatId, 'size_title') . "\n\n" . $this->t($chatId, 'size_message'),
            $this->sizesReplyKeyboard($chatId)
        );
    }

    private function sendProductGroupList(
        int|string $chatId,
        int|string|null $messageId,
        string $title,
        string $message,
        array $groups,
        string $callbackPrefix,
        string $backCallback,
        bool $singleColumn = false
    ): void {
        if ($groups === []) {
            $this->respond($chatId, $messageId, Config::get('products.empty_message'), $this->subMenuKeyboard([
                [
                    ['text' => Config::get('menu.back'), 'callback_data' => $backCallback],
                ],
            ]));
            return;
        }

        $buttons = array_map(static function (array $group) use ($callbackPrefix): array {
            return [
                'text' => $group['name'],
                'callback_data' => $callbackPrefix . $group['id'],
            ];
        }, $groups);

        $rows = $singleColumn
            ? $this->singleColumnButtons($buttons)
            : $this->pairButtons($buttons);

        $rows[] = [
            ['text' => Config::get('menu.back'), 'callback_data' => $backCallback],
        ];

        $this->respond($chatId, $messageId, $title . "\n\n" . $message, [
            'inline_keyboard' => $rows,
        ]);
    }

    private function sendProductDetail(int|string $chatId, string $productId, int|string|null $messageId = null): void
    {
        $product = $this->findProduct($productId);

        if ($product === null) {
            $this->respond($chatId, $messageId, 'محصول یافت نشد.', $this->subMenuKeyboard());
            return;
        }

        $text = "📦 * " . $product['name'] . " *\n\n";
        if (!empty($product['price'])) {
            $text .= "💰 قیمت: " . $product['price'] . "\n\n";
        }
        $text .= $product['description'];

        $w = Config::get('website');
        $extra = [];
        if (!empty($w['url'])) {
            $extra[] = [
                ['text' => '🛒 سفارش از سایت', 'url' => $w['url']],
            ];
        }

        $this->respond($chatId, $messageId, $text, $this->subMenuKeyboard(array_merge($extra, [
            [
                ['text' => '🛍 لیست محصولات', 'callback_data' => 'menu:products'],
            ],
        ])));
    }

    private const PRODUCTS_PER_PAGE = 8;
    private const CITIES_PER_PAGE = 24;

    private function sendSizeProducts(int|string $chatId, int|string|null $messageId, string $sizeId, int $page = 0): void
    {
        $sizes = Database::hasSizes()
            ? Database::getSizes()
            : Config::get('products.sizes', []);
        $group = $this->findGroup($sizes, $sizeId);

        if ($group === null) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'option_not_found'), $this->sizesReplyKeyboard($chatId));
            return;
        }

        $products = Database::getProductsBySize($sizeId);

        if ($products === []) {
            $text = $group['name'] . "\n\n" . $group['description'] . "\n\n" . $this->t($chatId, 'no_products_for_size');
            $w = Config::get('website.url');
            if (!empty($w)) {
                $text .= "\n" . $this->t($chatId, 'view_on_site') . ': ' . $w;
            }
            Database::setUserState($chatId, 'browsing_sizes');
            $this->api->sendMessage($chatId, $text, $this->sizesReplyKeyboard($chatId));
            return;
        }

        $total = count($products);
        $totalPages = (int) ceil($total / self::PRODUCTS_PER_PAGE);
        $page = max(0, min($page, max(0, $totalPages - 1)));
        $slice = array_slice($products, $page * self::PRODUCTS_PER_PAGE, self::PRODUCTS_PER_PAGE);

        Database::setUserState($chatId, 'browsing_size_products', ['sizeId' => $sizeId, 'page' => $page]);

        $text = '📐 ' . $group['name'] . ' (' . $this->t($chatId, 'products_count', ['count' => (string) $total]) . ')';
        if ($totalPages > 1) {
            $text .= "\n" . $this->t($chatId, 'page_info', [
                'page' => (string) ($page + 1),
                'total' => (string) $totalPages,
            ]);
        }

        $labels = array_map(
            fn(array $p): string => $this->formatProductButtonLabel($chatId, (string) ($p['name'] ?? '')),
            $slice
        );
        $rows = $this->buildLabelRows($labels, 1);

        if ($totalPages > 1) {
            $navRow = [];
            if ($page > 0) {
                $navRow[] = ['text' => $this->t($chatId, 'btn.prev')];
            }
            if ($page < $totalPages - 1) {
                $navRow[] = ['text' => $this->t($chatId, 'btn.next')];
            }
            if ($navRow !== []) {
                $rows[] = $navRow;
            }
        }

        $rows[] = [['text' => $this->t($chatId, 'menu.back')]];

        $this->api->sendMessage($chatId, $text, $this->replyKeyboard($rows));
    }

    private function sendStyleProducts(int|string $chatId, int|string|null $messageId, string $styleId, int $page = 0): void
    {
        $styles = Database::hasStyles()
            ? Database::getStyles()
            : Config::get('products.styles', []);
        $group = $this->findGroup($styles, $styleId);

        if ($group === null) {
            $this->respond($chatId, $messageId, 'گزینه مورد نظر یافت نشد.', $this->subMenuKeyboard([
                [['text' => Config::get('menu.back'), 'callback_data' => 'products:style']],
            ]));
            return;
        }

        $products = Database::getProductsByStyle($styleId);
        $styleUrl = 'https://www.persepolistile.ir/all-products/category/' . rawurlencode($styleId);

        if ($products === []) {
            $liveProducts = SizeScraper::scrapeProductsForStyle($styleId);
            if ($liveProducts !== []) {
                Database::saveProductsForStyle($styleId, $liveProducts);
                $products = Database::getProductsByStyle($styleId);
            }
        }

        if ($products === []) {
            $text = $group['name'] . "\n\n" . $group['description'] . "\n\nمحصولی برای این سبک ثبت نشده است.";
            $extra = [
                [['text' => 'مشاهده در سایت', 'url' => $styleUrl]],
                [['text' => Config::get('menu.back'), 'callback_data' => 'products:style']],
            ];
            $this->respond($chatId, $messageId, $text, ['inline_keyboard' => $extra]);
            return;
        }

        $total = count($products);
        $totalPages = (int) ceil($total / self::PRODUCTS_PER_PAGE);
        $page = max(0, min($page, $totalPages - 1));
        $slice = array_slice($products, $page * self::PRODUCTS_PER_PAGE, self::PRODUCTS_PER_PAGE);

        $text = "🎨 " . $group['name'] . " ({$total} محصول)";
        if ($totalPages > 1) {
            $text .= "\n📄 صفحه " . ($page + 1) . " از {$totalPages}";
        }

        $rows = $this->singleColumnButtons(
            array_map(function (array $p) use ($styleId): array {
                return [
                    'text' => $this->formatProductButtonLabel($chatId, (string) ($p['name'] ?? '')),
                    'callback_data' => 'dbproduct:' . $p['id'] . ':style-' . $styleId,
                ];
            }, $slice)
        );

        if ($totalPages > 1) {
            $navRow = [];
            if ($page > 0) {
                $navRow[] = ['text' => '◀️ قبلی', 'callback_data' => "stylepage:{$styleId}:" . ($page - 1)];
            }
            if ($page < $totalPages - 1) {
                $navRow[] = ['text' => 'بعدی ▶️', 'callback_data' => "stylepage:{$styleId}:" . ($page + 1)];
            }
            $rows[] = $navRow;
        }

        $rows[] = [['text' => 'مشاهده در سایت', 'url' => $styleUrl]];
        $rows[] = [['text' => Config::get('menu.back'), 'callback_data' => 'products:style']];

        $this->respond($chatId, $messageId, $text, ['inline_keyboard' => $rows]);
    }

    private function sendDbProductDetail(int|string $chatId, int|string|null $messageId, string $productId, string $backGroup): void
    {
        $product = Database::getProduct($productId);

        if ($product === null) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'product_not_found'));
            return;
        }

        $text = $this->buildDbProductDetailText($chatId, $product);

        if (!empty($product['page_url'])) {
            $text .= "\n\n🌐 " . $product['page_url'];
        }
        if (!empty($product['catalog_url'])) {
            $text .= "\n📘 " . $product['catalog_url'];
        }

        $stateRow = Database::getUserState($chatId);
        $statePayload = is_array($stateRow['payload'] ?? null) ? $stateRow['payload'] : [];
        $detailPayload = [
            'productId' => $productId,
            'backGroup' => $backGroup,
            'page' => (int) ($statePayload['page'] ?? 0),
            'query' => (string) ($statePayload['query'] ?? ''),
        ];
        Database::setUserState($chatId, 'viewing_product', $detailPayload);

        $detailKeyboard = $this->replyKeyboard([
            [['text' => $this->t($chatId, 'order.register')]],
            [['text' => $this->t($chatId, 'menu.back')]],
        ]);

        $this->sendProductGallery($chatId, $product);
        $this->api->sendMessage($chatId, $text, $detailKeyboard);
    }

    private function handleOrderCallback(int|string $chatId, string $data, array $callback): void
    {
        if ($data === 'order:cancel') {
            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'order.cancelled'), $this->mainReplyKeyboard($chatId));
            return;
        }

        if (str_starts_with($data, 'order:start:')) {
            $productId = substr($data, strlen('order:start:'));
            $product = Database::getProduct($productId);
            if ($product === null) {
                $this->api->sendMessage($chatId, $this->t($chatId, 'product_not_found'));
                return;
            }

            $payload = [
                'productId' => $productId,
                'productName' => (string) ($product['name'] ?? ''),
            ];
            Database::setUserState($chatId, 'awaiting_order_province', $payload);
            $this->sendOrderProvinceSelection($chatId, $payload, 0);
            return;
        }

        if (str_starts_with($data, 'order:pp:')) {
            $page = (int) substr($data, strlen('order:pp:'));
            $payload = $this->orderPayloadFromState($chatId);
            if ($payload === null) {
                return;
            }
            $this->sendOrderProvinceSelection($chatId, $payload, max(0, $page));
            return;
        }

        if (str_starts_with($data, 'order:pr:')) {
            $provinceId = (int) substr($data, strlen('order:pr:'));
            $payload = $this->orderPayloadFromState($chatId);
            if ($payload === null || $provinceId <= 0) {
                return;
            }

            $province = Database::getProvinceById($provinceId);
            if ($province === null) {
                $this->api->sendMessage($chatId, $this->t($chatId, 'order.province_not_found'));
                return;
            }

            $this->continueOrderAfterProvince($chatId, array_merge($payload, [
                'provinceId' => $provinceId,
                'province' => (string) ($province['name'] ?? ''),
            ]));
            return;
        }

        if (str_starts_with($data, 'order:cp:')) {
            $parts = explode(':', substr($data, strlen('order:cp:')));
            $provinceId = (int) ($parts[0] ?? 0);
            $page = (int) ($parts[1] ?? 0);
            $payload = $this->orderPayloadFromState($chatId);
            if ($payload === null || $provinceId <= 0) {
                return;
            }

            $province = Database::getProvinceById($provinceId);
            if ($province === null) {
                $this->api->sendMessage($chatId, $this->t($chatId, 'order.province_not_found'));
                return;
            }

            $this->sendOrderCitySelection($chatId, array_merge($payload, [
                'provinceId' => $provinceId,
                'province' => (string) ($province['name'] ?? ''),
            ]), max(0, $page));
            return;
        }

        if (str_starts_with($data, 'order:ct:')) {
            $parts = explode(':', substr($data, strlen('order:ct:')));
            $provinceId = (int) ($parts[0] ?? 0);
            $cityId = (int) ($parts[1] ?? 0);
            $payload = $this->orderPayloadFromState($chatId);
            if ($payload === null || $provinceId <= 0 || $cityId <= 0) {
                return;
            }

            $city = Database::getCityById($cityId);
            if ($city === null || (int) ($city['province_id'] ?? 0) !== $provinceId) {
                $this->api->sendMessage($chatId, $this->t($chatId, 'order.city_not_found'));
                return;
            }

            $province = trim((string) ($payload['province'] ?? ''));
            if ($province === '') {
                $provinceRow = Database::getProvinceById($provinceId);
                $province = (string) ($provinceRow['name'] ?? '');
            }

            Database::clearUserState($chatId);
            $this->sendOrderRepresentatives(
                $chatId,
                (string) ($payload['productName'] ?? ''),
                $provinceId,
                $province,
                $cityId,
                (string) ($city['name'] ?? '')
            );
        }
    }

    /** @return array<string, mixed>|null */
    private function orderPayloadFromState(int|string $chatId): ?array
    {
        $stateRow = Database::getUserState($chatId);
        if ($stateRow === null) {
            return null;
        }

        $state = (string) ($stateRow['state'] ?? '');
        if (!in_array($state, ['awaiting_order_province', 'awaiting_order_city'], true)) {
            return null;
        }

        $payload = $stateRow['payload'] ?? null;

        return is_array($payload) ? $payload : null;
    }

    private function sendOrderProvinceSelection(int|string $chatId, array $payload, int $page = 0): void
    {
        if (!Database::hasProvinces()) {
            IranLocationsImporter::ensureImported();
        }

        $provinces = Database::getProvinces();
        if ($provinces === []) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'order.province_not_found'), $this->mainReplyKeyboard($chatId));
            Database::clearUserState($chatId);
            return;
        }

        $total = count($provinces);
        $perPage = self::ORDER_PROVINCES_PER_PAGE;
        $maxPage = max(0, (int) ceil($total / $perPage) - 1);
        $page = max(0, min($page, $maxPage));
        $slice = array_slice($provinces, $page * $perPage, $perPage);

        Database::setUserState($chatId, 'awaiting_order_province', array_merge($payload, ['page' => $page]));

        $text = $this->t($chatId, 'order.ask_province');
        if ($maxPage > 0) {
            $text .= "\n" . $this->t($chatId, 'page_info', [
                'page' => (string) ($page + 1),
                'total' => (string) ($maxPage + 1),
            ]);
        }

        $labels = array_map(
            static fn(array $province): string => (string) ($province['name'] ?? ''),
            $slice
        );
        $rows = $this->buildLabelRows($labels, 2);

        if ($maxPage > 0) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => $this->t($chatId, 'btn.prev')];
            }
            if ($page < $maxPage) {
                $nav[] = ['text' => $this->t($chatId, 'btn.next')];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => $this->t($chatId, 'menu.back')]];

        $this->api->sendMessage($chatId, $text . "\n\n/cancel_order", $this->replyKeyboard($rows));
    }

    private function sendOrderCitySelection(int|string $chatId, array $payload, int $page = 0): void
    {
        $provinceId = (int) ($payload['provinceId'] ?? 0);
        $province = Database::getProvinceById($provinceId);
        if ($province === null) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'order.province_not_found'));
            $this->sendOrderProvinceSelection($chatId, [
                'productId' => (string) ($payload['productId'] ?? ''),
                'productName' => (string) ($payload['productName'] ?? ''),
            ]);
            return;
        }

        $total = Database::countCitiesWithRepresentativesByProvince($provinceId);
        if ($total <= 0) {
            $this->api->sendMessage($chatId, $this->t($chatId, 'order.city_not_found'), $this->replyKeyboard([
                [['text' => $this->t($chatId, 'menu.back')]],
            ]));
            return;
        }

        $totalPages = (int) ceil($total / self::CITIES_PER_PAGE);
        $page = max(0, min($page, max(0, $totalPages - 1)));
        $cities = Database::getCitiesWithRepresentativesByProvincePaged(
            $provinceId,
            self::CITIES_PER_PAGE,
            $page * self::CITIES_PER_PAGE
        );

        Database::setUserState($chatId, 'awaiting_order_city', array_merge($payload, [
            'provinceId' => $provinceId,
            'province' => (string) ($province['name'] ?? ''),
            'page' => $page,
        ]));

        $text = $this->t($chatId, 'order.ask_city_with_rep') . "\n📍 " . ($province['name'] ?? '');
        if ($totalPages > 1) {
            $text .= "\n" . $this->t($chatId, 'page_info', [
                'page' => (string) ($page + 1),
                'total' => (string) $totalPages,
            ]);
        }

        $labels = array_map(
            static fn(array $city): string => (string) ($city['name'] ?? ''),
            $cities
        );
        $rows = $this->buildLabelRows($labels, 2);

        if ($totalPages > 1) {
            $nav = [];
            if ($page > 0) {
                $nav[] = ['text' => $this->t($chatId, 'btn.prev')];
            }
            if ($page < $totalPages - 1) {
                $nav[] = ['text' => $this->t($chatId, 'btn.next')];
            }
            if ($nav !== []) {
                $rows[] = $nav;
            }
        }

        $rows[] = [['text' => $this->t($chatId, 'menu.back')]];

        $this->api->sendMessage($chatId, $text . "\n\n/cancel_order", $this->replyKeyboard($rows));
    }

    private function handleOrderProvinceInput(int|string $chatId, string $text, array $payload): bool
    {
        $page = (int) ($payload['page'] ?? 0);

        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            $productId = (string) ($payload['productId'] ?? '');
            if ($productId !== '') {
                $this->sendDbProductDetail($chatId, null, $productId, (string) ($payload['backGroup'] ?? ''));
                return true;
            }

            Database::clearUserState($chatId);
            $this->api->sendMessage($chatId, $this->t($chatId, 'back_to_main'), $this->mainReplyKeyboard($chatId));
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.prev')) {
            if ($page > 0) {
                $this->sendOrderProvinceSelection($chatId, $payload, $page - 1);
            }
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.next')) {
            $this->sendOrderProvinceSelection($chatId, $payload, $page + 1);
            return true;
        }

        $province = Database::findProvinceByName(trim($text));
        if ($province === null) {
            return false;
        }

        $this->continueOrderAfterProvince($chatId, array_merge($payload, [
            'provinceId' => (int) ($province['id'] ?? 0),
            'province' => (string) ($province['name'] ?? ''),
        ]));

        return true;
    }

    private function continueOrderAfterProvince(int|string $chatId, array $payload): void
    {
        $provinceId = (int) ($payload['provinceId'] ?? 0);
        $provinceName = (string) ($payload['province'] ?? '');
        $productName = (string) ($payload['productName'] ?? '');

        if (Database::countCitiesWithRepresentativesByProvince($provinceId) > 0) {
            $this->sendOrderCitySelection($chatId, $payload, 0);
            return;
        }

        if (Database::findRepresentativesByProvince($provinceId) === []) {
            Database::clearUserState($chatId);
            $this->sendOrderNoRepresentativeMessage($chatId, $productName, $provinceName, '');
            return;
        }

        Database::clearUserState($chatId);
        $this->sendOrderRepresentatives($chatId, $productName, $provinceId, $provinceName, 0, '');
    }

    private function handleOrderCityInput(int|string $chatId, string $text, array $payload): bool
    {
        $provinceId = (int) ($payload['provinceId'] ?? 0);
        $page = (int) ($payload['page'] ?? 0);

        if (Lang::isFlowButton($chatId, $text, 'menu.back')) {
            $this->sendOrderProvinceSelection($chatId, $payload, (int) ($payload['page'] ?? 0));
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.prev')) {
            if ($page > 0) {
                $this->sendOrderCitySelection($chatId, $payload, $page - 1);
            }
            return true;
        }

        if (Lang::isFlowButton($chatId, $text, 'btn.next')) {
            $this->sendOrderCitySelection($chatId, $payload, $page + 1);
            return true;
        }

        $city = Database::findCityWithRepresentativeByNameInProvince($provinceId, trim($text));
        if ($city === null) {
            return false;
        }

        $province = trim((string) ($payload['province'] ?? ''));
        $productName = trim((string) ($payload['productName'] ?? ''));
        $cityName = (string) ($city['name'] ?? '');

        Database::clearUserState($chatId);
        $this->sendOrderRepresentatives(
            $chatId,
            $productName,
            $provinceId,
            $province,
            (int) ($city['id'] ?? 0),
            $cityName
        );

        return true;
    }

    private function sendOrderRepresentatives(
        int|string $chatId,
        string $productName,
        int $provinceId,
        string $province,
        int $cityId,
        string $city
    ): void {
        $result = RepresentativeLookup::findByIds($provinceId, $cityId);

        $text = '📦 ' . $this->t($chatId, 'order.product') . ': ' . $productName . "\n\n";

        if ($result['level'] === 'city') {
            $text .= $this->t($chatId, 'order.found_city', ['city' => $city]) . "\n\n";
            $text .= $this->formatRepresentativeList($chatId, $result['items']);
        } elseif ($result['level'] === 'province') {
            if ($result['city_tried'] && $city !== '') {
                $text .= $this->t($chatId, 'order.found_province_fallback', [
                    'city' => $city,
                    'province' => $province,
                ]) . "\n\n";
            } else {
                $text .= $this->t($chatId, 'order.found_province', ['province' => $province]) . "\n\n";
            }
            $text .= $this->formatRepresentativeList($chatId, $result['items']);
        } else {
            $this->sendOrderNoRepresentativeMessage($chatId, $productName, $province, $city);
            return;
        }

        $this->api->sendMessage($chatId, trim($text), $this->mainReplyKeyboard($chatId));
    }

    private function sendOrderNoRepresentativeMessage(
        int|string $chatId,
        string $productName,
        string $province,
        string $city
    ): void {
        $text = '📦 ' . $this->t($chatId, 'order.product') . ': ' . $productName . "\n\n";

        if ($city !== '') {
            $text .= $this->t($chatId, 'order.not_found', [
                'city' => $city,
                'province' => $province,
            ]) . "\n\n";
        } else {
            $text .= $this->t($chatId, 'order.no_rep_in_province', ['province' => $province]) . "\n\n";
        }

        $text .= $this->t($chatId, 'order.contact_fallback') . "\n";
        $text .= $this->buildContactDetailsText($chatId, false);

        $this->api->sendMessage($chatId, trim($text), $this->mainReplyKeyboard($chatId));
    }

    /** @param array<int, array<string, mixed>> $reps */
    private function formatRepresentativeList(int|string $chatId, array $reps): string
    {
        $blocks = [];
        foreach ($reps as $index => $rep) {
            $block = $this->formatRepresentativeBlock($chatId, $rep);
            if ($block === '') {
                continue;
            }

            if (count($reps) > 1) {
                $blocks[] = ($index + 1) . '. ' . $block;
            } else {
                $blocks[] = $block;
            }
        }

        return implode("\n\n", $blocks);
    }

    private function formatRepresentativeBlock(int|string $chatId, array $rep): string
    {
        $lines = [];

        $firstName = $this->isEmptyRepField($rep['first_name'] ?? '') ? '' : trim((string) ($rep['first_name'] ?? ''));
        $lastName = $this->isEmptyRepField($rep['last_name'] ?? '') ? '' : trim((string) ($rep['last_name'] ?? ''));
        $fullName = trim($firstName . ' ' . $lastName);
        if (!$this->isEmptyRepField($fullName)) {
            $lines[] = $this->t($chatId, 'order.rep_name') . ': ' . $fullName;
        }

        $shopName = trim((string) ($rep['shop_name'] ?? ''));
        if (!$this->isEmptyRepField($shopName)) {
            $lines[] = $this->t($chatId, 'label.shop') . ': ' . $shopName;
        }

        $phone = trim((string) ($rep['phone'] ?? ''));
        if (!$this->isEmptyRepField($phone)) {
            $lines[] = $this->t($chatId, 'label.phone') . ': ' . $phone;
        }

        $address = trim((string) ($rep['address'] ?? ''));
        if (!$this->isEmptyRepField($address)) {
            $lines[] = $this->t($chatId, 'label.address') . ': ' . $address;
        }

        return implode("\n", $lines);
    }

    private function answerCallback(array $callback, ?string $text = null): void
    {
        $candidates = [
            $callback['id'] ?? null,
            $callback['callback_query_id'] ?? null,
            $callback['callbackQueryId'] ?? null,
        ];

        foreach ($candidates as $id) {
            if (is_string($id) && $id !== '') {
                $this->api->answerCallbackQuery($id, $text);
                return;
            }
            if (is_int($id)) {
                $this->api->answerCallbackQuery((string) $id, $text);
                return;
            }
        }
    }

    private function buildDbProductDetailText(int|string $chatId, array $product): string
    {
        $text = '📦 ' . ($product['name'] ?? $this->t($chatId, 'product_default')) . "\n\n";

        $description = trim((string) ($product['description'] ?? ''));
        if ($description !== '') {
            $text .= $description . "\n\n";
        }

        if (!empty($product['specs']) && is_array($product['specs'])) {
            $text .= $this->t($chatId, 'specs') . "\n";
            foreach ($product['specs'] as $key => $value) {
                $k = trim((string) $key);
                $v = trim((string) $value);
                if ($k === '' || $v === '') {
                    continue;
                }
                $text .= "• {$k}: {$v}\n";
            }
            $text .= "\n";
        }

        if (!empty($product['attributes']) && is_array($product['attributes'])) {
            $detailLines = [];
            foreach ($product['attributes'] as $title => $items) {
                $cleanTitle = trim((string) $title);
                if ($cleanTitle === '') {
                    continue;
                }

                $labels = [];
                if (is_array($items)) {
                    foreach ($items as $item) {
                        $value = trim((string) $item);
                        if ($value !== '' && !preg_match('/^https?:\/\//i', $value)) {
                            $labels[] = $value;
                        }
                    }
                }

                $labels = array_values(array_unique($labels));
                if ($labels !== []) {
                    $detailLines[] = "• {$cleanTitle}: " . implode('، ', $labels);
                }
            }

            if ($detailLines !== []) {
                $text .= $this->t($chatId, 'details') . "\n" . implode("\n", $detailLines) . "\n";
            }
        }

        return trim($text);
    }

    private function sendProductGallery(int|string $chatId, array $product, ?array $lastPhotoMarkup = null): bool
    {
        $gallery = [];

        if (!empty($product['gallery']) && is_array($product['gallery'])) {
            foreach ($product['gallery'] as $item) {
                $url = trim((string) $item);
                if ($url !== '' && preg_match('/^https?:\/\//i', $url)) {
                    $gallery[$url] = true;
                }
            }
        }

        $cover = trim((string) ($product['image_url'] ?? ''));
        if ($cover !== '' && preg_match('/^https?:\/\//i', $cover)) {
            $gallery[$cover] = true;
        }

        if ($gallery === []) {
            return false;
        }

        $urls = array_keys($gallery);
        $caption = $this->buildPhotoCaption($product);
        $lastIndex = count($urls) - 1;

        foreach ($urls as $index => $url) {
            $isLast = $index === $lastIndex;
            $sent = $this->api->sendPhoto(
                $chatId,
                $url,
                $isLast ? $caption : null,
                $isLast ? $lastPhotoMarkup : null
            );
            if ($sent === null) {
                $this->api->sendMessage($chatId, '🖼 عکس ' . ($index + 1) . "\n{$url}");
            }
        }

        return true;
    }

    private function buildPhotoCaption(array $product): string
    {
        $caption = "📦 " . trim((string) ($product['name'] ?? 'محصول'));

        if (!empty($product['specs']) && is_array($product['specs'])) {
            foreach ($product['specs'] as $key => $value) {
                $k = trim((string) $key);
                $v = trim((string) $value);
                if ($k === '' || $v === '') {
                    continue;
                }
                $caption .= "\n• {$k}: {$v}";
            }
        }

        if (function_exists('mb_strlen') && function_exists('mb_substr')) {
            if (mb_strlen($caption, 'UTF-8') > 900) {
                $caption = mb_substr($caption, 0, 900, 'UTF-8') . '...';
            }
        } elseif (strlen($caption) > 900) {
            $caption = substr($caption, 0, 900) . '...';
        }

        return $caption;
    }

    private function resolveBackCallback(string $backGroup): string
    {
        if ($backGroup === 'search') {
            return 'search:back';
        }

        if (str_starts_with($backGroup, 'style-')) {
            return 'product_style:' . substr($backGroup, strlen('style-'));
        }

        return 'product_size:' . $backGroup;
    }

    private function respond(int|string $chatId, int|string|null $messageId, string $text, ?array $replyMarkup = null): void
    {
        $hasInline = is_array($replyMarkup) && isset($replyMarkup['inline_keyboard']);
        if ($messageId !== null && !$hasInline && !isset($replyMarkup['keyboard'])) {
            $edited = $this->api->editMessageText($chatId, $messageId, $text, $replyMarkup);
            if ($edited !== null) {
                return;
            }
        }

        $this->api->sendMessage($chatId, $text, $replyMarkup);
    }

    private function sendUnknown(int|string $chatId): void
    {
        $this->api->sendMessage(
            $chatId,
            $this->t($chatId, 'unknown'),
            $this->mainReplyKeyboard($chatId)
        );
    }

    private function handleMenuText(int|string $chatId, string $text): bool
    {
        $menuKey = Lang::menuKeyFromText($text);
        if ($menuKey === null) {
            return false;
        }

        if ($menuKey === 'admin' && !Config::isAdmin($chatId)) {
            return false;
        }

        $actions = [
            'search' => fn(): mixed => $this->startProductSearch($chatId),
            'products' => fn(): mixed => $this->sendProductsBySize($chatId),
            'catalog' => fn(): mixed => $this->sendCatalog($chatId),
            'contact' => fn(): mixed => $this->sendContact($chatId),
            'telegram' => fn(): mixed => $this->sendTelegram($chatId),
            'website' => fn(): mixed => $this->sendWebsite($chatId),
            'support' => fn(): mixed => $this->sendSupport($chatId),
            'change_language' => fn(): mixed => $this->sendChangeLanguage($chatId),
            'admin' => fn(): mixed => $this->sendAdminPanel($chatId),
        ];

        $this->clearUserFlowState($chatId);
        $actions[$menuKey]();

        return true;
    }

    private function findProduct(string $id): ?array
    {
        foreach (Config::get('products.items', []) as $product) {
            if (($product['id'] ?? '') === $id) {
                return $product;
            }
        }

        return null;
    }

    private function findGroup(array $groups, string $id): ?array
    {
        foreach ($groups as $group) {
            if (($group['id'] ?? '') === $id) {
                return $group;
            }
        }

        return null;
    }

    private function findGroupByName(array $groups, string $name): ?array
    {
        foreach ($groups as $group) {
            if (($group['name'] ?? '') === $name) {
                return $group;
            }
        }

        return null;
    }

    private function findProductByButtonLabel(array $products, string $text, int|string $chatId): ?array
    {
        foreach ($products as $product) {
            $label = $this->formatProductButtonLabel($chatId, (string) ($product['name'] ?? ''));
            if ($label === $text) {
                return $product;
            }
        }

        return null;
    }

    private function replyKeyboard(array $rows): array
    {
        return [
            'keyboard' => $rows,
            'resize_keyboard' => true,
        ];
    }

    private function sizesReplyKeyboard(int|string $chatId): array
    {
        $sizes = Database::hasSizes()
            ? Database::getSizes()
            : Config::get('products.sizes', []);
        $labels = array_map(
            static fn(array $group): string => (string) ($group['name'] ?? ''),
            $sizes
        );
        $rows = $this->buildLabelRows($labels, 2);
        $rows[] = [['text' => $this->t($chatId, 'menu.back')]];

        return $this->replyKeyboard($rows);
    }

    private function buildLabelRows(array $labels, int $columns = 2): array
    {
        $rows = [];
        $row = [];

        foreach ($labels as $label) {
            if ($label === '') {
                continue;
            }
            $row[] = ['text' => $label];
            if (count($row) === $columns) {
                $rows[] = $row;
                $row = [];
            }
        }

        if ($row !== []) {
            $rows[] = $row;
        }

        return $rows;
    }

    /** منوی اصلی — Reply Keyboard (دکمه‌های زیر کادر پیام) */
    private function mainReplyKeyboard(int|string $chatId): array
    {
        $rows = [
            [
                ['text' => $this->t($chatId, 'menu.search')],
            ],
            [
                ['text' => $this->t($chatId, 'menu.products')],
            ],
            [
                ['text' => $this->t($chatId, 'menu.catalog')],
                ['text' => $this->t($chatId, 'menu.contact')],
                ['text' => $this->t($chatId, 'menu.telegram')],
            ],
            [
                ['text' => $this->t($chatId, 'menu.website')],
                ['text' => $this->t($chatId, 'menu.support')],
                ['text' => $this->t($chatId, 'menu.change_language')],
            ],
        ];

        if (Config::isAdmin($chatId)) {
            $rows[] = [
                ['text' => $this->t($chatId, 'menu.admin')],
            ];
        }

        return [
            'keyboard' => $rows,
            'resize_keyboard' => true,
        ];
    }

    /** زیرمنوها + دکمه بازگشت تمام‌عرض */
    private function subMenuKeyboard(array $extraRows = []): array
    {
        return [
            'inline_keyboard' => array_merge($extraRows, $this->backRow()),
        ];
    }

    private function backRow(): array
    {
        return [
            [
                ['text' => Config::get('menu.back'), 'callback_data' => 'menu:main'],
            ],
        ];
    }

    /** دکمه‌ها را دو به دو در هر ردیف می‌چیند (مثل BotFather) */
    private function pairButtons(array $buttons): array
    {
        $rows = [];
        $row = [];

        foreach ($buttons as $button) {
            $row[] = $button;
            if (count($row) === 2) {
                $rows[] = $row;
                $row = [];
            }
        }

        if ($row !== []) {
            $rows[] = $row;
        }

        return $rows;
    }

    /** دکمه‌ها را تک‌ستونه می‌چیند تا متن کامل‌تر نمایش داده شود. */
    private function singleColumnButtons(array $buttons): array
    {
        return array_map(static fn(array $button): array => [$button], $buttons);
    }

    private function formatProductButtonLabel(int|string $chatId, string $name): string
    {
        $name = trim((string) preg_replace('/\s+/u', ' ', $name));
        if ($name === '') {
            return $this->t($chatId, 'product_default');
        }

        return $name;
    }

    private function adminPanelKeyboard(): array
    {
        return $this->replyKeyboard([
            [
                ['text' => '🔄 بروزرسانی سایزها'],
                ['text' => '🚀 بروزرسانی کامل'],
            ],
            [
                ['text' => '👁 تنظیمات محصولات'],
                ['text' => '👁 تنظیمات تماس'],
            ],
            [
                ['text' => '🌐 تنظیمات سایت'],
                ['text' => '📝 متن خوش‌آمد'],
            ],
            [
                ['text' => '👥 مدیریت ادمین‌ها'],
                ['text' => '✏️ ویرایش تماس'],
            ],
            [
                ['text' => '🏪 نمایندگان'],
                ['text' => '🗺 استان/شهر'],
            ],
            [
                ['text' => '👤 کاربران ربات'],
            ],
            [
                ['text' => '📚 راهنمای دستورات'],
            ],
            [
                ['text' => '🏠 بازگشت به منوی اصلی'],
            ],
        ]);
    }

    private function adminBackKeyboard(): array
    {
        return $this->replyKeyboard([
            [['text' => '◀️ بازگشت به پنل مدیریت']],
        ]);
    }

    private function sendAdminContactEditPanel(int|string $chatId): void
    {
        Database::setAdminState($chatId, 'browsing_contact');

        $rows = [
            [
                ['text' => 'عنوان'],
                ['text' => 'تلفن'],
            ],
            [
                ['text' => 'موبایل'],
                ['text' => 'ایمیل'],
            ],
            [
                ['text' => 'آدرس'],
                ['text' => 'ساعات کاری'],
            ],
            [
                ['text' => 'دفتر مرکزی'],
                ['text' => 'پشتیبانی بله'],
            ],
            [
                ['text' => 'تلگرام'],
            ],
        ];

        $customLabels = array_keys($this->customContactAdminFieldMap());
        if ($customLabels !== []) {
            $rows = array_merge($rows, $this->buildLabelRows($customLabels, 2));
        }

        $rows[] = [['text' => '➕ افزودن فیلد جدید']];
        $rows[] = [['text' => '◀️ بازگشت به پنل مدیریت']];

        $this->api->sendMessage(
            $chatId,
            "فیلد موردنظر برای ویرایش را انتخاب کنید.\nبرای مخفی کردن فیلد از کاربر «-» یا فاصله بگذارید.",
            $this->replyKeyboard($rows)
        );
    }

    /** @return array<string, string> */
    private function contactAdminFieldMap(): array
    {
        return array_merge($this->standardContactAdminFieldMap(), $this->customContactAdminFieldMap());
    }

    /** @return array<string, string> */
    private function standardContactAdminFieldMap(): array
    {
        return [
            'عنوان' => 'contact.title',
            'تلفن' => 'contact.phone',
            'موبایل' => 'contact.mobile',
            'ایمیل' => 'contact.email',
            'آدرس' => 'contact.address',
            'ساعات کاری' => 'contact.hours',
            'دفتر مرکزی' => 'contact.office_number',
            'پشتیبانی بله' => 'contact.support_bale',
            'تلگرام' => 'links.telegram',
        ];
    }

    /** @return array<string, string> */
    private function customContactAdminFieldMap(): array
    {
        $knownKeys = array_map(
            static fn(string $path): string => str_starts_with($path, 'contact.') ? substr($path, 8) : '',
            array_values($this->standardContactAdminFieldMap())
        );
        $knownKeys = array_values(array_filter($knownKeys, static fn(string $key): bool => $key !== ''));

        $fields = [];
        $contact = Config::get('contact', []);
        if (!is_array($contact)) {
            return $fields;
        }

        foreach ($contact as $key => $value) {
            if (!is_string($key) || $key === '' || in_array($key, $knownKeys, true)) {
                continue;
            }

            $fields[$key] = 'contact.' . $key;
        }

        ksort($fields, SORT_NATURAL);

        return $fields;
    }

    /**
     * @return list<array{button: string, path: string, lang_key: ?string}>
     */
    private function contactDisplayFieldDefinitions(): array
    {
        $fields = [
            ['button' => 'عنوان', 'path' => 'contact.title', 'lang_key' => 'label.title'],
            ['button' => 'موبایل', 'path' => 'contact.mobile', 'lang_key' => 'label.mobile'],
            ['button' => 'تلفن', 'path' => 'contact.phone', 'lang_key' => 'label.phone'],
            ['button' => 'ایمیل', 'path' => 'contact.email', 'lang_key' => 'label.email'],
            ['button' => 'آدرس', 'path' => 'contact.address', 'lang_key' => 'label.address'],
            ['button' => 'ساعات کاری', 'path' => 'contact.hours', 'lang_key' => 'label.hours'],
            ['button' => 'دفتر مرکزی', 'path' => 'contact.office_number', 'lang_key' => 'label.office'],
            ['button' => 'پشتیبانی بله', 'path' => 'contact.support_bale', 'lang_key' => 'label.support_bale'],
            ['button' => 'تلگرام', 'path' => 'links.telegram', 'lang_key' => 'label.telegram'],
        ];

        foreach ($this->customContactAdminFieldMap() as $button => $path) {
            $fields[] = ['button' => $button, 'path' => $path, 'lang_key' => null];
        }

        return $fields;
    }

    /** @param list<string>|null $allowedPaths */
    private function buildContactDetailsText(int|string $chatId, bool $includeTitle, ?array $allowedPaths = null): string
    {
        $text = '';

        foreach ($this->contactDisplayFieldDefinitions() as $field) {
            if (!$includeTitle && $field['path'] === 'contact.title') {
                continue;
            }

            if ($allowedPaths !== null && !in_array($field['path'], $allowedPaths, true)) {
                continue;
            }

            $value = Config::get($field['path'], '');
            if ($field['lang_key'] !== null) {
                $text = $this->appendContactLine($chatId, $text, $field['lang_key'], $value);
                continue;
            }

            $text = $this->appendContactLineRaw($text, (string) $field['button'], $value);
        }

        return $text;
    }

    private function formatAdminContactSection(): string
    {
        $lines = ["تنظیمات تماس (فقط فیلدهای فعال):\n"];
        $hasValue = false;

        foreach ($this->contactDisplayFieldDefinitions() as $field) {
            $value = Config::get($field['path'], '');
            if ($this->isEmptyRepField($value)) {
                continue;
            }

            $hasValue = true;
            $lines[] = '• ' . $field['button'] . ': ' . trim((string) $value);
        }

        if (!$hasValue) {
            $lines[] = '• هیچ فیلد فعالی ثبت نشده است.';
        }

        $lines[] = "\nبرای ویرایش: ✏️ ویرایش تماس";
        $lines[] = 'برای مخفی کردن فیلد: «-» یا فاصله';

        return implode("\n", $lines);
    }

    /** @param array<string, mixed> $payload */
    private function resolveContactConfigPath(array $payload): ?string
    {
        $path = trim((string) ($payload['path'] ?? ''));
        if ($path !== '' && preg_match('/^[a-zA-Z0-9_.]+$/', $path) === 1) {
            return $path;
        }

        $field = trim((string) ($payload['field'] ?? ''));
        if ($field === '') {
            return null;
        }

        if (str_contains($field, '.')) {
            return preg_match('/^[a-zA-Z0-9_.]+$/', $field) === 1 ? $field : null;
        }

        if (!$this->isValidContactFieldName($field)) {
            return null;
        }

        return 'contact.' . $field;
    }

    private function contactFieldPrompt(string $label): string
    {
        return $label . "\nدر صورت عدم نیاز «-» یا فاصله بفرستید.\nبرای لغو: /cancel_edit";
    }

    private function appendContactLine(int|string $chatId, string $text, string $labelKey, mixed $value): string
    {
        if ($this->isEmptyRepField($value)) {
            return $text;
        }

        return $text . $this->t($chatId, $labelKey) . ': ' . trim((string) $value) . "\n";
    }

    private function appendContactLineRaw(string $text, string $label, mixed $value): string
    {
        if ($this->isEmptyRepField($value)) {
            return $text;
        }

        return $text . $label . ': ' . trim((string) $value) . "\n";
    }

    private function isAdminInputState(?array $stateRow): bool
    {
        if ($stateRow === null) {
            return false;
        }

        $state = (string) ($stateRow['state'] ?? '');

        return str_starts_with($state, 'awaiting_')
            || str_starts_with($state, 'admin_rep_')
            || str_starts_with($state, 'admin_loc_');
    }

    private function handleAdminMenuText(int|string $chatId, string $text): bool
    {
        if (!Config::isAdmin($chatId)) {
            return false;
        }

        if ($this->matchesAdminButton($text, '🏠 بازگشت به منوی اصلی')) {
            Database::clearAdminState($chatId);
            $this->sendWelcome($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '◀️ بازگشت به پنل مدیریت')) {
            $this->sendAdminPanel($chatId);
            return true;
        }

        if ($this->handleAdminMainPanelButton($chatId, $text)) {
            return true;
        }

        $stateRow = Database::getAdminState($chatId);
        if ($this->isAdminInputState($stateRow)) {
            return false;
        }

        $state = (string) ($stateRow['state'] ?? 'browsing_menu');
        $payload = is_array($stateRow['payload'] ?? null) ? $stateRow['payload'] : [];

        if ($this->matchesAdminButton($text, '◀️ قبلی')) {
            return $this->handleAdminPagination($chatId, $state, $payload, -1);
        }

        if ($this->matchesAdminButton($text, 'بعدی ▶️')) {
            return $this->handleAdminPagination($chatId, $state, $payload, 1);
        }

        return match ($state) {
            'browsing_admins' => $this->handleAdminAdminsButton($chatId, $text),
            'browsing_users' => false,
            'browsing_reps' => $this->handleAdminRepsButton($chatId, $text, $payload),
            'browsing_rep_provinces' => $this->handleAdminRepProvinceButton($chatId, $text, $payload),
            'browsing_rep_scope' => $this->handleAdminRepScopeButton($chatId, $text, $payload),
            'browsing_rep_cities' => $this->handleAdminRepCityButton($chatId, $text, $payload),
            'browsing_contact' => $this->handleAdminContactButton($chatId, $text),
            'browsing_locations' => $this->handleAdminLocationsButton($chatId, $text, $payload),
            'browsing_location_cities' => $this->handleAdminLocationCitiesButton($chatId, $text, $payload),
            default => false,
        };
    }

    private function handleAdminMainPanelButton(int|string $chatId, string $text): bool
    {
        if ($this->matchesAdminButton($text, '🔄 بروزرسانی سایزها')) {
            $this->runUpdateSizes($chatId);
            $this->sendAdminPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '🚀 بروزرسانی کامل')) {
            $this->runUpdateProducts($chatId);
            $this->sendAdminPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '👁 تنظیمات محصولات')) {
            $this->sendAdminConfigSection($chatId, null, 'products');
            return true;
        }

        if ($this->matchesAdminButton($text, '👁 تنظیمات تماس')) {
            $this->sendAdminConfigSection($chatId, null, 'contact');
            return true;
        }

        if ($this->matchesAdminButton($text, '🌐 تنظیمات سایت')) {
            $this->sendAdminConfigSection($chatId, null, 'website');
            return true;
        }

        if ($this->matchesAdminButton($text, '📝 متن خوش‌آمد')) {
            Database::setAdminState($chatId, 'awaiting_welcome_text');
            $this->api->sendMessage($chatId, "متن جدید خوش‌آمد را ارسال کنید.\nبرای لغو: /cancel_edit", $this->adminBackKeyboard());
            return true;
        }

        if ($this->matchesAdminButton($text, '✏️ ویرایش تماس')) {
            $this->sendAdminContactEditPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '👥 مدیریت ادمین‌ها')) {
            $this->sendAdminAdminsPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '👤 کاربران ربات')) {
            $this->sendAdminUsersPanel($chatId);
            return true;
        }

        if ($this->isRepsAdminButton($text)) {
            $this->clearUserFlowState($chatId);
            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '🗺 استان/شهر')
            || $this->matchesAdminButton($text, '🗺 استان و شهر')) {
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '📚 راهنمای دستورات')) {
            $this->sendAdminHelp($chatId);
            return true;
        }

        return false;
    }

    private function isKnownAdminMenuText(string $text): bool
    {
        foreach ($this->knownAdminMenuTexts() as $label) {
            if ($this->matchesAdminButton($text, $label)) {
                return true;
            }
        }

        return $this->isRepsAdminButton($text);
    }

    private function isRepsAdminButton(string $text): bool
    {
        if ($this->matchesAdminButton($text, '🏪 نمایندگان')
            || $this->matchesAdminButton($text, '🏪 مدیریت نمایندگان')) {
            return true;
        }

        $normalized = $this->normalizeAdminButtonText($text);

        return $normalized === 'نمایندگان'
            || str_contains($normalized, 'مدیریت نمایندگان')
            || str_contains($normalized, 'مدیریت نمایند');
    }

    /** @return list<string> */
    private function knownAdminMenuTexts(): array
    {
        return [
            '🔄 بروزرسانی سایزها',
            '🚀 بروزرسانی کامل',
            '👁 تنظیمات محصولات',
            '👁 تنظیمات تماس',
            '🌐 تنظیمات سایت',
            '📝 متن خوش‌آمد',
            '👥 مدیریت ادمین‌ها',
            '✏️ ویرایش تماس',
            '🏪 نمایندگان',
            '🏪 مدیریت نمایندگان',
            '🗺 استان/شهر',
            '🗺 استان و شهر',
            '👤 کاربران ربات',
            '📚 راهنمای دستورات',
            '➕ افزودن استان',
            '➕ افزودن شهر',
            '🔄 بازسازی از فایل',
            '🏠 بازگشت به منوی اصلی',
            '◀️ بازگشت به پنل مدیریت',
            '◀️ بازگشت به نمایندگان',
            '◀️ بازگشت به استان‌ها',
            '◀️ قبلی',
            'بعدی ▶️',
            '➕ افزودن نماینده',
            '➕ افزودن ادمین',
            '🏛 نماینده سطح استان',
            '🏛 ثبت سطح استان',
            '📍 انتخاب شهر',
            '◀️ بازگشت',
            'عنوان',
            'تلفن',
            'موبایل',
            'ایمیل',
            'آدرس',
            'ساعات کاری',
            'پشتیبانی بله',
            'دفتر مرکزی',
            'تلگرام',
            '➕ افزودن فیلد جدید',
        ];
    }

    private function normalizeAdminButtonText(string $text): string
    {
        $text = trim(preg_replace('/\s+/u', ' ', $text) ?? $text);

        return trim(preg_replace('/^\p{Extended_Pictographic}+\s*/u', '', $text) ?? $text);
    }

    private function matchesAdminButton(string $text, string $label): bool
    {
        $text = trim($text);
        if ($text === $label) {
            return true;
        }

        $textNorm = $this->normalizeAdminButtonText($text);
        $labelNorm = $this->normalizeAdminButtonText($label);
        if ($textNorm === $labelNorm) {
            return true;
        }

        if ($textNorm === '' || $labelNorm === '') {
            return false;
        }

        $minLen = min(
            function_exists('mb_strlen') ? mb_strlen($textNorm, 'UTF-8') : strlen($textNorm),
            function_exists('mb_strlen') ? mb_strlen($labelNorm, 'UTF-8') : strlen($labelNorm)
        );

        // بله گاهی برچسب دکمه‌های طولانی را کوتاه می‌کند
        if ($minLen >= 4 && (str_starts_with($labelNorm, $textNorm) || str_starts_with($textNorm, $labelNorm))) {
            return true;
        }

        return false;
    }

    private function handleAdminPagination(int|string $chatId, string $state, array $payload, int $delta): bool
    {
        $page = max(0, (int) ($payload['page'] ?? 0) + $delta);

        if ($state === 'browsing_users') {
            $this->sendAdminUsersPanel($chatId, null, $page);
            return true;
        }

        if ($state === 'browsing_reps') {
            $this->sendAdminRepsPanel($chatId, null, $page);
            return true;
        }

        if ($state === 'browsing_rep_provinces') {
            $this->sendAdminRepProvincePicker($chatId, null, $page);
            return true;
        }

        if ($state === 'browsing_rep_cities') {
            $this->sendAdminRepCityPicker($chatId, null, (int) ($payload['province_id'] ?? 0), $page);
            return true;
        }

        if ($state === 'browsing_locations') {
            $this->sendAdminLocationsPanel($chatId, $page);
            return true;
        }

        if ($state === 'browsing_location_cities') {
            $this->sendAdminLocationCitiesPanel($chatId, (int) ($payload['province_id'] ?? 0), $page);
            return true;
        }

        return false;
    }

    private function handleAdminLocationsButton(int|string $chatId, string $text, array $payload): bool
    {
        if ($this->matchesAdminButton($text, '➕ افزودن استان')) {
            Database::setAdminState($chatId, 'admin_loc_province_name');
            $this->api->sendMessage($chatId, "نام استان جدید را بنویسید.\nبرای لغو: /cancel_edit", $this->adminBackKeyboard());
            return true;
        }

        if ($this->matchesAdminButton($text, '🔄 بازسازی از فایل')) {
            $repCount = Database::countRepresentatives();
            $result = IranLocationsImporter::importFromFiles(true);
            $message = $result['message'];
            if ($repCount > 0) {
                $message .= "\n\nتوجه: {$repCount} نماینده ثبت‌شده دارید. پس از بازسازی، نمایندگان را بازبینی کنید.";
            }
            $this->api->sendMessage($chatId, $message);
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if (preg_match('/^✏️\s*P(\d+)$/u', $text, $matches) === 1) {
            $this->startAdminProvinceEdit($chatId, (int) ($matches[1] ?? 0));
            return true;
        }

        if (preg_match('/^🗑\s*P(\d+)$/u', $text, $matches) === 1) {
            $provinceId = (int) ($matches[1] ?? 0);
            $result = Database::deleteProvince($provinceId);
            $message = $result === true ? 'استان حذف شد.' : (string) $result;
            $this->api->sendMessage($chatId, $message);
            $this->sendAdminLocationsPanel($chatId, (int) ($payload['page'] ?? 0));
            return true;
        }

        $province = Database::findProvinceByName($text);
        if ($province !== null) {
            $this->sendAdminLocationCitiesPanel($chatId, (int) ($province['id'] ?? 0));
            return true;
        }

        return false;
    }

    private function handleAdminLocationCitiesButton(int|string $chatId, string $text, array $payload): bool
    {
        $provinceId = (int) ($payload['province_id'] ?? 0);

        if ($this->matchesAdminButton($text, '◀️ بازگشت به استان‌ها')) {
            $this->sendAdminLocationsPanel($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '➕ افزودن شهر')) {
            if ($provinceId <= 0) {
                $this->sendAdminLocationsPanel($chatId);
                return true;
            }

            Database::setAdminState($chatId, 'admin_loc_city_name', ['province_id' => $provinceId]);
            $province = Database::getProvinceById($provinceId);
            $provinceName = (string) ($province['name'] ?? '');
            $this->api->sendMessage(
                $chatId,
                "نام شهر جدید برای استان «{$provinceName}» را بنویسید.\nبرای لغو: /cancel_edit",
                $this->adminBackKeyboard()
            );
            return true;
        }

        if (preg_match('/^✏️\s*C(\d+)$/u', $text, $matches) === 1) {
            $this->startAdminCityEdit($chatId, (int) ($matches[1] ?? 0));
            return true;
        }

        if (preg_match('/^🗑\s*C(\d+)$/u', $text, $matches) === 1) {
            $cityId = (int) ($matches[1] ?? 0);
            $result = Database::deleteCity($cityId);
            $message = $result === true ? 'شهر حذف شد.' : (string) $result;
            $this->api->sendMessage($chatId, $message);
            $this->sendAdminLocationCitiesPanel($chatId, $provinceId, (int) ($payload['page'] ?? 0));
            return true;
        }

        return false;
    }

    private function handleAdminAdminsButton(int|string $chatId, string $text): bool
    {
        if ($text === '➕ افزودن ادمین') {
            Database::setAdminState($chatId, 'awaiting_admin_add');
            $this->api->sendMessage($chatId, "شناسه چت (chat_id) ادمین جدید را ارسال کنید.\nبرای لغو: /cancel_edit", $this->adminBackKeyboard());
            return true;
        }

        if (str_starts_with($text, '🗑 ')) {
            $adminId = trim(substr($text, strlen('🗑 ')));
            $adminId = preg_replace('/\s*\(شما\)\s*$/u', '', $adminId) ?? $adminId;
            $admins = Config::listAdmins();
            $index = array_search($adminId, array_map('strval', $admins), true);
            if ($index === false) {
                return false;
            }

            $result = Config::removeAdmin($admins[$index]);
            $message = $result === true ? 'ادمین حذف شد.' : (string) $result;
            $this->api->sendMessage($chatId, $message);
            $this->sendAdminAdminsPanel($chatId);
            return true;
        }

        return false;
    }

    private function handleAdminRepsButton(int|string $chatId, string $text, array $payload): bool
    {
        if ($this->matchesAdminButton($text, '➕ افزودن نماینده')) {
            $this->clearUserFlowState($chatId);
            $this->sendAdminRepProvincePicker($chatId);
            return true;
        }

        if (preg_match('/^✏️\s*(\d+)$/u', $text, $matches) === 1) {
            $this->startAdminRepEdit($chatId, (int) ($matches[1] ?? 0));
            return true;
        }

        if (preg_match('/^🗑\s*(\d+)$/u', $text, $matches) === 1) {
            $repId = (int) ($matches[1] ?? 0);
            $deleted = $repId > 0 && Database::deleteRepresentative($repId);
            $this->api->sendMessage($chatId, $deleted ? 'نماینده حذف شد.' : 'نماینده مورد نظر پیدا نشد.');
            $page = (int) ($payload['page'] ?? 0);
            $this->sendAdminRepsPanel($chatId, null, $page);
            return true;
        }

        return false;
    }

    private function handleAdminRepProvinceButton(int|string $chatId, string $text, array $payload): bool
    {
        if ($this->matchesAdminButton($text, '◀️ بازگشت به نمایندگان')) {
            $this->sendAdminRepsPanel($chatId);
            return true;
        }

        $province = Database::findProvinceByName($text);
        if ($province === null) {
            return false;
        }

        $this->sendAdminRepScopeChoice($chatId, (int) ($province['id'] ?? 0));
        return true;
    }

    private function handleAdminRepScopeButton(int|string $chatId, string $text, array $payload): bool
    {
        $provinceId = (int) ($payload['province_id'] ?? 0);

        if ($this->matchesAdminButton($text, '◀️ بازگشت به استان‌ها')) {
            $this->sendAdminRepProvincePicker($chatId);
            return true;
        }

        if ($this->matchesAdminButton($text, '🏛 ثبت سطح استان')
            || $this->matchesAdminButton($text, '🏛 نماینده سطح استان')) {
            $this->startAdminRepForm($chatId, $provinceId, null);
            return true;
        }

        if ($this->matchesAdminButton($text, '📍 انتخاب شهر')) {
            $this->sendAdminRepCityPicker($chatId, null, $provinceId);
            return true;
        }

        return false;
    }

    private function handleAdminRepCityButton(int|string $chatId, string $text, array $payload): bool
    {
        $provinceId = (int) ($payload['province_id'] ?? 0);

        if ($this->matchesAdminButton($text, '◀️ بازگشت')
            || $this->matchesAdminButton($text, '◀️ بازگشت به استان‌ها')) {
            $this->sendAdminRepScopeChoice($chatId, $provinceId);
            return true;
        }

        $city = Database::findCityByNameInProvince($provinceId, $text);
        if ($city === null) {
            return false;
        }

        $this->startAdminRepForm($chatId, $provinceId, (int) ($city['id'] ?? 0));
        return true;
    }

    private function handleAdminContactButton(int|string $chatId, string $text): bool
    {
        $fieldMap = $this->contactAdminFieldMap();

        if ($text === '➕ افزودن فیلد جدید') {
            Database::setAdminState($chatId, 'awaiting_contact_new_field');
            $this->api->sendMessage(
                $chatId,
                "نام فیلد جدید را ارسال کنید. مثال: instagram\n(فقط حروف انگلیسی، عدد و _)\nبرای لغو: /cancel_edit",
                $this->adminBackKeyboard()
            );
            return true;
        }

        if (!isset($fieldMap[$text])) {
            return false;
        }

        $path = $fieldMap[$text];
        Database::setAdminState($chatId, 'awaiting_contact_value', ['path' => $path]);
        $this->api->sendMessage(
            $chatId,
            $this->contactFieldPrompt("مقدار جدید برای «{$text}» را ارسال کنید."),
            $this->adminBackKeyboard()
        );

        return true;
    }
}
