Вот смотри, человек ввел сумму пополнения баланса и оплатил вот один доллар но у меня это в вебхуке не обработалось, и баланс не пополнился у него, но оплата проведена точно.
API cryptomus
```
Webhook
Webhook is a kind of feedback method for payment information.
When invoice status changes, a POST request is sent to the url_callback specified when creating the invoice.
Response
Response parameters
Name	Definition
type	Invoice type (wallet / payment)
uuid	uuid of the payment
order_id	Order ID in your system (to identify the order)
amount	The amount of the invoice
payment_amount	Amount actually paid by client
payment_amount_usd	Amount actually paid by client in USD
merchant_amount	The amount added to the merchant's balance, with all commissions subtracted.
commission	Cryptomus commission amount
is_final	Whether the invoice is finalized. When invoice is finalized it is impossible to pay an invoice (it's either paid or expired)
status	Payment status
Available options:
•
confirm_check
•
paid
•
paid_over
•
fail
•
wrong_amount
•
cancel
•
system_fail
•
refund_process
•
refund_fail
•
refund_paid
from	Payer's wallet address
wallet_address_uuid	uuid of the static wallet
network	The blockchain network in which the payment is made
currency	Invoice currency
payer_currency	The currency that the client actually paid with
additional_data	Additional information string that you provided when creating an invoice
convert	
Information about the currency to which the payment will be automatically converted. Conversion is performed from payer_currency to USDT
The convert field will not exist if you have not enabled the automatic conversion function for payer_currency (e.g. auto convert BTC to USDT)
Structure
txid	
Transaction hash on the blockchain.
The txid field will not exist if
1) payment was paid by p2p (The payer withdrew funds from his Cryptomus account to the address indicated in the invoice and the payment was made without blockchain, only in our system)
2) Payment was not paid
3) Something was wrong with the payment or the client made a mistake and we marked it as ‘paid’ manually
sign	Signature
Structure of convert
Name	Definition
to_currency	The currency code to which the payment will be converted
commission	Conversion fee
rate	Conversion rate
amount	
Conversion amount in to_currency that was added to the merchant's balance, with all commissions subtracted.
amount here equals merchant_amount * rate
Response example

{
  "type": "payment",
  "uuid": "62f88b36-a9d5-4fa6-aa26-e040c3dbf26d",
  "order_id": "97a75bf8eda5cca41ba9d2e104840fcd",
  "amount": "3.00000000",
  "payment_amount": "3.00000000",
  "payment_amount_usd": "0.23",
  "merchant_amount": "2.94000000",
  "commission": "0.06000000",
  "is_final": true,
  "status": "paid",
  "from": "THgEWubVc8tPKXLJ4VZ5zbiiAK7AgqSeGH",
  "wallet_address_uuid": null,
  "network": "tron",
  "currency": "TRX",
  "payer_currency": "TRX",
  "additional_data": null,
  "convert": {
    "to_currency": "USDT",
    "commission": null,
    "rate": "0.07700000",
    "amount": "0.22638000"
  },
  "txid": "6f0d9c8374db57cac0d806251473de754f361c83a03cd805f74aa9da3193486b",
  "sign": "a76c0d77f3e8e1a419b138af04ab600a"
}
COPY
Webhook verification
Since by receiving webhooks you are releasing products or crediting your users' balances, you need to make sure that you are receiving webhooks from cryptomus and not from anyone else.

We recommend you to check it both ways:

use the ip address whitelist and allow requests to url_callback only from our ips. We send webhooks from ip. 91.227.144.54
Verify signature in every webhook that is coming to your url_callback, read more about this below.
Verifying webhook signature
Your api keys are secret and no one except you and cryptomus should know them. So, when verifying the signature, you will be sure that the webhook was sent by cryptomus.

We create a sign using this algorithm. MD5 hash of the body of the POST request encoded in base64 and combined with your API key.

As the signature comes in the body of the request, to verify it, you need to extract the sign from the response body, generate a hash from the body and your API KEY and match it with the sign parameter.

An example in php:
To receive a json data sent by post to your webhook handler:


$data = file_get_contents('php://input');
$data = json_decode($data, true);
COPY

Lets say we received webhook with data in this array
First, we need to extract the sign from the array:


$sign = $data['sign'];
unset($data['sign']);
COPY

Now lets generate a sign using our api payment key:


$hash = md5(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . $apiPaymentKey);
COPY

Finally, we can check if the sign we generated with our api payment key equals the sign that came to webhook.


if (!hash_equals($hash, $sign)) {
   return new InvalidHashException();
}

// or

if ($hash !== $sign) {
   return new InvalidHashException();
}
COPY

At this point, you can be sure that the webhook was from cryptomus and that you received all the data correctly

There is a difference when encoding an array of data in php and other languages. PHP does escape slashes and some other languages don’t. Therefore, you may encounter a sign mismatch. You have to escape slashes with backslash to make it work properly.
in php:


//  data array
$data = [
    'amount' => '20',
    'currency' => 'USDT',
    'network' => 'tron',
    'txid' => 'someTxidWith/Slash'
];

// json data we send to webhooks
$data = json_encode($data, true);
echo $data;
// Outputs a string, slash in txid is escaped, pay attention to this.
// we send a webhook data with all escaped slashes
// {"amount":"20","currency":"USD","network":"btc","txid":"someTxidWith/Slash"}
COPY
in js:


const data = {
    amount: '20',
    currency: 'USDT',
    network: 'tron',
    txid: 'someTxidWith/Slash'
};

const jsonData = JSON.stringify(data);
console.log(jsonData);
// {"amount":"20","currency":"USDT","network":"tron","txid":"someTxidWith/Slash"}
// slash in txid is not escaped and you will get error checking sign.
// Instead, you should do it like this:
// const jsonData = JSON.stringify(data).replace(///mg, "\/");
            
```
my code (sign точно правильно генерируется в вебхуке, это было проверено в генерации платежа, платеж с этим методом генерации подписи генерируется правильно.)
```
import base64
import requests
import hmac
import hashlib
import uuid
from flask import Flask, request, jsonify
import json
import os
import random
import threading
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()
app = Flask(__name__)
# ---------------------- Настройка Логирования ----------------------
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."
    )

CRYPTOMUS_API_KEY = os.getenv("CRYPTOMUS_API_KEY")
CRYPTOMUS_API_SECRET = os.getenv("CRYPTOMUS_API_SECRET")
CRYPTOMUS_CALLBACK_URL = os.getenv("CRYPTOMUS_CALLBACK_URL")
CRYPTOMUS_BASE_URL = 'https://api.cryptomus.com/v1'

if not CRYPTOMUS_API_KEY or not CRYPTOMUS_API_SECRET or not CRYPTOMUS_CALLBACK_URL:
    logging.error("Cryptomus configurations are not set in environment variables.")
    raise ValueError("Cryptomus configurations are missing.")

bot = telebot.TeleBot(TOKEN)

# Загрузка конфигурации из config.json
try:
    with open("config.json", "r", encoding="utf-8") as file:
        config = json.load(file)
        logging.info("Configuration loaded successfully.")
except FileNotFoundError:
    logging.error("config.json file not found.")
    raise
except json.JSONDecodeError as e:
    logging.error(f"JSON decoding error: {e}")
    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)
            if 'pending_payments' not in users_db:
                users_db['pending_payments'] = []
            logging.info("User database loaded.")
    except FileNotFoundError:
        save_users()
        logging.info("User database not found. Created new db.json.")


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}"
          KEYBOARD: "ru-RU"
        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, node_name=None):
    # Retrieve node based on node_name
    if node_name is None:
        logging.error("Node name is required for executing docker command.")
        return False, "Node name not specified."
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if not node:
        logging.error(f"Node '{node_name}' not found in configuration.")
        return False, "Node not found."

    ssh_client = connect_via_ssh(node)
    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 {node['host']}")
    else:
        logging.error(
            f"Cannot execute docker command '{command}' on server {node['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 parse_size(size_str):
    if size_str.endswith('G'):
        return float(size_str[:-1])
    elif size_str.endswith('GB'):
        return float(size_str[:-2])
    elif size_str.endswith('M'):
        return float(size_str[:-1]) / 1024
    elif size_str.endswith('MB'):
        return float(size_str[:-2]) / 1024
    else:
        # Assume GB
        return float(size_str)


def calculate_node_available_resources(node):
    total_CPU = int(node["CPU"])
    total_RAM = parse_size(node["RAM"])
    total_DISK = parse_size(node["DISK"])

    used_CPU = 0
    used_RAM = 0.0
    used_DISK = 0.0

    for user in users_db["users"]:
        for server in user["servers"]:
            if server.get("node_name") == node["name"]:
                plan = server["plan"]
                plan_config = config["plans"][plan]
                used_CPU += int(plan_config["CPU"])
                used_RAM += parse_size(plan_config["RAM"])
                used_DISK += parse_size(plan_config["DISK"])

    available_CPU = total_CPU - used_CPU
    available_RAM = total_RAM - used_RAM
    available_DISK = total_DISK - used_DISK

    return {
        "CPU": available_CPU,
        "RAM": available_RAM,
        "DISK": available_DISK
    }


# ---------------------- Основная проверка биллинга ----------------------
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:
        if server["status"] == "Заморожен":
            return
        else:
            # Заморозка сервера, если недостаточно средств
            server["status"] = "Заморожен"
            server["freeze_reason"] = "Недостаточно средств."
            server["freeze_date"] = datetime.now().isoformat()
            save_users()
            bot.send_message(
                user["id"],
                f"❄️ Ваш сервер '{server['name']}' заморожен из-за отсутствия достаточного баланса. Пополните баланс в течении 5-ти дней.",
            )
            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=5):
                    # Удаляем сервер, так как он оставался замороженным более трех дней
                    logging.info(
                        f"Deleting server '{server['name']}' for user {user['id']} after 5 days of being frozen."
                    )
                    execute_docker_command(server["id"], "kill", server["node_name"])
                    execute_docker_command(server["id"], "rm", server["node_name"])
                    remove_server_files(server["id"], server["node_name"])
                    user["servers"].remove(server)
                    save_users()
                    bot.send_message(
                        user["id"],
                        f"🗑️ Ваш сервер '{server['name']}' удален после более чем 5 дней заморозки.",
                    )
                    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 = 10  # Количество разрешенных запросов
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=2)
    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"
        "• /start - Перезапустить бота\n"
        "• /shop - Открыть магазин и приобрести сервер\n"
        "• /menu - Управление вашими серверами\n"
        "• /profile - Просмотр вашего профиля\n"
        "• /support - Связаться со службой поддержки\n"
        "• /admin - Админ-панель (доступна только администраторам)"
    )
    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"])
        if server_count > 0:
            servers = "\n".join(
                [
                    f"• `{server['name']}` (Статус: {server['status']})"
                    for server in user["servers"]
                ]
            )
        else:
            servers = "*У вас нет активных серверов.*"

        profile_text = (
            f"👤 *Ваш профиль*\n"
            f"——————————————\n"
            f"🆔 *ID пользователя:* `{user['id']}`\n"
            f"💰 *Баланс:* `${user['balance']:.2f}`\n"
            f"💻 *Кол-во серверов:* `{server_count}`\n\n"
            f"{servers}"
        )

        # Добавляем кнопку "Передать баланс"
        markup = telebot.types.InlineKeyboardMarkup(row_width=1)
        markup.add(
            telebot.types.InlineKeyboardButton(
                text="💸 Передать баланс", callback_data="transfer_balance"
            ),
            telebot.types.InlineKeyboardButton(
                text="💳 Пополнить баланс", callback_data="replenish_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)

        # Проверка, что сумма положительная
        if amount <= 0:
            bot.send_message(
                message.chat.id,
                "⚠️ Сумма должна быть положительным числом. Пожалуйста, введите корректную сумму.",
            )
            logging.warning(
                f"User {message.chat.id} попытался передать некорректную сумму: {amount}"
            )
            return

        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\n"
            "👥 `/setadmin [ID]` - Назначить пользователя админом\n"
            "🚫 `/removeadmin [ID]` - Удалить пользователя из админов\n"
            "📋 `/listusers` - Показать всех пользователей\n"
            "💸 `/givebalance [ID] [AMOUNT]` - Выдать баланс пользователю\n"
            "🧸 `/setbalance [ID] [AMOUNT]` - Установить конкретный баланс пользователю\n"
            "📑 `/tickets` - Панель тикетов поддержки\n"
            "🖥️ `/viewservers` - Просмотреть все сервера пользователей\n"
            "❄️ `/freeze [SERVER_NAME] [REASON]` - Заморозить сервер\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, "🚫 У вас нет доступа к этой команде.")


def save_users():
    try:
        with open(db_file, "w") as file:
            if 'pending_payments' not in users_db:
                users_db['pending_payments'] = []
            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}")


@bot.message_handler(commands=["setbalance"])
def set_balance_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 setbalance command without permissions."
        )
        return

    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,
            "⚠️ Используйте команду в формате /setbalance [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}`.",
        )
        logging.info(
            f"Admin {message.chat.id} set balance of user {user_id} to ${amount}."
        )
    else:
        bot.send_message(message.chat.id, f"🚫 Пользователь {user_id} не найден.")
        logging.warning(
            f"Admin {message.chat.id} attempted to set balance of non-existent user {user_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:
            # Заморозка сервера
            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}."
            )
            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"——————————————\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
    )

    plan_price = config["plans"][plan]["price"]

    # Проверка, что цена плана положительная
    if plan_price <= 0:
        bot.send_message(
            call.message.chat.id,
            "⚠️ Некорректная цена выбранного тарифа. Пожалуйста, свяжитесь с поддержкой.",
        )
        logging.error(
            f"Plan '{plan}' has non-positive price: {plan_price}. Purchase aborted."
        )
        bot.answer_callback_query(call.id)
        return

    if user and user["balance"] >= 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", "2016", "win10", "win11", "winxp"]
    version_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for version in versions:
        version_display = (
            f"Windows Server {version}"
            if version.isdigit()
            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",
        "2016",
        "winxp",
    ]

    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
    # Prompt for node selection here
    # Build InlineKeyboardMarkup with nodes
    node_markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for node in config["nodes"]:
        node_name = node["name"]
        node_description = node.get("description", "")
        node_markup.add(
            telebot.types.InlineKeyboardButton(
                text=f"{node_name} ({node_description})",
                callback_data=f"node:{node_name}",
            )
        )
    bot.send_message(
        call.message.chat.id,
        "📍 Пожалуйста, выберите ноду для размещения вашего сервера:",
        reply_markup=node_markup,
        parse_mode="Markdown",
    )
    logging.info(
        f"User {call.message.chat.id} selected version '{version}' for plan '{plan}'. Prompting for node selection."
    )
    bot.answer_callback_query(call.id)


@bot.callback_query_handler(func=lambda call: call.data.startswith("node:"))
def node_callback(call):
    node_name = call.data.split(":")[1]
    user_id = call.message.chat.id

    # Find the node in config
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if node:
        pending_server_creations[user_id]["node_name"] = node_name
        bot.send_message(
            user_id,
            "📝 Пожалуйста, введите имя для вашего сервера:",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(call.message, handle_server_name)
        logging.info(
            f"User {user_id} selected node '{node_name}'. Prompting for server name."
        )
    else:
        bot.send_message(user_id, "⚠️ Выбранная нода не найдена. Попробуйте снова.")
        logging.error(f"Node '{node_name}' not found in configuration.")
    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

            node_name = server.get("node_name")
            if node_name is None:
                bot.send_message(
                    user_id,
                    "⚠️ Не удалось определить ноду сервера. Пожалуйста, свяжитесь с поддержкой.",
                )
                logging.error(
                    f"Server '{server['name']}' (ID: {server_id}) has no node_name specified."
                )
                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, node_name)
            if success:
                if action == "start":
                    server["status"] = "Запущен"
                elif action == "stop":
                    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", "2016", "win10", "win11", "winxp"]

    for version in versions:
        version_display = (
            f"Windows Server {version}"
            if version.isdigit()
            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_str, new_version = call.data.split(":")
    server_id = validate_integer(server_id_str)

    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

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

    if new_version not in supported_versions:
        bot.send_message(user_id, "🚫 Выбранная версия Windows не поддерживается.")
        logging.warning(f"User {user_id} selected unsupported version '{new_version}'.")
        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

    node_name = server.get("node_name")
    if node_name is None:
        bot.send_message(
            user_id,
            "⚠️ Не удалось определить ноду сервера. Пожалуйста, свяжитесь с поддержкой.",
        )
        logging.error(
            f"Server '{server['name']}' (ID: {server_id}) has no node_name specified."
        )
        bot.answer_callback_query(call.id)
        return
    if server["status"] != "Остановлен":
        # Останавливаем и удаляем контейнер
        success_kill, kill_response = execute_docker_command(server_id, "kill", node_name)
        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", node_name)
    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, node_name):
        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, node_name)
    bot.answer_callback_query(call.id)


def modify_docker_compose(user_id, server_id, new_version, node_name):
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if not node:
        logging.error(f"Node '{node_name}' not found in configuration.")
        return False

    ssh_client = connect_via_ssh(node)
    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, node_name):
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if not node:
        logging.error(f"Node '{node_name}' not found in configuration.")
        return

    ssh_client = connect_via_ssh(node)
    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"] == "Запущен":
                if server["status"] != "Остановлен":
                    success_kill, kill_response = execute_docker_command(server_id, "kill", server["node_name"])
                    if not success_kill:
                        bot.send_message(
                            user_id, f"⚠️ Не удалось остановить сервер '{server['name']}'."
                        )
                        logging.error(
                            f"Failed to kill server '{server['name']}' (ID: {server_id}) for user {user_id}: {kill_response}"
                        )
                        return

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

                try:
                    if server.get("control_message_id"):
                        bot.delete_message(
                            chat_id=user_id, message_id=server["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, node_name):
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if not node:
        logging.error(f"Node '{node_name}' not found in configuration.")
        return

    ssh_client = connect_via_ssh(node)
    if ssh_client:
        try:
            # Команда для удаления файлов сервера
            remove_command = f"rm -rf /root/windows{server_id}"
            logging.info(
                f"Removing server files for server ID: {server_id}: {remove_command}"
            )
            stdin, stdout, stderr = ssh_client.exec_command(remove_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}"
                )

            # Добавление команды для очистки неиспользуемых сетей Docker
            prune_command = "docker network prune -f"
            logging.info(
                f"Executing command to remove unused Docker networks: {prune_command}"
            )
            stdin, stdout, stderr = ssh_client.exec_command(prune_command)
            exit_status = stdout.channel.recv_exit_status()

            if exit_status == 0:
                logging.info(
                    f"Successfully pruned unused Docker networks for server ID: {server_id}"
                )
            else:
                error = stderr.read().decode().strip()
                logging.error(
                    f"Failed to prune Docker networks for server ID: {server_id}: {error}"
                )

            # Добавление команды для очистки неиспользуемых volume Docker
            volume_prune_command = "docker volume prune -f"
            logging.info(
                f"Executing command to remove unused Docker volumes: {volume_prune_command}"
            )
            stdin, stdout, stderr = ssh_client.exec_command(volume_prune_command)
            exit_status = stdout.channel.recv_exit_status()

            if exit_status == 0:
                logging.info(
                    f"Successfully pruned unused Docker volumes for server ID: {server_id}"
                )
            else:
                error = stderr.read().decode().strip()
                logging.error(
                    f"Failed to prune Docker volumes for server ID: {server_id}: {error}"
                )

        except Exception as e:
            logging.error(
                f"Failed to execute server cleanup commands 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"]
    node_name = pending_server_creations[user_id]["node_name"]
    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

    # Find the selected node
    node = next((n for n in config["nodes"] if n["name"] == node_name), None)
    if not node:
        bot.send_message(user_id, "⚠️ Выбранная нода не найдена.")
        # Optionally, restart the process
        return

    # Now, check resource availability
    plan_config = config["plans"][plan]
    required_CPU = float(plan_config["CPU"])
    required_RAM = parse_size(plan_config["RAM"])
    required_DISK = parse_size(plan_config["DISK"])

    # Function to calculate node available resources
    available_resources = calculate_node_available_resources(node)
    if (available_resources["CPU"] < required_CPU or
        available_resources["RAM"] < required_RAM or
        available_resources["DISK"] < required_DISK):
        bot.send_message(
            user_id,
            "⚠️ На выбранной ноде недостаточно ресурсов для создания сервера. Пожалуйста, выберите другую ноду.",
        )
        # Optionally, re-prompt for node selection
        return

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

    server_id = create_server_id()
    try:
        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

    ssh_client = connect_via_ssh(node)
    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,
        "node_name": node_name,
    }
    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"✅ **Сервер создан! (Ждите установки 10-30 минут.)**\n\n"
        f"**🖥️ Имя сервера:** `{server_name}`\n"
        f"👤 **Пользователь:** `admin`\n"
        f"🌐 **RDP IP:** `{node['host']}:{rdp_port}`\n"
        f"🌐 **VNC:** [http://{node['host']}:{vnc_port}](http://{node['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']}`\n"
        f"🌐 **Нода:** `{node_name}` ({node.get('description', '')})"
    )

    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=3)

    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']}"
            ),
        )
        markup.add(
            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):
    node = next((n for n in config["nodes"] if n["name"] == server["node_name"]), None)
    if not node:
        node = {"host": "Unknown", "name": "Unknown", "description": ""}
    plan_info = config["plans"][server["plan"]]
    status_text = server["status"]

    details = (
        f"🔧 **Управление сервером '{server['name']}':**\n"
        f"——————————————\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:** `{node['host']}:{server['rdp_port']}`\n"
            f"**🌐 VNC:** [http://{node['host']}:{server['vnc_port']}](http://{node['host']}:{server['vnc_port']})\n"
            f"👤 **Пользователь:** `admin`\n"
            f"🔑 **Пароль:** `{server['password']}`\n"
            f"🌐 **Нода:** `{server['node_name']}` ({node.get('description', '')})"
        )

    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"——————————————\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 create_cryptomus_payment(amount, user_id):
    order_id = str(uuid.uuid4())

    data = {
        'amount': str(amount),
        'currency': 'USDT', 
        'order_id': order_id,
        'url_callback': CRYPTOMUS_CALLBACK_URL
    }

    data = json.dumps(data, separators=(',', ':'))
    sign = hashlib.md5((base64.b64encode(data.encode('utf-8')).decode('utf-8') + CRYPTOMUS_API_SECRET).encode('utf-8')).hexdigest()

    url = 'https://api.cryptomus.com/v1/payment'
    headers = {
        'merchant': CRYPTOMUS_API_KEY,
        'sign': sign,
        'Content-Type': 'application/json'
    }

    response = requests.post(url, headers=headers, data=data)
    if response.status_code == 200:
        response_data = response.json()
        # Сохраняем информацию о платеже
        users_db.setdefault('pending_payments', []).append({
            'order_id': order_id,
            'user_id': user_id,
            'amount': amount,
            'status': 'pending'
        })
        save_users()
        return response_data
    else:
        logging.error(f"Failed to create payment: {response.text}")
        return None

def handle_replenish_amount(message):
    try:
        amount = float(message.text)
        if amount <= 0:
            bot.send_message(
                message.chat.id,
                "⚠️ Сумма должна быть положительным числом. Попробуйте снова."
            )
            return

        # Создаем платеж в Cryptomus
        payment_info = create_cryptomus_payment(amount, message.chat.id)
        if payment_info:
            # Отправляем пользователю ссылку на оплату
            pay_url = payment_info.get('result', {}).get('url')
            if pay_url:
                bot.send_message(
                    message.chat.id,
                    f"💳 Для пополнения баланса на `${amount}`, пожалуйста, перейдите по ссылке и совершите оплату:\n{pay_url}"
                )
                logging.info(f"Payment link sent to user {message.chat.id} for amount ${amount}.")
            else:
                bot.send_message(
                    message.chat.id,
                    "⚠️ Ошибка при получении ссылки для оплаты. Пожалуйста, попробуйте позже."
                )
                logging.error(f"Payment creation failed for user {message.chat.id}. No pay_url.")
        else:
            bot.send_message(
                message.chat.id,
                "⚠️ Не удалось создать запрос на оплату. Пожалуйста, попробуйте позже."
            )
            logging.error(f"Payment creation failed for user {message.chat.id}.")
    except ValueError:
        bot.send_message(
            message.chat.id,
            "⚠️ Пожалуйста, введите корректную сумму числом (например, '50')."
        )

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 == "replenish_balance")
def replenish_balance_callback(call):
    bot.send_message(
        call.message.chat.id,
        "💵 Пожалуйста, введите сумму, на которую вы хотите пополнить баланс (например, '50'):"
    )
    bot.register_next_step_handler(call.message, handle_replenish_amount)
    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)


@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}")

@app.route('/cryptomus_callback', methods=['POST'])
def cryptomus_callback():
    data = request.get_json()
    if not data:
        return jsonify({'status': 'error', 'message': 'No data received'}), 400

    # Проверка подписи
    sign = request.headers.get('sign')
    if not sign:
        return jsonify({'status': 'error', 'message': 'No signature provided'}), 400

    expected_sign = hashlib.md5((base64.b64encode(data.encode('utf-8')).decode('utf-8') + CRYPTOMUS_API_SECRET).encode('utf-8')).hexdigest()

    if sign != expected_sign:
        logging.error("Invalid signature in callback")
        return jsonify({'status': 'error', 'message': 'Invalid signature'}), 400
    payment_status = data.get('status') or data.get('payment_status')
    if payment_status == 'paid':
        order_id = data.get('order_id')
        amount = float(data.get('amount', 0))

        # Поиск информации о платеже
        pending_payment = next((p for p in users_db.get('pending_payments', []) if p['order_id'] == order_id), None)
        if pending_payment:
            user_id = pending_payment['user_id']
            # Обновление баланса пользователя
            user = next((user for user in users_db['users'] if user['id'] == user_id), None)
            if user:
                user['balance'] += amount
                pending_payment['status'] = 'paid'
                save_users()
                bot.send_message(user_id, f"💰 Ваш баланс пополнен на `${amount}`.")
                logging.info(f"User {user_id} balance replenished by ${amount}.")
                return jsonify({'status': 'success'}), 200
            else:
                logging.error(f"User not found for order_id {order_id}")
                return jsonify({'status': 'error', 'message': 'User not found'}), 400
        else:
            logging.error(f"Pending payment not found for order_id {order_id}")
            return jsonify({'status': 'error', 'message': 'Order not found'}), 400
    else:
        return jsonify({'status': 'success'}), 200  # Игнорируем другие статусы


def periodic_checks():
    while True:
        try:
            check_frozen_servers()
            monthly_billing_check()
        except Exception as e:
            logging.error(f"Periodic check failed: {e}")
            logging.error(traceback.format_exc())
        time.sleep(60) 

# ---------------------- Функция Главного Запуска Бота ----------------------
def run_flask_app():
    app.run(host='0.0.0.0', port=5000)

def main():
    load_users()
    logging.info("Bot started")
    threading.Thread(target=periodic_checks, daemon=True).start()
    threading.Thread(target=run_flask_app).start()
    logging.info("Bot is polling...")
    while True:
        try:
            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()
```