#!/bin/sh
# LTStream bootstrap installer (macOS + Linux) — LTS-21
#
# Version : 1.3.0
# Repo    : LTStream (lttech) — docs/superpowers/specs/2026-08-17-unbundled-ffmpeg-design.md (mục 3.3)
# Purpose : 1 lệnh cài LTStream app + ffmpeg/ffprobe, idempotent, không sudo, không telemetry.
# License : Script này là code của LTStream. Binary ffmpeg/ffprobe (nếu tải static) thuộc
#           license upstream tương ứng: johnvansickle.com builds (GPL) và evermeet.cx (GPL).
#           Xem THIRDPARTY.md trong repo.
#
# Usage:
#   curl -fsSL https://dl.workvps.com/ltstream/install.sh | sudo sh   # Linux cần root (native package)
#   sh install.sh [--dry-run] [--version X] [--next]
#   curl -fsSL https://dl.workvps.com/ltstream/install.sh | sudo sh -s -- --version X   # pipe + flag (ADR-0017)
#
# Flags (ADR-0017 — điền cuối lệnh được, kể cả dạng pipe qua `sh -s --`):
#   --version X   pin version app (vd 0.3.0) — bỏ qua latest.json; thắng env + --next
#   --next        cài kênh next (canary, ADR-0016) — đọc latest-next.json; thua pin
#   --dry-run     in plan install, KHÔNG đổi máy
#
# Env (arg thắng env cùng knob):
#   LTSTREAM_VERSION    pin version app — như --version
#   LTSTREAM_NEXT=1     kênh next — như --next (pin vẫn thắng: ADR-0016)
#   LTSTREAM_SKIP_APP=1 chỉ ensure ffmpeg/ffprobe, không cài app
#   LTSTREAM_BASE_URL   override public base (mặc định https://dl.workvps.com/ltstream) — cho test
#   LTSTREAM_UPGRADE_BINARIES=1  tự nâng cấp binary quá cũ, không hỏi (mặc định hỏi qua tty)
#
# NOTE pipefail: `set -o pipefail` không có trên POSIX sh của macOS (/bin/sh).
# Dùng `set -eu`; mọi pipeline viết để fail an toàn dưới set -e (curl -f ở đầu pipeline).

set -eu

SCRIPT_VERSION=1.3.0
BASE_URL="${LTSTREAM_BASE_URL:-https://dl.workvps.com/ltstream}"
TARGET_VERSION="${LTSTREAM_VERSION:-}"
SKIP_APP="${LTSTREAM_SKIP_APP:-0}"
DRY_RUN=0
NEXT_CHANNEL=0 # 1 = kênh next — resolve ở parse_args (arg --next | env LTSTREAM_NEXT); pin vẫn thắng (ADR-0016/0017)
# Review !257: `curl ... | sudo sh` chạy toàn bộ script với HOME=/root
# (sudo env_reset) → mọi state user-scope (bin/wrapper/desktop/cleanup)
# phải dùng home của user THẬT. Root trực tiếp (không SUDO_USER) → HOME.
REAL_HOME="${HOME}"
if [ "$(id -u)" = 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
    _rh="$(getent passwd "$SUDO_USER" 2>/dev/null | cut -d: -f6 || true)"
    [ -n "$_rh" ] && REAL_HOME="$_rh"
    unset _rh
fi
BIN_DIR="${REAL_HOME}/.ltstream/bin"
SUDO_CMD="" # defensive init — detect_sudo() (main) gán giá trị thật sau đó

# Ngưỡng "quá cũ" cho ffmpeg/ffprobe tìm thấy trên máy (major < ngưỡng →
# đề nghị nâng cấp static vào ~/.ltstream/bin). 4.x là mốc có đủ concat
# demuxer + libx264/aac ổn định cho push RTMP dài hạn; distro cũ (Ubuntu
# 18.04 = ffmpeg 3.4) đã từng gây lỗi codec khi stream.
FFMPEG_MIN_MAJOR="${LTSTREAM_FFMPEG_MIN_MAJOR:-4}"
# Pinned static ffmpeg upstream (fallback khi KHÔNG có package manager).
# macOS (pin 2026-08-17, verified trực tiếp từ máy dev): evermeet.cx 9.0.1
#   versioned URLs (stable — không rolling như get-release) + SHA256 tính
#   từ chính file đã tải về.
# Linux: johnvansickle static builds (GPL). Upstream publish .md5 cạnh mỗi
#   tarball → nhánh static linux verify qua upstream .md5 runtime. (JV không
#   reachable từ máy pin ngày 2026-08-17 nên SHA256 slots để trống — muốn khóa
#   tuyệt đối: tải tarball, sha256sum, điền 2 slot.)
FFMPEG_SHA256_MACOS_FFMPEG="8a8c9e549983409fe6604b9aa665648b7a5def9407fe814c39c8b2ea7f64a48f"
FFMPEG_SHA256_MACOS_FFPROBE="d13f35db03456b7f65b7edb6437c86e23810fbfe91795e571f5b77211343b4f1"
FFMPEG_SHA256_LINUX_AMD64=""
FFMPEG_SHA256_LINUX_ARM64=""
JV_BASE_LINUX="https://johnvansickle.com/ffmpeg"
EVERMEET_FFMPEG_URL="https://evermeet.cx/ffmpeg/ffmpeg-9.0.1.zip"
EVERMEET_FFPROBE_URL="https://evermeet.cx/ffmpeg/ffprobe-9.0.1.zip"

APP_TMP="" # temp dir của install_app — dọn qua trap EXIT trong main
cleanup_app_tmp() { if [ -n "${APP_TMP:-}" ]; then rm -rf "$APP_TMP" || true; fi; }

log()  { printf '==> %s\n' "$*"; }
warn() { printf 'WARN: %s\n' "$*" >&2; }
die()  { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

# ---------------------------------------------------------------------------
detect_platform() {
    OS="$(uname -s)"
    ARCH="$(uname -m)"
    case "$OS" in
        Darwin) PLATFORM_OS=macos ;;
        Linux)  PLATFORM_OS=linux ;;
        *) die "OS không hỗ trợ: $OS (chỉ macOS/Linux)" ;;
    esac
    case "$ARCH" in
        arm64|aarch64) PLATFORM_ARCH=arm64 ;;
        x86_64|amd64)  PLATFORM_ARCH=amd64 ;;
        *) die "Arch không hỗ trợ: $ARCH" ;;
    esac
    case "$PLATFORM_OS/$PLATFORM_ARCH" in
        macos/arm64) JSON_KEY="darwin-aarch64" ;;
        macos/amd64) JSON_KEY="darwin-x86_64" ;;
        linux/amd64) JSON_KEY="linux-x86_64" ;;
        linux/arm64) JSON_KEY="linux-aarch64" ;;
    esac
    log "Platform: $PLATFORM_OS $PLATFORM_ARCH (latest.json key: $JSON_KEY)"
}

sha256_file() {
    if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1"
    elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"
    else die "Không có shasum/sha256sum để verify checksum"; fi
}

md5_file() {
    if command -v md5 >/dev/null 2>&1; then md5 -q "$1"
    elif command -v md5sum >/dev/null 2>&1; then md5sum "$1" | cut -d' ' -f1
    else return 1; fi
}

# ---------------------------------------------------------------------------
# Idempotency binaries (resolver spec 3.2 + #104 rung 5: env > settings >
# ~/.ltstream/bin > OS-native dirs > $PATH). Installer đồng bộ tiêu chí
# "app-visible" với resolver: binary app nhìn thấy ở BẤT KỲ rung nào đều
# tính "đã resolve" (kể cả $PATH — spawn media binary cần env sạch nhưng
# resolution quét PATH bình thường).
app_native_dirs() {
    case "$PLATFORM_OS" in
    macos) printf '/opt/homebrew/bin /usr/local/bin' ;;
    *)     printf '/usr/bin /usr/local/bin' ;;
    esac
}
app_visible_path() { # $1 = ffmpeg | ffprobe — in ra path app resolver nhìn thấy (BIN_DIR > native > PATH)
    if [ -x "$BIN_DIR/$1" ]; then printf '%s' "$BIN_DIR/$1"; return 0; fi
    for d in $(app_native_dirs); do
        if [ -x "$d/$1" ]; then printf '%s' "$d/$1"; return 0; fi
    done
    p="$(command -v "$1" 2>/dev/null || true)"
    if [ -n "$p" ]; then printf '%s' "$p"; return 0; fi
    return 1
}
bin_app_visible() { # $1 = ffmpeg | ffprobe
    [ -n "$(app_visible_path "$1")" ]
}

# Major version của binary ($1 = path) từ dòng đầu `-version`.
# "ffmpeg version 7.1.1-static…" → 7; "…version n6.1…" → 6;
# git snapshot ("version N-111111-g…") / không chạy được → rỗng (coi như
# không rõ — không ép nâng cấp, git build thường là bản mới nhất).
bin_major_version() { # $1 = binary path
    line="$("$1" -version 2>/dev/null | head -n1)" || line=""
    printf '%s\n' "$line" | sed -n 's/.*version n\{0,1\}\([0-9]\{1,\}\)\.\([0-9]\).*/\1/p' | head -n1
}

# Hỏi user qua /dev/tty (stdin là pipe khi `curl | sh`). Non-tty (CI…) →
# mặc định KHÔNG nâng cấp; LTSTREAM_UPGRADE_BINARIES=1 bỏ hỏi.
want_upgrade() {
    [ "${LTSTREAM_UPGRADE_BINARIES:-0}" = 1 ] && return 0
    if [ "$DRY_RUN" = 1 ]; then log "Plan: nâng cấp ffmpeg/ffprobe cũ → static $BIN_DIR"; return 0; fi
    if [ -r /dev/tty ]; then
        printf '==> Nâng cấp ffmpeg/ffprobe vào %s? [y/N] ' "$BIN_DIR" > /dev/tty
        read -r ans < /dev/tty || ans=""
        case "$ans" in [yY]|[yY][eE][sS]) return 0 ;; esac
    fi
    return 1
}

have_pm() { command -v "$1" >/dev/null 2>&1; }
# SUDO_CMD: "" = đang root, "sudo" = có sudo, "none" = không root & không sudo.
# PHẢI gọi ở parent scope (detect_sudo) — không set trong subshell.
detect_sudo() {
    if [ "$(id -u)" = 0 ]; then SUDO_CMD=""
    elif command -v sudo >/dev/null 2>&1; then SUDO_CMD="sudo"
    else SUDO_CMD="none"; fi
}

# #109 native-first: Linux cài package native theo package manager của distro
# (apt→.deb, dnf/yum/zypper→.rpm, apk→.apk) — KHÔNG còn AppImage (110MB/lần
# update; native deps do hệ thống cung cấp, gói ~7MB).
LINUX_PM=""
RPM_INSTALLER=""
detect_linux_pm() {
    # Idempotent — main gọi 1 lần, install_app gọi lại cho chắc (không double-log).
    [ -n "$LINUX_PM" ] && return 0
    if have_pm apt-get || have_pm dpkg; then LINUX_PM=apt
    elif have_pm dnf; then LINUX_PM=rpm; RPM_INSTALLER=dnf
    elif have_pm yum;  then LINUX_PM=rpm; RPM_INSTALLER=yum
    elif have_pm zypper; then LINUX_PM=rpm; RPM_INSTALLER=zypper
    elif have_pm apk; then LINUX_PM=apk
    else
        die "Distro Linux không hỗ trợ (cần apt/dnf/yum/zypper/apk).
Tải package thủ công tại: $BASE_URL"
    fi
    log "Package manager: $LINUX_PM${RPM_INSTALLER:+ ($RPM_INSTALLER)}"
}

linux_package_arch() { # tên arch theo chuẩn package của $LINUX_PM
    case "$PLATFORM_ARCH:$LINUX_PM" in
    amd64:apt) printf 'amd64' ;;
    arm64:apt) printf 'arm64' ;;
    amd64:rpm) printf 'x86_64' ;;
    arm64:rpm) printf 'aarch64' ;;
    amd64:apk) printf 'x86_64' ;;
    arm64:apk) printf 'aarch64' ;;
    esac
}

# Chọn artifact từ nội dung SHA256SUMS (stdin) khớp pm+arch — miễn nhiễm
# naming drift giữa tauri bundler (deb/rpm) và fpm (apk).
linux_pick_artifact() {
    local ext arch
    case "$LINUX_PM" in apt) ext=deb ;; rpm) ext=rpm ;; apk) ext=apk ;; esac
    arch="$(linux_package_arch)"
    awk -v ext="$ext" -v arch="$arch" '
        $2 ~ ("[_-]" arch "\\." ext "$") {print $2; exit}'
}

linux_artifact_name() { # ước lượng tên file (log/pin-path) — nguồn thật là SHA256SUMS
    local ext
    case "$LINUX_PM" in apt) ext=deb ;; rpm) ext=rpm ;; apk) ext=apk ;; esac
    printf 'LTStream_%s_%s.%s' "$VERSION" "$(linux_package_arch)" "$ext"
}
ensure_binaries_pm() {
    # QUAN TRỌNG: hàm này được gọi trong `if ensure_binaries_pm; then` → set -e
    # bị suppressed trong toàn bộ body. PHẢI tự capture exit status từng lệnh PM
    # và return 1 khi fail để caller rơi nhánh static fallback.
    # Linux pm cần root (brew macOS thì không). Không sudo → thẳng static.
    if [ "$PLATFORM_OS" = linux ] && [ "$SUDO_CMD" = "none" ]; then
        warn "Không có sudo/root — không cài ffmpeg qua package manager được"
        return 1
    fi
    if have_pm brew; then
        log "Plan: brew install ffmpeg"
        if [ "$DRY_RUN" = 1 ]; then return 0; fi
        if ! brew install ffmpeg; then
            warn "brew install ffmpeg thất bại — thử fallback static"
            return 1
        fi
    elif have_pm apt-get; then
        log "Plan: ${SUDO_CMD:+$SUDO_CMD }apt-get update && ${SUDO_CMD:+$SUDO_CMD }apt-get install -y ffmpeg"
        if [ "$DRY_RUN" = 1 ]; then return 0; fi
        if ! ($SUDO_CMD apt-get update && $SUDO_CMD apt-get install -y ffmpeg); then
            warn "apt-get install ffmpeg thất bại — thử fallback static"
            return 1
        fi
    elif have_pm dnf; then
        log "Plan: ${SUDO_CMD:+$SUDO_CMD }dnf install -y ffmpeg"
        if [ "$DRY_RUN" = 1 ]; then return 0; fi
        if ! $SUDO_CMD dnf install -y ffmpeg; then
            warn "dnf install ffmpeg thất bại — thử fallback static"
            return 1
        fi
    elif have_pm pacman; then
        log "Plan: ${SUDO_CMD:+$SUDO_CMD }pacman -S --noconfirm ffmpeg"
        if [ "$DRY_RUN" = 1 ]; then return 0; fi
        if ! $SUDO_CMD pacman -S --noconfirm ffmpeg; then
            warn "pacman install ffmpeg thất bại — thử fallback static"
            return 1
        fi
    else
        return 1
    fi
    return 0
}

verify_or_die() { # $1=file $2=expected-sha256
    actual="$(sha256_file "$1" | cut -d' ' -f1)" || die "Không tính được SHA256: $1"
    if [ "$actual" != "$2" ]; then
        die "SHA256 mismatch: $1 (expected $2, got $actual)"
    fi
    log "SHA256 OK: $1 ($actual)"
}

ensure_static_linux() { # $1=arch (amd64|arm64)
    url="$JV_BASE_LINUX/ffmpeg-release-$1-static.tar.xz"
    md5url="$url.md5"
    case "$1" in
        amd64) pinned="$FFMPEG_SHA256_LINUX_AMD64" ;;
        arm64) pinned="$FFMPEG_SHA256_LINUX_ARM64" ;;
    esac
    log "Tải static ffmpeg (linux $1): $url"
    log "Checksum: ${pinned:+SHA256 pin $pinned}${pinned:-upstream .md5: $md5url}"
    if [ "$DRY_RUN" = 1 ]; then return 0; fi
    tmp="$(mktemp -d)"
    curl -fsSL -o "$tmp/ffmpeg.tar.xz" "$url"
    if [ -n "$pinned" ]; then
        verify_or_die "$tmp/ffmpeg.tar.xz" "$pinned"
    else
        curl -fsSL -o "$tmp/ffmpeg.md5" "$md5url" \
            || die "Không fetch được checksum upstream: $md5url (hoặc pin FFMPEG_SHA256_LINUX_$(echo "$1" | tr "[:lower:]" "[:upper:]"))"
        want="$(awk '{print $1}' "$tmp/ffmpeg.md5")"
        got="$(md5_file "$tmp/ffmpeg.tar.xz")" || die "Không có lệnh md5/md5sum để verify"
        if [ "$got" != "$want" ]; then die "MD5 mismatch: $url (expected $want, got $got)"; fi
        log "MD5 upstream OK: $got"
    fi
    tar -xJf "$tmp/ffmpeg.tar.xz" -C "$tmp"
    inner="$(find "$tmp" -maxdepth 2 -name ffmpeg -type f | head -n 1)"
    [ -n "$inner" ] || die "Không tìm thấy binary ffmpeg trong tarball"
    mkdir -p "$BIN_DIR"
    cp "$inner" "$BIN_DIR/ffmpeg"
    cp "$(dirname "$inner")/ffprobe" "$BIN_DIR/ffprobe"
    chmod +x "$BIN_DIR/ffmpeg" "$BIN_DIR/ffprobe"
    rm -rf "$tmp"
}

ensure_static_macos() {
    log "Tải static ffmpeg/ffprobe (evermeet.cx):"
    log "  ffmpeg : $EVERMEET_FFMPEG_URL"
    log "  ffprobe: $EVERMEET_FFPROBE_URL"
    if [ "$DRY_RUN" = 1 ]; then return 0; fi
    # evermeet không có checksum-file URL ổn định → fail-closed nếu chưa pin SHA256
    ff_sha="${LTSTREAM_FFMPEG_SHA256_FFMPEG:-$FFMPEG_SHA256_MACOS_FFMPEG}"
    fp_sha="${LTSTREAM_FFMPEG_SHA256_FFPROBE:-$FFMPEG_SHA256_MACOS_FFPROBE}"
    if [ -z "$ff_sha" ]; then
        die "Chưa pin SHA256 cho evermeet ffmpeg. Lấy giá trị:
  curl -fsSL -o /tmp/ff.zip $EVERMEET_FFMPEG_URL && shasum -a 256 /tmp/ff.zip
rồi export LTSTREAM_FFMPEG_SHA256_FFMPEG=<hash> (hoặc điền FFMPEG_SHA256_MACOS_FFMPEG)."
    fi
    if [ -z "$fp_sha" ]; then
        die "Chưa pin SHA256 cho evermeet ffprobe. Lấy giá trị:
  curl -fsSL -o /tmp/fp.zip $EVERMEET_FFPROBE_URL && shasum -a 256 /tmp/fp.zip
rồi export LTSTREAM_FFMPEG_SHA256_FFPROBE=<hash> (hoặc điền FFMPEG_SHA256_MACOS_FFPROBE)."
    fi
    tmp="$(mktemp -d)"
    curl -fsSL -o "$tmp/ffmpeg.zip" "$EVERMEET_FFMPEG_URL"
    curl -fsSL -o "$tmp/ffprobe.zip" "$EVERMEET_FFPROBE_URL"
    verify_or_die "$tmp/ffmpeg.zip" "$ff_sha"
    verify_or_die "$tmp/ffprobe.zip" "$fp_sha"
    mkdir -p "$BIN_DIR"
    unzip -oq "$tmp/ffmpeg.zip" -d "$BIN_DIR"
    unzip -oq "$tmp/ffprobe.zip" -d "$BIN_DIR"
    chmod +x "$BIN_DIR/ffmpeg" "$BIN_DIR/ffprobe"
    rm -rf "$tmp"
}

ensure_binaries() {
    miss=""
    stale=""
    for b in ffmpeg ffprobe; do
        if ! bin_app_visible "$b"; then
            miss="$miss $b"
            continue
        fi
        # App-visible — kiểm tra version (#104 follow-up): quá cũ → đề nghị
        # nâng cấp static (không parse được version = git build → bỏ qua).
        major="$(bin_major_version "$(app_visible_path "$b")")"
        if [ -n "$major" ] && [ "$major" -lt "$FFMPEG_MIN_MAJOR" ]; then
            stale="$stale $b"
        fi
    done
    if [ -z "$miss" ] && [ -z "$stale" ]; then
        log "ffmpeg/ffprobe đã app-visible và đủ mới (≥ $FFMPEG_MIN_MAJOR.x) — skip bước binaries"
        return 0
    fi

    if [ -n "$stale" ]; then
        log "Phát hiện ffmpeg/ffprobe quá cũ (< $FFMPEG_MIN_MAJOR.x):$stale"
        for b in $stale; do
            log "  $b: $(app_visible_path "$b" | head -c 120) → $("$b" -version 2>/dev/null | head -n1 | cut -c1-80)"
        done
        if want_upgrade; then
            # Static vào BIN_DIR — resolver ưu tiên BIN_DIR trước native dirs
            # nên bản mới thắng ngay mà không đụng install hệ thống.
            if [ "$PLATFORM_OS" = macos ]; then
                ensure_static_macos
            else
                ensure_static_linux "$PLATFORM_ARCH"
            fi
            return 0
        fi
        warn "Giữ nguyên bản cũ — nếu gặp lỗi codec khi stream, chạy lại với LTSTREAM_UPGRADE_BINARIES=1"
    fi

    [ -z "$miss" ] && return 0
    log "Plan: ensure ffmpeg/ffprobe (chưa app-visible:$miss)"
    # PATH-only: binary có trong PATH nhưng ngoài OS-native dirs (snap/
    # conda/...) → symlink vào BIN_DIR để resolve nhanh + không phụ thuộc
    # env shell khởi động app (resolver có rung PATH nhưng symlink vẫn chắc hơn).
    if [ "$DRY_RUN" != 1 ]; then
        linked_all=1
        for b in $miss; do
            p="$(command -v "$b" 2>/dev/null || true)"
            if [ -n "$p" ]; then
                mkdir -p "$BIN_DIR"
                ln -sf "$p" "$BIN_DIR/$b"
                log "symlink PATH-only $p → $BIN_DIR/$b"
            else
                linked_all=0
            fi
        done
        [ "$linked_all" = 1 ] && return 0
    fi
    if ensure_binaries_pm; then
        return 0
    fi
    warn "Package manager không có hoặc cài ffmpeg thất bại — dùng static upstream pinned"
    if [ "$PLATFORM_OS" = macos ]; then
        ensure_static_macos
    else
        ensure_static_linux "$PLATFORM_ARCH"
    fi
}

# ---------------------------------------------------------------------------
# latest.json machine-generated — chấp nhận CẢ HAI format:
#   compact (1 dòng per platform key): "linux-x86_64": { "signature": "..", "url": "..." }
#   jq pretty (publish-release.sh):   "linux-x86_64": {\n "signature": "..",\n "url": ".."\n }
# sed range từ dòng chứa key đến `}` đầu tiên sau đó → extract field trong block.
fetch_json_field() { # $1=file $2=platform-key (bỏ qua khi lấy version) $3=field
    if [ "$3" = version ]; then
        sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -n 1
    else
        # normalize: mỗi platform-key block xuống dòng riêng (xử lý JSON tất-cả-trên-1-dòng)
        sed 's/,[[:space:]]*"\([a-zA-Z0-9_-]*\)":[[:space:]]*{/\n"\1": {/g' "$1" \
            | sed -n "/\"$2\"/,/}/p" \
            | grep -o "\"$3\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n 1 \
            | sed 's/.*"\([^"]*\)"$/\1/'
    fi
}

# #232 SemVer compare có kênh next (ADR-0016): tách suffix `-next` khỏi core
# x.y.z, so 3 field số của core; core bằng → stable (không suffix) mới hơn
# next (prerelease). Field non-digit/thiếu → 0 (fail-safe như cũ).
ver_is_next() { # true nếu $1 có suffix -next
    case "$1" in *-next) return 0 ;; *) return 1 ;; esac
}

version_gt() { # true nếu $1 > $2 (SemVer + kênh next)
    va="${1%%-*}"; vb="${2%%-*}"; i=1
    while [ "$i" -le 3 ]; do
        a="$(printf '%s' "$va" | cut -d. -f"$i")"; b="$(printf '%s' "$vb" | cut -d. -f"$i")"
        case "$a" in ''|*[!0-9]*) a=0 ;; esac
        case "$b" in ''|*[!0-9]*) b=0 ;; esac
        if [ "$a" -gt "$b" ]; then return 0; fi
        if [ "$a" -lt "$b" ]; then return 1; fi
        i=$((i + 1))
    done
    # Core bằng: stable > next (prerelease); next vs next / stable vs stable
    # cùng core → không newer.
    ver_is_next "$1" && return 1
    ver_is_next "$2" && return 0
    return 1
}

app_version_installed() { # in version hiện tại hoặc chuỗi rỗng
    if [ "$PLATFORM_OS" = macos ]; then
        plist="/Applications/LTStream.app/Contents/Info.plist"
        if [ ! -f "$plist" ]; then printf ''; return 0; fi
        # plist sinh bởi Tauri: <key>CFBundleVersion</key> <string>x.y.z</string>
        sed -n '/CFBundleVersion/{n;s/^[[:space:]]*<string>\([^<]*\)<\/string>.*/\1/p;}' "$plist" | head -n 1
    else
        # #109 native-first: version từ package database của pm (idempotency
        # + no-downgrade đọc nguồn thật). Package name "lt-stream" (Tauri
        # bundler normalize productName). PM version là canonical; file
        # VERSION cũ (AppImage r1-r3) chỉ là fallback legacy.
        pm_version=""
        case "$LINUX_PM" in
        apt)
            pm_version="$(dpkg-query -W -f='${Version}' lt-stream 2>/dev/null || true)" ;;
        rpm)
            pm_version="$(rpm -q --qf '%{VERSION}' lt-stream 2>/dev/null || true)" ;;
        apk)
            # "lt-stream-0.5.3-r0 x86_64 {pkg}..." → 0.5.3
            pm_version="$(apk list --installed 2>/dev/null \
                | awk -F- '$1=="lt" && $2=="stream" {sub(/-r[0-9]+.*/,"",$3); print $3; exit}')" ;;
        esac
        if [ -n "$pm_version" ]; then
            printf '%s' "$pm_version"
            return 0
        fi
        # Legacy AppImage r1-r3: ~/.ltstream/app/VERSION.
        if [ -f "$REAL_HOME/.ltstream/app/VERSION" ]; then
            cat "$REAL_HOME/.ltstream/app/VERSION"
            return 0
        fi
    fi
}

# ---------------------------------------------------------------------------
# Native-first Linux (#109): launcher wrapper env-fix + desktop integration.

install_wrapper() {
    WRAP_DIR="$REAL_HOME/.local/bin"
    mkdir -p "$WRAP_DIR"
    WRAP="$WRAP_DIR/ltstream"
    cat > "$WRAP" <<'WRAPPER'
#!/bin/sh
# LTStream launcher — env-fix cho binary native (pm package #109):
#  - preload libwayland-client hệ thống (chống lỗi WebKit wlCompositor)
#  - VPS không GPU (/dev/dri vắng) → software GL + tắt compositing WebKit
APP="/usr/bin/ltstream"
WL="$(ldconfig -p 2>/dev/null | awk '/libwayland-client\.so\.0 \(/{print $NF; exit}')"
if [ -z "$WL" ]; then
    for d in /usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu /usr/lib64 /usr/lib; do
        if [ -f "$d/libwayland-client.so.0" ]; then WL="$d/libwayland-client.so.0"; break; fi
    done
fi
[ -n "$WL" ] && [ -f "$WL" ] && export LD_PRELOAD="$WL${LD_PRELOAD:+:$LD_PRELOAD}"
export WEBKIT_DISABLE_DMABUF_RENDERER=1
if [ ! -e /dev/dri ]; then
    export LIBGL_ALWAYS_SOFTWARE=1
    export WEBKIT_DISABLE_COMPOSITING_MODE=1
fi
exec "$APP" "$@"
WRAPPER
    chmod +x "$WRAP"
    log "Đã cài launcher $WRAP"
}

install_desktop_entry() {
    # Chỉ tích hợp khi có desktop environment — VPS headless bỏ qua.
    if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${XDG_CURRENT_DESKTOP:-}" ]; then
        log "Headless (không DE) — skip menu entry/icon"
        return 0
    fi
    APPS_DIR="$REAL_HOME/.local/share/applications"
    ICON_DIR="$REAL_HOME/.local/share/icons/hicolor/256x256/apps"
    mkdir -p "$APPS_DIR" "$ICON_DIR"
    # Package native (deb/rpm) đã cài desktop entry + icon hệ thống;
    # entry user-scope dưới đè Exec qua wrapper env-fix (~/.local/bin
    # đứng trước /usr/bin trong PATH chuẩn). Icon: CDN (bản user).
    curl -fsSL -o "$ICON_DIR/lt-stream.png" "$BASE_URL/icon.png" \
        || warn "Không lấy được icon — menu dùng icon của package"
    cat > "$APPS_DIR/lt-stream.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=LTStream
Comment=Stream nhiều video lên nhiều luồng YouTube
Exec=$REAL_HOME/.local/bin/ltstream
Icon=lt-stream
Terminal=false
Categories=AudioVideo;Video;Network;
DESKTOP
    # Shortcut lên Desktop (nếu có) + đánh dấu trusted cho GNOME (best-effort).
    if [ -d "$REAL_HOME/Desktop" ]; then
        cp "$APPS_DIR/lt-stream.desktop" "$REAL_HOME/Desktop/lt-stream.desktop"
        command -v gio >/dev/null 2>&1 \
            && gio set "$REAL_HOME/Desktop/lt-stream.desktop" metadata::trusted true 2>/dev/null || true
    fi
    # Best-effort refresh index của DE.
    command -v desktop-file-validate >/dev/null 2>&1 \
        && desktop-file-validate "$APPS_DIR/lt-stream.desktop" 2>/dev/null || true
    command -v update-desktop-database >/dev/null 2>&1 \
        && update-desktop-database "$APPS_DIR" 2>/dev/null || true
    command -v gtk-update-icon-cache >/dev/null 2>&1 \
        && gtk-update-icon-cache -t "$REAL_HOME/.local/share/icons/hicolor" 2>/dev/null || true
    log "Đã thêm menu entry 'LTStream' + icon (AudioVideo)"
}

install_app() {
    tmp="$(mktemp -d)"
    APP_TMP="$tmp"
    resolve_pin_over_next
    if [ -n "$TARGET_VERSION" ]; then
        VERSION="$TARGET_VERSION"
        # pin version: artifact name chuẩn theo layout bucket (mục 3.3)
        case "$JSON_KEY" in
            darwin-aarch64) ARTIFACT="$BASE_URL/$VERSION/LTStream_${VERSION}_aarch64.app.tar.gz" ;;
            darwin-x86_64)  ARTIFACT="$BASE_URL/$VERSION/LTStream_${VERSION}_x64.app.tar.gz" ;;
            linux-*)        ARTIFACT="" ;; # chọn bằng linux_artifact_name() bên dưới
        esac
    else
        FEED="latest.json"
        # #232/ADR-0016: kênh next — feed riêng. NEXT_CHANNEL đã resolve ở
        # parse_args (--next | env LTSTREAM_NEXT); pin (nhánh trên) vẫn thắng
        # qua resolve_pin_over_next (ADR-0017: arg thắng env, pin thắng next).
        if [ "$NEXT_CHANNEL" = 1 ]; then
            FEED="latest-next.json"
        fi
        log "Fetch $BASE_URL/$FEED"
        if [ "$DRY_RUN" = 1 ]; then return 0; fi
        curl -fsSL -o "$tmp/$FEED" "$BASE_URL/$FEED"
        VERSION="$(fetch_json_field "$tmp/$FEED" '' version)"
        [ -n "$VERSION" ] || die "Không parse được version từ $FEED"
        ARTIFACT="$(fetch_json_field "$tmp/$FEED" "$JSON_KEY" url)"
        [ -n "$ARTIFACT" ] || die "Không có artifact cho platform $JSON_KEY trong $FEED"
    fi
    # #109 native-first (Linux): chọn package theo pm+arch TỪ SHA256SUMS —
    # miễn nhiễm naming drift giữa tauri bundler (deb/rpm) và fpm (apk).
    if [ "$PLATFORM_OS" = linux ]; then
        detect_linux_pm
        if [ "$DRY_RUN" = 1 ]; then
            fname="$(linux_artifact_name)"
            ARTIFACT="$BASE_URL/$VERSION/$fname"
        else
            curl -fsSL -o "$tmp/SHA256SUMS" "$BASE_URL/$VERSION/SHA256SUMS" \
                || die "Không fetch được $BASE_URL/$VERSION/SHA256SUMS"
            fname="$(linux_pick_artifact < "$tmp/SHA256SUMS")"
            [ -n "$fname" ] || die "Không có package $LINUX_PM arch $(linux_package_arch) cho $VERSION — xem $BASE_URL/$VERSION/ (hoặc distro chưa được publish artifact)"
            ARTIFACT="$BASE_URL/$VERSION/$fname"
        fi
    fi

    INSTALLED="$(app_version_installed)"
    # #232 (ADR-0016) kênh next: bỏ idempotency-skip lẫn no-downgrade guard
    # khi target HOẶC installed là next — next rolling force-re-tag (cùng
    # version string, bytes khác) phải install lại được; stable luôn cài đè
    # được next (next không được bảo vệ khỏi việc về stable).
    if ! ver_is_next "$VERSION" && ! ver_is_next "${INSTALLED:-}"; then
        if [ -n "$INSTALLED" ] && [ "$INSTALLED" = "$VERSION" ]; then
            log "LTStream $INSTALLED đã cài đúng version — skip app"
            return 0
        fi
        if [ -n "$INSTALLED" ] && version_gt "$INSTALLED" "$VERSION" ]; then
            log "LTStream $INSTALLED đã MỚI HƠN target $VERSION — skip app (installer không downgrade)"
            return 0
        fi
    fi
    log "Plan: cài LTStream $VERSION (hiện tại: ${INSTALLED:-chưa cài})"
    log "  Artifact : $ARTIFACT"
    log "  Checksums: $BASE_URL/$VERSION/SHA256SUMS"
    if [ "$DRY_RUN" = 1 ]; then return 0; fi
    if [ "$PLATFORM_OS" != linux ]; then
        fname="$(basename "$ARTIFACT")"
        curl -fsSL -o "$tmp/SHA256SUMS" "$BASE_URL/$VERSION/SHA256SUMS" \
            || die "Không fetch được $BASE_URL/$VERSION/SHA256SUMS"
    fi
    want="$(awk -v f="$fname" '$2 == f {print $1}' "$tmp/SHA256SUMS")"
    [ -n "$want" ] || die "$fname không có trong SHA256SUMS"
    curl -fsSL -o "$tmp/$fname" "$ARTIFACT"
    verify_or_die "$tmp/$fname" "$want"

    if [ "$PLATFORM_OS" = macos ]; then
        # Stage vào tmp TRƯỚC khi đụng /Applications: download/verify/extract
        # fail thì app cũ còn nguyên.
        rm -rf "$tmp/LTStream.app"
        case "$fname" in
        *.app.tar.gz)
            # Updater-format artifact — bootstrap + updater dùng chung 1 file.
            tar -xzf "$tmp/$fname" -C "$tmp" \
                || die "Không giải nén được $fname"
            [ -d "$tmp/LTStream.app" ] || die "$fname không chứa LTStream.app ở gốc"
            ;;
        *.dmg)
            # Mount deterministic qua -mountpoint (KHÔNG parse output hdiutil —
            # format 3 cột device<TAB>UUID<TAB>/Volumes/.. không ổn định).
            mkdir -p "$tmp/mnt"
            hdiutil attach "$tmp/$fname" -mountpoint "$tmp/mnt" -nobrowse -readonly >/dev/null \
                || die "Không mount được dmg: $fname"
            if [ ! -d "$tmp/mnt/LTStream.app" ]; then
                hdiutil detach "$tmp/mnt" >/dev/null 2>&1 || true
                die "dmg không chứa LTStream.app ở gốc volume"
            fi
            cp -R "$tmp/mnt/LTStream.app" "$tmp/LTStream.app" \
                || { hdiutil detach "$tmp/mnt" >/dev/null 2>&1 || true; die "Không copy được LTStream.app từ dmg"; }
            hdiutil detach "$tmp/mnt" >/dev/null 2>&1 \
                || hdiutil detach -force "$tmp/mnt" >/dev/null 2>&1 || true
            ;;
        *) die "Artifact macOS không hỗ trợ: $fname (cần .app.tar.gz hoặc .dmg)" ;;
        esac
        # Swap: rm cũ + mv staged (cùng filesystem /Applications → mv tức thời).
        # KHÔNG rm-trước-cp: mọi bước dễ fail đã xong trước khi rm.
        rm -rf "/Applications/LTStream.app.new"
        mv "$tmp/LTStream.app" "/Applications/LTStream.app.new"
        rm -rf "/Applications/LTStream.app"
        mv "/Applications/LTStream.app.new" "/Applications/LTStream.app"
        log "Đã cài /Applications/LTStream.app ($VERSION)"
    else
        # #109 native install qua pm. deb/rpm tự resolve depends webkit/gtk;
        # apk cần community repo có webkit2gtk-4.1 + gtk+3.0.
        case "$LINUX_PM" in
        apt)
            $SUDO_CMD apt-get install -y "$tmp/$fname" \
                || die "apt-get install thất bại — xem thông báo phía trên"
            ;;
        rpm)
            $SUDO_CMD "$RPM_INSTALLER" install -y "$tmp/$fname" \
                || die "$RPM_INSTALLER install thất bại (repo thiếu webkit2gtk4.1/gtk3?)"
            ;;
        apk)
            $SUDO_CMD apk add webkit2gtk-4.1 gtk+3.0 >/dev/null 2>&1 \
                || die "apk: cần webkit2gtk-4.1 + gtk+3.0 — bật community repo trong /etc/apk/repositories rồi chạy lại"
            $SUDO_CMD apk add --allow-untrusted "$tmp/$fname" \
                || die "apk add package thất bại"
            ;;
        esac
        install_wrapper
        install_desktop_entry
        # Dọn layout AppImage cũ (installer r1-r3) — native thay thế hoàn toàn.
        rm -f "$REAL_HOME/Applications/LTStream.AppImage" 2>/dev/null || true
        # Cache transcode ($REAL_HOME/.ltstream/cache) KHÔNG xóa — key là
        # content_hash+target_hash, không dính app version nên tái dùng được
        # qua upgrade (xóa cache ở đây từng làm mất toàn bộ cache mỗi lần
        # nâng cấp). Chỉ dọn layout AppImage legacy.
        rm -rf "$REAL_HOME/.ltstream/app" 2>/dev/null || true
        log "Đã cài lt-stream $VERSION qua $LINUX_PM"
        log "Chạy: ~/.local/bin/ltstream — hoặc 'LTStream' trong menu ứng dụng"
    fi
    cleanup_app_tmp
}

# ADR-0017: pin thắng kênh next ở MỌI tổ hợp (env-env / arg-env / arg-arg) —
# resolve đúng 1 chỗ, install_app gọi để cả đường test gọi trực tiếp vẫn
# nhất quán và luôn có log truy vết.
resolve_pin_over_next() {
    [ "$NEXT_CHANNEL" = 1 ] || return 0
    [ -n "$TARGET_VERSION" ] || return 0
    NEXT_CHANNEL=0
    log "Pin $TARGET_VERSION thắng kênh next — bỏ qua --next/LTSTREAM_NEXT"
}

# Parse flags (ADR-0017). Env version đọc ở script top vào TARGET_VERSION —
# arg ghi đè; env LTSTREAM_NEXT dịch sang NEXT_CHANNEL khi không có --next.
parse_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --dry-run) DRY_RUN=1 ;;
            -h|--help) usage ;;
            --version)
                [ $# -ge 2 ] || die "--version cần giá trị (vd: --version 0.3.0)"
                case "$2" in --*) die "--version cần giá trị version, không phải flag" ;; esac
                TARGET_VERSION="$2"
                shift
                ;;
            --version=) die "--version= rỗng — cần giá trị (vd: --version=0.3.0)" ;;
            --version=?*) TARGET_VERSION="${1#--version=}" ;;
            --next) NEXT_CHANNEL=1 ;;
            *) die "Argument không biết: $1 (chỉ hỗ trợ --dry-run, --version, --next, -h)" ;;
        esac
        shift
    done
    if [ "$NEXT_CHANNEL" != 1 ] && [ "${LTSTREAM_NEXT:-0}" = 1 ]; then
        NEXT_CHANNEL=1
    fi
}

usage() {
    # $0 không phải file script khi pipe `curl ... | sh` → fallback text tĩnh
    case "$0" in
        *install.sh) sed -n '2,26p' "$0" ;;
        *) cat <<'USAGE'
LTStream bootstrap installer (macOS + Linux) — LTS-21
Usage: sh install.sh [--dry-run] [--version X] [--next]
  --dry-run     in plan install, KHÔNG đổi máy
  --version X   pin version app (vd 0.3.0) — thắng env + --next
  --next        cài kênh next (canary) — đọc latest-next.json; thua pin
Pipe + flag: curl -fsSL https://dl.workvps.com/ltstream/install.sh | sh -s -- --version X
Env (arg thắng env):
  LTSTREAM_VERSION     pin version app — như --version
  LTSTREAM_NEXT=1      kênh next — như --next; thua pin LTSTREAM_VERSION
  LTSTREAM_SKIP_APP=1  chỉ ensure ffmpeg/ffprobe, không cài app
  LTSTREAM_BASE_URL    override public base URL (mặc định https://dl.workvps.com/ltstream)
Repo: LTStream — docs/superpowers/specs/2026-08-17-unbundled-ffmpeg-design.md
USAGE
    esac
    exit 0
}

main() {
    parse_args "$@"
    trap 'cleanup_app_tmp' EXIT
    if [ "$DRY_RUN" = 1 ]; then log "DRY-RUN: in plan, KHÔNG đổi máy"; fi
    log "LTStream installer v$SCRIPT_VERSION — base: $BASE_URL"
    detect_platform
    detect_sudo
    [ "$PLATFORM_OS" = linux ] && detect_linux_pm
    # Review !257: die SỚM khi không có root (trước ensure_binaries) — máy
    # không sudo không tải 40MB static ffmpeg rồi mới die trong install_app.
    # Dry-run được phép chạy không root (chỉ in plan).
    if [ "$PLATFORM_OS" = linux ] && [ "$SKIP_APP" != 1 ] && [ "$SUDO_CMD" = none ] \
        && [ "$DRY_RUN" != 1 ]; then
        die "Cài package native cần root. Cài sudo rồi chạy lại, hoặc: curl -fsSL $BASE_URL/install.sh | sudo sh"
    fi
    ensure_binaries
    if [ "$SKIP_APP" = 1 ]; then
        log "LTSTREAM_SKIP_APP=1 — skip cài app"
    else
        install_app
    fi
    log "Done. Static binaries dir (nếu dùng): $BIN_DIR"
    if [ "$SKIP_APP" != 1 ] && [ "$DRY_RUN" != 1 ]; then
        if [ "$PLATFORM_OS" = macos ]; then
            log "Mở app: open /Applications/LTStream.app (hoặc Spotlight → LTStream)"
        else
            log "Mở app: ~/.local/bin/ltstream — hoặc 'LTStream' trong menu ứng dụng"
        fi
    fi
}

# Chỉ chạy main khi execute trực tiếp, không phải bị source cho test
# (scripts/test/install-parse-test.sh set LTSTREAM_SOURCED_FOR_TEST=1).
# KHÔNG dùng guard $0/BASH_SOURCE kiểu publish-release.sh: script là POSIX sh
# và đường cài chuẩn `curl | sh` chạy với $0 = "sh" — env flag là cách phân
# biệt an toàn duy nhất.
if [ "${LTSTREAM_SOURCED_FOR_TEST:-0}" != 1 ]; then
    main "$@"
fi
