#!/usr/bin/python3
import hashlib
import io
import json
import os
import random
import shutil
import socket
import subprocess
import sys
import tarfile
import tempfile
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": ["20.04", "22.04", "24.04"],
    "debian": ["12", "13"],
}

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",
        "ipset",
        "ipset-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",
        "ipset",
        "ipset-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",
        "libogg0",
        "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",
        "ipset",
        "certbot",
        "python3-certbot",
        "libssh2-1",
        "libssh2-1-dev",
        "mariadb-server",
        "mariadb-client",
        "mariadb-common",
        "libsodium23",
        "cpufrequtils",
        "mcrypt",
        "libogg0",
        "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",
        "ipset",
        "ipset-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",
        "libogg0",
        "libnuma1",
        "ipset",
    ],
}

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)



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 (the latest one when release_tag is None) and return the hash for file_name"""
    release_path = f"download/{release_tag}" if release_tag else "latest/download"
    hash_url = f"https://github.com/Vateron-Media/{repo}/releases/{release_path}/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:
                    continue
                # The name as md5sum may write it ("*name", "./name"), as the
                # updater and a node's install read it.
                name = parts[1].lstrip("*")
                if name.startswith("./"):
                    name = name[2:]
                if name == 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 ["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
    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_dir = None

    try:
        # A directory of root's own: no other user can put a file where the
        # bundle is checked and unpacked.
        temp_dir = tempfile.mkdtemp(prefix="xcvm.")
        temp_tar_file = os.path.join(temp_dir, patch_name)
        temp_extract_dir = os.path.join(temp_dir, "extract")

        # 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"Could not retrieve MD5 hash for {patch_name}: not installed unverified", col.FAIL)
            os.remove(temp_tar_file)
            return False

        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: debian12, debian_12, 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",
            # As update_binaries.sh leaves them on an update.
            "/home/xc_vm/bin/network",
            "/home/xc_vm/bin/network.py",
        ]
        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)

        # 7. xcvm_core is not in the bundle, and console.php cannot boot without it
        printc("Installing the xcvm_core PHP extension...", col.OKBLUE)
        result = subprocess.run(
            ["bash", "/home/xc_vm/bin/install/install_xcvm_core.sh"],
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            printc("Failed to install the xcvm_core PHP extension", col.FAIL)
            print((result.stderr or result.stdout).strip())
            return False
        printc("xcvm_core PHP extension installed", col.OKGREEN)

        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)
        return False
    finally:
        # The archive and what was unpacked of it, however the step ended
        if temp_dir:
            shutil.rmtree(temp_dir, ignore_errors=True)


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.split(".")[0] == version.split(".")[0]:
                    return True

    # Anything else (Ubuntu 18, Debian 11, the RHEL family) has no ffmpeg or
    # runtime builds: the operator is warned and asked before going on.
    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("Could not retrieve MD5 hash for XC_VM.zip: not used unverified", col.FAIL)
                os.remove("XC_VM.zip")
                return False

            # 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")

            # Checked against the latest release's own hashes.md5, like the download above.
            expected_hash = download_release_hash("XC_VM", None, "XC_VM.zip")
            if (
                expected_hash
                and compute_md5("XC_VM.zip") == expected_hash
                and is_valid_zip("XC_VM.zip")
            ):
                printc("Alternative download successful", col.OKGREEN)
                return True
            printc("Alternative download failed MD5 verification", col.FAIL)
        except Exception:
            pass

        # Not left behind: the next run would take it for a local archive.
        if os.path.exists("XC_VM.zip"):
            os.remove("XC_VM.zip")
        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")

    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 write_secret(path, text):
    """Write a file only its owner may read: 0600 from the moment it exists
    (a file left by an earlier run is closed too)."""
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with io.open(fd, "w", encoding="utf-8") as rFile:
        rFile.write(text)
    os.chmod(path, 0o600)


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", "20"): "ubuntu20",
    ("ubuntu", "22"): "ubuntu22",
    ("ubuntu", "24"): "ubuntu24",
    ("debian", "12"): "debian",
    ("debian", "13"): "debian13",
}

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

# The package, the SHA-256 Ubuntu's signed index lists for it (jammy,
# main/binary-amd64/Packages), and the library it puts in place.
_OPENSSL3_DEB_URL = "http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl3_3.0.2-0ubuntu1_amd64.deb"
_OPENSSL3_DEB_SHA256 = "11a83260542e05aebbbafce9164d594287d2584be80972e08708528fe06f80d3"
_OPENSSL3_LIB = "/usr/lib/x86_64-linux-gnu/libssl.so.3"


def _install_openssl3_compat(dist_id, major):
    """Download and install OpenSSL 3 compatibility .deb for older distros."""
    label = f"{dist_id}_{major}"
    printc("Installing OpenSSL 3 compatibility library for PHP binaries...", col.OKBLUE)
    tmp_dir = None
    try:
        # dpkg runs a package's scripts as root: it is handed this one only when
        # it is the pinned build, from a directory no other user can write to.
        tmp_dir = tempfile.mkdtemp(prefix="xcvm.")
        tmp_file = os.path.join(tmp_dir, f"libssl3_{label}.deb")
        run_command(f'wget -qO "{tmp_file}" "{_OPENSSL3_DEB_URL}"')
        actual_hash = None
        if os.path.exists(tmp_file):
            with open(tmp_file, "rb") as f:
                actual_hash = hashlib.sha256(f.read()).hexdigest()
        if actual_hash != _OPENSSL3_DEB_SHA256:
            printc("OpenSSL 3 library not installed: download failed or is not the expected package", col.WARNING)
            printc("PHP binaries may not work without libssl.so.3", col.WARNING)
            return
        run_command(
            f'dpkg --force-depends -i "{tmp_file}" 2>/dev/null '
            f'|| dpkg -i "{tmp_file}" 2>/dev/null || true'
        )
        if os.path.exists(_OPENSSL3_LIB):
            printc("OpenSSL 3 library installation completed", col.OKGREEN)
        else:
            printc("OpenSSL 3 library not installed: dpkg could not unpack it", col.WARNING)
            printc("PHP binaries may not work without libssl.so.3", col.WARNING)
    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)
    finally:
        if tmp_dir:
            shutil.rmtree(tmp_dir, ignore_errors=True)


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,
    )

    # EOL: the binaries release no longer ships debian_11 assets, so the install
    # would fail halfway through.
    if dist_info["id"] == "debian" and dist_info["version"].split(".")[0] == "11":
        printc("Debian 11 is end-of-life and no longer supported. Use Debian 12 or 13.", col.FAIL)
        sys.exit(1)
    if dist_info["family"] == "redhat":
        printc(
            f"{dist_info['id']} {dist_info['version']} is not supported: XC_VM runs on Ubuntu 20.04/22.04/24.04 and Debian 12/13.",
            col.FAIL,
        )
        sys.exit(1)

    # 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()

    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,
            )
        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: 12, 13
        elif dist_id == "debian" and any(
            version.startswith(v) for v in ["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,
            )

    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)
    # The database user and password: the panel's user only (chown -R below).
    write_secret(rConfigPath, rConfigData)

    # Per-install OPENSSL_EXTRA secret — key material for Encryption (hmac_keys,
    # cached image filenames, proxy URL keys, stream tokens). Generated ONCE and
    # never rotated: rotating it would make all existing encrypted data
    # undecryptable, so only create the file when absent (re-running the installer
    # must never overwrite it). PHP reads it via ConstantsInitializer, falling
    # back to the historical literal when the file is missing.
    rOpensslExtraPath = "/home/xc_vm/config/openssl_extra"
    if not os.path.exists(rOpensslExtraPath):
        with io.open(rOpensslExtraPath, "w", encoding="utf-8") as rFile:
            rFile.write(generate_random_password(40))
        os.chmod(rOpensslExtraPath, 0o600)

    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)
    printc("Enter new port values, or leave empty for defaults", col.OKBLUE)

    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
            # As AuthRepository::updateCodes() fills them: a placeholder left in
            # (#ALIAS#, #ZONE#) makes nginx refuse the whole config.
            nginx_conf_path = os.path.normpath(os.path.join(rCodeDir, os.pardir, "nginx.conf"))
            with open(nginx_conf_path, "r") as f:
                limit_zone = "panel" if "zone=panel:" in f.read() else "one"
            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")
            template_content = template_content.replace("#ALIAS#", "Public/Views/admin")
            template_content = template_content.replace("#ZONE#", limit_zone)

            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
    write_secret(
        "/root/credentials.txt",
        "MariaDB Root \n"
        "Username: root\n"
        f"Password: {root_password}\n\n"
        f"XC_VM Username: {rUsername}\n"
        f"XC_VM Password: {rPassword}\n"
        f"Database: {rDatabase}\n",
    )

    # Also save to installer directory (like original install script). The
    # installer may have been unpacked anywhere (/tmp, a home directory): the
    # file is root's alone, as the one in /root.
    local_creds_path = os.path.join(rPath, "credentials.txt")
    write_secret(
        local_creds_path,
        f"MariaDB Root Password: {root_password}\n"
        f"MariaDB Username: {rUsername}\n"
        f"MariaDB Password: {rPassword}\n"
        f"Database: {rDatabase}\n"
        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"
        )

    # A new panel starts on the cluster API (ADR 0004): its root and the switch,
    # so a load balancer installed from it enrols at its install and never runs
    # the legacy link, then MAIN's own data-plane client (its agent from GitHub).
    # Non-fatal: with an xcvm_core that has no cluster API the panel stays on
    # the legacy link, as before; the switch is in Settings -> Cluster.
    if os.path.exists(startup_cmd):
        printc("Switching the cluster API on...", col.OKBLUE)
        run_command(
            f"/home/xc_vm/bin/php/bin/php {startup_cmd} cluster:init --enable && "
            f"/home/xc_vm/bin/php/bin/php {startup_cmd} cluster:main-dataplane on"
        )
        run_command("chown -R xc_vm:xc_vm /home/xc_vm/config/")

    # 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"
        )

    # Fetch the ffmpeg builds for this distribution (XC_VM_FFMPEG). They are no
    # longer bundled in the release archive; cron:root_signals keeps them current
    # daily. Runs as xc_vm (owner of bin/ffmpeg_bin/); non-fatal, like the above.
    if os.path.exists(startup_cmd):
        printc("Downloading the ffmpeg builds...", col.OKBLUE)
        run_command(
            f"sudo -u xc_vm /home/xc_vm/bin/php/bin/php {startup_cmd} ffmpeg"
        )

    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)
