#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Xtream UI installer — Ubuntu 24.04+ (Python 3, MariaDB 10.11)
Uses local Apache mirror: http://SERVER_IP/Files/ (see setup-mirror.sh)
"""
from __future__ import print_function

import base64
import glob
import json
import os
import random
import re
import shutil
import socket
import string
import subprocess
import sys
import time
import zipfile
from itertools import cycle
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

INSTALL_KEY = {"MAIN": "main.tar.gz", "LB": "LB.tar.gz", "CACHE": "CACHE.tar.gz"}

# Local mirror (Apache): http://YOUR_IP/Files/ — no GitHub during install
LOCAL_FILES_DIR = os.environ.get("XTREAM_FILES_DIR", "/var/www/html/Files")

# Ubuntu 24.04 (Noble) package set
PACKAGES_MAIN = [
    "libcurl3t64-gnutls",
    "libxslt1-dev",
    "libgeoip-dev",
    "e2fsprogs",
    "wget",
    "mcrypt",
    "nscd",
    "htop",
    "zip",
    "unzip",
    "mc",
    "libjemalloc2",
    "python3-paramiko",
    "mariadb-server",
    "mariadb-client",
    "redis-server",
]
PACKAGES_LB = PACKAGES_MAIN[:-3]
PACKAGES_CACHE = PACKAGES_LB

LIBPNG_DEB_NAME = "libpng12-0_1.2.54-1ubuntu1_amd64.deb"
LIBSSL10_DEB_NAME = "libssl1.0.0_1.0.2n-1ubuntu5.13_amd64.deb"
CURL_TARBALL_NAME = "curl-8.2.1.tar.gz"
CURL_PREBUILT_NAME = "xtream-curl-prebuilt.tar.gz"
XTREAM_BASE = "/home/xtreamcodes/iptv_xtream_codes"
XTREAM_CURL_PREFIX = XTREAM_BASE + "/lib/curl"
XTREAM_CURL_LIB = XTREAM_CURL_PREFIX + "/lib"
XTREAM_LD_LIBRARY_PATH = (
    XTREAM_BASE + "/lib/legacy:" + XTREAM_CURL_LIB + ":" + XTREAM_BASE + "/lib"
)
JEMALLOC_PATH = "/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
MARIADB_SERVICE = "mariadb"
MARIADB_UNIT_DIR = "/etc/systemd/system/mariadb.service.d"
MARIADB_ENV_FILE = "/etc/mysql/mysqld"
MY_CNF = "/etc/mysql/my.cnf"
DB_PORT = 7999

MARIADB_CNF = """# Xtream Codes

[client]
port            = 3306

[mysqld_safe]
nice            = 0

[mysqld]
user            = mysql
port            = {db_port}
basedir         = /usr
datadir         = /var/lib/mysql
tmpdir          = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
skip-name-resolve = 1

bind-address            = *
key_buffer_size         = 128M

myisam_sort_buffer_size = 4M
max_allowed_packet      = 64M
myisam-recover-options  = BACKUP
max_length_for_sort_data = 8192

expire_logs_days        = 10
max_binlog_size         = 100M

max_statement_time = 0
transaction_isolation = READ-COMMITTED

max_connections         = 20000
back_log                = 4096
open_files_limit        = 20240
innodb_open_files       = 20240
max_connect_errors      = 3072
table_open_cache        = 4096
table_definition_cache  = 4096

tmp_table_size          = 1G
max_heap_table_size     = 1G

innodb_buffer_pool_size     = 10G
innodb_buffer_pool_instances = 10
innodb_read_io_threads      = 64
innodb_write_io_threads     = 64
innodb_flush_log_at_trx_commit = 0
innodb_flush_method         = O_DIRECT
performance_schema          = 0
innodb_file_per_table       = 1
innodb_io_capacity          = 20000
innodb_table_locks          = 0
innodb_lock_wait_timeout    = 100
innodb_deadlock_detect      = 0

sql-mode = "NO_ENGINE_SUBSTITUTION"

[mysqldump]
quick
quote-names
max_allowed_packet      = 24M
complete-insert

[mysql]

[isamchk]
key_buffer_size         = 16M
""".format(db_port=DB_PORT)


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


def generate(length=19):
    return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(length))


def get_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        return s.getsockname()[0]
    finally:
        s.close()


def get_version():
    try:
        out = subprocess.check_output(["lsb_release", "-d"], text=True)
        return out.split(":")[-1].strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return ""


def get_files_base_url():
    if os.environ.get("XTREAM_FILES_URL"):
        return os.environ["XTREAM_FILES_URL"].rstrip("/")
    ip = get_ip() or "127.0.0.1"
    port = os.environ.get("XTREAM_FILES_PORT", "8080").strip()
    if port and port not in ("80", "443"):
        return "http://%s:%s/Files" % (ip, port)
    return "http://%s/Files" % ip


def fetch_asset(relative_path, dest, quiet=True):
    """Load file from LOCAL_FILES_DIR or local Apache mirror (no GitHub)."""
    rel = relative_path.lstrip("/")
    src = os.path.join(LOCAL_FILES_DIR, rel)
    dest_dir = os.path.dirname(dest)
    if dest_dir:
        os.makedirs(dest_dir, exist_ok=True)
    if os.path.isfile(src) and os.path.getsize(src) > 0:
        shutil.copy2(src, dest)
        return True
    url = "%s/%s" % (get_files_base_url(), rel)
    run('wget -q -O "%s" "%s"' % (dest, url), quiet=quiet)
    return os.path.isfile(dest) and os.path.getsize(dest) > 0


def get_network_interface():
    try:
        out = subprocess.check_output(["ip", "route", "get", "8.8.8.8"], text=True)
        m = re.search(r"\bdev\s+(\S+)", out)
        if m:
            return m.group(1)
    except (subprocess.CalledProcessError, FileNotFoundError):
        pass
    return "eth0"


def run(cmd, quiet=True, check=False):
    kwargs = {"shell": True}
    if quiet:
        kwargs["stdout"] = subprocess.DEVNULL
        kwargs["stderr"] = subprocess.DEVNULL
    return subprocess.run(cmd, check=check, **kwargs)


def printc(text, colour=col.OKBLUE, padding=0):
    width = 42
    print("%s +%s+ %s" % (colour, "-" * width, col.ENDC))
    for _ in range(padding):
        print("%s |%s| %s" % (colour, " " * width, col.ENDC))
    pad = max(0, (width - len(text)) // 2)
    line = (" " * pad + text)[:width].ljust(width)
    print("%s |%s| %s" % (colour, line, col.ENDC))
    for _ in range(padding):
        print("%s |%s| %s" % (colour, " " * width, col.ENDC))
    print("%s +%s+ %s" % (colour, "-" * width, col.ENDC))
    print()


def install_libpng12():
    printc("Installing libpng12")
    lib_dir = "/usr/lib/x86_64-linux-gnu"
    so_path = os.path.join(lib_dir, "libpng12.so.0")
    if os.path.exists(so_path):
        return True
    if not fetch_asset(LIBPNG_DEB_NAME, "/tmp/libpng12.deb"):
        printc("Missing %s in %s" % (LIBPNG_DEB_NAME, LOCAL_FILES_DIR), col.FAIL)
        return False
    run("dpkg-deb -x /tmp/libpng12.deb /tmp/libpng12-extract")
    src = "/tmp/libpng12-extract/lib/x86_64-linux-gnu"
    if os.path.isdir(src):
        run("cp -a %s/libpng12.so.0* %s/" % (src, lib_dir))
        run("ldconfig")
    try:
        os.remove("/tmp/libpng12.deb")
        shutil.rmtree("/tmp/libpng12-extract", ignore_errors=True)
    except OSError:
        pass
    return os.path.exists(so_path)


def install_libssl10():
    printc("Installing libssl 1.0 (PHP runtime)")
    if os.path.exists("/usr/lib/x86_64-linux-gnu/libssl.so.1.0.0"):
        return True
    if not fetch_asset(LIBSSL10_DEB_NAME, "/tmp/libssl1.0.0.deb"):
        printc("Missing %s in %s" % (LIBSSL10_DEB_NAME, LOCAL_FILES_DIR), col.FAIL)
        return False
    run("dpkg -i /tmp/libssl1.0.0.deb")
    try:
        os.remove("/tmp/libssl1.0.0.deb")
    except OSError:
        pass
    return os.path.exists("/usr/lib/x86_64-linux-gnu/libssl.so.1.0.0")


def build_xtream_curl(env):
    """Bundled PHP needs libcurl with CURL_OPENSSL_3; Noble ships OPENSSL_4 only."""
    libcurl = os.path.join(XTREAM_CURL_LIB, "libcurl.so.4")
    if bundled_curl_installed():
        printc("Using bundled libcurl from core tarball", col.OKGREEN)
        return True
    if os.path.exists(libcurl):
        return True
    prebuilt = "/tmp/%s" % CURL_PREBUILT_NAME
    if fetch_asset(CURL_PREBUILT_NAME, prebuilt):
        printc("Installing prebuilt libcurl from local mirror")
        run("rm -rf %s" % XTREAM_CURL_PREFIX)
        run("mkdir -p %s" % XTREAM_CURL_PREFIX)
        run("tar xzf %s -C %s --strip-components=1" % (prebuilt, XTREAM_CURL_PREFIX))
        try:
            os.remove(prebuilt)
        except OSError:
            pass
        if os.path.exists(libcurl):
            return True
    printc("Building libcurl 8.2 for PHP (from local mirror)")
    subprocess.run(
        ["apt-get", "install", "-y", "build-essential", "libssl-dev", "zlib1g-dev"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )
    if not fetch_asset(CURL_TARBALL_NAME, "/tmp/curl-8.2.1.tar.gz"):
        printc("Missing %s in local Files mirror" % CURL_TARBALL_NAME, col.FAIL)
        return False
    run(
        "cd /tmp && rm -rf curl-8.2.1 && tar xzf curl-8.2.1.tar.gz && cd curl-8.2.1 && "
        './configure --prefix=%s --with-openssl --disable-static && '
        "make -j$(nproc) && make install"
        % XTREAM_CURL_PREFIX
    )
    return os.path.exists(libcurl)


def install_runtime_libraries(env):
    install_libpng12()
    install_libssl10()
    build_xtream_curl(env)


def ensure_ipv4_prefer_gai():
    """Prefer IPv4 DNS (bundled ffmpeg/ffprobe often cannot reach IPv6-only answers)."""
    path = "/etc/gai.conf"
    line = "precedence ::ffff:0:0/96  100"
    try:
        existing = ""
        if os.path.isfile(path):
            with open(path) as f:
                existing = f.read()
        if line in existing:
            return
        with open(path, "a") as f:
            if existing and not existing.endswith("\n"):
                f.write("\n")
            f.write("# Xtream: prefer IPv4 for ffmpeg/ffprobe DNS\n")
            f.write(line + "\n")
        printc("IPv4 DNS preference configured (/etc/gai.conf)", col.OKGREEN)
    except OSError as exc:
        printc("WARN: could not write /etc/gai.conf: %s" % exc, col.WARNING)


def fix_cache_ports_main_db():
    """After CACHE install on edge node: ensure MAIN DB has 8880/8443 for watchdog."""
    php = XTREAM_BASE + "/php/bin/php"
    if not os.path.isfile(php) or not os.path.isfile(XTREAM_BASE + "/config"):
        return
    run(
        '%s -r "chdir(\'%s/admin\'); require_once \'functions.php\'; '
        'xtFixCacheServerPortsInDb();" 2>/dev/null'
        % (php, XTREAM_BASE)
    )


def post_deploy_sync(install_type="MAIN"):
    """Normalize legacy ports in nginx + (MAIN) MariaDB servers_cache."""
    helper = XTREAM_BASE + "/pytools/sync_ports_after_deploy.py"
    post_reload = XTREAM_BASE + "/pytools/post_full_reload_main.py"
    if not os.path.isfile(helper):
        return
    printc("Post-deploy sync (ports + cache)")
    if install_type != "CACHE":
        run("/usr/bin/python3 %s --local-nginx" % helper)
    if install_type == "MAIN" and os.path.isfile(XTREAM_BASE + "/config"):
        run("/usr/bin/python3 %s --main-db" % helper)
    if install_type == "LB" and os.path.isfile(XTREAM_BASE + "/config"):
        rebuild = XTREAM_BASE + "/pytools/rebuild_servers_cache.php"
        php = XTREAM_BASE + "/php/bin/php"
        if os.path.isfile(rebuild) and os.path.isfile(php):
            run("%s %s" % (php, rebuild))
    # MAIN-side hooks are also run from balancer.py / cache.py after remote full reload.
    if install_type == "LB" and os.path.isfile(post_reload) and os.path.isfile(
        XTREAM_BASE + "/config"
    ):
        printc("Post full reload (bouquet + adaptive) — also queued when install runs from panel")


def post_node_hooks(install_type):
    """Run on the node itself immediately after install / full reload."""
    ensure_ipv4_prefer_gai()
    if install_type == "LB":
        lb_post = XTREAM_BASE + "/pytools/post_full_reload_lb.py"
        if os.path.isfile(lb_post):
            printc("LB post-install: native streams + nginx (background boot)")
            # Do NOT wait for mass ffmpeg boot — that kept panel on Installing 10–20min.
            run("/usr/bin/python3 %s --background-boot" % lb_post)
    elif install_type == "CACHE":
        cache_post = XTREAM_BASE + "/pytools/post_full_reload_cache.py"
        if os.path.isfile(cache_post):
            printc("CACHE post-install: nginx reload")
            run("/usr/bin/python3 %s" % cache_post)


def patch_start_services():
    base = "/home/xtreamcodes/iptv_xtream_codes"
    path = base + "/start_services.sh"
    wrapper = base + "/php/sbin/php-fpm-xtream.sh"
    if not os.path.exists(path):
        return
    with open(wrapper, "w") as f:
        f.write(
            "#!/bin/bash\n"
            'export LD_LIBRARY_PATH="%s${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\n'
            'exec %s/php/sbin/php-fpm "$@"\n' % (XTREAM_LD_LIBRARY_PATH, base)
        )
    run("chmod +x %s" % wrapper)
    with open(path) as f:
        data = f.read()
    line = 'export LD_LIBRARY_PATH="%s${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\n' % (
        XTREAM_LD_LIBRARY_PATH
    )
    if "/lib/legacy" not in data and line.strip() not in data:
        if data.startswith("#!"):
            first_nl = data.index("\n") + 1
            data = data[:first_nl] + line + data[first_nl:]
        else:
            data = line + data
    fpm = base + "/php/sbin/php-fpm-xtream.sh"
    etc_dir = base + "/php/etc"
    if os.path.isdir(etc_dir):
        for name in os.listdir(etc_dir):
            if not name.endswith(".conf"):
                continue
            conf = name[:-5]
            old = (
                "start-stop-daemon --start --quiet --pidfile %s/php/%s.pid "
                "--exec %s/php/sbin/php-fpm -- --daemonize "
                "--fpm-config %s/php/etc/%s.conf"
                % (base, conf, base, base, conf)
            )
            new = "%s --daemonize --fpm-config %s/php/etc/%s.conf" % (fpm, base, conf)
            data = data.replace(old, new)
            old2 = (
                "start-stop-daemon --start --quiet --pidfile %s/php/%s.pid "
                "--exec %s/php/sbin/php-fpm-xtream.sh -- --daemonize "
                "--fpm-config %s/php/etc/%s.conf"
                % (base, conf, base, base, conf)
            )
            data = data.replace(old2, new)
    for conf in ("VaiIb8", "JdlJXm", "CWcfSP"):
        old = (
            "start-stop-daemon --start --quiet --pidfile %s/php/%s.pid "
            "--exec %s/php/sbin/php-fpm -- --daemonize "
            "--fpm-config %s/php/etc/%s.conf"
            % (base, conf, base, base, conf)
        )
        new = "%s --daemonize --fpm-config %s/php/etc/%s.conf" % (fpm, base, conf)
        data = data.replace(old, new)
    data = data.replace(
        "/php/sbin/php-fpm --",
        "/php/sbin/php-fpm-xtream.sh --",
    )
    data = re.sub(
        r"sudo -u xtreamcodes (/home/xtreamcodes/iptv_xtream_codes/php/bin/php)",
        r'sudo -u xtreamcodes env LD_LIBRARY_PATH="$LD_LIBRARY_PATH" \1',
        data,
    )
    with open(path, "w") as f:
        f.write(data)
    run("chmod +x %s" % path)
    patch_php_cli_wrapper()


def _bundled_file_ok(path, min_size=1):
    return os.path.isfile(path) and os.path.getsize(path) >= min_size


def panel_on_cms_only():
    """MAIN has no local panel — admin runs on CMS."""
    return _bundled_file_ok(XTREAM_BASE + "/.panel_on_cms_only", 0)


def bundled_panel_installed():
    """True when main.tar.gz already contains our custom admin (not release_22f.zip)."""
    if panel_on_cms_only():
        return False
    base = XTREAM_BASE
    return _bundled_file_ok(base + "/admin/cache_manager.php") or _bundled_file_ok(
        base + "/admin/.bundled_panel", 0
    )


def bundled_curl_installed():
    return _bundled_file_ok(XTREAM_CURL_LIB + "/libcurl.so.4", 1000)


def fetch_asset_if_missing(rel, dest, min_size=1):
    if _bundled_file_ok(dest, min_size):
        return True
    return fetch_asset(rel, dest)


def patch_php_cli_wrapper():
    """CLI PHP (stream_monitor, crons) must load bundled libcurl without env vars."""
    base = XTREAM_BASE
    php = base + "/php/bin/php"
    php_real = php + ".real"
    if not os.path.isfile(php):
        return
    if os.path.isfile(php) and not os.path.isfile(php_real):
        with open(php) as f:
            head = f.read(80)
        if head.startswith("#!/bin/bash"):
            return
    if not os.path.isfile(php_real):
        run("mv %s %s" % (php, php_real))
    with open(php, "w") as f:
        f.write(
            "#!/bin/bash\n"
            'export LD_LIBRARY_PATH="%s${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"\n'
            'exec %s "$@"\n' % (XTREAM_CURL_LIB, php_real)
        )
    run("chmod +x %s %s" % (php, php_real))
    run("chown xtreamcodes:xtreamcodes %s %s" % (php, php_real))


def verify_installation(install_type="MAIN"):
    ok = True
    base = XTREAM_BASE
    if install_type == "CACHE":
        nginx = base + "/nginx/sbin/nginx"
        nginx_conf = base + "/nginx/conf/nginx.conf"
        if os.path.isfile(nginx) and os.path.isfile(nginx_conf):
            test = subprocess.run(
                [nginx, "-t", "-c", nginx_conf],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
            if test.returncode != 0:
                printc("CACHE nginx config invalid", col.FAIL)
                ok = False
        listen = subprocess.run(
            ["ss", "-tln"],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
        )
        if ":8880" not in (listen.stdout or ""):
            printc("CACHE edge not listening on :8880", col.FAIL)
            ok = False
        return ok
    if subprocess.run(
        ["systemctl", "is-active", "--quiet", MARIADB_SERVICE]
    ).returncode != 0:
        printc("MariaDB is not running", col.FAIL)
        ok = False
    php_fpm = "/home/xtreamcodes/iptv_xtream_codes/php/sbin/php-fpm"
    if os.path.isfile(php_fpm):
        test = subprocess.run(
            [
                "bash",
                "-c",
                'LD_LIBRARY_PATH=%s ldd "%s" 2>&1 | grep -q "not found" && exit 1; exit 0'
                % (XTREAM_CURL_LIB, php_fpm),
            ],
        )
        if test.returncode != 0:
            printc("PHP-FPM missing libraries", col.FAIL)
            ok = False
    time.sleep(12)
    socks = glob.glob("/home/xtreamcodes/iptv_xtream_codes/php/*.sock")
    if not socks:
        printc("PHP-FPM sockets missing", col.FAIL)
        ok = False
    if panel_on_cms_only():
        code = subprocess.run(
            [
                "env", "-i",
                "PATH=/usr/bin:/bin",
                "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                "http://127.0.0.1/",
            ],
            capture_output=True,
            text=True,
        )
        if code.stdout.strip() not in ("200", "301", "302", "403", "404"):
            printc("Streaming :80 not responding", col.WARNING)
            ok = False
    else:
        code = subprocess.run(
            [
                "env", "-i",
                "PATH=/usr/bin:/bin",
                "curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}",
                "https://127.0.0.1:8443/login.php",
            ],
            capture_output=True,
            text=True,
        )
        if code.stdout.strip() not in ("200", "301", "302"):
            printc("Admin panel not responding on :8443", col.WARNING)
            ok = False
    return ok


def prepare(install_type="MAIN"):
    if install_type == "MAIN":
        packages = list(PACKAGES_MAIN)
    elif install_type == "CACHE":
        packages = list(PACKAGES_CACHE)
    else:
        packages = list(PACKAGES_LB)
    printc("Preparing Installation")

    for lock in (
        "/var/lib/dpkg/lock-frontend",
        "/var/cache/apt/archives/lock",
        "/var/lib/dpkg/lock",
    ):
        try:
            os.remove(lock)
        except OSError:
            pass

    env = os.environ.copy()
    env["DEBIAN_FRONTEND"] = "noninteractive"
    subprocess.run(
        ["apt-get", "update"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        env=env,
    )

    printc("Installing compatibility libraries (libcurl3t64-gnutls)")
    run("apt-get install -y libcurl3t64-gnutls")

    for package in packages:
        printc("Installing %s" % package)
        subprocess.run(
            ["apt-get", "install", "-y", package],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            env=env,
        )

    install_runtime_libraries(env)

    try:
        subprocess.check_output(
            ["getent", "passwd", "xtreamcodes"],
            stderr=subprocess.DEVNULL,
        )
    except subprocess.CalledProcessError:
        printc("Creating user xtreamcodes")
        run(
            "adduser --system --shell /bin/false --group "
            "--disabled-login xtreamcodes"
        )

    os.makedirs("/home/xtreamcodes", exist_ok=True)
    return True


def install_xtream(install_type="MAIN"):
    printc("Loading Software from local mirror")
    try:
        asset = INSTALL_KEY[install_type]
    except KeyError:
        printc("Invalid install type!", col.FAIL)
        return False

    archive = "/tmp/xtreamcodes.tar.gz"
    if not fetch_asset(asset, archive) and install_type == "CACHE":
        printc("CACHE.tar.gz not on mirror — falling back to LB.tar.gz", col.WARNING)
        asset = "LB.tar.gz"
        if not fetch_asset(asset, archive):
            printc("Missing %s — run: sudo bash /home/pack-mirror.sh" % asset, col.FAIL)
            return False
    elif not fetch_asset(asset, archive):
        printc("Missing %s — run: sudo bash /home/pack-mirror.sh" % asset, col.FAIL)
        return False
    if os.path.exists(archive):
        printc("Installing Software")
        run('tar -zxf "%s" -C "/home/xtreamcodes/"' % archive)
        try:
            os.remove(archive)
        except OSError:
            pass
        return True

    printc("Failed to download installation file!", col.FAIL)
    return False


def _http_check(url):
    hdr = {
        "User-Agent": (
            "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
            "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        ),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Connection": "keep-alive",
    }
    req = Request(url, headers=hdr)
    try:
        urlopen(req, timeout=30)
        return True
    except (HTTPError, URLError, OSError):
        return False


def update_panel(install_type="MAIN"):
    zpath = "/tmp/update.zip"
    if install_type == "UPDATE":
        printc("Enter update zip URL or local filename:", col.WARNING)
        link = input(
            "Example: http://%s/Files/release_22f.zip\n\nNow enter:\n\n"
            % get_ip()
        ).strip()
        if link.startswith("http"):
            run('wget -q -O "%s" "%s"' % (zpath, link))
        elif os.path.isfile(link):
            shutil.copy2(link, zpath)
        elif os.path.isfile(os.path.join(LOCAL_FILES_DIR, link)):
            shutil.copy2(os.path.join(LOCAL_FILES_DIR, link), zpath)
        else:
            fetch_asset(link, zpath)
    else:
        printc("Installing Admin Panel")
        if not fetch_asset("release_22f.zip", zpath):
            printc("Missing release_22f.zip in local Files mirror", col.FAIL)
            return False
    if not os.path.exists(zpath):
        printc("Failed to download installation file!", col.FAIL)
        return False

    try:
        zipfile.ZipFile(zpath)
    except zipfile.BadZipFile:
        printc("Invalid link or zip file is corrupted!", col.FAIL)
        os.remove(zpath)
        return False

    geolite = "/home/xtreamcodes/iptv_xtream_codes/GeoLite2.mmdb"
    fetch_asset("GeoLite2.mmdb", "/tmp/GeoLite2.mmdb")
    printc("Updating Software")
    run(
        "chattr -i %s 2>/dev/null; "
        "rm -rf /home/xtreamcodes/iptv_xtream_codes/admin "
        "/home/xtreamcodes/iptv_xtream_codes/pytools && "
        "unzip -o /tmp/update.zip -d /tmp/update/ && "
        "cp -rf /tmp/update/XtreamUI-master/* "
        "/home/xtreamcodes/iptv_xtream_codes/ && "
        "rm -rf /tmp/update/XtreamUI-master /tmp/update"
        % geolite
    )
    if os.path.isfile("/tmp/GeoLite2.mmdb"):
        run("cp -f /tmp/GeoLite2.mmdb %s" % geolite)
    run(
        "chown -R xtreamcodes:xtreamcodes /home/xtreamcodes && "
        "chmod +x /home/xtreamcodes/iptv_xtream_codes/permissions.sh && "
        "chattr +i %s 2>/dev/null" % geolite
    )

    perm = "/home/xtreamcodes/iptv_xtream_codes/permissions.sh"
    marker = "sudo chmod 400 /home/xtreamcodes/iptv_xtream_codes/config"
    if os.path.exists(perm) and marker not in open(perm).read():
        script = r"""#!/bin/bash
sudo chmod -R 777 /home/xtreamcodes 2>/dev/null
sudo find /home/xtreamcodes/iptv_xtream_codes/admin/ -type f -exec chmod 644 {} \; 2>/dev/null
sudo find /home/xtreamcodes/iptv_xtream_codes/admin/ -type d -exec chmod 755 {} \; 2>/dev/null
sudo find /home/xtreamcodes/iptv_xtream_codes/wwwdir/ -type f -exec chmod 644 {} \; 2>/dev/null
sudo find /home/xtreamcodes/iptv_xtream_codes/wwwdir/ -type d -exec chmod 755 {} \; 2>/dev/null
sudo chmod +x /home/xtreamcodes/iptv_xtream_codes/nginx/sbin/nginx 2>/dev/null
# nginx_rtmp removed — HLS-only stack
sudo chmod 400 /home/xtreamcodes/iptv_xtream_codes/config 2>/dev/null
"""
        with open(perm, "w") as f:
            f.write(script)

    run("/home/xtreamcodes/iptv_xtream_codes/permissions.sh")
    try:
        os.remove(zpath)
    except OSError:
        pass
    return True


def _mysql_cmd(extra_args, sql, password=""):
    cmd = ["mysql", "-u", "root"]
    if password:
        cmd.append("-p%s" % password)
    cmd.extend(extra_args)
    cmd.extend(["-e", sql])
    return subprocess.run(
        cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
    ).returncode == 0


def _mysql_import(password, sql_file):
    cmd = ["mysql", "-u", "root"]
    if password:
        cmd.append("-p%s" % password)
    cmd.extend(["xtream_iptvpro"])
    with open(sql_file) as f:
        return (
            subprocess.run(
                cmd,
                stdin=f,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            ).returncode
            == 0
        )


def setup_mariadb(username, password):
    printc("Configuring MariaDB")
    create_cnf = True
    if os.path.exists(MY_CNF):
        try:
            if open(MY_CNF).read(14) == "# Xtream Codes":
                create_cnf = False
        except OSError:
            pass

    if create_cnf:
        if os.path.exists(MY_CNF):
            shutil.copy(MY_CNF, MY_CNF + ".xc")
        with open(MY_CNF, "w") as f:
            f.write(MARIADB_CNF)
        run("systemctl restart %s" % MARIADB_SERVICE)

    _configure_mariadb_jemalloc()

    printc("Enter MariaDB Root Password (empty = socket auth):", col.WARNING)
    for _ in range(5):
        root_pw = input("  ")
        print()
        extra = ("-p%s" % root_pw) if root_pw else ""

        printc("Drop existing & create database? Y/N", col.WARNING)
        drop_db = input("  ").upper() == "Y"
        print()

        if not drop_db:
            return True

        iface = get_network_interface()
        ok = True
        ok = ok and run(
            'mysql -u root%s -e "DROP DATABASE IF EXISTS xtream_iptvpro; '
            'CREATE DATABASE IF NOT EXISTS xtream_iptvpro;"' % extra,
            quiet=True,
        ).returncode == 0

        db_sql = "/home/xtreamcodes/iptv_xtream_codes/database.sql"
        if os.path.exists(db_sql):
            ok = ok and _mysql_import(root_pw, db_sql)

        ok = ok and _mysql_cmd(
            [],
            "USE xtream_iptvpro; UPDATE settings SET "
            "live_streaming_pass = '%s', unique_id = '%s', "
            "crypt_load_balancing = '%s', get_real_ip_client='';"
            % (generate(20), generate(10), generate(20)),
            root_pw,
        )

        ok = ok and _mysql_cmd(
            [],
            "USE xtream_iptvpro; REPLACE INTO streaming_servers "
            "(id, server_name, domain_name, server_ip, vpn_ip, ssh_password, "
            "ssh_port, diff_time_main, http_broadcast_port, total_clients, "
            "system_os, network_interface, latency, status, enable_geoip, "
            "geoip_countries, last_check_ago, can_delete, server_hardware, "
            "total_services, persistent_connections, rtmp_port, geoip_type, "
            "isp_names, isp_type, enable_isp, boost_fpm, http_ports_add, "
            "network_guaranteed_speed, https_broadcast_port, https_ports_add, "
            "whitelist_ips, watchdog_data, timeshift_only) VALUES "
            "(1, 'Main Server', '', '%s', '', NULL, NULL, 0, 80, 1000, "
            "'%s', '%s', 0, 1, 0, '', 0, 0, '{}', 3, 0, 2095, "
            "'low_priority', '', 'low_priority', 0, 1, '', 1000, 443, '', "
            "'[\"127.0.0.1\",\"\"]', '{}', 0);"
            % (get_ip(), get_version(), iface),
            root_pw,
        )

        ok = ok and _mysql_cmd(
            [],
            "USE xtream_iptvpro; REPLACE INTO reg_users "
            "(id, username, password, email, member_group_id, verified, status) "
            "VALUES (1, 'admin', "
            "'$6$rounds=20000$xtreamcodes$XThC5OwfuS0YwS4ahiifzF14vkGbGsFF1w7ETL4sRRC5sOrAWCjWvQJDromZUQoQuwbAXAFdX3h3Cp3vqulpS0', "
            "'admin@website.com', 1, 1, 1);",
            root_pw,
        )

        ok = ok and _mysql_cmd(
            [],
            "CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s'; "
            "GRANT ALL PRIVILEGES ON xtream_iptvpro.* TO '%s'@'%%' "
            "WITH GRANT OPTION; "
            "GRANT SELECT, LOCK TABLES ON *.* TO '%s'@'%%'; "
            "FLUSH PRIVILEGES;"
            % (username, password, username, username),
            root_pw,
        )

        ok = ok and _mysql_cmd(
            [],
            "USE xtream_iptvpro; CREATE TABLE IF NOT EXISTS dashboard_statistics "
            "(id int(11) NOT NULL AUTO_INCREMENT, type varchar(16) NOT NULL "
            "DEFAULT '', time int(16) NOT NULL DEFAULT '0', count int(16) NOT NULL "
            "DEFAULT '0', PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=latin1; "
            "INSERT INTO dashboard_statistics (type, time, count) VALUES "
            "('conns', UNIX_TIMESTAMP(), 0),('users', UNIX_TIMESTAMP(), 0);",
            root_pw,
        )

        if ok:
            try:
                os.remove(db_sql)
            except OSError:
                pass
            return True

        printc("Invalid password or SQL error — try again", col.FAIL)

    return False


def _configure_mariadb_jemalloc():
    if not os.path.isfile(JEMALLOC_PATH):
        return
    os.makedirs(os.path.dirname(MARIADB_ENV_FILE), exist_ok=True)
    with open(MARIADB_ENV_FILE, "w") as f:
        f.write("LD_PRELOAD=%s\n" % JEMALLOC_PATH)
    os.makedirs(MARIADB_UNIT_DIR, exist_ok=True)
    drop_in = os.path.join(MARIADB_UNIT_DIR, "xtream-jemalloc.conf")
    content = "[Service]\nEnvironmentFile=-%s\n" % MARIADB_ENV_FILE
    if not os.path.exists(drop_in) or open(drop_in).read() != content:
        with open(drop_in, "w") as f:
            f.write(content)
        run("systemctl daemon-reload")
        run("systemctl restart %s" % MARIADB_SERVICE)


def encrypt(
    host="127.0.0.1",
    username="user_iptvpro",
    password="",
    database="xtream_iptvpro",
    server_id=1,
    port=DB_PORT,
):
    printc("Encrypting config")
    config_path = "/home/xtreamcodes/iptv_xtream_codes/config"
    try:
        os.remove(config_path)
    except OSError:
        pass

    plain = (
        '{"host":"%s","db_user":"%s","db_pass":"%s","db_name":"%s",'
        '"server_id":"%d", "db_port":"%d"}'
        % (host, username, password, database, server_id, port)
    )
    key = cycle("5709650b0d7806074842c6de575025b1")
    xored = "".join(chr(ord(c) ^ ord(k)) for c, k in zip(plain, key))
    encoded = base64.b64encode(xored.encode("latin-1")).decode("ascii")
    with open(config_path, "wb") as f:
        f.write(encoded.encode("ascii"))


def configure(install_type="MAIN"):
    printc("Configuring System")
    base = "/home/xtreamcodes/iptv_xtream_codes"

    with open("/etc/fstab") as f:
        fstab = f.read()
    if base + "/" not in fstab:
        with open("/etc/fstab", "a") as f:
            f.write(
                "tmpfs %s/streams tmpfs defaults,noatime,nosuid,nodev,"
                "noexec,mode=1777,size=90%% 0 0\n"
                "tmpfs %s/tmp tmpfs defaults,noatime,nosuid,nodev,"
                "noexec,mode=1777,size=2G 0 0\n" % (base, base)
            )

    with open("/etc/sudoers") as f:
        sudoers = f.read()
    if "xtreamcodes" not in sudoers:
        run(
            'echo "xtreamcodes ALL = (root) NOPASSWD: '
            '/sbin/iptables, /usr/bin/chattr" >> /etc/sudoers'
        )

    init_script = "/etc/init.d/xtreamcodes"
    if not os.path.exists(init_script):
        with open(init_script, "w") as f:
            f.write("#! /bin/bash\n%s/start_services.sh\n" % base)
        run("chmod +x %s" % init_script)

    try:
        os.remove("/usr/bin/ffmpeg")
    except OSError:
        pass

    if install_type == "MAIN":
        for name in ("panel_api.php", "player_api.php", "xmltv.php"):
            src = "%s/wwwdir/%s" % (base, name)
            if _bundled_file_ok(src, 100):
                continue
            if os.path.exists(src):
                run("mv %s %s/.%s_original" % (src, base + "/wwwdir", name))
            fetch_asset("wwwdir/%s" % name, src)

    os.makedirs("%s/tv_archive" % base, exist_ok=True)
    run("ln -sf %s/bin/ffmpeg /usr/bin/ffmpeg" % base)
    run("chattr -i %s/GeoLite2.mmdb" % base)
    fetch_asset_if_missing("nginx", "%s/nginx/sbin/nginx" % base, 1000000)
    fetch_asset_if_missing("GeoLite2.mmdb", "%s/GeoLite2.mmdb" % base, 1000000)
    fetch_asset_if_missing("pid_monitor.php", "%s/crons/pid_monitor.php" % base, 500)
    run("chmod +x %s/nginx/sbin/nginx 2>/dev/null" % base)
    run("chown -R xtreamcodes:xtreamcodes /home/xtreamcodes")
    run("chmod -R 0777 /home/xtreamcodes")
    run("chattr +i %s/GeoLite2.mmdb" % base)
    run(
        "sed -i 's|chown -R xtreamcodes:xtreamcodes /home/xtreamcodes|"
        "chown -R xtreamcodes:xtreamcodes /home/xtreamcodes 2>/dev/null|g' "
        "%s/start_services.sh" % base
    )
    run("chmod +x %s/start_services.sh" % base)
    run("mount -a")
    run("chmod 0700 %s/config" % base)
    run(
        'sed -i \'s|echo "Xtream Codes Reborn";|'
        'header("Location: https://www.google.com/");|g\' '
        "%s/wwwdir/index.php" % base
    )

    with open("/etc/hosts") as f:
        hosts = f.read()
    for host in (
        "xtream-codes.com",
        "api.xtream-codes.com",
        "downloads.xtream-codes.com",
    ):
        if host not in hosts:
            run('echo "127.0.0.1    %s" >> /etc/hosts' % host)

    cron_line = "@reboot root %s/start_services.sh" % base
    with open("/etc/crontab") as f:
        crontab = f.read()
    if cron_line not in crontab:
        run('echo "%s" >> /etc/crontab' % cron_line)

    patch_start_services()


def configure_redis(install_type="MAIN"):
    """Xtream core Redis — MAIN only; CACHE/LB get disabled redis.json."""
    base = XTREAM_BASE
    redis_json = base + "/redis.json"
    enabled = 1 if install_type == "MAIN" else 0
    if install_type == "MAIN":
        run("DEBIAN_FRONTEND=noninteractive apt-get install -y redis-server 2>/dev/null || true")
        snippet = base + "/redis/xtream-redis.conf"
        etc_snippet = "/etc/redis/xtream-codes.conf"
        if os.path.isfile(snippet):
            run("cp %s %s && chmod 644 %s" % (snippet, etc_snippet, etc_snippet))
            conf = "/etc/redis/redis.conf"
            if os.path.isfile(conf):
                with open(conf) as f:
                    txt = f.read()
                if "xtream-codes.conf" not in txt:
                    run('echo "include %s" >> %s' % (etc_snippet, conf))
                run(
                    "sed -i '/include %s\\/redis\\/xtream-redis.conf/d' %s 2>/dev/null"
                    % (base, conf)
                )
        run("systemctl enable redis-server 2>/dev/null || systemctl enable redis 2>/dev/null || true")
        run("systemctl restart redis-server 2>/dev/null || systemctl restart redis 2>/dev/null || true")
    data = {
        "enabled": enabled,
        "host": "127.0.0.1",
        "port": 6379,
        "password": "",
        "prefix": "xtream:",
        "db": 0,
    }
    import json as _json

    if os.path.isfile(redis_json):
        try:
            with open(redis_json) as f:
                old = _json.load(f)
            if isinstance(old, dict):
                old.update(data)
                data = old
                data["enabled"] = enabled
        except Exception:
            pass
    os.makedirs(os.path.dirname(redis_json), exist_ok=True)
    with open(redis_json, "w") as f:
        _json.dump(data, f, indent=2)
        f.write("\n")
    run("chown xtreamcodes:xtreamcodes %s 2>/dev/null" % redis_json)
    run("chmod 640 %s 2>/dev/null" % redis_json)
    sync_sh = base + "/crons/run_redis_sync.sh"
    listen_sh = base + "/crons/run_redis_listener.sh"
    for sh in (sync_sh, listen_sh):
        if os.path.isfile(sh):
            run("chmod +x %s" % sh)
    if install_type == "MAIN":
        cron_sync = "* * * * * root %s\n" % sync_sh
        cron_listen = "@reboot root %s\n" % listen_sh
        with open("/etc/crontab") as f:
            crontab = f.read()
        if sync_sh not in crontab:
            run('echo "%s" >> /etc/crontab' % cron_sync.strip())
        if listen_sh not in crontab:
            run('echo "%s" >> /etc/crontab' % cron_listen.strip())
        helper = base + "/pytools/redis_service.py"
        if os.path.isfile(helper):
            run("/usr/bin/python3 %s health 2>/dev/null" % helper)


def write_cache_upstream(lb_ip, lb_port=80, lb_server_id=0, cache_server_id=0):
    base = "/home/xtreamcodes/iptv_xtream_codes"
    import json

    os.makedirs(base + "/cache_data", exist_ok=True)
    data = {
        "upstream_lb_ip": lb_ip,
        "upstream_lb_port": int(lb_port),
        "upstream_lb_server_id": int(lb_server_id),
        "cache_server_id": int(cache_server_id),
        "role": "cache",
    }
    with open(base + "/cache_upstream.json", "w") as f:
        json.dump(data, f, indent=2)
    run("chmod 600 %s/cache_upstream.json" % base)


def patch_nginx_cache_listen_ports(nginx_conf):
    """CACHE nodes listen on 8880/8443 instead of 80/443."""
    if not os.path.isfile(nginx_conf):
        return
    data = open(nginx_conf).read()
    if "listen 8880" in data:
        return
    data = data.replace("listen 80;", "listen 8880;")
    data = data.replace("listen 443 ssl;", "listen 8443 ssl;")
    with open(nginx_conf, "w") as f:
        f.write(data)


def configure_cache_edge(lb_ip=None, lb_port=80, max_disk_gb=500):
    """CACHE node: nginx disk cache for live + timeshift proxied to upstream LB."""
    printc("Configuring cache edge (live + timeshift proxy)")
    base = "/home/xtreamcodes/iptv_xtream_codes"
    cache_dir = base + "/cache_data"
    os.makedirs(cache_dir, exist_ok=True)
    run("chown -R xtreamcodes:xtreamcodes %s" % cache_dir)

    upstream_path = base + "/cache_upstream.json"
    if lb_ip is None and os.path.isfile(upstream_path):
        import json

        with open(upstream_path) as f:
            cfg = json.load(f)
        lb_ip = cfg.get("upstream_lb_ip", "127.0.0.1")
        lb_port = int(cfg.get("upstream_lb_port", 80))
    if not lb_ip:
        lb_ip = "127.0.0.1"

    edge_up_tpl = base + "/nginx/conf/cache_edge_upstream.conf"
    edge_loc_tpl = base + "/nginx/conf/cache_edge_locations.conf"
    edge_up_live = base + "/nginx/conf/cache_edge_upstream_active.conf"
    edge_loc_live = base + "/nginx/conf/cache_edge_active.conf"
    for tpl, live in ((edge_up_tpl, edge_up_live), (edge_loc_tpl, edge_loc_live)):
        if not os.path.isfile(tpl):
            name = os.path.basename(tpl)
            fetch_asset(name, tpl)
        if os.path.isfile(tpl):
            content = open(tpl).read()
            content = content.replace("@LB_HOST@", lb_ip).replace("@LB_PORT@", str(lb_port))
            with open(live, "w") as f:
                f.write(content)
        else:
            printc("Missing %s — add to Files mirror" % os.path.basename(tpl), col.WARNING)

    nginx_conf = base + "/nginx/conf/nginx.conf"
    nginx_ready = base + "/nginx/conf/nginx.conf.cache.ready"
    if os.path.isfile(nginx_ready):
        shutil.copy(nginx_ready, nginx_conf)
        printc("nginx.conf <- nginx.conf.cache.ready (CACHE edge)", col.OKGREEN)
    if not os.path.isfile(nginx_conf):
        return
    data = open(nginx_conf).read()
    cache_zone = (
        "proxy_cache_path %s levels=1:2 keys_zone=xtream_live:512m "
        "max_size=%dg inactive=6h use_temp_path=off;\n"
        % (cache_dir, max(10, int(max_disk_gb)))
    )
    if "keys_zone=xtream_live" not in data:
        data = data.replace(
            "limit_req_zone $binary_remote_addr",
            cache_zone + "\tlimit_req_zone $binary_remote_addr",
            1,
        )
    if "cache_edge_allow_geo.conf" not in data:
        data = data.replace(
            "limit_req_zone $binary_remote_addr",
            "include cache_edge_allow_geo.conf;\n\tlimit_req_zone $binary_remote_addr",
            1,
        )
    if "cache_edge_ts_cache_key.conf" not in data:
        data = data.replace(
            "limit_req_zone $binary_remote_addr",
            "include cache_edge_ts_cache_key.conf;\n\tlimit_req_zone $binary_remote_addr",
            1,
        )
    if "cache_edge_upstream_active.conf" not in data:
        for marker in ("include balance.conf;", "\tinclude balance.conf;"):
            if marker in data:
                data = data.replace(
                    marker,
                    "include cache_edge_upstream_active.conf;\n\t" + marker.lstrip("\t"),
                    1,
                )
                break

    log_fmt = base + "/nginx/conf/cache_edge_log_format.conf"
    if os.path.isfile(log_fmt) and "cache_edge_log_format.conf" not in data:
        for marker in ("include       mime.types;", "include mime.types;"):
            if marker in data:
                data = data.replace(
                    marker,
                    marker + "\n\tinclude cache_edge_log_format.conf;",
                    1,
                )
                break

    data = data.replace("        include adaptive_rewrite_first.conf;\n", "")
    data = data.replace("\tinclude adaptive_rewrite_first.conf;\n", "")

    drop_prefixes = (
        "        rewrite ^/live/",
        "        rewrite ^/(.*)/(.*)/(.*).ch$",
        "        rewrite ^/(.*).ch$",
        "        rewrite ^/ch(.*).m3u8$",
        "\t\trewrite ^/hls/",
        "\t\trewrite ^/hlsr/",
        "\t\trewrite ^/timeshift/",
        "\t\trewrite ^/timeshifts/",
        "\t\trewrite ^/(.*)/(.*)/(\\d+)$",
    )
    lines = data.splitlines(keepends=True)
    filtered = []
    for line in lines:
        if any(line.startswith(p) for p in drop_prefixes):
            continue
        if "location ^~ /timeshift/" in line or "location ^~ /timeshifts/" in line:
            continue
        filtered.append(line)
    data = "".join(filtered)

    if "include cache_edge_active.conf" not in data:
        marker = "        rewrite ^/movie/"
        if marker in data:
            data = data.replace(
                marker,
                "        include cache_edge_active.conf;\n\n"
                "        location ^~ /movie/ { return 403; }\n"
                "        location ^~ /series/ { return 403; }\n\n"
                "        rewrite ^/movie/",
                1,
            )

    with open(nginx_conf, "w") as f:
        f.write(data)

    patch_nginx_cache_listen_ports(nginx_conf)

    inc_helper = base + "/pytools/refresh_cache_access.py"
    if os.path.isfile(inc_helper):
        try:
            import importlib.util

            spec = importlib.util.spec_from_file_location("refresh_cache_access", inc_helper)
            mod = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(mod)
            if hasattr(mod, "apply_cache_nginx_includes_local"):
                mod.apply_cache_nginx_includes_local(nginx_conf)
        except Exception:
            pass

    run("%s/nginx/sbin/nginx -t 2>/dev/null && %s/nginx/sbin/nginx -s reload 2>/dev/null" % (base, base))
    configure_cache_start_services()
    fix_cache_ports_main_db()


def configure_cache_start_services():
    """Cache nodes: nginx edge only via start_services_cache.sh (no php-fpm / LB daemons)."""
    base = "/home/xtreamcodes/iptv_xtream_codes"
    enable = base + "/config/enable_rtmp"
    if os.path.isfile(enable):
        try:
            os.remove(enable)
        except OSError:
            pass
    cache_start = base + "/start_services_cache.sh"
    run("chmod +x %s 2>/dev/null" % cache_start)
    run("chmod +x %s/start_services.sh 2>/dev/null" % base)
    init_script = "/etc/init.d/xtreamcodes"
    if os.path.isfile(init_script):
        with open(init_script, "w") as f:
            f.write("#! /bin/bash\n%s\n" % cache_start)
        run("chmod +x %s" % init_script)
    cron_line = "@reboot root %s\n" % cache_start
    crontab_path = "/etc/crontab"
    if os.path.isfile(crontab_path):
        with open(crontab_path) as f:
            crontab = f.read()
        crontab = re.sub(
            r"@reboot root /home/xtreamcodes/iptv_xtream_codes/start_services\.sh\n?",
            "",
            crontab,
        )
        if cache_start not in crontab:
            crontab = crontab.rstrip() + "\n" + cron_line
        with open(crontab_path, "w") as f:
            f.write(crontab)


def read_existing_config():
    """Decrypt local MAIN/LB config if present."""
    config_path = XTREAM_BASE + "/config"
    if not os.path.isfile(config_path):
        return None
    try:
        raw = open(config_path, "rb").read().decode("ascii")
        xored = base64.b64decode(raw).decode("latin-1")
        key = cycle("5709650b0d7806074842c6de575025b1")
        plain = "".join(
            chr(ord(c) ^ ord(k)) for c, k in zip(xored, key)
        )
        return json.loads(plain)
    except (OSError, ValueError, json.JSONDecodeError):
        return None


def stop_xtream_services():
    """Stop nginx/php/ffmpeg before overwriting MAIN tree (full reload)."""
    printc("Stopping Xtream services")
    base = XTREAM_BASE
    run(
        "pkill -u xtreamcodes 2>/dev/null; "
        "pkill -f iptv_xtream_codes 2>/dev/null; "
        "%s/nginx/sbin/nginx -s stop 2>/dev/null; "
        "%s/nginx_rtmp/sbin/nginx_rtmp -s stop 2>/dev/null; "
        "sleep 2" % (base, base)
    )


def cleanup_main_stale_paths():
    """Remove paths excluded from main.tar.gz (CMS-only image from .209 mirror)."""
    base = XTREAM_BASE
    for rel in ("admin", "migrations", "nginx_rtmp"):
        path = os.path.join(base, rel)
        if os.path.exists(path):
            printc("Removing stale %s (not in main.tar.gz)" % rel, col.WARNING)
            run("rm -rf %s" % path)


def main_full_reload():
    """Reinstall MAIN from main.tar.gz; preserve MariaDB data and config credentials."""
    printc("MAIN full reload (preserve database)", col.OKGREEN, 2)
    cfg = read_existing_config()
    if not cfg:
        printc("No existing config — run fresh MAIN install first", col.FAIL)
        return False

    host = str(cfg.get("host") or "127.0.0.1")
    username = str(cfg.get("db_user") or "user_iptvpro")
    password = str(cfg.get("db_pass") or "")
    database = str(cfg.get("db_name") or "xtream_iptvpro")
    server_id = int(cfg.get("server_id") or 1)
    port = int(cfg.get("db_port") or DB_PORT)

    if not password:
        printc("Existing config missing db_pass", col.FAIL)
        return False

    stop_xtream_services()
    cleanup_main_stale_paths()
    prepare("MAIN")
    if not install_xtream("MAIN"):
        return False

    encrypt(host, username, password, database, server_id, port)
    configure("MAIN")
    configure_redis("MAIN")

    if panel_on_cms_only():
        printc("Panel on CMS only — no :8443 on MAIN", col.OKGREEN)
        run(
            "python3 %s/pytools/strip_main_panel_nginx.py 2>/dev/null || true"
            % XTREAM_BASE
        )
        geolite = XTREAM_BASE + "/GeoLite2.mmdb"
        fetch_asset_if_missing("GeoLite2.mmdb", geolite, 1000000)
        run("chown -R xtreamcodes:xtreamcodes /home/xtreamcodes")
        perm = XTREAM_BASE + "/permissions.sh"
        if os.path.isfile(perm):
            run("chmod +x %s && %s" % (perm, perm))

    cms_vlan = os.environ.get("XC_CMS_VLAN", "").strip()
    if cms_vlan:
        with open(XTREAM_BASE + "/.cms_panel_host", "w") as f:
            f.write(cms_vlan)

    start_services(restart=True)
    ensure_ipv4_prefer_gai()
    post_deploy_sync("MAIN")

    post_reload = XTREAM_BASE + "/pytools/post_full_reload_main.py"
    if os.path.isfile(post_reload):
        # ABR relink runs on CMS (post_full_reload_cluster) — ffprobe sync here breaks M3U children.
        run("/usr/bin/python3 %s --skip-ports" % post_reload)

    printc("MAIN full reload complete", col.OKGREEN)
    return True


def start_services(restart=False, install_type=None):
    printc("Restarting Xtream Codes" if restart else "Starting Xtream Codes")
    base = "/home/xtreamcodes/iptv_xtream_codes"
    if install_type == "CACHE":
        run("%s/start_services_cache.sh" % base)
        return
    nginx_conf = base + "/nginx/conf/nginx.conf"
    if os.path.isfile(nginx_conf):
        try:
            body = open(nginx_conf).read()
        except OSError:
            body = ""
        if "cache_edge_active.conf" in body and "listen 8880" in body:
            run("%s/start_services_cache.sh" % base)
            return
    run("%s/start_services.sh" % base)


def modify_nginx():
    printc("Modifying Nginx")
    if panel_on_cms_only():
        printc("Skipping panel :8443 block (CMS-only)", col.OKGREEN)
        return
    path = "/home/xtreamcodes/iptv_xtream_codes/nginx/conf/nginx.conf"
    if not os.path.exists(path):
        return
    prev = open(path).read()
    if "listen 8443 ssl" in prev and "iptv_xtream_codes/admin/" in prev:
        return
    shutil.copy(path, path + ".xc")
    block = r"""    server {
        listen 8443 ssl;
        ssl_certificate server.crt;
        ssl_certificate_key server.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        index index.php index.html index.htm;
        root /home/xtreamcodes/iptv_xtream_codes/admin/;

        location ~ \.php$ {
            limit_req zone=one burst=8;
            try_files $uri =404;
            fastcgi_index index.php;
            fastcgi_pass php;
            include fastcgi_params;
            fastcgi_buffering on;
            fastcgi_buffers 96 32k;
            fastcgi_buffer_size 32k;
            fastcgi_max_temp_file_size 0;
            fastcgi_keep_conn on;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        }
    }
}"""
    data = "}".join(prev.split("}")[:-1]) + block
    with open(path, "w") as f:
        f.write(data)


def main():
    if os.geteuid() != 0:
        printc("Run as root: sudo python3 install.py", col.FAIL)
        sys.exit(1)

    printc("Xtream UI - Installer (Ubuntu 24 / MariaDB)", col.OKGREEN, 2)
    print(
        "%s | Local files: %s | %s"
        % (col.OKGREEN, get_files_base_url(), col.ENDC)
    )
    if not os.path.isdir(LOCAL_FILES_DIR) or not os.listdir(LOCAL_FILES_DIR):
        printc(
            "Run once: sudo bash /home/setup-mirror.sh (needs internet)",
            col.WARNING,
        )
    print()

    install_type = input(
        "  Installation Type [MAIN, LB, CACHE, UPDATE]: "
    ).strip().upper()
    print()

    if install_type in ("MAIN", "LB", "CACHE"):
        if install_type == "LB":
            host = input("  Main Server IP Address: ").strip()
            db_password = input("  MariaDB Password: ").strip()
            try:
                server_id = int(input("  Load Balancer Server ID: ").strip())
            except ValueError:
                server_id = -1
            lb_ip = ""
            lb_server_id = 0
            print()
        elif install_type == "CACHE":
            host = input("  Main Server IP Address: ").strip()
            db_password = input("  MariaDB Password: ").strip()
            try:
                server_id = int(input("  Cache Server ID (streaming_servers): ").strip())
            except ValueError:
                server_id = -1
            lb_ip = input("  Upstream LB IP Address: ").strip()
            try:
                lb_server_id = int(input("  Upstream LB Server ID: ").strip())
            except ValueError:
                lb_server_id = -1
            try:
                lb_port = int(input("  Upstream LB HTTP Port [80]: ").strip() or "80")
            except ValueError:
                lb_port = 80
            print()
        else:
            host = "127.0.0.1"
            db_password = generate()
            server_id = 1
            lb_ip = ""
            lb_server_id = 0
            lb_port = 80

        username = os.environ.get("XTREAM_LB_DB_USER", "").strip() or "user_iptvpro"
        database = "xtream_iptvpro"
        port = DB_PORT
        if install_type == "LB":
            env_db_host = os.environ.get("XTREAM_LB_DB_HOST", "").strip()
            if env_db_host:
                host = env_db_host

        cache_ok = install_type != "CACHE" or (lb_ip and lb_server_id > -1)
        if host and db_password and server_id > -1 and cache_ok:
            printc("Start installation? Y/N", col.WARNING)
            if input("  ").strip().upper() != "Y":
                printc("Installation cancelled", col.FAIL)
                return

            print()
            prepare(install_type)
            if not install_xtream(install_type):
                sys.exit(1)
            if install_type == "MAIN":
                if not setup_mariadb(username, db_password):
                    sys.exit(1)
            encrypt(host, username, db_password, database, server_id, port)
            configure(install_type)
            configure_redis(install_type)
            if install_type == "CACHE":
                write_cache_upstream(lb_ip, lb_port, lb_server_id, server_id)
                configure_cache_edge(lb_ip, lb_port)
            if install_type == "MAIN":
                if panel_on_cms_only():
                    printc(
                        "Panel on CMS only (.panel_on_cms_only) — no :8443 on MAIN",
                        col.OKGREEN,
                    )
                    run(
                        "python3 %s/pytools/strip_main_panel_nginx.py 2>/dev/null || true"
                        % XTREAM_BASE
                    )
                    geolite = XTREAM_BASE + "/GeoLite2.mmdb"
                    fetch_asset_if_missing("GeoLite2.mmdb", geolite, 1000000)
                    run("chown -R xtreamcodes:xtreamcodes /home/xtreamcodes")
                    perm = XTREAM_BASE + "/permissions.sh"
                    if os.path.isfile(perm):
                        run("chmod +x %s && %s" % (perm, perm))
                else:
                    modify_nginx()
                    if bundled_panel_installed():
                        printc(
                            "Bundled admin panel in main.tar.gz — skipping release_22f.zip",
                            col.OKGREEN,
                        )
                        geolite = XTREAM_BASE + "/GeoLite2.mmdb"
                        fetch_asset_if_missing("GeoLite2.mmdb", geolite, 1000000)
                        run("chown -R xtreamcodes:xtreamcodes /home/xtreamcodes")
                        perm = XTREAM_BASE + "/permissions.sh"
                        if os.path.isfile(perm):
                            run("chmod +x %s && %s" % (perm, perm))
                    else:
                        printc(
                            "No bundled admin — installing legacy release_22f.zip panel",
                            col.WARNING,
                        )
                        update_panel(install_type)
            start_services(install_type=install_type)
            post_deploy_sync(install_type)
            post_node_hooks(install_type)
            if not verify_installation(install_type):
                printc("Install finished with warnings — check services", col.WARNING)
            printc("Installation completed!", col.OKGREEN, 2)
            if install_type == "MAIN":
                printc("Store your MariaDB password:")
                printc(db_password)
                if panel_on_cms_only():
                    printc("Admin UI: CMS only — install cms.tar.gz + install-cms-remote.sh")
                    printc("MAIN: streaming :80 | MariaDB :%s" % DB_PORT)
                else:
                    printc("Admin UI: https://%s:8443" % get_ip())
                    printc("Default login: admin / admin")
        else:
            printc("Invalid entries", col.FAIL)

    elif install_type == "UPDATE":
        if os.path.exists("/home/xtreamcodes/iptv_xtream_codes/wwwdir/api.php"):
            printc("Update Admin Panel? Y/N", col.WARNING)
            if input("  ").strip().upper() == "Y":
                if not update_panel(install_type):
                    sys.exit(1)
                printc("Update completed!", col.OKGREEN, 2)
                start_services(restart=True)
            else:
                printc("Update cancelled", col.WARNING)
        else:
            printc("Install Xtream Codes MAIN first!", col.FAIL)
    else:
        printc("Invalid installation type", col.FAIL)


if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "--refresh-cache-edge":
        if os.geteuid() != 0:
            printc("Run as root", col.FAIL)
            sys.exit(1)
        configure_cache_edge()
        printc("Cache edge config refreshed (live + timeshift)", col.OKGREEN)
        sys.exit(0)
    if len(sys.argv) > 1 and sys.argv[1] == "--main-full-reload":
        if os.geteuid() != 0:
            printc("Run as root: sudo python3 install.py --main-full-reload", col.FAIL)
            sys.exit(1)
        sys.exit(0 if main_full_reload() else 1)
    if len(sys.argv) > 1 and sys.argv[1] == "--sync-ports":
        if os.geteuid() != 0:
            printc("Run as root", col.FAIL)
            sys.exit(1)
        role = "MAIN" if os.path.isfile(XTREAM_BASE + "/config") else "LB"
        post_deploy_sync(role)
        printc("Port sync done", col.OKGREEN)
        sys.exit(0)
    main()
