hayo i need to make big project on telegram miniapp. i will give you code of my telegram bot. you need to save all features and transfer them into miniapp. we will have frontend and backend part. write for me backend part on your own way. here is the plan of my frontend(we will make it for mobile): on the first page we will request from user special code, which we will send him into chat with telegram bot, when he will enter it, he will get access to the main page, there will be three buttons on the black-grey page with white outline: Меню, Товары and Поддержка. Customize each of them with emoji. Remember to do security part on api, that the users can edit only their servers, get information about on their profiles. at the end of the page there will be button with username of user, id under it. when he will click on it nothing will happend. you can use any framework you want, like react, vue.js and tailwind css.



Project Overview:

The mini-app will be a mobile-optimized application that integrates with a Telegram bot, providing users with a seamless and secure experience.
The app will have a simple and intuitive interface, with a focus on user accessibility and security.

Frontend Plan:

First Page:

Request a special code from the user. This code will be sent to their Telegram chat with the bot.
Upon entering the correct code, the user will be granted access to the main page.
Implement a loading animation or a progress bar during the authentication process to provide user feedback.

Main Page:

Design a black-grey themed page with white outlines for buttons.

Customize the three main buttons with emojis:

"Меню" (Menu) - Use a hamburger menu emoji :hamburger: or a simple menu icon.
"Товары" (Products) - A shopping cart emoji :shopping_cart: or a product icon.
"Поддержка" (Support) - A support or help desk emoji :speech_balloon: or a speech bubble icon.
Ensure that the buttons are accessible and have a clear call-to-action.

At the bottom of the page, display the user's username and their ID. This information should be static and not clickable.


Backend Plan:

Authentication and Security:

Develop a secure API endpoint for code verification. Ensure that the code sent to the user's Telegram chat is unique and time-limited to prevent unauthorized access.
Implement a robust authentication system that verifies the user's identity and grants access to the mini-app.
Utilize encryption and secure data storage to protect user information.

Data Management:

Create a database to store user profiles, including their server information, preferences, and any other relevant data.
Design an API that allows users to retrieve and edit their server configurations securely.
Implement role-based access control to ensure that users can only access and edit their own servers and profiles.

Server Communication:

Establish a secure communication channel between the mini-app and the Telegram bot.
Develop an API endpoint that allows the mini-app to send and receive data from the Telegram bot, enabling features like product updates, support tickets, and menu changes.

Error Handling:

Implement comprehensive error handling and logging mechanisms to identify and resolve any issues promptly.
Provide user-friendly error messages and feedback to guide users through any potential issues.

Frameworks and Technologies:

For the frontend, you can use React with Tailwind CSS for a modern and responsive design.
Vue.js is another excellent choice, especially if you prefer a more lightweight and flexible framework.
For the backend, consider using a robust Node.js framework like Express.js, which is well-suited for building APIs and handling authentication.

Development Process:

Start by designing the database schema and API endpoints, ensuring a secure and efficient data flow.
Develop the backend API, focusing on authentication, data management, and server communication.
Simultaneously, work on the frontend, creating a user-friendly interface and integrating it with the backend API.
Test the application thoroughly, ensuring a smooth user experience and addressing any security vulnerabilities.



import json
import os
import random
import paramiko
import telebot
import string
import textwrap
import logging
import time
import traceback
from datetime import datetime, timedelta
from dotenv import load_dotenv
from collections import defaultdict

# ---------------------- Загрузка Переменных Окружения ----------------------
load_dotenv()

# ---------------------- Настройка Логирования ----------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.FileHandler("bot.log"), logging.StreamHandler()],
)

# ---------------------- Конфигурация и Данные Настроек ----------------------
TOKEN = os.getenv("BOT_TOKEN")
if not TOKEN:
    logging.error(
        "Bot token is not set. Please set BOT_TOKEN in your environment variables."
    )
    raise ValueError(
        "Bot token is not set. Please set BOT_TOKEN in your environment variables."
    )

bot = telebot.TeleBot(TOKEN)

# Загрузка конфигурации из config.json
try:
    with open("config.json", "r") as file:
        config = json.load(file)
        logging.info("Configuration loaded successfully.")
except FileNotFoundError:
    logging.error("config.json file not found.")
    raise

plan_names = list(config["plans"].keys())

# Файл базы данных
db_file = "db.json"

if not os.path.exists(db_file):
    with open(db_file, "w") as file:
        json.dump(
            {"users": [], "server_counter": 0, "support_tickets": []}, file, indent=4
        )
        logging.info("Created new db.json file.")

with open(db_file, "r") as file:
    users_db = json.load(file)

# ---------------------- Структура для временного хранения данных о создаваемых серверах ----------------------
pending_server_creations = {}


# ---------------------- Функции Утилиты ----------------------
def load_users():
    global users_db
    try:
        with open(db_file, "r") as file:
            users_db = json.load(file)
            logging.info("User database loaded.")
    except FileNotFoundError:
        save_users()
        logging.info("User database not found. Created new db.json.")


def save_users():
    try:
        with open(db_file, "w") as file:
            json.dump(users_db, file, indent=4)
            logging.info("User database saved.")
    except Exception as e:
        logging.error(f"Failed to save user database: {e}")


def add_user(user_id, username):
    if user_id not in [user["id"] for user in users_db["users"]]:
        users_db["users"].append(
            {
                "id": user_id,
                "username": username,
                "is_admin": False,
                "servers": [],
                "balance": 0,
            }
        )
        save_users()
        logging.info(f"Added new user: {user_id} ({username})")
        bot.send_message(
            user_id,
            "👋 Добро пожаловать! Используйте кнопки ниже для взаимодействия с ботом.",
        )
        send_main_keyboard(user_id)


def set_admin(user_id, status=True):
    for user in users_db["users"]:
        if user["id"] == user_id:
            user["is_admin"] = status
            save_users()
            logging.info(f"Set admin status for user {user_id} to {status}")
            return True
    logging.warning(f"Attempted to set admin for non-existent user {user_id}")
    return False


def is_admin(user_id):
    admin_status = any(
        user["id"] == user_id and user.get("is_admin", False)
        for user in users_db["users"]
    )
    logging.debug(f"User {user_id} admin status: {admin_status}")
    return admin_status


def get_unique_port():
    while True:
        port = random.randint(4000, 10000)
        existing_ports = config.get("rdp_ports", [])
        if port not in existing_ports:
            config["rdp_ports"].append(port)
            save_config()
            logging.debug(f"Assigned unique port: {port}")
            return port


def generate_random_password(length=12):
    characters = string.ascii_uppercase + "123456789"
    if length < 2:
        raise ValueError(
            "Password length must be at least 2 to include one letter and one digit."
        )
    password = [random.choice(string.ascii_uppercase), random.choice("123456789")]
    password += random.choices(characters, k=length - 2)
    random.shuffle(password)
    generated_password = "".join(password)
    logging.debug(f"Generated password: {generated_password}")
    return generated_password


def create_docker_compose(plan, server_id, version, rdp_port, vnc_port, password):
    plan_config = config["plans"][plan]

    docker_compose_content = textwrap.dedent(
        f"""
    version: '3'
    services:
      windows:
        image: dockurr/windows
        container_name: windows{server_id}
        environment:
          VERSION: "{version}"
          DISK_SIZE: "{plan_config['DISK']}"
          RAM_SIZE: "{plan_config['RAM']}"
          CPU_CORES: "{plan_config['CPU']}"
          USERNAME: "admin"
          PASSWORD: "{password}"
        devices:
          - /dev/kvm
        cap_add:
          - NET_ADMIN
        ports:
          - "{vnc_port}:8006/tcp"
          - "{rdp_port}:3389/tcp"
          - "{rdp_port}:3389/udp"
        stop_grace_period: 2m
        restart: unless-stopped
    """
    )

    logging.debug(
        f"Generated docker-compose.yml for server {server_id}:\n{docker_compose_content}"
    )
    return docker_compose_content


def save_docker_compose(ssh_client, docker_compose_content, server_id):
    try:
        sftp = ssh_client.open_sftp()
        folder_path = f"/root/windows{server_id}"
        try:
            sftp.mkdir(folder_path)
            logging.info(f"Created directory {folder_path} on server.")
        except IOError as e:
            if "Failure" in str(e):
                logging.warning(f"Directory {folder_path} already exists.")
            else:
                logging.error(f"Failed to create directory {folder_path}: {e}")
                raise
        with sftp.open(f"{folder_path}/docker-compose.yml", "w") as file:
            file.write(docker_compose_content)
        sftp.close()
        logging.info(f"docker-compose.yml saved successfully for server {server_id}")
    except Exception as e:
        logging.error(f"Error saving docker-compose.yml: {e}")
        raise


def connect_via_ssh(server):
    ssh_client = paramiko.SSHClient()
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        ssh_client.connect(
            server["host"],
            username=server["username"],
            password=server["password"],
            timeout=10,
        )
        logging.info(f"SSH connection established to {server['host']}")
    except Exception as e:
        logging.error(f"SSH connection failed to {server['host']}: {e}")
        return None
    return ssh_client


def execute_docker_command(server_id, command):
    server = config["servers"][0]
    ssh_client = connect_via_ssh(server)
    if ssh_client:
        try:
            full_command = f"docker {command} windows{server_id}"
            logging.info(f"Executing command: {full_command}")
            stdin, stdout, stderr = ssh_client.exec_command(full_command)
            exit_status = stdout.channel.recv_exit_status()
            output = stdout.read().decode().strip()
            error = stderr.read().decode().strip()
            if exit_status != 0:
                logging.error(f"Error executing '{full_command}': {error}")
                return False, error
            else:
                logging.info(
                    f"Command '{full_command}' executed successfully: {output}"
                )
                return True, output
        except Exception as e:
            logging.error(
                f"Error executing docker command '{command}' on server {server_id}: {e}"
            )
            return False, str(e)
        finally:
            ssh_client.close()
            logging.info(f"SSH connection closed for server {server['host']}")
    else:
        logging.error(
            f"Cannot execute docker command '{command}' on server {server['host']} because SSH connection failed."
        )
        return False, "SSH connection failed."


def validate_integer(value):
    try:
        return int(value)
    except ValueError:
        return None


def validate_float(value):
    try:
        return float(value)
    except ValueError:
        return None


def validate_docker_compose(ssh_client, server_id):
    try:
        command = (
            f"docker-compose -f /root/windows{server_id}/docker-compose.yml config"
        )
        logging.info(f"Validating docker-compose.yml for server {server_id}")
        stdin, stdout, stderr = ssh_client.exec_command(command)
        exit_status = stdout.channel.recv_exit_status()
        if exit_status != 0:
            error = stderr.read().decode().strip()
            logging.error(f"Validation failed for server {server_id}: {error}")
            return False
        logging.info(
            f"Docker Compose file validated successfully for server {server_id}"
        )
        return True
    except Exception as e:
        logging.error(
            f"Error validating docker-compose.yml for server {server_id}: {e}"
        )
        return False


def save_config():
    try:
        with open("config.json", "w") as file:
            json.dump(config, file, indent=4)
            logging.info("Configuration saved.")
    except Exception as e:
        logging.error(f"Failed to save configuration: {e}")


def create_server_id():
    users_db["server_counter"] += 1
    save_users()
    return users_db["server_counter"]


# ---------------------- Основная проверка биллинга ----------------------
def monthly_billing_check():
    logging.info("Checking for monthly billing...")
    for user in users_db["users"]:
        for server in user["servers"]:
            current_time = datetime.now()
            if server.get(
                "next_payment_due"
            ) and current_time >= datetime.fromisoformat(server["next_payment_due"]):
                handle_billing_cycle(user, server)


def handle_billing_cycle(user, server):
    plan = server["plan"]
    plan_price = config["plans"][plan]["price"]

    if user["balance"] >= plan_price:
        # Списание средств и обновление следующей даты оплаты
        user["balance"] -= plan_price
        server["next_payment_due"] = (datetime.now() + timedelta(days=30)).isoformat()
        save_users()
        logging.info(
            f"Successfully billed user {user['id']} for server {server['name']}. Next payment due on {server['next_payment_due']}."
        )
    else:
        # Заморозка сервера, если недостаточно средств
        server["status"] = "Заморожен"
        server["freeze_reason"] = "Недостаточно средств."
        server["freeze_date"] = datetime.now().isoformat()
        save_users()
        bot.send_message(
            user["id"],
            f"❄️ Ваш сервер '{server['name']}' заморожен из-за отсутствия достаточного баланса. Пополните баланс в течение 3 дней.",
        )
        logging.info(
            f"Server '{server['name']}' for user {user['id']} frozen due to insufficient funds."
        )


# ---------------------- Проверка замороженных серверов ----------------------
def check_frozen_servers():
    logging.info("Checking for frozen servers...")
    for user in users_db["users"]:
        for server in user["servers"]:
            if server["status"] == "Заморожен":
                freeze_date = datetime.fromisoformat(server["freeze_date"])
                if datetime.now() > freeze_date + timedelta(days=3):
                    # Удаляем сервер, так как он оставался замороженным более трех дней
                    logging.info(
                        f"Deleting server '{server['name']}' for user {user['id']} after 3 days of being frozen."
                    )
                    execute_docker_command(server["id"], "kill")
                    execute_docker_command(server["id"], "rm")
                    user["servers"].remove(server)
                    save_users()
                    bot.send_message(
                        user["id"],
                        f"🗑️ Ваш сервер '{server['name']}' удален после более чем 3 дней заморозки.",
                    )
                    logging.info(
                        f"Server '{server['name']}' for user {user['id']} has been deleted after remaining frozen for too long."
                    )


# ---------------------- Ограничение по Частоте Запросов ----------------------
user_command_times = defaultdict(list)
RATE_LIMIT = 5555
TIME_WINDOW = 60


def is_rate_limited(user_id):
    current_time = time.time()
    user_command_times[user_id] = [
        cmd_time
        for cmd_time in user_command_times[user_id]
        if current_time - cmd_time < TIME_WINDOW
    ]
    if len(user_command_times[user_id]) >= RATE_LIMIT:
        return True
    user_command_times[user_id].append(current_time)
    return False


def rate_limit_decorator(handler):
    def wrapper(message, *args, **kwargs):
        if is_rate_limited(message.chat.id):
            bot.send_message(
                message.chat.id,
                "⚠️ Слишком много запросов. Пожалуйста, попробуйте позже.",
            )
            logging.warning(f"User {message.chat.id} is rate limited.")
            return
        return handler(message, *args, **kwargs)

    return wrapper


# ---------------------- Функции Клавиатур ----------------------
def send_main_keyboard(chat_id):
    markup = telebot.types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=3)
    markup.add(
        telebot.types.KeyboardButton("📂 Мои сервера"),
        telebot.types.KeyboardButton("🛒 Товары"),
    )
    markup.add(
        telebot.types.KeyboardButton("📞 Поддержка"),
        telebot.types.KeyboardButton("👤 Профиль"),
    )
    bot.send_message(
        chat_id,
        "📚 Главное меню: Выберите опцию ниже.",
        reply_markup=markup,
        parse_mode="Markdown",
    )


# ---------------------- Обработчики Команд Бота ----------------------
@bot.message_handler(commands=["start"])
def start_command(message):
    add_user(message.chat.id, message.from_user.username)
    logging.info(f"User {message.chat.id} started the bot.")
    send_main_keyboard(message.chat.id)


@bot.message_handler(commands=["help"])
def help_command(message):
    help_text = (
        "🛠️ Доступные команды:\n"
        "🛒 Товары - Приобрести сервер\n"
        "📂 Меню - Управление вашими серверами\n"
        "💰 Профиль - Просмотр вашего профиля\n"
        "📞 Поддержка - Связаться со службой поддержки\n"
        "🔧 Админ-панель - Доступна только администраторам"
    )
    bot.send_message(message.chat.id, help_text, parse_mode="Markdown")
    send_main_keyboard(message.chat.id)
    logging.info(f"Sent help to user {message.chat.id}.")


@bot.message_handler(commands=["profile"])
def profile_command(message):
    user = next(
        (user for user in users_db["users"] if user["id"] == message.chat.id), None
    )
    if user:
        server_count = len(user["servers"])
        servers = (
            "\n".join(
                [
                    f"• {server['name']} (Статус: {server['status']})"
                    for server in user["servers"]
                ]
            )
            or "Нет серверов"
        )
        profile_text = (
            f"🧑‍💼 Ваш профиль:\n\n"
            f"✅ ID пользователя: {user['id']}\n"
            f"💵 Баланс: ${user['balance']}\n"
            f"🖥️ Серверов: {server_count}\n\n"
            f"Список серверов:\n{servers}"
        )

        # Добавляем кнопку "Передать баланс"
        markup = telebot.types.InlineKeyboardMarkup(row_width=1)
        markup.add(
            telebot.types.InlineKeyboardButton(
                text="💸 Передать баланс", callback_data="transfer_balance"
            ),
        )

        bot.send_message(
            message.chat.id, profile_text, reply_markup=markup, parse_mode="Markdown"
        )
        logging.info(f"Displayed profile for user {message.chat.id}.")
    else:
        bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
        logging.warning(f"Profile check for non-existent user {message.chat.id}.")


@bot.callback_query_handler(func=lambda call: call.data == "transfer_balance")
def start_transfer_balance(call):
    bot.send_message(
        call.message.chat.id,
        "💸 Введите через пробел @username и сумму, которую хотите передать (например, @friend 10):",
    )
    bot.register_next_step_handler(call.message, handle_transfer_request)
    bot.answer_callback_query(call.id)


def handle_transfer_request(message):
    try:
        username, amount_str = message.text.split()
        if not username.startswith("@"):
            raise ValueError("Invalid username format")

        amount = float(amount_str)
        giver = next(
            (user for user in users_db["users"] if user["id"] == message.chat.id), None
        )

        if giver["balance"] < amount:
            bot.send_message(
                message.chat.id, "🚫 У вас недостаточно средств для этой операции."
            )
            return

        receiver = next(
            (user for user in users_db["users"] if user["username"] == username[1:]),
            None,
        )

        if not receiver:
            bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
            return

        if giver["id"] == receiver["id"]:
            bot.send_message(
                message.chat.id, "🚫 Вы не можете передать средства самому себе."
            )
            return

        giver["balance"] -= amount
        receiver["balance"] += amount
        save_users()

        bot.send_message(
            message.chat.id,
            f"✅ Вы передали ${amount} пользователю @{receiver['username']}.",
        )
        bot.send_message(
            receiver["id"],
            f"💸 Вам передали ${amount} от пользователя @{giver['username']}.",
        )

        logging.info(
            f"User {giver['id']} transferred ${amount} to user {receiver['id']}."
        )
    except ValueError as e:
        bot.send_message(
            message.chat.id,
            "⚠️ Формат ввода некорректен. Используйте: @username сумма",
        )
        logging.error(f"ValueError in transfer: {e}")
    except Exception as e:
        bot.send_message(
            message.chat.id, "⚠️ Произошла ошибка. Пожалуйста, попробуйте еще раз."
        )
        logging.error(f"Error in handle_transfer_request: {e}")


@bot.message_handler(commands=["balance"])
def balance_redirect(message):
    bot.send_message(
        message.chat.id,
        "⚠️ Эта команда была заменена на /profile. Используйте /profile для просмотра вашего баланса.",
    )
    logging.info(
        f"User {message.chat.id} attempted to use deprecated /balance command."
    )


@bot.message_handler(commands=["shop"])
def shop_command(message):
    user = next(
        (user for user in users_db["users"] if user["id"] == message.chat.id), None
    )
    if not user:
        add_user(message.chat.id, message.from_user.username)
        user = next(
            (user for user in users_db["users"] if user["id"] == message.chat.id), None
        )

    markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for plan in plan_names:
        markup.add(
            telebot.types.InlineKeyboardButton(
                text=f"{plan.capitalize()} 🛡️", callback_data=f"plan:{plan}"
            )
        )
    bot.send_message(
        message.chat.id,
        "🛒 **Выберите тарифный план:**",
        reply_markup=markup,
        parse_mode="Markdown",
    )
    logging.info(f"Displayed shop to user {message.chat.id}.")


@bot.message_handler(commands=["admin"])
def admin_command(message):
    if is_admin(message.chat.id):
        admin_text = (
            "🔧 Админ-панель:\n"
            "👥 /setadmin [ID] - Назначить пользователя админом\n"
            "🚫 /removeadmin [ID] - Удалить пользователя из админов\n"
            "📋 /listusers - Показать всех пользователей\n"
            "💸 /givebalance [ID] [AMOUNT] - Выдать баланс пользователю\n"
            "📑 /tickets - Панель тикетов поддержки\n"
            "🖥️ /viewservers - Просмотреть все сервера пользователей\n"
            "❄️ /freeze [SERVER_NAME] - Заморозить сервер\n"
            "🔓 /unfreeze [SERVER_NAME] - Разморозить сервер\n"
        )
        bot.send_message(message.chat.id, admin_text, parse_mode="Markdown")
        logging.info(f"Displayed admin panel to admin user {message.chat.id}.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к админ-панели")
        logging.warning(
            f"User {message.chat.id} attempted to access admin panel without permissions."
        )


@bot.message_handler(commands=["setadmin"])
def set_admin_command(message):
    if is_admin(message.chat.id):
        try:
            _, user_id = message.text.split()
            user_id = int(user_id)
        except (ValueError, IndexError):
            bot.send_message(
                message.chat.id, "⚠️ Используйте команду в формате /setadmin [ID]"
            )
            return

        if set_admin(user_id):
            bot.send_message(
                message.chat.id, f"✅ Пользователь {user_id} назначен администратором."
            )
        else:
            bot.send_message(
                message.chat.id,
                f"🚫 Не удалось назначить пользователя {user_id} администратором.",
            )
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")


@bot.message_handler(commands=["removeadmin"])
def remove_admin_command(message):
    if is_admin(message.chat.id):
        try:
            _, user_id = message.text.split()
            user_id = int(user_id)
        except (ValueError, IndexError):
            bot.send_message(
                message.chat.id, "⚠️ Используйте команду в формате /removeadmin [ID]"
            )
            return

        if set_admin(user_id, status=False):
            bot.send_message(
                message.chat.id, f"✅ Пользователь {user_id} удален из администраторов."
            )
        else:
            bot.send_message(
                message.chat.id,
                f"🚫 Не удалось удалить пользователя {user_id} из администраторов.",
            )
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")


@bot.message_handler(commands=["listusers"])
def list_users_command(message):
    if is_admin(message.chat.id):
        user_list = ["👥 Список пользователей:"]
        for user in users_db["users"]:
            user_info = f"• ID: {user['id']}, Имя: @{user.get('username', 'нет')}, Баланс: ${user['balance']}"
            user_list.append(user_info)
        bot.send_message(message.chat.id, "\n".join(user_list), parse_mode="Markdown")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")


@bot.message_handler(commands=["givebalance"])
def give_balance_command(message):
    if is_admin(message.chat.id):
        try:
            _, user_id, amount_str = message.text.split()
            user_id = int(user_id)
            amount = float(amount_str)
        except (ValueError, IndexError):
            bot.send_message(
                message.chat.id,
                "⚠️ Используйте команду в формате /givebalance [ID] [AMOUNT]",
            )
            return

        user = next((user for user in users_db["users"] if user["id"] == user_id), None)
        if user:
            user["balance"] += amount
            save_users()
            bot.send_message(
                message.chat.id,
                f"✅ Баланс пользователя {user_id} увеличен на ${amount}.",
            )
        else:
            bot.send_message(message.chat.id, f"🚫 Пользователь {user_id} не найден.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")


@bot.message_handler(commands=["tickets"])
def view_tickets_command(message):
    if not is_admin(message.chat.id):
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")
        return

    tickets_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for ticket in users_db["support_tickets"]:
        tickets_markup.add(
            telebot.types.InlineKeyboardButton(
                text=f"Тикет {ticket['id']} от @{ticket['username']}",
                callback_data=f"admin_ticket_view:{ticket['id']}",
            )
        )
    bot.send_message(
        message.chat.id, "🔎 Все тикеты поддержки:", reply_markup=tickets_markup
    )


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("admin_ticket_view:")
)
def admin_view_ticket(call):
    ticket_id = validate_integer(call.data.split(":")[1])
    admin_id = call.message.chat.id

    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )
    if not ticket:
        bot.send_message(admin_id, "⚠️ Тикет не найден.")
    else:
        markup = telebot.types.InlineKeyboardMarkup(row_width=1)
        markup.add(
            telebot.types.InlineKeyboardButton(
                text="Ответить", callback_data=f"reply_admin_ticket:{ticket_id}"
            ),
            telebot.types.InlineKeyboardButton(
                text="Удалить", callback_data=f"delete_ticket:{ticket_id}"
            ),
        )
        bot.send_message(
            admin_id,
            f"🔹 Тикет ID: {ticket['id']}\n"
            f"👤 Пользователь: @{ticket['username']}\n"
            f"📅 Дата: {ticket['date']}\n"
            f"💬 Сообщение: {ticket['message']}\n\n"
            f"Ответы:\n"
            + "\n".join(
                [
                    f'🗨️ {reply["from"]}: {reply["text"]} ({reply["date"]})'
                    for reply in ticket["replies"]
                ]
            ),
            reply_markup=markup,
        )

    bot.answer_callback_query(call.id)


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("reply_admin_ticket:")
)
def reply_admin_ticket(call):
    ticket_id = validate_integer(call.data.split(":")[1])
    admin_id = call.message.chat.id

    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )

    if ticket:
        bot.send_message(admin_id, f"✍️ Введите ваш ответ для тикета ID {ticket_id}:")
        bot.register_next_step_handler(
            call.message, handle_admin_reply_to_ticket, ticket_id
        )
    else:
        bot.send_message(admin_id, "⚠️ Тикет не найден.")

    bot.answer_callback_query(call.id)


def handle_admin_reply_to_ticket(message, ticket_id):
    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )

    if ticket:
        reply = {
            "from": "Администратор",
            "text": message.text,
            "date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        }
        ticket["replies"].append(reply)
        save_users()

        bot.send_message(
            ticket["user_id"],
            f"💬 Новый ответ на ваш тикет ID {ticket_id} от администратора.",
        )
        bot.send_message(
            ticket["user_id"], f"🗨️ {reply['from']}: {reply['text']} ({reply['date']})"
        )

        bot.send_message(
            message.chat.id, f"✅ Ваш ответ добавлен в тикет ID {ticket_id}."
        )
        logging.info(f"Admin {message.chat.id} replied to ticket {ticket_id}.")
    else:
        bot.send_message(message.chat.id, "⚠️ Тикет не найден.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("delete_ticket:"))
def admin_delete_ticket(call):
    ticket_id = validate_integer(call.data.split(":")[1])
    admin_id = call.message.chat.id

    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )
    if ticket:
        users_db["support_tickets"].remove(ticket)
        save_users()
        bot.send_message(admin_id, f"🗑️ Тикет {ticket_id} удален.")
        logging.info(f"Admin {admin_id} deleted ticket {ticket_id}.")
    else:
        bot.send_message(admin_id, "⚠️ Тикет не найден.")

    bot.answer_callback_query(call.id)


@bot.message_handler(commands=["viewservers"])
def view_servers_command(message):
    if is_admin(message.chat.id):
        servers = []
        for user in users_db["users"]:
            if user["servers"]:
                for server in user["servers"]:
                    servers.append((user["username"], server))

        if servers:
            markup = telebot.types.InlineKeyboardMarkup(row_width=1)
            for username, server in servers:
                markup.add(
                    telebot.types.InlineKeyboardButton(
                        text=f"{username}'s {server['name']} 🖥️",
                        callback_data=f"admin_server:{server['id']}",
                    )
                )
            bot.send_message(
                message.chat.id,
                "🖥️ **Список всех серверов пользователей:**",
                reply_markup=markup,
                parse_mode="Markdown",
            )
            logging.info(f"Admin {message.chat.id} requested to view servers.")
        else:
            bot.send_message(message.chat.id, "Нет активных серверов у пользователей.")
            logging.info(
                f"Admin {message.chat.id} requested to view servers but none were found."
            )
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")
        logging.warning(
            f"User {message.chat.id} attempted to access server view without permissions."
        )


@bot.message_handler(commands=["freeze"])
def freeze_command(message):
    if not is_admin(message.chat.id):
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")
        logging.warning(
            f"User {message.chat.id} attempted to use freeze command without permissions."
        )
        return

    try:
        _, server_name, *reason_parts = message.text.split()
        reason = " ".join(reason_parts)  # Сбор всех частей причины в одно сообщение
    except ValueError:
        bot.send_message(
            message.chat.id,
            "⚠️ Используйте команду в формате /freeze [SERVER_NAME] [REASON]",
        )
        return

    # Поиск сервера и выполнение заморозки
    for user in users_db["users"]:
        server = next((s for s in user["servers"] if s["name"] == server_name), None)
        if server:
            success, response = execute_docker_command(server["id"], "kill")
            if success:
                server["status"] = "Заморожен"
                server["freeze_reason"] = reason  # Сохранение причины
                save_users()
                bot.send_message(
                    user["id"],
                    f"❄️ Ваш сервер '{server['name']}' был заморожен по причине: {reason}.",
                )
                bot.send_message(
                    message.chat.id,
                    f"❄️ Сервер '{server['name']}' успешно заморожен по причине: {reason}.",
                )
                logging.info(
                    f"Server '{server['name']}' (ID: {server['id']}) frozen by admin {message.chat.id} with reason: {reason}."
                )
            else:
                bot.send_message(
                    message.chat.id,
                    f"⚠️ Не удалось заморозить сервер '{server['name']}'.\nОшибка: {response}",
                )
                logging.error(
                    f"Failed to freeze server '{server['name']}' (ID: {server['id']}) by admin {message.chat.id}: {response}"
                )
            return

    bot.send_message(message.chat.id, f"⚠️ Сервер с именем '{server_name}' не найден.")
    logging.warning(
        f"Admin {message.chat.id} attempted to freeze non-existent server '{server_name}'."
    )


@bot.message_handler(commands=["unfreeze"])
def unfreeze_command(message):
    if not is_admin(message.chat.id):
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")
        logging.warning(
            f"User {message.chat.id} attempted to use unfreeze command without permissions."
        )
        return

    try:
        _, server_name = message.text.split(maxsplit=1)
    except ValueError:
        bot.send_message(
            message.chat.id, "⚠️ Используйте команду в формате /unfreeze [SERVER_NAME]"
        )
        return

    for user in users_db["users"]:
        server = next((s for s in user["servers"] if s["name"] == server_name), None)
        if server:
            if server["status"] == "Заморожен":
                # Снимаем заморозку только в случае оплаты
                user_balance = user["balance"]
                plan_price = config["plans"][server["plan"]]["price"]
                if user_balance >= plan_price:
                    user["balance"] -= plan_price
                    server["status"] = "Запущен"
                    server["next_payment_due"] = (
                        datetime.now() + timedelta(days=30)
                    ).isoformat()
                    del server["freeze_reason"]
                    save_users()
                    bot.send_message(
                        user["id"],
                        f"🔓 Ваш сервер '{server['name']}' был разморожен и запущен.",
                    )
                    bot.send_message(
                        message.chat.id,
                        f"🔓 Сервер '{server['name']}' успешно разморожен и запущен.",
                    )
                    logging.info(
                        f"Server '{server['name']}' (ID: {server['id']}) unfrozen by admin {message.chat.id}."
                    )
                else:
                    bot.send_message(
                        message.chat.id,
                        f"⚠️ Недостаточно средств для разморозки сервера '{server['name']}'.",
                    )
            else:
                bot.send_message(
                    message.chat.id, "🚫 Сервер не находится в замороженном состоянии."
                )
                logging.info(
                    f"Attempted to unfreeze non-frozen server '{server['name']}' by admin {message.chat.id}."
                )
            return

    bot.send_message(message.chat.id, f"⚠️ Сервер с именем '{server_name}' не найден.")
    logging.warning(
        f"Admin {message.chat.id} attempted to unfreeze non-existent server '{server_name}'."
    )


# Обработчик текстовых команд из клавиатуры
@bot.message_handler(
    func=lambda message: message.text
    in ["📂 Мои сервера", "🛒 Товары", "📞 Поддержка", "👤 Профиль"]
)
def handle_text_commands(message):
    command = message.text
    if command == "📂 Мои сервера":
        menu_command(message)
    elif command == "🛒 Товары":
        shop_command(message)
    elif command == "📞 Поддержка":
        support_command(message)
    elif command == "👤 Профиль":
        profile_command(message)
    else:
        bot.send_message(
            message.chat.id,
            "❓ Неизвестная команда. Используйте кнопки ниже для взаимодействия с ботом.",
        )
        logging.warning(
            f"User {message.chat.id} sent an unknown command: {message.text}"
        )


@bot.message_handler(commands=["menu"])
def menu_command(message):
    user = next(
        (user for user in users_db["users"] if user["id"] == message.chat.id), None
    )
    if user:
        if user["servers"]:
            markup = telebot.types.InlineKeyboardMarkup(row_width=1)
            for server in user["servers"]:
                markup.add(
                    telebot.types.InlineKeyboardButton(
                        text=f"{server['name']} 🖥️",
                        callback_data=f"select_server:{server['id']}",
                    )
                )
            bot.send_message(
                message.chat.id,
                "📂 Выберите сервер для управления:",
                reply_markup=markup,
                parse_mode="Markdown",
            )
            logging.info(
                f"Displayed server list to user {message.chat.id} for selection."
            )
        else:
            bot.send_message(message.chat.id, "🚫 У вас нет серверов.")
            logging.info(f"User {message.chat.id} has no servers.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа.")
        logging.warning(
            f"Non-existent user {message.chat.id} attempted to access menu."
        )


# ---------------------- Обработчики Callback Query ----------------------
@bot.callback_query_handler(func=lambda call: call.data.startswith("plan:"))
def plan_callback(call):
    plan = call.data.split(":")[1]
    plan_info = config["plans"].get(plan)

    if not plan_info:
        bot.send_message(call.message.chat.id, "🚫 Некорректный план.")
        return

    # Путь к изображению тарифа
    image_path = f"tariffs/{plan}.jpg"

    # Проверяем, существует ли изображение для плана
    if not os.path.isfile(image_path):
        bot.send_message(call.message.chat.id, "🚫 Изображение тарифа не найдено.")
        logging.error(f"Image for plan '{plan}' not found at path '{image_path}'.")
        return

    # Показываем пользователю информацию о плане
    plan_details = (
        f"🛡️ Тарифный план: {plan.capitalize()}\n"
        f"- **Диск:** {plan_info['DISK']}\n"
        f"- **ОЗУ:** {plan_info['RAM']}\n"
        f"- **CPU:** {plan_info['CPU']} ядра\n"
        f"- **Цена:** `${plan_info['price']}`"
    )

    # Добавляем кнопки "Назад" и "Купить"
    markup = telebot.types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        telebot.types.InlineKeyboardButton(text="⬅️ Назад", callback_data="shop"),
        telebot.types.InlineKeyboardButton(
            text="🛒 Купить", callback_data=f"confirm_purchase:{plan}"
        ),
    )

    # Отправляем изображение вместе с описанием тарифа
    with open(image_path, "rb") as photo:
        bot.send_photo(
            call.message.chat.id,
            photo,
            caption=plan_details,
            reply_markup=markup,
            parse_mode="Markdown",
        )

    logging.info(
        f"User {call.message.chat.id} checked details of plan '{plan}'. Image sent."
    )
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("confirm_purchase:"))
def confirm_purchase_callback(call):
    plan = call.data.split(":")[1]
    user = next(
        (user for user in users_db["users"] if user["id"] == call.message.chat.id), None
    )

    if user and user["balance"] >= config["plans"][plan]["price"]:
        pending_server_creations[call.message.chat.id] = {"plan": plan}

        markup = telebot.types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            telebot.types.InlineKeyboardButton(
                text="✅ Да", callback_data=f"autorenew:yes"
            ),
            telebot.types.InlineKeyboardButton(
                text="❌ Нет", callback_data=f"autorenew:no"
            ),
        )
        bot.send_message(
            call.message.chat.id, "🔄 Включить автопродление?", reply_markup=markup
        )
        logging.info(
            f"User {call.message.chat.id} selected plan '{plan}' to purchase. Prompting for auto-renew."
        )
    else:
        bot.send_message(
            call.message.chat.id,
            "🚫 У вас недостаточно средств для покупки этого тарифа.",
        )
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data == "shop")
def return_to_shop(call):
    shop_command(call.message)
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("autorenew:"))
def autorenew_callback(call):
    response = call.data.split(":")[1]
    pending_server_creations[call.message.chat.id]["autorenew"] = response == "yes"
    bot.send_message(
        call.message.chat.id,
        (
            "ℹ️ Автопродление включено."
            if response == "yes"
            else "ℹ️ Автопродление отключено."
        ),
    )
    plan = pending_server_creations[call.message.chat.id]["plan"]

    versions = ["2022", "2025", "2019", "win10", "win11"]
    version_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for version in versions:
        version_display = (
            f"Windows Server {version}"
            if not version.startswith("win")
            else f"Windows {version[-2:].upper()}"
        )
        version_markup.add(
            telebot.types.InlineKeyboardButton(
                text=f"{version_display} 🖥️",
                callback_data=f"version:{version}:plan:{plan}",
            )
        )
    bot.send_message(
        call.message.chat.id,
        "📋 **Выберите версию Windows:**",
        reply_markup=version_markup,
        parse_mode="Markdown",
    )
    logging.info(
        f"User {call.message.chat.id} selected auto-renew option '{response}'. Prompting for OS version."
    )
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("select_server:"))
def select_server_callback(call):
    user_id = call.message.chat.id
    server_id = validate_integer(call.data.split(":")[1])

    if server_id is None:
        bot.send_message(user_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in select_server callback: {call.data}")
        bot.answer_callback_query(call.id)
        return

    user = next((user for user in users_db["users"] if user["id"] == user_id), None)
    if not user:
        bot.send_message(user_id, "🚫 Пользователь не найден.")
        logging.error(f"User {user_id} not found in select_server_callback.")
        bot.answer_callback_query(call.id)
        return

    server = next((s for s in user["servers"] if s["id"] == server_id), None)
    if not server:
        bot.send_message(user_id, "🚫 Сервер не найден.")
        logging.error(
            f"Server ID {server_id} not found for user {user_id} in select_server_callback."
        )
        bot.answer_callback_query(call.id)
        return

    send_server_control_message(user_id, server, is_admin=False)
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("admin_server:"))
def admin_server_callback(call):
    server_id = validate_integer(call.data.split(":")[1])
    admin_id = call.message.chat.id

    if server_id is None:
        bot.send_message(admin_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in admin_server callback: {call.data}")
        bot.answer_callback_query(call.id)
        return

    for user in users_db["users"]:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            send_server_control_message(admin_id, server, is_admin=True)
            bot.answer_callback_query(call.id)
            return

    bot.send_message(admin_id, "🚫 Сервер не найден.")
    logging.error(f"Server ID {server_id} not found.")
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("version:"))
def version_callback(call):
    data_parts = call.data.split(":")
    if len(data_parts) != 4:
        bot.send_message(
            call.message.chat.id, "⚠️ Некорректные данные. Попробуйте снова."
        )
        logging.error(
            f"Incorrect version callback data from user {call.message.chat.id}: {call.data}"
        )
        bot.answer_callback_query(call.id)
        return

    _, version, _, plan = data_parts

    supported_versions = ["2022", "2025", "2019", "win10", "win11"]

    if version not in supported_versions:
        bot.send_message(
            call.message.chat.id, "🚫 Выбранная версия Windows не поддерживается."
        )
        logging.warning(
            f"User {call.message.chat.id} selected unsupported version '{version}'."
        )
        bot.answer_callback_query(call.id)
        return

    pending_server_creations[call.message.chat.id]["version"] = version
    bot.send_message(
        call.message.chat.id,
        "📝 **Введите имя для вашего сервера:**",
        parse_mode="Markdown",
    )
    bot.register_next_step_handler(call.message, handle_server_name)
    logging.info(
        f"User {call.message.chat.id} selected version '{version}' for plan '{plan}'. Prompting for server name."
    )
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("start:")
    or call.data.startswith("stop:")
    or call.data.startswith("restart:")
    or call.data.startswith("delete:")
    or call.data.startswith("reinstall:")
)
def server_control_callback(call):
    action, server_id = call.data.split(":")
    server_id = validate_integer(server_id)
    user_id = call.message.chat.id

    if server_id is None:
        bot.send_message(user_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in callback: {call.data}")
        bot.answer_callback_query(call.id)
        return

    for user in users_db["users"]:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            # Restrict any server control if frozen
            if server["status"] == "Заморожен":
                bot.send_message(
                    user_id, "🚫 Сервер заморожен. Вы не можете управлять им."
                )
                logging.warning(
                    f"User {user_id} attempted to manage a frozen server '{server['name']}'."
                )
                bot.answer_callback_query(call.id)
                return

            if action == "reinstall":
                # Отправляем запрос на подтверждение переустановки
                confirm_markup = telebot.types.InlineKeyboardMarkup(row_width=2)
                confirm_markup.add(
                    telebot.types.InlineKeyboardButton(
                        text="✅ Подтвердить",
                        callback_data=f"confirm_reinstall:{server_id}",
                    ),
                    telebot.types.InlineKeyboardButton(
                        text="❌ Отмена", callback_data="cancel"
                    ),
                )
                bot.send_message(
                    user_id,
                    f"❗ Вы уверены, что хотите переустановить сервер ID {server_id}?",
                    reply_markup=confirm_markup,
                )
                return

            if action == "delete":
                confirm_markup = telebot.types.InlineKeyboardMarkup(row_width=2)
                confirm_markup.add(
                    telebot.types.InlineKeyboardButton(
                        text="✅ Подтвердить",
                        callback_data=f"confirm_delete:{server_id}",
                    ),
                    telebot.types.InlineKeyboardButton(
                        text="❌ Отмена", callback_data="cancel"
                    ),
                )
                bot.send_message(
                    user_id,
                    f"❗ Вы уверены, что хотите удалить сервер ID {server_id}?",
                    reply_markup=confirm_markup,
                )
                return

            success, response = execute_docker_command(server_id, action)
            if success:
                if action == "start":
                    server["status"] = "Запущен"
                elif action == "stop":
                    server["status"] = "Остановлен"
                elif action == "restart":
                    server["status"] = "Перезапущен"
                save_users()

                status_text = server["status"]
                bot.send_message(
                    user_id,
                    f"✅ Сервер '{server['name']}' успешно {status_text.lower()}.",
                )
                logging.info(
                    f"User {user_id} {action}ed server '{server['name']}' (ID: {server_id})."
                )

            else:
                bot.send_message(
                    user_id,
                    f"⚠️ Не удалось {action}ить сервер '{server['name']}'.\nОшибка: {response}",
                )
                logging.error(
                    f"Failed to {action} server '{server['name']}' (ID: {server_id}) for user {user_id}: {response}"
                )

    bot.answer_callback_query(call.id, f"✅ Действие {action} выполнено.")


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("confirm_reinstall:")
)
def confirm_reinstall_callback(call):
    server_id = validate_integer(call.data.split(":")[1])
    user_id = call.message.chat.id

    if server_id is None:
        bot.send_message(user_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in confirm_reinstall callback: {call.data}")
        bot.answer_callback_query(call.id)
        return

    restart_server_with_new_version(user_id, server_id)
    bot.answer_callback_query(call.id, "☑️ Переустановка сервера...")


def restart_server_with_new_version(user_id, server_id):
    version_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    versions = ["2022", "2025", "2019", "win10", "win11"]

    for version in versions:
        version_display = (
            f"Windows Server {version}"
            if not version.startswith("win")
            else f"Windows {version[-2:].upper()}"
        )
        version_markup.add(
            telebot.types.InlineKeyboardButton(
                text=f"{version_display} 🖥️",
                callback_data=f"reinstall_version:{server_id}:{version}",
            )
        )

    bot.send_message(
        user_id,
        "📋 **Выберите новую версию Windows для переустановки:**",
        reply_markup=version_markup,
        parse_mode="Markdown",
    )


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("reinstall_version:")
)
def handle_reinstall_version_selection(call):
    user_id = call.message.chat.id
    _, server_id, new_version = call.data.split(":")

    server_id = validate_integer(server_id)

    if server_id is None:
        bot.send_message(user_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in reinstall version selection: {call.data}")
        bot.answer_callback_query(call.id)
        return

    # Найти сервер
    user = next((user for user in users_db["users"] if user["id"] == user_id), None)
    if not user:
        bot.send_message(user_id, "🚫 Пользователь не найден.")
        logging.error(f"User {user_id} not found in reinstall_version_selection.")
        bot.answer_callback_query(call.id)
        return

    server = next((s for s in user["servers"] if s["id"] == server_id), None)
    if not server:
        bot.send_message(user_id, "🚫 Сервер не найден.")
        logging.error(
            f"Server ID {server_id} not found for user {user_id} in reinstall_version_selection."
        )
        bot.answer_callback_query(call.id)
        return

    # Останавливаем и удаляем контейнер
    success_kill, kill_response = execute_docker_command(server_id, "kill")
    if not success_kill:
        logging.error(
            f"Failed to kill container for server {server_id}. Response: {kill_response}"
        )
        bot.send_message(
            user_id,
            f"⚠️ Не удалось остановить сервер '{server_id}' перед переустановкой.",
        )
        return

    success_rm, rm_response = execute_docker_command(server_id, "rm")
    if not success_rm:
        logging.error(
            f"Failed to remove container for server {server_id}. Response: {rm_response}"
        )
        bot.send_message(user_id, f"⚠️ Не удалось удалить сервер '{server_id}'.")
        return

    # Модифицируем версию в docker-compose
    if not modify_docker_compose(user_id, server_id, new_version):
        logging.error(
            f"Failed to modify docker-compose.yml for server ID: {server_id}."
        )
        bot.send_message(
            user_id,
            "⚠️ Ошибка при модификации docker-compose.yml. Невозможно переустановить.",
        )
        return

    # Переустанавливаем сервер
    reinstall_user_server(user_id, server_id)
    bot.answer_callback_query(call.id)


def modify_docker_compose(user_id, server_id, new_version):
    ssh_client = connect_via_ssh(config["servers"][0])
    if not ssh_client:
        return False

    try:
        path = f"/root/windows{server_id}/docker-compose.yml"
        sftp = ssh_client.open_sftp()

        # Чтение текущего содержимого docker-compose.yml
        with sftp.open(path, "r") as file:
            content = file.read().decode()

        # Логика замены: находим строку с VERSION и заменяем значение
        lines = content.splitlines()
        for i, line in enumerate(lines):
            if "VERSION" in line:
                # Замена значения версии
                lines[i] = f'      VERSION: "{new_version}"'

        # Объединяем строки обратно в контент
        new_content = "\n".join(lines)

        # Запись обратно в файл
        with sftp.open(path, "w") as file:
            file.write(new_content)

        logging.info(
            f"Modified docker-compose.yml for server ID: {server_id} to version: {new_version}"
        )
        return True

    except Exception as error:
        logging.error(
            f"Failed to modify docker-compose.yml for server ID: {server_id}: {error}"
        )
        return False

    finally:
        ssh_client.close()


def reinstall_user_server(user_id, server_id):
    ssh_client = connect_via_ssh(config["servers"][0])
    if not ssh_client:
        return

    try:
        command = f"cd /root/windows{server_id} && docker-compose up -d"
        logging.info(f"Reinstalling server ID {server_id}")
        stdin, stdout, stderr = ssh_client.exec_command(command)
        exit_status = stdout.channel.recv_exit_status()
        if exit_status != 0:
            error = stderr.read()
            logging.error(f"Failed to reinstall the server (ID: {server_id}): {error}")
            bot.send_message(
                user_id, f"⚠️ Не удалось переустановить сервер '{server_id}'."
            )
        else:
            bot.send_message(
                user_id, f"✅ Сервер '{server_id}' успешно переустановлен."
            )

    except Exception as error:
        logging.error(
            f"Error reinstalling Docker container for server {server_id}: {error}"
        )

    finally:
        ssh_client.close()


#
# ------------- Обработчики для удаления сервера, функция удаления файлов, и создание сервера ----------
#
@bot.callback_query_handler(func=lambda call: call.data.startswith("confirm_delete:"))
def confirm_delete_callback(call):
    server_id = validate_integer(call.data.split(":")[1])
    user_id = call.message.chat.id

    if server_id is None:
        bot.send_message(user_id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in confirm_delete callback: {call.data}")
        bot.answer_callback_query(call.id)
        return

    for user in users_db["users"]:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            if server["status"] == "Запущен":
                success_kill, _ = execute_docker_command(server_id, "kill")
                if not success_kill:
                    bot.send_message(
                        user_id, f"⚠️ Не удалось остановить сервер '{server['name']}'."
                    )
                    return

            success_rm, response_rm = execute_docker_command(server_id, "rm")
            if not success_rm:
                bot.send_message(
                    user_id,
                    f"⚠️ Не удалось удалить сервер '{server['name']}'.\nОшибка: {response_rm}",
                )
                logging.error(
                    f"Failed to remove server '{server['name']}' (ID: {server_id}) for user {user_id}: {response_rm}"
                )
            else:
                remove_server_files(server_id)
                user["servers"] = [s for s in user["servers"] if s["id"] != server_id]
                save_users()

                try:
                    bot.delete_message(
                        chat_id=user_id, message_id=server.get("control_message_id")
                    )
                    logging.info(
                        f"Deleted control message for server '{server['name']}' (ID: {server_id}) for user {user_id}."
                    )
                except Exception as e:
                    logging.error(
                        f"Failed to delete message for server '{server['name']}' (ID: {server_id}): {e}"
                    )

                bot.send_message(
                    user_id, f"✅ Сервер '{server['name']}' успешно удален."
                )
                logging.info(
                    f"Server '{server['name']}' (ID: {server_id}) deleted for user {user_id}."
                )
            break
    else:
        bot.send_message(user_id, "⚠️ Сервер не найден.")

    bot.answer_callback_query(call.id, "✅ Сервер удален.")


def remove_server_files(server_id):
    server_info = config["servers"][0]
    ssh_client = connect_via_ssh(server_info)
    if ssh_client:
        try:
            command = f"rm -rf /root/windows{server_id}"
            logging.info(f"Removing server files for server ID: {server_id}: {command}")
            stdin, stdout, stderr = ssh_client.exec_command(command)
            exit_status = stdout.channel.recv_exit_status()
            if exit_status == 0:
                logging.info(f"Removed server files for server ID: {server_id}")
            else:
                error = stderr.read().decode().strip()
                logging.error(
                    f"Failed to remove server files for server ID: {server_id}: {error}"
                )
        except Exception as e:
            logging.error(
                f"Failed to remove server files for server ID: {server_id}: {e}"
            )
        finally:
            ssh_client.close()
    else:
        logging.error(
            f"SSH connection failed while deleting server files for server ID: {server_id}."
        )


def handle_server_name(message):
    user_id = message.chat.id

    if user_id not in pending_server_creations:
        bot.send_message(
            user_id,
            "⚠️ В процессе создания сервера произошла ошибка. Пожалуйста, попробуйте снова.",
        )
        logging.error(
            f"User {user_id} tried to set server name without pending creation."
        )
        return

    plan = pending_server_creations[user_id]["plan"]
    version = pending_server_creations[user_id]["version"]
    server_name = message.text.strip()

    if not server_name:
        bot.send_message(
            user_id, "⚠️ Имя сервера не может быть пустым. Пожалуйста, попробуйте снова."
        )
        logging.warning(f"User {user_id} provided empty server name.")
        return

    # Check uniqueness
    user = next((user for user in users_db["users"] if user["id"] == user_id), None)
    if user:
        if any(s["name"] == server_name for s in user["servers"]):
            bot.send_message(
                user_id,
                "⚠️ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.",
            )
            logging.warning(
                f"User {user_id} attempted to use duplicate server name '{server_name}'."
            )
            return
    else:
        bot.send_message(user_id, "🚫 Пользователь не найден.")
        logging.error(f"User {user_id} not found while setting server name.")
        return

    rdp_port = get_unique_port()
    vnc_port = get_unique_port()
    password = generate_random_password()
    del pending_server_creations[user_id]

    try:
        server_id = create_server_id()
        docker_compose_content = create_docker_compose(
            plan, server_id, version, rdp_port, vnc_port, password
        )
    except Exception as e:
        bot.send_message(user_id, "⚠️ Ошибка при создании конфигурации сервера.")
        logging.error(
            f"Failed to create docker compose for server {server_id} and user {user_id}: {e}"
        )
        logging.error(traceback.format_exc())
        return

    server_info = config["servers"][0]
    ssh_client = connect_via_ssh(server_info)
    if not ssh_client:
        bot.send_message(
            user_id,
            "🚫 Не удалось подключиться к серверу. Пожалуйста, попробуйте позже.",
        )
        logging.error(
            f"SSH connection failed while creating server {server_id} for user {user_id}."
        )
        return

    try:
        save_docker_compose(ssh_client, docker_compose_content, server_id)
    except Exception as e:
        bot.send_message(user_id, "⚠️ Не удалось сохранить конфигурацию сервера.")
        logging.error(f"Failed to save docker-compose.yml for server {server_id}: {e}")
        logging.error(traceback.format_exc())
        ssh_client.close()
        return

    if not validate_docker_compose(ssh_client, server_id):
        bot.send_message(
            user_id,
            "⚠️ Некорректный docker-compose.yml. Пожалуйста, свяжитесь с поддержкой.",
        )
        ssh_client.close()
        return

    try:
        logging.info(f"Starting Docker container for server {server_id}.")
        stdin, stdout, stderr = ssh_client.exec_command(
            f"cd /root/windows{server_id} && docker-compose up -d"
        )
        exit_status = stdout.channel.recv_exit_status()
        if exit_status != 0:
            logging.error(f"Failed to start Docker container for server {server_id}.")
            bot.send_message(
                user_id,
                "⚠️ Не удалось запустить контейнер. Пожалуйста, попробуйте позже.",
            )
            ssh_client.close()
            return
    except Exception as e:
        logging.error(f"Error starting Docker container for server {server_id}: {e}")
        bot.send_message(
            user_id, "⚠️ Не удалось запустить контейнер. Пожалуйста, попробуйте позже."
        )
        ssh_client.close()
        return

    ssh_client.close()

    server_entry = {
        "id": server_id,
        "name": server_name,
        "plan": plan,
        "status": "Запущен",
        "rdp_port": rdp_port,
        "vnc_port": vnc_port,
        "username": "admin",
        "password": password,
        "next_payment_due": (datetime.now() + timedelta(days=30)).isoformat(),
        "control_message_id": None,
    }
    if user:
        user["servers"].append(server_entry)
    else:
        bot.send_message(user_id, "🚫 Пользователь не найден.")
        logging.error(f"User {user_id} not found while updating server {server_id}.")
        return

    user["balance"] -= config["plans"][plan]["price"]
    save_users()

    server_details = (
        f"✅ **Сервер создан!**\n\n"
        f"**🖥️ Имя сервера:** `{server_name}`\n"
        f"👤 **Пользователь:** `admin`\n"
        f"🌐 **RDP IP:** {server_info['host']}:{rdp_port}\n"
        f"🌐 **VNC:** [http://{server_info['host']}:{vnc_port}](http://{server_info['host']}:{vnc_port})\n"
        f"🔑 **Пароль:** `{password}`\n\n"
        f"**🛡️ Характеристики:**\n"
        f"- **Диск:** {config['plans'][plan]['DISK']}\n"
        f"- **ОЗУ:** {config['plans'][plan]['RAM']}\n"
        f"- **CPU:** {config['plans'][plan]['CPU']} ядра\n"
        f"- **Цена:** `${config['plans'][plan]['price']}`"
    )

    control_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    control_markup.add(
        telebot.types.InlineKeyboardButton(
            text="В панель", callback_data=f"select_server:{server_id}"
        )
    )

    sent_message = bot.send_message(
        user_id, server_details, reply_markup=control_markup, parse_mode="Markdown"
    )
    logging.info(
        f"Server {server_id} created for user {user_id} with name '{server_name}'."
    )

    server_entry["control_message_id"] = sent_message.message_id
    save_users()


def send_server_control_message(chat_id, server, is_admin):
    markup = telebot.types.InlineKeyboardMarkup(row_width=4)

    if server["status"] != "Заморожен":
        markup.add(
            telebot.types.InlineKeyboardButton(
                text="🟢 Старт", callback_data=f"start:{server['id']}"
            ),
            telebot.types.InlineKeyboardButton(
                text="🛑 Стоп", callback_data=f"stop:{server['id']}"
            ),
            telebot.types.InlineKeyboardButton(
                text="🔄 Рестарт", callback_data=f"restart:{server['id']}"
            ),
            telebot.types.InlineKeyboardButton(
                text="🔄 Переустановить", callback_data=f"reinstall:{server['id']}"
            ),
            telebot.types.InlineKeyboardButton(
                text="❌ Удалить", callback_data=f"delete:{server['id']}"
            ),
        )

    bot.send_message(
        chat_id,
        compose_server_details(server),
        reply_markup=markup,
        parse_mode="Markdown",
    )


def compose_server_details(server):
    server_info = config["servers"][0]
    plan_info = config["plans"][server["plan"]]
    status_text = server["status"]

    details = (
        f"🔧 **Управление сервером '{server['name']}':**\n\n"
        f"**🛡️ Тарифный план:** {server['plan'].capitalize()}\n\n"
        f"**🛠️ Характеристики:**\n"
        f"- **Диск:** {plan_info['DISK']}\n"
        f"- **ОЗУ:** {plan_info['RAM']}\n"
        f"- **CPU:** {plan_info['CPU']} ядра\n"
        f"- **Цена:** `${plan_info['price']}`\n\n"
        f"**📌 Статус:** {status_text}\n"
    )

    if server["status"] == "Заморожен":
        details += f"**Причина заморозки:** {server.get('freeze_reason', 'Нет причины указано.')}\n"

    if server["status"] != "Заморожен":
        details += (
            f"**🌐 RDP IP:** {server_info['host']}:{server['rdp_port']}\n"
            f"**🌐 VNC:** [http://{server_info['host']}:{server['vnc_port']}](http://{server_info['host']}:{server['vnc_port']})\n"
            f"👤 **Пользователь:** `admin`\n"
            f"🔑 **Пароль:** `{server['password']}`"
        )

    return details


# ---------------------- Функция Поддержки ----------------------
@bot.message_handler(commands=["support"])
def support_command(message):
    markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        telebot.types.InlineKeyboardButton(
            text="Создать тикет 📝", callback_data="start_ticket_creation"
        ),
        telebot.types.InlineKeyboardButton(
            text="Мои тикеты 📋", callback_data="view_user_tickets"
        ),
    )
    bot.send_message(
        message.chat.id,
        "📞 Поддержка: выберите действие ниже.",
        reply_markup=markup,
        parse_mode="Markdown",
    )


@bot.callback_query_handler(func=lambda call: call.data == "start_ticket_creation")
def start_ticket_creation(call):
    bot.send_message(call.message.chat.id, "📝 Введите тему вашего тикета:")
    bot.register_next_step_handler(call.message, handle_ticket_subject)
    bot.answer_callback_query(call.id)


def handle_ticket_subject(message):
    subject = message.text.strip()
    if not subject:
        bot.send_message(
            message.chat.id, "⚠️ Тема не может быть пустой. Попробуйте снова."
        )
        return support_command(message)

    bot.send_message(message.chat.id, "💬 Теперь введите ваш вопрос или проблему:")
    bot.register_next_step_handler(message, handle_ticket_message, subject)


def handle_ticket_message(message, subject):
    ticket_id = len(users_db["support_tickets"]) + 1
    ticket = {
        "id": ticket_id,
        "user_id": message.chat.id,
        "username": message.from_user.username,
        "date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        "subject": subject,
        "message": message.text,
        "replies": [],
    }
    users_db["support_tickets"].append(ticket)
    save_users()

    bot.send_message(
        message.chat.id, "✅ Ваш тикет создан. Ожидайте ответ от поддержки."
    )

    notify_admins_about_new_ticket(ticket)
    create_ticket_chat_interface(message.chat.id, ticket_id)


def notify_admins_about_new_ticket(ticket):
    admin_users = [user for user in users_db["users"] if user["is_admin"]]
    for admin in admin_users:
        bot.send_message(
            admin["id"], f"🔔 Новый тикет ID {ticket['id']} от @{ticket['username']}"
        )


@bot.callback_query_handler(func=lambda call: call.data == "view_user_tickets")
def view_user_tickets(call):
    user_id = call.message.chat.id
    user_tickets = [t for t in users_db["support_tickets"] if t["user_id"] == user_id]

    if not user_tickets:
        bot.send_message(user_id, "ℹ️ У вас нет активных тикетов.")
        bot.answer_callback_query(call.id)
        return

    for ticket in user_tickets:
        ticket_interface(user_id, ticket)

    bot.answer_callback_query(call.id)


def ticket_interface(chat_id, ticket):
    markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        telebot.types.InlineKeyboardButton(
            text="Ответить", callback_data=f"reply_user_ticket:{ticket['id']}"
        )
    )
    bot.send_message(
        chat_id,
        f"📝 Тикет ID: {ticket['id']}\n"
        f"📅 Дата: {ticket['date']}\n"
        f"**Тема:** {ticket['subject']}\n"
        f"💬 **Сообщение:** {ticket['message']}\n\n"
        f"Ответы:\n"
        + "\n".join(
            [
                f'🗨️ {reply["from"]}: {reply["text"]} ({reply["date"]})'
                for reply in ticket["replies"]
            ]
        ),
        reply_markup=markup,
        parse_mode="Markdown",
    )


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("reply_user_ticket:")
)
def reply_user_ticket(call):
    ticket_id = validate_integer(call.data.split(":")[1])
    user_id = call.message.chat.id

    ticket = next(
        (
            t
            for t in users_db["support_tickets"]
            if t["id"] == ticket_id and t["user_id"] == user_id
        ),
        None,
    )

    if ticket:
        bot.send_message(user_id, f"✍️ Введите ваш ответ для тикета ID {ticket_id}:")
        bot.register_next_step_handler(
            call.message, handle_user_reply_to_ticket, ticket_id
        )
    else:
        bot.send_message(user_id, "⚠️ Тикет не найден.")

    bot.answer_callback_query(call.id)


def handle_user_reply_to_ticket(message, ticket_id):
    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )

    if ticket:
        reply = {
            "from": message.from_user.username,
            "text": message.text,
            "date": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
        }
        ticket["replies"].append(reply)
        save_users()

        notify_admins_about_reply(ticket_id)
        bot.send_message(
            message.chat.id, f"✅ Ваш ответ добавлен в тикет ID {ticket_id}."
        )
    else:
        bot.send_message(message.chat.id, "⚠️ Тикет не найден.")


def notify_admins_about_reply(ticket_id):
    admin_users = [user for user in users_db["users"] if user["is_admin"]]
    for admin in admin_users:
        bot.send_message(admin["id"], f"🔔 Новый ответ на тикет ID {ticket_id}.")


def create_ticket_chat_interface(chat_id, ticket_id):
    markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    markup.add(
        telebot.types.InlineKeyboardButton(
            text="Ответить", callback_data=f"reply_user_ticket:{ticket_id}"
        ),
        telebot.types.InlineKeyboardButton(
            text="Выйти из чата", callback_data="exit_ticket_chat"
        ),
    )
    bot.send_message(
        chat_id, f"💬 Вы вошли в чат тикета ID {ticket_id}.", reply_markup=markup
    )


@bot.callback_query_handler(
    func=lambda call: call.data.startswith("reply_admin_ticket:")
)
def reply_admin_ticket(call):
    ticket_id = validate_integer(call.data.split(":")[1])
    admin_id = call.message.chat.id

    ticket = next(
        (t for t in users_db["support_tickets"] if t["id"] == ticket_id), None
    )

    if ticket:
        bot.send_message(admin_id, f"✍️ Введите ваш ответ для тикета ID {ticket_id}:")
        bot.register_next_step_handler(
            call.message, handle_admin_reply_to_ticket, ticket_id
        )
    else:
        bot.send_message(admin_id, "⚠️ Тикет не найден.")

    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data == "exit_ticket_chat")
def exit_ticket_chat(call):
    bot.send_message(call.message.chat.id, "📴 Вы вышли из чата тикета.")
    send_main_keyboard(call.message.chat.id)
    bot.answer_callback_query(call.id)


# ---------------------- Обработчик Неизвестных Команд ----------------------
@bot.message_handler(func=lambda message: True)
def unknown_command(message):
    bot.send_message(
        message.chat.id,
        "❓ Неизвестная команда. Используйте кнопки ниже для взаимодействия с ботом или введите /help для получения списка доступных команд.",
    )
    logging.warning(f"User {message.chat.id} sent an unknown command: {message.text}")


# ---------------------- Функция Главного Запуска Бота ----------------------
def main():
    load_users()
    logging.info("Bot started")
    logging.info("Bot is polling...")
    while True:
        try:
            monthly_billing_check()  # Проверка ежемесячного биллинга
            check_frozen_servers()  # Проверка замороженных серверов
            bot.polling(none_stop=True)
        except Exception as e:
            logging.error(f"Bot polling failed: {e}")
            logging.error(traceback.format_exc())
            time.sleep(15)  # Wait before restarting polling


if __name__ == "__main__":
    main()