#!/data/data/com.termux/files/usr/bin/bash
# Single-file distribution. Run: bash start_dragon_v2.sh
set -eu
if ! command -v pkg >/dev/null 2>&1; then
    echo "Please run this file inside Termux."
    exit 1
fi
if [ "$(id -u)" = 0 ]; then
    echo "Run in normal Termux, not through su. Dragon requests root itself."
    exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
    echo "Installing Python from Termux repository..."
    pkg install -y python
fi
task_python=$(mktemp "${TMPDIR:-/data/data/com.termux/files/usr/tmp}/dragon-v2.XXXXXXXX.py")
trap 'rm -f -- "$task_python"' EXIT
cat > "$task_python" <<'DRAGON_V2_EMBEDDED_PYTHON_END'
#!/usr/bin/env python3
import os
import sys
import time
import json
import select
import tty
import termios
import subprocess
import threading
import re
import shlex
import copy
import shutil
import base64
import hashlib
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime

# ================= PALETTE: ASTRAL CYBER MATRIX =================
# Tone màu Gradient hiện đại & tương phản cao
CLR_RESET   = "\033[0m"
CLR_WHITE   = "\033[1;97m"
CLR_DARK    = "\033[38;5;236m"
CLR_GRAY    = "\033[38;5;244m"
CLR_NEBULA  = "\033[38;5;141m"  # Tím Tinh Vân
CLR_VIOLET  = "\033[38;5;99m"   # Tím Huỳnh Quang
CLR_CYAN    = "\033[38;5;51m"   # Xanh Băng Neon
CLR_AQUA    = "\033[38;5;45m"   # Xanh Biển Sâu
CLR_AURORA  = "\033[38;5;48m"   # Xanh Ngọc Cực Quang
CLR_GOLD    = "\033[38;5;220m"  # Vàng Hoàng Kim
CLR_AMBER   = "\033[38;5;208m"  # Cam Lửa
CLR_ROSE    = "\033[38;5;198m"  # Hồng Cyber
CLR_CRIMSON = "\033[38;5;196m"  # Đỏ Cảnh Báo

# Badges nền cao cấp
BG_ACTIVE = "\033[48;5;22m\033[38;5;48m\033[1m"
BG_WARN   = "\033[48;5;52m\033[38;5;196m\033[1m"
BG_WAIT   = "\033[48;5;17m\033[38;5;51m\033[1m"
BG_VIP    = "\033[48;5;53m\033[38;5;220m\033[1m"

# File config/log riêng, tách khỏi bản gốc để không ảnh hưởng dữ liệu cũ
CONFIG_FILE  = os.path.expanduser("~/.dragon2026_v2_config.json")
LOG_FILE     = os.path.expanduser("~/.dragon2026_v2.log")
VERSION      = "v3.1 ASTRAL-PROTECTED"
DISCORD_URL  = "https://discord.gg/JXUVA2WWqy"
# The web download embeds this deployment's URL and public verification key.
# No server signing key, payment secret, or dealer API secret belongs in this file.
LICENSE_API_URL = "https://test.codegenplay.com"
LICENSE_SERVER_PUBLIC_KEY = "d_kayoNpZXUVSqSQOuQ8AAjWWDdXn32w0egzNxKLkFE="
CLIENT_DEVELOPMENT = False  # Only a loopback development server may deliver source.

TARGET_DEVICE = None
_VIP_AUTHORIZED = False  # Session only; never grant access from saved config.
_VIP_LEASE_UNTIL = 0.0
_VIP_ACTIVE_KEY = ""
_VIP_HEARTBEAT_STARTED = False
_VIP_LAST_MESSAGE = ""
_VIP_VALIDATION_LOCK = threading.Lock()
_VIP_RUNTIME = None
_VIP_RUNTIME_HASH = ""
_VIP_NATIVE_FD = None
_VIP_LOADED_HASH = ""  # Never hot-swap a native image in a running CPython process.

DEFAULT_CONFIG = {
    "place_id": "920587237",
    "link_code": "",
    "check_interval": 5,
    "anti_afk_interval": 300,
    "auto_keep_alive": True,
    "auto_clean_ram": False,
    "light_mode": True,
    "light_fps": 30,
    "light_scale": 0.5,
    "auto_zram": False,
    # Mặc định TẮT auto-restart theo lịch để loại trừ 1 nguồn force-stop định kỳ
    "auto_restart_hours": 0,
    # Thời gian chờ (giây) trước khi coi là mất tiến trình tạm thời, thay vì CRASHED ngay
    "crash_grace_seconds": 30,
    # Số lần kiểm tra liên tiếp KHÔNG thấy PID mới xác nhận CRASHED thật sự (chống báo nhầm do ADB giật/timeout)
    "crash_debounce_count": 3,
    "tabs": [
        {"name": "Roblox Gốc [t]",     "pkg": "com.roblox.client", "enabled": True},
        {"name": "Roblox Clone 1 [u]", "pkg": "com.roblox.clienu", "enabled": True},
        {"name": "Roblox Clone 2 [v]", "pkg": "com.roblox.clienv", "enabled": True}
    ]
}

def load_config():
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, "r") as f:
                cfg = json.load(f)
                for k, v in DEFAULT_CONFIG.items():
                    cfg.setdefault(k, copy.deepcopy(v))
                return cfg
        except Exception:
            return copy.deepcopy(DEFAULT_CONFIG)
    return copy.deepcopy(DEFAULT_CONFIG)

def save_config(cfg):
    try:
        with open(CONFIG_FILE, "w") as f:
            json.dump(cfg, f, indent=4)
    except Exception as e:
        write_log(f"Lỗi lưu file: {e}", "ERR")

def write_log(msg, level="SYS"):
    t_str = datetime.now().strftime("%H:%M:%S")
    entry = f"[{t_str}] [{level}] {msg}\n"
    try:
        with open(LOG_FILE, "a") as f:
            f.write(entry)
        if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > 200000:
            with open(LOG_FILE, "r") as f:
                lines = f.readlines()
            with open(LOG_FILE, "w") as f:
                f.writelines(lines[-500:])
    except Exception:
        pass

# ================= HỆ THỐNG AUTO-BRIDGE ADB =================
def auto_fix_and_setup_adb():
    global TARGET_DEVICE
    clean_env = os.environ.copy()
    clean_env.pop("LD_PRELOAD", None)
    clean_env.pop("LD_LIBRARY_PATH", None)

    pub_key_path = "/data/data/com.termux/files/home/.android/adbkey.pub"
    if not os.path.exists(pub_key_path):
        subprocess.run(["adb", "start-server"], env=clean_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        time.sleep(1)

    if os.path.exists(pub_key_path):
        inject_cmd = (
            f"cat {pub_key_path} >> /data/misc/adb/adb_keys 2>/dev/null; "
            "chmod 640 /data/misc/adb/adb_keys 2>/dev/null; "
            "chown system:shell /data/misc/adb/adb_keys 2>/dev/null; "
            "setprop ctl.restart adbd 2>/dev/null"
        )
        subprocess.run(["su", "-c", inject_cmd], env=clean_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    subprocess.run(["adb", "disconnect", "127.0.0.1:5555"], env=clean_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    try:
        res = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=clean_env, text=True, timeout=5)
        lines = [line.strip() for line in res.stdout.splitlines() if line.strip() and not line.startswith("List of")]
        valid_devs = [l.split()[0] for l in lines if len(l.split()) >= 2 and l.split()[1] == "device"]

        if valid_devs:
            emulators = [d for d in valid_devs if "emulator" in d]
            TARGET_DEVICE = emulators[0] if emulators else valid_devs[0]
            write_log(f"Đã liên kết thiết bị: {TARGET_DEVICE}", "BRIDGE")
            return True
        else:
            subprocess.run(["adb", "connect", "127.0.0.1:5555"], env=clean_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            time.sleep(1)
            res2 = subprocess.run(["adb", "devices"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=clean_env, text=True, timeout=5)
            lines2 = [line.strip() for line in res2.stdout.splitlines() if line.strip() and not line.startswith("List of")]
            for l in lines2:
                parts = l.split()
                if len(parts) >= 2 and parts[1] == "device":
                    TARGET_DEVICE = parts[0]
                    write_log(f"Đã liên kết local: {TARGET_DEVICE}", "BRIDGE")
                    return True
    except Exception as e:
        write_log(f"Lỗi khởi tạo ADB: {e}", "ERR")

    TARGET_DEVICE = None
    return False

def run_adb_cmd(cmd, retries=1):
    """So với bản gốc: thử lại (retry) trước khi chấp nhận thất bại, để giảm
    báo nhầm 'mất PID' do ADB qua mạng (cloud phone) bị giật/timeout thoáng qua."""
    global TARGET_DEVICE
    clean_env = os.environ.copy()
    clean_env.pop("LD_PRELOAD", None)
    clean_env.pop("LD_LIBRARY_PATH", None)

    adb_call = ["adb"]
    if TARGET_DEVICE:
        adb_call.extend(["-s", TARGET_DEVICE])
    adb_call.extend(["shell", cmd])

    last_err = None
    for attempt in range(retries + 1):
        try:
            res = subprocess.run(
                adb_call,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                env=clean_env,
                text=True,
                timeout=8
            )
            if res.returncode == 0:
                return res.stdout.strip()
            last_err = res.stderr.strip()
        except Exception as e:
            last_err = str(e)
        if attempt < retries:
            time.sleep(0.5)

    if last_err:
        write_log(f"ADB lỗi (đã thử {retries + 1} lần) cho lệnh '{cmd}': {last_err}", "WARN")

    try:
        full_cmd = f"unset LD_PRELOAD LD_LIBRARY_PATH; export PATH=/system/bin:/system/xbin:$PATH; {cmd}"
        res = subprocess.run(
            ["su", "-c", full_cmd],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=clean_env,
            text=True,
            timeout=8
        )
        return res.stdout.strip()
    except Exception:
        return ""

# Android reference: https://developer.android.com/games/optimize/adpf/gamemode/gamemode-interventions
# A selected remote target never falls back to local root.
def light_local_android():
    return os.path.isfile("/system/bin/getprop") and os.path.isdir("/data/data/com.termux/files")


def light_shell(command):
    env = os.environ.copy()
    env.pop("LD_PRELOAD", None)
    env.pop("LD_LIBRARY_PATH", None)
    if TARGET_DEVICE:
        call = ["adb", "-s", TARGET_DEVICE, "shell", command]
    elif light_local_android():
        call = ["su", "-c", "export PATH=/system/bin:/system/xbin; " + command]
    else:
        raise RuntimeError("Cần chọn thiết bị ADB hoặc chạy trong Termux có root")
    result = subprocess.run(
        call,
        capture_output=True, text=True, timeout=10, env=env)
    output = result.stdout.strip()
    if result.returncode or re.search(
            r"error|exception|permission denied|not supported|not found|unknown command",
            output + result.stderr, re.I):
        raise RuntimeError((output + " " + result.stderr.strip()).strip())
    return output


def zram_shell(command):
    return light_shell("su -c " + shlex.quote(command) if TARGET_DEVICE else command)


def has_vip_access():
    return _VIP_AUTHORIZED is True and time.monotonic() < _VIP_LEASE_UNTIL


def current_plan():
    return "VIP" if has_vip_access() else "FREE"


def validate_vip_key(key):
    """Only a device-bound, server-signed, fresh lease can grant VIP."""
    global _VIP_AUTHORIZED, _VIP_LEASE_UNTIL, _VIP_ACTIVE_KEY, _VIP_LAST_MESSAGE
    global _VIP_RUNTIME, _VIP_RUNTIME_HASH, _VIP_NATIVE_FD
    if not key.strip():
        return False, "Chưa nhập key VIP."
    if not LICENSE_API_URL or not LICENSE_SERVER_PUBLIC_KEY:
        return False, "Chưa kết nối máy chủ xác thực key VIP. Tải bản Dragon từ web để kết nối đúng server."
    with _VIP_VALIDATION_LOCK:
        try:
            private, device_id, label = license_device_identity()
            from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
            from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
            device_public = base64.urlsafe_b64encode(private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)).decode()
            delivery_private = X25519PrivateKey.generate()
            delivery_public = base64.urlsafe_b64encode(delivery_private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)).decode()
            known_hash = _VIP_RUNTIME_HASH if _VIP_RUNTIME is not None else ""
            challenge = license_post("/api/client/challenge/", {"key": key, "device_id": device_id, "public_key": device_public})
            message = f"dragon-v2|{challenge['challenge_id']}|{challenge['nonce']}|{device_id}|{delivery_public}|{known_hash}".encode()
            result = license_post("/api/client/activate/", {"key": key, "challenge_id": challenge["challenge_id"],
                                   "signature": base64.urlsafe_b64encode(private.sign(message)).decode(), "device_label": label,
                                   "delivery_public_key": delivery_public, "artifact_sha256": known_hash})
            claims = verify_license_lease(result["lease"], device_id, device_public, challenge["nonce"])
            if claims.get("protocol") != 2:
                raise RuntimeError("Máy chủ chưa hỗ trợ giao thức bảo vệ VIP.")
            runtime = load_vip_runtime(result["artifact"], delivery_private, claims)
            runtime.bind_context(zram_shell, write_log, result["lease"], LICENSE_SERVER_PUBLIC_KEY,
                                 device_id, device_public, challenge["nonce"])
            _VIP_LEASE_UNTIL = time.monotonic() + max(0, claims["exp"] - time.time())
            _VIP_AUTHORIZED = True
            _VIP_ACTIVE_KEY = key
            _VIP_LAST_MESSAGE = ""
            return True, "Đã xác thực VIP; key đã gắn với thiết bị này."
        except Exception as error:
            _VIP_AUTHORIZED = False
            _VIP_LEASE_UNTIL = 0.0
            _VIP_RUNTIME, _VIP_RUNTIME_HASH = None, ""
            if _VIP_NATIVE_FD is not None:
                try: os.close(_VIP_NATIVE_FD)
                except OSError: pass
                _VIP_NATIVE_FD = None
            _VIP_LAST_MESSAGE = str(error) if isinstance(error, RuntimeError) else "Không xác minh được giấy phép VIP."
            return False, _VIP_LAST_MESSAGE


def license_device_identity():
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
    except ImportError:
        if not light_local_android() or not shutil.which("pkg") or os.geteuid() == 0:
            raise RuntimeError("Cần thư viện cryptography để xác minh chữ ký VIP.")
        print("[SETUP] Đang cài python-cryptography từ kho Termux...", flush=True)
        result = subprocess.run(["pkg", "install", "-y", "python-cryptography"], timeout=300)
        if result.returncode:
            raise RuntimeError("Chưa cài được thư viện xác minh VIP. Kiểm tra kết nối kho Termux.")
        from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
    from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption
    android_id = light_shell("settings get secure android_id").strip()
    serial = light_shell("getprop ro.serialno").strip()
    if not re.fullmatch(r"[a-fA-F0-9]{8,64}", android_id):
        raise RuntimeError("Không đọc được mã thiết bị Android; chưa thể gắn key.")
    path = os.path.expanduser("~/.dragon2026_device.key")
    if os.path.islink(path): raise RuntimeError("File khóa thiết bị không hợp lệ.")
    if not os.path.exists(path):
        generated = Ed25519PrivateKey.generate().private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0)
        try:
            descriptor = os.open(path, flags, 0o600)
            with os.fdopen(descriptor, "wb") as output: output.write(generated)
        except FileExistsError:
            pass
    os.chmod(path, 0o600)
    with open(path, "rb") as source: private = Ed25519PrivateKey.from_private_bytes(source.read(32))
    device_id = hashlib.sha256(("dragon-device-v1:" + android_id.lower() + ":" + serial).encode()).hexdigest()
    label = light_shell("getprop ro.product.model").strip()[:60] or "Android"
    return private, device_id, label


def license_post(path, payload):
    parsed = urllib.parse.urlparse(LICENSE_API_URL)
    local = parsed.hostname in ("127.0.0.1", "localhost", "::1")
    if parsed.scheme != "https" and not (local and parsed.scheme == "http"):
        raise RuntimeError("Máy chủ giấy phép phải sử dụng HTTPS.")
    if parsed.username or parsed.password or parsed.query or parsed.fragment:
        raise RuntimeError("Địa chỉ máy chủ giấy phép không hợp lệ.")
    class NoRedirect(urllib.request.HTTPRedirectHandler):
        def redirect_request(self, request, fp, code, msg, headers, newurl): return None
    request = urllib.request.Request(LICENSE_API_URL.rstrip("/") + path,
                                     data=json.dumps(payload).encode(), method="POST",
                                     headers={"Content-Type": "application/json", "User-Agent": "Dragon/3.1"})
    opener = urllib.request.build_opener(NoRedirect())
    try:
        with opener.open(request, timeout=15) as response:
            raw = response.read(16 * 1024 * 1024 + 1)
            if len(raw) > 16 * 1024 * 1024: raise RuntimeError("Phản hồi giấy phép quá lớn.")
            body = json.loads(raw)
    except urllib.error.HTTPError as error:
        try: message = json.loads(error.read(65536)).get("message", "Key không được cấp quyền.")
        except (ValueError, AttributeError): message = "Máy chủ từ chối giấy phép VIP."
        raise RuntimeError(str(message)[:240]) from None
    except (urllib.error.URLError, TimeoutError):
        raise RuntimeError("Không kết nối được máy chủ giấy phép VIP.") from None
    if not isinstance(body, dict) or body.get("success") is not True or not isinstance(body.get("data"), dict):
        raise RuntimeError("Phản hồi giấy phép không hợp lệ.")
    return body["data"]


def verify_license_lease(token, device_id, device_public, nonce):
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    from cryptography.exceptions import InvalidSignature
    try:
        encoded, signature = token.split(".")
        decode = lambda text: base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
        Ed25519PublicKey.from_public_bytes(decode(LICENSE_SERVER_PUBLIC_KEY)).verify(decode(signature), encoded.encode())
        claims = json.loads(decode(encoded))
        now = time.time()
        if (claims.get("aud") != "dragon-client" or claims.get("device") != device_id
                or claims.get("public_key") != device_public or claims.get("nonce") != nonce
                or type(claims.get("exp")) is not int or type(claims.get("iat")) is not int
                or not now < claims["exp"] <= now + 360 or claims["iat"] > now + 60
                or not 0 < claims["exp"] - claims["iat"] <= 300
                or "zram" not in claims.get("features", [])
                or claims.get("profile") != {"zram_gib": 6, "render_scale": 0.5, "fps": 30}):
            raise ValueError("claims mismatch")
        return claims
    except (ValueError, TypeError, KeyError, InvalidSignature):
        raise RuntimeError("Chữ ký hoặc thời hạn giấy phép không hợp lệ.") from None


def vip_decode(value, length=None, limit=8 * 1024 * 1024 + 16):
    if not isinstance(value, str) or len(value) > 2 * limit:
        raise ValueError("Invalid protected data")
    raw = base64.b64decode(value, altchars=b"-_", validate=True)
    if (len(raw) > limit or (length is not None and len(raw) != length)
            or base64.urlsafe_b64encode(raw).decode() != value):
        raise ValueError("Invalid protected data")
    return raw


def load_vip_runtime(envelope, delivery_private, claims):
    """Verify the pinned signature/context before decrypting any executable bytes."""
    global _VIP_RUNTIME, _VIP_RUNTIME_HASH, _VIP_NATIVE_FD, _VIP_LOADED_HASH
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
    from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    from cryptography.hazmat.primitives.kdf.hkdf import HKDF
    from cryptography.hazmat.primitives import hashes
    import types
    new_fd = None
    needs_restart = False
    try:
        if not isinstance(envelope, dict) or not isinstance(envelope.get("manifest"), str) or len(envelope["manifest"]) > 10000:
            raise ValueError("Invalid manifest")
        encoded, signature = envelope["manifest"].split(".")
        decode_token = lambda value: base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
        Ed25519PublicKey.from_public_bytes(decode_token(LICENSE_SERVER_PUBLIC_KEY)).verify(decode_token(signature), encoded.encode())
        metadata = json.loads(decode_token(encoded))
        canonical = lambda value: json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
        if not isinstance(metadata, dict) or canonical(metadata) != decode_token(encoded):
            raise ValueError("Non-canonical metadata")
        delivery_public = base64.urlsafe_b64encode(delivery_private.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)).decode()
        if (metadata.get("aud") != "dragon-vip-artifact" or metadata.get("protocol") != 2
                or metadata.get("delivery_public_key") != delivery_public
                or any(metadata.get(field) != claims.get(field) for field in
                       ("sub", "device", "public_key", "nonce", "generation", "iat", "exp"))
                or not time.time() < metadata["exp"]
                or not re.fullmatch(r"[a-f0-9]{64}", str(metadata.get("sha256", "")))
                or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]{0,63}", str(metadata.get("version", "")))):
            raise ValueError("Artifact context mismatch")
        kind = metadata.get("format")
        if kind not in ("python-source-v1", "nuitka-module-v1"):
            raise ValueError("Unsupported module format")
        if kind == "python-source-v1" and not (CLIENT_DEVELOPMENT is True and
                urllib.parse.urlparse(LICENSE_API_URL).hostname in ("127.0.0.1", "localhost", "::1")):
            raise ValueError("Source modules are development-only")
        size_limit = 24576 if kind == "python-source-v1" else 8 * 1024 * 1024
        if type(metadata.get("size")) is not int or not 0 < metadata["size"] <= size_limit:
            raise ValueError("Invalid module size")
        if globals().get("_VIP_LOADED_HASH") and _VIP_LOADED_HASH != metadata["sha256"]:
            needs_restart = True
            raise ValueError("Restart required before loading another runtime release")
        if envelope.get("cached") is True:
            if (metadata.get("cached") is not True or set(envelope) != {"cached", "manifest"}
                    or _VIP_RUNTIME is None or _VIP_RUNTIME_HASH != metadata["sha256"]):
                raise ValueError("No matching verified module in memory")
            return _VIP_RUNTIME
        if (metadata.get("cached") is not False or envelope.get("cached") is not False
                or set(envelope) != {"cached", "manifest", "server_public_key", "nonce", "ciphertext"}):
            raise ValueError("Invalid delivery envelope")
        if (envelope["server_public_key"] != metadata.get("server_public_key")
                or envelope["nonce"] != metadata.get("encryption_nonce")):
            raise ValueError("Delivery metadata mismatch")
        ciphertext = vip_decode(envelope["ciphertext"], limit=size_limit + 16)
        if hashlib.sha256(ciphertext).hexdigest() != metadata.get("ciphertext_sha256"):
            raise ValueError("Ciphertext mismatch")
        shared = delivery_private.exchange(X25519PublicKey.from_public_bytes(vip_decode(envelope["server_public_key"], length=32)))
        key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=b"dragon-vip-delivery-v2").derive(shared)
        aad = canonical({name: value for name, value in metadata.items() if name != "ciphertext_sha256"})
        payload = AESGCM(key).decrypt(vip_decode(envelope["nonce"], length=12), ciphertext, aad)
        if len(payload) != metadata["size"] or hashlib.sha256(payload).hexdigest() != metadata["sha256"]:
            raise ValueError("Module digest mismatch")
        if kind == "python-source-v1":
            module = types.ModuleType("dragon_vip")
            exec(compile(payload.decode("utf-8"), "<dragon-vip-memory>", "exec"), module.__dict__)
        else:
            # Loading from a memory fd avoids an unencrypted module on persistent storage.
            if not hasattr(os, "memfd_create"):
                raise RuntimeError("Thiết bị chưa hỗ trợ nạp module VIP từ bộ nhớ.")
            import fcntl
            from importlib.machinery import ExtensionFileLoader
            from importlib.util import module_from_spec, spec_from_loader
            new_fd = os.memfd_create("dragon_vip", os.MFD_CLOEXEC | os.MFD_ALLOW_SEALING)
            try:
                with os.fdopen(os.dup(new_fd), "wb") as memory:
                    memory.write(payload)
                    memory.flush()
                fcntl.fcntl(new_fd, fcntl.F_ADD_SEALS,
                            fcntl.F_SEAL_WRITE | fcntl.F_SEAL_GROW | fcntl.F_SEAL_SHRINK | fcntl.F_SEAL_SEAL)
                loader = ExtensionFileLoader("dragon_vip", f"/proc/self/fd/{new_fd}")
                spec = spec_from_loader("dragon_vip", loader)
                module = module_from_spec(spec)
                loader.exec_module(module)
            except Exception:
                raise
        if not callable(getattr(module, "ensure_zram", None)) or not callable(getattr(module, "bind_context", None)):
            raise ValueError("Invalid VIP module interface")
        if _VIP_NATIVE_FD is not None:
            try: os.close(_VIP_NATIVE_FD)
            except OSError: pass
        _VIP_NATIVE_FD = new_fd
        new_fd = None  # Ownership transfers only after the module interface is verified.
        _VIP_RUNTIME, _VIP_RUNTIME_HASH = module, metadata["sha256"]
        _VIP_LOADED_HASH = metadata["sha256"]
        return module
    except Exception:
        if needs_restart:
            raise RuntimeError("Module VIP đã được cập nhật. Khởi động lại Dragon để dùng bản mới.") from None
        raise RuntimeError("Không xác minh hoặc giải mã được module VIP. Quyền VIP chưa được cấp.") from None
    finally:
        if new_fd is not None:
            try: os.close(new_fd)
            except OSError: pass


def start_license_heartbeat():
    global _VIP_HEARTBEAT_STARTED
    if _VIP_HEARTBEAT_STARTED: return
    _VIP_HEARTBEAT_STARTED = True
    def worker():
        while True:
            time.sleep(120)
            key = _VIP_ACTIVE_KEY
            if key:
                valid, _ = validate_vip_key(key)
                if not valid: write_log("Mất quyền VIP; giữ nguyên vùng swap hệ thống đang chạy.", "LICENSE")
    threading.Thread(target=worker, daemon=True, name="dragon-license").start()


def activate_vip():
    key = input("Dán key VIP rồi nhấn Enter: ").strip()
    try:
        valid, message = validate_vip_key(key)
    except Exception:
        return "Không xác thực được key VIP. Vui lòng thử lại sau."
    if valid is True and has_vip_access():
        start_license_heartbeat()
        return "Đã kích hoạt gói VIP."
    return message


def ensure_zram(cfg):
    """Only the verified, server-delivered VIP runtime may perform optimization."""
    if not has_vip_access():
        return "Gói FREE: không thiết lập tối ưu VIP."
    if not cfg.get("auto_zram", True):
        return "Tối ưu VIP đang TẮT; giữ cấu hình hệ thống đang chạy."
    runtime = _VIP_RUNTIME
    if runtime is None:
        return "Chưa nhận được module VIP hợp lệ từ máy chủ."
    return runtime.ensure_zram(cfg)


def light_mode_state(pkg):
    output = light_shell(f"cmd game list-modes {shlex.quote(pkg)}")
    match = re.search(r"current mode:\s*(standard|performance|battery|custom)\b", output)
    if not match:
        raise RuntimeError("ROM không cho đọc Game Mode; bỏ qua để giữ cấu hình cũ")
    return match.group(1)


def light_backup_path():
    # Serial + build fingerprint prevent restoring another phone's configuration.
    import hashlib
    identity = (TARGET_DEVICE or "local:" + light_shell("getprop ro.serialno"))
    identity += "\n" + light_shell("getprop ro.build.fingerprint")
    key = hashlib.sha256(identity.encode()).hexdigest()[:20]
    return os.path.expanduser(f"~/.dragon2026_v2_light_{key}.json")


def light_save(path, data):
    with open(path + ".tmp", "w", encoding="utf-8") as handle:
        json.dump(data, handle, ensure_ascii=False, indent=2)
    os.replace(path + ".tmp", path)


def light_restore_package(pkg, previous):
    quoted = shlex.quote(pkg)
    if previous["mode"] not in ("standard", "performance", "battery", "custom"):
        raise RuntimeError("Game Mode trong bản sao không hợp lệ")
    value = previous["overlay"]
    if value == "null":
        light_shell(f"device_config delete game_overlay {quoted}")
    else:
        light_shell(f"device_config put game_overlay {quoted} {shlex.quote(value)}")
    if light_shell(f"device_config get game_overlay {quoted}") != value:
        raise RuntimeError("Chưa khôi phục được game_overlay")
    light_shell(f"cmd game mode {previous['mode']} {quoted}")
    if light_mode_state(pkg) != previous["mode"]:
        raise RuntimeError("Chưa khôi phục được Game Mode")


def configure_light_mode(cfg, restore=False):
    """Persist originals before mutation; retain backups on any partial failure."""
    messages = []
    try:
        sdk = int(light_shell("getprop ro.build.version.sdk"))
        if sdk < 31:
            raise RuntimeError("Cần Android 12+ có Game Manager")
        if light_shell("am get-current-user") != "0":
            raise RuntimeError("Chỉ hỗ trợ user 0 như luồng mở game hiện tại")
        path = light_backup_path()
        if os.path.exists(path):
            with open(path, encoding="utf-8") as handle:
                backups = json.load(handle)
        else:
            backups = {}
        fps = int(cfg.get("light_fps", 30))
        scale = float(cfg.get("light_scale", 0.5))
        if fps not in (30, 40, 45, 60) or scale not in (0.5, 0.7, 0.8, 0.9):
            raise RuntimeError("Profile không hợp lệ")
        packages = list(backups) if restore else list(dict.fromkeys(
            t["pkg"] for t in cfg["tabs"] if t.get("enabled", True)))
        for pkg in packages:
            try:
                if not re.fullmatch(r"[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+", pkg):
                    raise RuntimeError("Package không hợp lệ")
                quoted = shlex.quote(pkg)
                if restore:
                    light_restore_package(pkg, backups[pkg])
                    del backups[pkg]
                    light_save(path, backups)
                    messages.append(f"{pkg}: đã khôi phục; mở lại game để có hiệu lực")
                    continue
                previous = {
                    "mode": light_mode_state(pkg),
                    "overlay": light_shell(f"device_config get game_overlay {quoted}")}
                if not previous["overlay"]:
                    raise RuntimeError("Không đọc được cấu hình gốc")
                if pkg not in backups:
                    backups[pkg] = previous
                    light_save(path, backups)
                profile = f"mode=3,downscaleFactor={scale}"
                if sdk >= 33:
                    profile += f",fps={fps}"
                try:
                    light_shell(f"device_config put game_overlay {quoted} {shlex.quote(profile)}")
                    if light_shell(f"device_config get game_overlay {quoted}") != profile:
                        raise RuntimeError("Đọc lại cấu hình không khớp")
                    light_shell(f"cmd game mode battery {quoted}")
                    if light_mode_state(pkg) != "battery":
                        raise RuntimeError("Thiết bị không nhận battery mode")
                except Exception:
                    light_restore_package(pkg, previous)
                    raise
                suffix = f"{fps} FPS" if sdk >= 33 else "Android 12: không đặt FPS"
                messages.append(f"{pkg}: đã lưu profile scale {scale}, {suffix}")
            except Exception as error:
                messages.append(f"{pkg}: lỗi/bỏ qua: {error}; giữ bản sao để khôi phục")
    except Exception as error:
        messages.append(f"Light mode không khả dụng: {error}")
    for message in messages:
        write_log(message, "LIGHT")
    return messages


def light_verify(cfg):
    """Read active window scaling and compositor UID overrides, not just settings."""
    try:
        windows = light_shell("dumpsys window windows | grep -E 'Window #[0-9]|mOwnerUid=|mGlobalScale='")
        compositor = light_shell("dumpsys SurfaceFlinger | sed -n '/GameFrameRateOverrides=/,+2p'")
    except Exception as error:
        return [f"Không đọc được trạng thái áp dụng: {error}"]
    overrides = {int(uid): float(fps) for uid, fps in re.findall(
        r"\{\s*(\d+)\s*,\s*([\d.]+)\s+[-\d.]+\s*\}", compositor)}
    blocks = re.split(r"(?=\s*Window #\d+)", windows)
    reports = []
    for tab in cfg["tabs"]:
        if not tab.get("enabled", True):
            continue
        pkg = tab["pkg"]
        matching = [block for block in blocks if re.search(
            r"\b" + re.escape(pkg) + r"/", block)]
        if not matching:
            reports.append(f"{pkg}: CHỜ GAME — chưa có cửa sổ để kiểm tra")
            continue
        uid_match = re.search(r"mOwnerUid=(\d+)", "\n".join(matching))
        scales = [float(value) for block in matching for value in re.findall(
            r"mGlobalScale=([\d.]+)", block)]
        expected = float(cfg.get("light_scale", 0.5))
        if scales and all(abs(1 / value - expected) < 0.02 for value in scales if value > 0):
            render = f"RENDER ĐÃ ÁP DỤNG {expected:.0%}"
        elif scales:
            render = "RENDER CHƯA KHỚP — cần đóng/mở lại game"
        else:
            render = "ROM không cung cấp scale cửa sổ"
        fps = overrides.get(int(uid_match.group(1))) if uid_match else None
        if fps == cfg.get("light_fps", 30):
            limit = f"ĐÃ ÁP DỤNG giới hạn {fps:g} FPS"
        elif fps is not None:
            limit = f"giới hạn thực tế trong hệ thống: {fps:g} FPS"
        else:
            limit = "chưa thấy giới hạn FPS của UID trong SurfaceFlinger"
        reports.append(f"{pkg}: {render}; {limit}")
    for report in reports:
        write_log(report, "VERIFY")
    return reports


def menu_light_mode(cfg):
    print("\nTREO NHẸ: Android Game Mode, áp dụng cho các tab đang bật.")
    print("Game/ROM có thể bỏ qua; cần kiểm tra FPS thực tế. Không tự đóng game.")
    print(f"Tự áp dụng lúc vào Dashboard: {cfg.get('light_mode', False)}")
    print("[1] Bật + áp dụng 30 FPS / scale 50%  [2] Tắt + khôi phục  [3] Kiểm tra đang áp dụng  [0] Trở lại")
    choice = input("Chọn: ").strip()
    if choice == "3":
        for message in light_verify(cfg):
            print(message)
        print("FPS ở đây là giới hạn hệ thống, không phải FPS game đo được.")
        input("Enter để trở lại...")
        return
    if choice not in ("1", "2"):
        return
    cfg["light_mode"] = choice == "1"
    cfg["light_fps"], cfg["light_scale"] = 30, 0.5
    save_config(cfg)
    if not TARGET_DEVICE and not light_local_android() and not auto_fix_and_setup_adb():
        print("Không kết nối được ADB.")
    else:
        for message in configure_light_mode(cfg, restore=choice == "2"):
            print(message)
        if choice == "1":
            for message in light_verify(cfg):
                print(message)
        print("Mở lại từng game để thay đổi có hiệu lực. Không dùng R để thay cho đóng/mở game.")
    input("Enter để trở lại...")


def get_pkg_pid(pkg):
    out = run_adb_cmd(f"pidof {pkg}")
    if out:
        parts = out.split()
        if parts and parts[0].isdigit():
            return parts[0]
    out = run_adb_cmd(f"ps -A | grep {pkg}")
    for line in out.splitlines():
        if pkg in line:
            parts = line.split()
            if len(parts) >= 2 and parts[1].isdigit():
                return parts[1]
    return ""

def init_system(cfg):
    run_adb_cmd("settings put global enable_freeform_support 1")
    run_adb_cmd("settings put secure enable_freeform_support 1")
    run_adb_cmd("settings put global force_resizable_activities 1")
    run_adb_cmd("settings put secure force_resizable_activities 1")
    if cfg.get("auto_keep_alive", True):
        run_adb_cmd("svc power stayon true")

def get_ram_info():
    total, avail = 0, 0
    try:
        with open("/proc/meminfo", "r") as f:
            for line in f:
                if line.startswith("MemTotal:"):
                    total = int(line.split()[1])
                elif line.startswith("MemAvailable:"):
                    avail = int(line.split()[1])
        if total > 0:
            used_pct = int(((total - avail) / total) * 100)
            return used_pct, f"{used_pct}%"
    except Exception:
        pass
    return 50, "50%"

def make_gauge_bar(pct, length=10):
    filled = int((pct / 100) * length)
    color = CLR_AURORA if pct < 70 else (CLR_GOLD if pct < 85 else CLR_CRIMSON)
    return f"{color}" + "▰" * filled + f"{CLR_DARK}" + "▱" * (length - filled) + f"{CLR_RESET}"

# ================= 2-STEP SAFE BOOT =================
def launch_tab_safe_boot(tab, cfg, reason="MANUAL"):
    pkg = tab["pkg"]
    cmd_step1 = f"am start --user 0 --windowingMode 5 -f 0x10000000 -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -p {pkg}"
    run_adb_cmd(cmd_step1)
    write_log(f"Khởi động cửa sổ sảnh ({reason}): {tab['name']}", "LAUNCH")

    time.sleep(3.5)

    url = f"roblox://placeId={cfg['place_id']}"
    if cfg.get("link_code"):
        url += f"&linkCode={cfg['link_code']}"
    cmd_step2 = f'am start --user 0 --windowingMode 5 -f 0x10000000 -a android.intent.action.VIEW -d "{url}" -p {pkg}'
    run_adb_cmd(cmd_step2)
    write_log(f"Điều hướng bản đồ ({reason}): {tab['name']}", "JOIN")

def get_key_pressed():
    if not sys.stdin.isatty():
        return None
    dr, _, _ = select.select([sys.stdin], [], [], 0)
    if dr:
        return sys.stdin.read(1)
    return None

# ================= GIAO DIỆN LOGO MỚI: CYBER ASTRAL =================
def print_astral_banner():
    print(f"{CLR_NEBULA}    ◈━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━◈{CLR_RESET}")
    print(f"{CLR_VIOLET}     ██████╗ ██████╗   █████╗   ██████╗  ██████╗ ███╗   ██╗{CLR_RESET}")
    print(f"{CLR_ROSE}     ██╔══██╗██╔══██╗ ██╔══██╗ ██╔════╝ ██╔═══██╗████╗  ██║{CLR_RESET}")
    print(f"{CLR_CYAN}     ██║  ██║██████╔╝ ███████║ ██║  ███╗██║   ██║██╔██╗ ██║{CLR_RESET}")
    print(f"{CLR_AQUA}     ██║  ██║██╔══██╗ ██╔══██║ ██║   ██║██║   ██║██║╚██╗██║{CLR_RESET}")
    print(f"{CLR_AURORA}     ██████╔╝██║  ██║ ██║  ██║ ╚██████╔╝╚██████╔╝██║ ╚████║{CLR_RESET}")
    print(f"{CLR_GOLD}     ╚═════╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝  ╚═════╝  ╚═════╝ ╚═╝  ╚═══╝{CLR_RESET}")
    print(f"{CLR_NEBULA}    ◈━━━━━━━━━━━━━ {CLR_WHITE}A S T R A L   M A T R I X   (SAFE){CLR_NEBULA} ━━━━━━━━━━━━━◈{CLR_RESET}")

# ================= LIVE TUI DASHBOARD (CYBER MATRIX) =================
def run_dashboard(cfg):
    os.system("clear")
    print(f"\n{CLR_CYAN}⚡ [SYSTEM] Đang tối ưu hóa kết nối ADB Bridge...{CLR_RESET}")
    auto_fix_and_setup_adb()
    init_system(cfg)
    if has_vip_access():
        print(ensure_zram(cfg), flush=True)
    else:
        print("Gói FREE: không thiết lập zRAM.", flush=True)
    if cfg.get("light_mode", False):
        for message in configure_light_mode(cfg):
            print(message)
        print("Tab đã chạy cần đóng/mở lại để nhận downscale.")

    active_tabs = [t for t in cfg["tabs"] if t.get("enabled", True)]
    if not active_tabs:
        print(f"\n{CLR_CRIMSON}[!] Không có Tab nào được BẬT. Hãy vào [2] để kích hoạt.{CLR_RESET}")
        input(f"{CLR_GOLD}Nhấn Enter để quay lại...{CLR_RESET}")
        return

    tab_status = []
    for t in active_tabs:
        tab_status.append({
            "name": t["name"],
            "pkg": t["pkg"],
            "status": "STARTING",
            "pid": "--",
            "rejoins": 0,
            "last_launch": time.time(),
            "miss_count": 0
        })

    # Bung lần lượt từng tab
    for t in tab_status:
        launch_tab_safe_boot(t, cfg, reason="BOOT")
        t["last_launch"] = time.time()
        for remaining in range(150, 0, -10):
            print(f"Đang chờ {t['pkg']} tải game: {remaining}s; sau đó kiểm tra áp dụng.", flush=True)
            time.sleep(10)
        if cfg.get("light_mode", False):
            for message in light_verify(cfg):
                print(message, flush=True)

    start_time = datetime.now()
    last_restart_time = time.time()
    cycles = 0
    verification = []
    last_verification = 0
    total_rejoins = 0
    paused = False

    grace_seconds = cfg.get("crash_grace_seconds", 30)
    debounce_count = cfg.get("crash_debounce_count", 3)

    afk_running = True
    def anti_afk_task():
        while afk_running:
            time.sleep(cfg["anti_afk_interval"])
            if not afk_running or paused:
                continue
            run_adb_cmd("input tap 500 500")
            time.sleep(0.4)
            run_adb_cmd("input tap 500 1500")
            write_log("Nhịp chạm chống ngắt AFK", "AFK")

    afk_thread = threading.Thread(target=anti_afk_task, daemon=True)
    afk_thread.start()

    is_tty = sys.stdin.isatty()
    old_settings = None
    if is_tty:
        old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())

    try:
        while True:
            k = get_key_pressed()
            if k:
                k_l = k.lower()
                if k_l == 'q':
                    break
                elif k_l == 'p':
                    paused = not paused
                    write_log(f"Trạng thái: {'TẠM DỪNG' if paused else 'TIẾP TỤC'}", "USER")
                elif k_l == 'l':
                    open(LOG_FILE, "w").close()
                    write_log("Đã làm sạch nhật ký", "USER")
                elif k_l == 'r':
                    for t in tab_status:
                        launch_tab_safe_boot(t, cfg, reason="USER-FORCE")
                        t["last_launch"] = time.time()
                        t["rejoins"] += 1
                        t["miss_count"] = 0
                        total_rejoins += 1
                    write_log("Cưỡng chế Rejoin toàn bộ tab (THỦ CÔNG)", "USER")

            if not paused:
                cycles += 1

                if cfg.get("auto_restart_hours", 0) > 0:
                    if time.time() - last_restart_time > cfg["auto_restart_hours"] * 3600:
                        write_log("Khởi động lại tab chống tràn RAM (LỊCH ĐỊNH KỲ)", "LEAK")
                        for t in tab_status:
                            write_log(f"Force-stop theo lịch: {t['name']}", "LEAK")
                            run_adb_cmd(f"am force-stop {t['pkg']}")
                            time.sleep(1)
                            launch_tab_safe_boot(t, cfg, reason="SCHEDULED-RESTART")
                            t["last_launch"] = time.time()
                            t["miss_count"] = 0
                        last_restart_time = time.time()

                curr_t = time.time()
                for t in tab_status:
                    pid = get_pkg_pid(t['pkg'])
                    if pid:
                        t["pid"] = pid
                        t["status"] = "RUNNING"
                        t["miss_count"] = 0
                    else:
                        if curr_t - t["last_launch"] < grace_seconds:
                            t["status"] = "STARTING"
                        else:
                            t["miss_count"] += 1
                            if t["miss_count"] < debounce_count:
                                # Chưa đủ số lần liên tiếp để xác nhận crash thật —
                                # tránh relaunch nhầm do ADB mạng giật/timeout thoáng qua.
                                t["status"] = "STARTING"
                                write_log(
                                    f"Nghi ngờ mất tiến trình ({t['miss_count']}/{debounce_count}): {t['name']}",
                                    "WARN"
                                )
                            else:
                                t["pid"] = "--"
                                t["status"] = "CRASHED"
                                write_log(
                                    f"XÁC NHẬN CRASHED sau {t['miss_count']} lần kiểm tra liên tiếp — Relaunch: {t['name']}",
                                    "LEAK"
                                )
                                launch_tab_safe_boot(t, cfg, reason="CRASH-RECOVER")
                                t["last_launch"] = time.time()
                                t["rejoins"] += 1
                                total_rejoins += 1
                                t["miss_count"] = 0
                                time.sleep(3.0)

            if cfg.get("light_mode", False) and time.monotonic() - last_verification >= 60:
                verification = light_verify(cfg)
                last_verification = time.monotonic()
            os.system("clear")
            uptime_str = str(datetime.now() - start_time).split(".")[0]
            if _VIP_LAST_MESSAGE:
                print(f"[VIP] {_VIP_LAST_MESSAGE} · Gói hiện tại: {current_plan()}")
            for message in verification:
                print(message)
            ram_pct, ram_txt = get_ram_info()
            ram_bar = make_gauge_bar(ram_pct, 10)
            st_badge = f"{BG_WAIT} ⏸ PAUSED {CLR_RESET}" if paused else f"{BG_ACTIVE} ◉ ONLINE {CLR_RESET}"
            # Bảng điều khiển mới bo góc sắc nét
            print(f"{CLR_NEBULA}╭─────────────────── ❖ TELEMETRY CONTROL HUB ❖ ──────────────────╮{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_WHITE}Map ID:{CLR_RESET} {CLR_CYAN}{cfg['place_id']:<14}{CLR_RESET} │ {CLR_WHITE}Gói:{CLR_RESET} {CLR_AQUA}{current_plan()}{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_WHITE}Discord:{CLR_RESET} {CLR_CYAN}{DISCORD_URL}{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_WHITE}Trạng thái:{CLR_RESET} {st_badge} │ {CLR_WHITE}Uptime:{CLR_RESET} {CLR_GOLD}{uptime_str:<8}{CLR_RESET} │ {CLR_WHITE}RAM:{CLR_RESET} [{ram_bar}] {ram_txt:<4}{CLR_NEBULA}│{CLR_RESET}")
            print(f"{CLR_NEBULA}├───────┬──────────────────────────┬──────────┬───────────┬──────┤{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_GOLD}ID{CLR_RESET}    │ {CLR_GOLD}TÊN TIẾN TRÌNH (GÓI APK){CLR_RESET}   │ {CLR_GOLD}PID{CLR_RESET}      │ {CLR_GOLD}TÌNH TRẠNG{CLR_RESET}  │ {CLR_GOLD}HỒI{CLR_RESET}  {CLR_NEBULA}│{CLR_RESET}")
            print(f"{CLR_NEBULA}├───────┼──────────────────────────┼──────────┼───────────┼──────┤{CLR_RESET}")

            for idx, t in enumerate(tab_status):
                if t["status"] == "RUNNING":
                    pill = f"{CLR_AURORA}● ACTIVE {CLR_RESET}"
                elif t["status"] == "CRASHED":
                    pill = f"{CLR_CRIMSON}✖ RESCUE {CLR_RESET}"
                else:
                    pill = f"{CLR_CYAN}◌ BOOT   {CLR_RESET}"

                t_id = f"{CLR_ROSE}0{idx+1}{CLR_RESET}"
                pkg_str = f"{CLR_WHITE}{t['pkg']:<24}{CLR_RESET}"
                pid_str = f"{CLR_GOLD}{t['pid']:<8}{CLR_RESET}"
                rejoin_str = f"{CLR_AQUA}{t['rejoins']:<4}{CLR_RESET}"

                print(f"{CLR_NEBULA}│{CLR_RESET}  {t_id}   │ {pkg_str} │ {pid_str} │ {pill} │ {rejoin_str} {CLR_NEBULA}│{CLR_RESET}")

            r_fill = min(18, total_rejoins)
            r_bar = f"{CLR_ROSE}" + "■" * r_fill + f"{CLR_DARK}" + "·" * (18 - r_fill) + f"{CLR_RESET}"
            print(f"{CLR_NEBULA}├───────┴──────────────────────────┴──────────┴───────────┴──────┤{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_WHITE}Tổng số lần cứu cánh :{CLR_RESET} [{r_bar}] {CLR_GOLD}{total_rejoins:<3} lần{CLR_RESET} │ {CLR_WHITE}Chu kỳ:{CLR_RESET} {CLR_NEBULA}{cycles:<6}{CLR_RESET} {CLR_NEBULA}│{CLR_RESET}")
            print(f"{CLR_NEBULA}├────────────────────────────────────────────────────────────────┤{CLR_RESET}")
            print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_GOLD}[Q]{CLR_RESET} Menu  │  {CLR_NEBULA}[P]{CLR_RESET} Dừng  │  {CLR_CYAN}[R]{CLR_RESET} Rejoin  │  {CLR_GRAY}[L]{CLR_RESET} Dọn Log   {CLR_NEBULA}│{CLR_RESET}")
            print(f"{CLR_NEBULA}├────────────────────────────────────────────────────────────────┤{CLR_RESET}")

            logs = []
            if os.path.exists(LOG_FILE):
                with open(LOG_FILE, "r") as f:
                    logs = [line.strip() for line in f.readlines()[-3:]]
            print(f"{CLR_NEBULA}│{CLR_RESET} {CLR_GRAY}❖ Nhật ký truyền phát tín hiệu (Live Stream):{CLR_RESET}                   {CLR_NEBULA}│{CLR_RESET}")
            for l in logs:
                print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_CYAN}›{CLR_RESET} {CLR_WHITE}{l[:58]:<58}{CLR_RESET} {CLR_NEBULA}│{CLR_RESET}")
            print(f"{CLR_NEBULA}╰────────────────────────────────────────────────────────────────╯{CLR_RESET}")

            time.sleep(cfg["check_interval"])

    finally:
        afk_running = False
        if is_tty and old_settings:
            termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

# ================= MENU QUẢN LÝ TAB MATRIX =================
def list_available_delta_packages(cfg):
    global TARGET_DEVICE
    if not TARGET_DEVICE:
        auto_fix_and_setup_adb()
    out = run_adb_cmd("pm list packages | grep -i roblox")
    all_pkgs = []
    for line in out.splitlines():
        pkg = line.replace("package:", "").strip()
        if pkg:
            all_pkgs.append(pkg)
    used_pkgs = {t["pkg"] for t in cfg["tabs"]}
    return [p for p in all_pkgs if p not in used_pkgs]

def menu_tab_manager(cfg):
    while True:
        os.system("clear")
        print(f"\n{CLR_NEBULA}╭────────────────── ❖ QUẢN LÝ DANH SÁCH TAB ❖ ──────────────────╮{CLR_RESET}")
        for idx, t in enumerate(cfg["tabs"]):
            st = f"{CLR_AURORA}[✔ BẬT]{CLR_RESET}" if t.get("enabled", True) else f"{CLR_DARK}[✖ TẮT]{CLR_RESET}"
            print(f"{CLR_NEBULA}│{CLR_RESET}  [{idx+1}] {st} {CLR_WHITE}{t['name']:<20}{CLR_RESET} ({CLR_NEBULA}{t['pkg']}{CLR_RESET})")
        print(f"{CLR_NEBULA}├────────────────────────────────────────────────────────────────┤{CLR_RESET}")
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_GOLD}[1-{len(cfg['tabs'])}]{CLR_RESET} Bật/Tắt Tab tương ứng")
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_CYAN}[E]{CLR_RESET} Đổi tên hiển thị Tab")
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_AURORA}[A]{CLR_RESET} ➕ Thêm Tab mới")
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_CRIMSON}[X]{CLR_RESET} ✖ Xóa bớt một Tab")
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_GRAY}[0]{CLR_RESET} Quay lại Menu chính")
        print(f"{CLR_NEBULA}╰────────────────────────────────────────────────────────────────╯{CLR_RESET}")

        ch = input(f"\n{CLR_CYAN}❖ Lựa chọn của bạn:{CLR_RESET} ").strip()
        if ch == "0":
            save_config(cfg)
            break
        elif ch.isdigit() and 1 <= int(ch) <= len(cfg["tabs"]):
            idx = int(ch) - 1
            cfg["tabs"][idx]["enabled"] = not cfg["tabs"][idx].get("enabled", True)
            save_config(cfg)
        elif ch.upper() == "E":
            t_num = input("Số thứ tự Tab muốn đổi tên: ").strip()
            if t_num.isdigit() and 1 <= int(t_num) <= len(cfg["tabs"]):
                idx = int(t_num) - 1
                new_n = input("Tên mới: ").strip()
                if new_n:
                    cfg["tabs"][idx]["name"] = new_n
                    save_config(cfg)
        elif ch.upper() == "A":
            new_title = input("Tên hiển thị (VD: Clone 3 [w]): ").strip()
            if not new_title:
                continue
            available = list_available_delta_packages(cfg)
            if not available:
                print(f"\n{CLR_CRIMSON}[!] Không còn Package Roblox/Delta nào khả dụng để thêm.{CLR_RESET}")
                time.sleep(1.5)
                continue
            print(f"\n{CLR_NEBULA}── Chọn Package (đã lọc bỏ Package đã dùng) ──{CLR_RESET}")
            for idx, p in enumerate(available):
                print(f"  [{idx+1}] {p}")
            sel = input(f"\n{CLR_CYAN}❖ Chọn số thứ tự Package: {CLR_RESET}").strip()
            if sel.isdigit() and 1 <= int(sel) <= len(available):
                new_pkg = available[int(sel) - 1]
                cfg["tabs"].append({"name": new_title, "pkg": new_pkg, "enabled": True})
                save_config(cfg)
                print(f"\n{CLR_AURORA}✔ Đã thêm Tab: {new_title} ({new_pkg}){CLR_RESET}")
                time.sleep(1.5)
        elif ch.upper() == "X":
            t_num = input("Số thứ tự Tab muốn xóa: ").strip()
            if t_num.isdigit() and 1 <= int(t_num) <= len(cfg["tabs"]):
                cfg["tabs"].pop(int(t_num) - 1)
                save_config(cfg)

# ================= MENU CHÍNH ASTRAL =================
def prepare_environment():
    """Install the known Termux dependency, then verify it really executes."""
    if not light_local_android():
        print("Dragon v2 cần chạy trong Termux trên Android.")
        return False
    if not shutil.which("adb"):
        pkg = shutil.which("pkg")
        if not pkg or os.geteuid() == 0:
            print("Không thể tự cài ADB: hãy mở bằng Termux thường, không chạy Python qua su.")
            return False
        print("[SETUP] Thiếu ADB. Đang cài android-tools từ kho Termux...", flush=True)
        try:
            result = subprocess.run([pkg, "install", "-y", "android-tools"], timeout=300)
            if result.returncode or not shutil.which("adb"):
                print("[SETUP] Cài chưa thành công. Kiểm tra mạng/kho Termux rồi mở lại; tool chưa chạy game.")
                return False
        except (OSError, subprocess.TimeoutExpired) as error:
            print(f"[SETUP] Không cài được android-tools: {error}")
            return False
    env = os.environ.copy()
    env.pop("LD_PRELOAD", None)
    env.pop("LD_LIBRARY_PATH", None)
    try:
        result = subprocess.run(["adb", "version"], capture_output=True, text=True, env=env, timeout=10)
        if result.returncode:
            print("[SETUP] ADB đã có nhưng không chạy được: " + result.stderr.strip())
            return False
    except (OSError, subprocess.TimeoutExpired) as error:
        print(f"[SETUP] Không chạy được ADB: {error}")
        return False
    if not shutil.which("su"):
        print("[SETUP] Chưa có root/su. Tool cần quyền root để thiết lập kết nối ADB tự động.")
        return False
    try:
        result = subprocess.run(["su", "-c", "/system/bin/id -u"], capture_output=True,
                                text=True, env=env, timeout=15)
        if result.returncode or result.stdout.strip() != "0":
            print("[SETUP] Chưa được cấp root cho Termux. Cho phép trong trình quản lý root rồi mở lại.")
            return False
    except (OSError, subprocess.TimeoutExpired) as error:
        print(f"[SETUP] Chưa xác nhận được quyền root: {error}")
        return False
    print("[SETUP] Python, ADB và quyền root: OK", flush=True)
    return True


def start_auto(cfg):
    # Apply the graphics profile once; saved settings never grant VIP access.
    cfg.update(light_mode=True, light_fps=30, light_scale=0.5,
               auto_zram=has_vip_access())
    save_config(cfg)
    run_dashboard(cfg)


def print_main_menu(cfg, notice=""):
    print(f"\n{CLR_NEBULA}╭────────────────────── ❖ BẢNG ĐIỀU KHIỂN CHÍNH ❖ ──────────────────────╮{CLR_RESET}")
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_WHITE}Map ID  :{CLR_RESET} {CLR_CYAN}{cfg['place_id']:<20}{CLR_RESET} │ {CLR_WHITE}Gói:{CLR_RESET} {CLR_AQUA}{current_plan()}{CLR_RESET}")
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_WHITE}Discord:{CLR_RESET} {CLR_CYAN}{DISCORD_URL}{CLR_RESET}")
    print(f"{CLR_NEBULA}├────────────────────────────────────────────────────────────────────────┤{CLR_RESET}")
    plan_label = "VIP" if has_vip_access() else "Free"
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_GOLD}[1]{CLR_RESET} {CLR_CYAN}⚡ Khởi động treo auto (bản {plan_label}){CLR_RESET}")
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_NEBULA}[2]{CLR_RESET} 🎮 Quản lý danh sách Tab (Bật/Tắt, đổi gói clone)")
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_AQUA}[3]{CLR_RESET} 🕹️  Thiết lập Place ID & LinkCode phòng chơi")
    if not has_vip_access():
        print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_AURORA}[4]{CLR_RESET} Dán key VIP")
    print(f"{CLR_NEBULA}│{CLR_RESET}  {CLR_CRIMSON}[5]{CLR_RESET} 🚪 Thoát chương trình")
    print(f"{CLR_NEBULA}╰────────────────────────────────────────────────────────────────────────╯{CLR_RESET}")
    if notice:
        print(f"\n{CLR_GOLD}{notice}{CLR_RESET}")


def main():
    if not prepare_environment():
        return
    cfg = load_config()
    notice = ""

    while True:
        os.system("clear")
        print_astral_banner()
        print_main_menu(cfg, notice)
        notice = ""

        choice = input(f"\n{CLR_CYAN}Dragon{CLR_ROSE}@{CLR_AURORA}Astral{CLR_RESET}:{CLR_DARK}~${CLR_RESET} ").strip()
        if choice == "1":
            start_auto(cfg)
        elif choice == "2":
            menu_tab_manager(cfg)
        elif choice == "3":
            nid = input(f"\n{CLR_GOLD}Nhập Place ID mới: {CLR_RESET}").strip()
            if nid:
                cfg["place_id"] = nid
            nlc = input(f"{CLR_GOLD}Nhập LinkCode VIP (Enter nếu bỏ qua): {CLR_RESET}").strip()
            cfg["link_code"] = nlc
            save_config(cfg)
        elif choice == "4" and not has_vip_access():
            notice = activate_vip()
        elif choice == "5":
            break

if __name__ == "__main__":
    main()
DRAGON_V2_EMBEDDED_PYTHON_END
# Python reads the real terminal; its stdin is not the source heredoc.
python3 "$task_python" "$@"
