#!/usr/bin/env bash
# Build a self-signed Android APK from solitaire_app's cdylib targets.
#
# Replaces the cargo-apk pipeline with explicit cargo-ndk + aapt2 + apksigner
# steps. The CI runner was hitting an SDK-discovery bug inside cargo-apk's
# ndk-build crate that we couldn't isolate; running each Android toolchain
# step explicitly gives us a debuggable pipeline.
#
# Environment:
#   ANDROID_HOME            Path to Android SDK root. If unset, common SDK
#                           locations such as ~/Android/Sdk are tried.
#   ANDROID_NDK_HOME        Path to the specific NDK version. If unset, the
#                           newest $ANDROID_HOME/ndk/* directory is used.
#   BUILD_TOOLS_VERSION     e.g. "34.0.0". If unset, newest installed build-tools
#                           version is used.
#   PLATFORM                e.g. "android-34". If unset, newest installed
#                           $ANDROID_HOME/platforms/android-* platform is used.
#
# Optional environment:
#   PROFILE                 "debug" (default) | "release"
#   ABIS                    Space-separated Android ABIs to build (default:
#                           "arm64-v8a armeabi-v7a x86_64"). Reduce in CI to
#                           fit the runner's disk budget — a full three-ABI
#                           debug build can exceed 25 GB of target/ output.
#   APK_OUT                 Output APK path (default: target/$PROFILE/apk/ferrous-solitaire.apk)
#   STRIP_NATIVE_LIBS       1 to strip .so files before packaging (default: 1)
#   KEYSTORE                Path to keystore for signing (default: target/android/debug.keystore)
#   KEYSTORE_PASS           Keystore password (default: "android" for the generated debug keystore)
#   KEY_ALIAS               Key alias (default: "androiddebugkey")
#   KEY_PASS                Key password (default: same as KEYSTORE_PASS)
#
# Outputs:
#   $APK_OUT                Signed, zipaligned APK
set -euo pipefail

infer_latest_dir_name() {
  local pattern="$1"
  local latest=""
  shopt -s nullglob
  local dirs=( $pattern )
  shopt -u nullglob
  if [ ${#dirs[@]} -gt 0 ]; then
    latest="$(printf '%s\n' "${dirs[@]}" | sort -V | tail -n 1)"
    basename "$latest"
  fi
}

if [ -z "${ANDROID_HOME:-}" ]; then
  for candidate in "$HOME/Android/Sdk" "$HOME/Library/Android/sdk" "/opt/android-sdk" "/usr/lib/android-sdk"; do
    if [ -d "$candidate" ]; then
      ANDROID_HOME="$candidate"
      export ANDROID_HOME
      break
    fi
  done
fi
: "${ANDROID_HOME:?ANDROID_HOME must be set or discoverable under a common SDK path}"

if [ -z "${ANDROID_NDK_HOME:-}" ]; then
  NDK_VERSION="$(infer_latest_dir_name "$ANDROID_HOME/ndk/*")"
  if [ -n "$NDK_VERSION" ]; then
    ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION"
    export ANDROID_NDK_HOME
  fi
fi
: "${ANDROID_NDK_HOME:?ANDROID_NDK_HOME must be set or discoverable under ANDROID_HOME/ndk}"

if [ -z "${BUILD_TOOLS_VERSION:-}" ]; then
  BUILD_TOOLS_VERSION="$(infer_latest_dir_name "$ANDROID_HOME/build-tools/*")"
  export BUILD_TOOLS_VERSION
fi
: "${BUILD_TOOLS_VERSION:?BUILD_TOOLS_VERSION must be set or discoverable under ANDROID_HOME/build-tools}"

if [ -z "${PLATFORM:-}" ]; then
  PLATFORM="$(infer_latest_dir_name "$ANDROID_HOME/platforms/android-*")"
  export PLATFORM
fi
: "${PLATFORM:?PLATFORM must be set or discoverable under ANDROID_HOME/platforms}"

PROFILE="${PROFILE:-debug}"
ABIS="${ABIS:-arm64-v8a armeabi-v7a x86_64}"
APK_OUT="${APK_OUT:-target/${PROFILE}/apk/ferrous-solitaire.apk}"
STRIP_NATIVE_LIBS="${STRIP_NATIVE_LIBS:-1}"

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

echo ">>> Android SDK: $ANDROID_HOME"
echo ">>> Android NDK: $ANDROID_NDK_HOME"
echo ">>> Build tools: $BUILD_TOOLS_VERSION"
echo ">>> Platform: $PLATFORM"

BT="$ANDROID_HOME/build-tools/$BUILD_TOOLS_VERSION"
PLATFORM_JAR="$ANDROID_HOME/platforms/$PLATFORM/android.jar"
MANIFEST="solitaire_app/android/AndroidManifest.xml"
RES_DIR="solitaire_app/res"
ASSETS_DIR="assets"

# --- sanity ----------------------------------------------------------------
for f in "$BT/aapt2" "$BT/zipalign" "$BT/apksigner" "$PLATFORM_JAR" "$MANIFEST"; do
  [ -e "$f" ] || { echo "missing: $f"; exit 1; }
done

STAGING="$(mktemp -d)"
trap 'rm -rf "$STAGING"' EXIT
mkdir -p "$STAGING/lib" "$STAGING/compiled-res"

# --- 1. native libraries via cargo-ndk -------------------------------------
# `-o $STAGING/lib` lays out files as $STAGING/lib/<abi>/libsolitaire_app.so
# which is the directory structure the APK expects under lib/.
CARGO_NDK_ARGS=( --platform 26 -o "$STAGING/lib" )
for abi in $ABIS; do
  CARGO_NDK_ARGS+=( -t "$abi" )
done
CARGO_NDK_ARGS+=( build --package solitaire_app --lib )
if [ "$PROFILE" = "release" ]; then
  CARGO_NDK_ARGS+=( --release )
fi
echo ">>> cargo ndk ${CARGO_NDK_ARGS[*]}"
cargo ndk "${CARGO_NDK_ARGS[@]}"

if [ "$STRIP_NATIVE_LIBS" = "1" ]; then
  LLVM_STRIP=""
  shopt -s nullglob
  STRIP_CANDIDATES=( "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/llvm-strip )
  shopt -u nullglob
  if [ ${#STRIP_CANDIDATES[@]} -gt 0 ]; then
    LLVM_STRIP="${STRIP_CANDIDATES[0]}"
  fi
  if [ -z "$LLVM_STRIP" ]; then
    echo "llvm-strip not found under ANDROID_NDK_HOME; native libraries will remain unstripped" >&2
  else
    echo ">>> strip native libraries with $LLVM_STRIP"
    find "$STAGING/lib" -name '*.so' -print0 | while IFS= read -r -d '' so; do
      "$LLVM_STRIP" --strip-debug "$so"
    done
  fi
fi

# --- 2. compile + link resources and manifest ------------------------------
if [ -d "$RES_DIR" ]; then
  echo ">>> aapt2 compile resources"
  "$BT/aapt2" compile --dir "$RES_DIR" -o "$STAGING/compiled-res"
fi

# Derive versionCode/versionName from VERSION_NAME env var (e.g. "v0.28.0" → code 2800, name "0.28.0").
# AndroidManifest.xml intentionally has no versionCode/versionName — aapt2's --version-* flags only
# inject when absent, so the manifest must be clean for CI injection to work. Local debug builds
# fall back to code=1 / name="0.0.0-dev".
if [ -n "${VERSION_NAME:-}" ]; then
  VN="${VERSION_NAME#v}"
  IFS='.' read -r _MAJ _MIN _PAT <<< "$VN"
  VERSION_CODE=$(( ${_MAJ:-0} * 10000 + ${_MIN:-0} * 100 + ${_PAT:-0} ))
else
  VERSION_CODE=1
  VERSION_NAME="0.0.0-dev"
fi

LINK_ARGS=(
  link
  -o "$STAGING/app-unsigned.apk"
  -I "$PLATFORM_JAR"
  --manifest "$MANIFEST"
)
[ -n "$VERSION_CODE" ]         && LINK_ARGS+=( --version-code "$VERSION_CODE" )
[ -n "${VERSION_NAME:-}" ]     && LINK_ARGS+=( --version-name "${VERSION_NAME#v}" )
[ -d "$ASSETS_DIR" ] && LINK_ARGS+=( -A "$ASSETS_DIR" )
# Add compiled resources if any
shopt -s nullglob
RES_FLATS=( "$STAGING/compiled-res"/*.flat )
shopt -u nullglob
if [ ${#RES_FLATS[@]} -gt 0 ]; then
  LINK_ARGS+=( "${RES_FLATS[@]}" )
fi
echo ">>> aapt2 link"
"$BT/aapt2" "${LINK_ARGS[@]}"

# --- 3. add native libraries to the APK ------------------------------------
echo ">>> bundle native libraries"
( cd "$STAGING" && zip -r -q app-unsigned.apk lib/ )

# --- 4. zipalign -----------------------------------------------------------
echo ">>> zipalign"
"$BT/zipalign" -p -f 4 "$STAGING/app-unsigned.apk" "$STAGING/app-aligned.apk"
# Free the unsigned intermediate now — apksigner reads $app-aligned.apk and
# writes $APK_OUT, and the runner's disk is tight after a multi-ABI build.
rm -f "$STAGING/app-unsigned.apk"

# --- 5. sign ---------------------------------------------------------------
if [ -z "${KEYSTORE:-}" ]; then
  KEYSTORE="target/android/debug.keystore"
fi

KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"

if [ ! -f "$KEYSTORE" ]; then
  mkdir -p "$(dirname "$KEYSTORE")"
  echo ">>> generating debug keystore at $KEYSTORE"
  keytool -genkeypair -v \
    -keystore "$KEYSTORE" \
    -storepass "$KEYSTORE_PASS" \
    -alias "$KEY_ALIAS" \
    -keypass "$KEY_PASS" \
    -keyalg RSA -keysize 2048 -validity 10000 \
    -dname "CN=Android Debug,O=Android,C=US" > /dev/null
fi

KEYSTORE_PASS="${KEYSTORE_PASS:-android}"
KEY_ALIAS="${KEY_ALIAS:-androiddebugkey}"
KEY_PASS="${KEY_PASS:-$KEYSTORE_PASS}"

mkdir -p "$(dirname "$APK_OUT")"
echo ">>> apksigner sign -> $APK_OUT"
# Sign the schemes explicitly instead of relying on apksigner's auto behaviour.
# Left to "auto", this pipeline produced an APK carrying invalid v1 (JAR)
# signature files (META-INF/*.SF/.RSA present but failing v1 verification).
# Android installs it fine via v2/v3, but Obtainium parses the APK's legacy v1
# certificate at install time, gets an empty cert list, and crashes with
# "RangeError (length): Invalid value: valid value range is empty: 0".
# minSdk is 26 (solitaire_app/android/AndroidManifest.xml), so v1/JAR signing is
# not needed at all — disable it and ship a clean v2+v3 signature, matching what
# modern Android tooling produces for minSdk >= 24.
"$BT/apksigner" sign \
  --ks "$KEYSTORE" \
  --ks-pass "pass:$KEYSTORE_PASS" \
  --ks-key-alias "$KEY_ALIAS" \
  --key-pass "pass:$KEY_PASS" \
  --min-sdk-version 26 \
  --v1-signing-enabled false \
  --v2-signing-enabled true \
  --v3-signing-enabled true \
  --out "$APK_OUT" \
  "$STAGING/app-aligned.apk"

echo ">>> verify"
"$BT/apksigner" verify --min-sdk-version 26 --verbose "$APK_OUT"

# Guard: no leftover v1/JAR signature files may remain — their presence (valid or
# not) is what tripped Obtainium. Fail the build if any slipped through.
if unzip -l "$APK_OUT" 2>/dev/null | grep -qiE 'META-INF/.*\.(SF|RSA|DSA|EC)$'; then
  echo "ERROR: APK still contains v1/JAR signature files; expected v2+v3 only" >&2
  exit 1
fi

echo ">>> done: $APK_OUT"
