я тебе высылаю полный проект своего телегра бота, предложи мне идеи по улучшению, модернизации, улучшения структуры или еше чего-то и пришли мне полностьбю гтовый испраленный код


# /main.py

import asyncio
import logging
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from config import TELEGRAM_TOKEN
from bot.handlers import register_handlers
from bot.utils import on_startup
from aiogram.client.bot import DefaultBotProperties

# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

async def main():
    bot = Bot(token=TELEGRAM_TOKEN, default=DefaultBotProperties(parse_mode='HTML'))
    storage = MemoryStorage()
    dp = Dispatcher(storage=storage)

    # Регистрация обработчиков
    register_handlers(dp)

    # Регистрация функции on_startup
    dp.startup.register(on_startup)

    try:
        logger.info("Запуск бота...")
        await dp.start_polling(bot)
    finally:
        await bot.close()

if __name__ == "__main__":
    asyncio.run(main())
	
	
	
	
	
	
	# config.py

import os
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("API_KEY")
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
ADMIN_CHAT_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))









# bot/api.py

import aiohttp
import logging
import json
from typing import Optional, Dict

logger = logging.getLogger(__name__)

BASE_URL = "https://smshub.org/stubs/handler_api.php"

class SmsHubAPI:
    def __init__(self, api_key: str):
        self.api_key = api_key

    async def get_balance(self) -> Optional[str]:
        """Запрос баланса с заданной валютой (RUB)."""
        logger.info("Запрос баланса с валютой: 643")
        params = {
            "api_key": self.api_key,
            "action": "getBalance",
            "currency": "643"
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                if response.status == 200:
                    text = await response.text()
                    logger.info(f"Ответ баланса: {text}")
                    return text
                else:
                    logger.error(f"Ошибка при запросе баланса: {response.status} - {text}")
                    return None

    async def get_number(self, service: str, country: str = "0", operator: Optional[str] = None, max_price: Optional[float] = None) -> Optional[str]:
        """Получение номера для указанного сервиса и страны."""
        logger.info(f"Запрос номера для сервиса: {service}, страна: {country}, оператор: {operator}, макс. цена: {max_price}, валюта: 643")
        params = {
            "api_key": self.api_key,
            "action": "getNumber",
            "service": service,
            "country": country,
            "currency": "643"
        }
        if operator and operator != "any":
            params["operator"] = operator
        if max_price:
            params["maxPrice"] = max_price

        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ номера: {text}")
                    return text
                logger.error(f"Ошибка при запросе номера: {response.status} - {text}")
                return None

    async def get_price_for_service(self, service: str, country: str = "0") -> Optional[Dict]:
        """Получение цен для указанного сервиса."""
        logger.info(f"Запрос цен для сервиса: {service}, страна: {country}, валюта: 643")
        params = {
            "api_key": self.api_key,
            "action": "getPrices",
            "service": service,
            "country": country,
            "currency": "643"
        }

        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                response_text = await response.text()
                if response.status == 200:
                    try:
                        prices_data = json.loads(response_text)
                        logger.info(f"Цены для сервиса {service}: {prices_data}")
                        country_data = prices_data.get(country)
                        if not country_data:
                            logger.error(f"Страна '{country}' отсутствует в данных: {prices_data}")
                            return None
                        service_data = country_data.get(service)
                        if not service_data:
                            logger.error(f"Сервис '{service}' отсутствует среди доступных для страны '{country}'. Данные: {country_data}")
                            return None
                        return service_data  # Возвращает словарь { "Цена": Количество }
                    except json.JSONDecodeError:
                        logger.error(f"Ошибка парсинга JSON: {response_text}")
                        return None
                else:
                    logger.error(f"Ошибка при запросе цен: {response.status} - {response_text}")
                    return None

    async def get_numbers_status(self, country: str = "0", operator: Optional[str] = None) -> Optional[Dict]:
        """Получение статуса номеров по стране и оператору."""
        logger.info(f"Запрос статуса номеров для страны: {country}, оператор: {operator}")
        params = {
            "api_key": self.api_key,
            "action": "getNumbersStatus",
            "country": country
        }
        if operator and operator != "any":
            params["operator"] = operator
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                response_text = await response.text()
                if response.status == 200:
                    try:
                        numbers_status = json.loads(response_text)
                        logger.info(f"Статус номеров: {numbers_status}")
                        return numbers_status  # Возвращает словарь { "SERVICE_CODE": Количество }
                    except json.JSONDecodeError:
                        logger.error(f"Ошибка парсинга JSON: {response_text}")
                        return None
                else:
                    logger.error(f"Ошибка при запросе статуса номеров: {response.status} - {response_text}")
                    return None

    async def get_status(self, activation_id: str) -> Optional[str]:
        """Получение статуса активации по ID."""
        logger.info(f"Запрос статуса для активации: {activation_id}")
        params = {
            "api_key": self.api_key,
            "action": "getStatus",
            "id": activation_id
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ статуса: {text}")
                    return text
                logger.error(f"Ошибка при запросе статуса: {response.status} - {text}")
                return None

    async def set_status(self, activation_id: str, status: str) -> Optional[str]:
        """Установка статуса для активации."""
        logger.info(f"Изменение статуса активации: {activation_id} на статус {status}")
        params = {
            "api_key": self.api_key,
            "action": "setStatus",
            "id": activation_id,
            "status": status
        }
        async with aiohttp.ClientSession() as session:
            async with session.get(BASE_URL, params=params) as response:
                text = await response.text()
                if response.status == 200:
                    logger.info(f"Ответ изменения статуса: {text}")
                    return text
                logger.error(f"Ошибка при изменении статуса: {response.status} - {text}")
                return None

    async def cancel_activation(self, activation_id: str) -> Optional[str]:
        """Отмена активации."""
        return await self.set_status(activation_id, status="8")
		
		
		
		
		
		
		
		
		
		
		
		# Словарь операторов с их отображаемыми названиями
OPERATOR_DISPLAY_NAMES = {
    "any": "Любой оператор",
    "aiva": "AIVA",
    "beeline": "Билайн",
    "center2m": "Центр 2М",
    "danycom": "Danycom"
}

# Словарь доступных стран (код страны: название)
COUNTRIES = {
    "0": "Россия",
    "1": "Украина",
    "2": "Казахстан",
    "3": "Китай"
}

# Маппинг стран и операторов с "any" на первом месте
COUNTRY_OPERATORS = {
    "0": ["any", "mts", "megafon", "beeline", "tele2", "yota", "rostelecom", "motiv", "lycamobile", "sber", "tinkoff", "vtb_mobile", "aiva", "ttk", "winmobile", "center2m", "matrix", "mcn", "danycom", "ezmobile", "simsim", "mtt", "mtt_virtual", "gazprombank_mobile"],
    "1": ["any", "kyivstar", "vodafone", "lifecell", "mts", "lycamobile", "3mob", "utel", "intertelecom"],
    "2": ["any", "kcell", "beeline", "tele2", "activ", "altel", "forte_mobile"]
}

# Словарь сервисов с кодами и названиями
SERVICE_DISPLAY_NAMES = {
    "jl": "Hopi",
    "avu": "Karos",
    "apd": "2dehands",
    "sj": "HandyPick",
    "bdt": "HOT51"
}












# bot/data.py

import json
import os
import threading
import logging
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)

# Путь к файлу для хранения данных пользователей
DATA_FILE = os.path.join(os.path.dirname(__file__), "data.json")
lock = threading.RLock()

# Загружаем данные из файла при запуске
user_data: Dict[str, Any] = {}


def load_data() -> None:
    """Загружает данные из JSON файла."""
    global user_data
    with lock:
        if os.path.exists(DATA_FILE):
            try:
                with open(DATA_FILE, "r", encoding="utf-8") as file:
                    user_data = json.load(file)
                    logger.info("Данные пользователей успешно загружены.")
            except json.JSONDecodeError:
                user_data = {}
                logger.warning("Не удалось декодировать JSON из файла. Используем пустые данные.")
        else:
            user_data = {}
            logger.info("Файл данных не найден. Используем пустые данные.")


# Вызываем load_data() при импорте модуля
load_data()


def save_data() -> None:
    """Сохраняет текущие данные в JSON файл."""
    with lock:
        with open(DATA_FILE, "w", encoding="utf-8") as file:
            json.dump(user_data, file, ensure_ascii=False, indent=4)
            logger.info("Данные пользователей успешно сохранены.")


# Методы для работы с API ключами
def set_user_api_key(user_id: int, api_key: str) -> None:
    """Сохраняет API ключ для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["api_key"] = api_key
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"API ключ для пользователя {user_id} сохранён.")


def get_user_api_key(user_id: int) -> Optional[str]:
    """Возвращает API ключ для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("api_key")


def delete_user_api_key(user_id: int) -> None:
    """Удаляет API ключ для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "api_key" in user:
            del user["api_key"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            logger.info(f"API ключ для пользователя {user_id} удалён.")
        else:
            logger.warning(f"У пользователя {user_id} нет API ключа для удаления.")


# Методы для работы с оператором
def set_user_operator(user_id: int, operator: str) -> None:
    """Сохраняет выбранный оператор для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["operator"] = operator
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Оператор '{operator}' для пользователя {user_id} сохранён.")


def get_user_operator(user_id: int) -> Optional[str]:
    """Возвращает выбранный оператор для конкретного пользователя, если он сохранен."""
    with lock:
        return user_data.get(str(user_id), {}).get("operator")


def delete_user_operator(user_id: int) -> None:
    """Удаляет выбранный оператор для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "operator" in user:
            del user["operator"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            logger.info(f"Оператор для пользователя {user_id} удалён.")
        else:
            logger.warning(f"У пользователя {user_id} нет оператора для удаления.")


# Методы для работы со страной
def set_user_country(user_id: int, country: str) -> None:
    """Сохраняет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        user["country"] = country
        user_data[str(user_id)] = user
        save_data()
        logger.info(f"Страна '{country}' для пользователя {user_id} сохранена.")


def get_user_country(user_id: int) -> Optional[str]:
    """Возвращает выбранную страну для конкретного пользователя, если она сохранена."""
    with lock:
        return user_data.get(str(user_id), {}).get("country")


def delete_user_country(user_id: int) -> None:
    """Удаляет выбранную страну для конкретного пользователя."""
    with lock:
        user = user_data.get(str(user_id), {})
        if "country" in user:
            del user["country"]
            if not user:
                del user_data[str(user_id)]
            save_data()
            logger.info(f"Страна для пользователя {user_id} удалена.")
        else:
            logger.warning(f"У пользователя {user_id} нет страны для удаления.")
			
			
			
			
			
			
			
			
			
			
			
			
			
			
			# bot/utils.py

import logging
import time
import aiohttp
from aiogram import Bot, types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram import Dispatcher
from typing import Optional

logger = logging.getLogger(__name__)

EXCHANGE_RATE_API_URL = "https://api.exchangerate-api.com/v4/latest/USD"
USD_TO_RUB_RATE_TTL = 3600  # Кэшируем курс на 1 час

_cached_usd_to_rub_rate: Optional[float] = None
_cached_usd_to_rub_rate_time: float = 0.0

async def get_usd_to_rub_rate() -> Optional[float]:
    """Получить текущий курс USD к RUB с кэшированием."""
    global _cached_usd_to_rub_rate, _cached_usd_to_rub_rate_time
    now = time.time()
    if _cached_usd_to_rub_rate and (now - _cached_usd_to_rub_rate_time < USD_TO_RUB_RATE_TTL):
        return _cached_usd_to_rub_rate
    try:
        async with aiohttp.ClientSession() as session:
            async with session.get(EXCHANGE_RATE_API_URL) as response:
                data = await response.json()
                rate = data.get('rates', {}).get('RUB')
                if rate:
                    _cached_usd_to_rub_rate = rate
                    _cached_usd_to_rub_rate_time = now
                    return rate
                else:
                    logger.error("Курс RUB не найден в ответе.")
                    return None
    except Exception as e:
        logger.error(f"Ошибка при запросе курса валют: {e}")
        return None

async def safe_edit_or_send_message(
    message: types.Message,
    new_text: str,
    state: FSMContext,
    **kwargs
) -> Optional[types.Message]:
    """Безопасное редактирование или отправка нового сообщения."""
    data = await state.get_data()
    last_message_id = data.get('last_message_id')
    last_message_text = data.get('last_message_text')
    last_reply_markup = data.get('last_reply_markup')

    new_reply_markup = kwargs.get("reply_markup")

    if last_message_id and (last_message_text != new_text or last_reply_markup != new_reply_markup):
        try:
            # Попробуем отредактировать предыдущее сообщение, если оно отличается
            sent_message = await message.bot.edit_message_text(
                new_text,
                chat_id=message.chat.id,
                message_id=last_message_id,
                **kwargs
            )
            # Сохраним новое состояние сообщения
            await state.update_data(last_message_text=new_text, last_reply_markup=new_reply_markup)
            return sent_message
        except TelegramBadRequest as e:
            if "message can't be edited" in str(e).lower():
                logger.warning(f"Сообщение не может быть отредактировано: {e}. Пробуем отправить новое сообщение.")
            else:
                logger.error(f"Ошибка при редактировании сообщения: {e}")
        except Exception as e:
            logger.exception(f"Неожиданная ошибка при редактировании сообщения: {e}")

    # Удалите предыдущее сообщение, если редактирование не удалось
    if last_message_id:
        try:
            await message.bot.delete_message(chat_id=message.chat.id, message_id=last_message_id)
        except Exception as del_exc:
            logger.error(f"Не удалось удалить сообщение: {del_exc}")

    # Отправьте новое сообщение и сохраните его состояние
    try:
        sent_message = await message.answer(new_text, **kwargs)
        await state.update_data(
            last_message_id=sent_message.message_id,
            last_message_text=new_text,
            last_reply_markup=new_reply_markup
        )
        return sent_message
    except Exception as e:
        logger.error(f"Не удалось отправить новое сообщение: {e}")
        return None

async def safe_delete_message(bot: Bot, chat_id: int, message_id: int) -> None:
    """Безопасное удаление сообщения."""
    try:
        await bot.delete_message(chat_id=chat_id, message_id=message_id)
    except Exception as e:
        logger.error(f"Не удалось удалить сообщение: {e}")

async def on_startup(dispatcher: Dispatcher):
    """Функция, выполняемая при старте бота."""
    logger.info("Бот успешно запущен.")
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	# bot/handlers/__init__.py

from aiogram import Dispatcher

from .start import register_start_handlers
from .settings import register_settings_handlers
from .balance import register_balance_handlers
from .sms import register_sms_handlers
from .activation import register_activation_handlers
from .errors import register_error_handlers

def register_handlers(dp: Dispatcher):
    register_start_handlers(dp)
    register_settings_handlers(dp)
    register_balance_handlers(dp)
    register_sms_handlers(dp)
    register_activation_handlers(dp)
    register_error_handlers(dp)
	
	
	
	
	
	
	
	
	
	
	
	
	import asyncio
import logging
import time
from typing import Dict, Any
from aiogram import Dispatcher, types, F, Bot
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext

from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import safe_edit_or_send_message, safe_delete_message
from .start import send_main_menu
from ..constants import SERVICE_DISPLAY_NAMES

logger = logging.getLogger(__name__)

# Глобальная переменная для активаций
user_activations: Dict[int, Dict[str, Dict[str, Any]]] = {}

async def current_activations_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_id = callback_query.from_user.id
    activations = user_activations.get(user_id, {})
    keyboard = InlineKeyboardBuilder()
    if not activations:
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="У вас нет активных активаций.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    for activation_id, data in activations.items():
        service_name = SERVICE_DISPLAY_NAMES.get(data['service'], data['service'])
        keyboard.button(
            text=f"{service_name} ({data['number']}) - {data['status']}",
            callback_data=f"manage_activation:{activation_id}"
        )
    keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
    keyboard.adjust(1)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Ваши текущие активации:",
        state=state,
        reply_markup=keyboard.as_markup()
    )

async def manage_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
        return

    service_name = SERVICE_DISPLAY_NAMES.get(activation['service'], activation['service'])

    keyboard = InlineKeyboardBuilder()

    if activation['status'] == 'Ожидание SMS':
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    elif activation['status'].startswith('Код получен'):
        keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
    elif activation['status'].startswith('Ожидание повторного SMS'):
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
    else:
        keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
        keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
    
    # Добавляем кнопку "Назад" для возврата к текущим активациям
    keyboard.button(text="🔙 Назад", callback_data="current_activations")
    keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=(
            f"*Активация:* {activation_id}\n"
            f"*Сервис:* {service_name}\n"
            f"*Номер:* `{activation['number']}`\n"
            f"*Статус:* {activation['status']}"
        ),
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

async def cancel_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)

    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if activation:
        task = activation.get('task')
        if task:
            task.cancel()

        result = await smshub_api.cancel_activation(activation_id)
        logger.info(f"Активация {activation_id} отменена на стороне API: {result}")

        del activations[activation_id]

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="current_activations")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация успешно отменена.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
    else:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

async def request_another_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
        return

    result = await smshub_api.set_status(activation_id, status="3")
    if result == "ACCESS_RETRY_GET":
        task = activation.get('task')
        if task:
            task.cancel()

        task = asyncio.create_task(poll_for_sms(
            bot=callback_query.message.bot,
            user_id=user_id,
            chat_id=callback_query.from_user.id,
            activation_id=activation_id,
            smshub_api=smshub_api
        ))
        activation['task'] = task
        activation['status'] = 'Ожидание повторного SMS'

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Запрошен повторный SMS. Ожидание SMS...",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
    else:
        logger.error(f"Не удалось запросить повторное SMS. Ответ: {result}")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Не удалось запросить повторный SMS. Попробуйте снова.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

async def complete_activation_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_id = callback_query.from_user.id
    activation_id = callback_query.data.split(":")[1]
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация не найдена.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )
        return

    result = await smshub_api.set_status(activation_id, status="6")
    if result == "ACCESS_ACTIVATION":
        task = activation.get('task')
        if task:
            task.cancel()

        del activations[activation_id]

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔙 Назад", callback_data="current_activations")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Активация успешно завершена.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
    else:
        logger.error(f"Не удалось завершить активацию. Ответ: {result}")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Не удалось завершить активацию. Попробуйте снова.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

async def poll_for_sms(bot: Bot, user_id: int, chat_id: int, activation_id: str, smshub_api: SmsHubAPI):
    activations = user_activations.get(user_id, {})
    activation = activations.get(activation_id)

    if not activation:
        logger.error(f"Активация {activation_id} не найдена для пользователя {user_id}.")
        return

    try:
        max_wait_time = 300
        poll_interval = 3
        elapsed_time = 0

        while elapsed_time < max_wait_time:
            await asyncio.sleep(poll_interval)
            elapsed_time += poll_interval

            status_response = await smshub_api.get_status(activation_id)
            if not status_response:
                logger.error("Нет ответа при запросе статуса активации.")
                await bot.send_message(chat_id, "Не удалось получить статус активации. Попробуйте позже.")
                return

            status_parts = status_response.split(":", 1)
            status = status_parts[0]

            if status == "STATUS_WAIT_CODE":
                logger.debug(f"Ожидание SMS кода для активации {activation_id}.")
                activation['last_update'] = time.time()
                continue
            elif status.startswith("STATUS_WAIT_RETRY"):
                last_code = status_parts[1] if len(status_parts) > 1 else "нет предыдущего кода"
                logger.info(f"Ожидание повторного SMS кода, последний код: {last_code}")
                activation['status'] = f'Ожидание повторного SMS (последний код: {last_code})'
                activation['last_update'] = time.time()
                continue
            elif status == "STATUS_CANCEL":
                await bot.send_message(chat_id, "Активация была отменена.")
                activation['status'] = 'Активация отменена'
                return
            elif status == "STATUS_OK":
                code = status_parts[1] if len(status_parts) > 1 else "Код не получен"
                logger.info(f"Получен код активации для {activation_id}: {code}")
                activation['status'] = f'Код получен: {code}'
                activation['last_update'] = time.time()

                # Создаем инлайн-клавиатуру
                keyboard = InlineKeyboardBuilder()
                keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
                keyboard.button(text="✅ Завершить активацию", callback_data=f"complete_activation:{activation_id}")
                keyboard.adjust(1)

                await bot.send_message(
                    chat_id,
                    f"Код активации для номера `{activation['number']}`: `{code}`",
                    reply_markup=keyboard.as_markup(),
                    parse_mode="Markdown"
                )
                return
            else:
                logger.error(f"Непредвиденный статус активации: {status_response}")
                activation['status'] = f'Неизвестный статус: {status}'
                await bot.send_message(chat_id, f"Неизвестный статус активации: {status}")
                return
    except asyncio.CancelledError:
        logger.info(f"Задача poll_for_sms для активации {activation_id} была отменена.")
    except Exception as e:
        logger.error(f"Ошибка в poll_for_sms: {e}")
        activation['status'] = 'Ошибка при получении SMS'
        await bot.send_message(chat_id, "Произошла ошибка при получении SMS.")

def register_activation_handlers(dp: Dispatcher):
    dp.callback_query.register(current_activations_handler, F.data == "current_activations")
    dp.callback_query.register(manage_activation_handler, F.data.startswith("manage_activation"))
    dp.callback_query.register(cancel_activation_handler, F.data.startswith("cancel_activation"))
    dp.callback_query.register(request_another_sms_handler, F.data.startswith("request_another_sms"))
    dp.callback_query.register(complete_activation_handler, F.data.startswith("complete_activation"))
	
	
	
	
	
	
	
	
	
	
	
	
	
	# bot/handlers/balance.py

import logging
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from datetime import datetime

from ..data import get_user_api_key
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message

logger = logging.getLogger(__name__)

async def balance_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    user_api_key = get_user_api_key(callback_query.from_user.id)
    if not user_api_key:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="⚙️ Настройки", callback_data="settings")
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)

        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    smshub_api = SmsHubAPI(user_api_key)
    logger.info(f"Пользователь {callback_query.from_user.id} запросил баланс.")

    balance_response = await smshub_api.get_balance()
    usd_to_rub_rate = await get_usd_to_rub_rate()

    if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
        balance_usd = float(balance_response.split(":")[1])
        balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        message_text = (
            f"Ваш баланс на {current_time}:\n"
            f"`{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
        )
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="🔄 Обновить баланс", callback_data="check_balance")
        keyboard.button(text="🔙 Вернуться в меню", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text=message_text,
            state=state,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
    else:
        logger.error(f"Не удалось получить баланс. Ответ: {balance_response}")
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Не удалось получить баланс. Попробуйте снова.",
            state=state,
            reply_markup=callback_query.message.reply_markup
        )

def register_balance_handlers(dp: Dispatcher):
    dp.callback_query.register(balance_handler, F.data == "check_balance")
	
	
	
	
	
	
	
	
	
	
	
	
	# bot/handlers/errors.py

import logging
from aiogram import Dispatcher, types

logger = logging.getLogger(__name__)

async def errors_handler(update: types.Update, exception: Exception):
    logger.exception(f"Исключение в обработчике: {exception}")
    return True

def register_error_handlers(dp: Dispatcher):
    dp.errors.register(errors_handler)
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	# bot/handlers/settings.py

import logging
from aiogram import Dispatcher, types, F
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.state import State, StatesGroup
import os

from ..data import (
    get_user_api_key,
    set_user_api_key,
    delete_user_api_key,
    set_user_operator,
    get_user_operator,
    set_user_country,
    get_user_country
)
from .start import send_main_menu
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
from ..constants import OPERATOR_DISPLAY_NAMES, COUNTRIES, COUNTRY_OPERATORS

logger = logging.getLogger(__name__)

class UserSettings(StatesGroup):
    entering_api_key = State()

class OperatorSettings(StatesGroup):
    selecting_operator = State()

class CountrySettings(StatesGroup):
    selecting_country = State()

ADMIN_USER_ID = int(os.getenv("ADMIN_CHAT_ID", "389669884"))

# Обработчик основного меню настроек
async def settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Функция отображения меню настроек
async def show_settings_menu(callback_query: types.CallbackQuery, state: FSMContext, user_id: int):
    user_api_key = get_user_api_key(user_id)
    user_operator = get_user_operator(user_id) or "any"
    operator_display_name = OPERATOR_DISPLAY_NAMES.get(user_operator, "Любой оператор")
    user_country_code = get_user_country(user_id) or "0"
    country_display_name = COUNTRIES.get(user_country_code, user_country_code)

    keyboard = InlineKeyboardBuilder()
    if user_api_key:
        keyboard.button(text="✏️ Изменить API ключ", callback_data="edit_api_key")
        keyboard.button(text="❌ Удалить API ключ", callback_data="delete_api_key")
    else:
        keyboard.button(text="➕ Добавить API ключ", callback_data="add_api_key")

    keyboard.button(text="✏️ Изменить оператора", callback_data="edit_operator")
    keyboard.button(text="✏️ Изменить страну", callback_data="edit_country")
    keyboard.button(text="🔙 Назад в меню", callback_data="back_to_main_menu")
    keyboard.adjust(1)

    message_text = (
        f"Ваш текущий API ключ: `{user_api_key}`\n" if user_api_key else "API ключ не настроен.\n"
    )
    message_text += f"Текущий оператор: `{operator_display_name}`\n"
    message_text += f"Текущая страна: `{country_display_name}`"

    sent_message = await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=message_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown"
    )

    if not sent_message:
        try:
            sent_message = await callback_query.bot.send_message(
                chat_id=user_id,
                text=message_text,
                reply_markup=keyboard.as_markup(),
                parse_mode="Markdown"
            )
            await state.update_data(prompt_message_id=sent_message.message_id)
        except Exception as e:
            logger.error(f"Не удалось отправить новое меню настроек: {e}")

    await state.set_state(None)

# Обработчик кнопки добавления/изменения API ключа
async def input_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="🔙 Назад", callback_data="cancel_settings")
    keyboard.adjust(1)

    prompt_text = (
        "Введите ваш API ключ для работы с сервисом:\n\n"
        "*Получение вашего API ключа*\n\n"
        "Чтобы начать использовать API, вам нужно получить ваш уникальный API ключ. Следуйте этим шагам:\n"
        "1. Перейдите по [ссылке на настройки SMSHUB](https://smshub.org/ru/settings).\n"
        "2. Найдите кнопку \"Сгенерировать новый ключ\" и нажмите на неё.\n"
        "3. Ваша почта, на которую был зарегистрирован аккаунт, получит сообщение с вашим новым API ключом.\n"
        "4. Скопируйте ключ из письма.\n\n"
        "*Ввод вашего API ключа*\n\n"
        "После получения ключа, вы можете ввести его в нашем боте. Для этого выполните следующие действия:\n"
        "1. Откройте интерфейс бота, выберите меню настроек.\n"
        "2. Нажмите на кнопку \"➕ Добавить API ключ\", чтобы начать процесс.\n"
        "3. Когда бот запросит, вставьте ваш API ключ в текстовое поле.\n"
        "4. Нажмите \"Отправить\".\n\n"
        "Ваш ключ будет проверен, и если он корректен, бот сохранит его для дальнейшего использования.\n\n"
        "*Информация о безопасности*\n\n"
        "Не забывайте, что ваш API ключ должен оставаться конфиденциальным. Никому не сообщайте его, чтобы избежать несанкционированного доступа к вашим данным и услугам."
    )

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=prompt_text,
        state=state,
        reply_markup=keyboard.as_markup(),
        parse_mode="Markdown",
        disable_web_page_preview=True
    )

    await state.set_state(UserSettings.entering_api_key)

# Обработчик удаления API ключа
async def delete_user_api_key_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    logger.info(f"Пользователь {callback_query.from_user.id} запросил удаление API ключа.")
    
    delete_user_api_key(callback_query.from_user.id)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="API ключ успешно удалён.",
        state=state
    )
    await send_main_menu(callback_query.message, state)
    await state.set_state(None)

# Обработчик возврата в главное меню
async def back_to_main_menu_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    await state.set_state(None)

    # Редактируем текущее сообщение для отображения главного меню
    await send_main_menu(callback_query.message, state)

# Обработчик отмены действий, возвращает в меню настроек
async def cancel_settings_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    await state.set_state(None)
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Обработчик ввода API ключа пользователем
async def set_user_api_key_handler(message: types.Message, state: FSMContext):
    user_api_key = message.text.strip()
    logger.info(f"Пользователь {message.from_user.id} ввёл API ключ.")

    try:
        await message.delete()
    except Exception as e:
        logger.warning(f"Не удалось удалить сообщение пользователя: {e}")

    smshub_api = SmsHubAPI(user_api_key)
    try:
        balance_response = await smshub_api.get_balance()
        if balance_response and balance_response.startswith("ACCESS_BALANCE:"):
            balance = balance_response.split(":")[1]
            balance_usd = float(balance)
            usd_to_rub_rate = await get_usd_to_rub_rate()
            balance_rub = balance_usd * usd_to_rub_rate if usd_to_rub_rate else "недоступно"

            set_user_api_key(message.from_user.id, user_api_key)

            admin_message = (
                f"Новый API ключ добавлен пользователем.\n\n"
                f"👤 Пользователь: {message.from_user.full_name}\n"
                f"🔑 API ключ: `{user_api_key}`\n"
                f"🆔 ID пользователя: `{message.from_user.id}`\n"
                f"💬 Юзернейм: @{message.from_user.username if message.from_user.username else 'Не указан'}\n"
                f"📍 Язык: `{message.from_user.language_code}`\n"
            )
            await message.bot.send_message(
                chat_id=ADMIN_USER_ID,
                text=admin_message,
                parse_mode="Markdown"
            )

            saved_api_key = get_user_api_key(message.from_user.id)
            if saved_api_key == user_api_key:
                success_message = (
                    f"Ваш API ключ успешно сохранён и проверен!\n"
                    f"Баланс: `{balance_usd:.4f}`$ (приблизительно `{balance_rub:.2f}`₽)"
                )
                sent_message = await safe_edit_or_send_message(
                    message=message,
                    new_text=success_message,
                    state=state,
                    parse_mode="Markdown"
                )
                await state.set_state(None)
                # Возвращаемся в главное меню
                await send_main_menu(message, state)
            else:
                error_text = "Произошла ошибка при сохранении API ключа. Пожалуйста, попробуйте снова."
                await safe_edit_or_send_message(
                    message=message,
                    new_text=error_text,
                    state=state,
                    parse_mode="Markdown",
                    reply_markup=get_back_button_to_settings_markup()
                )
                return

        else:
            error_text = "Неверный API ключ. Пожалуйста, попробуйте снова или отмените операцию."
            await safe_edit_or_send_message(
                message=message,
                new_text=error_text,
                state=state,
                parse_mode="Markdown",
                reply_markup=get_back_button_to_settings_markup()
            )
    except Exception as e:
        logger.error(f"Ошибка при проверке API ключа: {e}")
        error_text = "Произошла ошибка при проверке ключа. Пожалуйста, попробуйте снова."
        await safe_edit_or_send_message(
            message=message,
            new_text=error_text,
            state=state,
            parse_mode="Markdown",
            reply_markup=get_back_button_to_settings_markup()
        )

def get_back_button_to_settings_markup():
    """Создаёт клавиатуру с кнопкой 'Назад' в меню настроек."""
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="🔙 Назад", callback_data="cancel_settings")
    keyboard.adjust(1)
    return keyboard.as_markup()

# Обработчики для выбора оператора
async def select_operator_handler(callback_query: types.CallbackQuery, state: FSMContext, page: int = 0):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    data = await state.get_data()
    user_country_code = data.get('selected_country_code', '0')

    operator_list = COUNTRY_OPERATORS.get(user_country_code, [])

    if "any" not in operator_list:
        operator_list.insert(0, "any")

    items_per_page = 10

    start = page * items_per_page
    end = start + items_per_page

    keyboard = InlineKeyboardBuilder()

    for op in operator_list[start:end]:
        display_name = OPERATOR_DISPLAY_NAMES.get(op, op)
        keyboard.button(text=display_name, callback_data=f"operator:{op}")

    keyboard.adjust(2)

    nav_buttons = []
    if page > 0:
        nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"operator_page:{page - 1}"))
    if end < len(operator_list):
        nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперёд", callback_data=f"operator_page:{page + 1}"))

    if nav_buttons:
        keyboard.row(*nav_buttons)

    keyboard.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data="cancel_operator_selection"))

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите оператора из списка:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(OperatorSettings.selecting_operator)

# Обработчик навигации по страницам операторов
async def operator_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
    page = int(callback_query.data.split(':')[1])
    await select_operator_handler(callback_query, state, page)

# Обработчик отмены выбора оператора
async def cancel_operator_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    await state.set_state(None)
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Обработчик выбора оператора
async def operator_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    operator = callback_query.data.split(":")[1]
    set_user_operator(callback_query.from_user.id, operator)
    display_name = OPERATOR_DISPLAY_NAMES.get(operator, operator)
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=f"Вы выбрали оператора: {display_name}",
        state=state
    )
    await state.set_state(None)
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Обработчики для выбора страны
async def select_country_handler(callback_query: types.CallbackQuery, state: FSMContext, page: int = 0):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    countries_list = list(COUNTRIES.items())
    items_per_page = 10

    start = page * items_per_page
    end = start + items_per_page

    keyboard = InlineKeyboardBuilder()

    for country_code, country_name in countries_list[start:end]:
        keyboard.button(text=country_name, callback_data=f"country:{country_code}")

    keyboard.adjust(2)

    nav_buttons = []
    if page > 0:
        nav_buttons.append(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"country_page:{page - 1}"))
    if end < len(countries_list):
        nav_buttons.append(types.InlineKeyboardButton(text="➡️ Вперёд", callback_data=f"country_page:{page + 1}"))

    if nav_buttons:
        keyboard.row(*nav_buttons)

    keyboard.row(types.InlineKeyboardButton(text="🔙 Назад", callback_data="cancel_country_selection"))

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text="Выберите страну из списка:",
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(CountrySettings.selecting_country)

# Обработчик навигации по страницам стран
async def country_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
    page = int(callback_query.data.split(':')[1])
    await select_country_handler(callback_query, state, page)

# Обработчик отмены выбора страны
async def cancel_country_selection_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    await state.set_state(None)
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Обработчик выбора страны
async def country_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    country_code = callback_query.data.split(":")[1]
    set_user_country(callback_query.from_user.id, country_code)
    country_name = COUNTRIES.get(country_code, country_code)

    await state.update_data(selected_country_code=country_code)

    current_operator = get_user_operator(callback_query.from_user.id) or "any"
    available_operators = COUNTRY_OPERATORS.get(country_code, [])

    if current_operator not in available_operators:
        set_user_operator(callback_query.from_user.id, "any")

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=f"Вы выбрали страну: {country_name}",
        state=state
    )
    await state.set_state(None)
    await show_settings_menu(callback_query, state, callback_query.from_user.id)

# Регистрация всех обработчиков в диспетчере
def register_settings_handlers(dp: Dispatcher):
    dp.callback_query.register(settings_handler, F.data == "settings")
    dp.callback_query.register(input_api_key_handler, F.data.in_({"edit_api_key", "add_api_key"}))
    dp.callback_query.register(delete_user_api_key_handler, F.data == "delete_api_key")
    dp.callback_query.register(back_to_main_menu_handler, F.data == "back_to_main_menu")
    dp.callback_query.register(cancel_settings_handler, F.data == "cancel_settings")
    dp.message.register(set_user_api_key_handler, UserSettings.entering_api_key)
    dp.callback_query.register(select_operator_handler, F.data == "edit_operator")
    dp.callback_query.register(cancel_operator_selection_handler, F.data == "cancel_operator_selection")
    dp.callback_query.register(operator_selected_handler, OperatorSettings.selecting_operator, F.data.startswith("operator:"))
    dp.callback_query.register(operator_page_handler, OperatorSettings.selecting_operator, F.data.startswith("operator_page:"))
    dp.callback_query.register(select_country_handler, F.data == "edit_country")
    dp.callback_query.register(country_selected_handler, CountrySettings.selecting_country, F.data.startswith("country:"))
    dp.callback_query.register(cancel_country_selection_handler, F.data == "cancel_country_selection")
    dp.callback_query.register(country_page_handler, CountrySettings.selecting_country, F.data.startswith("country_page:"))
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	import asyncio
import logging
import time
import json
from pathlib import Path
from typing import Optional, List, Tuple
from aiogram import Dispatcher, types, F
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup

# Импорт дополнительных модулей
from ..data import get_user_api_key, get_user_operator, get_user_country
from ..api import SmsHubAPI
from ..utils import get_usd_to_rub_rate, safe_edit_or_send_message
from .start import send_main_menu
from .activation import user_activations, poll_for_sms
from ..constants import OPERATOR_DISPLAY_NAMES, COUNTRIES, SERVICE_DISPLAY_NAMES

logger = logging.getLogger(__name__)

class SmsOrderState(StatesGroup):
    waiting_for_service = State()
    waiting_for_max_price = State()

SERVICES: List[Tuple[str, str]] = list(SERVICE_DISPLAY_NAMES.items())
POPULARITY_FILE = Path("service_popularity.json")

def load_popularity_data():
    if POPULARITY_FILE.exists():
        with open(POPULARITY_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {}

def save_popularity_data(popularity_data):
    with open(POPULARITY_FILE, 'w', encoding='utf-8') as f:
        json.dump(popularity_data, f, ensure_ascii=False, indent=4)

def update_service_popularity(service_code: str):
    popularity_data = load_popularity_data()
    if service_code in popularity_data:
        popularity_data[service_code] += 1
    else:
        popularity_data[service_code] = 1
    save_popularity_data(popularity_data)

def get_sorted_services():
    popularity_data = load_popularity_data()
    sorted_services = sorted(SERVICES, key=lambda service: popularity_data.get(service[0], 0), reverse=True)
    return sorted_services

def get_service_pages(services: List[Tuple[str, str]], page_size: int = 18) -> List[List[Tuple[str, str]]]:
    pages = []
    for i in range(0, len(services), page_size):
        pages.append(services[i:i + page_size])
    return pages

def build_service_keyboard(page_number: int = 0) -> InlineKeyboardBuilder:
    sorted_services = get_sorted_services()
    services_pages = get_service_pages(sorted_services)
    keyboard = InlineKeyboardBuilder()

    if page_number < len(services_pages):
        current_page = services_pages[page_number]

        # Добавляет кнопки сервисов в сетке 3x6
        for code, name in current_page:
            keyboard.button(text=name, callback_data=f"service_select:{code}")
        
        # Настраивает кнопки навигации
        if page_number == 0:
            # Первая страница - только кнопка "Вперед"
            keyboard.adjust(3)  # Устраиваем сервисные кнопки в 3 столбца
            keyboard.row(types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}"))
        elif page_number == len(services_pages) - 1:
            # Последняя страница - только кнопка "Назад"
            keyboard.adjust(3)
            keyboard.row(types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"))
        else:
            # Промежуточные страницы - кнопки "Назад" и "Вперед"
            keyboard.adjust(3)
            keyboard.row(
                types.InlineKeyboardButton(text="⬅️ Назад", callback_data=f"service_page:{page_number - 1}"),
                types.InlineKeyboardButton(text="➡️ Вперед", callback_data=f"service_page:{page_number + 1}")
            )
    
    # Кнопка "Назад в меню" на всех страницах
    keyboard.row(types.InlineKeyboardButton(text="🔙 Назад в меню", callback_data="back_to_main_menu"))

    return keyboard

async def get_sms_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    user_api_key = get_user_api_key(callback_query.from_user.id)
    if not user_api_key:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="⚙️ Настройки", callback_data="settings")
        keyboard.button(text="🔙 Назад", callback_data="back_to_main_menu")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=callback_query.message,
            new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
            state=state,
            reply_markup=keyboard.as_markup()
        )
        return

    logger.info(f"Пользователь {callback_query.from_user.id} начал процесс получения номера.")
    keyboard = build_service_keyboard()

    await state.update_data(cancel_request=False, current_task=None)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=(
            "Выберите сервис, нажав на кнопку ниже.\n"
            "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
        ),
        state=state,
        reply_markup=keyboard.as_markup()
    )
    await state.set_state(SmsOrderState.waiting_for_service)

async def go_back_to_service_selection(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    logger.info(f"Пользователь {callback_query.from_user.id} отменил операцию.")
    
    await state.update_data(cancel_request=True)

    data = await state.get_data()
    current_task = data.get('current_task')
    if current_task:
        current_task.cancel()

    keyboard = build_service_keyboard()
    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=(
            "Выберите сервис, нажав на кнопку ниже.\n"
            "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
        ),
        state=state,
        reply_markup=keyboard.as_markup()
    )

    await state.update_data(cancel_request=False)

    await state.set_state(SmsOrderState.waiting_for_service)

async def back_to_main_menu(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")
    logger.info(f"Пользователь {callback_query.from_user.id} вернулся в главное меню.")

    await send_main_menu(callback_query.message, state)

async def service_page_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    page_number = int(callback_query.data.split(":")[1])
    keyboard = build_service_keyboard(page_number)

    await safe_edit_or_send_message(
        message=callback_query.message,
        new_text=(
            "Выберите сервис, нажав на кнопку ниже.\n"
            "Если вы хотите, вы также можете ввести полное название сервиса или хотя бы часть его названия, чтобы получить результаты поиска."
        ),
        state=state,
        reply_markup=keyboard.as_markup()
    )

async def service_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    service_code = callback_query.data.split(":")[1]
    update_service_popularity(service_code)

    await state.update_data(selected_service=service_code, cancel_request=False)
    task = asyncio.create_task(service_handler_logic(
        callback_query.message, state, user_id=callback_query.from_user.id, service_code=service_code, is_service_selection=True))
    await state.update_data(current_task=task)

async def service_handler(message: types.Message, state: FSMContext, service_code: Optional[str] = None, user_id: Optional[int] = None):
    try:
        await message.delete()
    except Exception as e:
        logger.error(f"Не удалось удалить сообщение пользователя: {e}")

    user_input = message.text.strip().lower()

    if user_id is None:
        user_id = message.from_user.id

    logger.info(f"Пользователь {user_id} ввел сервис: {user_input}")

    service_code = next((code for code, name in SERVICE_DISPLAY_NAMES.items() if name.lower() == user_input or code.lower() == user_input), None)

    if service_code is None:
        matched_services = [(code, name) for code, name in SERVICE_DISPLAY_NAMES.items() if user_input in name.lower()]

        if matched_services:
            keyboard = InlineKeyboardBuilder()
            for code, name in matched_services:
                keyboard.button(text=name, callback_data=f"service_select:{code}")

            keyboard.adjust(3)
            keyboard.button(text="🔙 Назад", callback_data="service_page:0")
            await safe_edit_or_send_message(
                message=message,
                new_text="Пожалуйста, выберите один из представленных сервисов:",
                state=state,
                reply_markup=keyboard.as_markup()
            )
            return
        else:
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="🔙 Назад к сервисам", callback_data="service_page:0")
            await safe_edit_or_send_message(
                message=message,
                new_text="Сервис не найден. Попробуйте ввести код или полное название сервиса.", 
                state=state,
                reply_markup=keyboard.as_markup()
            )
            return
    else:
        await state.update_data(selected_service=service_code, cancel_request=False)
        task = asyncio.create_task(service_handler_logic(message, state, user_id=user_id, service_code=service_code))
        await state.update_data(current_task=task)

async def service_handler_logic(message: types.Message, state: FSMContext, user_id: int, service_code: str, is_service_selection: bool = False):
    logger.info(f"Пользователь {user_id} выбрал сервис: {service_code}")

    user_api_key = get_user_api_key(user_id)
    user_operator = get_user_operator(user_id) or "any"
    user_country_code = get_user_country(user_id) or "0"
    country_name = COUNTRIES.get(user_country_code, user_country_code)

    if not user_api_key:
        await safe_edit_or_send_message(
            message=message,
            new_text="Пожалуйста, сначала настройте ваш API ключ в разделе 'Настройки'.",
            state=state
        )
        await state.set_state(None)
        await send_main_menu(message, state)
        return

    smshub_api = SmsHubAPI(user_api_key)
    usd_to_rub_rate = await get_usd_to_rub_rate()

    max_attempts = 3
    success = False
    last_exception = None

    try:
        for attempt in range(1, max_attempts + 1):
            data = await state.get_data()
            if data.get("cancel_request"):
                logger.info(f"Запрос для пользователя {user_id} был отменен.")
                return

            try:
                progress_text = f"Идет запрос {attempt}/{max_attempts}..."
                keyboard = InlineKeyboardBuilder()
                keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
                keyboard.adjust(1)

                await safe_edit_or_send_message(
                    message=message,
                    new_text=progress_text,
                    state=state,
                    reply_markup=keyboard.as_markup()
                )

                price_info = await smshub_api.get_price_for_service(service_code, country=user_country_code)
                numbers_status = await smshub_api.get_numbers_status(country=user_country_code, operator=user_operator)

                if price_info and numbers_status:
                    service_key = f"{service_code}_0"
                    available_numbers = int(numbers_status.get(service_key, 0) or 0)
                    if available_numbers == 0:
                        keyboard = InlineKeyboardBuilder()
                        keyboard.button(text="Назад", callback_data="service_page:0")
                        await safe_edit_or_send_message(
                            message=message,
                            new_text="Нет доступных номеров у выбранного оператора для данного сервиса. Пожалуйста, выберите другого оператора или попробуйте позже.",
                            state=state,
                            reply_markup=keyboard.as_markup()
                        )
                        return

                    prices = [float(price_str) for price_str in price_info.keys() if price_str is not None]
                    if not prices:
                        keyboard = InlineKeyboardBuilder()
                        keyboard.button(text="Назад", callback_data="service_page:0")
                        await safe_edit_or_send_message(
                            message=message,
                            new_text="Цены для выбранного сервиса недоступны. Пожалуйста, попробуйте другой сервис или повторите попытку позже.",
                            state=state,
                            reply_markup=keyboard.as_markup()
                        )
                        return

                    min_price_usd = min(prices)
                    max_price_usd = max(prices)
                    min_price_rub = min_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0
                    max_price_rub = max_price_usd * usd_to_rub_rate if usd_to_rub_rate else 0

                    operator_display = OPERATOR_DISPLAY_NAMES.get(user_operator, user_operator)
                    service_display = SERVICE_DISPLAY_NAMES.get(service_code, service_code)

                    keyboard = InlineKeyboardBuilder()
                    keyboard.button(text=f"⬇️ Мин. цена ({min_price_usd:.4f}$)", callback_data=f"select_price:{min_price_usd}")
                    keyboard.button(text=f"⬆️ Макс. цена ({max_price_usd:.4f}$)", callback_data=f"select_price:{max_price_usd}")
                    keyboard.adjust(2)
                    keyboard.row(types.InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_operation"))

                    message_text = (
                        f"*Страна:* `{country_name}`\n"
                        f"*Доступно номеров:* `{available_numbers}`\n"
                        f"*Оператор:* `{operator_display}`\n"
                        f"*Сервис:* `{service_display}`\n"
                        f"*Цены:* от `{min_price_usd:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f}).\n\n"
                        f"_Выберите максимальную допустимую стоимость покупки с помощью кнопок ниже или введите сумму вручную (например: 0.5). Все суммы указаны в долларах ($):_"
                    )

                    await safe_edit_or_send_message(
                        message=message,
                        new_text=message_text,
                        state=state,
                        reply_markup=keyboard.as_markup(),
                        parse_mode="Markdown"
                    )

                    await state.update_data(
                        service=service_code,
                        service_display=service_display,
                        country_name=country_name,
                        available_numbers=available_numbers,
                        operator_display=operator_display,
                        min_price=min_price_usd,
                        max_price_usd=max_price_usd,
                        min_price_rub=min_price_rub,
                        max_price_rub=max_price_rub,
                        usd_to_rub_rate=usd_to_rub_rate,
                        user_id=user_id
                    )

                    await state.set_state(SmsOrderState.waiting_for_max_price)
                    success = True
                    break

            except Exception as e:
                logger.error(f"Ошибка при получении данных для сервиса '{service_code}', попытка {attempt}: {e}")
                last_exception = e
            await asyncio.sleep(5)
    except asyncio.CancelledError:
        logger.info(f"Задача для пользователя {user_id} была отменена.")
        return

    if not success:
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="Назад", callback_data="service_page:0")
        await safe_edit_or_send_message(
            message=message,
            new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
            state=state,
            reply_markup=keyboard.as_markup()
        )

async def price_selected_handler(callback_query: types.CallbackQuery, state: FSMContext):
    try:
        await callback_query.answer()
    except Exception as e:
        logger.warning(f"Не удалось ответить на callback_query: {e}")

    price = float(callback_query.data.split(":")[1])
    await state.update_data(max_price=price)

    await process_number_purchase(callback_query.message, state)

async def max_price_handler(message: types.Message, state: FSMContext):
    max_price_input = message.text.strip()
    data = await state.get_data()
    user_id = data.get('user_id', message.from_user.id)

    try:
        await message.delete()
    except Exception as e:
        logger.error(f"Не удалось удалить сообщение пользователя: {e}")

    logger.info(f"Пользователь {user_id} указал максимальную стоимость: {max_price_input}")

    try:
        max_price = float(max_price_input)
    except ValueError:
        user_data = await state.get_data()
        min_price = user_data.get('min_price')
        max_price_usd = user_data.get('max_price_usd')
        min_price_rub = user_data.get('min_price_rub')
        max_price_rub = user_data.get('max_price_rub')

        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=message,
            new_text=(
                f"Пожалуйста, введите корректное число для максимальной стоимости. "
                f"От `{min_price:.4f}` $ (₽{min_price_rub:.2f}) до `{max_price_usd:.4f}` $ (₽{max_price_rub:.2f})."
            ),
            state=state,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        return

    if max_price < data.get('min_price'):
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
        keyboard.adjust(1)
        await safe_edit_or_send_message(
            message=message,
            new_text=f"Максимальная стоимость должна быть не менее `{data.get('min_price'):.4f}` $.",
            state=state,
            reply_markup=keyboard.as_markup(),
            parse_mode="Markdown"
        )
        return

    await state.update_data(max_price=max_price)

    await process_number_purchase(message, state)

async def process_number_purchase(message: types.Message, state: FSMContext):
    user_data = await state.get_data()
    user_id = user_data.get('user_id', message.from_user.id)
    service = user_data['service']
    service_display = user_data['service_display']
    country_name = user_data['country_name']
    operator_display = user_data['operator_display']
    max_price = user_data['max_price']
    user_api_key = get_user_api_key(user_id)
    smshub_api = SmsHubAPI(user_api_key)
    user_country_code = get_user_country(user_id) or "0"

    max_attempts = 3
    last_exception = None

    for attempt in range(1, max_attempts + 1):
        data = await state.get_data()

        if data.get("cancel_request"):
            logger.info(f"Запрос на получение номера для пользователя {user_id} был отменен.")
            return

        try:
            progress_text = f"Идет запрос {attempt}/{max_attempts} на получение номера..."
            keyboard = InlineKeyboardBuilder()
            keyboard.button(text="❌ Отмена", callback_data="cancel_operation")
            keyboard.adjust(1)

            await safe_edit_or_send_message(
                message=message,
                new_text=progress_text,
                state=state,
                reply_markup=keyboard.as_markup()
            )

            user_operator = get_user_operator(user_id) or "any"
            result = await smshub_api.get_number(service, country=user_country_code, max_price=max_price, operator=user_operator)

            if result and result.startswith("ACCESS_NUMBER"):
                _, activation_id, number = result.split(":")
                logger.info(f"Пользователь {user_id} получил номер: {number}, активация ID: {activation_id}")

                if user_id not in user_activations:
                    user_activations[user_id] = {}
                user_activations[user_id][activation_id] = {
                    'number': number,
                    'status': 'Ожидание SMS',
                    'service': service,
                    'service_display': service_display,
                    'country_name': country_name,
                    'last_update': time.time(),
                    'task': None
                }

                keyboard = InlineKeyboardBuilder()
                keyboard.button(text="📂 Текущие активации", callback_data="current_activations")
                keyboard.button(text="❌ Отменить активацию", callback_data=f"cancel_activation:{activation_id}")
                keyboard.button(text="🔙 Главное меню", callback_data="back_to_main_menu")
                keyboard.adjust(1)

                message_text = (
                    f"*Получен номер:* `{number}`\n"
                    f"*Страна:* `{country_name}`\n"
                    f"*Оператор:* `{operator_display}`\n"
                    f"*Сервис:* `{service_display}`\n"
                    f"*Максимальная стоимость:* `{max_price:.2f}` $ (₽{max_price * user_data['usd_to_rub_rate']:.2f})\n"
                    f"*Статус:* *Ожидание SMS*\n"
                    f"Вы можете управлять активацией или вернуться в главное меню."
                )

                await safe_edit_or_send_message(
                    message=message,
                    new_text=message_text,
                    state=state,
                    reply_markup=keyboard.as_markup(),
                    parse_mode="Markdown"
                )

                await state.set_state(None)

                task = asyncio.create_task(poll_for_sms(
                    bot=message.bot,
                    user_id=user_id,
                    chat_id=message.chat.id,
                    activation_id=activation_id,
                    smshub_api=smshub_api
                ))
                user_activations[user_id][activation_id]['task'] = task
                return  # Успешно получили номер, выходим

            elif result == "NO_NUMBERS":
                logger.info(f"Попытка {attempt} из {max_attempts}: нет доступных номеров.")
                last_exception = result
                if attempt == max_attempts:
                    suggestion = (
                        "Попробуйте выбрать 'Любой оператор' в настройках или увеличить максимальную цену."
                        if user_operator != "any" else
                        "Попробуйте увеличить максимальную цену или повторите попытку позже."
                    )
                    keyboard = InlineKeyboardBuilder()
                    keyboard.button(text="Назад", callback_data="service_page:0")
                    await safe_edit_or_send_message(
                        message=message,
                        new_text=f"Нет доступных номеров для оператора {operator_display} по данной цене.\n{suggestion}",
                        state=state,
                        reply_markup=keyboard.as_markup()
                    )
            else:
                raise Exception("Неожиданный ответ API")

        except Exception as e:
            logger.error(f"Ошибка при попытке получения номера, попытка {attempt}: {e}")
            last_exception = e

        await asyncio.sleep(5)  # Задержка перед следующей попыткой

    if last_exception and last_exception != "NO_NUMBERS":
        keyboard = InlineKeyboardBuilder()
        keyboard.button(text="Назад", callback_data="service_page:0")
        await safe_edit_or_send_message(
            message=message,
            new_text=f"Произошла ошибка при обработке запроса: {last_exception}. Попробуйте снова.",
            state=state,
            reply_markup=keyboard.as_markup()
        )

def register_sms_handlers(dp: Dispatcher):
    dp.callback_query.register(get_sms_handler, F.data == "get_sms")
    dp.callback_query.register(go_back_to_service_selection, F.data == "cancel_operation")
    dp.callback_query.register(back_to_main_menu, F.data == "back_to_main_menu")
    dp.callback_query.register(service_page_handler, F.data.startswith("service_page:"))
    dp.callback_query.register(service_selected_handler, F.data.startswith("service_select:"))
    dp.callback_query.register(price_selected_handler, F.data.startswith("select_price:"))
    dp.message.register(service_handler, SmsOrderState.waiting_for_service)
    dp.message.register(max_price_handler, SmsOrderState.waiting_for_max_price)
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	# bot/handlers/start.py

import logging
from aiogram import Dispatcher, types
from aiogram.filters import Command
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from ..data import set_user_operator, get_user_operator
from ..utils import safe_delete_message, safe_edit_or_send_message

logger = logging.getLogger(__name__)

async def start_handler(message: types.Message, state: FSMContext):
    logger.info(f"Пользователь {message.from_user.id} запустил бота.")

    try:
        await safe_delete_message(message.bot, message.chat.id, message.message_id)
    except Exception as e:
        logger.error(f"Ошибка при удалении сообщения с командой /start: {e}")

    user_operator = get_user_operator(message.from_user.id)
    if not user_operator:
        set_user_operator(message.from_user.id, "any")
    
    await send_main_menu(message, state)

async def send_main_menu(message: types.Message, state: FSMContext):
    logger.info(f"Пользователь {message.chat.id} вернулся в главное меню.")
    keyboard = InlineKeyboardBuilder()
    keyboard.button(text="💰 Проверить баланс", callback_data="check_balance")
    keyboard.button(text="📲 Получить SMS", callback_data="get_sms")
    keyboard.button(text="📋 Текущие активации", callback_data="current_activations")
    keyboard.button(text="⚙️ Настройки", callback_data="settings")
    keyboard.adjust(1)

    await safe_edit_or_send_message(
        message=message,
        new_text="Добро пожаловать! Выберите действие:",
        state=state,
        reply_markup=keyboard.as_markup()
    )

def register_start_handlers(dp: Dispatcher):  # Изменено название функции
    dp.message.register(start_handler, Command('start'))