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

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

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

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

bot = telebot.TeleBot(TOKEN)

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

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

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

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

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

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

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

def add_user(user_id, username):
    if username not in users_db["users"]:
        users_db["users"][username] = {
            "password": "",
            "servers": [],
            "balance": 0,
            "tickets": [],
            "logs": [],
            "created_at": datetime.now().isoformat(),
            "role": "user",
        }
        save_users()
        log_action(username, "register")
        logging.info(f"Added new user: {username}")
        bot.send_message(
            user_id,
            "👋 Добро пожаловать! Используйте кнопки ниже для взаимодействия с ботом.",
        )
        send_main_keyboard(user_id)

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

def is_admin(user_id):
    user = bot.get_chat_member(user_id, user_id)
    username = user.user.username
    return users_db["users"].get(username, {}).get("role") == "admin"

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

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

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

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

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

def create_server_id():
    server_ids = [s["id"] for u in users_db["users"].values() for s in u["servers"]]
    return max(server_ids) + 1 if server_ids else 1

def log_action(username, action, ip="127.0.0.1", user_agent="tgbot-1.0"):
    log_entry = {
        "timestamp": datetime.now().isoformat(),
        "username": username,
        "action": action,
        "browser": "Telegram",
        "os": "Unknown",
        "device": "Bot",
        "ip": ip,
    }
    if username in users_db["users"]:
        users_db["users"][username]["logs"].append(log_entry)
    users_db["logs"].append(log_entry)
    save_users()

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

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

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

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

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

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

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

    return wrapper

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

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

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

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

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

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

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

if __name__ == "__main__":
    main()


import os
import json
import traceback
import string
import random
from datetime import datetime, timedelta
from functools import wraps
import uuid

import paramiko
from flask import Flask, jsonify, request, render_template, send_from_directory
from flask_cors import CORS
from flask_jwt_extended import (
    JWTManager, create_access_token,
    jwt_required, get_jwt_identity,
    verify_jwt_in_request
)
from passlib.hash import pbkdf2_sha256
from logging.handlers import RotatingFileHandler
from werkzeug.exceptions import HTTPException, NotFound
from apscheduler.schedulers.background import BackgroundScheduler
from jinja2 import Template
import posixpath
import logging
import user_agents

# Load environment variables for sensitive configurations
from dotenv import load_dotenv
load_dotenv()  # Loads variables from a .env file

# SSH Configuration
SSH_CONFIG = {
    'hostname': os.getenv('SSH_HOSTNAME', 'your_server_ip'),  # Replace with your server IP or hostname
    'port': int(os.getenv('SSH_PORT', 22)),
    'username': os.getenv('SSH_USERNAME', 'your_ssh_username'),
    'password': os.getenv('SSH_PASSWORD'),  # Ensure this is set
    'key_filepath': os.getenv('SSH_KEY_FILEPATH')  # Optional: path to SSH private key
}

# Load the server IP from the environment variable
SERVER_IP = os.getenv('SERVER_IP', 'your_server_ip')  # Replace with your server IP

app = Flask(
    __name__,
    static_folder='static',
    static_url_path='/static',  # Ensure static files are served from /static
    template_folder='templates'
)
CORS(app)

# JWT Configuration
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'your_jwt_secret_key_here')  # Set this in environment
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
jwt = JWTManager(app)

# Server Plans
SERVER_PLANS = [
    {
        "key": "basic",
        "name": "Базовый",
        "cpu": 2,
        "ram": 4,
        "disk": "50GB",
        "price": 500
    },
    {
        "key": "advanced",
        "name": "Продвинутый",
        "cpu": 4,
        "ram": 8,
        "disk": "100GB",
        "price": 1000
    },
    {
        "key": "premium",
        "name": "Премиум",
        "cpu": 8,
        "ram": 16,
        "disk": "200GB",
        "price": 2000
    }
]

OS_OPTIONS = {
    'win2020': 'Windows Server 2020',
    'win2022': 'Windows Server 2022',
    'win2025': 'Windows Server 2025',
    'win10': 'Windows 10',
    'win11': 'Windows 11'
}

# Define Remote Containers Directory
REMOTE_CONTAINERS_DIR = "/root"

# Define Remote Containers Directory
REMOTE_CONTAINERS_DIR = "/root"  # Base directory on the remote server

# Add the missing get_remote_server_dir function
def get_remote_server_dir(container_name):
    """
    Returns the remote directory path for the given container_name.
    """
    return posixpath.join(REMOTE_CONTAINERS_DIR, container_name)

# Initialize IP Pool
IP_POOL_START = [192, 168, 1, 100]  # Starting IP address
current_ip = IP_POOL_START.copy()

# Log directories (local)
LOGS_DIR = 'logs'
if not os.path.exists(LOGS_DIR):
    os.makedirs(LOGS_DIR)

# Configure Logging
def setup_logging():
    formatter = logging.Formatter(
        '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
    )
    file_handler = RotatingFileHandler(
        os.path.join(LOGS_DIR, 'app.log'),
        maxBytes=1024 * 1024,
        backupCount=10
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)

    error_file_handler = RotatingFileHandler(
        os.path.join(LOGS_DIR, 'error.log'),
        maxBytes=1024 * 1024,
        backupCount=10
    )
    error_file_handler.setFormatter(formatter)
    error_file_handler.setLevel(logging.ERROR)

    app.logger.addHandler(file_handler)
    app.logger.addHandler(error_file_handler)
    app.logger.setLevel(logging.INFO)

    @app.before_request
    def log_request_info():
        app.logger.info(f'Request: {request.method} {request.url} from {request.remote_addr}')
        app.logger.debug(f'Headers: {dict(request.headers)}')

setup_logging()

# Error Handlers
@app.errorhandler(NotFound)
def handle_404(e):
    app.logger.error(f'404 Error: {request.url} not found.')
    return render_template('404.html'), 404

@app.errorhandler(Exception)
def handle_exception(e):
    if isinstance(e, HTTPException):
        return e
    app.logger.error(f'Unhandled Exception: {str(e)}')
    app.logger.error(traceback.format_exc())
    return jsonify({'error': 'An internal server error occurred'}), 500

# Database Functions
def load_db():
    try:
        db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db.json')
        if not os.path.exists(db_path):
            with open(db_path, 'w', encoding='utf-8') as f:
                json.dump({'users': {}, 'logs': []}, f, indent=2)
            app.logger.info('Initialized new db.json with default structure.')

        with open(db_path, 'r', encoding='utf-8') as f:
            content = f.read().strip()
            app.logger.debug(f'Content of db.json: {content}')  # Debug log
            if not content:
                app.logger.warning('db.json is empty. Initializing with default structure.')
                db = {'users': {}, 'logs': []}
            else:
                db = json.loads(content)

        db.setdefault('logs', [])
        db.setdefault('users', {})

        # Clean and validate user data
        cleaned_users = {}
        for username, user in db['users'].items():
            if isinstance(user, dict):
                user.setdefault('role', 'user')
                cleaned_users[username] = user
            else:
                app.logger.warning(f"Skipping invalid user entry: {username} is not a dictionary.")

        db['users'] = cleaned_users

        save_db(db)
        return db
    except json.JSONDecodeError as e:
        app.logger.error(f'JSON Decode Error in load_db: {e}')
        raise
    except Exception as e:
        app.logger.error(f'Database load error: {str(e)}')
        app.logger.error(traceback.format_exc())
        raise

def save_db(db):
    db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'db.json')
    with open(db_path, 'w', encoding='utf-8') as f:
        json.dump(db, f, indent=2, ensure_ascii=False)

def log_activity(username, action, user_agent_string):
    try:
        db = load_db()
        ua = user_agents.parse(user_agent_string)
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'username': username,
            'action': action,
            'browser': ua.browser.family,
            'os': ua.os.family,
            'device': ua.device.family,
            'ip': request.remote_addr
        }
        db['logs'].append(log_entry)
        save_db(db)
    except Exception as e:
        app.logger.error(f'Error logging activity: {str(e)}')
        app.logger.error(traceback.format_exc())

def generate_password(length=12):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

def connect_via_ssh():
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    connect_params = {
        'hostname': SSH_CONFIG['hostname'],
        'port': SSH_CONFIG.get('port', 22),
        'username': SSH_CONFIG['username']
    }
    if SSH_CONFIG.get('key_filepath'):
        connect_params['key_filename'] = SSH_CONFIG['key_filepath']
    elif SSH_CONFIG.get('password'):
        connect_params['password'] = SSH_CONFIG['password']
    else:
        raise ValueError("Either 'key_filepath' or 'password' must be provided in SSH_CONFIG.")

    try:
        ssh.connect(**connect_params)
        app.logger.info(f"SSH connection established to {SSH_CONFIG['hostname']}")
    except Exception as e:
        app.logger.error(f'SSH connection failed: {e}')
        return None
    return ssh

def execute_ssh_command(command, ssh_client, remote_path=None):
    try:
        if remote_path:
            command = f"cd {remote_path} && {command}"

        app.logger.info(f"Executing SSH Command: {command}")
        stdin, stdout, stderr = ssh_client.exec_command(command)
        exit_status = stdout.channel.recv_exit_status()
        stdout_str = stdout.read().decode().strip()
        stderr_str = stderr.read().decode().strip()
        return stdout_str, stderr_str, exit_status
    except Exception as e:
        app.logger.error(f'SSH command execution error: {e}')
        return None, str(e), -1

def create_docker_compose_content(container_name, version, plan_details, rdp_username, rdp_password, vnc_password, public_port_8006, public_port_rdp_tcp, public_port_rdp_udp, public_port_vnc):
    docker_compose_template = """
version: '3'
services:
  windows:
    image: dockurr/windows
    container_name: {{ container_name }}
    environment:
      VERSION: "{{ version }}"
      DISK_SIZE: "{{ disk_size }}"
      RAM_SIZE: "{{ ram_size }}"
      CPU_CORES: "{{ cpu_cores }}"
      USERNAME: "{{ username }}"
      PASSWORD: "{{ password }}"
      VNC_PASSWORD: "{{ vnc_password }}"
      KEYBOARD: "us"
    devices:
      - /dev/kvm
    cap_add:
      - NET_ADMIN
    ports:
      - "{{ public_port_8006 }}:8006/tcp"
      - "{{ public_port_rdp_tcp }}:3389/tcp"
      - "{{ public_port_rdp_udp }}:3389/udp"
      - "{{ public_port_vnc }}:5900/tcp"
    stop_grace_period: 2m
    restart: unless-stopped
    """
    template = Template(docker_compose_template)
    return template.render(
        container_name=container_name,
        version=version,
        disk_size=plan_details['disk'],
        ram_size=plan_details['ram'],
        cpu_cores=plan_details['cpu'],
        username=rdp_username,
        password=rdp_password,
        vnc_password=vnc_password,
        public_port_8006=public_port_8006,
        public_port_rdp_tcp=public_port_rdp_tcp,
        public_port_rdp_udp=public_port_rdp_udp,
        public_port_vnc=public_port_vnc
    )

def is_valid_username(username):
    import re
    pattern = re.compile(r'^[A-Za-z0-9_]{3,30}$')
    return bool(pattern.match(username))

# Admin Decorator
def admin_required(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        verify_jwt_in_request()
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)
        if not user_data or user_data.get('role') != 'admin':
            app.logger.warning(f'Unauthorized admin access attempt by user: {current_user}')
            return jsonify({'error': 'Admin access required'}), 403
        return fn(*args, **kwargs)
    return wrapper

# Routes
@app.route('/')
def home():
    return render_template('index.html')

@app.route('/admin')
def admin_page():
    return render_template('admin.html')

# Serve favicon
@app.route('/favicon.ico')
def favicon():
    return send_from_directory(os.path.join(app.root_path, 'static'),
                               'favicon.ico', mimetype='image/vnd.microsoft.icon')

@app.route('/api/login', methods=['POST'])
def login():
    try:
        app.logger.info('Processing login request')
        db = load_db()
        data = request.get_json()

        if not data:
            app.logger.warning('No JSON data in login request')
            return jsonify({'error': 'No JSON data provided'}), 400

        username = data.get('username')
        password = data.get('password')

        app.logger.info(f'Login attempt for user: {username}')

        if not username or not password:
            app.logger.warning('Missing username or password in login request')
            return jsonify({'error': 'Username and password are required'}), 400

        user_data = db['users'].get(username)
        if not user_data:
            app.logger.warning(f'Login failed: User {username} not found')
            return jsonify({'error': 'Invalid credentials'}), 401

        # Only use the password verification method
        if pbkdf2_sha256.verify(password, user_data['password']):
            app.logger.info(f'Login successful for user: {username}')
            access_token = create_access_token(identity=username)
            try:
                log_activity(username, 'login', request.headers.get('User-Agent', ''))
            except Exception as e:
                app.logger.error(f'Failed to log login activity: {str(e)}')
            return jsonify({
                'token': access_token,
                'username': username,
                'balance': user_data.get('balance', 0),
                'role': user_data.get('role', 'user')  # Include role in response
            }), 200

        app.logger.warning(f'Login failed: Invalid password for user {username}')
        return jsonify({'error': 'Invalid credentials'}), 401

    except Exception as e:
        app.logger.error(f'Login error: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/register', methods=['POST'])









def register():
    try:
        app.logger.info('Processing registration request')
        db = load_db()
        data = request.get_json()

        if not data:
            app.logger.warning('No JSON data in registration request')
            return jsonify({'error': 'No JSON data provided'}), 400

        username = data.get('username')
        password = data.get('password')

        app.logger.info(f'Registration attempt for username: {username}')

        if not username or not password:
            app.logger.warning('Missing username or password in registration request')
            return jsonify({'error': 'Username and password are required'}), 400

        if not is_valid_username(username):
            app.logger.warning(f'Invalid username attempted: {username}')
            return jsonify({'error': 'Invalid username. Please use only letters, numbers, and underscores. Length between 3-30 characters.'}), 400

        if username in db['users']:
            app.logger.warning(f'Registration failed: Username {username} already exists')
            return jsonify({'error': 'Username already taken'}), 400

        hashed_password = pbkdf2_sha256.hash(password)

        db['users'][username] = {
            'password': hashed_password,
            'servers': [],
            'balance': 0,
            'tickets': [],
            'logs': [],
            'created_at': datetime.now().isoformat(),
            'role': 'user'  # Assign 'user' role by default
        }

        save_db(db)
        app.logger.info(f'Registration successful for user: {username}')
        log_activity(username, 'register', request.headers.get('User-Agent', ''))
        return jsonify({'message': 'Registration successful'}), 201

    except Exception as e:
        app.logger.error(f'Registration error: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/servers', methods=['GET', 'POST'])
@jwt_required()
def manage_servers():
    current_user = get_jwt_identity()
    db = load_db()

    user_data = db['users'].get(current_user)
    if not user_data:
        app.logger.warning(f'User {current_user} not found when accessing /api/servers')
        return jsonify({'error': 'User not found'}), 404

    if request.method == 'GET':
        servers = user_data.get('servers', [])
        return jsonify(servers), 200

    elif request.method == 'POST':
        data = request.get_json()

        if not data or 'plan' not in data or 'os' not in data or 'name' not in data:
            app.logger.warning('Invalid request data for server creation')
            return jsonify({'error': 'Invalid request data'}), 400

        plan_key = data['plan']
        os_selected = data['os']

        selected_plan = next((p for p in SERVER_PLANS if p['key'] == plan_key), None)

        if not selected_plan:
            app.logger.error(f"Invalid plan selected: {plan_key}")
            return jsonify({'error': 'Invalid plan selected'}), 400

        if user_data.get('balance', 0) < selected_plan['price']:
            app.logger.error(f"User {current_user} has insufficient funds.")
            return jsonify({'error': 'Insufficient funds'}), 400

        if os_selected not in OS_OPTIONS:
            app.logger.error(f"Invalid operating system selected: {os_selected}")
            return jsonify({'error': 'Invalid operating system selected'}), 400

        # Check if a server with the same name exists
        existing_server = next((s for s in user_data['servers'] if s['name'] == data['name']), None)
        if existing_server:
            return jsonify({'error': 'Server with this name already exists'}), 400

        # Generate a unique server ID
        new_server_id = max([s['id'] for s in user_data['servers']], default=0) + 1
        server_uuid = str(uuid.uuid4(   ))  # Generate a new UUID for the server
        container_name = f"server_{new_server_id}"

        rdp_password = generate_password()
        vnc_password = generate_password()
        public_port_8006 = random.randint(10000, 60000)
        public_port_rdp_tcp = random.randint(10000, 60000)
        public_port_rdp_udp = public_port_rdp_tcp
        public_port_vnc = random.randint(5900, 6000)

        docker_compose_content = create_docker_compose_content(
            container_name, os_selected, selected_plan,
            data.get('rdp_username', 'admin'), rdp_password,
            vnc_password,  # Pass VNC password
            public_port_8006, public_port_rdp_tcp, public_port_rdp_udp,
            public_port_vnc  # Pass VNC port
        )

        ssh_client = connect_via_ssh()
        if not ssh_client:
            return jsonify({'error': 'Failed to establish SSH connection'}), 500

        remote_path = get_remote_server_dir(container_name)

        try:
            mkdir_command = f"mkdir -p {remote_path}"
            app.logger.info(f"Creating remote directory: {remote_path}")
            stdout_str, stderr_str, exit_status = execute_ssh_command(mkdir_command, ssh_client)
            if exit_status != 0:
                app.logger.error(f"Failed to create remote directory {remote_path}: {stderr_str}")
                raise Exception(f"Failed to create remote directory: {stderr_str}")
            app.logger.info(f"Remote directory created: {remote_path}")

            docker_compose_command = f"""cat <<EOF > {remote_path}/docker-compose.yml
{docker_compose_content}
EOF
"""
            app.logger.info(f"Creating docker-compose.yml on remote server for {container_name}")
            stdout_str, stderr_str, exit_status = execute_ssh_command(docker_compose_command, ssh_client)
            if exit_status != 0:
                app.logger.error(f"Error creating docker-compose.yml on remote server: {stderr_str}")
                raise Exception(f"Failed to create docker-compose.yml: {stderr_str}")
            app.logger.info(f"docker-compose.yml created successfully at {remote_path}")

            validate_command = f"docker-compose -f {remote_path}/docker-compose.yml config"
            validate_stdout, validate_stderr, validate_exit_status = execute_ssh_command(validate_command, ssh_client)
            if validate_exit_status != 0:
                app.logger.error(f'Docker Compose Validation Error: {validate_stderr}')
                raise Exception(f'Docker Compose validation failed: {validate_stderr}')
            else:
                app.logger.info("Docker Compose configuration is valid.")

            deploy_command = f"docker-compose -f {remote_path}/docker-compose.yml up -d"
            app.logger.info(f"Deploying server {container_name} on remote server")
            stdout_str, stderr_str, deploy_exit_status = execute_ssh_command(deploy_command, ssh_client)
            if deploy_exit_status != 0:
                app.logger.error(f'Docker Compose Deployment Error: {stderr_str}')
                raise Exception(f'Docker Compose deployment failed: {stderr_str}')
            else:
                app.logger.info(f"Server {container_name} deployed successfully.")
        except Exception as e:
            app.logger.error(f'Error during SSH operation: {e}')
            return jsonify({'error': f'Failed to deploy server: {str(e)}'}), 500
        finally:
            ssh_client.close()

        new_server = {
    'id': new_server_id,
    'name': data['name'],
    'uuid': server_uuid,  # Include UUID in server record
    'plan': {
        'key': selected_plan['key'],
        'name': selected_plan['name'],
        'cpu': selected_plan['cpu'],
        'ram': selected_plan['ram'],
        'disk': selected_plan['disk'],
        'price': selected_plan['price']
    },
    'os': OS_OPTIONS[os_selected],
    'status': 'active',
    'created_at': datetime.now().isoformat(),
    'container_name': container_name,
    'ports': {
        '8006/tcp': public_port_8006,
        '3389/tcp': public_port_rdp_tcp,
        '3389/udp': public_port_rdp_udp,
        '5900/tcp': public_port_vnc
    },
    'vnc_password': vnc_password,
    'vnc_address': f"http://{SERVER_IP}:{public_port_vnc}",  # Assuming SERVER_IP is the public IP
    'ip': SERVER_IP  # Add this line
}

        user_data['balance'] -= selected_plan['price']
        user_data['servers'].append(new_server)
        save_db(db)
        log_activity(current_user, 'create_server', request.headers.get('User-Agent', ''))
        return jsonify(new_server), 201

def monthly_billing_check():
    app.logger.info("Checking for monthly billing...")
    try:
        db = load_db()
        for username, user in db['users'].items():
            for server in user.get('servers', []):
                if server.get("next_payment_due"):
                    due_date = datetime.fromisoformat(server["next_payment_due"])
                    if datetime.now() >= due_date:
                        handle_billing_cycle(user, server)
        save_db(db)
    except Exception as e:
        app.logger.error(f"Error during monthly billing check: {e}")
        app.logger.error(traceback.format_exc())

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

    if user.get("balance", 0) >= plan_price:
        user["balance"] -= plan_price
        server["next_payment_due"] = (datetime.now() + timedelta(days=30)).isoformat()
        app.logger.info(f"Successfully billed user for server '{server['name']}'. Next payment due on {server['next_payment_due']}.")
    else:
        server["status"] = "Заморожен"
        server["freeze_reason"] = "Недостаточно средств."
        server["freeze_date"] = datetime.now().isoformat()
        app.logger.info(f"Server '{server['name']}' frozen due to insufficient funds.")

def check_frozen_servers():
    app.logger.info("Checking for frozen servers...")
    try:
        db = load_db()
        for username, user in db['users'].items():
            servers_to_remove = []
            for server in user.get('servers', []):
                if server["status"] == "Заморожен":
                    freeze_date = datetime.fromisoformat(server["freeze_date"])
                    if datetime.now() > freeze_date + timedelta(days=3):
                        ssh_client = connect_via_ssh()
                        if not ssh_client:
                            app.logger.error(f"Failed to establish SSH connection to delete frozen server '{server['name']}'.")
                            continue
                        try:
                            execute_command = f"docker-compose -f {get_remote_server_dir(server['container_name'])}/docker-compose.yml kill && docker-compose -f {get_remote_server_dir(server['container_name'])}/docker-compose.yml rm -f"
                            app.logger.info(f"Deleting frozen server '{server['name']}' from remote server.")
                            stdout_str, stderr_str, exit_status = execute_ssh_command(execute_command, ssh_client)
                            if exit_status != 0:
                                app.logger.error(f"Failed to delete server '{server['name']}': {stderr_str}")
                                continue
                            servers_to_remove.append(server)
                            app.logger.info(f"Server '{server['name']}' deleted after being frozen for over 3 days.")
                        finally:
                            ssh_client.close()
            for server in servers_to_remove:
                user['servers'].remove(server)
        save_db(db)
    except Exception as e:
        app.logger.error(f"Error during frozen server check: {e}")
        app.logger.error(traceback.format_exc())

def initialize_scheduler():
    scheduler = BackgroundScheduler()
    scheduler.add_job(func=monthly_billing_check, trigger="interval", days=30)
    scheduler.add_job(func=check_frozen_servers, trigger="interval", hours=24)
    scheduler.start()
    app.logger.info("Scheduler started for billing and frozen server checks.")

initialize_scheduler()

@app.route('/api/servers/<int:server_id>', methods=['GET'])
@jwt_required()
def get_server(server_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)

        if not user_data:
            app.logger.warning(f'User {current_user} not found when accessing /api/servers/{server_id}')
            return jsonify({'error': 'User not found'}), 404

        server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
        if not server:
            app.logger.warning(f'Server with ID {server_id} not found for user {current_user}')
            return jsonify({'error': 'Server not found'}), 404

        plan = server.get('plan', {})
        plan_key = plan.get('key')
        plan_name = plan.get('name', 'Unknown')

        if not plan_key:
            app.logger.error(f"Server {server_id} has a plan without a 'key'. Cannot process request.")
            return jsonify({'error': "Server's plan information is incomplete. Please contact support."}), 500

        formatted_server = {
            'id': server['id'],
            'uuid': server.get('uuid'),  # Add UUID to individual server response
            'name': server['name'],
            'plan': {
                'key': plan_key,
                'name': plan_name,
                'cpu': plan.get('cpu'),
                'ram': plan.get('ram'),
                'disk': plan.get('disk'),
                'price': plan.get('price')
            },
            'os': server['os'],
            'status': server['status'],
            'ip': server['ip'],
            'created_at': server['created_at'],
            'container_name': server['container_name'],
            'ports': server['ports'],
            'vnc_address': server.get('vnc_address'),
            'vnc_password': server.get('vnc_password')
        }

        return jsonify(formatted_server), 200

    except Exception as e:
        app.logger.error(f'Error in get_server endpoint: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/servers/<int:server_id>/toggle', methods=['POST'])
@jwt_required()
def toggle_server(server_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()

        user_data = db['users'].get(current_user)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
        if not server:
            return jsonify({'error': 'Server not found'}), 404

        container_name = server.get('container_name')
        if not container_name:
            app.logger.error('Container name not found for this server')
            return jsonify({'error': 'Container name not found for this server. Please contact support.'}), 400

        server_dir = get_remote_server_dir(container_name)
        if server['status'] == 'active':
            cmd = f"docker-compose -f {server_dir}/docker-compose.yml stop"
            new_status = 'frozen'
        else:
            cmd = f"docker-compose -f {server_dir}/docker-compose.yml start"
            new_status = 'active'

        ssh_client = connect_via_ssh()
        if not ssh_client:
            return jsonify({'error': 'Failed to establish SSH connection'}), 500

        try:
            stdout_str, stderr_str, exit_status = execute_ssh_command(cmd, ssh_client)
            if exit_status != 0:
                app.logger.error(f'SSH command error: {stderr_str}')
                return jsonify({'error': f'Failed to toggle server status: {stderr_str}'}), 500

            server['status'] = new_status
            save_db(db)
            log_activity(current_user, f'toggle_server_{server_id}', request.headers.get('User-Agent', ''))
            return jsonify({'message': f"Server status changed to {server['status']}"}), 200

        except Exception as e:
            app.logger.error(f'Error during SSH operation: {e}')
            return jsonify({'error': f'Failed to toggle server status: {str(e)}'}), 500
        finally:
            ssh_client.close()

    except Exception as e:
        app.logger.error(f'Unhandled exception in toggle_server: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/servers/<int:server_id>', methods=['DELETE'])
@jwt_required()
def delete_server(server_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)

        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
        if not server:
            return jsonify({'error': 'Server not found'}), 404

        container_name = server.get('container_name')
        if not container_name:
            return jsonify({'error': 'Container name not found for this server'}), 400

        server_dir = get_remote_server_dir(container_name)
        # Добавляем команды docker volume prune -f и docker system prune -f
        commands = [
            f"docker-compose -f {server_dir}/docker-compose.yml down",
            f"rm -rf {server_dir}",
            "docker volume prune -f",  # Удаление неиспользуемых томов
            "docker system prune -f"    # Дополнительная очистка Docker
        ]
        full_command = ' && '.join(commands)

        ssh_client = connect_via_ssh()
        if not ssh_client:
            app.logger.error('Failed to establish SSH connection for deleting server.')
            return jsonify({'error': 'Failed to establish SSH connection'}), 500

        try:
            stdout_str, stderr_str, exit_status = execute_ssh_command(full_command, ssh_client)

            if exit_status != 0:
                app.logger.error(f'SSH command error: {stderr_str}')
                return jsonify({'error': f'Failed to delete server: {stderr_str}'}), 500

            user_data['servers'].remove(server)
            save_db(db)
            log_activity(current_user, f'delete_server_{server_id}', request.headers.get('User-Agent', ''))
            app.logger.info(f"Server {server_id} deleted successfully for user {current_user}.")
            return jsonify({'message': 'Server deleted successfully'}), 200

        except Exception as e:
            app.logger.error(f'Error during SSH operation: {e}')
            return jsonify({'error': f'Failed to delete server: {str(e)}'}), 500
        finally:
            ssh_client.close()

    except Exception as e:
        app.logger.error(f'Unhandled exception in delete_server: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/servers/<int:server_id>', methods=['PUT'])
@jwt_required()
def update_server(server_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)

        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
        if not server:
            return jsonify({'error': 'Server not found'}), 404

        data = request.get_json()
        if not data:
            return jsonify({'error': 'No data provided'}), 400

        new_name = data.get('name')
        new_os = data.get('os')

        if new_name:
            server['name'] = new_name
            app.logger.info(f"Server ID {server_id} name updated to {new_name} by user {current_user}")

        if new_os:
            if new_os not in OS_OPTIONS:
                app.logger.error(f"Invalid operating system selected: {new_os}")
                return jsonify({'error': 'Invalid operating system selected'}), 400
                
            container_name = server.get('container_name')
            server_dir = get_remote_server_dir(container_name)
            docker_compose_path = posixpath.join(server_dir, 'docker-compose.yml')

            docker_compose_content = create_docker_compose_content(
                container_name, new_os, server['plan'],
                server.get('username', 'admin'), server.get('password', generate_password()),
                server.get('vnc_password', generate_password()),
                server['ports']['8006/tcp'], server['ports']['3389/tcp'],
                server['ports']['3389/udp'], server['ports']['5900/tcp']
            )

            ssh_client = connect_via_ssh()
            if not ssh_client:
                app.logger.error('Failed to establish SSH connection for updating server.')
                return jsonify({'error': 'Failed to establish SSH connection'}), 500

            try:
                docker_compose_command = f"""cat <<EOF > {docker_compose_path}
{docker_compose_content}
EOF
"""
                app.logger.info(f"Updating docker-compose.yml on remote server for {container_name}")
                stdout_str, stderr_str, exit_status = execute_ssh_command(docker_compose_command, ssh_client)
                if exit_status != 0:
                    app.logger.error(f"Error updating docker-compose.yml on remote server: {stderr_str}")
                    raise Exception(f"Failed to update docker-compose.yml: {stderr_str}")
                app.logger.info(f"docker-compose.yml updated successfully at {docker_compose_path}")

                restart_command = f"docker-compose -f {docker_compose_path} up -d"
                app.logger.info(f"Restarting server {container_name} to apply OS changes")
                stdout_str, stderr_str, exit_status = execute_ssh_command(restart_command, ssh_client)
                if exit_status != 0:
                    app.logger.error(f"Error restarting server on remote: {stderr_str}")
                    raise Exception(f"Failed to restart server: {stderr_str}")
                app.logger.info(f"Server {container_name} restarted successfully with new OS")

                server['os'] = OS_OPTIONS[new_os]

            except Exception as e:
                app.logger.error(f'Error during SSH operation: {e}')
                return jsonify({'error': f'Failed to update server: {str(e)}'}), 500
            finally:
                ssh_client.close()

        save_db(db)
        log_activity(current_user, f'update_server_{server_id}', request.headers.get('User-Agent', ''))
        return jsonify({'message': 'Server updated successfully', 'server': server}), 200
    except Exception as e:
        app.logger.error(f'Error in update_server endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/userinfo', methods=['GET', 'PUT'])
@jwt_required()
def userinfo():
    try:
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)

        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        if request.method == 'GET':
            return jsonify({'username': current_user, 'balance': user_data.get('balance', 0)}), 200

        elif request.method == 'PUT':
            data = request.get_json()
            if not data:
                return jsonify({'error': 'No data provided'}), 400

            current_password = data.get('current_password')
            new_username = data.get('new_username')
            new_password = data.get('new_password')

            if not current_password:
                return jsonify({'error': 'Current password is required'}), 400

            if not pbkdf2_sha256.verify(current_password, user_data['password']):
                return jsonify({'error': 'Current password is incorrect'}), 401

            if new_username:
                if not is_valid_username(new_username):
                    return jsonify({'error': 'Invalid new username. Please use only letters, numbers, and underscores. Length between 3-30 characters.'}), 400
                if new_username in db['users']:
                    return jsonify({'error': 'Username already taken'}), 400
                db['users'][new_username] = db['users'].pop(current_user)
                current_user = new_username

            if new_password:
                db['users'][current_user]['password'] = pbkdf2_sha256.hash(new_password)

            save_db(db)
            log_activity(current_user, 'update_userinfo', request.headers.get('User-Agent', ''))
            return jsonify({'message': 'User information updated successfully', 'username': current_user}), 200

    except Exception as e:
        app.logger.error(f'Error in userinfo endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/tickets', methods=['GET', 'POST'])
@jwt_required()
def tickets():
    try:
        current_user = get_jwt_identity()
        db = load_db()

        if request.method == 'GET':
            user_data = db['users'].get(current_user)
            if not user_data:
                return jsonify({'error': 'User not found'}), 404

            tickets = user_data.get('tickets', [])
            return jsonify([
                {
                    'id': t['id'],
                    'title': t['title'],
                    'description': t['description'],
                    'status': t.get('status', 'open'),  # Статус по умолчанию "open"
                    'created_at': t['created_at'],
                    'last_updated': t.get('last_updated', t['created_at']),  # Добавляем поле last_updated
                    'messages': t['messages']
                }
                for t in tickets
            ]), 200

        elif request.method == 'POST':
            data = request.get_json()
            if not data or 'title' not in data or 'description' not in data:
                return jsonify({'error': 'Invalid request data'}), 400

            new_ticket = {
                'id': len(user_data['tickets']) + 1,
                'title': data['title'],
                'description': data.get('description', ''),
                'status': 'open',
                'created_at': datetime.now().isoformat(),
                'last_updated': datetime.now().isoformat(),  # Дата последнего обновления
                'messages': []
            }

            user_data['tickets'].append(new_ticket)
            save_db(db)
            log_activity(current_user, 'create_ticket', request.headers.get('User-Agent', ''))
            return jsonify(new_ticket), 201
    except Exception as e:
        app.logger.error(f'Error in tickets endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/tickets/<int:ticket_id>', methods=['GET'])
@jwt_required()
def get_ticket(ticket_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()
        user_data = db['users'].get(current_user)

        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
        if not ticket:
            return jsonify({'error': 'Ticket not found'}), 404

        return jsonify({
            'id': ticket['id'],
            'title': ticket['title'],
            'description': ticket.get('description', ''),
            'status': ticket.get('status', 'open'),
            'created_at': ticket['created_at'],
            'last_updated': ticket.get('last_updated', ticket['created_at']),
            'messages': ticket.get('messages', [])
        }), 200
    except Exception as e:
        app.logger.error(f'Error in get_ticket endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/tickets/<int:ticket_id>/reply', methods=['POST'])
@jwt_required()
def reply_ticket(ticket_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()

        user_data = db['users'].get(current_user)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
        if not ticket:
            return jsonify({'error': 'Ticket not found'}), 404

        data = request.get_json()
        if not data or 'message' not in data:
            return jsonify({'error': 'Message is required'}), 400

        message = {
            'content': data['message'],
            'sender': current_user,
            'timestamp': datetime.now().isoformat()
        }

        ticket['messages'].append(message)
        ticket['last_updated'] = datetime.now().isoformat()  # Обновление даты последнего изменения
        save_db(db)
        log_activity(current_user, f'reply_ticket_{ticket_id}', request.headers.get('User-Agent', ''))
        return jsonify(message), 201

    except Exception as e:
        app.logger.error(f'Error in reply_ticket endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500
    

@app.route('/api/tickets/<int:ticket_id>/status', methods=['PATCH'])
@jwt_required()
def change_ticket_status(ticket_id):
    try:
        current_user = get_jwt_identity()
        db = load_db()  # Загрузка текущей базы данных

        # Проверяем, существует ли пользователь
        user_data = db['users'].get(current_user)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        # Ищем тикет по ID
        ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
        if not ticket:
            return jsonify({'error': 'Ticket not found'}), 404

        # Получаем статус из тела запроса
        data = request.get_json()
        if not data or 'status' not in data:
            return jsonify({'error': 'No status provided'}), 400

        # Проверяем правильность статуса
        new_status = data['status']
        if new_status not in ['open', 'closed']:
            return jsonify({'error': 'Invalid status. Valid values are "open" and "closed".'}), 400

        # Обновляем статус тикета
        ticket['status'] = new_status
        ticket['last_updated'] = datetime.now().isoformat()  # Обновляем дату последнего изменения

        # Сохраняем изменения в базе данных
        save_db(db)

        # Логируем событие изменения статуса
        log_activity(current_user, f'change_ticket_status_{ticket_id}', request.headers.get('User-Agent', ''))

        return jsonify({
            'message': f"Ticket status updated to {new_status}",
            'ticket_id': ticket_id,
            'status': new_status
        }), 200

    except Exception as e:
        app.logger.error(f'Error in change_ticket_status endpoint: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

# Admin Routes
@app.route('/api/admin/users', methods=['GET'])
@admin_required
def get_all_users():
    try:
        db = load_db()
        users = [
            {
                'username': username,
                'balance': user.get('balance', 0),
                'servers_count': len(user.get('servers', [])),
                'tickets_count': len(user.get('tickets', [])),
                'created_at': user.get('created_at'),
                'role': user.get('role', 'user')
            }
            for username, user in db['users'].items()
        ]
        app.logger.info(f'Admin accessed all users list.')
        return jsonify(users), 200
    except Exception as e:
        app.logger.error(f'Error in get_all_users: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/users/<string:username>', methods=['DELETE'])
@admin_required
def delete_user(username):
    try:
        current_user = get_jwt_identity()  # Retrieve the current admin's username
        db = load_db()
        user = db['users'].get(username)
        if not user:
            app.logger.warning(f'Admin tried to delete non-existent user: {username}')
            return jsonify({'error': 'User not found'}), 404

        # Delete all servers associated with the user
        for server in user.get('servers', []):
            container_name = server.get('container_name')
            server_dir = get_remote_server_dir(container_name)
            commands = [
                f"docker-compose -f {server_dir}/docker-compose.yml down",
                f"rm -rf {server_dir}",
                "docker volume prune -f",
                "docker system prune -f"
            ]
            full_command = ' && '.join(commands)

            ssh_client = connect_via_ssh()
            if ssh_client:
                try:
                    stdout_str, stderr_str, exit_status = execute_ssh_command(full_command, ssh_client)
                    if exit_status != 0:
                        app.logger.error(f'Failed to delete server {server["name"]} for user {username}: {stderr_str}')
                except Exception as e:
                    app.logger.error(f'Error deleting server {server["name"]} for user {username}: {e}')
                finally:
                    ssh_client.close()

        del db['users'][username]
        save_db(db)
        log_activity(current_user, f'delete_user_{username}', request.headers.get('User-Agent', ''))
        app.logger.info(f'Admin {current_user} deleted user: {username}')
        return jsonify({'message': f'User {username} deleted successfully'}), 200
    except Exception as e:
        app.logger.error(f'Error in delete_user: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/logs', methods=['GET'])
@admin_required
def get_logs():
    try:
        log_file_path = os.path.join(LOGS_DIR, 'app.log')
        if not os.path.exists(log_file_path):
            return jsonify({'logs': []}), 200

        with open(log_file_path, 'r') as log_file:
            logs = log_file.readlines()
        return jsonify({'logs': logs}), 200
    except Exception as e:
        app.logger.error(f'Error in get_logs: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'Unable to retrieve logs'}), 500

@app.route('/api/admin/tickets', methods=['GET'])
@admin_required
def admin_get_all_tickets():
    try:
        db = load_db()
        all_tickets = []
        for username, user in db['users'].items():
            for ticket in user.get('tickets', []):
                ticket_info = ticket.copy()
                ticket_info['username'] = username
                all_tickets.append(ticket_info)
        return jsonify(all_tickets), 200
    except Exception as e:
        app.logger.error(f'Error in admin_get_all_tickets: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/tickets/<string:username>/<int:ticket_id>/reply', methods=['POST'])
@admin_required
def admin_reply_ticket(username, ticket_id):
    try:
        db = load_db()
        user_data = db['users'].get(username)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
        if not ticket:
            return jsonify({'error': 'Ticket not found'}), 404

        data = request.get_json()
        if not data or 'message' not in data:
            return jsonify({'error': 'Message is required'}), 400

        message = {
            'content': data['message'],
            'sender': 'admin',  # Indicate that the admin is replying
            'timestamp': datetime.now().isoformat()
        }

        ticket['messages'].append(message)
        save_db(db)
        return jsonify(message), 201

    except Exception as e:
        app.logger.error(f'Error in admin_reply_ticket: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/users/<string:username>/tickets/<int:ticket_id>', methods=['GET'])
@admin_required
def admin_get_ticket(username, ticket_id):
    try:
        db = load_db()
        user_data = db['users'].get(username)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404

        ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
        if not ticket:
            return jsonify({'error': 'Ticket not found'}), 404

        return jsonify(ticket), 200
    except Exception as e:
        app.logger.error(f'Error in admin_get_ticket: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/servers', methods=['GET'])
@admin_required
def get_all_servers():
    try:
        db = load_db()
        servers = []
        for username, user in db['users'].items():
            for server in user.get('servers', []):
                server_info = server.copy()
                server_info['owner'] = username
                servers.append(server_info)
        app.logger.info('Admin accessed all servers list.')
        return jsonify([
    {
        'id': server['id'],
        'uuid': server.get('uuid'),  # Include UUID in the response
        'name': server['name'],
        'plan': server['plan'],
        'os': server['os'],
        'status': server['status'],
        'created_at': server['created_at']
    }
    for server in servers
]), 200
    except Exception as e:
        app.logger.error(f'Error in get_all_servers: {e}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/admin/servers/<int:server_id>/toggle', methods=['POST'])
@admin_required
def admin_toggle_server(server_id):
    try:
        db = load_db()
        target_server = None
        target_user = None

        # Find the server and its owner
        for username, user in db['users'].items():
            server = next((s for s in user.get('servers', []) if s['id'] == server_id), None)
            if server:
                target_server = server
                target_user = user
                break

        if not target_server:
            app.logger.warning(f'Admin tried to toggle non-existent server ID: {server_id}')
            return jsonify({'error': 'Server not found'}), 404

        container_name = target_server.get('container_name')
        if not container_name:
            app.logger.error('Container name not found for the server')
            return jsonify({'error': 'Container name not found for this server.'}), 400

        server_dir = get_remote_server_dir(container_name)
        if target_server['status'] == 'active':
            cmd = f"docker-compose -f {server_dir}/docker-compose.yml stop"
            new_status = 'frozen'
        else:
            cmd = f"docker-compose -f {server_dir}/docker-compose.yml start"
            new_status = 'active'

        ssh_client = connect_via_ssh()
        if not ssh_client:
            return jsonify({'error': 'Failed to establish SSH connection'}), 500

        try:
            stdout_str, stderr_str, exit_status = execute_ssh_command(cmd, ssh_client)
            if exit_status != 0:
                app.logger.error(f'SSH command error: {stderr_str}')
                return jsonify({'error': f'Failed to toggle server status: {stderr_str}'}), 500

            target_server['status'] = new_status
            save_db(db)
            log_activity(target_user['username'], f'admin_toggle_server_{server_id}', request.headers.get('User-Agent', ''))
            app.logger.info(f'Admin toggled server ID {server_id} to {new_status}')
            return jsonify({'message': f"Server status changed to {new_status}"}), 200

        except Exception as e:
            app.logger.error(f'Error during SSH operation: {e}')
            return jsonify({'error': f'Failed to toggle server status: {str(e)}'}), 500
        finally:
            ssh_client.close()
    except Exception as e:
        app.logger.error(f'Unhandled exception in admin_toggle_server: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

# Scheduler Initialization
initialize_scheduler()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)


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