import json
import os
import random
import paramiko
import telebot
import string
import textwrap
import logging
import time
from dotenv import load_dotenv
from collections import defaultdict

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

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

# ---------------------- Конфигурация и Данные Настроек ----------------------
TOKEN = os.getenv("BOT_TOKEN")  # Убедитесь, что у вас есть BOT_TOKEN в вашем .env файле
if not TOKEN:
    logging.error("Bot token is not set. Please set BOT_TOKEN in your environment variables.")
    raise ValueError("Bot token is not set. Please set BOT_TOKEN in your environment variables.")

bot = telebot.TeleBot(TOKEN)

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

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

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

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

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


# ---------------------- Функции Утилиты ----------------------

def load_users():
    global users_db
    try:
        with open(db_file, "r") as file:
            users_db = json.load(file)
        logging.info("User database loaded.")
    except FileNotFoundError:
        save_users()
        logging.info("User database not found. Created new db.json.")


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


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


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'  # Разрешены: A-Z и 1-9
    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):
    plan_config = config["plans"][plan]
    rdp_port = get_unique_port()
    vnc_port = get_unique_port()
    password = generate_random_password()

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

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


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


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


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


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"]


# ---------------------- Ограничение по Частоте Запросов (Rate Limiting) ----------------------

user_command_times = defaultdict(list)
RATE_LIMIT = 5  # Количество команд
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


# ---------------------- Обработчики Команд Бота ----------------------

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


@bot.message_handler(commands=["help"])
def help_command(message):
    help_text = (
        "🛠️ **Доступные команды:**\n"
        "/shop - Приобрести сервер\n"
        "/admin - Админ-панель (только для админов)\n"
        "/menu - Меню управления серверами\n"
        "/balance - Проверить баланс\n"
        "/support - Связаться со службой поддержки\n"
        "/backup - Создать резервную копию данных\n"
        "/restore [backup_filename] - Восстановить данные из резервной копии"
    )
    bot.send_message(message.chat.id, help_text, parse_mode="Markdown")
    logging.info(f"Sent help to user {message.chat.id}.")


@bot.message_handler(commands=["balance"])
@rate_limit_decorator
def balance_command(message):
    user = next((user for user in users_db["users"] if user["id"] == message.chat.id), None)
    if user:
        bot.send_message(message.chat.id, f"💵 Ваш баланс: ${user['balance']}")
        logging.info(f"Displayed balance for user {message.chat.id}.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет баланса.")
        logging.warning(f"Balance check for non-existent user {message.chat.id}.")


@bot.message_handler(commands=["shop"])
@rate_limit_decorator
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=plan.capitalize(), callback_data=f"plan:{plan}"))
    bot.send_message(message.chat.id, "🛒 Выберите тарифный план:", reply_markup=markup)
    logging.info(f"Displayed shop to user {message.chat.id}.")


@bot.message_handler(commands=["admin"])
def admin_command(message):
    if is_admin(message.chat.id):
        admin_text = (
            "🔧 **Админ-панель:**\n"
            "/setadmin [ID] - Назначить пользователя админом\n"
            "/removeadmin [ID] - Удалить пользователя из админов\n"
            "/listusers - Показать всех пользователей\n"
            "/givebalance [ID] [AMOUNT] - Выдать баланс пользователю\n"
            "/viewtickets - Просмотреть все запросы в поддержку\n"
            "/backup - Создать резервную копию данных\n"
            "/restore [backup_filename] - Восстановить данные из резервной копии"
        )
        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", "removeadmin"])
def set_admin_command(message):
    if is_admin(message.chat.id):
        parts = message.text.split()
        if len(parts) != 2:
            bot.send_message(message.chat.id, "⚠️ Использование: /setadmin [ID] или /removeadmin [ID]")
            logging.warning(f"Incorrect admin command usage by {message.chat.id}: {message.text}")
            return
        command, user_id_str = parts
        user_id = validate_integer(user_id_str)
        if user_id is None:
            bot.send_message(message.chat.id, "⚠️ ID пользователя должно быть числом.")
            logging.warning(f"Non-integer user ID provided by {message.chat.id}: {user_id_str}")
            return
        if command == "/setadmin":
            success = set_admin(user_id, True)
            if success:
                bot.send_message(message.chat.id, f"✅ Пользователь {user_id} назначен администратором.")
                logging.info(f"User {user_id} set as admin by {message.chat.id}.")
            else:
                bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
                logging.error(f"Failed to set admin for non-existent user {user_id} by {message.chat.id}.")
        elif command == "/removeadmin":
            success = set_admin(user_id, False)
            if success:
                bot.send_message(message.chat.id, f"✅ Пользователь {user_id} удален из администраторов.")
                logging.info(f"User {user_id} removed from admin by {message.chat.id}.")
            else:
                bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
                logging.error(f"Failed to remove admin for non-existent user {user_id} by {message.chat.id}.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет прав.")
        logging.warning(f"User {message.chat.id} attempted to use admin commands without permissions.")


@bot.message_handler(commands=["givebalance"])
def give_balance_command(message):
    if is_admin(message.chat.id):
        parts = message.text.split()
        if len(parts) != 3:
            bot.send_message(message.chat.id, "⚠️ Использование: /givebalance [ID] [AMOUNT]")
            logging.warning(f"Incorrect givebalance command usage by {message.chat.id}: {message.text}")
            return
        _, user_id_str, amount_str = parts
        user_id = validate_integer(user_id_str)
        amount = validate_float(amount_str)
        if user_id is None or amount is None:
            bot.send_message(message.chat.id, "⚠️ ID должно быть числом и сумма должна быть числом.")
            logging.warning(
                f"Invalid parameters for givebalance by {message.chat.id}: ID={user_id_str}, AMOUNT={amount_str}")
            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"✅ Выдано ${amount} пользователю {user_id}.")
            logging.info(f"User {user_id} given ${amount} by admin {message.chat.id}.")
        else:
            bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
            logging.error(f"Attempted to give balance to non-existent user {user_id} by {message.chat.id}.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет прав.")
        logging.warning(f"User {message.chat.id} attempted to use givebalance without permissions.")


@bot.message_handler(commands=["menu"])
@rate_limit_decorator
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['id']}",
                    callback_data=f"control:{server['id']}"
                ))
            bot.send_message(message.chat.id, "🖥️ Ваши серверы:", reply_markup=markup)
            logging.info(f"Displayed server menu to user {message.chat.id}.")
        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.")


@bot.message_handler(commands=["support"])
@rate_limit_decorator
def support_command(message):
    user = next((user for user in users_db["users"] if user["id"] == message.chat.id), None)
    if user:
        bot.send_message(message.chat.id, "📝 Введите ваше сообщение для поддержки:")
        bot.register_next_step_handler(message, handle_support_message)
        logging.info(f"User {message.chat.id} initiated support request.")
    else:
        bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
        logging.warning(f"User {message.chat.id} attempted to use support without being registered.")


# ---------------------- Обработчики Сообщений Поддержки ----------------------

def handle_support_message(message):
    user = next((user for user in users_db["users"] if user["id"] == message.chat.id), None)
    if user:
        ticket = {
            "user_id": message.chat.id,
            "username": user["username"] if user["username"] else "NoUsername",
            "message": message.text,
            "status": "open",
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        }
        users_db["support_tickets"].append(ticket)
        save_users()
        # Notify all admins about the new support ticket
        admins = [user for user in users_db["users"] if user.get("is_admin", False)]
        for admin in admins:
            bot.send_message(admin["id"],
                             f"🔔 **Новый запрос в поддержку от {message.chat.id} ({ticket['username']}):**\n{ticket['message']}",
                             parse_mode="Markdown")
        bot.send_message(message.chat.id, "✅ Ваш запрос отправлен в поддержку.")
        logging.info(f"Support ticket from user {message.chat.id}: {message.text}")
    else:
        bot.send_message(message.chat.id, "🚫 Пользователь не найден.")
        logging.error(f"User {message.chat.id} not found while sending support message.")


# ---------------------- Обработчики Callback Query ----------------------

@bot.callback_query_handler(func=lambda call: call.data.startswith("plan:"))
def plan_callback(call):
    plan = call.data.split(":")[1]

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)
    if user and user["balance"] >= config["plans"][plan]["price"]:
        # Перейти к подтверждению без списания баланса
        server_id = create_server_id()

        # Подготовить сообщение с характеристиками сервера
        plan_info = config["plans"][plan]
        characteristics = (
            f"**Тарифный план:** {plan.capitalize()}\n"
            f"**Диск:** {plan_info['DISK']}\n"
            f"**ОЗУ:** {plan_info['RAM']}\n"
            f"**CPU:** {plan_info['CPU']} ядра\n"
            f"**Цена:** ${plan_info['price']}"
        )

        # Создать inline клавиатуру с кнопками "Назад" и "Купить"
        markup = telebot.types.InlineKeyboardMarkup(row_width=2)
        markup.add(
            telebot.types.InlineKeyboardButton(text="🔙 Назад", callback_data=f"back_plan:{plan}"),
            telebot.types.InlineKeyboardButton(text="🛒 Купить", callback_data=f"confirm_buy:{plan}")
        )

        # Отправить сообщение с характеристиками и кнопками
        bot.send_message(call.message.chat.id, characteristics, reply_markup=markup, parse_mode="Markdown")
        logging.info(
            f"User {call.message.chat.id} selected plan '{plan}'. Sent characteristics with Buy and Back options.")
    else:
        bot.send_message(call.message.chat.id, "🚫 У вас недостаточно средств для покупки этого тарифа.")
        logging.warning(f"User {call.message.chat.id} has insufficient balance for plan '{plan}'.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("back_plan:"))
def back_plan_callback(call):
    plan = call.data.split(":")[1]

    # Повторно отобразить меню выбора тарифных планов
    markup = telebot.types.InlineKeyboardMarkup(row_width=1)
    for plan in plan_names:
        markup.add(telebot.types.InlineKeyboardButton(text=plan.capitalize(), callback_data=f"plan:{plan}"))
    bot.send_message(call.message.chat.id, "🛒 Выберите тарифный план:", reply_markup=markup)
    logging.info(f"User {call.message.chat.id} clicked Back. Redisplaying plan selection.")


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

    if user and user["balance"] >= config["plans"][plan]["price"]:
        # Переход к выбору версии ОС
        versions = ["2022", "2025", "2019", "win10", "win11"]
        version_markup = telebot.types.InlineKeyboardMarkup(row_width=1)

        for version in versions:
            if version.startswith("win"):
                version_display = f"Windows {version[-2:].upper()}"
            else:
                version_display = f"Windows Server {version}"
            version_markup.add(
                telebot.types.InlineKeyboardButton(
                    text=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} confirmed purchase of plan '{plan}'. Prompting for OS version.")
    else:
        bot.send_message(call.message.chat.id, "🚫 У вас недостаточно средств для покупки этого тарифа.")
        logging.warning(f"User {call.message.chat.id} attempted to buy plan '{plan}' without sufficient balance.")


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

    _, version, _, plan = data_parts

    supported_versions = ["2022", "2025", "2019", "win10", "win11"]  # Убедитесь, что '2025' включен

    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}'.")
        return

    # Создать содержимое docker-compose
    try:
        server_id = create_server_id()
        docker_compose_content, rdp_port, vnc_port, password = create_docker_compose(plan, server_id, version)
    except Exception as e:
        bot.send_message(call.message.chat.id, "⚠️ Ошибка при создании конфигурации сервера.")
        logging.error(f"Failed to create docker compose for server {server_id}: {e}")
        return

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

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

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

    # Запустить Docker контейнер
    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()
        output = stdout.read().decode().strip()
        error = stderr.read().decode().strip()
        if exit_status != 0:
            logging.error(f"Failed to start Docker container for server {server_id}: {error}")
            bot.send_message(call.message.chat.id, "⚠️ Не удалось запустить контейнер. Пожалуйста, попробуйте позже.")
            ssh_client.close()
            return
        logging.info(f"Docker container for server {server_id} started successfully: {output}")
    except Exception as e:
        logging.error(f"Error starting Docker container for server {server_id}: {e}")
        bot.send_message(call.message.chat.id, "⚠️ Не удалось запустить контейнер. Пожалуйста, попробуйте позже.")
        ssh_client.close()
        return

    ssh_client.close()

    # Обновить данные пользователя
    server_entry = {
        "id": server_id,
        "plan": plan,
        "status": "Запущен",
        "rdp_port": rdp_port,
        "vnc_port": vnc_port,
        "password": password
    }
    user["servers"].append(server_entry)

    # Списать цену с баланса пользователя
    user["balance"] -= config["plans"][plan]["price"]
    save_users()

    # Отправить детали сервера пользователю
    bot.send_message(
        call.message.chat.id,
        f"✅ **Сервер создан!**\n\n"
        f"🌐 **RDP IP:** {server_info['host']}:{rdp_port}\n"
        f"🌐 **VNC:** [http://{server_info['host']}:{vnc_port}](http://{server_info['host']}:{vnc_port})\n"
        f"🔑 **Пароль:** `{password}`",
        parse_mode="Markdown"
    )
    logging.info(f"Server {server_id} created for user {call.message.chat.id}.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("control:"))
def control_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in control callback from user {call.message.chat.id}: {server_id_str}")
        return

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)
    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            server_status = f"**Статус:** {server['status']}\n"
            control_markup = telebot.types.InlineKeyboardMarkup(row_width=3)
            control_markup.row(
                telebot.types.InlineKeyboardButton(text="🛑 Остановить", callback_data=f"stop:{server_id}"),
                telebot.types.InlineKeyboardButton(text="🔄 Перезапустить", callback_data=f"restart:{server_id}"),
                telebot.types.InlineKeyboardButton(text="▶️ Запустить", callback_data=f"start:{server_id}")
            )
            control_markup.add(
                telebot.types.InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete:{server_id}")
            )
            bot.send_message(
                call.message.chat.id,
                f"🔧 **Управление сервером {server_id}:**\n{server_status}",
                reply_markup=control_markup,
                parse_mode="Markdown"
            )
            logging.info(f"Displayed control menu for server {server_id} to user {call.message.chat.id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(f"User {call.message.chat.id} attempted to control non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 У вас нет доступа к этому серверу.")
        logging.warning(f"Non-existent user {call.message.chat.id} attempted to access server controls.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("start:"))
def start_server_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in start callback from user {call.message.chat.id}: {server_id_str}")
        return

    execute_docker_command(server_id, "start")

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)
    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            server["status"] = "Запущен"
            save_users()
            bot.send_message(call.message.chat.id, f"✅ Сервер {server_id} запущен.")
            logging.info(f"User {call.message.chat.id} started server {server_id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(f"User {call.message.chat.id} attempted to start non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 Пользователь не найден.")
        logging.error(f"User {call.message.chat.id} not found while starting server {server_id}.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("stop:"))
def stop_server_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in stop callback from user {call.message.chat.id}: {server_id_str}")
        return

    execute_docker_command(server_id, "stop")

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)
    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            server["status"] = "Остановлен"
            save_users()
            bot.send_message(call.message.chat.id, f"🛑 Сервер {server_id} остановлен.")
            logging.info(f"User {call.message.chat.id} stopped server {server_id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(f"User {call.message.chat.id} attempted to stop non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 Пользователь не найден.")
        logging.error(f"User {call.message.chat.id} not found while stopping server {server_id}.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("restart:"))
def restart_server_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in restart callback from user {call.message.chat.id}: {server_id_str}")
        return

    execute_docker_command(server_id, "restart")

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)
    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            server["status"] = "Запущен"
            save_users()
            bot.send_message(call.message.chat.id, f"🔄 Сервер {server_id} перезапущен.")
            logging.info(f"User {call.message.chat.id} restarted server {server_id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(f"User {call.message.chat.id} attempted to restart non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 Пользователь не найден.")
        logging.error(f"User {call.message.chat.id} not found while restarting server {server_id}.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("delete:"))
def delete_server_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in delete callback from user {call.message.chat.id}: {server_id_str}")
        return

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)

    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            markup = telebot.types.InlineKeyboardMarkup(row_width=2)
            markup.add(
                telebot.types.InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete:{server_id}"),
                telebot.types.InlineKeyboardButton(text="❌ Нет", callback_data="cancel_delete")
            )
            bot.send_message(
                call.message.chat.id,
                f"❗ Вы уверены, что хотите удалить сервер {server_id}?",
                reply_markup=markup
            )
            logging.info(f"User {call.message.chat.id} initiated delete for server {server_id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(f"User {call.message.chat.id} attempted to delete non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 У вас нет доступа к этому серверу.")
        logging.warning(f"Non-existent user {call.message.chat.id} attempted to delete server {server_id}.")


@bot.callback_query_handler(func=lambda call: call.data.startswith("confirm_delete:"))
def confirm_delete_callback(call):
    server_id_str = call.data.split(":")[1]
    server_id = validate_integer(server_id_str)
    if server_id is None:
        bot.send_message(call.message.chat.id, "⚠️ Некорректный ID сервера.")
        logging.error(f"Invalid server ID in confirm_delete callback from user {call.message.chat.id}: {server_id_str}")
        return

    user = next((user for user in users_db["users"] if user["id"] == call.message.chat.id), None)

    if user:
        server = next((s for s in user["servers"] if s["id"] == server_id), None)
        if server:
            # Остановить и удалить Docker контейнер
            execute_docker_command(server_id, "stop")
            execute_docker_command(server_id, "rm")

            # Удалить файлы docker-compose.yml с сервера
            server_info = config["servers"][0]
            ssh_client = connect_via_ssh(server_info)
            if ssh_client:
                try:
                    command = f"rm -rf /root/windows{server_id}"
                    logging.info(f"Removing server files for server {server_id}: {command}")
                    ssh_client.exec_command(command)
                    logging.info(f"Removed server files for server {server_id}.")
                except Exception as e:
                    logging.error(f"Failed to remove server files for server {server_id}: {e}")
                finally:
                    ssh_client.close()
            else:
                logging.error(f"SSH connection failed while deleting server files for server {server_id}.")

            # Удалить сервер из списка пользователя
            user["servers"] = [s for s in user["servers"] if s["id"] != server_id]
            save_users()
            bot.send_message(call.message.chat.id, f"✅ Сервер {server_id} успешно удален.")
            logging.info(f"Server {server_id} deleted for user {call.message.chat.id}.")
        else:
            bot.send_message(call.message.chat.id, "🚫 Сервер не найден.")
            logging.error(
                f"User {call.message.chat.id} attempted to confirm delete for non-existent server {server_id}.")
    else:
        bot.send_message(call.message.chat.id, "🚫 Пользователь не найден.")
        logging.error(f"User {call.message.chat.id} not found while confirming delete for server {server_id}.")


@bot.callback_query_handler(func=lambda call: call.data == "cancel_delete")
def cancel_delete_callback(call):
    bot.send_message(call.message.chat.id, "❌ Удаление сервера отменено.")
    logging.info(f"User {call.message.chat.id} canceled server deletion.")


# ---------------------- Обработчики Поддержки ----------------------

@bot.message_handler(commands=["viewtickets"])
def view_tickets_command(message):
    if is_admin(message.chat.id):
        open_tickets = [ticket for ticket in users_db.get("support_tickets", []) if ticket["status"] == "open"]
        if not open_tickets:
            bot.send_message(message.chat.id, "📭 Нет открытых запросов в поддержку.")
            logging.info(f"Admin {message.chat.id} viewed tickets: No open tickets.")
            return

        for ticket in open_tickets:
            bot.send_message(
                message.chat.id,
                f"🔔 **Запрос от {ticket['user_id']} ({ticket['username']}):**\n{ticket['message']}\n🕒 {ticket['timestamp']}",
                parse_mode="Markdown"
            )
            # Опционально, добавить кнопки для отметки как решенные
        logging.info(f"Admin {message.chat.id} viewed all open support tickets.")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет доступа к этой команде.")
        logging.warning(f"User {message.chat.id} attempted to view support tickets without permissions.")


# ---------------------- Резервное Копирование и Восстановление ----------------------

@bot.message_handler(commands=["backup"])
def backup_command(message):
    if is_admin(message.chat.id):
        try:
            with open(db_file, "r") as file:
                data = json.load(file)
            backup_filename = f"backup_{int(time.time())}.json"
            with open(backup_filename, "w") as backup_file:
                json.dump(data, backup_file, indent=4)
            bot.send_message(message.chat.id, f"✅ Резервная копия создана: {backup_filename}")
            logging.info(f"Admin {message.chat.id} created a backup: {backup_filename}")
        except Exception as e:
            bot.send_message(message.chat.id, "⚠️ Не удалось создать резервную копию.")
            logging.error(f"Failed to create backup by admin {message.chat.id}: {e}")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет прав для выполнения этой команды.")
        logging.warning(f"User {message.chat.id} attempted to create a backup without permissions.")


@bot.message_handler(commands=["restore"])
def restore_command(message):
    if is_admin(message.chat.id):
        args = message.text.split()
        if len(args) != 2:
            bot.send_message(message.chat.id, "⚠️ Использование: /restore [backup_filename]")
            logging.warning(f"Incorrect restore command usage by admin {message.chat.id}: {message.text}")
            return
        _, backup_filename = args
        if not os.path.exists(backup_filename):
            bot.send_message(message.chat.id, "🚫 Файл резервной копии не найден.")
            logging.error(f"Backup file {backup_filename} not found for restore by admin {message.chat.id}.")
            return
        try:
            with open(backup_filename, "r") as backup_file:
                data = json.load(backup_file)
            with open(db_file, "w") as file:
                json.dump(data, file, indent=4)
            bot.send_message(message.chat.id, f"✅ Данные успешно восстановлены из {backup_filename}.")
            logging.info(f"Admin {message.chat.id} restored data from {backup_filename}.")
        except Exception as e:
            bot.send_message(message.chat.id, "⚠️ Не удалось восстановить данные.")
            logging.error(f"Failed to restore data from {backup_filename} by admin {message.chat.id}: {e}")
    else:
        bot.send_message(message.chat.id, "🚫 У вас нет прав для выполнения этой команды.")
        logging.warning(f"User {message.chat.id} attempted to restore data without permissions.")


# ---------------------- Обработчики Callback Query ----------------------

# Ошибка при предыдущем ответе: create_server_id уже определена выше
# Убедитесь, что все функции определены перед их использованием

# ---------------------- Функция Главного Запуска Бота ----------------------

def main():
    load_users()
    logging.info("Bot is polling...")
    try:
        bot.polling(none_stop=True)
    except Exception as e:
        logging.error(f"Bot polling failed: {e}")


if __name__ == "__main__":
    main()

мой код для создания и управления серверами через докер композ
вот его конфиг: 

{
    "plans": {
        "basic": {
            "DISK": "50G",
            "RAM": "2G",
            "CPU": "1",
            "price": 10.0
        },
        "standard": {
            "DISK": "100G",
            "RAM": "4G",
            "CPU": "2",
            "price": 20.0
        },
        "premium": {
            "DISK": "200G",
            "RAM": "8G",
            "CPU": "4",
            "price": 30.0
        }
    },
    "servers": [
        {
            "host": "78.46.62.242",
            "username": "root",
            "password": "imp6Vjj8s4z3Ui"
        }
    ],
    "rdp_ports": []
}

проблема что при том что когда я выбирал ос при покупке сервера то сервер не создало, бота крашнуло с таким логом:

D:\netpcbot>py main.py
2024-10-30 20:21:17,359 [INFO] Configuration loaded successfully.
2024-10-30 20:21:17,359 [INFO] User database loaded.
2024-10-30 20:21:17,359 [INFO] Bot is polling...
2024-10-30 20:21:21,256 [INFO] Displayed shop to user 7577326891.
2024-10-30 20:21:22,507 [INFO] User database saved.
2024-10-30 20:21:22,687 [INFO] User 7577326891 selected plan 'basic'. Sent characteristics with Buy and Back options.
2024-10-30 20:21:25,689 [INFO] User 7577326891 confirmed purchase of plan 'basic'. Prompting for OS version.
2024-10-30 20:21:29,298 [INFO] User database saved.
2024-10-30 20:21:29,299 [INFO] Configuration saved.
2024-10-30 20:21:29,300 [INFO] Configuration saved.
2024-10-30 20:21:29,669 [INFO] Connected (version 2.0, client OpenSSH_8.9p1)
2024-10-30 20:21:30,445 [INFO] Authentication (publickey) failed.
2024-10-30 20:21:30,697 [INFO] Authentication (password) successful!
2024-10-30 20:21:30,698 [INFO] SSH connection established to 78.46.62.242
2024-10-30 20:21:31,631 [INFO] [chan 0] Opened sftp connection (server version 3)
2024-10-30 20:21:31,772 [INFO] Created directory /root/windows2 on server.
2024-10-30 20:21:32,485 [INFO] [chan 0] sftp session closed.
2024-10-30 20:21:32,486 [INFO] docker-compose.yml saved successfully for server 2
2024-10-30 20:21:32,486 [INFO] Validating docker-compose.yml for server 2
2024-10-30 20:21:33,003 [INFO] Docker Compose file validated successfully for server 2
2024-10-30 20:21:33,003 [INFO] Starting Docker container for server 2.
2024-10-30 20:21:34,464 [INFO] Docker container for server 2 started successfully:
2024-10-30 20:21:34,526 [ERROR] Bot polling failed: name 'user' is not defined

D:\netpcbot>