#!/usr/bin/python3
import hashlib
import io
import json
import os
import random
import shutil
import socket
import subprocess
import sys
import tarfile
import time
import urllib.request
import zipfile

if sys.version_info.major != 3:
    print("Please run with python3.")
    sys.exit(1)

rPath = os.path.dirname(os.path.realpath(__file__))

# Distribuciones soportadas
SUPPORTED_DISTROS = {
    "ubuntu": ["18.04", "20.04", "22.04", "24.04"],
    "debian": ["11", "12", "13"],
    "rocky": ["8", "9"],
    "almalinux": ["8", "9"],
    "centos": ["7", "8"],
    "rhel": ["8", "9"],
}

PACKAGES = {
    "debian": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "libcurl4",
        "libgeoip-dev",
        "libxslt1-dev",
        "libonig-dev",
        "e2fsprogs",
        "wget",
        "mariadb-server",
        "mariadb-client",
        "sysstat",
        "alsa-utils",
        "v4l-utils",
        "certbot",
        "iptables-persistent",
        "libjpeg-dev",
        "libpng-dev",
        "libharfbuzz-dev",
        "libfribidi-dev",
        "libogg0",
        "libnuma1",
        "xz-utils",
        "zip",
        "unzip",
        "libssh2-1",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
        "cron",
        "git",
        "curl",
    ],
    "debian13": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "libcurl4",
        "wget",
        "unzip",
        "zip",
        "xz-utils",
        "cron",
        "git",
        "sysstat",
        "perl",
        "gawk",
        "socat",
        "libxml2-dev",
        "libxslt1-dev",
        "libonig5",
        "libonig-dev",
        "zlib1g-dev",
        "libssl-dev",
        "pkg-config",
        "autoconf",
        "automake",
        "alsa-utils",
        "v4l-utils",
        "e2fsprogs",
        "certbot",
        "iptables-persistent",
        "libssh2-1",
        "libssh2-1-dev",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "libjpeg-dev",
        "libpng-dev",
        "libharfbuzz-dev",
        "libfribidi-dev",
        "libgeoip1",
        "geoip-bin",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
        "libnuma1",
    ],
    "ubuntu20": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "wget",
        "curl",
        "unzip",
        "zip",
        "xz-utils",
        "cron",
        "git",
        "sysstat",
        "ca-certificates",
        "libcurl4-gnutls-dev",
        "libxml2-dev",
        "libxslt1-dev",
        "libonig5",
        "libonig-dev",
        "libjpeg-dev",
        "libpng-dev",
        "zlib1g-dev",
        "alsa-utils",
        "v4l-utils",
        "e2fsprogs",
        "iptables-persistent",
        "certbot",
        "python3-certbot",
        "libssh2-1",
        "libssh2-1-dev",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
        "libnuma1",
    ],
    "ubuntu22": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "libcurl4",
        "libgeoip-dev",
        "libxslt1-dev",
        "libonig-dev",
        "e2fsprogs",
        "wget",
        "curl",
        "unzip",
        "zip",
        "xz-utils",
        "cron",
        "git",
        "sysstat",
        "ca-certificates",
        "libxml2-dev",
        "libonig5",
        "zlib1g-dev",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "alsa-utils",
        "v4l-utils",
        "certbot",
        "python3-certbot",
        "iptables-persistent",
        "libjpeg-dev",
        "libpng-dev",
        "libharfbuzz-dev",
        "libfribidi-dev",
        "libogg0",
        "libnuma1",
        "libssh2-1",
        "libssh2-1-dev",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
    ],
    "ubuntu24": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "libcurl4t64",
        "wget",
        "unzip",
        "zip",
        "xz-utils",
        "cron",
        "git",
        "sysstat",
        "perl",
        "gawk",
        "socat",
        "libxml2-dev",
        "libxslt1-dev",
        "libonig5",
        "libonig-dev",
        "zlib1g-dev",
        "libssl-dev",
        "pkg-config",
        "autoconf",
        "automake",
        "alsa-utils",
        "v4l-utils",
        "e2fsprogs",
        "certbot",
        "python3-certbot",
        "ufw",
        "libssh2-1t64",
        "libssh2-1-dev",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "libjpeg-dev",
        "libpng-dev",
        "libharfbuzz-dev",
        "libfribidi-dev",
        "libgeoip1t64",
        "geoip-bin",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
        "libnuma1",
    ],
    "debian11": [
        "iproute2",
        "net-tools",
        "dirmngr",
        "gpg-agent",
        "software-properties-common",
        "libcurl4",
        "libgeoip-dev",
        "libxslt1-dev",
        "libonig-dev",
        "e2fsprogs",
        "wget",
        "curl",
        "unzip",
        "zip",
        "xz-utils",
        "cron",
        "git",
        "sysstat",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "alsa-utils",
        "v4l-utils",
        "certbot",
        "iptables-persistent",
        "libjpeg-dev",
        "libpng-dev",
        "libharfbuzz-dev",
        "libfribidi-dev",
        "libogg0",
        "libnuma1",
        "libssh2-1",
        "libssh2-1-dev",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
    ],
    "redhat": [
        "epel-release",
        "wget",
        "mariadb-server",
        "mariadb",
        "sysstat",
        "alsa-utils",
        "v4l-utils",
        "libcurl-devel",
        "geoip-devel",
        "libxslt-devel",
        "oniguruma-devel",
        "e2fsprogs",
        "libjpeg-turbo-devel",
        "libpng-devel",
        "harfbuzz-devel",
        "fribidi-devel",
        "libogg",
        "xz",
        "zip",
        "unzip",
        "libssh2-devel",
        "cronie",
        "certbot",
        "iptables-services",
        "GeoIP-update",
        "git",
        "curl",
        "libsodium",
        "numactl",
        "kernel-tools",
    ],
}

rRemove = ["mysql-server"]
rMySQLCnfTemplate = """\
# XC_VM
[client]
port                            = 3306

[mysqld_safe]
nice                            = 0

[mysqld]
user                            = mysql
port                            = 3306
basedir                         = /usr
datadir                         = /var/lib/mysql
tmpdir                          = /tmp
lc-messages-dir                 = /usr/share/mysql
skip-external-locking
skip-name-resolve
bind-address                    = *

# MyISAM
key_buffer_size                 = {{KEY_BUFFER}}M
myisam_sort_buffer_size         = 4M
myisam-recover-options          = BACKUP
max_length_for_sort_data        = 4096

# Connections
max_connections                 = {{MAX_CONNECTIONS}}
back_log                        = {{BACK_LOG}}
max_connect_errors              = 1000

# Packet and cache
max_allowed_packet              = 16M
open_files_limit                = 2048
innodb_open_files               = 1024
table_open_cache                = 1024
table_definition_cache          = 1024

# Temp tables
tmp_table_size                  = {{TMP_TABLE_SIZE}}M
max_heap_table_size             = {{TMP_TABLE_SIZE}}M

# InnoDB
innodb_buffer_pool_size         = {{BUFFER_POOL_SIZE}}
innodb_buffer_pool_instances    = {{BUFFER_POOL_INSTANCES}}
innodb_read_io_threads          = 4
innodb_write_io_threads         = 4
innodb_flush_log_at_trx_commit  = 1
innodb_flush_method             = O_DIRECT
innodb_file_per_table           = 1
innodb_io_capacity              = 1000
innodb_table_locks              = 1
innodb_lock_wait_timeout        = 30

# Logging
expire_logs_days                = 7
max_binlog_size                 = 64M

# Query cache - disabled
query_cache_limit               = 0
query_cache_size                = 0
query_cache_type                = 0

performance_schema              = 0

sql_mode                        = "NO_ENGINE_SUBSTITUTION"

[mariadb]
thread_cache_size               = {{THREAD_CACHE}}
thread_handling                 = pool-of-threads
thread_pool_size                = 4
thread_pool_idle_timeout        = 20
thread_pool_max_threads         = {{THREAD_POOL_MAX_THREADS}}

[mysqldump]
quick
quote-names
max_allowed_packet              = 16M

[mysql]

[isamchk]
key_buffer_size                 = 8M"""
rConfig = """\
; XC_VM Configuration
; -----------------
; To change your username or password, modify BOTH
; below and XC_VM will read and re-encrypt them.

[XC_VM]
hostname    =   "127.0.0.1"
database    =   "xc_vm"
port        =   3306
server_id   =   1

[Encrypted]
username    =   "%s"
password    =   "%s"
"""
rSysCtl = """\
# XC_VM

net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
net.ipv4.tcp_rmem = 8192 87380 134217728
net.ipv4.udp_rmem_min = 16384
net.core.rmem_default = 262144
net.core.rmem_max = 268435456
net.ipv4.tcp_wmem = 8192 65536 134217728
net.ipv4.udp_wmem_min = 16384
net.core.wmem_default = 262144
net.core.wmem_max = 268435456
net.core.somaxconn = 1000000
net.core.netdev_max_backlog = 250000
net.core.optmem_max = 65535
net.ipv4.tcp_max_tw_buckets = 1440000
net.ipv4.tcp_max_orphans = 16384
net.ipv4.ip_local_port_range = 2000 65000
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 15
fs.file-max=20970800
fs.nr_open=20970800
fs.aio-max-nr=20970800
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_mtu_probing = 1
net.ipv4.route.flush = 1
net.ipv6.route.flush = 1"""
rSystemd = """\
[Unit]
SourcePath=/home/xc_vm/service
Description=XC_VM Service
After=network.target
StartLimitIntervalSec=0

[Service]
Type=simple
User=root
Restart=always
RestartSec=1
LimitNOFILE=655350
TimeoutStopSec=30
KillMode=mixed
ExecStart=/bin/bash /home/xc_vm/service start
ExecStop=/bin/bash /home/xc_vm/service stop
ExecReload=/bin/bash /home/xc_vm/service restart

[Install]
WantedBy=multi-user.target"""
rChoice = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
rConfigPath = "/home/xc_vm/config/config.ini"


class col:
    HEADER = "\033[95m"
    OKBLUE = "\033[94m"
    OKGREEN = "\033[92m"
    WARNING = "\033[93m"
    FAIL = "\033[91m"
    ENDC = "\033[0m"
    BOLD = "\033[1m"
    UNDERLINE = "\033[4m"


# Check if running as root
def check_root():
    """Check if script is running as root"""
    if os.geteuid() != 0:
        printc("This script must be run as root.", col.FAIL)
        printc("Please use: su -c 'python3 your_script_name.py'", col.OKBLUE)
        sys.exit(1)


# Install prerequisites
def install_prerequisites(dist_info):
    """Install prerequisites like sudo and curl if they are missing"""
    if dist_info["family"] == "debian":
        printc("Checking for prerequisites (sudo, curl)...", col.OKBLUE)

        # Check if sudo exists
        ret, _, _ = run_command("which sudo", capture_output=True)
        sudo_missing = ret != 0

        # Check if curl exists
        ret, _, _ = run_command("which curl", capture_output=True)
        curl_missing = ret != 0

        if sudo_missing or curl_missing:
            printc("Installing missing prerequisites...", col.WARNING)
            printc("Updating package lists...", col.OKBLUE)
            run_command("apt-get update -y")

            packages_to_install = []
            if sudo_missing:
                packages_to_install.append("sudo")
            if curl_missing:
                packages_to_install.append("curl")

            if packages_to_install:
                packages_str = " ".join(packages_to_install)
                printc(f"Installing {packages_str}...", col.OKBLUE)
                run_command(f"apt-get install -y {packages_str}")
                printc("Prerequisites installed successfully.", col.OKGREEN)
        else:
            printc("Prerequisites (sudo, curl) are already installed.", col.OKGREEN)

    elif dist_info["family"] == "redhat":
        printc("Checking for prerequisites (sudo, curl, wget)...", col.OKBLUE)

        ret, _, _ = run_command("which sudo", capture_output=True)
        sudo_missing = ret != 0

        ret, _, _ = run_command("which curl", capture_output=True)
        curl_missing = ret != 0

        ret, _, _ = run_command("which wget", capture_output=True)
        wget_missing = ret != 0

        if sudo_missing or curl_missing or wget_missing:
            printc("Installing missing prerequisites...", col.WARNING)
            packages_to_install = []
            if sudo_missing:
                packages_to_install.append("sudo")
            if curl_missing:
                packages_to_install.append("curl")
            if wget_missing:
                packages_to_install.append("wget")

            if packages_to_install:
                packages_str = " ".join(packages_to_install)
                printc(f"Installing {packages_str}...", col.OKBLUE)
                run_command(
                    f"yum install -y {packages_str} || dnf install -y {packages_str}"
                )
                printc("Prerequisites installed successfully.", col.OKGREEN)
        else:
            printc("Prerequisites are already installed.", col.OKGREEN)


def compute_md5(file_path):
    """Compute MD5 hash of a file"""
    md5 = hashlib.md5()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            md5.update(chunk)
    return md5.hexdigest()


def download_release_hash(repo, release_tag, file_name):
    """Download hashes.md5 from a GitHub release and return the hash for file_name"""
    hash_url = f"https://github.com/Vateron-Media/{repo}/releases/download/{release_tag}/hashes.md5"
    try:
        req = urllib.request.Request(hash_url)
        req.add_header("User-Agent", "XC_VM-Installer/1.0")
        with urllib.request.urlopen(req, timeout=15) as response:
            content = response.read().decode().strip()
            for line in content.split("\n"):
                line = line.strip()
                if not line:
                    continue
                parts = line.split(None, 1)
                if len(parts) == 2 and parts[1] == file_name:
                    return parts[0]
    except Exception as e:
        printc(f"Warning: Could not download hash file: {e}", col.WARNING)
    return None


def get_latest_binaries_tag():
    """Get the latest release tag from XC_VM_Binaries GitHub repo"""
    api_url = (
        "https://api.github.com/repos/Vateron-Media/XC_VM_Binaries/releases/latest"
    )
    try:
        req = urllib.request.Request(api_url)
        req.add_header("User-Agent", "XC_VM-Installer/1.0")
        with urllib.request.urlopen(req, timeout=15) as response:
            data = json.loads(response.read().decode())
            return data["tag_name"]
    except Exception as e:
        printc(f"Failed to get latest binaries release tag: {e}", col.WARNING)
        return None


def write_bin_version_file(release_tag, asset_name, dist_id, version):
    """Write installed binaries release metadata to /home/xc_vm/bin/bin_version.json"""
    version_path = "/home/xc_vm/bin/bin_version.json"
    payload = {
        "owner": "Vateron-Media",
        "repository": "XC_VM_Binaries",
        "release": release_tag,
        "asset": asset_name,
        "distribution": dist_id,
        "distribution_version": version,
        "updated_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }

    try:
        with open(version_path, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=4)
            f.write("\n")
        try:
            run_command(f"chown xc_vm:xc_vm {version_path}")
        except Exception:
            pass
        printc(
            f"Binaries version file updated: {version_path} ({release_tag})",
            col.OKGREEN,
        )
        return True
    except Exception as e:
        printc(f"Warning: Failed to update {version_path}: {e}", col.WARNING)
        return False


# Install distribution-specific binaries
def install_distribution_binaries(dist_id, version):
    """Download and install distribution-specific binaries from GitHub releases"""
    printc(f"Installing {dist_id} {version} specific binaries...", col.OKBLUE)

    # Determinar el nombre del archivo en GitHub releases
    major = version.split(".")[0]
    if dist_id == "ubuntu":
        if major in ["18", "20", "22", "24"]:
            patch_name = f"ubuntu_{major}.tar.gz"
            display_name = f"Ubuntu {major}"
        else:
            printc(f"Ubuntu version {version} not supported for patches", col.WARNING)
            return False
    elif dist_id == "debian":
        if major in ["11", "12", "13"]:
            patch_name = f"debian_{major}.tar.gz"
            display_name = f"Debian {major}"
        else:
            printc(f"Debian version {version} not supported for patches", col.WARNING)
            return False
    elif dist_id in ["rocky", "almalinux", "rhel", "centos"]:
        if major in ["8", "9"]:
            patch_name = f"rhel_{major}.tar.gz"
            display_name = f"RHEL/Rocky/Alma {major}"
        else:
            printc(
                f"{dist_id} version {version} not supported for patches", col.WARNING
            )
            return False
    else:
        printc(f"Distribution {dist_id} not supported for patches", col.WARNING)
        return False

    # Get latest release tag from GitHub
    release_tag = get_latest_binaries_tag()
    if not release_tag:
        printc("Could not determine latest binaries release, skipping", col.WARNING)
        return False

    remote_url = f"https://github.com/Vateron-Media/XC_VM_Binaries/releases/download/{release_tag}/{patch_name}"
    temp_tar_file = f"/tmp/{patch_name}"
    temp_extract_dir = f"/tmp/{patch_name.replace('.tar.gz', '_extract')}"

    try:
        # Always download fresh binaries from GitHub
        printc(
            f"Downloading {display_name} binaries from GitHub release {release_tag}...",
            col.OKBLUE,
        )
        printc(f"URL: {remote_url}", col.OKBLUE)

        # Remove old file if exists
        if os.path.exists(temp_tar_file):
            os.remove(temp_tar_file)

        # Retry on transient network/DNS failures (e.g. resolver not ready yet).
        last_err = None
        for attempt in range(1, 5):
            try:
                urllib.request.urlretrieve(remote_url, temp_tar_file)
                last_err = None
                break
            except Exception as e:
                last_err = e
                printc(f"Download attempt {attempt}/4 failed: {e}", col.WARNING)
                time.sleep(5)
        if last_err is not None:
            raise last_err

        if not (os.path.exists(temp_tar_file) and os.path.getsize(temp_tar_file) > 0):
            printc(f"Failed to download {display_name} binaries", col.FAIL)
            return False

        # MD5 verification
        expected_hash = download_release_hash("XC_VM_Binaries", release_tag, patch_name)
        if expected_hash:
            actual_hash = compute_md5(temp_tar_file)
            if actual_hash != expected_hash:
                printc(f"MD5 verification failed for {patch_name}: expected {expected_hash}, got {actual_hash}", col.FAIL)
                os.remove(temp_tar_file)
                return False
            printc(f"MD5 verification passed for {patch_name}", col.OKGREEN)
        else:
            printc(f"Warning: Could not retrieve MD5 hash for {patch_name}, skipping verification", col.WARNING)

        file_size_mb = os.path.getsize(temp_tar_file) / (1024 * 1024)
        printc(
            f"{display_name} binaries downloaded: {file_size_mb:.1f} MB", col.OKGREEN
        )

        # 2. Clean previous extraction directory if exists
        if os.path.exists(temp_extract_dir):
            shutil.rmtree(temp_extract_dir)

        # 3. Extract to temporary directory
        printc(
            f"Extracting {display_name} binaries to temporary location...", col.OKBLUE
        )
        with tarfile.open(temp_tar_file, "r:gz") as tar:
            tar.extractall(
                path=temp_extract_dir, members=_safe_tar_members(tar, temp_extract_dir)
            )

        # Auto-detect directory structure
        major = version.split(".")[0]
        # Variants: debian11, debian_11, ubuntu20, ubuntu_20
        distro_variants = [
            f"{dist_id}{major}",
            f"{dist_id}_{major}",
        ]

        # Possible paths where binaries might be
        possible_paths = []
        for dname in distro_variants:
            possible_paths.append(os.path.join(temp_extract_dir, dname, "bin"))
            possible_paths.append(os.path.join(temp_extract_dir, dname))
        possible_paths.append(os.path.join(temp_extract_dir, "bin"))
        possible_paths.append(temp_extract_dir)

        source_bin_dir = None
        for path in possible_paths:
            if os.path.exists(path):
                # Check if it contains binary files or typical directories
                contents = os.listdir(path)
                has_binaries = any(
                    item in contents for item in ["php", "nginx", "nginx_rtmp", "bin"]
                )

                if has_binaries or path.endswith("/bin"):
                    source_bin_dir = path
                    printc(f"Structure found: {source_bin_dir}", col.OKGREEN)
                    break

        # If not found in expected paths, search recursively
        if not source_bin_dir:
            printc("Searching directory structure recursively...", col.OKBLUE)
            for root, dirs, files in os.walk(temp_extract_dir):
                # Search for directories containing typical binaries
                if any(item in dirs for item in ["php", "nginx", "nginx_rtmp", "bin"]):
                    source_bin_dir = root
                    printc(
                        f"Estructura encontrada recursivamente: {source_bin_dir}",
                        col.OKGREEN,
                    )
                    break

        target_bin_dir = "/home/xc_vm/bin"

        if not source_bin_dir:
            printc(
                "Error: No se pudo encontrar la estructura de binarios en el parche.",
                col.FAIL,
            )
            printc(
                f"Contenido de {temp_extract_dir}: {os.listdir(temp_extract_dir)}",
                col.WARNING,
            )
            # Show full structure for debugging
            printc("Full structure of extracted directory:", col.WARNING)
            for root, dirs, files in os.walk(temp_extract_dir):
                level = root.replace(temp_extract_dir, "").count(os.sep)
                indent = " " * 2 * level
                printc(f"{indent}{os.path.basename(root)}/", col.WARNING)
                subindent = " " * 2 * (level + 1)
                for file in files[:10]:  # Limit to 10 files to avoid clutter
                    printc(f"{subindent}{file}", col.WARNING)
                if len(files) > 10:
                    printc(
                        f"{subindent}... and {len(files) - 10} more files", col.WARNING
                    )
            return False

        # 4. Replace specific files, not the entire directory
        printc(
            f"Replacing specific binaries with {display_name} versions...",
            col.OKBLUE,
        )

        # Recursively walk source directory files
        for root, dirs, files in os.walk(source_bin_dir):
            # Calculate relative path from source directory
            rel_path = os.path.relpath(root, source_bin_dir)

            # Create corresponding target directories if they don't exist
            if rel_path != ".":
                target_dir = os.path.join(target_bin_dir, rel_path)
                os.makedirs(target_dir, exist_ok=True)

            # Copy files
            for file in files:
                source_file = os.path.join(root, file)
                if rel_path == ".":
                    target_file = os.path.join(target_bin_dir, file)
                else:
                    target_file = os.path.join(target_bin_dir, rel_path, file)

                # Create directories if needed
                os.makedirs(os.path.dirname(target_file), exist_ok=True)

                # Copy file (overwriting if exists)
                shutil.copy2(source_file, target_file)
                # printc(f"Updated: {target_file}", col.OKBLUE)

        printc(f"{display_name} binaries updated successfully", col.OKGREEN)

        # 5. Set permissions on key executables
        printc(f"Setting permissions for {display_name} binaries...", col.OKBLUE)
        executables_to_chmod = [
            "/home/xc_vm/bin/php/bin/php",
            "/home/xc_vm/bin/php/sbin/php-fpm",
            "/home/xc_vm/bin/nginx/sbin/nginx",
            "/home/xc_vm/bin/nginx_rtmp/sbin/nginx_rtmp",
        ]
        for exe_path in executables_to_chmod:
            if os.path.exists(exe_path):
                run_command(f"chmod +x {exe_path}")

        # 6. Clean up temporary files
        printc("Cleaning up temporary files...", col.OKBLUE)
        if os.path.exists(temp_tar_file):
            os.remove(temp_tar_file)
        if os.path.exists(temp_extract_dir):
            shutil.rmtree(temp_extract_dir)

        write_bin_version_file(release_tag, patch_name, dist_id, version)

        printc(
            f"{display_name} specific binary installation completed",
            col.OKGREEN,
        )
        return True

    except Exception as e:
        printc(f"Error installing {display_name} binaries: {e}", col.FAIL)
        # Clean up archive files on error
        if os.path.exists(temp_tar_file):
            os.remove(temp_tar_file)
        if os.path.exists(temp_extract_dir):
            shutil.rmtree(temp_extract_dir)
        return False


def detect_distribution():
    """Detect Linux distribution and version without external modules"""
    dist_id = "unknown"
    version = "unknown"
    family = "unknown"

    # Try /etc/os-release first (standard method)
    if os.path.exists("/etc/os-release"):
        try:
            with open("/etc/os-release", "r") as f:
                lines = f.readlines()
                for line in lines:
                    line = line.strip()
                    if line.startswith("ID="):
                        dist_id = line.split("=")[1].strip().strip('"')
                    elif line.startswith("VERSION_ID="):
                        version = line.split("=")[1].strip().strip('"')
        except Exception:
            pass

    # Try older methods
    if dist_id == "unknown":
        if os.path.exists("/etc/redhat-release"):
            dist_id = "centos"
            try:
                with open("/etc/redhat-release", "r") as f:
                    content = f.read().lower()
                    if "rocky" in content:
                        dist_id = "rocky"
                    elif "alma" in content:
                        dist_id = "almalinux"
                    elif "rhel" in content:
                        dist_id = "rhel"
                    elif "fedora" in content:
                        dist_id = "fedora"
            except Exception:
                pass
        elif os.path.exists("/etc/debian_version"):
            dist_id = "debian"
            try:
                with open("/etc/debian_version", "r") as f:
                    version = f.read().strip()
            except Exception:
                pass
        elif os.path.exists("/etc/lsb-release"):
            try:
                with open("/etc/lsb-release", "r") as f:
                    lines = f.readlines()
                    for line in lines:
                        if line.startswith("DISTRIB_ID="):
                            dist_id = line.split("=")[1].strip().lower().strip('"')
                        elif line.startswith("DISTRIB_RELEASE="):
                            version = line.split("=")[1].strip().strip('"')
            except Exception:
                pass

    # Determine family
    if dist_id in ["centos", "rhel", "rocky", "almalinux", "fedora"]:
        family = "redhat"
    elif dist_id in ["ubuntu", "debian"]:
        family = "debian"
    else:
        family = dist_id

    return {
        "id": dist_id,
        "family": family,
        "version": version,
        "full_version": version,
    }


def check_supported_distro(dist_info):
    """Check if distribution is supported"""
    dist_id = dist_info["id"]
    version = dist_info["version"]

    if dist_id in SUPPORTED_DISTROS:
        if version in SUPPORTED_DISTROS[dist_id]:
            return True
        else:
            # Check if any supported version starts with the same major version
            for supported_version in SUPPORTED_DISTROS[dist_id]:
                if supported_version.startswith(version.split(".")[0]):
                    return True

    # If not in list but is a known family, we'll try anyway
    if dist_info["family"] in ["debian", "redhat"]:
        return True

    return False


def run_command(cmd, shell=True, capture_output=False):
    """Run shell command with error handling"""
    try:
        if capture_output:
            result = subprocess.run(cmd, shell=shell, capture_output=True, text=True)
            return result.returncode, result.stdout, result.stderr
        else:
            result = subprocess.run(cmd, shell=shell)
            return result.returncode, None, None
    except Exception as e:
        return 1, None, str(e)


def generate_self_signed_cert():
    """Generate a fresh, per-install self-signed TLS certificate for nginx.

    The archive ships a placeholder bin/nginx/conf/server.{crt,key}; reusing it
    would mean every install shares the same private key. Here we overwrite it
    with a freshly generated unique key/cert BEFORE nginx ever starts. Certbot
    later replaces this with a real Let's Encrypt certificate (CertbotCronJob),
    but until then nginx serves this self-signed pair.
    """
    conf_dir = "/home/xc_vm/bin/nginx/conf"
    key_path = conf_dir + "/server.key"
    crt_path = conf_dir + "/server.crt"
    common_name = socket.gethostname() or "xc_vm"

    printc("Generating a unique self-signed TLS certificate...", col.OKBLUE)
    rc, _, err = run_command(
        "openssl req -x509 -newkey rsa:2048 -nodes "
        '-keyout "' + key_path + '" -out "' + crt_path + '" '
        '-days 3650 -subj "/CN=' + common_name + '"',
        capture_output=True,
    )
    if rc != 0 or not os.path.exists(key_path) or not os.path.exists(crt_path):
        printc("Failed to generate self-signed certificate: " + str(err), col.FAIL)
        sys.exit(1)

    run_command('chown xc_vm:xc_vm "' + key_path + '" "' + crt_path + '"')
    run_command('chmod 640 "' + key_path + '"')
    run_command('chmod 644 "' + crt_path + '"')
    printc("Self-signed certificate generated.", col.OKGREEN)


def _safe_tar_members(tar, dest):
    """Filter tar members to prevent path traversal attacks (CVE-2007-4559)"""
    dest = os.path.realpath(dest)
    for member in tar.getmembers():
        member_path = os.path.realpath(os.path.join(dest, member.name))
        if not member_path.startswith(dest + os.sep) and member_path != dest:
            printc(f"Skipping unsafe tar member: {member.name}", col.WARNING)
            continue
        yield member


def is_valid_zip(file_path):
    """Check if a file is a valid ZIP archive"""
    try:
        with zipfile.ZipFile(file_path, "r") as zip_ref:
            # Try to list contents
            zip_ref.namelist()
            return True
    except Exception:
        return False


def is_valid_tar(file_path):
    """Check if a file is a valid TAR archive"""
    try:
        with tarfile.open(file_path, "r:*") as tar_ref:
            # Try to list contents
            tar_ref.getmembers()
            return True
    except Exception:
        return False


def download_xc_vm():
    """Download XC_VM from GitHub releases"""
    printc("Checking for XC_VM installation files...", col.OKBLUE)

    # Check if valid files already exist locally
    if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"):
        printc("Valid xc_vm.tar.gz found locally", col.OKGREEN)
        return True

    if os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"):
        printc("Valid XC_VM.zip found locally", col.OKGREEN)
        return True

    printc("Not found. Trying to download from GitHub...", col.OKBLUE)

    try:
        # 1. Get latest version from GitHub API
        printc("Getting latest version from GitHub API...", col.OKBLUE)
        api_url = "https://api.github.com/repos/Vateron-Media/XC_VM/releases/latest"
        req = urllib.request.Request(api_url)
        req.add_header("User-Agent", "XC_VM-Installer/1.0")

        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode())
            latest_version = data["tag_name"]
            printc(f"Latest version: {latest_version}", col.OKGREEN)

        # 2. Download XC_VM.zip directly from GitHub releases
        download_url = f"https://github.com/Vateron-Media/XC_VM/releases/download/{latest_version}/XC_VM.zip"
        printc(f"Downloading: {download_url}", col.OKBLUE)

        # Download using urllib
        urllib.request.urlretrieve(download_url, "XC_VM.zip")

        # Verify download
        if os.path.exists("XC_VM.zip") and os.path.getsize("XC_VM.zip") > 0:
            file_size = os.path.getsize("XC_VM.zip")
            printc(f"Download successful: XC_VM.zip ({file_size} bytes)", col.OKGREEN)

            # MD5 verification
            expected_hash = download_release_hash("XC_VM", latest_version, "XC_VM.zip")
            if expected_hash:
                actual_hash = compute_md5("XC_VM.zip")
                if actual_hash != expected_hash:
                    printc(f"MD5 verification failed for XC_VM.zip: expected {expected_hash}, got {actual_hash}", col.FAIL)
                    os.remove("XC_VM.zip")
                    return False
                printc("MD5 verification passed for XC_VM.zip", col.OKGREEN)
            else:
                printc("Warning: Could not retrieve MD5 hash for XC_VM.zip, skipping verification", col.WARNING)

            # Validate ZIP file
            if is_valid_zip("XC_VM.zip"):
                printc("ZIP archive validated successfully", col.OKGREEN)
                return True
            else:
                printc("Downloaded file is not a valid ZIP archive", col.WARNING)
                os.remove("XC_VM.zip")
                return False
        else:
            printc("Download failed or file is empty", col.FAIL)
            return False

    except Exception as e:
        printc(f"Download error: {e}", col.FAIL)

        # Try alternative methods if download fails
        printc("Trying alternative download methods...", col.WARNING)

        # Alternative method: Direct download from latest
        try:
            alt_url = "https://github.com/Vateron-Media/XC_VM/releases/latest/download/XC_VM.zip"
            printc(f"Trying alternative: {alt_url}", col.OKBLUE)
            urllib.request.urlretrieve(alt_url, "XC_VM.zip")

            if os.path.exists("XC_VM.zip") and is_valid_zip("XC_VM.zip"):
                printc("Alternative download successful", col.OKGREEN)
                return True
        except Exception:
            pass

        return False


def install_mariadb_repo(dist_info):
    """Install MariaDB repository based on distribution"""
    dist_id = dist_info["id"]
    version = dist_info["version"]
    family = dist_info["family"]

    printc(f"Configuring MariaDB repository for {dist_id} {version}", col.OKBLUE)

    if family == "debian":
        # Debian/Ubuntu
        if dist_id == "ubuntu":
            # Try to get codename from /etc/os-release
            codename = "jammy"  # Default for Ubuntu 22.04
            try:
                with open("/etc/os-release", "r") as f:
                    for line in f:
                        if line.startswith("UBUNTU_CODENAME="):
                            codename = line.split("=")[1].strip().strip('"')
                            break
                        elif line.startswith("VERSION_CODENAME="):
                            codename = line.split("=")[1].strip().strip('"')
                            break
            except Exception:
                # Fallback based on version
                if version.startswith("20"):
                    codename = "focal"
                elif version.startswith("22"):
                    codename = "jammy"
                elif version.startswith("24"):
                    codename = "noble"

            printc(f"Using Ubuntu codename: {codename}", col.OKGREEN)

        # Install prerequisites
        run_command(
            "apt-get install -y apt-transport-https curl gnupg software-properties-common"
        )

        # Special handling for Ubuntu 20.04 LTS (EOL)
        if dist_id == "ubuntu" and version.startswith("20"):
            printc(
                "Ubuntu 20.04 LTS detected, using system MariaDB packages...",
                col.WARNING,
            )
            printc(
                "MariaDB 11.4 is not compatible with Ubuntu 20.04 (libc6 incompatibility)",
                col.OKBLUE,
            )
            printc(
                "Using Ubuntu 20.04 default MariaDB packages for compatibility",
                col.OKGREEN,
            )

            # For Ubuntu 20.04, we'll use the system MariaDB packages
            # Ubuntu 20.04 default repositories have MariaDB 10.3 which is compatible
            try:
                # Remove any existing MariaDB repository files
                if os.path.exists("/etc/apt/sources.list.d/mariadb.list"):
                    os.remove("/etc/apt/sources.list.d/mariadb.list")
                    printc("Removed incompatible MariaDB repository", col.OKBLUE)

                if os.path.exists("/usr/share/keyrings/mariadb.gpg"):
                    os.remove("/usr/share/keyrings/mariadb.gpg")

                printc(
                    "Will use Ubuntu 20.04 default MariaDB packages (10.3.x)",
                    col.OKGREEN,
                )
                # Skip external repository setup for Ubuntu 20.04

            except Exception as e:
                printc(f"Error cleaning up MariaDB repositories: {e}", col.WARNING)
                printc("Continuing with system packages...", col.WARNING)

        else:
            # For other versions, use the official script
            printc(
                "Adding MariaDB repository using official setup script...", col.OKBLUE
            )
            ret, _, _ = run_command(
                "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version='mariadb-11.4'",
                capture_output=True,
            )
            if ret != 0:
                printc(
                    "MariaDB repo setup script failed, will use system packages",
                    col.WARNING,
                )
                # Clean up any partial repo configuration
                run_command(
                    "rm -f /etc/apt/sources.list.d/mariadb.list "
                    "/etc/apt/sources.list.d/mariadb.sources || true"
                )

        # Add MaxMind repository for Ubuntu (with error handling)
        if dist_id == "ubuntu":
            printc("Adding MaxMind repository for GeoIP...", col.OKBLUE)
            try:
                run_command("add-apt-repository -y ppa:maxmind/ppa")
                printc("MaxMind repository added successfully", col.OKGREEN)
            except Exception as e:
                printc(f"MaxMind PPA failed to add: {e}", col.WARNING)
                printc(
                    "Continuing without MaxMind PPA (not critical for functionality)",
                    col.OKBLUE,
                )
        else:
            printc("Skipping MaxMind PPA (not available for Debian)", col.OKBLUE)

        # Update package list
        printc("Updating package list...", col.OKBLUE)
        run_command("apt-get update")

    elif family == "redhat":
        # RedHat based distributions
        run_command("yum install -y curl")

        # Install MariaDB repository using official script
        printc("Adding MariaDB repository...", col.OKBLUE)
        run_command(
            "curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | bash -s -- --mariadb-server-version='mariadb-11.4'"
        )

    else:
        printc(f"Unsupported distribution: {dist_id}", col.WARNING)
        return False

    return True


def secure_mariadb_installation(root_password, dist_info=None):
    """Secure MariaDB installation with root password (from bash script)"""
    printc("Securing MariaDB installation", col.OKBLUE)

    # Check if MariaDB is running
    ret, out, err = run_command("systemctl is-active mariadb", capture_output=True)

    if ret != 0:
        printc("Starting MariaDB service", col.OKBLUE)
        run_command("systemctl start mariadb")
        time.sleep(5)

    # Get MariaDB version to determine syntax
    printc("Checking MariaDB version...", col.OKBLUE)
    version_cmd = "mariadb --version 2>/dev/null | head -n 1 || mysql --version 2>/dev/null | head -n 1"
    ret, out, err = run_command(version_cmd, capture_output=True)

    mariadb_version = out.strip() if out else ""
    is_mariadb_103 = ("10.3" in mariadb_version or "5.7" in mariadb_version or "5.6" in mariadb_version)

    if is_mariadb_103:
        printc("MariaDB 10.3 detected, using legacy password syntax", col.OKBLUE)
    else:
        printc("MariaDB 11.x detected, using modern password syntax", col.OKBLUE)

    # Determine authentication plugin
    printc("Checking MariaDB authentication plugin...", col.OKBLUE)
    auth_cmd = "mariadb -u root -e \"SELECT plugin FROM mysql.user WHERE User='root' AND Host='localhost';\" 2>/dev/null | tail -n +2"
    ret, out, err = run_command(auth_cmd, capture_output=True)

    auth_plugin = out.strip() if out else ""

    if ret == 0 and auth_plugin == "unix_socket":
        printc(
            "Using unix_socket authentication, converting to password...", col.OKBLUE
        )
        # Convert from unix_socket to password authentication with version-specific syntax
        if is_mariadb_103:
            # MariaDB 10.3 and older syntax
            sql_commands = [
                "FLUSH PRIVILEGES;",
                f"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{root_password}');",
                "DELETE FROM mysql.user WHERE User='';",
                "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');",
                "DROP DATABASE IF EXISTS test;",
                "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\\\_%';",
                "FLUSH PRIVILEGES;",
            ]
        else:
            # MariaDB 10.4+ and MariaDB 11.x syntax
            sql_commands = [
                "FLUSH PRIVILEGES;",
                f"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING PASSWORD('{root_password}');",
                "DELETE FROM mysql.user WHERE User='';",
                "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');",
                "DROP DATABASE IF EXISTS test;",
                "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\\\_%';",
                "FLUSH PRIVILEGES;",
            ]

        for sql in sql_commands:
            if is_mariadb_103 and "SET PASSWORD" in sql:
                # Special handling for SET PASSWORD command
                run_command(f'mariadb -u root -e "{sql}"', shell=True)
            else:
                run_command(f'mariadb -u root -e "{sql}"')

        printc("MariaDB secured with password authentication", col.OKGREEN)
    else:
        printc("Setting MariaDB root password...", col.OKBLUE)
        # Try to set password with version-specific syntax
        if is_mariadb_103:
            # MariaDB 10.3 syntax
            set_cmd = f"mariadb -u root -e \"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('{root_password}');\" 2>/dev/null || true"
        else:
            # MariaDB 11.x syntax
            set_cmd = f"mariadb -u root -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '{root_password}';\" 2>/dev/null || true"
        run_command(set_cmd)

    # Create custom security configuration (like 99-custom.cnf from bash script)
    printc("Creating custom MariaDB security configuration...", col.OKBLUE)
    custom_conf = """[mysqld]
bind-address = 0.0.0.0
skip-name-resolve
local-infile = 0
symbolic-links = 0
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 2
log_error = /var/log/mysql/error.log"""

    conf_dir = "/etc/mysql/mariadb.conf.d/"
    if not os.path.exists(conf_dir):
        conf_dir = "/etc/my.cnf.d/"
        if not os.path.exists(conf_dir):
            os.makedirs(conf_dir, exist_ok=True)

    custom_path = os.path.join(conf_dir, "99-custom.cnf")
    with open(custom_path, "w") as f:
        f.write(custom_conf)

    # Create log directory and set permissions
    run_command("mkdir -p /var/log/mysql && chown mysql:mysql /var/log/mysql")

    # Restart MariaDB
    run_command("systemctl restart mariadb")
    time.sleep(3)

    # Don't create /root/mariadb_root_password.txt, only use /root/credentials.txt
    printc("MariaDB security hardening completed", col.OKGREEN)
    return root_password


def get_system_ram_mb():
    """Get total system RAM in MB"""
    try:
        with open("/proc/meminfo", "r") as f:
            for line in f:
                if line.startswith("MemTotal:"):
                    mem_kb = int(line.split()[1])
                    return mem_kb // 1024  # Convert to MB
    except Exception:
        pass
    return 1024  # Value by default if unable to determine


def generate_mysql_config(total_ram_mb):
    """Generate MySQL configuration based on total RAM (from bash script logic)"""

    # Calculate based on RAM (similar to bash script)
    buffer_pool_mb = int(total_ram_mb * 0.25)

    if total_ram_mb < 512:
        buffer_pool_mb = 64
        max_connections = 40
    elif total_ram_mb < 1024:
        if buffer_pool_mb > 128:
            buffer_pool_mb = 128
        max_connections = 80
    elif total_ram_mb < 2048:
        if buffer_pool_mb > 256:
            buffer_pool_mb = 256
        max_connections = 120
    elif total_ram_mb < 4096:
        if buffer_pool_mb > 512:
            buffer_pool_mb = 512
        max_connections = 200
    elif total_ram_mb < 8192:
        if buffer_pool_mb > 1024:
            buffer_pool_mb = 1024
        max_connections = 300
    elif total_ram_mb < 16384:
        if buffer_pool_mb > 2048:
            buffer_pool_mb = 2048
        max_connections = 450
    else:
        if buffer_pool_mb > 4096:
            buffer_pool_mb = 4096
        max_connections = 600

    # Format buffer pool size
    if buffer_pool_mb >= 1024:
        buffer_pool_size = f"{buffer_pool_mb // 1024}G"
    else:
        buffer_pool_size = f"{buffer_pool_mb}M"

    # Calculate other values
    key_buffer = min(buffer_pool_mb // 8, 32)
    tmp_table_size = min(buffer_pool_mb // 4, 64)
    back_log = min(max(max_connections // 2, 128), 1024)
    thread_cache = min(max(max_connections // 4, 32), 256)
    thread_pool_max_threads = min(max(max_connections, 256), 1024)
    buffer_pool_instances = "1" if buffer_pool_mb < 1024 else "2"

    # Generate config from template
    config = rMySQLCnfTemplate
    config = config.replace("{{KEY_BUFFER}}", str(key_buffer))
    config = config.replace("{{MAX_CONNECTIONS}}", str(max_connections))
    config = config.replace("{{BACK_LOG}}", str(back_log))
    config = config.replace("{{TMP_TABLE_SIZE}}", str(tmp_table_size))
    config = config.replace("{{BUFFER_POOL_SIZE}}", buffer_pool_size)
    config = config.replace("{{BUFFER_POOL_INSTANCES}}", buffer_pool_instances)
    config = config.replace("{{THREAD_CACHE}}", str(thread_cache))
    config = config.replace("{{THREAD_POOL_MAX_THREADS}}", str(thread_pool_max_threads))

    printc(f"RAM detected: {total_ram_mb}MB", col.OKGREEN)
    printc(
        f"Buffer pool configured: {buffer_pool_size} ({buffer_pool_mb}MB)", col.OKGREEN
    )
    printc(f"Max connections: {max_connections}", col.OKGREEN)
    printc(f"Thread pool max threads: {thread_pool_max_threads}", col.OKGREEN)

    return config


def generate_random_password(length=32):
    """Generate random password (similar to bash script)"""
    chars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
    return "".join(random.choice(chars) for _ in range(length))


def generate_root_password():
    """Generate secure root password of 20 characters (SQL/shell safe)"""
    # Only use characters safe for SQL literals and shell quoting
    chars = "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
    return "".join(random.choice(chars) for _ in range(20))


def sanitize_password_for_sql(password):
    """Escape single quotes for safe use in SQL strings"""
    return password.replace("'", "''")


def sanitize_password_for_shell(password):
    """Escape password for safe use in shell double-quoted strings"""
    # Escape characters special to bash inside double quotes
    for ch in ("\\", '"', "$", "`", "!"):
        password = password.replace(ch, "\\" + ch)
    return password


def getIP():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        # Fallback method
        try:
            hostname = socket.gethostname()
            ip = socket.gethostbyname(hostname)
            if ip and ip != "127.0.0.1":
                return ip
        except Exception:
            pass
        return "127.0.0.1"


def printc(rText, rColour=col.OKBLUE, rPadding=0):
    rLeft = int(30 - (len(rText) / 2))
    rRight = 60 - rLeft - len(rText)
    print(
        "%s |--------------------------------------------------------------| %s"
        % (rColour, col.ENDC)
    )
    for i in range(rPadding):
        print(
            "%s |                                                              | %s"
            % (rColour, col.ENDC)
        )
    print("%s | %s%s%s | %s" % (rColour, " " * rLeft, rText, " " * rRight, col.ENDC))
    for i in range(rPadding):
        print(
            "%s |                                                              | %s"
            % (rColour, col.ENDC)
        )
    print(
        "%s |--------------------------------------------------------------| %s"
        % (rColour, col.ENDC)
    )
    print(" ")


def extract_archive(archive_path):
    """Extract archive with proper validation and error handling"""
    printc(f"Extracting {archive_path}...", col.OKBLUE)

    if archive_path.endswith(".tar.gz") or archive_path.endswith(".tgz"):
        try:
            with tarfile.open(archive_path, "r:gz") as tar:
                # Get member list for debugging
                members = tar.getmembers()
                printc(
                    f"Archive contains {len(members)} files/directories", col.OKGREEN
                )

                # Extract with path traversal protection
                safe = list(_safe_tar_members(tar, "/home/xc_vm/"))
                tar.extractall(path="/home/xc_vm/", members=safe)
                printc("Extraction successful", col.OKGREEN)
                return True
        except Exception as e:
            printc(f"Failed to extract tar.gz: {e}", col.FAIL)
            return False

    elif archive_path.endswith(".zip"):
        try:
            with zipfile.ZipFile(archive_path, "r") as zip_ref:
                # Get file list for debugging
                file_list = zip_ref.namelist()
                printc(f"Archive contains {len(file_list)} files", col.OKGREEN)

                # Extract all files
                zip_ref.extractall(path="/home/xc_vm/")
                printc("Extraction successful", col.OKGREEN)

                # Check if zip contains nested tar.gz
                for file in file_list:
                    if file.endswith("xc_vm.tar.gz") or file.endswith(".tar.gz"):
                        nested_path = os.path.join("/home/xc_vm", file)
                        if os.path.exists(nested_path):
                            printc(
                                f"Found nested archive: {file}, extracting...",
                                col.OKBLUE,
                            )
                            return extract_archive(nested_path)
                return True
        except Exception as e:
            printc(f"Failed to extract zip: {e}", col.FAIL)
            return False

    else:
        printc(f"Unsupported archive format: {archive_path}", col.FAIL)
        return False


def fix_ssh2_library_issue():
    """Fix SSH2 library issue by creating proper symlinks"""
    printc("Configuring SSH2 libraries for PHP", col.OKBLUE)

    # List of possible libssh2.so.1 locations
    libssh2_paths = [
        "/usr/lib/x86_64-linux-gnu/libssh2.so.1",
        "/usr/lib/x86_64-linux-gnu/libssh2.so.1.0.1",
        "/usr/lib/x86_64-linux-gnu/libssh2.so",
        "/usr/lib64/libssh2.so.1",
        "/usr/lib/libssh2.so.1",
        "/usr/local/lib/libssh2.so.1",
    ]

    found_lib = None
    for lib_path in libssh2_paths:
        if os.path.exists(lib_path):
            found_lib = lib_path
            printc(f"Found SSH2 library: {lib_path}", col.OKGREEN)
            break

    # If not found in static list, search dynamically (handles t64 suffix on 24.04+)
    if not found_lib:
        try:
            ret, out, err = run_command(
                "find /usr/lib -name 'libssh2.so*' -type f -o -name 'libssh2.so*' -type l 2>/dev/null | head -5",
                capture_output=True,
            )
            if ret == 0 and out and out.strip():
                found_lib = out.strip().split("\n")[0]
                printc(f"Found SSH2 library via search: {found_lib}", col.OKGREEN)
        except Exception:
            pass

    if found_lib:
        # Create symlinks in common locations
        symlink_targets = [
            "/usr/lib/libssh2.so.1",
            "/usr/local/lib/libssh2.so.1",
            "/lib/libssh2.so.1",
        ]

        for target in symlink_targets:
            if not os.path.exists(target):
                try:
                    run_command(f"ln -sf {found_lib} {target}")
                    printc(f"Created symlink: {found_lib} -> {target}", col.OKGREEN)
                except Exception:
                    printc(f"Failed to create symlink for {target}", col.WARNING)

        # Also check if we need to symlink in PHP extensions directory
        php_ext_dir = "/home/xc_vm/bin/php/lib/php/extensions/"
        if os.path.exists(php_ext_dir):
            # Find the actual extensions directory
            for dirpath, dirnames, filenames in os.walk(php_ext_dir):
                if "ssh2.so" in filenames:
                    ssh2_so_path = os.path.join(dirpath, "ssh2.so")
                    printc(f"Found PHP ssh2.so at: {ssh2_so_path}", col.OKGREEN)
                    break

        # Update dynamic linker cache
        run_command("ldconfig 2>/dev/null || true")
    else:
        printc(
            "Warning: libssh2.so.1 not found. SSH2 may not work properly.", col.WARNING
        )

    # Don't disable ssh2 extension - only create symlinks


# Mapping: (dist_id, major_version) -> PACKAGES key
_PACKAGE_KEY_MAP = {
    ("ubuntu", "18"): "ubuntu20",
    ("ubuntu", "20"): "ubuntu20",
    ("ubuntu", "22"): "ubuntu22",
    ("ubuntu", "24"): "ubuntu24",
    ("debian", "11"): "debian11",
    ("debian", "12"): "debian",
    ("debian", "13"): "debian13",
}

# Distros that need OpenSSL 3 compatibility library
_NEEDS_OPENSSL3 = {"ubuntu_18", "ubuntu_20", "debian_11"}


def _install_openssl3_compat(dist_id, major):
    """Download and install OpenSSL 3 compatibility .deb for older distros."""
    label = f"{dist_id}_{major}"
    tmp_file = f"/tmp/libssl3_{label}.deb"
    printc("Installing OpenSSL 3 compatibility library for PHP binaries...", col.OKBLUE)
    try:
        run_command(
            f"wget -qO {tmp_file} "
            '"http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl3_3.0.2-0ubuntu1_amd64.deb"'
        )
        run_command(
            f"dpkg --force-depends -i {tmp_file} 2>/dev/null "
            f"|| dpkg -i {tmp_file} 2>/dev/null || true"
        )
        run_command(f"rm -f {tmp_file}")
        printc("OpenSSL 3 library installation completed", col.OKGREEN)
    except Exception as e:
        printc(f"OpenSSL 3 installation warning: {e}", col.WARNING)
        printc("PHP binaries may not work without libssl.so.3", col.WARNING)


def _apt_install_framed(packages):
    """Install packages one at a time, each under its own frame.

    For every package a printc frame names what is being installed, and apt's raw
    log streams straight to the terminal below that frame (no capture_output), so
    the operator sees which package is installing and its full output. Returns the
    list of packages whose install returned a non-zero code.
    """
    failed = []
    total = len(packages)
    for idx, pkg in enumerate(packages, 1):
        printc(f"Installing package {idx}/{total}: {pkg}", col.OKBLUE)
        # No capture_output → apt's log is shown directly under the frame.
        ret, _, _ = run_command(
            f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {pkg}"
        )
        if ret != 0:
            printc(f"Package failed: {pkg}", col.WARNING)
            failed.append(pkg)
    return failed


def install_deb_packages(dist_info):
    """Install Debian/Ubuntu packages based on detected distribution."""
    dist_id = dist_info["id"]
    version = dist_info["version"]
    major = version.split(".")[0]
    label = f"{dist_id.capitalize()} {version}"

    package_key = _PACKAGE_KEY_MAP.get((dist_id, major), "debian")
    printc(f"Using {label} compatible package installation", col.OKBLUE)

    # Fix broken packages first
    printc("Fixing any broken packages...", col.OKBLUE)
    run_command("apt --fix-broken install -y || true")
    run_command("apt-get autoremove -y || true")
    run_command("apt-get autoclean || true")

    # Install OpenSSL 3 compat for older distros
    ssl3_key = f"{dist_id}_{major}"
    if ssl3_key in _NEEDS_OPENSSL3:
        _install_openssl3_compat(dist_id, major)

    # Install packages
    packages = PACKAGES.get(package_key, [])
    if packages:
        # Separate MariaDB packages from the rest to avoid atomic apt-get failure
        _mariadb_pkgs = {"mariadb-server", "mariadb-client", "mariadb-common"}
        db_packages = [p for p in packages if p in _mariadb_pkgs]
        other_packages = [p for p in packages if p not in _mariadb_pkgs]

        # Install non-DB packages one by one: a frame per package, log shown below.
        if other_packages:
            printc(
                f"Installing {label} system packages ({len(other_packages)})...",
                col.OKBLUE,
            )
            failed = _apt_install_framed(other_packages)
            if failed:
                printc(
                    "These packages failed to install: " + ", ".join(failed),
                    col.WARNING,
                )

        # Install MariaDB packages separately with fallback
        if db_packages:
            db_str = " ".join(db_packages)
            printc(
                f"Installing MariaDB packages: {db_str}", col.OKBLUE
            )
            # No capture_output → apt's log is shown directly under the frame.
            ret, _, _ = run_command(
                f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {db_str}"
            )
            if ret != 0:
                printc(
                    "MariaDB from configured repo failed, trying system packages...",
                    col.WARNING,
                )
                # Remove broken MariaDB repo and retry with system packages
                run_command(
                    "rm -f /etc/apt/sources.list.d/mariadb.list "
                    "/etc/apt/sources.list.d/mariadb.sources || true"
                )
                run_command("apt-get update -y")
                ret2, _, _ = run_command(
                    f"DEBIAN_FRONTEND=noninteractive apt-get -yq install {db_str}",
                    capture_output=True,
                )
                if ret2 != 0:
                    printc(
                        "CRITICAL: MariaDB packages could not be installed!",
                        col.FAIL,
                    )

    # For unknown distros, also try installing SSH2 libraries
    if (dist_id, major) not in _PACKAGE_KEY_MAP:
        printc("Installing SSH2 libraries...", col.OKBLUE)
        run_command(
            "apt-get install -y libssh2-1 libssh2-1-dev "
            "|| apt-get install -y libssh2-1 libssh2-1t64 || true"
        )

    # Fix broken packages after installation
    printc("Final fix for any remaining broken packages...", col.OKBLUE)
    run_command("apt --fix-broken install -y || true")


if __name__ == "__main__":
    ##################################################
    # START                                          #
    ##################################################

    printc("XC_VM Multi-Distribution Installer", col.OKGREEN, 2)

    # Check root
    check_root()

    # Detect distribution
    dist_info = detect_distribution()
    printc(
        f"Detected: {dist_info['id']} {dist_info['version']} ({dist_info['family']} family)",
        col.OKGREEN,
    )

    # Install prerequisites before continuing
    install_prerequisites(dist_info)

    if not check_supported_distro(dist_info):
        printc(
            f"Warning: {dist_info['id']} {dist_info['version']} is not officially supported",
            col.WARNING,
        )
        response = input("Continue anyway? (Y/N): ").strip().upper()
        if response != "Y":
            sys.exit(1)
        printc("Continuing with installation...", col.WARNING)

    # Try to download XC_VM if not present
    has_valid_archive = False
    archive_path = None

    # Check for existing valid archives
    if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"):
        has_valid_archive = True
        archive_path = "./xc_vm.tar.gz"
        printc("Found valid xc_vm.tar.gz", col.OKGREEN)
    elif os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"):
        has_valid_archive = True
        archive_path = "./XC_VM.zip"
        printc("Found valid XC_VM.zip", col.OKGREEN)

    # Download if needed
    if not has_valid_archive:
        if download_xc_vm():
            # Check what was downloaded
            if os.path.exists("./xc_vm.tar.gz") and is_valid_tar("./xc_vm.tar.gz"):
                has_valid_archive = True
                archive_path = "./xc_vm.tar.gz"
            elif os.path.exists("./XC_VM.zip") and is_valid_zip("./XC_VM.zip"):
                has_valid_archive = True
                archive_path = "./XC_VM.zip"

    if not has_valid_archive or not archive_path:
        printc(
            "XC_VM package not found or invalid. Please download manually.", col.FAIL
        )
        printc(
            "You can download from: https://github.com/Vateron-Media/XC_VM/releases",
            col.OKBLUE,
        )
        sys.exit(1)

    rHost = "127.0.0.1"
    rServerID = 1
    rUsername = generate_random_password(32)  # Username de 32 caracteres
    rPassword = generate_random_password(32)  # Password de 32 caracteres
    rDatabase = "xc_vm"
    rPort = 3306

    # Ask for MariaDB root password (or generate)
    printc("MariaDB Root Password Configuration", col.OKBLUE)
    print("For security, you should set a strong root password for MariaDB.")
    print("Leave empty to generate a random password.")

    root_password = input(
        "MariaDB root password (or press Enter to generate): "
    ).strip()
    if not root_password:
        root_password = generate_root_password()
        printc(f"Generated root password: {root_password}", col.OKGREEN)
    else:
        # Reject characters that break SQL/shell quoting
        forbidden = set("'\"`$;")
        if forbidden & set(root_password):
            printc(
                "Password contains unsafe characters (' \" ` $ ;). Generating a safe one.",
                col.WARNING,
            )
            root_password = generate_root_password()
            printc(f"Generated root password: {root_password}", col.OKGREEN)
        elif len(root_password) < 12:
            printc(
                f"Warning: Password is only {len(root_password)} chars. Minimum recommended: 12.",
                col.WARNING,
            )
            response = input("Continue with provided password? (Y/N): ").strip().upper()
            if response != "Y":
                root_password = generate_root_password()
                printc(f"Using generated root password: {root_password}", col.OKGREEN)
        else:
            printc("Using provided root password", col.OKGREEN)

    if os.path.exists("/home/xc_vm/"):
        printc("XC_VM Directory Exists!", col.WARNING)
        while True:
            rAnswer = input("Continue and overwrite? (Y / N) : ").strip().upper()
            if rAnswer in ["Y", "N"]:
                break
        if rAnswer == "N":
            sys.exit(1)

    ##################################################
    # SYSTEM PREPARATION                             #
    ##################################################

    printc("Preparing System", col.OKBLUE)

    if dist_info["family"] == "debian":
        # Debian/Ubuntu
        printc("Cleaning package locks", col.OKBLUE)
        for rFile in [
            "/var/lib/dpkg/lock-frontend",
            "/var/cache/apt/archives/lock",
            "/var/lib/dpkg/lock",
            "/var/lib/apt/lists/lock",
        ]:
            if os.path.exists(rFile):
                try:
                    os.remove(rFile)
                except Exception:
                    pass

        printc("Updating system", col.OKBLUE)
        run_command("apt-get update -y")

        # Install MariaDB repository
        if not install_mariadb_repo(dist_info):
            printc("Using system MariaDB repository", col.WARNING)

        # Stop conflicting services
        printc("Stopping conflicting services (Apache/System Nginx)...", col.OKBLUE)
        run_command("systemctl stop apache2 nginx 2>/dev/null || true")
        run_command("systemctl disable apache2 nginx 2>/dev/null || true")

        # Remove conflicting packages
        for rPackage in rRemove:
            printc(f"Removing {rPackage}", col.OKBLUE)
            run_command(f"apt-get remove {rPackage} -y")

        # Install packages
        printc("Installing system packages", col.OKBLUE)
        install_deb_packages(dist_info)

        # Verify MariaDB was actually installed
        ret, _, _ = run_command("dpkg -l mariadb-server 2>/dev/null | grep -q '^ii'", capture_output=True)
        if ret != 0:
            printc("MariaDB not installed after package step, retrying...", col.WARNING)
            run_command("apt-get update -y")
            ret2, _, _ = run_command(
                "DEBIAN_FRONTEND=noninteractive apt-get -yq install mariadb-server mariadb-client",
                capture_output=True,
            )
            if ret2 != 0:
                printc("CRITICAL: Could not install MariaDB!", col.FAIL)
                sys.exit(1)

        # Enable MariaDB
        run_command("systemctl enable mariadb")

        # Fix SSH2 library issue
        printc("Configuring SSH2 libraries", col.OKBLUE)
        fix_ssh2_library_issue()

    elif dist_info["family"] == "redhat":
        # RedHat based distributions
        printc("Configuring repositories", col.OKBLUE)

        # Install EPEL
        run_command("yum install -y epel-release")

        # Install MariaDB repository
        if not install_mariadb_repo(dist_info):
            printc("Using system MariaDB repository", col.WARNING)

        printc("Updating system", col.OKBLUE)
        run_command("yum update -y")

        # Install system packages from PACKAGES['redhat']
        redhat_packages = PACKAGES.get("redhat", [])
        if redhat_packages:
            packages_str = " ".join(redhat_packages)
            printc(
                f"Installing RedHat packages ({len(redhat_packages)} packages)...",
                col.OKBLUE,
            )
            run_command(
                f"yum install -y {packages_str} || echo 'Some packages may not be available'"
            )

        # Ensure libssh2 libraries are installed
        printc("Verifying SSH2 libraries for RedHat...", col.OKBLUE)
        run_command("yum install -y libssh2 libssh2-devel || true")

        # Enable services
        run_command("systemctl enable mariadb")
        run_command("systemctl enable crond")

        # Fix SSH2 library issue
        printc("Configuring SSH2 libraries", col.OKBLUE)
        fix_ssh2_library_issue()
    else:
        printc(f"Unsupported distribution family: {dist_info['family']}", col.FAIL)
        sys.exit(1)

    # Create user if doesn't exist
    printc("Creating/verifying xc_vm user", col.OKBLUE)
    try:
        ret, out, err = run_command("getent passwd xc_vm", capture_output=True)
        if ret == 0:
            printc("User xc_vm already exists", col.OKGREEN)
        else:
            raise Exception("User not found")
    except Exception:
        printc("Creating user xc_vm", col.OKBLUE)
        # capture_output=True keeps adduser/useradd's raw "info:" lines out of the
        # terminal so the framed printc messages stay clean and consistent.
        if dist_info["family"] == "debian":
            rc, _, err = run_command(
                "adduser --system --shell /bin/false --no-create-home --home /nonexistent --group --disabled-login xc_vm",
                capture_output=True,
            )
        else:  # redhat
            run_command("groupadd -r xc_vm", capture_output=True)
            rc, _, err = run_command(
                "useradd -r -g xc_vm -s /bin/false -M -d /nonexistent xc_vm",
                capture_output=True,
            )
        if rc == 0:
            printc("User xc_vm created", col.OKGREEN)
        else:
            printc("Failed to create user xc_vm: " + str(err), col.FAIL)
            sys.exit(1)

    if not os.path.exists("/home/xc_vm"):
        os.makedirs("/home/xc_vm", exist_ok=True)
        run_command("chown xc_vm:xc_vm /home/xc_vm")

    ##################################################
    # INSTALL XC_VM                                  #
    ##################################################

    printc("Installing XC_VM", col.OKBLUE)

    # Extract the archive
    if not extract_archive(archive_path):
        printc("Failed to extract archive! Exiting", col.FAIL)
        sys.exit(1)

    # Verify extraction
    if not os.path.exists("/home/xc_vm/console.php"):
        printc("Extraction failed: /home/xc_vm/console.php not found", col.FAIL)
        sys.exit(1)
    else:
        printc("XC_VM extracted successfully", col.OKGREEN)

    # Replace the shipped placeholder cert with a unique per-install one
    # (before nginx is started further below).
    generate_self_signed_cert()

    # Install distribution-specific binaries for supported distros
    dist_id = dist_info["id"]
    version = dist_info["version"]

    # Check if distribution has patches available
    if dist_id in ["ubuntu", "debian"]:
        # Ubuntu: 20, 22, 24
        if dist_id == "ubuntu" and any(
            version.startswith(v) for v in ["20", "22", "24"]
        ):
            if not install_distribution_binaries(dist_id, version):
                printc(
                    f"FATAL: failed to install {dist_id} {version} binaries — the panel "
                    f"cannot run without them. Check network/DNS and re-run the installer.",
                    col.FAIL,
                )
                sys.exit(1)

        # Debian: 11, 12, 13
        elif dist_id == "debian" and any(
            version.startswith(v) for v in ["11", "12", "13"]
        ):
            if not install_distribution_binaries(dist_id, version):
                printc(
                    f"FATAL: failed to install {dist_id} {version} binaries — the panel "
                    f"cannot run without them. Check network/DNS and re-run the installer.",
                    col.FAIL,
                )
                sys.exit(1)

        else:
            printc(
                f"No specific patches available for {dist_id} {version}, using default binaries",
                col.OKBLUE,
            )

    elif dist_id in ["rocky", "almalinux", "rhel", "centos"]:
        major = version.split(".")[0]
        if major in ["8", "9"]:
            if not install_distribution_binaries(dist_id, version):
                printc(
                    f"FATAL: failed to install {dist_id} {version} binaries — the panel "
                    f"cannot run without them. Check network/DNS and re-run the installer.",
                    col.FAIL,
                )
                sys.exit(1)
        else:
            printc(
                f"No specific patches available for {dist_id} {version}, using default binaries",
                col.OKBLUE,
            )
    else:
        printc(
            f"No patches available for {dist_id} {version}, using default binaries",
            col.OKBLUE,
        )

    ##################################################
    # MariaDB CONFIGURATION                          #
    ##################################################

    printc("Configuring MariaDB", col.OKBLUE)

    # Secure MariaDB installation (using bash script logic)
    secure_mariadb_installation(root_password, dist_info)

    # Get total system RAM and generate config
    total_ram_mb = get_system_ram_mb()
    rMySQLCnf = generate_mysql_config(total_ram_mb)

    # Write MySQL configuration
    printc("Writing MySQL performance configuration", col.OKBLUE)
    if dist_info["family"] == "debian":
        mysql_conf_path = "/etc/mysql/mariadb.conf.d/50-server.cnf"
    else:
        mysql_conf_path = "/etc/my.cnf.d/server.cnf"

    # Ensure directory exists
    os.makedirs(os.path.dirname(mysql_conf_path), exist_ok=True)

    with io.open(mysql_conf_path, "w", encoding="utf-8") as rFile:
        rFile.write(rMySQLCnf)

    # Restart MariaDB
    run_command("systemctl restart mariadb")
    time.sleep(5)

    # Connect to MariaDB and configure databases
    printc("Setting up databases and users", col.OKBLUE)

    # Create databases
    run_command(
        f'mariadb -u root -p"{root_password}" -e "CREATE DATABASE IF NOT EXISTS xc_vm; CREATE DATABASE IF NOT EXISTS xc_vm_migrate;"'
    )

    # Import database schema
    printc("Importing database schema", col.OKBLUE)
    db_schema_path = "/home/xc_vm/bin/install/database.sql"
    if os.path.exists(db_schema_path):
        run_command(f'mariadb -u root -p"{root_password}" xc_vm < "{db_schema_path}"')
    else:
        printc(f"Database schema not found at {db_schema_path}", col.WARNING)

    # Create XC_VM user with all privileges
    printc("Creating database user", col.OKBLUE)

    # Localhost grants
    commands_localhost = [
        f"CREATE USER IF NOT EXISTS '{rUsername}'@'localhost' IDENTIFIED BY '{rPassword}';",
        f"GRANT ALL PRIVILEGES ON xc_vm.* TO '{rUsername}'@'localhost';",
        f"GRANT ALL PRIVILEGES ON xc_vm_migrate.* TO '{rUsername}'@'localhost';",
        f"GRANT ALL PRIVILEGES ON mysql.* TO '{rUsername}'@'localhost';",
        f"GRANT GRANT OPTION ON xc_vm.* TO '{rUsername}'@'localhost';",
    ]

    # 127.0.0.1 grants (REQUIRED for startup.php)
    commands_127 = [
        f"CREATE USER IF NOT EXISTS '{rUsername}'@'127.0.0.1' IDENTIFIED BY '{rPassword}';",
        f"GRANT ALL PRIVILEGES ON xc_vm.* TO '{rUsername}'@'127.0.0.1';",
        f"GRANT ALL PRIVILEGES ON xc_vm_migrate.* TO '{rUsername}'@'127.0.0.1';",
        f"GRANT ALL PRIVILEGES ON mysql.* TO '{rUsername}'@'127.0.0.1';",
        f"GRANT GRANT OPTION ON xc_vm.* TO '{rUsername}'@'127.0.0.1';",
        "FLUSH PRIVILEGES;",
    ]

    all_commands = commands_localhost + commands_127

    for cmd in all_commands:
        run_command(f'mariadb -u root -p"{root_password}" -e "{cmd}"')

    # Write XC_VM configuration
    printc("Writing XC_VM configuration", col.OKBLUE)
    os.makedirs(os.path.dirname(rConfigPath), exist_ok=True)
    rConfigData = rConfig % (rUsername, rPassword)
    with io.open(rConfigPath, "w", encoding="utf-8") as rFile:
        rFile.write(rConfigData)

    printc("MariaDB configuration completed", col.OKGREEN)

    ##################################################
    # SYSTEM CONFIGURATION                           #
    ##################################################

    printc("Configuring System", col.OKBLUE)

    # Configure tmpfs mounts
    if not os.path.exists("/etc/fstab"):
        printc("/etc/fstab not found", col.WARNING)
    else:
        try:
            with open("/etc/fstab", "r") as f:
                fstab_content = f.read()

            if "/home/xc_vm/" not in fstab_content:
                printc("Adding tmpfs mounts to /etc/fstab", col.OKBLUE)
                # Create directories first
                run_command("mkdir -p /home/xc_vm/content/streams")
                run_command("mkdir -p /home/xc_vm/tmp")

                with io.open("/etc/fstab", "a", encoding="utf-8") as rFile:
                    rFile.write(
                        "\ntmpfs /home/xc_vm/content/streams tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=90% 0 0\ntmpfs /home/xc_vm/tmp tmpfs defaults,noatime,nosuid,nodev,noexec,mode=1777,size=6G 0 0"
                    )

                # Mount immediately
                run_command("mount -a")
                printc("Reloading systemd to recognize fstab changes", col.OKBLUE)
                run_command("systemctl daemon-reload")
        except Exception as e:
            printc(f"Error updating /etc/fstab: {e}", col.WARNING)

    # Remove any restrictive sudoers rules
    sudoers_file = "/etc/sudoers.d/xc_vm"
    if os.path.exists(sudoers_file):
        run_command(f"rm -f {sudoers_file}")

    # Configure HTTP/HTTPS ports
    printc("Port Configuration", col.OKBLUE)
    print(
        "If you want to change the ports, enter new values, or leave empty to use the default ports"
    )

    while True:
        http_port = input("HTTP port (default 80): ").strip()
        if not http_port:
            http_port = "80"
            break
        if http_port.isdigit() and 1 <= int(http_port) <= 65535:
            break
        printc("Error: port must be a number between 1 and 65535", col.FAIL)

    while True:
        https_port = input("HTTPS port (default 443): ").strip()
        if not https_port:
            https_port = "443"
            break
        if https_port.isdigit() and 1 <= int(https_port) <= 65535:
            break
        printc("Error: port must be a number between 1 and 65535", col.FAIL)

    # Write HTTP ports configuration
    http_conf_path = "/home/xc_vm/bin/nginx/conf/ports/http.conf"
    os.makedirs(os.path.dirname(http_conf_path), exist_ok=True)
    with io.open(http_conf_path, "w", encoding="utf-8") as rFile:
        rFile.write(f"listen {http_port};")

    # Write HTTPS ports configuration
    https_conf_path = "/home/xc_vm/bin/nginx/conf/ports/https.conf"
    os.makedirs(os.path.dirname(https_conf_path), exist_ok=True)
    with io.open(https_conf_path, "w", encoding="utf-8") as rFile:
        rFile.write(f"listen {https_port} ssl;")

    printc(f"Ports configured: HTTP - {http_port}, HTTPS - {https_port}", col.OKGREEN)

    # Update Main Server broadcast ports in database
    run_command(
        f'mariadb -u root -p"{root_password}" xc_vm -e '
        f'"UPDATE servers SET http_broadcast_port={int(http_port)}, https_broadcast_port={int(https_port)} WHERE is_main=1;"'
    )
    printc("Main Server broadcast ports updated in database", col.OKGREEN)

    # Configure sysctl
    print(
        "Custom sysctl.conf - If you have your own custom sysctl.conf, type N or it will be overwritten. If you don't know what a sysctl configuration is, type Y as it will correctly set your TCP settings and open file limits."
    )
    print(" ")
    while True:
        rAnswer = input("Overwrite sysctl configuration? Recommended! (Y / N): ")
        if rAnswer.upper() in ["Y", "N"]:
            break

    if rAnswer.upper() == "Y":
        try:
            run_command("modprobe ip_conntrack 2>/dev/null || true")
        except Exception:
            pass
        try:
            with io.open("/etc/sysctl.conf", "w", encoding="utf-8") as rFile:
                rFile.write(rSysCtl)
            run_command("sysctl -p > /dev/null 2>&1")
            with open("/home/xc_vm/config/sysctl.on", "w") as rFile:
                pass
        except Exception:
            printc("Failed to write to sysctl file.", col.WARNING)
    else:
        if os.path.exists("/home/xc_vm/config/sysctl.on"):
            os.remove("/home/xc_vm/config/sysctl.on")

    # Configure systemd limits
    printc("Configuring systemd file limits", col.OKBLUE)
    systemd_conf = "/etc/systemd/system.conf"
    if os.path.exists(systemd_conf):
        with open(systemd_conf, "r") as f:
            systemd_content = f.read()

        if "DefaultLimitNOFILE=655350" not in systemd_content:
            with open(systemd_conf, "a") as f:
                f.write("\nDefaultLimitNOFILE=655350\n")

    user_conf = "/etc/systemd/user.conf"
    if os.path.exists(user_conf):
        with open(user_conf, "r") as f:
            user_content = f.read()

        if "DefaultLimitNOFILE=655350" not in user_content:
            with open(user_conf, "a") as f:
                f.write("\nDefaultLimitNOFILE=655350\n")

    # Configure systemd service (always, like etalon)
    printc("Configuring systemd service", col.OKBLUE)
    if os.path.exists("/etc/init.d/xc_vm"):
        os.remove("/etc/init.d/xc_vm")
    if os.path.exists("/etc/systemd/system/xc_vm.service"):
        os.remove("/etc/systemd/system/xc_vm.service")
    service_path = "/etc/systemd/system/xc_vm.service"
    with io.open(service_path, "w", encoding="utf-8") as rFile:
        rFile.write(rSystemd)
    run_command("chmod +x /etc/systemd/system/xc_vm.service")
    run_command("systemctl daemon-reload")
    run_command("systemctl enable xc_vm")

    ##################################################
    # ACCESS CODE                                    #
    ##################################################

    printc("Generating access code", col.OKBLUE)
    rCodeDir = "/home/xc_vm/bin/nginx/conf/codes/"

    # Ensure access codes directory exists
    os.makedirs(rCodeDir, exist_ok=True)

    admin_code = None

    if os.path.exists(rCodeDir):
        for filename in os.listdir(rCodeDir):
            if filename.endswith(".conf"):
                filepath = os.path.join(rCodeDir, filename)
                if filename.split(".")[0] == "setup":
                    os.remove(filepath)
                else:
                    try:
                        with open(filepath, "r") as f:
                            content = f.read()
                            if "/home/xc_vm/admin" in content:
                                admin_code = filename.split(".")[0]
                                break
                    except Exception:
                        pass

    if not admin_code:
        admin_code = generate_random_password(8)
        printc(f"Generated access code: {admin_code}", col.OKGREEN)

        # Insert into database
        insert_cmd = f"mariadb -u root -p\"{root_password}\" -e \"USE xc_vm; INSERT INTO access_codes(code, type, enabled, groups) VALUES('{admin_code}', 0, 1, '[1]');\""
        run_command(insert_cmd)

        # Create nginx configuration
        template_path = os.path.join(rCodeDir, "template")
        if os.path.exists(template_path):
            with open(template_path, "r") as f:
                template_content = f.read()

            # Replace placeholders
            template_content = template_content.replace("#WHITELIST#", "")
            template_content = template_content.replace("#TYPE#", "admin")
            template_content = template_content.replace("#CODE#", admin_code)
            template_content = template_content.replace("#BURST#", "500")

            code_conf_path = os.path.join(rCodeDir, f"{admin_code}.conf")
            with io.open(code_conf_path, "w", encoding="utf-8") as rFile:
                rFile.write(template_content)
            printc(f"Access code configuration created: {admin_code}.conf", col.OKGREEN)
        else:
            printc("Template file not found, creating basic configuration", col.WARNING)
            # Fallback configuration
            fallback_config = f"location /{admin_code} {{ include /home/xc_vm/bin/nginx/conf/proxy.conf; proxy_pass http://127.0.0.1:8080/admin; }}"
            code_conf_path = os.path.join(rCodeDir, f"{admin_code}.conf")
            with io.open(code_conf_path, "w", encoding="utf-8") as rFile:
                rFile.write(fallback_config)
    else:
        printc(f"Using existing access code: {admin_code}", col.OKGREEN)

    ##################################################
    # FINAL CONFIGURATION                            #
    ##################################################

    printc("Finalizing installation", col.OKBLUE)

    # Set permissions
    run_command("chown -R xc_vm:xc_vm /home/xc_vm")

    # Set executable permissions on key files
    run_command("chmod +x /home/xc_vm/service")
    run_command("chmod +x /home/xc_vm/console.php")
    run_command("chmod +x /home/xc_vm/bin/nginx/sbin/nginx")

    # Check for RTMP nginx
    nginx_rtmp_path = "/home/xc_vm/bin/nginx_rtmp/sbin/nginx_rtmp"
    if os.path.exists(nginx_rtmp_path):
        run_command(f"chmod +x {nginx_rtmp_path}")

    # Set capabilities for binding to privileged ports
    nginx_bin = "/home/xc_vm/bin/nginx/sbin/nginx"
    if os.path.exists(nginx_bin):
        run_command(
            f"setcap 'cap_net_bind_service=+ep' {nginx_bin} 2>/dev/null || true"
        )

    if os.path.exists(nginx_rtmp_path):
        run_command(
            f"setcap 'cap_net_bind_service=+ep' {nginx_rtmp_path} 2>/dev/null || true"
        )

    # Save credentials in the format you requested
    printc("Saving credentials", col.OKBLUE)

    # Save to /root/credentials.txt
    with io.open("/root/credentials.txt", "w", encoding="utf-8") as rFile:
        rFile.write("MariaDB Root \n")
        rFile.write("Username: root\n")
        rFile.write(f"Password: {root_password}\n\n")
        rFile.write(f"XC_VM Username: {rUsername}\n")
        rFile.write(f"XC_VM Password: {rPassword}\n")
        rFile.write(f"Database: {rDatabase}\n")

    # Also save to installer directory (like original install script)
    local_creds_path = os.path.join(rPath, "credentials.txt")
    with io.open(local_creds_path, "w", encoding="utf-8") as rFile:
        rFile.write(f"MariaDB Root Password: {root_password}\n")
        rFile.write(f"MariaDB Username: {rUsername}\n")
        rFile.write(f"MariaDB Password: {rPassword}\n")
        rFile.write(f"Database: {rDatabase}\n")
        rFile.write(f"Admin Access Code: {admin_code}\n")

    # Remove the old mariadb_root_password.txt file if it exists
    if os.path.exists("/root/mariadb_root_password.txt"):
        os.remove("/root/mariadb_root_password.txt")

    printc("Credentials saved to /root/credentials.txt", col.OKGREEN)
    printc(f"Credentials also saved to {local_creds_path}", col.OKGREEN)

    # Mount tmpfs filesystems (like original install script)
    run_command("mount -a >/dev/null 2>&1 || true")

    # Reload systemd daemon
    run_command("systemctl daemon-reload")

    # Start service
    run_command("systemctl start xc_vm")

    # Post-install startup
    printc("Starting XC_VM processes...", col.OKBLUE)
    time.sleep(10)

    # Run status command (root by design: root crontab, system limits, DB
    # migrations — the command refuses to run as any other user).
    if os.path.exists("/home/xc_vm/console.php"):
        run_command("/home/xc_vm/bin/php/bin/php /home/xc_vm/console.php status 1")

    # Set config permissions
    run_command("chown -R xc_vm:xc_vm /home/xc_vm/config/")

    # Run startup command via console.php
    startup_cmd = "/home/xc_vm/console.php"
    if os.path.exists(startup_cmd):
        run_command(
            f"/home/xc_vm/bin/php/bin/php {startup_cmd} startup >/dev/null 2>&1"
        )

    # Download GeoLite2 GeoIP databases (City/Country/ASN). They are no longer
    # bundled in the repo/release archive, so fetch them from the XC_VM_Update
    # release on install. This also refreshes bin/maxmind/version.json.
    # Non-fatal: run_command never raises, so a download failure won't abort.
    if os.path.exists(startup_cmd):
        printc("Downloading GeoLite2 GeoIP databases...", col.OKBLUE)
        run_command(
            f"/home/xc_vm/bin/php/bin/php {startup_cmd} cron:maxmind --force"
        )

    # Download the proxy-node archive (proxy.tar.gz). Like GeoLite2 it is no longer
    # bundled in the release archive — fetch it from the XC_VM_Proxy release on
    # install so the first proxy-node install runs from a local copy, and write the
    # bin/install/proxy_version.json index. Runs as xc_vm (owner of bin/install/);
    # non-fatal — run_command never raises, so a download failure won't abort install.
    if os.path.exists(startup_cmd):
        printc("Downloading proxy node archive...", col.OKBLUE)
        run_command(
            f"sudo -u xc_vm /home/xc_vm/bin/php/bin/php {startup_cmd} cron:proxy --force"
        )

    time.sleep(3)

    # Final restart
    run_command("service xc_vm restart")

    # Get server IP
    server_ip = getIP()

    ##################################################
    # FINISHED - SHOW SUMMARY                        #
    ##################################################

    printc("=" * 60, col.OKGREEN)
    printc("INSTALLATION COMPLETED SUCCESSFULLY!", col.OKGREEN, 1)

    printc(f"Distribution: {dist_info['id']} {dist_info['version']}", col.OKGREEN)
    printc(f"Continue Setup: http://{server_ip}:{http_port}/{admin_code}", col.OKBLUE)
    printc(f"Total RAM: {total_ram_mb}MB", col.OKGREEN)

    printc("Credentials have been saved to:", col.OKBLUE)
    printc(f"{local_creds_path}", col.OKGREEN)

    printc("IMPORTANT: Move the credentials file to a secure location!", col.WARNING)

    printc("=" * 60, col.OKGREEN)
