# -*- coding: utf-8 -*-
import atexit
from datetime import datetime, timedelta
import json
import secrets
import logging
import mimetypes
import os
import re
import shutil
import signal
import sqlite3
import struct
import subprocess
import sys
import tempfile
import threading
import time
import zipfile
from flask import Flask, request, jsonify, request
from threading import Thread
import psutil
import requests
import telebot
from telebot import types

try:
    import mysql.connector
    from mysql.connector import Error as MySQLError
except ImportError:
    mysql = None
    MySQLError = Exception
else:
    mysql = mysql.connector


class _DBCursorProxy:
    """Small DB-API compatibility layer so the existing bot SQL can use MySQL."""

    def __init__(self, cursor):
        self._cursor = cursor

    @property
    def rowcount(self):
        return self._cursor.rowcount

    def execute(self, query, params=None):
        if DB_TYPE == "mysql":
            query = query.replace("INSERT OR IGNORE INTO", "INSERT IGNORE INTO")
            query = query.replace("INSERT OR REPLACE INTO", "REPLACE INTO")
            query = query.replace("AUTOINCREMENT", "AUTO_INCREMENT")
            query = query.replace("?", "%s")
        return self._cursor.execute(query, params or ())

    def executemany(self, query, seq):
        if DB_TYPE == "mysql":
            query = query.replace("INSERT OR IGNORE INTO", "INSERT IGNORE INTO")
            query = query.replace("INSERT OR REPLACE INTO", "REPLACE INTO")
            query = query.replace("AUTOINCREMENT", "AUTO_INCREMENT")
            query = query.replace("?", "%s")
        return self._cursor.executemany(query, seq)

    def fetchone(self):
        return self._cursor.fetchone()

    def fetchall(self):
        return self._cursor.fetchall()


class _DBConnectionProxy:
    def __init__(self, conn):
        self._conn = conn

    def cursor(self):
        return _DBCursorProxy(self._conn.cursor())

    def commit(self):
        return self._conn.commit()

    def rollback(self):
        return self._conn.rollback()

    def close(self):
        return self._conn.close()


def db_connect():
    if DB_TYPE == "sqlite":
        return _DBConnectionProxy(db_connect())
    if mysql is None:
        raise RuntimeError("mysql-connector-python is required when DB_TYPE=mysql.")
    kwargs = {
        "host": MYSQL_HOST,
        "port": MYSQL_PORT,
        "user": MYSQL_USER,
        "password": MYSQL_PASSWORD,
        "database": MYSQL_DATABASE,
        "autocommit": False,
    }
    if MYSQL_SSL:
        kwargs["ssl_disabled"] = False
    return _DBConnectionProxy(mysql.connect(**kwargs))


# --- Flask Keep Alive ---
app = Flask("")


@app.route("/")
def home():
    return "I'm Mukesh File Host"


def run_flask():
    port = int(os.environ.get("PORT", 8080))
    app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False)



SETUP_PAGE_HTML = """<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>AR BOT HOST Setup</title>
<style>
body{font-family:Arial,sans-serif;background:#f4f7fb;margin:0;padding:30px}
.card{max-width:650px;margin:auto;background:#fff;padding:25px;border-radius:14px;box-shadow:0 8px 30px #0001}
h1{margin-top:0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.full{grid-column:1/-1}label{font-size:13px;font-weight:700;display:block;margin-bottom:5px}
input{width:100%;box-sizing:border-box;padding:11px;border:1px solid #ddd;border-radius:8px}
button{width:100%;padding:12px;border:0;border-radius:8px;background:#1677ff;color:#fff;font-weight:700;margin-top:16px}
.note{font-size:12px;color:#666;line-height:1.5}@media(max-width:600px){.grid{grid-template-columns:1fr}}
</style></head><body><div class="card">
<h1>🤖 AR BOT HOST Setup</h1>
<p class="note">আগের PHP installer-এর মতো এই পেজ থেকেই Hosting, Database এবং Telegram তথ্য দিতে পারবেন।</p>
<form method="post" action="/setup"><div class="grid">
<div><label>Database Host</label><input name="db_host" value="localhost" required></div>
<div><label>Database Port</label><input name="db_port" value="3306" required></div>
<div><label>Database Name</label><input name="db_name" required></div>
<div><label>Database User</label><input name="db_user" required></div>
<div class="full"><label>Database Password</label><input type="password" name="db_password"></div>
<div class="full"><label>Telegram Bot Token</label><input name="bot_token" required></div>
<div><label>Owner Telegram ID</label><input name="owner_id" required></div>
<div><label>Admin Telegram ID</label><input name="admin_id"></div>
<div><label>Support Username</label><input name="support_username" value="@DEV_HOJAIFA"></div>
<div><label>Update Channel</label><input name="update_channel" value="https://t.me/DEV_HOJAIFA99"></div>
</div><button type="submit">💾 Save & Initialize</button></form>
<p class="note">Save করার পরে server restart করুন। Production-এ /setup এবং /install route বন্ধ/সুরক্ষিত রাখা উচিত।</p>
</div></body></html>"""

def _write_env_file(values):
    env_path = os.path.join(BASE_DIR, ".env")
    lines = [
        f"BOT_TOKEN={values['bot_token']}",
        f"OWNER_ID={values['owner_id']}",
        f"ADMIN_ID={values['admin_id'] or values['owner_id']}",
        f"SUPPORT_USERNAME={values['support_username']}",
        f"UPDATE_CHANNEL={values['update_channel']}",
        "DB_TYPE=mysql",
        f"DB_HOST={values['db_host']}",
        f"DB_PORT={values['db_port']}",
        f"DB_NAME={values['db_name']}",
        f"DB_USER={values['db_user']}",
        f"DB_PASSWORD={values['db_password']}",
        "DB_SSL=0",
        f"DATA_DIR={os.path.join(BASE_DIR, 'data')}",
    ]
    with open(env_path, "w", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")
    try:
        os.chmod(env_path, 0o600)
    except OSError:
        pass

def setup_routes(app):
    @app.route("/setup", methods=["GET", "POST"])
    @app.route("/install", methods=["GET", "POST"])
    def setup_page():
        if request.method == "GET":
            return SETUP_PAGE_HTML
        values = {
            "db_host": request.form.get("db_host", "localhost").strip(),
            "db_port": request.form.get("db_port", "3306").strip(),
            "db_name": request.form.get("db_name", "").strip(),
            "db_user": request.form.get("db_user", "").strip(),
            "db_password": request.form.get("db_password", ""),
            "bot_token": request.form.get("bot_token", "").strip(),
            "owner_id": request.form.get("owner_id", "").strip(),
            "admin_id": request.form.get("admin_id", "").strip(),
            "support_username": request.form.get("support_username", "@DEV_HOJAIFA").strip(),
            "update_channel": request.form.get("update_channel", "").strip(),
        }
        if any(not values[k] for k in ("db_name","db_user","bot_token","owner_id")):
            return "Required fields are missing.", 400
        try:
            int(values["owner_id"]); int(values["admin_id"] or values["owner_id"]); int(values["db_port"])
        except ValueError:
            return "Telegram ID এবং Database Port অবশ্যই number হতে হবে.", 400
        try:
            import mysql.connector
            test = mysql.connector.connect(
                host=values["db_host"], port=int(values["db_port"]),
                user=values["db_user"], password=values["db_password"],
                database=values["db_name"])
            test.close()
        except Exception as e:
            return f"<h3>Database connection failed</h3><pre>{e}</pre>", 400
        _write_env_file(values)
        return "<h2>✅ Setup Saved</h2><p>Database এবং Telegram configuration save হয়েছে। এখন server process restart করুন।</p>"

def keep_alive():
    t = Thread(target=run_flask)
    t.daemon = True
    t.start()
    print("Flask Keep-Alive server started.")

# ==============================
# TELEGRAM BOT CONFIGURATION
# ==============================

try:
    from dotenv import load_dotenv
    load_dotenv(os.path.join(os.path.abspath(os.path.dirname(__file__)), ".env"), override=False)
except ImportError:
    pass

TOKEN = os.environ.get("BOT_TOKEN", "").strip()

def _env_int(name, default=0):
    try:
        return int(os.environ.get(name, str(default)).strip())
    except (TypeError, ValueError):
        return default

OWNER_ID = _env_int("OWNER_ID", 0)
ADMIN_ID = _env_int("ADMIN_ID", OWNER_ID)

YOUR_USERNAME = os.environ.get("SUPPORT_USERNAME", "@DEV_HOJAIFA")
UPDATE_CHANNEL = os.environ.get("UPDATE_CHANNEL", "https://t.me/DEV_HOJAIFA99")
if not TOKEN:
    raise RuntimeError("BOT_TOKEN is not configured. Set it in the environment before starting the bot.")
if OWNER_ID <= 0:
    raise RuntimeError("OWNER_ID is not configured. Set it in the environment before starting the bot.")

# Folder setup - using absolute paths
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
DATA_DIR = os.environ.get("DATA_DIR", os.path.join(BASE_DIR, "data"))
UPLOAD_BOTS_DIR = os.path.join(DATA_DIR, "upload_bots")
IROTECH_DIR = os.path.join(DATA_DIR, "inf")
DATABASE_PATH = os.path.join(IROTECH_DIR, "bot_data.db")

# Hosting database: MySQL is the default for the hosting package.
# Set DB_TYPE=sqlite only when you intentionally want the local SQLite fallback.
DB_TYPE = os.environ.get("DB_TYPE", "mysql").strip().lower()
MYSQL_HOST = os.environ.get("DB_HOST", "").strip()
MYSQL_PORT = int(os.environ.get("DB_PORT", "3306") or 3306)
MYSQL_USER = os.environ.get("DB_USER", "").strip()
MYSQL_PASSWORD = os.environ.get("DB_PASSWORD", "")
MYSQL_DATABASE = os.environ.get("DB_NAME", "").strip()
MYSQL_SSL = os.environ.get("DB_SSL", "0").strip() == "1"

if DB_TYPE == "mysql" and not all([MYSQL_HOST, MYSQL_USER, MYSQL_DATABASE]):
    raise RuntimeError(
        "MySQL is enabled but DB_HOST, DB_USER or DB_NAME is missing. "
        "Set the hosting database environment variables, or set DB_TYPE=sqlite for local testing."
    )

# File upload limits
FREE_USER_LIMIT = 0  # Default free limit
SUBSCRIBED_USER_LIMIT = 15
ADMIN_LIMIT = 999
OWNER_LIMIT = float("inf")

# Create necessary directories
os.makedirs(UPLOAD_BOTS_DIR, exist_ok=True)
os.makedirs(IROTECH_DIR, exist_ok=True)

# Initialize bot
bot = telebot.TeleBot(TOKEN)

# --- Data structures ---
bot_scripts = {}
user_subscriptions = {}
user_files = {}
active_users = set()
admin_ids = {uid for uid in {ADMIN_ID, OWNER_ID} if uid > 0}
bot_locked = False
user_selected_plan = {}  # Temp state for upload flow
pending_bot_setup = {}  # Uploaded file -> bot metadata setup
pending_file_edit = {}  # user_id -> existing file waiting for replacement upload

# --- Malware Detection Configuration ---
MALWARE_SIGNATURES = [
    b"MZ",  # Windows executable
    b"\x7fELF",  # Linux executable
    b"\xfe\xed\xfa",  # Mach-O binary
    b"\xce\xfa\xed\xfe",  # Mach-O binary (reverse)
    b"Rar!",  # RAR archive
]

ENCRYPTED_FILE_INDICATORS = [
    b"openssl",
    b"encrypted",
    b"cipher",
    b"AES",
    b"DES",
    b"RSA",
    b"GPG",
    b"PGP",
]

SUSPICIOUS_KEYWORDS = [
    b"ransomware",
    b"trojan",
    b"virus",
    b"malware",
    b"backdoor",
    b"exploit",
    b"payload",
    b"botnet",
    b"keylogger",
    b"rootkit",
]

# --- Logging Setup ---
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)

# --- Command Button Layouts / Customizable Main Menu ---
DEFAULT_MENU={
 "home":{"emoji":"🏠","name":"HOME","description":"Your hosting dashboard"},
 "my_bot":{"emoji":"🤖","name":"MY BOT","description":"Manage your hosted bots"},
 "add_bot":{"emoji":"➕","name":"ADD BOT","description":"Deploy a new bot"},
 "profile":{"emoji":"👤","name":"MY PROFILE","description":"Your account information"},
 "vps_plan":{"emoji":"💎","name":"VPS PLAN","description":"Hosting and VPS plans"},
 "deposit":{"emoji":"💰","name":"DEPOSIT","description":"Add balance to your account"},
 "support":{"emoji":"🎧","name":"SUPPORT","description":"Get help and support"},
}

def get_setting(key, default=""):
    try:
        conn=db_connect(); c=conn.cursor(); c.execute("SELECT `value` FROM bot_settings WHERE `key`=?",(key,)); r=c.fetchone(); conn.close(); return r[0] if r else default
    except Exception: return default

def set_setting(key,value):
    with DB_LOCK:
        conn=db_connect(); c=conn.cursor(); c.execute("INSERT OR REPLACE INTO bot_settings(`key`,`value`) VALUES(?,?)",(key,str(value))); conn.commit(); conn.close()

def menu_label(key):
    d=DEFAULT_MENU[key]; return f"{get_setting('menu.'+key+'.emoji',d['emoji'])} {get_setting('menu.'+key+'.name',d['name'])}"

def render_dynamic(text,user_id,bot_name=""):
    sub=user_subscriptions.get(user_id,{})
    files=user_files.get(user_id,[])
    running=sum(1 for f,_ in files if is_bot_running(user_id,f))
    exp=sub.get('expiry'); exp_text=exp.strftime('%Y-%m-%d') if hasattr(exp,'strftime') else 'N/A'
    vals={'USER_NAME':get_setting(f'user.{user_id}.name','User'),'USER_ID':user_id,'BALANCE':f'{get_balance(user_id):.2f}','PLAN_NAME':sub.get('plan_name','Free') if sub else 'Free','PLAN_EXPIRY':exp_text,'BOT_COUNT':len(files),'RUNNING_BOTS':running,'JOIN_DATE':get_setting(f'user.{user_id}.joined','N/A'),'BOT_NAME':bot_name or get_setting('bot.name','BOT HOSTING'),'BOT_STATUS':'Running' if bot_name else 'Stopped'}
    for k,v in vals.items(): text=text.replace('{'+k+'}',str(v))
    return text

def menu_hidden(key): return get_setting(f'menu.{key}.hidden','0') == '1'
def menu_premium(key): return get_setting(f'menu.{key}.premium','0') == '1'
def menu_row(key):
    defaults={'home':1,'my_bot':1,'add_bot':2,'profile':2,'vps_plan':3,'deposit':3,'support':4}
    try: return max(1, min(4, int(get_setting(f'menu.{key}.row', str(defaults.get(key,1))))))
    except Exception: return defaults.get(key,1)

def create_reply_keyboard_main_menu(user_id):
    markup=types.ReplyKeyboardMarkup(resize_keyboard=True,row_width=2)
    groups={1:[],2:[],3:[],4:[]}
    defaults={'home':1,'my_bot':1,'add_bot':2,'profile':2,'vps_plan':3,'deposit':3,'support':4}
    for k in DEFAULT_MENU:
        if menu_hidden(k): continue
        groups[menu_row(k) if get_setting(f'menu.{k}.row','') else defaults[k]].append(k)
    for r in range(1,5):
        if groups[r]: markup.row(*[types.KeyboardButton(menu_label(k)) for k in groups[r]])
    # Only the explicitly configured OWNER_ID / ADMIN_ID accounts see this button.
    if user_id in {OWNER_ID, ADMIN_ID}:
        markup.row(types.KeyboardButton('🛡️ Admin Panel'))
    return markup

# --- Database Setup ---
DB_LOCK = threading.Lock()


def _original_init_db():
    """Initialize the database with required tables"""
    logger.info(f"Initializing database at: {DATABASE_PATH}")
    try:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            """CREATE TABLE IF NOT EXISTS subscriptions
                     (user_id INTEGER PRIMARY KEY, plan_name TEXT, expiry TEXT)"""
        )
        c.execute(
            """CREATE TABLE IF NOT EXISTS user_files
                     (user_id INTEGER, file_name TEXT, file_type TEXT,
                      PRIMARY KEY (user_id, file_name))"""
        )
        c.execute(
            """CREATE TABLE IF NOT EXISTS active_users
                     (user_id INTEGER PRIMARY KEY)"""
        )
        c.execute(
            """CREATE TABLE IF NOT EXISTS admins
                     (user_id INTEGER PRIMARY KEY)"""
        )
        c.execute(
            """CREATE TABLE IF NOT EXISTS plans
                     (plan_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_limit INTEGER, price TEXT, duration INTEGER, buy_link TEXT)"""
        )
        c.execute("CREATE TABLE IF NOT EXISTS bot_settings (key TEXT PRIMARY KEY, value TEXT)")
        c.execute("CREATE TABLE IF NOT EXISTS user_profiles (user_id INTEGER PRIMARY KEY, name TEXT, joined TEXT)")
        c.execute("CREATE TABLE IF NOT EXISTS bot_meta (user_id INTEGER, file_name TEXT, bot_name TEXT, description TEXT, entry_file TEXT, auto_restart INTEGER DEFAULT 0, auto_start INTEGER DEFAULT 0, PRIMARY KEY(user_id,file_name))")
        c.execute("CREATE TABLE IF NOT EXISTS vps_plans (plan_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, price TEXT, duration TEXT, ram TEXT, cpu TEXT, storage TEXT, bandwidth TEXT, bot_limit INTEGER, auto_restart INTEGER, features TEXT, description TEXT, button_text TEXT, badge TEXT)")
        c.execute("CREATE TABLE IF NOT EXISTS balances (user_id INTEGER PRIMARY KEY, balance REAL DEFAULT 0)")
        c.execute("CREATE TABLE IF NOT EXISTS transactions (tx_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, kind TEXT, amount REAL, status TEXT, reference TEXT, created TEXT)")
        c.execute("CREATE TABLE IF NOT EXISTS vouchers (voucher_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, code TEXT UNIQUE NOT NULL, amount REAL NOT NULL, used_by INTEGER, used_at TEXT, created_at TEXT NOT NULL)")

        # Legacy payment tables are intentionally removed by migration.
        # Useful hosting/subscription tables above are preserved.
        c.execute("DROP TABLE IF EXISTS pending_payments")
        c.execute("DROP TABLE IF EXISTS used_txids")
        c.execute("DROP TABLE IF EXISTS payment_methods")
        c.execute("DROP TABLE IF EXISTS deposits")

        c.execute(
            "INSERT OR IGNORE INTO admins (user_id) VALUES (?)", (OWNER_ID,)
        )
        if ADMIN_ID != OWNER_ID:
            c.execute(
                "INSERT OR IGNORE INTO admins (user_id) VALUES (?)", (ADMIN_ID,)
            )

        conn.commit()
        conn.close()
        logger.info("Database initialized successfully.")
    except Exception as e:
        logger.error(f"❌ Database initialization error: {e}", exc_info=True)


def load_data():
    """Load data from database into memory"""
    logger.info("Loading data from database...")
    try:
        conn = db_connect()
        c = conn.cursor()

        c.execute("SELECT user_id, plan_name, expiry FROM subscriptions")
        for row in c.fetchall():
            user_id = row[0]
            plan_name = row[1] if len(row) > 2 else "Premium"
            expiry = row[-1]
            try:
                user_subscriptions[user_id] = {
                    "plan_name": plan_name,
                    "expiry": datetime.fromisoformat(expiry),
                }
            except ValueError:
                logger.warning(
                    f"⚠️ Invalid expiry date format for user {user_id}: {expiry}. Skipping."
                )

        c.execute("SELECT user_id, file_name, file_type FROM user_files")
        for user_id, file_name, file_type in c.fetchall():
            if user_id not in user_files:
                user_files[user_id] = []
            user_files[user_id].append((file_name, file_type))

        c.execute("SELECT user_id FROM active_users")
        active_users.update(user_id for (user_id,) in c.fetchall())

        c.execute("SELECT user_id FROM admins")
        admin_ids.update(user_id for (user_id,) in c.fetchall())

        conn.close()
        logger.info(f"Data loaded successfully.")
    except Exception as e:
        logger.error(f"❌ Error loading data: {e}", exc_info=True)


if DB_TYPE == "mysql":
    init_mysql_db()
else:
    os.makedirs(IROTECH_DIR, exist_ok=True)
    _original_init_db()
load_data()


# --- Price Parser & Conversion Helper ---


# --- Database Helper Operations ---
def add_plan_db(name, file_limit, price, duration, buy_link):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            "INSERT INTO plans (name, file_limit, price, duration, buy_link) VALUES (?, ?, ?, ?, ?)",
            (name, file_limit, price, duration, buy_link),
        )
        conn.commit()
        conn.close()


def get_all_plans():
    conn = db_connect()
    c = conn.cursor()
    c.execute(
        "SELECT plan_id, name, file_limit, price, duration, buy_link FROM plans"
    )
    plans = c.fetchall()
    conn.close()
    return plans


def get_plan_by_id(plan_id):
    conn = db_connect()
    c = conn.cursor()
    c.execute(
        "SELECT plan_id, name, file_limit, price, duration, buy_link FROM plans WHERE plan_id = ?",
        (plan_id,),
    )
    plan = c.fetchone()
    conn.close()
    return plan


def delete_plan_db(plan_id):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute("DELETE FROM plans WHERE plan_id = ?", (plan_id,))
        conn.commit()
        conn.close()














# --- Malware Detection Functions ---
def is_suspicious_file(file_content, file_name):
    file_lower = file_name.lower()

    # Supported ZIP projects are inspected member-by-member instead of
    # blocking the normal ZIP magic signature.
    if file_lower.endswith(".zip"):
        try:
            with zipfile.ZipFile(__import__("io").BytesIO(file_content), "r") as archive:
                for member in archive.infolist():
                    member_name = member.filename.replace("\\", "/").lower()
                    if any(member_name.endswith(ext) for ext in [
                        ".exe", ".dll", ".bat", ".cmd", ".scr", ".msi", ".msp",
                        ".apk", ".dmg", ".deb", ".rpm", ".bin"
                    ]):
                        return True, f"Suspicious executable inside ZIP: {member.filename}"
        except zipfile.BadZipFile:
            return True, "Invalid ZIP archive"

    suspicious_extensions = [
        ".exe", ".dll", ".bat", ".cmd", ".scr", ".com", ".pif",
        ".application", ".gadget", ".msi", ".msp", ".hta", ".cpl",
        ".msc", ".jar", ".bin", ".deb", ".rpm", ".apk", ".app",
        ".dmg", ".iso", ".img",
    ]

    if any(file_lower.endswith(ext) for ext in suspicious_extensions):
        return True, f"Suspicious file extension: {file_name}"

    for signature in MALWARE_SIGNATURES:
        # PK is intentionally excluded from MALWARE_SIGNATURES because ZIP is supported.
        if file_content.startswith(signature):
            return True, f"Malware signature detected: {signature}"

    sample_size = min(len(file_content), 4096)
    file_sample = file_content[:sample_size]
    sample_text = file_sample.decode("utf-8", errors="ignore").lower()

    for keyword in SUSPICIOUS_KEYWORDS:
        if keyword.decode("utf-8").lower() in sample_text:
            return True, f"Suspicious keyword found: {keyword.decode('utf-8')}"

    return False, "File appears safe"


def scan_file_for_malware(file_content, file_name, user_id):
    if user_id == OWNER_ID:
        return True, "Owner bypassed security check"
    is_suspicious, reason = is_suspicious_file(file_content, file_name)
    if is_suspicious:
        logger.warning(
            f"🚨 Malware detected in {file_name} from user {user_id}: {reason}"
        )
        return False, f"Security violation: {reason}"
    return True, "File passed security check"


# --- Helper Functions ---
def get_user_folder(user_id):
    user_folder = os.path.join(UPLOAD_BOTS_DIR, str(user_id))
    os.makedirs(user_folder, exist_ok=True)
    return user_folder


def get_user_file_limit(user_id):
    if user_id == OWNER_ID:
        return OWNER_LIMIT
    if user_id in admin_ids:
        return ADMIN_LIMIT
    if (
        user_id in user_subscriptions
        and user_subscriptions[user_id]["expiry"] > datetime.now()
    ):
        return SUBSCRIBED_USER_LIMIT
    return FREE_USER_LIMIT


def get_user_file_count(user_id):
    return len(user_files.get(user_id, []))


def is_bot_running(script_owner_id, file_name):
    script_key = f"{script_owner_id}_{file_name}"
    script_info = bot_scripts.get(script_key)
    if script_info and script_info.get("process"):
        try:
            proc = psutil.Process(script_info["process"].pid)
            is_running = (
                proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
            )
            if not is_running:
                if (
                    "log_file" in script_info
                    and hasattr(script_info["log_file"], "close")
                    and not script_info["log_file"].closed
                ):
                    try:
                        script_info["log_file"].close()
                    except Exception:
                        pass
                if script_key in bot_scripts:
                    del bot_scripts[script_key]
            return is_running
        except psutil.NoSuchProcess:
            if script_key in bot_scripts:
                del bot_scripts[script_key]
            return False
        except Exception:
            return False
    return False


def kill_process_tree(process_info):
    try:
        if (
            "log_file" in process_info
            and hasattr(process_info["log_file"], "close")
            and not process_info["log_file"].closed
        ):
            try:
                process_info["log_file"].close()
            except Exception:
                pass
        process = process_info.get("process")
        if process and hasattr(process, "pid"):
            pid = process.pid
            if pid:
                parent = psutil.Process(pid)
                for child in parent.children(recursive=True):
                    try:
                        child.terminate()
                    except Exception:
                        pass
                try:
                    parent.terminate()
                except Exception:
                    pass
    except Exception as e:
        logger.error(f"❌ Error killing process: {e}")


# --- Module / Package Mapping ---
TELEGRAM_MODULES = {
    "telebot": "pyTelegramBotAPI",
    "telegram": "python-telegram-bot",
    "python_telegram_bot": "python-telegram-bot",
    "aiogram": "aiogram",
    "pyrogram": "pyrogram",
    "telethon": "telethon",
    "bs4": "beautifulsoup4",
    "requests": "requests",
    "pillow": "Pillow",
    "cv2": "opencv-python",
    "flask": "Flask",
    "psutil": "psutil",
}


# --- Automatic & Guided Script Running ---
def monitor_and_guide_error(
    process, log_file_path, script_owner_id, file_name, message_obj_for_reply
):
    """রানিং স্ক্রিপ্ট ব্যাকগ্রাউন্ডে চেক করে কোনো এরর থাকলে ইউজারকে বাটন দিয়ে বুঝিয়ে দেবে"""
    time.sleep(3)
    if process.poll() is not None:
        try:
            with open(log_file_path, "r", encoding="utf-8", errors="ignore") as f:
                log_content = f.read()

            match_py = re.search(
                r"(?:ModuleNotFoundError|ImportError): No module named '(.+?)'",
                log_content,
            )
            match_js = re.search(r"Cannot find module '(.+?)'", log_content)

            missing_module = None
            if match_py:
                missing_module = match_py.group(1).split(".")[0].strip("'\"")
            elif match_js:
                missing_module = match_js.group(1).split("/")[0].strip("'\"")

            if missing_module:
                pkg_name = TELEGRAM_MODULES.get(
                    missing_module.lower(), missing_module
                )
                ext = os.path.splitext(file_name)[1].lower()
                cmd_text = (
                    f"npm install {pkg_name}"
                    if ext == ".js"
                    else f"pip install {pkg_name}"
                )

                error_msg = (
                    f"⚠️ **ফাইল রান হতে সমস্যা হয়েছে!**\n\n"
                    f"📄 **File:** `{file_name}`\n"
                    f"❌ **সমস্যা:** আপনার কোডে `{missing_module}` মডিউলটি মিসিং আছে।\n"
                    f"💻 **প্রয়োজনীয় কমান্ড:** `{cmd_text}`\n\n"
                    f"👇 *নিচের বাটনে প্রেস করে সরাসরি মডিউলটি ইনস্টল করুন:*"
                )

                markup = types.InlineKeyboardMarkup()
                markup.add(
                    types.InlineKeyboardButton(
                        f"📦 Install {pkg_name}",
                        callback_data=f"instmod_{script_owner_id}_{missing_module}_{file_name}",
                    )
                )
                markup.add(
                    types.InlineKeyboardButton(
                        "📄 View Error Logs",
                        callback_data=f"viewlog_{script_owner_id}_{file_name}",
                    )
                )

                bot.reply_to(
                    message_obj_for_reply,
                    error_msg,
                    reply_markup=markup,
                    parse_mode="Markdown",
                )
            else:
                error_msg = (
                    f"⚠️ **আপনার কোডে ভুল (Syntax/Runtime Error) পাওয়া গেছে!**\n\n"
                    f"📄 **File:** `{file_name}`\n"
                    f"সুনির্দিষ্ট এরর জানতে নিচের **View Logs** বাটনে ক্লিক করুন।"
                )
                markup = types.InlineKeyboardMarkup()
                markup.add(
                    types.InlineKeyboardButton(
                        "📄 View Error Logs",
                        callback_data=f"viewlog_{script_owner_id}_{file_name}",
                    )
                )
                bot.reply_to(
                    message_obj_for_reply,
                    error_msg,
                    reply_markup=markup,
                    parse_mode="Markdown",
                )
        except Exception as e:
            logger.error(f"Error checking log file: {e}")


def run_script(
    script_path, script_owner_id, user_folder, file_name, message_obj_for_reply
):
    script_key = f"{script_owner_id}_{file_name}"
    try:
        log_file_path = os.path.join(
            user_folder, f"{os.path.splitext(file_name)[0]}.log"
        )
        log_file = open(log_file_path, "w", encoding="utf-8", errors="ignore")
        process = subprocess.Popen(
            [sys.executable, "-u", script_path],
            cwd=user_folder,
            stdout=log_file,
            stderr=log_file,
            stdin=subprocess.PIPE,
        )

        bot_scripts[script_key] = {
            "process": process,
            "log_file": log_file,
            "file_name": file_name,
            "script_owner_id": script_owner_id,
            "start_time": datetime.now(),
            "user_folder": user_folder,
            "type": "py",
            "script_key": script_key,
        }

        bot.reply_to(
            message_obj_for_reply,
            f"🚀 **Python Script Started!**\n📄 File: `{file_name}`\n🆔 PID: `{process.pid}`",
            parse_mode="Markdown",
        )

        threading.Thread(
            target=monitor_and_guide_error,
            args=(
                process,
                log_file_path,
                script_owner_id,
                file_name,
                message_obj_for_reply,
            ),
        ).start()

    except Exception as e:
        bot.reply_to(message_obj_for_reply, f"❌ Error running script: {str(e)}")


def run_js_script(
    script_path, script_owner_id, user_folder, file_name, message_obj_for_reply
):
    script_key = f"{script_owner_id}_{file_name}"
    try:
        log_file_path = os.path.join(
            user_folder, f"{os.path.splitext(file_name)[0]}.log"
        )
        log_file = open(log_file_path, "w", encoding="utf-8", errors="ignore")
        process = subprocess.Popen(
            ["node", script_path],
            cwd=user_folder,
            stdout=log_file,
            stderr=log_file,
            stdin=subprocess.PIPE,
        )

        bot_scripts[script_key] = {
            "process": process,
            "log_file": log_file,
            "file_name": file_name,
            "script_owner_id": script_owner_id,
            "start_time": datetime.now(),
            "user_folder": user_folder,
            "type": "js",
            "script_key": script_key,
        }

        bot.reply_to(
            message_obj_for_reply,
            f"🚀 **JS Script Started!**\n📄 File: `{file_name}`\n🆔 PID: `{process.pid}`",
            parse_mode="Markdown",
        )

        threading.Thread(
            target=monitor_and_guide_error,
            args=(
                process,
                log_file_path,
                script_owner_id,
                file_name,
                message_obj_for_reply,
            ),
        ).start()

    except Exception as e:
        bot.reply_to(
            message_obj_for_reply, f"❌ Error running JS script: {str(e)}"
        )


# --- Database Operations ---
def save_user_file(user_id, file_name, file_type="py"):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            "INSERT OR REPLACE INTO user_files (user_id, file_name, file_type) VALUES (?, ?, ?)",
            (user_id, file_name, file_type),
        )
        conn.commit()
        conn.close()
        if user_id not in user_files:
            user_files[user_id] = []
        user_files[user_id] = [
            (fn, ft) for fn, ft in user_files[user_id] if fn != file_name
        ]
        user_files[user_id].append((file_name, file_type))


def remove_user_file_db(user_id, file_name):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            "DELETE FROM user_files WHERE user_id = ? AND file_name = ?",
            (user_id, file_name),
        )
        conn.commit()
        conn.close()
        if user_id in user_files:
            user_files[user_id] = [
                f for f in user_files[user_id] if f[0] != file_name
            ]


def add_active_user(user_id):
    active_users.add(user_id)
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            "INSERT OR IGNORE INTO active_users (user_id) VALUES (?)", (user_id,)
        )
        conn.commit()
        conn.close()


def get_balance(user_id):
    conn=db_connect(); c=conn.cursor(); c.execute("SELECT balance FROM balances WHERE user_id=?",(user_id,)); r=c.fetchone(); conn.close(); return float(r[0]) if r else 0.0

def update_balance(user_id,amount,reference='deposit'):
    with DB_LOCK:
        conn=db_connect(); c=conn.cursor(); c.execute("INSERT OR IGNORE INTO balances(user_id,balance) VALUES(?,0)",(user_id,)); c.execute("UPDATE balances SET balance=balance+? WHERE user_id=?",(float(amount),user_id)); c.execute("INSERT INTO transactions(user_id,kind,amount,status,reference,created) VALUES(?,?,?,?,?,?)",(user_id,'deposit',float(amount),'Approved',reference,datetime.now().isoformat())); conn.commit(); conn.close()

def ensure_user_profile(user_id,name):
    if not get_setting(f'user.{user_id}.joined',''): set_setting(f'user.{user_id}.joined',datetime.now().strftime('%Y-%m-%d'))
    set_setting(f'user.{user_id}.name',name or 'User')

def save_bot_meta(user_id,file_name,bot_name,description='',entry_file=''):
    with DB_LOCK:
        conn=db_connect(); c=conn.cursor(); c.execute("INSERT OR REPLACE INTO bot_meta(user_id,file_name,bot_name,description,entry_file) VALUES(?,?,?,?,?)",(user_id,file_name,bot_name,description,entry_file or file_name)); conn.commit(); conn.close()

def get_bot_meta(user_id,file_name):
    conn=db_connect(); c=conn.cursor(); c.execute("SELECT bot_name,description,entry_file,auto_restart,auto_start FROM bot_meta WHERE user_id=? AND file_name=?",(user_id,file_name)); r=c.fetchone(); conn.close(); return r or (file_name,'',file_name,0,0)

def get_vps_plans():
    conn=db_connect(); c=conn.cursor(); c.execute("SELECT * FROM vps_plans ORDER BY plan_id"); r=c.fetchall(); conn.close(); return r


def get_vps_plan_by_id(plan_id):
    conn=db_connect()
    try:
        c=conn.cursor()
        c.execute("SELECT * FROM vps_plans WHERE plan_id=?", (int(plan_id),))
        return c.fetchone()
    finally:
        conn.close()


def _parse_vps_price(value):
    text = str(value or '').strip().replace(',', '')
    match = re.search(r'\\d+(?:\\.\\d+)?', text)
    if not match:
        raise ValueError(f"Invalid VPS price: {value}")
    return float(match.group(0))


def purchase_vps_plan(user_id, plan):
    if not plan or len(plan) < 14:
        return False, "❌ VPS plan data is invalid."
    plan_id, name, price_raw, duration_raw = plan[:4]
    try:
        price = _parse_vps_price(price_raw)
        duration = int(duration_raw)
        if duration <= 0:
            raise ValueError()
    except (TypeError, ValueError):
        return False, "❌ This VPS plan has invalid price/duration."

    privileged = user_id in admin_ids or user_id == OWNER_ID
    with DB_LOCK:
        conn = db_connect()
        try:
            c = conn.cursor()
            c.execute("INSERT IGNORE INTO balances(user_id,balance) VALUES(?,0)", (user_id,))
            c.execute("SELECT balance FROM balances WHERE user_id=?", (user_id,))
            row = c.fetchone()
            balance = float(row[0]) if row else 0.0

            if not privileged and balance < price:
                return False, f"❌ Insufficient balance.\n\n💰 Required: ৳{price:.2f}\n💳 Your Balance: ৳{balance:.2f}\n\nPlease deposit first."

            now = datetime.now()
            expiry_base = now
            if not privileged:
                c.execute(
                    "UPDATE balances SET balance=balance-? WHERE user_id=? AND balance>=?",
                    (price, user_id, price)
                )
                if c.rowcount != 1:
                    conn.rollback()
                    return False, "❌ Balance changed. Please try again."
                c.execute(
                    "INSERT INTO transactions(user_id,kind,amount,status,reference,created) VALUES(?,?,?,?,?,?)",
                    (user_id, "vps_purchase", -price, "Approved", f"vps:{plan_id}", now.isoformat())
                )

            c.execute("SELECT expiry FROM subscriptions WHERE user_id=?", (user_id,))
            existing = c.fetchone()
            if existing and existing[0]:
                try:
                    current_expiry = datetime.fromisoformat(str(existing[0]))
                    if current_expiry > now:
                        expiry_base = current_expiry
                except (TypeError, ValueError):
                    pass

            expiry = expiry_base + timedelta(days=duration)
            c.execute(
                "INSERT OR REPLACE INTO subscriptions(user_id,plan_name,expiry) VALUES(?,?,?)",
                (user_id, str(name), expiry.isoformat())
            )
            conn.commit()
            user_subscriptions[user_id] = {"plan_name": str(name), "expiry": expiry}
            return True, (str(name), price, duration)
        except Exception:
            conn.rollback()
            logger.exception("VPS purchase failed for user %s, plan %s", user_id, plan_id)
            return False, "❌ VPS purchase failed. Your balance was not changed."
        finally:
            conn.close()

def save_subscription(user_id, plan_name, expiry):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute(
            "INSERT OR REPLACE INTO subscriptions (user_id, plan_name, expiry) VALUES (?, ?, ?)",
            (user_id, plan_name, expiry.isoformat()),
        )
        conn.commit()
        conn.close()
        user_subscriptions[user_id] = {"plan_name": plan_name, "expiry": expiry}


def remove_subscription_db(user_id):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute("DELETE FROM subscriptions WHERE user_id = ?", (user_id,))
        conn.commit()
        conn.close()
        if user_id in user_subscriptions:
            del user_subscriptions[user_id]


# --- Voucher System ---
def create_voucher_db(name, amount):
    """Create a unique, single-use voucher with a fixed balance value."""
    amount = round(float(amount), 2)
    if amount <= 0:
        raise ValueError("Amount must be greater than 0")
    for _ in range(20):
        code = "VCH-" + secrets.token_urlsafe(9).replace("-", "").replace("_", "").upper()[:12]
        try:
            with DB_LOCK:
                conn = db_connect()
                c = conn.cursor()
                c.execute("INSERT INTO vouchers(name,code,amount,used_by,used_at,created_at) VALUES(?,?,?,?,?,?)",
                          (name.strip(), code, amount, None, None, datetime.now().isoformat()))
                conn.commit(); conn.close()
            return code
        except sqlite3.IntegrityError:
            try: conn.close()
            except Exception: pass
            continue
    raise RuntimeError("Could not generate a unique voucher code")


def redeem_voucher_db(user_id, code):
    """Atomically redeem a voucher exactly once and credit the user's balance."""
    code = code.strip().upper()
    with DB_LOCK:
        conn = db_connect()
        try:
            c = conn.cursor()
            c.execute("SELECT voucher_id,name,amount,used_by FROM vouchers WHERE code=?", (code,))
            row = c.fetchone()
            if not row:
                return False, "❌ Invalid voucher code.", 0
            voucher_id, name, amount, used_by = row
            if used_by is not None:
                return False, "❌ This voucher has already been used.", 0
            now = datetime.now().isoformat()
            c.execute("UPDATE vouchers SET used_by=?, used_at=? WHERE voucher_id=? AND used_by IS NULL",
                      (user_id, now, voucher_id))
            if c.rowcount != 1:
                conn.rollback()
                return False, "❌ This voucher has already been used.", 0
            c.execute("INSERT OR IGNORE INTO balances(user_id,balance) VALUES(?,0)", (user_id,))
            c.execute("UPDATE balances SET balance=balance+? WHERE user_id=?", (float(amount), user_id))
            c.execute("INSERT INTO transactions(user_id,kind,amount,status,reference,created) VALUES(?,?,?,?,?,?)",
                      (user_id, 'voucher_deposit', float(amount), 'Approved', f'voucher:{code}', now))
            conn.commit()
            return True, f"✅ Voucher redeemed successfully!\n\n🎟 Voucher: {name}\n💰 Added: ৳{float(amount):.2f}\n💳 New Balance: ৳{get_balance(user_id)+float(amount):.2f}", float(amount)
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()


def get_vouchers_db(limit=30):
    with DB_LOCK:
        conn=db_connect(); c=conn.cursor()
        c.execute("SELECT voucher_id,name,code,amount,used_by,created_at FROM vouchers ORDER BY voucher_id DESC LIMIT ?", (limit,))
        rows=c.fetchall(); conn.close(); return rows


def delete_unused_voucher_db(voucher_id):
    with DB_LOCK:
        conn=db_connect(); c=conn.cursor()
        c.execute("DELETE FROM vouchers WHERE voucher_id=? AND used_by IS NULL", (voucher_id,))
        ok=c.rowcount==1; conn.commit(); conn.close(); return ok


def process_voucher_redeem(message):
    user_id=message.from_user.id
    code=(message.text or '').strip()
    ok, text, amount=redeem_voucher_db(user_id, code)
    bot.reply_to(message, text)
    if ok:
        try:
            bot.send_message(OWNER_ID, f"🎟 Voucher Used\n\n👤 User: {user_id}\n💰 Amount: ৳{amount:.2f}\n🔑 Code: {code.upper()}")
        except Exception:
            pass


def process_create_voucher_name(message):
    if message.from_user.id not in admin_ids:
        return
    name=(message.text or '').strip()
    if not name:
        bot.reply_to(message, "❌ Voucher name cannot be empty."); return
    msg=bot.send_message(message.chat.id, "💰 Enter the voucher value in BDT.\n\nExample: `10` or `20` or `1.50`", parse_mode='Markdown')
    bot.register_next_step_handler(msg, process_create_voucher_amount, name)


def process_create_voucher_amount(message, name):
    if message.from_user.id not in admin_ids:
        return
    try:
        amount=float((message.text or '').strip())
        if amount <= 0: raise ValueError
        code=create_voucher_db(name, amount)
        bot.reply_to(message, f"✅ **Voucher Created**\n\n🏷 Name: `{name}`\n💰 Value: `৳{amount:.2f}`\n🎟 Code:\n`{code}`\n\n⚠️ This voucher can be redeemed only once.", parse_mode='Markdown')
    except Exception as e:
        bot.reply_to(message, f"❌ Invalid amount or creation error: {e}")


def voucher_manager_markup():
    markup=types.InlineKeyboardMarkup(row_width=2)
    markup.add(types.InlineKeyboardButton('🎟 Create Voucher', callback_data='voucher_create'))
    markup.add(types.InlineKeyboardButton('📋 Voucher List', callback_data='voucher_list'))
    return markup


# --- Menu Creation ---
def create_admin_panel_inline():
    markup = types.InlineKeyboardMarkup(row_width=2)
    markup.add(
        types.InlineKeyboardButton("➕ Add Plan", callback_data="add_plan_init"),
        types.InlineKeyboardButton(
            "🗑 Manage Plans", callback_data="manage_plans"
        ),
    )
    markup.add(
        types.InlineKeyboardButton(
            "💎 Add Subscription", callback_data="add_subscription"
        ),
        types.InlineKeyboardButton(
            "❌ Remove Subscription", callback_data="remove_subscription"
        ),
    )
    markup.add(
        types.InlineKeyboardButton("👑 Add Admin", callback_data="add_admin"),
        types.InlineKeyboardButton(
            "➖ Remove Admin", callback_data="remove_admin"
        ),
    )
    markup.add(
        types.InlineKeyboardButton("📣 Broadcast", callback_data="broadcast"),
        types.InlineKeyboardButton(
            "🔐 Lock / Unlock", callback_data="toggle_lock"
        ),
    )
    markup.add(
        types.InlineKeyboardButton(
            "⚙️ Run All Scripts", callback_data="run_all_scripts"
        ),
        types.InlineKeyboardButton("📊 Bot Stats", callback_data="stats"),
    )
    markup.add(types.InlineKeyboardButton("🎨 Menu Customizer", callback_data="menu_customizer"), types.InlineKeyboardButton("💎 VPS Manager", callback_data="vps_manager"))
    markup.add(types.InlineKeyboardButton("🎟 Voucher Manager", callback_data="voucher_manager"))
    return markup


# --- Core User Logic ---
def _logic_send_welcome(message):
    uid=message.from_user.id
    if bot_locked and uid not in admin_ids: bot.send_message(message.chat.id,'⚠️ Bot is temporarily locked by Admin.'); return
    if uid not in active_users: add_active_user(uid)
    ensure_user_profile(uid,message.from_user.first_name)
    sub=user_subscriptions.get(uid,{})
    plan=sub.get('plan_name','Free') if sub and sub.get('expiry',datetime.min)>datetime.now() else 'Free'
    expiry=sub.get('expiry').strftime('%Y-%m-%d') if sub and sub.get('expiry') else 'N/A'
    files=user_files.get(uid,[]); running=sum(1 for f,_ in files if is_bot_running(uid,f))
    home=get_setting('home.text','✨ {BOT_NAME} — your personal bot hosting dashboard.')
    name=get_setting('bot.name','BOT HOSTING')
    text=f"╭━━━━━━━━━━━━━━━━━━╮\n     🚀 {name}\n╰━━━━━━━━━━━━━━━━━━╯\n\n👋 Welcome, {message.from_user.first_name}!\n\n🆔 User ID: {uid}\n\n💎 Plan: {plan}\n📅 Expiry: {expiry}\n\n🤖 My Bots: {len(files)}\n🟢 Running: {running}\n\n💰 Balance: ৳{get_balance(uid):.2f}\n\n━━━━━━━━━━━━━━━━━━\n{home}"
    bot.send_message(message.chat.id,render_dynamic(text,uid),reply_markup=create_reply_keyboard_main_menu(uid))

def _logic_my_bots(message):
    uid=message.from_user.id; files=user_files.get(uid,[]); running=sum(1 for f,_ in files if is_bot_running(uid,f))
    text=f"🤖 MY BOT\n\n📊 Total Bots: {len(files)}\n🟢 Running: {running}\n🔴 Stopped: {len(files)-running}\n\n━━━━━━━━━━━━━━━━━━"
    markup=types.InlineKeyboardMarkup(row_width=1)
    for fname,_ in files:
        meta=get_bot_meta(uid,fname); st='🟢 Running' if is_bot_running(uid,fname) else '🔴 Stopped'; markup.add(types.InlineKeyboardButton(f'🤖 {meta[0]} — {st}',callback_data=f'mybot_{uid}_{fname}'))
    bot.send_message(message.chat.id,render_dynamic(text,uid),reply_markup=markup if files else None)

def _logic_bot_detail(call, uid, fname):
    meta=get_bot_meta(uid,fname); running=is_bot_running(uid,fname); status='Running' if running else 'Stopped'
    text=f"🤖 {meta[0]}\n\n🟢 Status: {status}\n📄 Entry File: {meta[2]}\n📝 {meta[1] or 'No description'}\n\n━━━━━━━━━━━━━━━━━━"
    markup=types.InlineKeyboardMarkup(row_width=2)
    markup.add(types.InlineKeyboardButton('🛑 STOP' if running else '▶️ START',callback_data=f"stop_{uid}_{fname}" if running else f"start_{uid}_{fname}"),types.InlineKeyboardButton('🔄 RESTART',callback_data=f'restart_{uid}_{fname}'))
    markup.add(types.InlineKeyboardButton('✏️ EDIT FILE',callback_data=f'editfile_{uid}_{fname}'),types.InlineKeyboardButton('📥 DOWNLOAD',callback_data=f'download_{uid}_{fname}'))
    markup.add(types.InlineKeyboardButton('📜 LOGS',callback_data=f'viewlog_{uid}_{fname}'),types.InlineKeyboardButton('⚙️ SETTINGS',callback_data=f'botsettings_{uid}_{fname}'))
    markup.add(types.InlineKeyboardButton('🗑 DELETE',callback_data=f'del_{uid}_{fname}'))
    bot.send_message(call.message.chat.id,text,reply_markup=markup)

def _logic_profile(message):
    uid=message.from_user.id; ensure_user_profile(uid,message.from_user.first_name); sub=user_subscriptions.get(uid,{})
    exp=sub.get('expiry').strftime('%Y-%m-%d') if sub and sub.get('expiry') else 'N/A'
    text=f"👤 MY PROFILE\n\n━━━━━━━━━━━━━━━━━━\n\n👤 Name: {message.from_user.first_name}\n🆔 ID: {uid}\n📅 Joined: {get_setting(f'user.{uid}.joined','N/A')}\n\n💰 Balance: ৳{get_balance(uid):.2f}\n\n💎 Plan: {sub.get('plan_name','Free') if sub else 'Free'}\n📅 Expiry: {exp}\n\n🤖 Total Bots: {len(user_files.get(uid,[]))}\n🟢 Running: {sum(1 for f,_ in user_files.get(uid,[]) if is_bot_running(uid,f))}\n\n━━━━━━━━━━━━━━━━━━"
    bot.send_message(message.chat.id,text)

def _logic_vps_plans(message):
    plans=get_vps_plans(); text='💎 VPS PLANS\n\n━━━━━━━━━━━━━━━━━━\n'
    markup=types.InlineKeyboardMarkup(row_width=1)
    if not plans: text+='\nNo VPS plans configured yet. Admin can add them from the VPS manager.'
    for p in plans:
        _,name,price,duration,ram,cpu,storage,bandwidth,limit,auto,features,desc,button,badge=p
        text+=f"\n{badge+' ' if badge else ''}💎 {name}\n💾 RAM: {ram}\n⚡ CPU: {cpu}\n💽 Storage: {storage}\n🌐 Bandwidth: {bandwidth}\n🤖 Bots: {limit}\n💰 Price: ৳{price} / {duration}\n📝 {desc}\n\n"
        markup.add(types.InlineKeyboardButton(button or '🛒 BUY NOW',callback_data=f'vpsbuy_{p[0]}'))
    bot.send_message(message.chat.id,text,reply_markup=markup if plans else None)

def _logic_support(message):
    text=get_setting('support.text','🎧 SUPPORT\n\nNeed help?\n\n━━━━━━━━━━━━━━━━━━\n📞 Contact Admin\n💬 Live Support\n📚 Help Center\n📢 Updates Channel\n❓ FAQ')
    markup=types.InlineKeyboardMarkup(row_width=1)
    username=get_setting('support.username',YOUR_USERNAME)
    markup.add(types.InlineKeyboardButton('📞 Contact Admin',url='https://t.me/'+username.lstrip('@')))
    if UPDATE_CHANNEL: markup.add(types.InlineKeyboardButton('📢 Updates Channel',url=UPDATE_CHANNEL))
    bot.send_message(message.chat.id,text,reply_markup=markup)

def _logic_deposit(message):
    uid=message.from_user.id
    text=(
        f"💰 DEPOSIT\n\nCurrent Balance:\n💳 ৳{get_balance(uid):.2f}\n\n"
        "🎟 Enter your voucher code to add balance.\n\n"
        "Each voucher can be used only once."
    )
    markup=types.InlineKeyboardMarkup()
    markup.add(types.InlineKeyboardButton('🎟 REDEEM VOUCHER', callback_data='redeem_voucher'))
    bot.send_message(message.chat.id, text, reply_markup=markup)


def _logic_add_bot(message):
    markup=types.InlineKeyboardMarkup(row_width=1)
    markup.add(types.InlineKeyboardButton('🐍 Python Bot',callback_data='addtype_py'),types.InlineKeyboardButton('🟨 Node.js Bot',callback_data='addtype_js'),types.InlineKeyboardButton('📦 ZIP Project',callback_data='addtype_zip'))
    bot.send_message(message.chat.id,'➕ ADD NEW BOT\n\nChoose your Bot type:',reply_markup=markup)

def _logic_upload_file(message):
    user_id = message.from_user.id
    if bot_locked and user_id not in admin_ids:
        bot.reply_to(message, "⚠️ **Bot is locked by Admin.**")
        return

    has_active_plan = False
    plan_name = "None"

    if user_id in admin_ids or user_id == OWNER_ID:
        has_active_plan = True
        plan_name = "Admin / Owner Unlimited"
    elif user_id in user_subscriptions:
        sub = user_subscriptions[user_id]
        if sub["expiry"] > datetime.now():
            has_active_plan = True
            plan_name = sub.get("plan_name", "Premium Plan")

    if not has_active_plan:
        markup = types.InlineKeyboardMarkup()
        markup.add(
            types.InlineKeyboardButton(
                "📦 View Plans", callback_data="view_plans_cb"
            )
        )
        bot.reply_to(
            message,
            "❌ **আপনার কোন এক্টিভ প্ল্যান নেই!**\n\n"
            "ফাইল আপলোড করতে হলে Admin-এর মাধ্যমে একটি subscription activate করতে হবে।\n"
            "নিচের বাটনে প্ল্যানের তথ্য দেখুন অথবা Admin-এর সাথে যোগাযোগ করুন।",
            reply_markup=markup,
            parse_mode="Markdown",
        )
        return

    markup = types.InlineKeyboardMarkup()
    markup.add(
        types.InlineKeyboardButton(
            f"✅ Continue with {plan_name}", callback_data="confirm_plan_upload"
        )
    )
    bot.reply_to(
        message,
        f"🔰 **𝗔𝗰𝘁𝗶𝘃𝗲 𝗣𝗹𝗮𝗻 𝗗𝗲𝘁𝗲𝗰𝘁𝗲𝗱:** `{plan_name}`\n\n"
        f"ফাইল আপলোড চালু করতে নিচের বাটনে সিলেক্ট করুন:",
        reply_markup=markup,
        parse_mode="Markdown",
    )


def _logic_check_files(message):
    user_id = message.from_user.id
    user_files_list = user_files.get(user_id, [])
    if not user_files_list:
        bot.reply_to(
            message,
            "📂 **Your Uploaded Files:**\n\n*(No files uploaded yet)*",
            parse_mode="Markdown",
        )
        return
    markup = types.InlineKeyboardMarkup(row_width=1)
    for file_name, file_type in sorted(user_files_list):
        is_running = is_bot_running(user_id, file_name)
        status_icon = "🟢 Running" if is_running else "🔴 Stopped"
        btn_text = f"📄 {file_name} ({file_type}) - {status_icon}"
        markup.add(
            types.InlineKeyboardButton(
                btn_text, callback_data=f"file_{user_id}_{file_name}"
            )
        )
    bot.reply_to(
        message,
        "📁 **𝗠𝗮𝗻𝗮𝗴𝗲 𝗬𝗼𝘂𝗿 𝗙𝗶𝗹𝗲𝘀:**",
        reply_markup=markup,
        parse_mode="Markdown",
    )


# --- Document Upload Processing ---
@bot.message_handler(content_types=["document"])
def handle_file_upload_doc(message):
    user_id = message.from_user.id
    chat_id = message.chat.id
    doc = message.document

    # --- Existing file editor / replacement upload ---
    if user_id in pending_file_edit:
        owner_id, old_name = pending_file_edit.pop(user_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.reply_to(message, '❌ Access denied.')
            return
        new_name = doc.file_name
        old_ext = os.path.splitext(old_name)[1].lower()
        new_ext = os.path.splitext(new_name)[1].lower()
        if new_ext != old_ext or new_ext not in ['.py', '.js', '.zip']:
            bot.reply_to(message, f'❌ File type must remain `{old_ext}` and supported types are .py, .js, .zip.')
            return
        if new_name != old_name:
            bot.reply_to(message, f'❌ Filename must remain `{old_name}` when editing.')
            return
        try:
            wait = bot.reply_to(message, f'⏳ **Updating `{old_name}`...**', parse_mode='Markdown')
            info = bot.get_file(doc.file_id)
            content = bot.download_file(info.file_path)
            if owner_id != OWNER_ID:
                safe, reason = scan_file_for_malware(content, old_name, owner_id)
                if not safe:
                    bot.edit_message_text(f'🚨 **Security Alert:** {reason}', chat_id, wait.message_id, parse_mode='Markdown')
                    return
            ufolder = get_user_folder(owner_id)
            target = os.path.join(ufolder, old_name)
            # Stop before replacing an active file so the old code cannot keep running.
            skey = f'{owner_id}_{old_name}'
            if skey in bot_scripts:
                kill_process_tree(bot_scripts[skey])
                bot_scripts.pop(skey, None)
            fd, temp_path = tempfile.mkstemp(prefix='.edit_', dir=ufolder)
            try:
                with os.fdopen(fd, 'wb') as tmp:
                    tmp.write(content)
                os.replace(temp_path, target)
            finally:
                if os.path.exists(temp_path):
                    os.remove(temp_path)
            save_user_file(owner_id, old_name, old_ext[1:])
            meta = get_bot_meta(owner_id, old_name)
            bot.edit_message_text(
                f'✅ **File updated successfully!**\n\n📄 `{old_name}`\n📦 Type: `{old_ext[1:]}`\n🛑 Previous process stopped.\n\nUse **🤖 MY BOT** to Start/Restart the updated file.',
                chat_id, wait.message_id, parse_mode='Markdown'
            )
        except Exception as e:
            bot.reply_to(message, f'❌ **Edit failed:** {e}', parse_mode='Markdown')
        return

    if user_id not in admin_ids and user_id != OWNER_ID:
        if (
            user_id not in user_subscriptions
            or user_subscriptions[user_id]["expiry"] <= datetime.now()
        ):
            bot.reply_to(
                message,
                "❌ **আপনার কোন এক্টিভ প্ল্যান নেই! Admin-এর সাথে যোগাযোগ করে subscription activate করুন।**",
                parse_mode="Markdown",
            )
            return

    file_name = doc.file_name
    file_ext = os.path.splitext(file_name)[1].lower()
    if file_ext not in [".py", ".js", ".zip"]:
        bot.reply_to(
            message,
            "⚠️ **Only `.py`, `.js`, and `.zip` files are supported!**",
            parse_mode="Markdown",
        )
        return

    current_limit = get_user_file_limit(user_id)
    if user_id not in admin_ids and current_limit != float("inf"):
        if get_user_file_count(user_id) >= current_limit:
            bot.reply_to(
                message,
                f"❌ **আপনার file limit পূর্ণ হয়েছে:** `{current_limit}` files.",
                parse_mode="Markdown",
            )
            return

    try:
        download_wait_msg = bot.reply_to(
            message,
            f"⏳ **Downloading `{file_name}`...**",
            parse_mode="Markdown",
        )
        file_info_tg_doc = bot.get_file(doc.file_id)
        downloaded_file_content = bot.download_file(file_info_tg_doc.file_path)

        if user_id != OWNER_ID:
            is_safe, reason = scan_file_for_malware(
                downloaded_file_content, file_name, user_id
            )
            if not is_safe:
                bot.edit_message_text(
                    f"🚨 **Security Alert:** {reason}",
                    chat_id,
                    download_wait_msg.message_id,
                    parse_mode="Markdown",
                )
                return

        user_folder = get_user_folder(user_id)
        file_path = os.path.join(user_folder, file_name)
        with open(file_path, "wb") as f:
            f.write(downloaded_file_content)

        bot.edit_message_text(
            f"✅ **File `{file_name}` uploaded successfully!**",
            chat_id,
            download_wait_msg.message_id,
            parse_mode="Markdown",
        )

        if file_ext in (".js", ".py", ".zip"):
            save_user_file(user_id, file_name, file_ext[1:])
            pending_bot_setup[user_id] = file_name
            msg = bot.send_message(
                chat_id,
                f"✅ FILE RECEIVED\n\n📄 File: {file_name}\n📦 Type: {'Python' if file_ext == '.py' else 'Node.js' if file_ext == '.js' else 'ZIP Project'}\n📏 Size: {len(downloaded_file_content)/1024/1024:.2f} MB\n\n🏷 Send Bot Name | Description | Entry File\nExample: MyBot | My first bot | {file_name}"
            )
            bot.register_next_step_handler(msg, process_bot_setup)

    except Exception as e:
        bot.reply_to(message, f"❌ **Error:** {str(e)}")



# --- Admin Management & Operational Helpers ---
def add_admin_db(user_id):
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute("INSERT OR IGNORE INTO admins (user_id) VALUES (?)", (user_id,))
        conn.commit()
        conn.close()
    admin_ids.add(user_id)


def remove_admin_db(user_id):
    if user_id == OWNER_ID:
        return False
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        c.execute("DELETE FROM admins WHERE user_id = ?", (user_id,))
        conn.commit()
        conn.close()
    admin_ids.discard(user_id)
    return True


def get_admin_list():
    return sorted(admin_ids)


def run_all_user_scripts(message):
    started = 0
    for owner_id, files in list(user_files.items()):
        for fname, ftype in list(files):
            if is_bot_running(owner_id, fname):
                continue
            if ftype not in ("py", "js"):
                continue
            folder = get_user_folder(owner_id)
            path = os.path.join(folder, fname)
            if not os.path.isfile(path):
                continue
            if ftype == "js":
                threading.Thread(
                    target=run_js_script,
                    args=(path, owner_id, folder, fname, message),
                    daemon=True,
                ).start()
            else:
                threading.Thread(
                    target=run_script,
                    args=(path, owner_id, folder, fname, message),
                    daemon=True,
                ).start()
            started += 1
    return started


def get_bot_stats_text():
    running = sum(1 for key in list(bot_scripts) if "_" in key and is_bot_running(
        int(key.split("_", 1)[0]), key.split("_", 1)[1]
    ))
    return (
        f"📊 **Bot Statistics**\n\n"
        f"👥 Active Users: `{len(active_users)}`\n"
        f"🛡️ Admins: `{len(admin_ids)}`\n"
        f"📁 Stored User Files: `{sum(len(v) for v in user_files.values())}`\n"
        f"🚀 Running Scripts: `{running}`"
    )


def send_broadcast(message, text):
    success = 0
    failed = 0
    for uid in list(active_users):
        try:
            bot.send_message(uid, text)
            success += 1
        except Exception:
            failed += 1
    bot.reply_to(
        message,
        f"📣 **Broadcast complete.**\n✅ Sent: `{success}`\n❌ Failed: `{failed}`",
        parse_mode="Markdown",
    )

def show_menu_customizer(chat_id):
    markup=types.InlineKeyboardMarkup(row_width=2)
    for key in DEFAULT_MENU:
        markup.add(types.InlineKeyboardButton(menu_label(key),callback_data=f'menuedit_{key}'))
    markup.add(types.InlineKeyboardButton('🏷 Bot Name',callback_data='edit_bot_name'),types.InlineKeyboardButton('📝 Text Manager',callback_data='edit_home_text'))
    markup.add(types.InlineKeyboardButton('😀 Emoji Manager',callback_data='emoji_manager'),types.InlineKeyboardButton('🔘 Button Manager',callback_data='button_manager'))
    markup.add(types.InlineKeyboardButton('📐 Menu Layout',callback_data='layout_manager'))
    bot.send_message(chat_id,'🎨 MENU CUSTOMIZER\n\nEdit Name, Emoji, Description, Buttons, Position, Show/Hide and Premium-only access from here.',reply_markup=markup)

def show_menu_edit(chat_id,key):
    markup=types.InlineKeyboardMarkup(row_width=2)
    markup.add(types.InlineKeyboardButton('✏️ Edit Name',callback_data=f'editmenu_{key}'),types.InlineKeyboardButton('😀 Change Emoji',callback_data=f'editemoji_{key}'))
    markup.add(types.InlineKeyboardButton('📝 Edit Description',callback_data=f'editdesc_{key}'),types.InlineKeyboardButton('📐 Change Position',callback_data=f'editrow_{key}'))
    markup.add(types.InlineKeyboardButton('👁 Show / Hide',callback_data=f'togglehide_{key}'),types.InlineKeyboardButton('💎 Premium Only',callback_data=f'togglepremium_{key}'))
    bot.send_message(chat_id,f'🎨 {menu_label(key)}\n\nChoose what to customize:',reply_markup=markup)

def process_menu_edit(message,key):
    try:
        name,emoji,desc=[x.strip() for x in message.text.split('|',2)]
        set_setting(f'menu.{key}.name',name); set_setting(f'menu.{key}.emoji',emoji); set_setting(f'menu.{key}.text',desc)
        bot.reply_to(message,f'✅ {key} menu updated.\nNew button: {emoji} {name}')
    except Exception as e: bot.reply_to(message,f'❌ Use: Name | Emoji | Description\nError: {e}')

def process_simple_setting(message,key,label):
    set_setting(key,message.text.strip()); bot.reply_to(message,f'✅ {label} updated.')

def process_add_vps(message):
    try:
        p=[x.strip() for x in message.text.split('|')]
        if len(p)<13: raise ValueError('13 fields required')
        name,price,duration,ram,cpu,storage,bandwidth,limit,auto,features,desc,button,badge=p[:13]
        with DB_LOCK:
            conn=db_connect(); c=conn.cursor(); c.execute('INSERT INTO vps_plans(name,price,duration,ram,cpu,storage,bandwidth,bot_limit,auto_restart,features,description,button_text,badge) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)',(name,price,duration,ram,cpu,storage,bandwidth,int(limit),int(auto),features,desc,button,badge)); conn.commit(); conn.close()
        bot.reply_to(message,f'✅ VPS plan {name} added.')
    except Exception as e: bot.reply_to(message,f'❌ VPS format error: Name|Price|Duration|RAM|CPU|Storage|Bandwidth|BotLimit|AutoRestart(0/1)|Features|Description|Button|Badge\n{e}')

# --- Callback Routing ---
@bot.callback_query_handler(func=lambda call: True)
def handle_callbacks(call):
    user_id = call.from_user.id
    data = call.data

    if data == 'home': _logic_send_welcome(call.message); return
    if data == 'my_bots': _logic_my_bots(call.message); return
    if data == 'add_bot': _logic_add_bot(call.message); return
    if data == 'profile': _logic_profile(call.message); return
    if data == 'vps_plans': _logic_vps_plans(call.message); return
    if data == 'deposit': _logic_deposit(call.message); return
    if data == 'support': _logic_support(call.message); return
    if data.startswith('mybot_'):
        _, owner, fname = data.split('_',2); _logic_bot_detail(call,int(owner),fname); return
    if data.startswith('addtype_'):
        bot.answer_callback_query(call.id); bot.send_message(call.message.chat.id,'📁 Upload your bot file now. Supported: .py, .js, .zip'); return
    if data.startswith('vpsbuy_'):
        try:
            plan_id = int(data.split('_', 1)[1])
        except (ValueError, IndexError):
            bot.answer_callback_query(call.id, '❌ Invalid VPS plan.', show_alert=True)
            return

        plan = get_vps_plan_by_id(plan_id)
        if not plan:
            bot.answer_callback_query(call.id, '❌ VPS plan not found.', show_alert=True)
            return

        ok, result = purchase_vps_plan(user_id, plan)
        if not ok:
            bot.answer_callback_query(call.id, result, show_alert=True)
            return

        plan_name, price, duration = result
        bot.answer_callback_query(call.id, '✅ Purchase successful!')
        bot.send_message(
            call.message.chat.id,
            f'✅ **VPS PLAN ACTIVATED**\n\n'
            f'💎 Plan: {plan_name}\n'
            f'💰 Paid: ৳{price:.2f}\n'
            f'📅 Duration: {duration} days\n'
            f'💳 Remaining Balance: ৳{get_balance(user_id):.2f}\n\n'
            f'🚀 Your VPS hosting plan is now active.',
            parse_mode='Markdown'
        )
        return
    if data.startswith('editfile_'):
        _, owner, fname = data.split('_', 2)
        owner_id = int(owner)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, '❌ Access denied.', show_alert=True)
            return
        fpath = os.path.join(get_user_folder(owner_id), fname)
        if not os.path.isfile(fpath):
            bot.answer_callback_query(call.id, 'File not found!', show_alert=True)
            return
        pending_file_edit[user_id] = (owner_id, fname)
        bot.answer_callback_query(call.id)
        bot.send_message(
            call.message.chat.id,
            f'✏️ **EDIT FILE**\n\n📄 `{fname}`\n\n📥 প্রথমে নিচের Download বাটন দিয়ে বর্তমান ফাইলটি নিয়ে নিজের মতো করে edit করুন।\n\nতারপর একই ধরনের `.py`, `.js` অথবা `.zip` ফাইল এখানে পাঠান।\n\n⚠️ একই filename ব্যবহার করুন। নতুন ফাইলটি security scan-এর পর পুরোনোটিকে replace করবে।',
            parse_mode='Markdown',
        )
        return

    if data.startswith('download_'):
        _, owner, fname = data.split('_', 2)
        owner_id = int(owner)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, '❌ Access denied.', show_alert=True)
            return
        fpath = os.path.join(get_user_folder(owner_id), fname)
        if not os.path.isfile(fpath):
            bot.answer_callback_query(call.id, 'File not found!', show_alert=True)
            return
        bot.answer_callback_query(call.id)
        with open(fpath, 'rb') as fh:
            bot.send_document(call.message.chat.id, fh, caption=f'📥 `{fname}`\n✏️ Edit করে আবার upload করতে পারবেন।', parse_mode='Markdown')
        return

    if data.startswith('botsettings_'):
        _,owner,fname=data.split('_',2); meta=get_bot_meta(int(owner),fname); bot.send_message(call.message.chat.id,f'⚙️ SETTINGS\n\n🤖 {meta[0]}\n🔄 Auto Restart: {"ON" if meta[3] else "OFF"}\n⏰ Auto Start: {"ON" if meta[4] else "OFF"}'); return
    if data.startswith('menuedit_') and user_id in admin_ids:
        show_menu_edit(call.message.chat.id,data.split('_',1)[1]); return
    if data.startswith('editemoji_') and user_id in admin_ids:
        key=data.split('_',1)[1]; msg=bot.send_message(call.message.chat.id,'😀 Send new Emoji:'); bot.register_next_step_handler(msg,process_simple_setting,f'menu.{key}.emoji','Emoji'); return
    if data.startswith('editdesc_') and user_id in admin_ids:
        key=data.split('_',1)[1]; msg=bot.send_message(call.message.chat.id,'📝 Send new Description:'); bot.register_next_step_handler(msg,process_simple_setting,f'menu.{key}.text','Description'); return
    if data.startswith('editrow_') and user_id in admin_ids:
        key=data.split('_',1)[1]; msg=bot.send_message(call.message.chat.id,'📐 Send row number 1-4:'); bot.register_next_step_handler(msg,process_simple_setting,f'menu.{key}.row','Position'); return
    if data.startswith('togglehide_') and user_id in admin_ids:
        key=data.split('_',1)[1]; val='0' if menu_hidden(key) else '1'; set_setting(f'menu.{key}.hidden',val); bot.answer_callback_query(call.id,'Hidden' if val=='1' else 'Shown'); show_menu_edit(call.message.chat.id,key); return
    if data.startswith('togglepremium_') and user_id in admin_ids:
        key=data.split('_',1)[1]; val='0' if menu_premium(key) else '1'; set_setting(f'menu.{key}.premium',val); bot.answer_callback_query(call.id,'Premium Only ON' if val=='1' else 'Premium Only OFF'); show_menu_edit(call.message.chat.id,key); return
    if data == 'emoji_manager' and user_id in admin_ids:
        show_menu_customizer(call.message.chat.id); return
    if data == 'button_manager' and user_id in admin_ids:
        show_menu_customizer(call.message.chat.id); return
    if data == 'layout_manager' and user_id in admin_ids:
        show_menu_customizer(call.message.chat.id); return

    if data == 'menu_customizer' and user_id in admin_ids:
        show_menu_customizer(call.message.chat.id); return
    if data.startswith('editmenu_') and user_id in admin_ids:
        key=data.split('_',1)[1]; msg=bot.send_message(call.message.chat.id,f'✏️ Edit {key}\n\nSend: Name | Emoji | Description'); bot.register_next_step_handler(msg,process_menu_edit,key); return
    if data == 'edit_bot_name' and user_id in admin_ids:
        msg=bot.send_message(call.message.chat.id,'🏷 Send new Bot Name:'); bot.register_next_step_handler(msg,process_simple_setting,'bot.name','Bot Name'); return
    if data == 'edit_home_text' and user_id in admin_ids:
        msg=bot.send_message(call.message.chat.id,'📝 Send new Home Text. Variables: {USER_NAME} {BALANCE} {PLAN_NAME} {BOT_COUNT} {RUNNING_BOTS}'); bot.register_next_step_handler(msg,process_simple_setting,'home.text','Home Text'); return
    if data == 'edit_support_text' and user_id in admin_ids:
        msg=bot.send_message(call.message.chat.id,'🎧 Send new Support Text:'); bot.register_next_step_handler(msg,process_simple_setting,'support.text','Support Text'); return
    if data == 'vps_manager' and user_id in admin_ids:
        msg=bot.send_message(call.message.chat.id,'💎 Add VPS Plan\n\nName|Price|Duration|RAM|CPU|Storage|Bandwidth|BotLimit|AutoRestart(0/1)|Features|Description|Button|Badge'); bot.register_next_step_handler(msg,process_add_vps); return
    if data == "view_plans_cb":
        bot.answer_callback_query(call.id)
        _logic_view_plans(call)

    elif data == "confirm_plan_upload":
        bot.answer_callback_query(call.id, "✅ Plan Verified!")
        bot.send_message(
            call.message.chat.id,
            "🚀 **এখন আপনার Python (.py), JS (.js) অথবা ZIP (.zip) ফাইল মেসেজে পাঠান।**",
            parse_mode="Markdown",
        )

    # --- Interactive Module Installer Handler ---
    elif data.startswith("instmod_"):
        _, owner_id, mod_name, fname = data.split("_", 3)
        if user_id != int(owner_id) and user_id not in admin_ids:
            bot.answer_callback_query(
                call.id,
                "❌ আপনি অন্য ইউজারের ফাইল কাস্টমাইজ করতে পারবেন না!",
                show_alert=True,
            )
            return

        bot.answer_callback_query(call.id)
        pkg_name = TELEGRAM_MODULES.get(mod_name.lower(), mod_name)
        ext = os.path.splitext(fname)[1].lower()

        status_msg = bot.send_message(
            call.message.chat.id,
            f"⏳ **`{pkg_name}` মডিউলটি ইনস্টল করা হচ্ছে...**",
            parse_mode="Markdown",
        )

        def do_pip_install():
            if ext == ".js":
                cmd = ["npm", "install", pkg_name]
            else:
                cmd = [sys.executable, "-m", "pip", "install", pkg_name]

            res = subprocess.run(cmd, capture_output=True, text=True)
            if res.returncode == 0:
                bot.edit_message_text(
                    f"✅ **`{pkg_name}` মডিউলটি সফলভাবে ইনস্টল হয়েছে!**\n🚀 ফাইলটি পুনরায় চালু করা হচ্ছে...",
                    call.message.chat.id,
                    status_msg.message_id,
                    parse_mode="Markdown",
                )
                time.sleep(1)
                ufolder = get_user_folder(int(owner_id))
                fpath = os.path.join(ufolder, fname)
                if ext == ".js":
                    run_js_script(
                        fpath, int(owner_id), ufolder, fname, call.message
                    )
                else:
                    run_script(
                        fpath, int(owner_id), ufolder, fname, call.message
                    )
            else:
                bot.edit_message_text(
                    f"❌ **ইনস্টলেশন ব্যর্থ হয়েছে!**\n\n```\n{res.stderr[:300]}\n```",
                    call.message.chat.id,
                    status_msg.message_id,
                    parse_mode="Markdown",
                )

        threading.Thread(target=do_pip_install).start()

    # --- Error Log Viewer Handler ---
    elif data.startswith("viewlog_"):
        _, owner_id, fname = data.split("_", 2)
        ufolder = get_user_folder(int(owner_id))
        log_fpath = os.path.join(
            ufolder, f"{os.path.splitext(fname)[0]}.log"
        )
        if os.path.exists(log_fpath):
            with open(log_fpath, "r", encoding="utf-8", errors="ignore") as f:
                logs = f.read()[-2000:]
            bot.send_message(
                call.message.chat.id,
                f"📜 **Error Log for `{fname}`:**\n\n```\n{logs if logs else 'No logs recorded.'}\n```",
                parse_mode="Markdown",
            )
        else:
            bot.answer_callback_query(
                call.id, "No log file found!", show_alert=True
            )


    elif data.startswith("contact_admin_"):
        plan_id = int(data.split("_")[2])
        plan = get_plan_by_id(plan_id)
        if not plan:
            bot.answer_callback_query(call.id, "Plan not found!", show_alert=True)
            return

        _, name, limit, price, duration, contact_link = plan
        bot.answer_callback_query(call.id)
        contact_text = (
            f"📩 **Contact Admin for `{name}`**\n\n"
            f"📁 File Limit: `{limit}`\n"
            f"⏱ Duration: `{duration} Days`\n"
            f"💰 Price: `{price}`\n\n"
            f"👑 Owner: {YOUR_USERNAME}"
        )
        markup = types.InlineKeyboardMarkup()
        if contact_link and str(contact_link).strip() and str(contact_link).lower() != "none":
            markup.add(types.InlineKeyboardButton("📩 Contact / Future Link", url=str(contact_link).strip()))
        bot.send_message(call.message.chat.id, contact_text, reply_markup=markup, parse_mode="Markdown")

    # --- Voucher Callbacks ---
    elif data == 'redeem_voucher':
        bot.answer_callback_query(call.id)
        msg=bot.send_message(call.message.chat.id, "🎟 **Enter your voucher code:**", parse_mode='Markdown')
        bot.register_next_step_handler(msg, process_voucher_redeem)

    elif data == 'voucher_manager' and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        bot.send_message(call.message.chat.id, "🎟 **VOUCHER MANAGER**\n\nCreate fixed-value, single-use balance vouchers.", reply_markup=voucher_manager_markup(), parse_mode='Markdown')

    elif data == 'voucher_create' and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        msg=bot.send_message(call.message.chat.id, "🏷 **Enter voucher name:**\n\nExample: `BDT 10 Voucher`", parse_mode='Markdown')
        bot.register_next_step_handler(msg, process_create_voucher_name)

    elif data == 'voucher_list' and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        rows=get_vouchers_db()
        if not rows:
            bot.send_message(call.message.chat.id, "🎟 No vouchers have been created yet.")
        else:
            for vid,name,code,amount,used_by,created in rows:
                status=f"🔴 Used by {used_by}" if used_by else "🟢 Unused"
                markup=types.InlineKeyboardMarkup()
                if used_by is None:
                    markup.add(types.InlineKeyboardButton('🗑 Delete', callback_data=f'del_voucher_{vid}'))
                bot.send_message(call.message.chat.id, f"🎟 **{name}**\n\n💰 Value: ৳{amount:.2f}\n🔑 Code: `{code}`\n📌 Status: {status}", reply_markup=markup, parse_mode='Markdown')

    elif data.startswith('del_voucher_') and user_id in admin_ids:
        vid=int(data.split('_')[2])
        if delete_unused_voucher_db(vid):
            bot.answer_callback_query(call.id, 'Voucher deleted')
            bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None)
            bot.send_message(call.message.chat.id, '✅ Unused voucher deleted.')
        else:
            bot.answer_callback_query(call.id, 'Used vouchers cannot be deleted', show_alert=True)

    # --- Admin Callbacks ---
    elif data == "add_plan_init" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "📝 **Enter Plan Details in format:**\n`Name | FileLimit | Price | DurationInDays | Contact/FutureLink`\n\n*Example:* `Basic | 5 | 500 BDT | 30 | https://t.me/yourusername`",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_add_plan)

    elif data == "manage_plans" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        plans = get_all_plans()
        if not plans:
            bot.send_message(call.message.chat.id, "No plans found.")
            return
        markup = types.InlineKeyboardMarkup()
        for p in plans:
            markup.add(
                types.InlineKeyboardButton(
                    f"🗑️ Delete {p[1]}", callback_data=f"del_plan_{p[0]}"
                )
            )
        bot.send_message(
            call.message.chat.id,
            "🗑️ **Select a Plan to Delete:**",
            reply_markup=markup,
        )

    elif data.startswith("del_plan_") and user_id in admin_ids:
        pid = int(data.split("_")[2])
        delete_plan_db(pid)
        bot.answer_callback_query(call.id, "Plan Deleted!")
        bot.send_message(call.message.chat.id, "✅ Plan successfully deleted.")

    elif data == "add_subscription" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "💎 **Enter User ID, Plan Name & Days:**\nFormat: `UserID PlanName Days`\n*Example:* `123456789 VIP 30`",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_add_subscription)


    elif data == "add_admin" and user_id == OWNER_ID:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "👑 **Enter the Telegram User ID to add as Admin:**",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_add_admin)

    elif data == "remove_admin" and user_id == OWNER_ID:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "➖ **Enter the Telegram User ID to remove from Admin:**",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_remove_admin)

    elif data == "remove_subscription" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "❌ **Enter User ID to remove subscription:**",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_remove_subscription)

    elif data == "broadcast" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        msg = bot.send_message(
            call.message.chat.id,
            "📣 **Send the broadcast message now:**",
            parse_mode="Markdown",
        )
        bot.register_next_step_handler(msg, process_broadcast)

    elif data == "stats" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        bot.send_message(call.message.chat.id, get_bot_stats_text(), parse_mode="Markdown")

    elif data == "run_all_scripts" and user_id in admin_ids:
        bot.answer_callback_query(call.id)
        count = run_all_user_scripts(call.message)
        bot.send_message(
            call.message.chat.id,
            f"⚙️ **Run All Scripts:** started `{count}` script(s).",
            parse_mode="Markdown",
        )

    elif data == "toggle_lock" and user_id in admin_ids:
        global bot_locked
        bot_locked = not bot_locked
        bot.answer_callback_query(call.id, f"Bot Locked: {bot_locked}")
        bot.send_message(
            call.message.chat.id,
            f"🔐 **Bot status changed to:** `{'Locked' if bot_locked else 'Unlocked'}`",
            parse_mode="Markdown",
        )

    # --- File Management Callbacks ---
    elif data.startswith("file_"):
        _, owner_id, fname = data.split("_", 2)
        owner_id = int(owner_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, "❌ Access denied.", show_alert=True)
            return
        is_running = is_bot_running(owner_id, fname)
        markup = types.InlineKeyboardMarkup(row_width=2)
        if is_running:
            markup.add(
                types.InlineKeyboardButton(
                    "🛑 Stop", callback_data=f"stop_{owner_id}_{fname}"
                )
            )
        else:
            markup.add(
                types.InlineKeyboardButton(
                    "▶️ Start", callback_data=f"start_{owner_id}_{fname}"
                )
            )
        markup.add(
            types.InlineKeyboardButton(
                "🔄 Restart", callback_data=f"restart_{owner_id}_{fname}"
            ),
            types.InlineKeyboardButton(
                "✏️ Edit File", callback_data=f"editfile_{owner_id}_{fname}"
            ),
        )
        markup.add(
            types.InlineKeyboardButton(
                "📥 Download", callback_data=f"download_{owner_id}_{fname}"
            ),
            types.InlineKeyboardButton(
                "🗑️ Delete", callback_data=f"del_{owner_id}_{fname}"
            ),
        )
        bot.send_message(
            call.message.chat.id,
            f"📄 **File:** `{fname}`\n🚦 Status: `{'Running' if is_running else 'Stopped'}`",
            reply_markup=markup,
            parse_mode="Markdown",
        )

    elif data.startswith("start_"):
        _, owner_id, fname = data.split("_", 2)
        owner_id = int(owner_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, "❌ Access denied.", show_alert=True)
            return
        ufolder = get_user_folder(owner_id)
        fpath = os.path.join(ufolder, fname)
        if not os.path.isfile(fpath):
            bot.answer_callback_query(call.id, "File not found!", show_alert=True)
            return
        if is_bot_running(owner_id, fname):
            bot.answer_callback_query(call.id, "Already running.")
            return
        ext = os.path.splitext(fname)[1].lower()
        bot.answer_callback_query(call.id)
        if ext == ".js":
            threading.Thread(target=run_js_script, args=(fpath, owner_id, ufolder, fname, call.message)).start()
        elif ext == ".py":
            threading.Thread(target=run_script, args=(fpath, owner_id, ufolder, fname, call.message)).start()
        else:
            bot.send_message(call.message.chat.id, "ℹ️ ZIP files are stored for project management and are not directly executable.")

    elif data.startswith("restart_"):
        _, owner_id, fname = data.split("_", 2)
        owner_id = int(owner_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, "❌ Access denied.", show_alert=True)
            return
        skey = f"{owner_id}_{fname}"
        if skey in bot_scripts:
            kill_process_tree(bot_scripts[skey])
            bot_scripts.pop(skey, None)
        ufolder = get_user_folder(owner_id)
        fpath = os.path.join(ufolder, fname)
        ext = os.path.splitext(fname)[1].lower()
        bot.answer_callback_query(call.id, "Restarting...")
        if ext == ".js":
            threading.Thread(target=run_js_script, args=(fpath, owner_id, ufolder, fname, call.message)).start()
        elif ext == ".py":
            threading.Thread(target=run_script, args=(fpath, owner_id, ufolder, fname, call.message)).start()

    elif data.startswith("stop_"):
        _, owner_id, fname = data.split("_", 2)
        owner_id = int(owner_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, "❌ Access denied.", show_alert=True)
            return
        skey = f"{owner_id}_{fname}"
        if skey in bot_scripts:
            kill_process_tree(bot_scripts[skey])
            del bot_scripts[skey]
        bot.answer_callback_query(call.id, "Stopped!")
        bot.send_message(
            call.message.chat.id,
            f"🛑 Script `{fname}` stopped.",
            parse_mode="Markdown",
        )

    elif data.startswith("del_"):
        _, owner_id, fname = data.split("_", 2)
        owner_id = int(owner_id)
        if user_id != owner_id and user_id not in admin_ids:
            bot.answer_callback_query(call.id, "❌ Access denied.", show_alert=True)
            return
        skey = f"{owner_id}_{fname}"
        if skey in bot_scripts:
            kill_process_tree(bot_scripts[skey])
            del bot_scripts[skey]
        remove_user_file_db(int(owner_id), fname)
        ufolder = get_user_folder(int(owner_id))
        fpath = os.path.join(ufolder, fname)
        if os.path.exists(fpath):
            os.remove(fpath)
        bot.answer_callback_query(call.id, "Deleted!")
        bot.send_message(
            call.message.chat.id,
            f"🗑️ File `{fname}` deleted.",
            parse_mode="Markdown",
        )


# --- Step Handlers ---


def process_bot_setup(message):
    uid=message.from_user.id; fname=pending_bot_setup.pop(uid,None)
    if not fname: bot.reply_to(message,'❌ Upload session expired. Use ➕ ADD BOT again.'); return
    try:
        parts=[x.strip() for x in message.text.split('|',2)]
        if len(parts)<1 or not parts[0]: raise ValueError('Bot name is required')
        name=parts[0]; desc=parts[1] if len(parts)>1 else ''; entry=parts[2] if len(parts)>2 and parts[2] else fname
        save_bot_meta(uid,fname,name,desc,entry)
        markup=types.InlineKeyboardMarkup(row_width=2)
        if fname.lower().endswith(('.py','.js')):
            markup.add(types.InlineKeyboardButton('🚀 Start Bot',callback_data=f'start_{uid}_{fname}'))
        markup.add(types.InlineKeyboardButton('🤖 MY BOT',callback_data='my_bots'))
        bot.send_message(message.chat.id,f'✅ BOT READY\n\n🤖 Name: {name}\n📄 File: {fname}\n📝 Description: {desc or "None"}\n⚙️ Entry File: {entry}',reply_markup=markup)
    except Exception as e: bot.reply_to(message,f'❌ Setup error: {e}\nUse: Bot Name | Description | Entry File')

def process_add_admin(message):
    try:
        target_id = int(message.text.strip())
        if target_id <= 0:
            raise ValueError("Invalid User ID")
        add_admin_db(target_id)
        bot.reply_to(message, f"✅ **User `{target_id}` is now an Admin.**", parse_mode="Markdown")
    except Exception as e:
        bot.reply_to(message, f"❌ Error: {e}")


def process_remove_admin(message):
    try:
        target_id = int(message.text.strip())
        if target_id == OWNER_ID:
            bot.reply_to(message, "❌ Owner cannot be removed.")
            return
        if remove_admin_db(target_id):
            bot.reply_to(message, f"✅ **Admin `{target_id}` removed.**", parse_mode="Markdown")
        else:
            bot.reply_to(message, "❌ Unable to remove this Admin.")
    except Exception as e:
        bot.reply_to(message, f"❌ Error: {e}")


def process_remove_subscription(message):
    try:
        target_id = int(message.text.strip())
        remove_subscription_db(target_id)
        bot.reply_to(
            message,
            f"✅ **Subscription removed for User `{target_id}`.**",
            parse_mode="Markdown",
        )
    except Exception as e:
        bot.reply_to(message, f"❌ Error: {e}")


def process_broadcast(message):
    text = message.text.strip()
    if not text:
        bot.reply_to(message, "❌ Broadcast message cannot be empty.")
        return
    send_broadcast(message, text)


def process_add_plan(message):
    try:
        parts = [p.strip() for p in message.text.split("|")]
        name, limit, price, duration, buy_link = (
            parts[0],
            int(parts[1]),
            parts[2],
            int(parts[3]),
            parts[4],
        )
        buy_link = buy_link if buy_link else "none"
        add_plan_db(name, limit, price, duration, buy_link)
        bot.reply_to(
            message,
            f"✅ **Plan `{name}` added successfully!**",
            parse_mode="Markdown",
        )
    except Exception as e:
        bot.reply_to(message, f"❌ Invalid Format! Error: {e}")


def process_add_subscription(message):
    try:
        parts = message.text.split()
        sub_uid, pname, days = int(parts[0]), parts[1], int(parts[2])
        exp = datetime.now() + timedelta(days=days)
        save_subscription(sub_uid, pname, exp)
        bot.reply_to(
            message,
            f"✅ **Subscription active for User `{sub_uid}` under Plan `{pname}` for {days} days!**",
            parse_mode="Markdown",
        )
    except Exception as e:
        bot.reply_to(message, f"❌ Error: {e}")


# --- Text Handler Mapping ---
BUTTON_MAPPING = {
    menu_label('home'): _logic_send_welcome,
    menu_label('my_bot'): _logic_my_bots,
    menu_label('add_bot'): _logic_add_bot,
    menu_label('profile'): _logic_profile,
    menu_label('vps_plan'): _logic_vps_plans,
    menu_label('deposit'): _logic_deposit,
    menu_label('support'): _logic_support,
    '🛡️ Admin Panel': lambda m: bot.reply_to(m,'🛡️ **Admin Control Panel:**',reply_markup=create_admin_panel_inline(),parse_mode='Markdown'),
}


@bot.message_handler(func=lambda m: m.text in [menu_label(k) for k in DEFAULT_MENU] or m.text == '🛡️ Admin Panel')
def handle_main_buttons(message):
    mapping={menu_label('home'):_logic_send_welcome,menu_label('my_bot'):_logic_my_bots,menu_label('add_bot'):_logic_add_bot,menu_label('profile'):_logic_profile,menu_label('vps_plan'):_logic_vps_plans,menu_label('deposit'):_logic_deposit,menu_label('support'):_logic_support}
    for key in DEFAULT_MENU:
        if message.text == menu_label(key) and menu_premium(key):
            sub=user_subscriptions.get(message.from_user.id)
            if not sub or sub.get('expiry',datetime.min) <= datetime.now():
                bot.reply_to(message,'🔒 Premium Feature\n💎 Upgrade Now'); return
    if message.text == '🛡️ Admin Panel':
        if message.from_user.id not in {OWNER_ID, ADMIN_ID}:
            bot.reply_to(message, '⛔ Access denied.')
            return
        bot.reply_to(message,'🛡️ **Admin Control Panel:**',reply_markup=create_admin_panel_inline(),parse_mode='Markdown')
        return
    if message.text in mapping:
        mapping[message.text](message)


@bot.message_handler(commands=["start"])
def start_cmd(message):
    _logic_send_welcome(message)


# --- Default Customization Settings ---
set_setting('bot.name', get_setting('bot.name','BOT HOSTING'))
set_setting('home.text', get_setting('home.text','✨ Welcome to your bot hosting dashboard.'))
set_setting('support.text', get_setting('support.text','🎧 SUPPORT\n\nNeed help?'))
set_setting('support.username', get_setting('support.username',YOUR_USERNAME))
for _k,_d in DEFAULT_MENU.items():
    set_setting(f'menu.{_k}.emoji',get_setting(f'menu.{_k}.emoji',_d['emoji']))
    set_setting(f'menu.{_k}.name',get_setting(f'menu.{_k}.name',_d['name']))


def restore_auto_start_bots():
    """Restart bots marked auto_start after the hosting process restarts."""
    try:
        conn = db_connect()
        c = conn.cursor()
        c.execute("SELECT user_id,file_name,entry_file,auto_start FROM bot_meta WHERE auto_start=1")
        rows = c.fetchall()
        conn.close()
    except Exception:
        logger.exception("Failed to load auto-start bot metadata")
        return
    for uid, fname, entry_file, enabled in rows:
        if not enabled:
            continue
        try:
            folder = get_user_folder(int(uid))
            path = os.path.join(folder, entry_file or fname)
            if os.path.isfile(path) and not is_bot_running(int(uid), fname):
                # Start through the same code path used by the existing bot manager.
                start_user_bot(int(uid), fname)
        except Exception:
            logger.exception("Failed to auto-start %s/%s", uid, fname)


# --- Cleanup & Start ---
def cleanup():
    for key in list(bot_scripts.keys()):
        kill_process_tree(bot_scripts[key])


atexit.register(cleanup)

if __name__ == "__main__":
    logger.info("🤖 Starting Bot with Voucher Deposit & Auto Module Guide...")
    keep_alive()
    restore_auto_start_bots()
    bot.infinity_polling(timeout=60, long_polling_timeout=30)
