#!/bin/bash
# alpha-omniforge — root helper behind the OmniForge Studio add-on.
# Manages the shared Android SDK at /opt/android-sdk (root-owned, so www-data
# cannot run sdkmanager itself). Fixed action list, strictly validated package
# specs — never runs an arbitrary command.
#
#   sdk-info                 print SDK root, size and installed packages
#   sdk-available [refresh]  list installable platforms / build-tools / NDKs
#   sdk-install <pkg>        download+install ONE package (detached, logged)
#   sdk-remove  <pkg>        uninstall ONE package
#   sdk-bootstrap            create the SDK from nothing (cmdline-tools + licences)
#   sdk-destroy              delete the whole SDK and its shims
#   sdk-job                  status of the running/last install
#   tool-info                which single-binary toolchains are present
#   tool-install <name>      install ONE toolchain (detached, logged)
#   tool-remove  <name>      remove ONE toolchain
#   tool-job                 status of the running/last toolchain install
#   cache-scan               size every build cache the next build would recreate
#   cache-clear <key|all>    delete ONE such cache (fixed keys, never a path)
set -u

SDK=/opt/android-sdk
SDKMAN="$SDK/cmdline-tools/latest/bin/sdkmanager"
# Gradle distributions live side by side here; /usr/local/bin/gradle points at
# the newest installed one.
GRADLE_ROOT=/opt/gradle
JOBDIR=/var/lib/alpha-omniforge
CACHE="$JOBDIR/available.txt"
LOG="$JOBDIR/job.log"
RC="$JOBDIR/job.rc"
CUR="$JOBDIR/job.pkg"
TLOG="$JOBDIR/tool.log"
TRC="$JOBDIR/tool.rc"
TCUR="$JOBDIR/tool.name"

ACTION="${1:-}"
ARG="${2:-}"

mkdir -p "$JOBDIR" 2>/dev/null
chmod 755 "$JOBDIR" 2>/dev/null

# Only these shapes may ever reach sdkmanager. Anything else is refused, so a
# package spec can't smuggle shell metacharacters or a path.
pkg_ok() {
  case "$1" in
    platforms\;android-[0-9]|platforms\;android-[0-9][0-9]) return 0 ;;
    build-tools\;[0-9]*.[0-9]*.[0-9]*) return 0 ;;
    ndk\;[0-9]*.[0-9]*.[0-9]*) return 0 ;;
    cmake\;[0-9]*.[0-9]*.[0-9]*) return 0 ;;
    platform-tools|cmdline-tools\;latest) return 0 ;;
  esac
  return 1
}
# reject anything with shell-significant characters, whatever the shape said
case "$ARG" in *[\$\`\"\'\\\&\|\<\>\(\)\{\}\[\]\!\*\?" "]*) echo "BAD_PKG"; exit 2 ;; esac

case "$ACTION" in
  sdk-info)
    echo "ROOT $SDK"
    [ -d "$SDK" ] || { echo "MISSING"; exit 0; }
    echo "SIZE $(du -sb "$SDK" 2>/dev/null | awk '{print $1}')"
    command -v java >/dev/null 2>&1 && echo "JAVA $(java -version 2>&1 | head -1 | sed 's/"//g')"
    [ -x "$SDKMAN" ] && echo "SDKMANAGER ok"
    # one line per installed package: KIND<TAB>version<TAB>bytes<TAB>path
    for d in "$SDK"/platforms/android-*; do
      [ -d "$d" ] || continue
      printf 'PKG\tplatform\t%s\t%s\t%s\n' "${d##*android-}" "$(du -sb "$d" 2>/dev/null | awk '{print $1}')" "$d"
    done
    for d in "$SDK"/build-tools/*; do
      [ -d "$d" ] || continue
      printf 'PKG\tbuild-tools\t%s\t%s\t%s\n' "${d##*/}" "$(du -sb "$d" 2>/dev/null | awk '{print $1}')" "$d"
    done
    for d in "$SDK"/ndk/*; do
      [ -d "$d" ] || continue
      printf 'PKG\tndk\t%s\t%s\t%s\n' "${d##*/}" "$(du -sb "$d" 2>/dev/null | awk '{print $1}')" "$d"
    done
    for d in "$SDK"/cmake/*; do
      [ -d "$d" ] || continue
      printf 'PKG\tcmake\t%s\t%s\t%s\n' "${d##*/}" "$(du -sb "$d" 2>/dev/null | awk '{print $1}')" "$d"
    done
    [ -d "$SDK/platform-tools" ] && printf 'PKG\tplatform-tools\t-\t%s\t%s\n' "$(du -sb "$SDK/platform-tools" 2>/dev/null | awk '{print $1}')" "$SDK/platform-tools"
    ;;

  sdk-available)
    # sdkmanager --list hits the network and is slow — cache it, refresh on demand
    if [ "$ARG" = "refresh" ] || [ ! -s "$CACHE" ] || [ -n "$(find "$CACHE" -mmin +1440 2>/dev/null)" ]; then
      [ -x "$SDKMAN" ] || { echo "NO_SDKMANAGER"; exit 2; }
      timeout 180 "$SDKMAN" --list 2>/dev/null | sed -n '/Available Packages/,/Available Updates/p' \
        | grep -oE '^[[:space:]]+(platforms;android-[0-9]+|build-tools;[0-9.]+|ndk;[0-9.]+|cmake;[0-9.]+)' \
        | tr -d ' ' | sort -u > "$CACHE.tmp" 2>/dev/null
      if [ -s "$CACHE.tmp" ]; then mv "$CACHE.tmp" "$CACHE"; chmod 644 "$CACHE"; else rm -f "$CACHE.tmp"; fi
    fi
    [ -s "$CACHE" ] && cat "$CACHE" || echo "EMPTY"
    ;;

  sdk-install)
    pkg_ok "$ARG" || { echo "BAD_PKG"; exit 2; }
    [ -x "$SDKMAN" ] || { echo "NO_SDKMANAGER"; exit 2; }
    if [ -f "$RC" ] || [ ! -f "$CUR" ]; then : ; fi
    # refuse to start a second install while one is running
    if [ -f "$CUR" ] && [ ! -f "$RC" ]; then echo "BUSY: $(cat "$CUR")"; exit 3; fi
    rm -f "$LOG" "$RC"
    printf '%s' "$ARG" > "$CUR"; chmod 644 "$CUR"
    date +%s > "$JOBDIR/job.start"; chmod 644 "$JOBDIR/job.start"
    # detached: an NDK is ~1 GB, far longer than a web request
    setsid bash -c "yes | '$SDKMAN' --install '$ARG' > '$LOG' 2>&1; echo \$? > '$RC'; chmod 644 '$LOG' '$RC'" \
      >/dev/null 2>&1 </dev/null &
    echo "OK: installing $ARG"
    ;;

  sdk-remove)
    pkg_ok "$ARG" || { echo "BAD_PKG"; exit 2; }
    [ -x "$SDKMAN" ] || { echo "NO_SDKMANAGER"; exit 2; }
    # never remove the pieces the builder itself needs to function
    case "$ARG" in platform-tools|cmdline-tools\;latest) echo "PROTECTED: $ARG"; exit 3 ;; esac
    out=$(timeout 300 "$SDKMAN" --uninstall "$ARG" 2>&1); rc=$?
    if [ $rc -eq 0 ]; then echo "OK: removed $ARG"; else echo "FAIL: $(echo "$out" | tail -3)"; exit 6; fi
    ;;

  sdk-job)
    [ -f "$CUR" ] || { echo "STATE idle"; exit 0; }
    echo "PKG $(cat "$CUR" 2>/dev/null)"
    if [ -f "$RC" ]; then
      r=$(cat "$RC" 2>/dev/null)
      [ "$r" = "0" ] && echo "STATE done" || echo "STATE failed"
      echo "CODE $r"
    else
      echo "STATE running"
    fi
    # last progress line sdkmanager printed (it uses \r, so split on those too)
    [ -f "$LOG" ] && echo "TAIL $(tr '\r' '\n' < "$LOG" | grep -v '^[[:space:]]*$' | tail -1 | cut -c1-160)"
    [ -f "$JOBDIR/job.start" ] && echo "START $(cat "$JOBDIR/job.start" 2>/dev/null)"
    if [ -f "$LOG" ]; then
      p=$(tr '\r' '\n' < "$LOG" | grep -oE '[0-9]{1,3}%' | tail -1 | tr -d '%')
      [ -n "$p" ] && echo "PCT $p"
    fi
    ;;


  # ------------------------------------------------------------------ toolchains
  # The single-binary toolchains beside the Android SDK. Each is installed from
  # its own upstream release (resolved at install time, so no version is frozen
  # into this script) and each can be removed again. Only these five names are
  # accepted; there is no path or URL input anywhere.
  tool-info)
    for t in buildtools bundletool java zsign repak flutter; do
      case "$t" in
        buildtools) [ -x /usr/local/bin/aapt2 ] && [ -x /usr/local/bin/apksigner ] && [ -x /usr/local/bin/zipalign ] \
                      && echo "buildtools installed" || echo "buildtools missing" ;;
        bundletool) [ -f /usr/local/lib/bundletool.jar ] && echo "bundletool installed" || echo "bundletool missing" ;;
        java)       command -v java >/dev/null 2>&1 && echo "java installed" || echo "java missing" ;;
        zsign)      [ -x /usr/local/bin/zsign ] && echo "zsign installed" || echo "zsign missing" ;;
        repak)      [ -x /usr/local/bin/repak ] && echo "repak installed" || echo "repak missing" ;;
        flutter)    [ -x /opt/flutter/bin/flutter ] && echo "flutter installed" || echo "flutter missing" ;;
      esac
    done
    ;;

  # ---------------------------------------------------- language toolchains
  # One line per compiler: NAME<TAB>path-or-'-'<TAB>version-or-''.
  # ⚠️ Probed with `command -v`, i.e. what is actually on PATH for a normal
  # process — NOT what happens to exist under /root. The panel compiles as
  # www-data, and a toolchain it cannot reach is a toolchain that is not there.
  lang-info)
    for t in gcc g++ cmake cargo rustc kotlinc swiftc python3 php java git make; do
      p="$(command -v "$t" 2>/dev/null || true)"
      if [ -z "$p" ]; then printf '%s\t-\t\n' "$t"; continue; fi
      case "$t" in
        gcc|g++)   v="$("$t" -dumpfullversion 2>/dev/null || "$t" -dumpversion 2>/dev/null)" ;;
        cmake)     v="$(cmake --version 2>/dev/null | head -1 | awk '{print $3}')" ;;
        cargo|rustc) v="$("$t" --version 2>/dev/null | awk '{print $2}')" ;;
        kotlinc)   v="$(kotlinc -version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" ;;
        swiftc)    v="$(swiftc --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)" ;;
        python3)   v="$(python3 -V 2>&1 | awk '{print $2}')" ;;
        php)       v="$(php -r 'echo PHP_VERSION;' 2>/dev/null)" ;;
        java)      v="$(java -version 2>&1 | head -1 | grep -oE '[0-9]+(\.[0-9]+)*' | head -1)" ;;
        git)       v="$(git --version 2>/dev/null | awk '{print $3}')" ;;
        make)      v="$(make --version 2>/dev/null | head -1 | awk '{print $3}')" ;;
        *)         v="" ;;
      esac
      printf '%s\t%s\t%s\n' "$t" "$p" "${v:-}"
    done
    # Disk each managed tree costs, so the UI can say what a removal frees.
    for d in /opt/rustup /opt/cargo /opt/kotlinc /opt/swift; do
      [ -d "$d" ] && printf 'SIZE\t%s\t%s\n' "$d" "$(du -sb "$d" 2>/dev/null | cut -f1)"
    done
    exit 0
    ;;

  tool-install)
    case "$ARG" in buildtools|bundletool|java|zsign|repak|flutter|gcc|cmake|rust|kotlin|swift) ;; *) echo "BAD_TOOL"; exit 2 ;; esac
    if [ -f "$TCUR" ] && [ ! -f "$TRC" ]; then echo "BUSY: $(cat "$TCUR")"; exit 3; fi
    rm -f "$TLOG" "$TRC"
    printf '%s' "$ARG" > "$TCUR"; chmod 644 "$TCUR"
    date +%s > "$JOBDIR/tool.start"; chmod 644 "$JOBDIR/tool.start"
    setsid bash -c '
      set -u
      T="'"$ARG"'"
      log() { printf "%s\n" "$*" >> "'"$TLOG"'"; }
      rc=1
      case "$T" in
        java)
          log "installing a headless JRE from the distribution"
          export DEBIAN_FRONTEND=noninteractive
          apt-get update -qq >> "'"$TLOG"'" 2>&1
          apt-get install -y -qq default-jre-headless >> "'"$TLOG"'" 2>&1 && rc=0
          ;;
        buildtools)
          # aapt2/apksigner/zipalign live inside the Android SDK build-tools; the
          # builder expects them on PATH, so publish the newest revision.
          BT=$(ls -1d /opt/android-sdk/build-tools/*/ 2>/dev/null | sort -V | tail -1)
          if [ -z "$BT" ]; then
            log "no Android build-tools installed yet — add one from the Android SDK card below first"
          else
            log "linking $BT into /usr/local/bin"
            ok=1
            for b in aapt2 apksigner zipalign; do
              if [ -x "$BT$b" ]; then ln -sfn "$BT$b" "/usr/local/bin/$b"; log "  $b"; else log "  $b MISSING in $BT"; ok=0; fi
            done
            [ "$ok" = 1 ] && rc=0
          fi
          ;;
        bundletool)
          U=$(curl -sL --max-time 30 https://api.github.com/repos/google/bundletool/releases/latest \
              | grep -oE "https://[^\"]*bundletool-all-[0-9.]+\.jar" | head -1)
          if [ -z "$U" ]; then log "could not resolve the bundletool release URL"; else
            log "downloading $U"
            mkdir -p /usr/local/lib
            curl -fSL -# --max-time 300 -o /usr/local/lib/bundletool.jar.tmp "$U" >> "'"$TLOG"'" 2>&1 \
              && mv -f /usr/local/lib/bundletool.jar.tmp /usr/local/lib/bundletool.jar \
              && chmod 644 /usr/local/lib/bundletool.jar && rc=0
            rm -f /usr/local/lib/bundletool.jar.tmp
          fi
          ;;
        zsign)
          U=$(curl -sL --max-time 30 https://api.github.com/repos/zhlynn/zsign/releases/latest \
              | grep -oE "https://[^\"]*zsign-linux-x86_64\.tar\.gz" | head -1)
          if [ -z "$U" ]; then log "could not resolve the zsign release URL"; else
            log "downloading $U"
            D=$(mktemp -d)
            curl -fSL -# --max-time 300 -o "$D/z.tgz" "$U" >> "'"$TLOG"'" 2>&1 \
              && tar xzf "$D/z.tgz" -C "$D" >> "'"$TLOG"'" 2>&1 \
              && B=$(find "$D" -type f -name zsign | head -1) \
              && [ -n "$B" ] && install -m 0755 "$B" /usr/local/bin/zsign && rc=0
            rm -rf "$D"
          fi
          ;;
        flutter)
          # The stable SDK, straight from Google. Big (~1 GB unpacked) because it
          # carries the Dart SDK and the engine artifacts, and `precache` pulls
          # the Android ones so the first real build is not a surprise download.
          log "downloading the Flutter stable SDK"
          if [ -d /opt/flutter ]; then
            log "/opt/flutter already exists — updating it instead"
            (cd /opt/flutter && git pull --ff-only >> "'"$TLOG"'" 2>&1) && rc=0
          else
            command -v git >/dev/null 2>&1 || { log "installing git"; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git >> "'"$TLOG"'" 2>&1; }
            # NEVER shallow-clone flutter: the tool resolves its version from git
            # tags and un-shallows the repo itself on first run — a full clone up
            # front is the same download without the surprise stall.
            # --progress: git suppresses its NN% lines when stderr is not a tty
            if git clone --progress -b stable https://github.com/flutter/flutter.git /opt/flutter >> "'"$TLOG"'" 2>&1; then
              # git refuses to run in a tree it does not own; the builds run as www-data
              git config --global --add safe.directory /opt/flutter >/dev/null 2>&1 || true
              chmod -R a+rX /opt/flutter 2>/dev/null || true
              log "fetching the Android engine artifacts (this is the slow part)"
              /opt/flutter/bin/flutter --version >> "'"$TLOG"'" 2>&1 || true
              /opt/flutter/bin/flutter precache --android --no-ios >> "'"$TLOG"'" 2>&1 || true
              # flutter SELF-MUTATES its SDK tree (bin/cache, packages/*/.dart_tool,
              # engine stamps) — and the panel builds as www-data, so the whole
              # toolchain belongs to www-data. Piecemeal chowns of just the cache
              # left root-owned .dart_tool dirs that broke `flutter create`.
              chown -R www-data:www-data /opt/flutter 2>/dev/null || true
              ln -sfn /opt/flutter/bin/flutter /usr/local/bin/flutter
              [ -x /opt/flutter/bin/flutter ] && rc=0
            fi
          fi
          ;;
        repak)
          U=$(curl -sL --max-time 30 https://api.github.com/repos/trumank/repak/releases/latest \
              | grep -oE "https://github.com/[^\"]*repak_cli-x86_64-unknown-linux-gnu\.tar\.xz" | head -1)
          if [ -z "$U" ]; then log "could not resolve the repak release URL"; else
            log "downloading $U"
            D=$(mktemp -d)
            curl -fSL -# --max-time 300 -o "$D/r.txz" "$U" >> "'"$TLOG"'" 2>&1 \
              && tar xJf "$D/r.txz" -C "$D" >> "'"$TLOG"'" 2>&1 \
              && B=$(find "$D" -type f -name repak | head -1) \
              && [ -n "$B" ] && install -m 0755 "$B" /usr/local/bin/repak && rc=0
            rm -rf "$D"
          fi
          ;;

        # ---------------- language toolchains ----------------
        # These are what the AlphaSDK release gate compiles with, and what a
        # build-from-source project needs.
        #
        # ⚠️ EVERY ONE INSTALLS SOMEWHERE WORLD-READABLE AND SYMLINKS INTO
        # /usr/local/bin. The panel compiles as www-data; a toolchain under
        # /root is invisible to it, and the failure looks like a compile error
        # rather than a permission one. This was learned the hard way with a
        # rustup install that worked perfectly from a root shell.
        gcc)
          log "installing the C/C++ toolchain from the distribution"
          export DEBIAN_FRONTEND=noninteractive
          apt-get update -qq >> "'"$TLOG"'" 2>&1
          apt-get install -y -qq build-essential libcurl4-openssl-dev >> "'"$TLOG"'" 2>&1 && rc=0
          ;;
        cmake)
          log "installing cmake"
          export DEBIAN_FRONTEND=noninteractive
          apt-get update -qq >> "'"$TLOG"'" 2>&1
          apt-get install -y -qq cmake >> "'"$TLOG"'" 2>&1 && rc=0
          ;;
        rust)
          # ⚠️ NOT the distribution package. Ubuntu ships a rustc that is years
          # old, and a modern dependency tree refuses to build on it ("feature
          # `edition2024` is required"). rustup gets current stable.
          log "installing the Rust toolchain (rustup, current stable)"
          export RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo
          mkdir -p /opt/rustup /opt/cargo
          if curl -fsSL --max-time 120 https://sh.rustup.rs -o /tmp/alpha-rustup.sh >> "'"$TLOG"'" 2>&1 \
             && sh /tmp/alpha-rustup.sh -y --no-modify-path --profile minimal --default-toolchain stable >> "'"$TLOG"'" 2>&1; then
            TC=$(ls -d /opt/rustup/toolchains/stable-* 2>/dev/null | head -1)
            if [ -n "$TC" ]; then
              # Link the REAL binaries, not the rustup proxies — the proxies need
              # RUSTUP_HOME in the environment and the panel does not set one.
              # ⚠️ rustdoc too: without it every unit test passes and `cargo test`
              # still fails, on the doc tests alone.
              for b in cargo rustc rustdoc; do
                [ -x "$TC/bin/$b" ] && ln -sfn "$TC/bin/$b" "/usr/local/bin/$b" && log "  linked $b"
              done
              chmod -R a+rX /opt/rustup /opt/cargo 2>/dev/null || true
              # cargo needs a writable home for its registry cache, and a target
              # dir outside the source tree or it leaves ~300 MB inside it.
              mkdir -p /var/www/alpha-private/cargo /var/www/alpha-private/cargo-target
              chown www-data:www-data /var/www/alpha-private/cargo /var/www/alpha-private/cargo-target
              [ -x /usr/local/bin/cargo ] && rc=0
            else
              log "rustup finished but no stable toolchain landed in /opt/rustup"
            fi
          fi
          rm -f /tmp/alpha-rustup.sh
          ;;
        kotlin)
          # ⚠️ NOT the snap. snapd refuses to start for a user whose home is
          # outside /home, and the www-data home is /var/www — so a snap install
          # succeeds and then fails for the only user that matters here.
          # ⚠️ No apostrophes anywhere in this block: it lives inside a
          # single-quoted bash -c string, and one would end the string.
          log "resolving the latest Kotlin compiler release"
          U=$(curl -sL --max-time 30 https://api.github.com/repos/JetBrains/kotlin/releases/latest \
              | grep -oE "https://github.com/[^\"]*kotlin-compiler-[0-9.]+\.zip" | head -1)
          [ -n "$U" ] || U="https://github.com/JetBrains/kotlin/releases/download/v2.1.21/kotlin-compiler-2.1.21.zip"
          log "downloading $U"
          command -v unzip >/dev/null 2>&1 || { DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unzip >> "'"$TLOG"'" 2>&1; }
          # kotlinc is a JVM program — without a JRE it installs and then cannot run
          command -v java >/dev/null 2>&1 || { log "installing a JRE for kotlinc"; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq default-jre-headless >> "'"$TLOG"'" 2>&1; }
          D=$(mktemp -d)
          if curl -fSL -# --max-time 900 -o "$D/k.zip" "$U" >> "'"$TLOG"'" 2>&1 \
             && rm -rf /opt/kotlinc && unzip -q "$D/k.zip" -d /opt >> "'"$TLOG"'" 2>&1 \
             && [ -x /opt/kotlinc/bin/kotlinc ]; then
            chmod -R a+rX /opt/kotlinc
            ln -sfn /opt/kotlinc/bin/kotlinc /usr/local/bin/kotlinc
            ln -sfn /opt/kotlinc/bin/kotlin  /usr/local/bin/kotlin
            # org.json is in the Android framework but not on a plain JVM, and the
            # SDK gate compiles against it.
            mkdir -p /opt/alphasdk-libs
            curl -fsSL --max-time 120 -o /opt/alphasdk-libs/json.jar \
              https://repo1.maven.org/maven2/org/json/json/20240303/json-20240303.jar >> "'"$TLOG"'" 2>&1 || true
            chmod -R a+rX /opt/alphasdk-libs 2>/dev/null || true
            rc=0
          fi
          rm -rf "$D"
          ;;
        swift)
          # ~750 MB download, ~2 GB unpacked. The tarball is the only supported
          # way to get Swift on Ubuntu.
          . /etc/os-release 2>/dev/null || true
          UV="${VERSION_ID:-24.04}"
          SV="6.0.3"
          U="https://download.swift.org/swift-${SV}-release/ubuntu$(echo "$UV" | tr -d .)/swift-${SV}-RELEASE/swift-${SV}-RELEASE-ubuntu${UV}.tar.gz"
          log "installing Swift ${SV} for Ubuntu ${UV}"
          export DEBIAN_FRONTEND=noninteractive
          apt-get install -y -qq binutils libc6-dev libcurl4-openssl-dev libedit2 libgcc-s1 \
            libpython3-dev libsqlite3-0 libstdc++-13-dev libxml2-dev libz3-dev pkg-config tzdata zlib1g-dev >> "'"$TLOG"'" 2>&1 || true
          mkdir -p /opt/swift
          log "downloading $U"
          if curl -fSL -# --max-time 1800 -o /opt/swift/swift.tar.gz "$U" >> "'"$TLOG"'" 2>&1 \
             && tar xzf /opt/swift/swift.tar.gz -C /opt/swift --strip-components=1 >> "'"$TLOG"'" 2>&1; then
            rm -f /opt/swift/swift.tar.gz
            chmod -R a+rX /opt/swift 2>/dev/null || true
            for b in swift swiftc; do
              [ -x "/opt/swift/usr/bin/$b" ] && ln -sfn "/opt/swift/usr/bin/$b" "/usr/local/bin/$b"
            done
            [ -x /usr/local/bin/swiftc ] && rc=0
          fi
          rm -f /opt/swift/swift.tar.gz
          ;;
      esac
      log "exit $rc"
      echo "$rc" > "'"$TRC"'"; chmod 644 "'"$TLOG"'" "'"$TRC"'" 2>/dev/null
    ' >/dev/null 2>&1 </dev/null &
    echo "OK: installing $ARG"
    ;;

  tool-remove)
    case "$ARG" in buildtools|bundletool|java|zsign|repak|flutter|gcc|cmake|rust|kotlin|swift) ;; *) echo "BAD_TOOL"; exit 2 ;; esac
    case "$ARG" in
      buildtools) rm -f /usr/local/bin/aapt2 /usr/local/bin/apksigner /usr/local/bin/zipalign; echo "OK: removed buildtools" ;;
      bundletool) rm -f /usr/local/lib/bundletool.jar; echo "OK: removed bundletool" ;;
      zsign)      rm -f /usr/local/bin/zsign; echo "OK: removed zsign" ;;
      repak)      rm -f /usr/local/bin/repak; echo "OK: removed repak" ;;
      flutter)    rm -rf /opt/flutter; rm -f /usr/local/bin/flutter; echo "OK: removed flutter" ;;
      rust)
        rm -f /usr/local/bin/cargo /usr/local/bin/rustc /usr/local/bin/rustdoc
        rm -rf /opt/rustup /opt/cargo
        # The registry cache and build output are scratch, not data.
        rm -rf /var/www/alpha-private/cargo-target
        echo "OK: removed rust"
        ;;
      kotlin)
        rm -f /usr/local/bin/kotlinc /usr/local/bin/kotlin
        rm -rf /opt/kotlinc /opt/alphasdk-libs
        echo "OK: removed kotlin"
        ;;
      swift)
        rm -f /usr/local/bin/swift /usr/local/bin/swiftc
        rm -rf /opt/swift
        echo "OK: removed swift"
        ;;
      cmake)
        DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq cmake >/dev/null 2>&1
        echo "OK: removed cmake"
        ;;
      gcc)
        # ⚠️ REFUSED, deliberately. build-essential is a dependency of half the
        # distribution: apt would take php extensions, the kernel headers and
        # anything else that build-depends on it with it. Removing a compiler is
        # never worth breaking the box that serves this page.
        echo "PROTECTED: removing the C toolchain would drag out half the distribution"; exit 3
        ;;
      java)
        # sdkmanager, bundletool and apksigner all need a JVM — refuse while the
        # Android SDK is installed rather than silently breaking every APK build.
        if [ -d /opt/android-sdk/platforms ] && [ -n "$(ls -A /opt/android-sdk/platforms 2>/dev/null)" ]; then
          echo "PROTECTED: the Android SDK needs Java"; exit 3
        fi
        DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq default-jre-headless >/dev/null 2>&1
        echo "OK: removed java"
        ;;
    esac
    ;;

  # ------------------------------------------------------------------ gradle
  # Gradle is what actually COMPILES a Kotlin/Java app — aapt2 and apksigner
  # only package and sign what a compiler already produced. Several versions may
  # sit side by side because the Android Gradle Plugin pins a minimum: AGP 8.5
  # needs Gradle 8.7+, while an older project may want an older one. The newest
  # installed version owns /usr/local/bin/gradle.
  gradle-list)
    echo "ROOT $GRADLE_ROOT"
    for d in "$GRADLE_ROOT"/gradle-*/; do
      [ -d "$d" ] || continue
      v="$(basename "$d")"; v="${v#gradle-}"
      sz=$(du -sb "$d" 2>/dev/null | cut -f1)
      echo "VER	$v	${sz:-0}"
    done
    [ -L /usr/local/bin/gradle ] && echo "ACTIVE $(readlink -f /usr/local/bin/gradle)"
    exit 0
    ;;

  gradle-install)
    # only a plain version number ever reaches the URL
    case "$ARG" in
      [0-9].[0-9]|[0-9].[0-9].[0-9]|[0-9].[0-9][0-9]|[0-9].[0-9][0-9].[0-9]|[0-9][0-9].[0-9]|[0-9][0-9].[0-9].[0-9]) ;;
      *) echo "BAD_VERSION"; exit 2 ;;
    esac
    [ -d "$GRADLE_ROOT/gradle-$ARG" ] && { echo "OK: $ARG is already installed"; exit 0; }
    if [ -f "$TCUR" ] && [ ! -f "$TRC" ]; then echo "BUSY: $(cat "$TCUR")"; exit 3; fi
    rm -f "$TLOG" "$TRC"
    printf 'gradle %s' "$ARG" > "$TCUR"; chmod 644 "$TCUR"
    date +%s > "$JOBDIR/tool.start"; chmod 644 "$JOBDIR/tool.start"
    setsid bash -c '
      set -u
      V="'"$ARG"'"
      R="'"$GRADLE_ROOT"'"
      log() { printf "%s\n" "$*" >> "'"$TLOG"'"; }
      rc=1
      U="https://services.gradle.org/distributions/gradle-$V-bin.zip"
      log "downloading $U"
      mkdir -p "$R"
      D=$(mktemp -d)
      # -# makes curl print a NN% progress bar into the log, which the UI reads
      if curl -fSL -# --max-time 900 -o "$D/g.zip" "$U" >> "'"$TLOG"'" 2>&1; then
        command -v unzip >/dev/null 2>&1 || { log "installing unzip"; DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unzip >> "'"$TLOG"'" 2>&1; }
        if unzip -q -o "$D/g.zip" -d "$R" >> "'"$TLOG"'" 2>&1 && [ -x "$R/gradle-$V/bin/gradle" ]; then
          # newest installed wins the PATH entry, so a fresh install of an OLDER
          # version never quietly downgrades every build on the box
          N=$(ls -1d "$R"/gradle-*/ 2>/dev/null | sed "s#.*/gradle-##;s#/##" | sort -V | tail -1)
          ln -sfn "$R/gradle-$N/bin/gradle" /usr/local/bin/gradle
          log "installed $V; active version is now $N"
          rc=0
        else
          log "the archive did not unpack into $R/gradle-$V"
        fi
      else
        log "download failed — is $V a real Gradle release?"
      fi
      rm -rf "$D"
      log "exit $rc"
      echo "$rc" > "'"$TRC"'"; chmod 644 "'"$TLOG"'" "'"$TRC"'" 2>/dev/null
    ' >/dev/null 2>&1 </dev/null &
    echo "OK: installing gradle $ARG"
    ;;

  gradle-remove)
    case "$ARG" in
      [0-9].[0-9]|[0-9].[0-9].[0-9]|[0-9].[0-9][0-9]|[0-9].[0-9][0-9].[0-9]|[0-9][0-9].[0-9]|[0-9][0-9].[0-9].[0-9]) ;;
      *) echo "BAD_VERSION"; exit 2 ;;
    esac
    [ -d "$GRADLE_ROOT/gradle-$ARG" ] || { echo "NOT_INSTALLED"; exit 2; }
    rm -rf "${GRADLE_ROOT:?}/gradle-$ARG"
    # re-point (or drop) the PATH entry so it can never dangle
    N=$(ls -1d "$GRADLE_ROOT"/gradle-*/ 2>/dev/null | sed "s#.*/gradle-##;s#/##" | sort -V | tail -1)
    if [ -n "${N:-}" ]; then
      ln -sfn "$GRADLE_ROOT/gradle-$N/bin/gradle" /usr/local/bin/gradle
      echo "OK: removed $ARG; active version is now $N"
    else
      rm -f /usr/local/bin/gradle
      echo "OK: removed $ARG; no Gradle remains"
    fi
    ;;

  tool-job)
    [ -f "$TCUR" ] || { echo "STATE idle"; exit 0; }
    echo "TOOL $(cat "$TCUR" 2>/dev/null)"
    if [ -f "$TRC" ]; then
      r=$(cat "$TRC" 2>/dev/null)
      [ "$r" = "0" ] && echo "STATE done" || echo "STATE failed"
      echo "CODE $r"
    else
      echo "STATE running"
    fi
    [ -f "$TLOG" ] && echo "TAIL $(tr '\r' '\n' < "$TLOG" | grep -v '^[[:space:]]*$' | tail -1 | cut -c1-160)"
    # progress: last NN% in the log (curl -# / git --progress / sdkmanager), or
    # a [n/m] step counter (flutter precache) mapped onto 0-100
    [ -f "$JOBDIR/tool.start" ] && echo "START $(cat "$JOBDIR/tool.start" 2>/dev/null)"
    if [ -f "$TLOG" ]; then
      p=$(tr '\r' '\n' < "$TLOG" | grep -oE '[0-9]{1,3}(\.[0-9])?%' | tail -1 | tr -d '%' | cut -d. -f1)
      if [ -z "$p" ]; then
        fr=$(tr '\r' '\n' < "$TLOG" | grep -oE '\[[0-9]+/[0-9]+\]' | tail -1 | tr -d '[]')
        case "$fr" in
          */*) d=${fr##*/}; [ "$d" -gt 0 ] 2>/dev/null && p=$(( ${fr%%/*} * 100 / d )) ;;
        esac
      fi
      [ -n "$p" ] && echo "PCT $p"
    fi
    ;;


  # ---------------------------------------------------------------- bootstrap
  # Create the SDK from nothing. Without this there was no way in on a fresh
  # server: /opt/android-sdk did not exist, so sdkmanager did not exist, so no
  # package could ever be installed — and the build-tools shims that depend on
  # them were unreachable too. Downloads Google's command-line tools, accepts
  # the licences, then pulls platform-tools.
  sdk-bootstrap)
    if [ -x "$SDKMAN" ]; then echo "OK: already bootstrapped"; exit 0; fi
    if [ -f "$CUR" ] && [ ! -f "$RC" ]; then echo "BUSY: $(cat "$CUR")"; exit 3; fi
    rm -f "$LOG" "$RC"
    printf '%s' "cmdline-tools;latest" > "$CUR"; chmod 644 "$CUR"
    setsid bash -c '
      set -u
      L="'"$LOG"'"
      log() { printf "%s
" "$*" >> "$L"; }
      rc=1
      # sdkmanager is a Java program — no JRE, no SDK.
      if ! command -v java >/dev/null 2>&1; then
        log "installing a headless JRE first (sdkmanager needs one)"
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -qq >> "$L" 2>&1
        apt-get install -y -qq default-jre-headless >> "$L" 2>&1
      fi
      command -v unzip >/dev/null 2>&1 || { export DEBIAN_FRONTEND=noninteractive; apt-get install -y -qq unzip >> "$L" 2>&1; }

      log "resolving the newest command-line tools from Google"
      X=$(mktemp)
      curl -fsSL --max-time 120 https://dl.google.com/android/repository/repository2-3.xml -o "$X" 2>>"$L"
      # sort -V, NOT plain sort: lexically "9862592" sorts after "15859902",
      # which would fetch a years-old build of the tools.
      Z=$(grep -oE "commandlinetools-linux-[0-9]+_latest[.]zip" "$X" | sort -u -V | tail -1)
      rm -f "$X"
      if [ -z "$Z" ]; then
        log "could not resolve the download URL"
      else
        log "downloading $Z"
        D=$(mktemp -d)
        if curl -fsSL --max-time 900 -o "$D/cmdline.zip" "https://dl.google.com/android/repository/$Z" >>"$L" 2>&1; then
          mkdir -p /opt/android-sdk/cmdline-tools
          log "unpacking"
          if unzip -q -o "$D/cmdline.zip" -d "$D" >>"$L" 2>&1 && [ -d "$D/cmdline-tools" ]; then
            rm -rf /opt/android-sdk/cmdline-tools/latest
            mv "$D/cmdline-tools" /opt/android-sdk/cmdline-tools/latest
            chmod -R a+rX /opt/android-sdk
            log "accepting licences"
            yes | /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses >>"$L" 2>&1
            log "installing platform-tools"
            yes | /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager --install "platform-tools" >>"$L" 2>&1
            [ -x /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager ] && rc=0
          else
            log "the archive did not contain cmdline-tools/"
          fi
        else
          log "download failed"
        fi
        rm -rf "$D"
      fi
      log "exit $rc"
      echo "$rc" > "'"$RC"'"; chmod 644 "$L" "'"$RC"'" 2>/dev/null
    ' >/dev/null 2>&1 </dev/null &
    echo "OK: bootstrapping the Android SDK"
    ;;

  # ---- build caches -------------------------------------------------------
  # Everything listed here is REGENERATED by the next build: Gradle re-downloads
  # its dependencies and wrapper, Flutter re-runs `pub get`, and per-project
  # outputs are rebuilt from source. Clearing frees space without breaking
  # anything except the speed of the first build afterwards.
  # Never touched: project source, built APKs (builds bucket), release
  # keystores, the debug signing key in .android/ (a new one would stop an
  # installed debug app from updating), gradle.properties (the JVM cap), the
  # Android SDK / Gradle / Flutter installs, and /root/build (the product
  # builder's staging tree — alpha-distbuild owns it).
  # The caller only ever names a KEY; every path is fixed in this file.
  cache-scan|cache-clear)
    BH=/var/www/alpha-private/omniforge-buildhome
    PROJ="/var/www/Addons/OmniForge Studio"
    KEYS="gradle-caches gradle-wrapper gradle-state pub-cache android-cache project-outputs root-gradle root-pub-cache root-android-cache root-androidbuild"

    # the generated dirs inside a GRADLE_USER_HOME (never its *.properties / init.d)
    gradle_parts() { for x in caches wrapper daemon native kotlin-profile notifications .tmp; do printf '%s\n' "$1/$x"; done; }
    # Output dirs that sit beside a Gradle/Flutter build file. Searching for the
    # build FILE (not for any dir called "build") keeps a web project's own
    # build/ folder — which may be source — out of reach. find does not follow
    # symlinks, so Flutter's ios/.symlinks into the pub cache are never walked.
    out_dirs() {
      [ -d "$1" ] || return 0
      find "$1" -xdev -maxdepth 6 -type f \
        \( -name settings.gradle -o -name settings.gradle.kts -o -name build.gradle -o -name build.gradle.kts -o -name pubspec.yaml \) \
        -not -path '*/build/*' -not -path '*/.gradle/*' -not -path '*/.dart_tool/*' -not -path '*/node_modules/*' \
        -printf '%h\n' 2>/dev/null | sort -u | while IFS= read -r d; do
          for o in build .gradle .cxx .dart_tool .kotlin .externalNativeBuild; do
            [ -d "$d/$o" ] && [ ! -L "$d/$o" ] && printf '%s\n' "$d/$o"
          done
        done
    }
    cache_paths() {
      case "$1" in
        gradle-caches)      printf '%s\n' "$BH/gradle/caches" ;;
        gradle-wrapper)     printf '%s\n' "$BH/gradle/wrapper" ;;
        gradle-state)       printf '%s\n' "$BH/gradle/daemon" "$BH/gradle/native" "$BH/gradle/kotlin-profile" "$BH/gradle/notifications" "$BH/gradle/.tmp" "$BH/.kotlin" ;;
        pub-cache)          printf '%s\n' "$BH/.pub-cache" ;;
        android-cache)      printf '%s\n' "$BH/.android/cache" ;;
        project-outputs)    out_dirs "$PROJ" ;;
        root-gradle)        gradle_parts /root/.gradle ;;
        root-pub-cache)     printf '%s\n' /root/.pub-cache ;;
        # bin/ + cli/ = the launcher and bundled JRE that newer cmdline-tools
        # unpack whenever sdkmanager runs as root (~250 MB, re-unpacked on demand)
        root-android-cache) printf '%s\n' /root/.android/cache /root/.android/bin /root/.android/cli ;;
        root-androidbuild)  out_dirs /root/androidbuild ;;
      esac
    }
    # belt and braces: whatever cache_paths produced must still sit under one
    # of these roots, with no .. anywhere, before rm ever sees it
    path_ok() {
      case "$1" in *"/../"*|*"/.."|"") return 1 ;; esac
      case "$1" in
        "$BH"/gradle/?*|"$BH"/.pub-cache|"$BH"/.android/cache|"$BH"/.kotlin) return 0 ;;
        "$PROJ"/?*/?*|/root/androidbuild/?*/?*)
          # inside a source tree only a known OUTPUT folder may ever go
          case "${1##*/}" in build|.gradle|.cxx|.dart_tool|.kotlin|.externalNativeBuild) return 0 ;; esac
          return 1 ;;
        /root/.gradle/?*|/root/.pub-cache|/root/.android/cache|/root/.android/bin|/root/.android/cli) return 0 ;;
      esac
      return 1
    }
    # A running build would crash mid-way if its cache vanished. Idle daemons
    # are not builds: they only hold memory and file locks, so they are stopped.
    build_running() { pgrep -f 'GradleWrapperMain|org\.gradle\.launcher\.GradleMain|flutter_tools\.snapshot' >/dev/null 2>&1; }

    if [ "$ACTION" = cache-scan ]; then
      build_running && echo "BUSY"
      for k in $KEYS; do
        echo "KEY	$k"
        cache_paths "$k" | while IFS= read -r p; do
          [ -e "$p" ] || continue
          path_ok "$p" || continue
          b=$(du -sb -- "$p" 2>/dev/null | cut -f1)
          printf 'ITEM\t%s\t%s\t%s\n' "$k" "${b:-0}" "$p"
        done
      done
      exit 0
    fi

    case "$ARG" in all) SEL="$KEYS" ;; *)
      case " $KEYS " in *" $ARG "*) SEL="$ARG" ;; *) echo "BAD_KEY"; exit 2 ;; esac ;;
    esac
    if build_running; then echo "BUSY: a build is running"; exit 3; fi
    pkill -f 'org\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon' 2>/dev/null
    pkill -f 'KotlinCompileDaemon' 2>/dev/null
    freed=0; n=0
    for k in $SEL; do
      while IFS= read -r p; do
        [ -e "$p" ] || continue
        path_ok "$p" || { echo "SKIP $p"; continue; }
        b=$(du -sb -- "$p" 2>/dev/null | cut -f1)
        rm -rf -- "$p" && { freed=$((freed + ${b:-0})); n=$((n + 1)); }
      done < <(cache_paths "$k")
    done
    echo "OK	$freed	$n"
    ;;

  sdk-destroy)
    # Remove the whole SDK — the only way to reclaim the several GB it uses.
    [ -d "$SDK" ] || { echo "OK: nothing to remove"; exit 0; }
    rm -rf "$SDK"
    rm -f /usr/local/bin/aapt2 /usr/local/bin/apksigner /usr/local/bin/zipalign
    echo "OK: removed the Android SDK"
    ;;

  *) echo "BAD_ACTION: $ACTION"; exit 1 ;;
esac
