#!/usr/bin/env bash
#
# build-webview-app.sh
#
# Builds a fullscreen-WebView Android APK that shows a given URL — no browser
# chrome, no address bar. The whole Android project is generated from scratch
# and the complete toolchain (JDK 17, Android SDK, Gradle 8.7) is downloaded
# into a local directory without root and without system-wide installation.
#
# Approach follows:
#   https://blog.dummzeuch.de/2026/05/21/building-an-android-apk-on-a-linux-box-no-root-required/
#
set -euo pipefail

# Keep ${var//pat/repl} replacements literal: on bash 5.2+ patsub_replacement
# is on by default, which turns '&' in a replacement into the matched text and
# would corrupt the XML/Android escaping below. Disabling it makes replacement
# semantics match bash 4.x (where the option does not exist).
shopt -u patsub_replacement 2>/dev/null || true

# ---------------------------------------------------------------------------
# Output helpers (colour only when stdout is a terminal)
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
    C_GREEN=$'\033[32m'
    C_YELLOW=$'\033[33m'
    C_RED=$'\033[31m'
    C_BOLD=$'\033[1m'
    C_RESET=$'\033[0m'
else
    C_GREEN=""; C_YELLOW=""; C_RED=""; C_BOLD=""; C_RESET=""
fi

info() { printf '%s==>%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf '%sWARNING:%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
fail() {
    printf '%sERROR:%s %s\n' "$C_RED" "$C_RESET" "$1" >&2
    shift || true
    for line in "$@"; do
        printf '       %s\n' "$line" >&2
    done
    exit 1
}

# ---------------------------------------------------------------------------
# Usage / help
# ---------------------------------------------------------------------------
usage() {
    cat <<'USAGE'
Usage:
  build-webview-app.sh --slug <slug> [OPTIONS] <url>

Builds a fullscreen-WebView Android APK for the given URL.

Required:
  --slug <slug>         Short name: lowercase letters, digits, hyphens only.
                        No leading/trailing hyphen. Determines APK file name,
                        project directory and package id.
  <url>                 Target URL, must start with http:// or https://

Options:
  --name <text>         Display name shown under the icon (default: slug)
  --publisher <domain>  Reversed-domain prefix for the package id
                        (default: com.example)
  --icon <file.png>     PNG file to use as app icon instead of the default
                        globe icon
  --js-off              Disable JavaScript in the WebView (default: enabled)
  --help                Show this help

Environment variables:
  BASE_DIR              Directory for toolchain downloads (default: ./toolchain)
  PROJECT_DIR           Directory for generated project files (default: ./<slug>)
  ASSUME_YES=1          Skip the confirmation prompt

Package id (the target URL has NO influence — anti-spoofing):
  <publisher>.webview.<slug_with_underscores>
USAGE
}

# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
SLUG=""
URL=""
DISPLAY_NAME=""
PUBLISHER="com.example"
ICON_FILE=""
JS_ENABLED="true"

while [ $# -gt 0 ]; do
    case "$1" in
        --slug)      SLUG="${2:-}"; shift 2 ;;
        --name)      DISPLAY_NAME="${2:-}"; shift 2 ;;
        --publisher) PUBLISHER="${2:-}"; shift 2 ;;
        --icon)      ICON_FILE="${2:-}"; shift 2 ;;
        --js-off)    JS_ENABLED="false"; shift ;;
        --help|-h)   usage; exit 0 ;;
        --)          shift; break ;;
        -*)          fail "Unknown option: $1" "Run with --help for usage." ;;
        *)
            if [ -z "$URL" ]; then
                URL="$1"; shift
            else
                fail "Unexpected extra argument: $1" "Run with --help for usage."
            fi
            ;;
    esac
done
# Allow a trailing URL after "--"
if [ $# -gt 0 ] && [ -z "$URL" ]; then
    URL="$1"; shift
fi

[ -n "$SLUG" ] || fail "Missing required --slug <slug>." "Run with --help for usage."
[ -n "$URL" ]  || fail "Missing required <url>." "Run with --help for usage."

# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------
echo "$SLUG" | grep -qE '^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$' \
    || fail "Invalid slug: '$SLUG'" \
            "Only lowercase letters, digits and hyphens; no leading/trailing hyphen."

echo "$PUBLISHER" | grep -qE '^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$' \
    || fail "Invalid publisher: '$PUBLISHER'" \
            "Must be a reversed domain, e.g. com.example or de.meinefirma."

echo "$URL" | grep -qE '^https?://[^[:space:]]+$' \
    || fail "Invalid URL: '$URL'" \
            "Must start with http:// or https:// and contain no whitespace."

[ -n "$DISPLAY_NAME" ] || DISPLAY_NAME="$SLUG"

# Derived names ------------------------------------------------------------
SLUG_UNDERSCORE="${SLUG//-/_}"
APP_PKG="${PUBLISHER}.webview.${SLUG_UNDERSCORE}"
PKG_PATH="${APP_PKG//.//}"

# Package segments must not start with a digit (invalid Kotlin/Java identifier)
if echo "$APP_PKG" | grep -qE '(^|\.)[0-9]'; then
    fail "Package id '$APP_PKG' has a segment starting with a digit." \
         "Choose a --slug that does not start with a digit, or a different --publisher."
fi

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
BASE_DIR="${BASE_DIR:-./toolchain}"
PROJECT_DIR="${PROJECT_DIR:-./$SLUG}"

mkdir -p "$BASE_DIR"
BASE_DIR="$(cd "$BASE_DIR" && pwd)"
mkdir -p "$PROJECT_DIR"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"

DL_DIR="$BASE_DIR/downloads"
JDK_DIR="$BASE_DIR/jdk"
ANDROID_SDK="$BASE_DIR/android-sdk"
GRADLE_DIR="$BASE_DIR/gradle-8.7"
ICON_CACHE="$BASE_DIR/icon-cache"
GRADLE_USER_HOME="$BASE_DIR/gradle-home"

mkdir -p "$DL_DIR" "$ICON_CACHE"

# Versions / URLs ----------------------------------------------------------
JDK_URL="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse"
CMDTOOLS_URL="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
GRADLE_URL="https://services.gradle.org/distributions/gradle-8.7-bin.zip"
ICON_SVG_URL="https://raw.githubusercontent.com/google/material-design-icons/master/symbols/web/language/materialsymbolsoutlined/language_24px.svg"
ICON_CACHE_FILE="$ICON_CACHE/language.pathdata"

SDK_PLATFORM="platforms;android-34"
SDK_BUILDTOOLS="build-tools;34.0.0"
SDK_PLATFORM_TOOLS="platform-tools"

# ===========================================================================
# Phase 0: Preflight checks
# ===========================================================================
info "Phase 0: Preflight checks"

[ "$(uname -s)" = "Linux" ] || fail "This script only runs on Linux (found: $(uname -s))."

case "$(uname -m)" in
    x86_64|amd64) ;;
    *) fail "Unsupported CPU architecture: $(uname -m)." \
            "Android build-tools are only available for x86_64/amd64." ;;
esac

if ldd --version 2>&1 | grep -qi musl; then
    fail "musl libc detected (Alpine?)." "The Android build-tools require glibc."
fi

# Download tool
DL=""
if command -v curl >/dev/null 2>&1; then
    DL="curl"
elif command -v wget >/dev/null 2>&1; then
    DL="wget"
else
    fail "Neither curl nor wget found." "Install one of them and retry."
fi

# Standard tools
for tool in tar unzip find awk df sed; do
    command -v "$tool" >/dev/null 2>&1 || fail "Required tool not found: $tool"
done

# Disk space: at least 3 GiB free in BASE_DIR
NEED_KIB=$((3 * 1024 * 1024))
FREE_KIB="$(df -Pk "$BASE_DIR" | awk 'NR==2 {print $4}')"
if [ -z "$FREE_KIB" ] || [ "$FREE_KIB" -lt "$NEED_KIB" ]; then
    fail "Not enough free disk space in $BASE_DIR." \
         "Need at least 3 GiB, have $(( ${FREE_KIB:-0} / 1024 )) MiB."
fi

# Download helpers ---------------------------------------------------------
http_head() {
    # Return 0 if the host responds at all (any HTTP status counts).
    local url="$1"
    if [ "$DL" = "curl" ]; then
        curl -s -o /dev/null -I --max-time 20 "$url" >/dev/null 2>&1
    else
        wget -q --spider --timeout=20 "$url" >/dev/null 2>&1
    fi
}

download() {
    local url="$1" out="$2"
    if [ "$DL" = "curl" ]; then
        curl -fL --retry 3 -o "$out" "$url"
    else
        wget -O "$out" "$url"
    fi
}

# Extraction helpers. On Windows-mounted filesystems under WSL (v9fs/drvfs/9p)
# tar and unzip cannot set timestamps/ownership/permissions and abort with
# "Cannot utime" / "Cannot change mode" / "cannot set times", even though the
# files themselves extract fine. We drop owner/mtime preservation to cut the
# noise, send the rest to a log, and ignore the exit status — callers verify
# the result explicitly afterwards.
extract_tar() {
    local archive="$1" dest="$2" log="${3:-/dev/null}"
    tar -xzf "$archive" -C "$dest" --no-same-owner -m 2>"$log" || true
}

extract_zip() {
    local archive="$1" dest="$2" log="${3:-/dev/null}"
    unzip -q -o "$archive" -d "$dest" 2>"$log" || true
}

# Return 0 if a path is on a Windows-mounted / network filesystem that cannot
# perform chmod/utime (WSL drvfs/v9fs/9p, CIFS/SMB). If stat -f is unavailable
# we can't tell, so assume native (return 1) and build in place.
is_windows_fs() {
    local t
    command -v stat >/dev/null 2>&1 || return 1
    t="$(stat -f -c %T "$1" 2>/dev/null || true)"
    case "$t" in
        drvfs|9p|v9fs|cifs|smbfs|smb2|smb3) return 0 ;;
        *) return 1 ;;
    esac
}

# Echo a writable directory on a native Linux filesystem (for GRADLE_USER_HOME
# and the build staging dir), or fail with status 1 if none can be found.
pick_native_root() {
    local c d
    for c in "${XDG_CACHE_HOME:-$HOME/.cache}" "${TMPDIR:-}" /tmp /var/tmp; do
        [ -n "$c" ] || continue
        d="$c/build-webview-app"
        mkdir -p "$d" 2>/dev/null || continue
        [ -w "$d" ] || continue
        is_windows_fs "$d" && continue
        echo "$d"; return 0
    done
    return 1
}

# Warn (don't fail) when a directory lives on a Windows-mounted filesystem.
warn_if_windows_fs() {
    local dir="$1" label="$2"
    if is_windows_fs "$dir"; then
        warn "$label ($dir) is on a Windows-mounted filesystem ($(stat -f -c %T "$dir" 2>/dev/null))."
        warn "  Builds there are slow and tar/unzip/Gradle cannot set permissions; the script works"
        warn "  around it by building on a native Linux filesystem. For best speed point"
        warn "  BASE_DIR/PROJECT_DIR at a native path (e.g. under \$HOME)."
    fi
}

# Network reachability of required hosts
NET_TARGETS=(
    "https://api.adoptium.net/"
    "https://dl.google.com/"
    "https://services.gradle.org/"
    "https://repo.maven.apache.org/maven2/"
    "https://dl.google.com/dl/android/maven2/"
)
if [ -z "$ICON_FILE" ]; then
    NET_TARGETS+=( "https://raw.githubusercontent.com/" )
fi
for t in "${NET_TARGETS[@]}"; do
    if ! http_head "$t"; then
        fail "Network host not reachable: $t" \
             "All download hosts must be reachable. Check your connection/proxy."
    fi
done

# Custom icon sanity check
if [ -n "$ICON_FILE" ]; then
    [ -f "$ICON_FILE" ] || fail "Custom icon file not found: $ICON_FILE"
    if command -v file >/dev/null 2>&1; then
        if ! file -b --mime-type "$ICON_FILE" 2>/dev/null | grep -qi 'image/png'; then
            warn "Custom icon '$ICON_FILE' does not look like a PNG file."
        fi
    else
        case "$ICON_FILE" in
            *.png|*.PNG) ;;
            *) warn "Custom icon '$ICON_FILE' does not have a .png extension." ;;
        esac
    fi
fi

warn_if_windows_fs "$BASE_DIR" "Toolchain directory"
[ "$PROJECT_DIR" = "$BASE_DIR" ] || warn_if_windows_fs "$PROJECT_DIR" "Project directory"

# ===========================================================================
# Phase 1: Determine component status + confirmation
# ===========================================================================
jdk_installed()    { [ -x "$JDK_DIR/bin/java" ]; }
gradle_installed() { [ -x "$GRADLE_DIR/bin/gradle" ]; }
sdkmgr_path()      { echo "$ANDROID_SDK/cmdline-tools/latest/bin/sdkmanager"; }

sdk_status() {
    local have=0 want=4
    [ -x "$(sdkmgr_path)" ]                          && have=$((have+1))
    [ -d "$ANDROID_SDK/$SDK_PLATFORM_TOOLS" ]         && have=$((have+1))
    [ -d "$ANDROID_SDK/platforms/android-34" ]        && have=$((have+1))
    [ -d "$ANDROID_SDK/build-tools/34.0.0" ]          && have=$((have+1))
    if [ "$have" -eq "$want" ]; then
        echo "installed"
    elif [ "$have" -eq 0 ]; then
        echo "missing"
    else
        echo "partial"
    fi
}

JDK_STAT="$( jdk_installed && echo "[already installed, will skip]" || echo "[will download]" )"
GRADLE_STAT="$( gradle_installed && echo "[already installed, will skip]" || echo "[will download]" )"
case "$(sdk_status)" in
    installed) SDK_STAT="[already installed, will skip]" ;;
    partial)   SDK_STAT="[will download missing parts]" ;;
    *)         SDK_STAT="[will download]" ;;
esac
if [ -n "$ICON_FILE" ]; then
    ICON_STAT="[custom PNG: $ICON_FILE]"
elif [ -f "$ICON_CACHE_FILE" ]; then
    ICON_STAT="[cached globe icon, will reuse]"
else
    ICON_STAT="[will download globe icon]"
fi

printf '\n'
printf '%sWebView APK build plan%s\n' "$C_BOLD" "$C_RESET"
printf '  App name:   %s\n' "$DISPLAY_NAME"
printf '  URL:        %s\n' "$URL"
printf '  Package:    %s\n' "$APP_PKG"
printf '  JavaScript: %s\n' "$( [ "$JS_ENABLED" = "true" ] && echo enabled || echo disabled )"
printf '  Project:    %s\n' "$PROJECT_DIR"
printf '  Toolchain:  %s\n' "$BASE_DIR"
printf '\n'
printf '  %-26s %s\n' "JDK 17 (Eclipse Temurin)" "$JDK_STAT"
printf '  %-26s %s\n' "Android SDK"               "$SDK_STAT"
printf '  %-26s %s\n' "Gradle 8.7"                "$GRADLE_STAT"
printf '  %-26s %s\n' "Icon"                      "$ICON_STAT"
printf '\n'
warn "Project files in $PROJECT_DIR are regenerated on every run."

if [ "${ASSUME_YES:-}" != "1" ]; then
    TTY_SRC=""
    [ -t 0 ] && TTY_SRC="stdin"
    { [ -z "$TTY_SRC" ] && ( : < /dev/tty ) 2>/dev/null; } && TTY_SRC="/dev/tty"
    [ -z "$TTY_SRC" ] && fail "No terminal for confirmation." "Set ASSUME_YES=1 or run interactively."

    printf 'Proceed? [y/N] '
    if [ "$TTY_SRC" = "stdin" ]; then
        read -r reply
    else
        read -r reply < /dev/tty
    fi
    case "$reply" in
        y|Y|yes|YES) ;;
        *) info "Aborted."; exit 0 ;;
    esac
fi

# ===========================================================================
# Phase 2: JDK 17 (Eclipse Temurin)
# ===========================================================================
if jdk_installed; then
    info "Phase 2: JDK 17 already present — skipping."
else
    info "Phase 2: Downloading JDK 17 (Eclipse Temurin)..."
    download "$JDK_URL" "$DL_DIR/jdk.tar.gz"
    info "Extracting JDK..."
    rm -rf "$BASE_DIR/.jdk-extract" "$JDK_DIR"
    mkdir -p "$BASE_DIR/.jdk-extract"
    extract_tar "$DL_DIR/jdk.tar.gz" "$BASE_DIR/.jdk-extract" "$DL_DIR/jdk-extract.log"
    JDK_TOP="$(find "$BASE_DIR/.jdk-extract" -mindepth 1 -maxdepth 1 -type d | head -n1)"
    [ -n "$JDK_TOP" ] || fail "Could not locate extracted JDK directory." \
        "tar extraction failed — see $DL_DIR/jdk-extract.log"
    mv "$JDK_TOP" "$JDK_DIR"
    rm -rf "$BASE_DIR/.jdk-extract"
    [ -x "$JDK_DIR/bin/java" ] || fail "JDK extraction failed: no $JDK_DIR/bin/java" \
        "See $DL_DIR/jdk-extract.log"
fi

export JAVA_HOME="$JDK_DIR"
export PATH="$JDK_DIR/bin:$PATH"

# ===========================================================================
# Phase 3: Android SDK
# ===========================================================================
export ANDROID_HOME="$ANDROID_SDK"
export ANDROID_SDK_ROOT="$ANDROID_SDK"

if [ ! -x "$(sdkmgr_path)" ]; then
    info "Phase 3: Downloading Android SDK command-line tools..."
    download "$CMDTOOLS_URL" "$DL_DIR/cmdline-tools.zip"
    rm -rf "$BASE_DIR/.cmdtools-extract"
    mkdir -p "$BASE_DIR/.cmdtools-extract"
    extract_zip "$DL_DIR/cmdline-tools.zip" "$BASE_DIR/.cmdtools-extract" "$DL_DIR/cmdtools-extract.log"
    [ -d "$BASE_DIR/.cmdtools-extract/cmdline-tools" ] \
        || fail "cmdline-tools extraction failed." "See $DL_DIR/cmdtools-extract.log"
    mkdir -p "$ANDROID_SDK/cmdline-tools"
    rm -rf "$ANDROID_SDK/cmdline-tools/latest"
    mv "$BASE_DIR/.cmdtools-extract/cmdline-tools" "$ANDROID_SDK/cmdline-tools/latest"
    rm -rf "$BASE_DIR/.cmdtools-extract"
    [ -x "$(sdkmgr_path)" ] || fail "sdkmanager not found after extraction." \
        "See $DL_DIR/cmdtools-extract.log"
fi

SDKMGR="$(sdkmgr_path)"

if [ "$(sdk_status)" = "installed" ]; then
    info "Phase 3: Android SDK packages already present — skipping."
else
    info "Phase 3: Accepting SDK licenses..."
    ( yes || true ) | "$SDKMGR" --sdk_root="$ANDROID_SDK" --licenses >/dev/null
    info "Phase 3: Installing SDK packages (platform-tools, android-34, build-tools 34.0.0)..."
    ( yes || true ) | "$SDKMGR" --sdk_root="$ANDROID_SDK" \
        "$SDK_PLATFORM_TOOLS" "$SDK_PLATFORM" "$SDK_BUILDTOOLS"
    [ "$(sdk_status)" = "installed" ] || fail "Android SDK package installation incomplete."
fi

# ===========================================================================
# Phase 4: Gradle 8.7
# ===========================================================================
if gradle_installed; then
    info "Phase 4: Gradle 8.7 already present — skipping."
else
    info "Phase 4: Downloading Gradle 8.7..."
    download "$GRADLE_URL" "$DL_DIR/gradle-8.7-bin.zip"
    info "Extracting Gradle..."
    rm -rf "$GRADLE_DIR"
    extract_zip "$DL_DIR/gradle-8.7-bin.zip" "$BASE_DIR" "$DL_DIR/gradle-extract.log"
    [ -x "$GRADLE_DIR/bin/gradle" ] || fail "Gradle extraction failed." \
        "See $DL_DIR/gradle-extract.log"
fi
# Ensure the distribution zip exists for the wrapper (offline reuse)
if [ ! -f "$DL_DIR/gradle-8.7-bin.zip" ]; then
    download "$GRADLE_URL" "$DL_DIR/gradle-8.7-bin.zip"
fi

# ===========================================================================
# Phase 5: Resolve icon
# ===========================================================================
ICON_IS_PNG="false"
if [ -n "$ICON_FILE" ]; then
    info "Phase 5: Using custom PNG icon."
    ICON_IS_PNG="true"
else
    if [ ! -f "$ICON_CACHE_FILE" ]; then
        info "Phase 5: Downloading Material Symbols globe icon..."
        SVG="$(download "$ICON_SVG_URL" /dev/stdout)"
        PATH_DATA="$(echo "$SVG" | sed 's/.*d="\([^"]*\)".*/\1/')"
        [ -n "$PATH_DATA" ] || fail "Could not extract SVG path data from icon."
        printf '%s' "$PATH_DATA" > "$ICON_CACHE_FILE"
    else
        info "Phase 5: Using cached globe icon."
    fi
    ICON_PATH_DATA="$(cat "$ICON_CACHE_FILE")"
fi

# ===========================================================================
# Phase 6: Generate project files
# ===========================================================================
info "Phase 6: Generating Android project in $PROJECT_DIR ..."

# Escape for Android string resources: XML entities for the structural
# characters & < > and Android backslash-escapes for \ ' " (aapt2 rejects
# unescaped apostrophes/quotes in a <string>).
xml_escape() {
    local s="$1"
    s="${s//\\/\\\\}"
    s="${s//&/&amp;}"
    s="${s//</&lt;}"
    s="${s//>/&gt;}"
    s="${s//\'/\\\'}"
    s="${s//\"/\\\"}"
    printf '%s' "$s"
}
DISPLAY_NAME_ESC="$(xml_escape "$DISPLAY_NAME")"
URL_ESC="$(xml_escape "$URL")"

APP_DIR="$PROJECT_DIR/app"
MAIN_DIR="$APP_DIR/src/main"
JAVA_DIR="$MAIN_DIR/java/$PKG_PATH"
RES_DIR="$MAIN_DIR/res"

rm -rf "$APP_DIR/src"
mkdir -p "$JAVA_DIR" \
         "$RES_DIR/layout" \
         "$RES_DIR/values" \
         "$RES_DIR/drawable" \
         "$RES_DIR/mipmap-anydpi-v26"

# --- settings.gradle.kts ---------------------------------------------------
cat > "$PROJECT_DIR/settings.gradle.kts" <<EOF
pluginManagement {
    repositories { google(); mavenCentral(); gradlePluginPortal() }
}
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
rootProject.name = "$SLUG"
include(":app")
EOF

# --- root build.gradle.kts -------------------------------------------------
cat > "$PROJECT_DIR/build.gradle.kts" <<'EOF'
plugins {
    id("com.android.application") version "8.5.0" apply false
    id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
EOF

# --- gradle.properties -----------------------------------------------------
cat > "$PROJECT_DIR/gradle.properties" <<'EOF'
org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=true
kotlin.code.style=official
EOF

# --- local.properties ------------------------------------------------------
cat > "$PROJECT_DIR/local.properties" <<EOF
sdk.dir=$ANDROID_SDK
EOF

# --- app/build.gradle.kts --------------------------------------------------
cat > "$APP_DIR/build.gradle.kts" <<EOF
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

android {
    namespace = "$APP_PKG"
    compileSdk = 34

    defaultConfig {
        applicationId = "$APP_PKG"
        minSdk = 26
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
    }

    signingConfigs {
        // Sign the debug APK with BOTH the v1 (JAR) and v2 schemes. AGP would
        // otherwise drop v1 because minSdk >= 24, leaving a v2-only APK that
        // some device package installers reject when sideloading with
        // "There was a problem parsing the package".
        getByName("debug") {
            enableV1Signing = true
            enableV2Signing = true
        }
    }

    buildTypes {
        release { isMinifyEnabled = false }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions { jvmTarget = "17" }
}

dependencies {
    implementation("androidx.core:core-ktx:1.13.1")
    implementation("androidx.appcompat:appcompat:1.7.0")
}
EOF

# --- AndroidManifest.xml ---------------------------------------------------
cat > "$MAIN_DIR/AndroidManifest.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:roundIcon="@mipmap/ic_launcher"
        android:allowBackup="true"
        android:theme="@style/Theme.WebViewApp"
        android:usesCleartextTraffic="true">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:configChanges="orientation|screenSize|keyboardHidden">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
EOF

# --- res/layout/activity_main.xml ------------------------------------------
cat > "$RES_DIR/layout/activity_main.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
EOF

# --- res/values/strings.xml ------------------------------------------------
cat > "$RES_DIR/values/strings.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">$DISPLAY_NAME_ESC</string>
    <string name="target_url">$URL_ESC</string>
</resources>
EOF

# --- res/values/themes.xml -------------------------------------------------
# MainActivity extends AppCompatActivity, which REQUIRES a Theme.AppCompat
# descendant or it crashes on startup with "You need to use a Theme.AppCompat
# theme (or descendant) with this activity." NoActionBar keeps the WebView
# truly chrome-free.
cat > "$RES_DIR/values/themes.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="Theme.WebViewApp" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources>
EOF

# --- res/values/ic_launcher_background.xml ---------------------------------
cat > "$RES_DIR/values/ic_launcher_background.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ic_launcher_background">#1565C0</color>
</resources>
EOF

# --- res/mipmap-anydpi-v26/ic_launcher.xml ---------------------------------
cat > "$RES_DIR/mipmap-anydpi-v26/ic_launcher.xml" <<'EOF'
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
EOF

# --- res/drawable/ic_launcher_foreground (vector or png) -------------------
if [ "$ICON_IS_PNG" = "true" ]; then
    cp "$ICON_FILE" "$RES_DIR/drawable/ic_launcher_foreground.png"
else
    cat > "$RES_DIR/drawable/ic_launcher_foreground.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <group
        android:scaleX="0.075"
        android:scaleY="0.075"
        android:translateX="18"
        android:translateY="90">
        <path
            android:fillColor="#FFFFFF"
            android:pathData="$ICON_PATH_DATA" />
    </group>
</vector>
EOF
fi

# --- MainActivity.kt -------------------------------------------------------
# Note: targetUrl is resolved inside onCreate (not as a property initializer)
# because getString() requires the Activity context to be attached.
cat > "$JAVA_DIR/MainActivity.kt" <<EOF
package $APP_PKG

import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val targetUrl = getString(R.string.target_url)

        webView = findViewById(R.id.webview)

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView,
                request: WebResourceRequest
            ): Boolean = false
        }

        webView.webChromeClient = WebChromeClient()

        val settings = webView.settings
        settings.javaScriptEnabled = $JS_ENABLED
        settings.domStorageEnabled = true
        settings.builtInZoomControls = true
        settings.displayZoomControls = false
        settings.useWideViewPort = true
        settings.loadWithOverviewMode = true

        if (savedInstanceState != null) {
            webView.restoreState(savedInstanceState)
        } else {
            webView.loadUrl(targetUrl)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        webView.saveState(outState)
    }

    @Deprecated("Required for minSdk < 33")
    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            @Suppress("DEPRECATION")
            super.onBackPressed()
        }
    }
}
EOF

# ===========================================================================
# Phase 7: Gradle wrapper + build APK
# ===========================================================================
# Gradle cannot run on a Windows-mounted filesystem: it chmods cache files in
# GRADLE_USER_HOME (the daemon registry) and the build outputs under app/build,
# and v9fs/drvfs reject chmod with EPERM. So when GRADLE_USER_HOME or the
# project is on such a mount, relocate GRADLE_USER_HOME to a native filesystem
# and run the build from a native-fs copy of the project, then copy the
# finished APK back to PROJECT_DIR.
BUILD_DIR="$PROJECT_DIR"
if is_windows_fs "$PROJECT_DIR" || is_windows_fs "$GRADLE_USER_HOME"; then
    NATIVE_ROOT="$(pick_native_root)" || fail \
        "The build must run on a native Linux filesystem, but none was found." \
        "PROJECT_DIR is on a Windows-mounted filesystem and no writable native" \
        "directory (\$XDG_CACHE_HOME, \$TMPDIR, /tmp, /var/tmp) is available." \
        "Re-run with BASE_DIR and PROJECT_DIR on a native Linux path (e.g. under \$HOME)."
    GRADLE_USER_HOME="$NATIVE_ROOT/gradle-home"
    BUILD_DIR="$NATIVE_ROOT/build/$SLUG"
    info "Phase 7: $PROJECT_DIR is on a Windows-mounted filesystem."
    info "         Building in $BUILD_DIR (native fs); the APK is copied back afterwards."
    rm -rf "$BUILD_DIR"
    mkdir -p "$BUILD_DIR"
    cp -r "$PROJECT_DIR/." "$BUILD_DIR/"
    rm -rf "$BUILD_DIR/app/build" "$BUILD_DIR/.gradle" "$BUILD_DIR/.gradle-extract.log"
fi

export GRADLE_USER_HOME
mkdir -p "$GRADLE_USER_HOME"

GRADLE_ZIP_ABS="$DL_DIR/gradle-8.7-bin.zip"

info "Phase 7: Generating Gradle wrapper..."
(
    cd "$BUILD_DIR"
    "$GRADLE_DIR/bin/gradle" wrapper \
        --gradle-version 8.7 \
        --distribution-type bin \
        --gradle-distribution-url "file://$GRADLE_ZIP_ABS" \
        --no-daemon
)

info "Phase 7: Building APK (assembleDebug)..."
(
    cd "$BUILD_DIR"
    ./gradlew assembleDebug --no-daemon
)

# ===========================================================================
# Phase 8: Verify + success message
# ===========================================================================
APK_BUILT="$BUILD_DIR/app/build/outputs/apk/debug/app-debug.apk"
[ -f "$APK_BUILT" ] || fail "Build finished but APK not found at $APK_BUILT"

APK_OUT="$PROJECT_DIR/$SLUG.apk"
cp "$APK_BUILT" "$APK_OUT"

APK_SIZE="$(du -h "$APK_OUT" | awk '{print $1}')"
ADB="$ANDROID_SDK/platform-tools/adb"

printf '\n'
info "BUILD SUCCESSFUL"
printf '  App name:   %s\n' "$DISPLAY_NAME"
printf '  URL:        %s\n' "$URL"
printf '  Package:    %s\n' "$APP_PKG"
printf '  JavaScript: %s\n' "$( [ "$JS_ENABLED" = "true" ] && echo enabled || echo disabled )"
printf '  APK:        %s\n' "$APK_OUT"
printf '  Size:       %s\n' "$APK_SIZE"
printf '  Toolchain:  %s\n' "$BASE_DIR"
[ "$BUILD_DIR" = "$PROJECT_DIR" ] || printf '  Built in:   %s (native fs; project dir is on a Windows mount)\n' "$BUILD_DIR"
if [ "$ICON_IS_PNG" = "true" ]; then
    printf '  Icon:       custom PNG (%s)\n' "$ICON_FILE"
else
    printf '  Icon:       language (Material Symbols, Apache 2.0)\n'
    printf '  Icon cache: %s\n' "$ICON_CACHE"
fi
printf '\n'
printf 'Install on a connected phone:\n'
printf '  %s install "%s"\n' "$ADB" "$APK_OUT"
printf '\n'
printf 'Or copy the APK to the device and tap it\n'
printf '(enable "Install unknown apps" first).\n'
