#!/bin/bash
# lite-game-center-helper — pkexec-elevated worker for Lite Game Center.
#
# Each verb is a single atomic action: install apt packages, drop config
# files, download/extract tarballs, switch audio servers. Streams plain
# text status lines to stdout so the GUI's Gtk.TextView can echo them
# live.
#
# Trust model: this runs as root via /usr/share/polkit-1/actions/
# com.linuxlite.lite-game-center.policy. The polkit action pins the
# action.id to this exact path, so a compromised caller can't substitute
# its own binary. All verbs are allowlisted in the case statement below.
#
# Per-user operations (tarball extraction into ~/.steam/, ~/.local/
# share/lutris/, user systemd services) run as $LITE_GAMECENTER_USER via
# `runuser`, so root never owns files that should be in the user's home.

set -u
set -o pipefail

# ── Common environment ─────────────────────────────────────────────
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export DEBIAN_FRONTEND=noninteractive
export PATH=/usr/sbin:/usr/bin:/sbin:/bin

# Owner of the desktop session (the GUI passes this through; helper
# refuses to act on per-user paths if it's empty).
TARGET_USER="${LITE_GAMECENTER_USER:-}"
TARGET_HOME="${LITE_GAMECENTER_HOME:-}"
if [ -z "$TARGET_USER" ] || [ -z "$TARGET_HOME" ]; then
    # SUDO_USER / PKEXEC_UID is set by pkexec; fall back to those if
    # the GUI somehow didn't pass them.
    if [ -n "${PKEXEC_UID:-}" ]; then
        TARGET_USER="$(getent passwd "$PKEXEC_UID" | cut -d: -f1)"
        TARGET_HOME="$(getent passwd "$PKEXEC_UID" | cut -d: -f6)"
    fi
fi

# Sentinel files the GUI reads to decide a tile's "Applied" badge.
SENTINEL_NVIDIA=/etc/environment.d/99-linuxlite-gaming-nvidia.conf
SENTINEL_SYSTEM=/etc/sysctl.d/99-linuxlite-gaming.conf

# Persistent log under the target user's home. Every install/remove
# session is appended (header + full stdout) so users can review past
# actions from the GUI's "Open Log" button. The helper tees its own
# stdout into this file via process substitution further down; that way
# both helper status lines AND raw apt output land in the file without
# us having to thread logging through every helper function.
LOG_FILE=""
if [ -n "$TARGET_HOME" ] && [ -d "$TARGET_HOME" ]; then
    LOG_DIR="$TARGET_HOME/.local/share/lite-gamecenter"
    LOG_FILE="$LOG_DIR/install.log"
    mkdir -p "$LOG_DIR" 2>/dev/null
    [ ! -e "$LOG_FILE" ] && touch "$LOG_FILE" 2>/dev/null
    chown -R "$TARGET_USER:$TARGET_USER" "$LOG_DIR" 2>/dev/null
fi

# Write session header + redirect stdout through tee so every subsequent
# line lands in both the GUI pipe AND the log file. Best-effort: if any
# of this fails we drop back to plain stdout only.
if [ -n "$LOG_FILE" ] && [ -w "$LOG_FILE" ]; then
    {
        printf '\n========================================\n'
        printf '%s  verb=%s  user=%s\n' \
            "$(date +%Y-%m-%dT%H:%M:%S%z)" \
            "${1:-(none)}" \
            "$TARGET_USER"
        printf '========================================\n'
    } >>"$LOG_FILE" 2>/dev/null
    # Forward stdout through tee. The original stdout (the pipe back to
    # the GUI) is preserved as tee's own stdout; the log file gets a
    # second copy. @@PROG@@ progress lines land in the file too, which
    # is useful for "did the bar move at all?" diagnostics.
    exec > >(tee -a "$LOG_FILE")
fi

# Tarball release feeds. The helper hits GitHub directly for tarballs;
# apt is used for everything that ships as a Debian package.
GE_PROTON_RELEASES="https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest"
WINE_GE_RELEASES="https://api.github.com/repos/GloriousEggroll/wine-ge-custom/releases/latest"

# Where the GUI keeps shipped config drop-ins.
CONFIG_DIR=/usr/share/lite-game-center/configs

# ── Helpers ────────────────────────────────────────────────────────
log()  { printf '%s\n' "$*"; }
err()  { printf 'ERROR: %s\n' "$*" >&2; }
need() { command -v "$1" >/dev/null 2>&1 || { err "missing dependency: $1"; return 1; }; }

# Run a command as the desktop session owner, with their XDG_RUNTIME_DIR
# wired so `systemctl --user` and similar work.
as_user() {
    local uid
    uid="$(id -u "$TARGET_USER")"
    runuser -u "$TARGET_USER" -- env \
        HOME="$TARGET_HOME" \
        XDG_RUNTIME_DIR="/run/user/$uid" \
        DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" \
        LANG=C.UTF-8 LC_ALL=C.UTF-8 \
        "$@"
}

# apt-install one or more packages. Uses APT::Status-Fd=3 to surface a
# structured progress stream — each line gets tagged @@PROG@@<payload>
# on stdout via a process-substitution sed, which the GUI parses for
# download/install percentage + MB/s. Regular apt output is also
# echoed so the user still sees what's being downloaded/unpacked.
apt_install() {
    log "→ apt-get update"
    apt-get update -qq 2>&1 \
        | grep -v "^Reading package lists" || true
    log "→ apt-get install $*"
    apt-get install -y --no-install-recommends \
        -o APT::Status-Fd=3 \
        "$@" 2>&1 \
        3> >(while IFS= read -r _line; do printf '@@PROG@@%s\n' "$_line"; done) \
        | grep -Ev "^(Reading package lists|Building dependency tree|Reading state information)"
    local rc=${PIPESTATUS[0]}
    return $rc
}

# Optional-package install with apt-cache pre-check.
# Args: $1 = package, $2 = one-line "what this covers" note.
# Logs a clean skip when the package isn't in repos instead of spewing
# "E: Package ... has no installation candidate". Used for hardware
# peripheral drivers (xpadneo, xone, dualsensectl, steam-devices) that
# may or may not be present in the active hematite repo over time.
apt_install_optional() {
    local pkg="$1"
    local note_text="${2:-}"
    if apt-cache show "$pkg" >/dev/null 2>&1; then
        log "  → $pkg ($note_text)"
        apt_install "$pkg" \
            || log "    ($pkg install failed — not fatal)"
    else
        log "  ($pkg not in repos on this release — skipping)"
        [ -n "$note_text" ] && log "   $note_text"
    fi
}

apt_remove() {
    log "→ apt-get remove --purge $*"
    apt-get remove --purge -y "$@" 2>&1 \
        | grep -Ev "^(Reading package lists|Building dependency tree|Reading state information)" \
        || return 1
    log "→ apt-get autoremove --purge"
    apt-get autoremove --purge -y 2>&1 \
        | grep -Ev "^(Reading package lists|Building dependency tree|Reading state information)" \
        || true
    return 0
}

# Download and extract a tarball asset (matching a regex pattern) from a
# GitHub releases JSON feed. Extracts as TARGET_USER. Used for GE-Proton
# and Wine-GE — the only two GitHub-tarball sources we touch.
download_github_tarball() {
    local feed="$1" pattern="$2" dest_dir="$3"
    need curl || return 1
    need tar  || return 1

    log "→ Querying GitHub release feed…"
    local json
    json="$(curl -fsSL "$feed")" || { err "GitHub feed unreachable"; return 1; }

    local asset_url
    asset_url="$(printf '%s' "$json" \
        | grep -Eo '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]+\.tar\.(gz|xz)"' \
        | grep -E "$pattern" \
        | head -n 1 \
        | sed -E 's/.*"(https[^"]+)".*/\1/')"
    if [ -z "$asset_url" ]; then
        err "no matching asset in release feed"; return 1
    fi
    log "  found: $asset_url"

    local tmpdir
    tmpdir="$(mktemp -d /tmp/lite-gamecenter.XXXXXX)"
    # mktemp -d creates 0700; we drop priv to $TARGET_USER below to do
    # the extract, and they need 'rx' on the parent dir to reach the
    # tarball. The tarball itself stays root-owned until we chown it.
    chmod 755 "$tmpdir"
    trap "rm -rf '$tmpdir'" RETURN

    local tarball="$tmpdir/$(basename "$asset_url")"
    log "→ Downloading…"
    curl -fL --progress-bar -o "$tarball" "$asset_url" 2>&1 \
        | tr '\r' '\n' | tail -n 1 || true
    [ -s "$tarball" ] || { err "download failed"; return 1; }

    as_user mkdir -p "$dest_dir"
    log "→ Extracting into $dest_dir"
    # Extract as the target user so files end up correctly owned.
    chown "$TARGET_USER:$TARGET_USER" "$tarball"
    as_user tar -C "$dest_dir" -xf "$tarball" || {
        err "extraction failed"; return 1; }

    log "  ✓ extracted."
    return 0
}

# ── Verb implementations ──────────────────────────────────────────

steam_essentials_pkgs() {
    # Steam itself plus the Vulkan + perf stack. Architecture note:
    # On 26.04 (WoW64-aware), steam-installer pulls steam-launcher which
    # downloads Steam's own multi-arch runtime container — we don't
    # need to `dpkg --add-architecture i386` ourselves.
    echo "steam-installer mesa-vulkan-drivers libvulkan1 vulkan-tools gamemode mangohud gamescope"
}

verb_install_steam_essentials() {
    log "== Steam Essentials =="
    log "→ Heads-up: steam-launcher is a single ~350 MB download from"
    log "  Valve's CDN. apt prints one Get: line for it then stays quiet"
    log "  for about a minute on a typical connection. The progress bar"
    log "  at the top of this window keeps pulsing to show work continues."
    log ""
    apt_install $(steam_essentials_pkgs) || return 1
    log "✓ Steam Essentials installed. Launch Steam from the menu."
}

verb_install_steam_proton_ge() {
    log "== Steam + Proton-GE =="
    apt_install $(steam_essentials_pkgs) || return 1
    log ""
    log "→ Fetching latest GE-Proton release…"
    [ -n "$TARGET_USER" ] && [ -d "$TARGET_HOME" ] \
        || { err "no target user"; return 1; }
    download_github_tarball "$GE_PROTON_RELEASES" 'GE-Proton.*\.tar\.gz' \
        "$TARGET_HOME/.steam/root/compatibilitytools.d" || return 1
    log "✓ Done. In Steam: Settings → Compatibility → enable Steam Play,"
    log "  pick the new GE-Proton from the dropdown."
}

verb_install_lutris_wine_ge() {
    log "== Lutris + Wine-GE =="
    # NOTE on Wine packaging on resolute (26.04):
    # The wine-staging package was dropped from Ubuntu's main archive
    # during the WoW64 transition (resolute/questing/forky). It is
    # *only* available via the WineHQ third-party repo. We deliberately
    # do NOT add WineHQ here — Wine-GE (downloaded as a tarball into
    # Lutris's own runners dir below) is the only Wine that Lutris
    # uses for games, and it includes everything wine-staging would
    # have provided plus the GE-specific patches. The system 'wine'
    # package is irrelevant to Lutris-managed prefixes.
    apt_install lutris mesa-vulkan-drivers libvulkan1 vulkan-tools \
        || return 1
    # winetricks is useful for ad-hoc prefix tinkering outside Lutris,
    # but Lutris bundles equivalent functionality, so the tile doesn't
    # fail if winetricks isn't in the repos on this release.
    apt_install winetricks \
        || log "  (winetricks skipped — not available in repos on this release)"
    log ""
    log "→ Fetching latest Wine-GE release…"
    [ -n "$TARGET_USER" ] && [ -d "$TARGET_HOME" ] \
        || { err "no target user"; return 1; }
    download_github_tarball "$WINE_GE_RELEASES" 'wine-lutris.*\.tar\.xz' \
        "$TARGET_HOME/.local/share/lutris/runners/wine" || return 1
    log "✓ Done. Lutris > Preferences > Runners > Wine — pick the new"
    log "  Wine-GE version per game."
}

verb_apply_nvidia_tweaks() {
    log "== NVIDIA Gaming Tweaks =="
    install -d -m 0755 /etc/environment.d
    install -m 0644 "$CONFIG_DIR/nvidia-env.conf" "$SENTINEL_NVIDIA"
    log "  wrote $SENTINEL_NVIDIA"

    # DXVK config (system-wide default — applies to all Proton/Wine
    # processes via DXVK_CONFIG_FILE)
    install -m 0644 "$CONFIG_DIR/dxvk.conf" /etc/dxvk.conf
    log "  wrote /etc/dxvk.conf"

    log "✓ Done. Effects apply on next user login (env vars are read"
    log "  by the session manager)."
}

verb_apply_system_tweaks() {
    log "== System Gaming Tweaks =="

    # vm.max_map_count + swappiness + other sysctls
    install -d -m 0755 /etc/sysctl.d
    install -m 0644 "$CONFIG_DIR/sysctl-gaming.conf" "$SENTINEL_SYSTEM"
    log "  wrote $SENTINEL_SYSTEM"
    sysctl -e -p "$SENTINEL_SYSTEM" -q || true
    log "  sysctl values applied"

    # File descriptor limits for Star Citizen, RDR2, etc.
    install -d -m 0755 /etc/security/limits.d
    install -m 0644 "$CONFIG_DIR/limits-gaming.conf" \
        /etc/security/limits.d/99-linuxlite-gaming.conf
    log "  wrote /etc/security/limits.d/99-linuxlite-gaming.conf"

    # System-wide MangoHud default config
    install -d -m 0755 /etc/MangoHud
    install -m 0644 "$CONFIG_DIR/mangohud.conf" /etc/MangoHud/MangoHud.conf
    log "  wrote /etc/MangoHud/MangoHud.conf"

    # GameMode tuning
    install -m 0644 "$CONFIG_DIR/gamemode.ini" /etc/gamemode.ini
    log "  wrote /etc/gamemode.ini"

    # PulseAudio low-latency drop-in (in-place tune, NOT a server swap)
    install -d -m 0755 /etc/pulse/daemon.conf.d
    install -m 0644 "$CONFIG_DIR/pulse-gaming.conf" \
        /etc/pulse/daemon.conf.d/99-linuxlite-gaming.conf
    log "  wrote /etc/pulse/daemon.conf.d/99-linuxlite-gaming.conf"

    # Controller-driver bundle. All four are apt-cache-pre-checked so
    # machines whose hematite repo doesn't yet carry one of these get a
    # clean skip note instead of "E: ... has no installation candidate"
    # log spam. None is fatal — basic controller support is in the
    # mainline kernel; these add the polish for specific hardware.
    log ""
    log "→ Installing controller-driver bundle…"
    apt_install_optional xpadneo-dkms \
        "Xbox One / Series / Elite over Bluetooth — rumble, battery, force feedback."
    apt_install_optional xone-dkms \
        "Xbox One / Series over USB and the proprietary 2.4 GHz wireless dongle."
    # xone-dongle-firmware is sibling-recommended by xone-dkms but our apt
    # call uses --no-install-recommends, so we add it explicitly. Multi-
    # line heads-up first so the user understands what the postinst is
    # about to do (network fetch from Microsoft Update Catalog + MS ToU)
    # — same "no surprises" pattern as the PipeWire-swap tile.
    log ""
    log "  Note: xone-dongle-firmware fetches Microsoft-signed firmware blobs"
    log "  from Microsoft Update Catalog (catalog.s.download.windowsupdate.com)"
    log "  at install time. Subject to Microsoft's Terms of Use. Only needed"
    log "  if you use the Xbox Wireless Adapter (2.4 GHz dongle); BT and"
    log "  USB-wired Xbox controllers don't require it."
    apt_install_optional xone-dongle-firmware \
        "Microsoft-signed firmware for the Xbox Wireless Adapter dongle (network fetch + MS ToU at install)."
    apt_install_optional dualsensectl \
        "PS5 DualSense polish — LED colour, microphone, adaptive triggers, haptics."
    apt_install_optional steam-devices \
        "udev rules so non-Steam apps (Lutris, native games) can access controllers."
    apt_install_optional piper \
        "GTK4 GUI for configuring gaming mouse DPI, buttons, RGB (Logitech G, SteelSeries, ROCCAT etc. via libratbag)."

    log ""
    log "✓ Done. Some changes need a reboot (limits) or app re-launch (MangoHud)."
}

verb_switch_to_pipewire() {
    log "== Switch to PipeWire =="
    # `pipewire-audio` is the meta-package that pulls daemon + wireplumber
    # + ALSA + Pulse-compat. Available since 24.04, present on resolute.
    if ! apt_install pipewire-audio wireplumber; then
        # Fallback: explicit packages
        apt_install pipewire wireplumber pipewire-pulse pipewire-alsa \
            || return 1
    fi

    # Drop the low-latency config (per-user, into TARGET_USER's config
    # dir, owned by them).
    as_user mkdir -p "$TARGET_HOME/.config/pipewire/pipewire.conf.d"
    install -m 0644 "$CONFIG_DIR/pipewire-gaming.conf" \
        "$TARGET_HOME/.config/pipewire/pipewire.conf.d/99-linuxlite-gaming.conf"
    chown "$TARGET_USER:$TARGET_USER" \
        "$TARGET_HOME/.config/pipewire/pipewire.conf.d/99-linuxlite-gaming.conf"
    log "  wrote ~/.config/pipewire/pipewire.conf.d/99-linuxlite-gaming.conf"

    log "→ Masking PulseAudio user services for $TARGET_USER…"
    as_user systemctl --user --now disable \
        pulseaudio.service pulseaudio.socket 2>&1 || true
    as_user systemctl --user mask \
        pulseaudio.service pulseaudio.socket 2>&1 || true

    log "→ Enabling PipeWire user services for $TARGET_USER…"
    as_user systemctl --user unmask \
        pipewire.service pipewire.socket pipewire-pulse.service \
        pipewire-pulse.socket wireplumber.service 2>&1 || true
    as_user systemctl --user --now enable \
        pipewire.service pipewire.socket pipewire-pulse.service \
        pipewire-pulse.socket wireplumber.service 2>&1 || true

    log "✓ PipeWire is now the active audio server."
    log "  For best results, log out and log back in — that gives every"
    log "  audio app a clean start on PipeWire. A reboot is NOT required."
    log "  Apps already playing audio (Firefox, Spotify, Discord, Steam"
    log "  voice chat) may stay silent until restarted."
}

verb_switch_to_pulseaudio() {
    log "== Switch back to PulseAudio =="
    log "→ Stopping PipeWire user services…"
    as_user systemctl --user --now disable \
        pipewire.service pipewire.socket pipewire-pulse.service \
        pipewire-pulse.socket wireplumber.service 2>&1 || true

    log "→ Unmasking + enabling PulseAudio user services…"
    as_user systemctl --user unmask \
        pulseaudio.service pulseaudio.socket 2>&1 || true
    as_user systemctl --user --now enable \
        pulseaudio.service pulseaudio.socket 2>&1 || true

    rm -f "$TARGET_HOME/.config/pipewire/pipewire.conf.d/99-linuxlite-gaming.conf"

    log "✓ PulseAudio is active. Audio may glitch briefly while apps reconnect."
}

verb_install_full_setup() {
    log "== Full Setup =="
    log "(skipping the PipeWire audio swap — that stays an explicit decision)"
    log ""
    verb_install_steam_essentials || return 1
    log ""
    verb_install_steam_proton_ge  || return 1
    log ""
    verb_install_lutris_wine_ge   || log "(Lutris skipped — see message above)"
    log ""
    verb_apply_system_tweaks      || return 1
    # Gate NVIDIA tweaks on the same condition the GUI tile uses:
    # `nvidia-smi` succeeds, i.e. the proprietary driver is loaded.
    # lspci-sees-NVIDIA-card was too loose — on Pascal/Maxwell/Kepler
    # machines running Nouveau or NVK, the __GL_* env vars are silent
    # no-ops. Applying them anyway is misleading ("✓ Done. Effects
    # apply on next login" when nothing actually reads them).
    if nvidia-smi --query-gpu=driver_version --format=csv,noheader >/dev/null 2>&1; then
        log ""
        verb_apply_nvidia_tweaks  || return 1
    else
        log ""
        log "(NVIDIA Gaming Tweaks skipped — proprietary NVIDIA driver"
        log " not loaded; the __GL_* env vars would be no-ops here.)"
    fi
    log ""
    log "✓✓ Full Setup complete. Open Steam, enable Steam Play, you're done."
}

# ── Remove verbs ───────────────────────────────────────────────────
# Removal is intentionally less aggressive than install: we don't
# uninstall xpadneo (might be in use by a controller right now), we
# leave Mesa Vulkan alone (other software depends on it), and we don't
# strip GameMode/MangoHud's deps. Only the things this tile uniquely
# added get reverted.

verb_remove_steam_essentials() {
    log "== Remove Steam Essentials =="
    apt_remove steam-installer steam steam-launcher gamescope mangohud \
        || true
    log "✓ Steam + Gamescope + MangoHud removed."
    log "  (Vulkan + GameMode kept — they're useful outside Steam too.)"
}

verb_remove_steam_proton_ge() {
    log "== Remove Steam + Proton-GE =="
    if [ -n "$TARGET_USER" ] && [ -d "$TARGET_HOME/.steam/root/compatibilitytools.d" ]; then
        log "→ Removing GE-Proton installs…"
        as_user bash -c \
            'rm -rf "$HOME"/.steam/root/compatibilitytools.d/GE-Proton*'
    fi
    verb_remove_steam_essentials
}

verb_remove_lutris_wine_ge() {
    log "== Remove Lutris + Wine-GE =="
    if [ -n "$TARGET_USER" ] && [ -d "$TARGET_HOME/.local/share/lutris/runners/wine" ]; then
        log "→ Removing Wine-GE runners…"
        as_user bash -c \
            'rm -rf "$HOME"/.local/share/lutris/runners/wine/lutris-* "$HOME"/.local/share/lutris/runners/wine/wine-*'
    fi
    apt_remove lutris wine-staging winetricks || true
    log "✓ Lutris stack removed."
}

verb_remove_nvidia_tweaks() {
    log "== Remove NVIDIA Gaming Tweaks =="
    rm -f "$SENTINEL_NVIDIA" /etc/dxvk.conf
    log "✓ NVIDIA tweak files removed. Re-login to drop the env vars."
}

verb_remove_system_tweaks() {
    log "== Remove System Gaming Tweaks =="
    rm -f "$SENTINEL_SYSTEM" \
        /etc/security/limits.d/99-linuxlite-gaming.conf \
        /etc/MangoHud/MangoHud.conf \
        /etc/gamemode.ini \
        /etc/pulse/daemon.conf.d/99-linuxlite-gaming.conf
    # Roll back the sysctls so they don't survive in the running kernel
    sysctl -w vm.swappiness=60 -q 2>/dev/null || true
    log "✓ System tweak files removed."
    log "  (xpadneo-dkms kept — uninstalling can disrupt an active controller.)"
}

verb_remove_full_setup() {
    log "== Remove Full Setup =="
    verb_remove_steam_proton_ge
    verb_remove_lutris_wine_ge
    verb_remove_system_tweaks
    verb_remove_nvidia_tweaks
}

# ── Dispatch ───────────────────────────────────────────────────────
VERB="${1:-}"
case "$VERB" in
    install-steam-essentials)  verb_install_steam_essentials ;;
    install-steam-proton-ge)   verb_install_steam_proton_ge ;;
    install-lutris-wine-ge)    verb_install_lutris_wine_ge ;;
    apply-nvidia-tweaks)       verb_apply_nvidia_tweaks ;;
    apply-system-tweaks)       verb_apply_system_tweaks ;;
    switch-to-pipewire)        verb_switch_to_pipewire ;;
    switch-to-pulseaudio)      verb_switch_to_pulseaudio ;;
    install-full-setup)        verb_install_full_setup ;;
    remove-steam-essentials)   verb_remove_steam_essentials ;;
    remove-steam-proton-ge)    verb_remove_steam_proton_ge ;;
    remove-lutris-wine-ge)     verb_remove_lutris_wine_ge ;;
    remove-nvidia-tweaks)      verb_remove_nvidia_tweaks ;;
    remove-system-tweaks)      verb_remove_system_tweaks ;;
    remove-full-setup)         verb_remove_full_setup ;;
    *)
        err "unknown verb: $VERB"
        echo "Usage: $0 {install-steam-essentials|install-steam-proton-ge|..." >&2
        exit 2
        ;;
esac
exit $?
