#!/bin/bash
# Lite Software Sources privileged helper.
# Invoked only via pkexec under the com.linuxlite.lite-software-sources.run polkit action.
#
# Subcommands:
#   write-source  <path>                   write DEB822 from stdin, atomic + backup
#   remove-source <path>                   delete a .sources file (and matching keyring)
#   add-ppa       <ppa:owner/name>         shell out to add-apt-repository --no-update
#   import-key    <keyring path>           dearmor key bytes from stdin
#   update                                 run apt-get update (streams to stdout)
#
# Paths are constrained to /etc/apt/sources.list.d/*.sources and /etc/apt/keyrings/*.gpg.
# Any other path is rejected outright.

set -e
set -o pipefail

PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH LANG=C.UTF-8 LC_ALL=C.UTF-8

SOURCES_DIR=/etc/apt/sources.list.d
# Writes are restricted to /etc/apt/keyrings/ — /usr/share/keyrings/ is dpkg-managed
# distro territory and must never be modified by us. The GUI reads from both
# directories but always passes /etc/apt/keyrings/<name>.gpg as the import dest.
KEYRINGS_DIR=/etc/apt/keyrings
# Pidfile for in-flight apt-get update so cancel-update can find the child PID.
UPDATE_PIDFILE=/run/lite-software-sources.update.pid

die() { echo "lite-software-sources-helper: $*" >&2; exit 1; }

[ "$(id -u)" = "0" ] || die "must run as root (via pkexec)"

ensure_in_sources_dir() {
    case "$1" in
        "$SOURCES_DIR"/*.sources) ;;
        *) die "path outside $SOURCES_DIR/*.sources: $1" ;;
    esac
    case "$1" in *..*) die "path traversal rejected: $1" ;; esac
}
ensure_in_keyrings_dir() {
    case "$1" in
        "$KEYRINGS_DIR"/*.gpg) ;;
        *) die "path outside $KEYRINGS_DIR/*.gpg: $1" ;;
    esac
    case "$1" in *..*) die "path traversal rejected: $1" ;; esac
}

# Atomic write — no .bak files in /etc/apt/sources.list.d/ per Jerry's policy
# (apt warns on any non-.sources/.list filename there, plus they're noise).
# Handles chattr +i which the LL installer's choose-apt-mirror.sh sets on
# /etc/apt/sources.list.d/ubuntu.sources to defend against ubuntu-pro-client
# rewrites. Bit is cleared before the rename and re-applied after.
cmd_write_source() {
    local file="$1"
    [ -n "$file" ] || die "write-source: missing path"
    ensure_in_sources_dir "$file"
    mkdir -p "$SOURCES_DIR"

    local tmp
    tmp="$(mktemp -p "$SOURCES_DIR" ".lite-ss.XXXXXX")"
    cat > "$tmp"

    # Round-trip sanity: well-formed DEB822 must declare Types + URIs + Suites.
    if ! awk '
        /^Types:/   {t=1}
        /^URIs:/    {u=1}
        /^Suites:/  {s=1}
        END {exit !(t && u && s)}
    ' "$tmp"; then
        rm -f "$tmp"
        die "rejected: written file lacks required Types/URIs/Suites"
    fi

    chmod 644 "$tmp"
    chown root:root "$tmp"

    # Clear the immutable bit if set, remember so we can re-apply after.
    local had_immutable=0
    if [ -f "$file" ] && command -v lsattr >/dev/null 2>&1; then
        if lsattr -d "$file" 2>/dev/null | awk '{print $1}' | grep -q i; then
            had_immutable=1
            chattr -i "$file" 2>/dev/null || true
        fi
    fi

    mv "$tmp" "$file"
    sync -f "$file" 2>/dev/null || true

    if [ "$had_immutable" = "1" ] && command -v chattr >/dev/null 2>&1; then
        chattr +i "$file" 2>/dev/null || true
    fi

    echo "wrote $file"
}

cmd_remove_source() {
    local file="$1"
    [ -n "$file" ] || die "remove-source: missing path"
    ensure_in_sources_dir "$file"
    [ -f "$file" ] || die "no such source: $file"

    local stem
    stem="$(basename "$file" .sources)"

    # Delete outright — no .bak (Jerry: backups create terminal noise).
    rm -f "$file"
    rm -f "$KEYRINGS_DIR/${stem}.gpg"
    echo "removed $file"
}

cmd_add_ppa() {
    local spec="$1"
    [ -n "$spec" ] || die "add-ppa: missing spec"
    case "$spec" in
        ppa:*/*) ;;
        *) die "add-ppa: spec must be ppa:owner/name (got: $spec)" ;;
    esac
    add-apt-repository -y --no-update "$spec"
    echo "added $spec"
}

cmd_import_key() {
    local dest="$1"
    [ -n "$dest" ] || die "import-key: missing dest"
    ensure_in_keyrings_dir "$dest"
    mkdir -p "$KEYRINGS_DIR"

    local tmp
    tmp="$(mktemp -p "$KEYRINGS_DIR" ".lite-ss-key.XXXXXX")"
    if ! gpg --dearmor --batch --yes -o "$tmp" 2>/dev/null; then
        rm -f "$tmp"
        die "key import failed: gpg --dearmor rejected input"
    fi

    chmod 644 "$tmp"
    chown root:root "$tmp"
    mv "$tmp" "$dest"
    echo "imported $dest"
}

cmd_remove_key() {
    local file="$1"
    [ -n "$file" ] || die "remove-key: missing path"
    ensure_in_keyrings_dir "$file"
    [ -f "$file" ] || die "no such key: $file"
    rm -f "$file"
    echo "removed $file"
}

cmd_update() {
    # Run apt-get as a child so cancel-update can target it. The PID is
    # written to UPDATE_PIDFILE; cancel-update reads it.
    apt-get update &
    local apt_pid=$!
    mkdir -p "$(dirname "$UPDATE_PIDFILE")" 2>/dev/null || true
    echo "$apt_pid" > "$UPDATE_PIDFILE"
    chmod 644 "$UPDATE_PIDFILE" 2>/dev/null || true
    local rc=0
    wait "$apt_pid" || rc=$?
    rm -f "$UPDATE_PIDFILE"
    return $rc
}

cmd_cancel_update() {
    # Kill the in-flight apt-get update, its children, and any apt-helper
    # subprocesses (http/https fetchers). SIGTERM with 3-second grace,
    # then SIGKILL.
    [ -f "$UPDATE_PIDFILE" ] || die "no update in progress"
    local apt_pid
    apt_pid="$(cat "$UPDATE_PIDFILE")"
    [ -n "$apt_pid" ] || die "empty pidfile"

    kill -TERM "$apt_pid" 2>/dev/null || true
    pkill -TERM -P "$apt_pid" 2>/dev/null || true

    local waited=0
    while [ $waited -lt 6 ]; do
        sleep 0.5
        if ! kill -0 "$apt_pid" 2>/dev/null; then
            break
        fi
        waited=$((waited+1))
    done

    pkill -KILL -P "$apt_pid" 2>/dev/null || true
    kill -KILL "$apt_pid" 2>/dev/null || true
    # Belt-and-suspenders: any apt-helper http/https fetchers still alive.
    pkill -KILL -f "/usr/lib/apt/methods/" 2>/dev/null || true

    rm -f "$UPDATE_PIDFILE"
    echo "cancelled"
}

case "${1:-}" in
    write-source)    shift; cmd_write_source    "$@" ;;
    remove-source)   shift; cmd_remove_source   "$@" ;;
    add-ppa)         shift; cmd_add_ppa         "$@" ;;
    import-key)      shift; cmd_import_key      "$@" ;;
    remove-key)      shift; cmd_remove_key      "$@" ;;
    update)          shift; cmd_update          "$@" ;;
    cancel-update)   shift; cmd_cancel_update   "$@" ;;
    *) die "usage: $0 {write-source|remove-source|add-ppa|import-key|remove-key|update|cancel-update} [args...]" ;;
esac
