#!/usr/bin/env bash
# Build / install / run helper for the Wear OS app.
#
# Usage:
#   ./run.sh <target> <subcommand> [args]
#
# A target prefix is REQUIRED for every device-bound subcommand. The script
# never silently picks "the first attached device" — if more than one candidate
# exists it asks, and if ADB_SERIAL/WEAR_AVD pins a choice it uses that.
#
# Target prefix (or DOCTATE_TARGET env var):
#   watch         Real Pixel Watch over ADB-WiFi. Uses the cached serial
#                 from .watch_serial when it's attached; otherwise menu.
#                 Points the build at the dev laptop's LAN URL
#                 ($DOCTATE_DEV_SERVER).
#   emulator      Wear OS AVD on this host.
#                 - 0 emulators running → menu of AVDs from `emulator -list-avds`
#                 - ≥1 running          → menu of running serials + "Start a new AVD"
#                 The script never auto-picks a running emulator. To bypass the
#                 menu, pin the choice via ADB_SERIAL=<serial> or WEAR_AVD=<name>.
#                 Build keeps its emulator default (10.0.2.2:3000).
#
# Subcommands that need a target:
#   (none)        install + start (default inner loop)
#   build         ./gradlew :app:assembleDebug
#   install       ./gradlew :app:installDebug
#   start         adb shell am start ...
#   stop          adb shell am force-stop ...
#   logcat        adb logcat, filtered to the app's PID
#   shot [path]   screencap to PNG (default /tmp/wear_screen.png)
#   test          run JVM unit tests, plus instrumented tests on the target
#                 (set SKIP_INSTRUMENTED=1 to skip the on-device tier)
#
# Target-free subcommands (run without a prefix):
#   devices       list attached devices
#   connect [host:port]
#                 adb connect to a Pixel Watch over WiFi and remember
#                 the serial in .watch_serial. Without an argument:
#                 reuse the cached serial.
#   clean         ./gradlew clean
#   help          show this help
#
# Env vars:
#   DOCTATE_TARGET    Same as the <target> prefix (watch|emulator).
#                     The CLI prefix takes precedence when both are set.
#   ADB_SERIAL        Pin a specific device serial. Skips the menu entirely.
#   DOCTATE_DEV_SERVER
#                     LAN URL of the Axum dev server, used by 'watch' target.
#                     Default: http://192.168.178.27:3000
#   DOCTATE_API_KEY   If set, passed as -Pdoctate.apiKey=... to Gradle and
#                     baked into BuildConfig.API_KEY at compile time.
#                     Otherwise the value from local.properties is used.
#   WEAR_AVD          AVD name to auto-boot when no emulator is attached
#                     (skips the AVD menu; useful for CI / scripted runs).
#   SKIP_INSTRUMENTED set to 1 to limit `test` to JVM unit tests (no device needed).

set -euo pipefail

# --- Config ----------------------------------------------------------------
PACKAGE="com.doctate.watch"
ACTIVITY=".presentation.MainActivity"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WATCH_SERIAL_FILE="$SCRIPT_DIR/.watch_serial"
DEFAULT_DEV_SERVER="http://192.168.178.27:3000"

# Populated by apply_target / parsed args. Empty = no override.
ADB_SERIAL="${ADB_SERIAL:-}"
DOCTATE_TARGET="${DOCTATE_TARGET:-}"
GRADLE_PROPS=()

# --- Output helpers --------------------------------------------------------
if [[ -t 1 ]]; then
    C_INFO=$'\e[1;34m'; C_OK=$'\e[1;32m'; C_ERR=$'\e[1;31m'; C_OFF=$'\e[0m'
else
    C_INFO=""; C_OK=""; C_ERR=""; C_OFF=""
fi
info() { printf "%s==>%s %s\n" "$C_INFO" "$C_OFF" "$*"; }
ok()   { printf "%s OK%s %s\n" "$C_OK"   "$C_OFF" "$*"; }
die()  { printf "%sERR%s %s\n" "$C_ERR"  "$C_OFF" "$*" >&2; exit 1; }

# --- Toolchain detection ---------------------------------------------------
detect_java() {
    if [[ -n "${JAVA_HOME:-}" && -x "$JAVA_HOME/bin/java" ]]; then
        return
    fi
    for candidate in /opt/android-studio/jbr /usr/lib/jvm/java-21-openjdk; do
        if [[ -x "$candidate/bin/java" ]]; then
            export JAVA_HOME="$candidate"
            return
        fi
    done
    die "No JDK found. Set JAVA_HOME or install jdk21-openjdk."
}

detect_adb() {
    if [[ -n "${ADB:-}" && -x "$ADB" ]]; then return; fi
    if [[ -x "$HOME/Android/Sdk/platform-tools/adb" ]]; then
        ADB="$HOME/Android/Sdk/platform-tools/adb"
    elif command -v adb >/dev/null 2>&1; then
        ADB="$(command -v adb)"
    else
        die "adb not found. Expected ~/Android/Sdk/platform-tools/adb."
    fi
}

# Wrapper: calls adb with -s $ADB_SERIAL when set, otherwise plain adb.
# Bash idiom ${VAR:+ARG} expands to ARG only if VAR is non-empty.
adb_cmd() {
    "$ADB" ${ADB_SERIAL:+-s "$ADB_SERIAL"} "$@"
}

# --- Target resolution -----------------------------------------------------
read_cached_watch_serial() {
    if [[ -f "$WATCH_SERIAL_FILE" ]]; then
        cat "$WATCH_SERIAL_FILE" | tr -d '\r\n'
    fi
}

# Interactive picker. Writes the chosen item to the global PICKED so the
# caller doesn't need $(...) (which would swallow `die` inside a subshell).
# Aborts (via die) on bad input or when stdin is not a TTY.
# Usage: pick_from_list "Prompt" "label1" "label2" ...; use "$PICKED"
PICKED=""
pick_from_list() {
    local prompt="$1"; shift
    local -a items=("$@")
    if [[ ! -t 0 ]]; then
        die "$prompt need a TTY to prompt; pin via ADB_SERIAL / WEAR_AVD instead. Items: ${items[*]}"
    fi
    info "$prompt"
    local i
    for i in "${!items[@]}"; do
        printf "  %d) %s\n" "$((i+1))" "${items[$i]}"
    done
    printf "  0) abort\n"
    local choice
    read -r -p "Choose: " choice
    if [[ ! "$choice" =~ ^[0-9]+$ ]]; then
        die "Invalid choice: '$choice' (expected a number)."
    fi
    if [[ "$choice" -eq 0 ]]; then
        die "Aborted by user."
    fi
    if [[ "$choice" -lt 1 || "$choice" -gt "${#items[@]}" ]]; then
        die "Out of range: $choice (have ${#items[@]} items)."
    fi
    PICKED="${items[$((choice-1))]}"
}

# Lists all running emulator serials (one per line).
list_emulator_serials() {
    detect_adb
    "$ADB" devices | awk 'NR>1 && $2=="device" && $1 ~ /^emulator-/ {print $1}'
}

# Lists attached Wear OS watch serials (one per line). Pixel Watch 2
# reports device code "eos", Pixel Watch 1 is "rohan".
list_watch_serials() {
    detect_adb
    "$ADB" devices -l | awk '$2=="device" && /device:(eos|rohan)/ {print $1}'
}

# Human-readable label for a serial, built from `adb devices -l` metadata.
# Falls back to the bare serial if no extra info is available.
serial_label() {
    local serial="$1"
    local line
    line=$("$ADB" devices -l | awk -v s="$serial" '$1==s {$1=""; sub(/^ +/,""); print}')
    if [[ -n "$line" ]]; then
        printf '%s — %s\n' "$serial" "$line"
    else
        printf '%s\n' "$serial"
    fi
}

# Output variable for the resolve_*_serial functions. Using a global
# instead of stdout+$() lets `die` inside these functions abort the main
# shell directly — $() would wrap everything in a subshell and swallow
# the exit, which is how the earlier version silently leaked past errors.
RESOLVED_SERIAL=""

# Resolves the emulator serial the user wants to target. The script never
# auto-picks a running emulator — even when only one is up, the user may
# want to boot a different AVD instead. Bypass the menu by setting
# ADB_SERIAL=<serial> or WEAR_AVD=<name>.
# - 0 emulators running   → boot one (WEAR_AVD env or AVD menu); take its serial.
# - ≥1 emulators running  → always prompt; "Start a new AVD" is the last option.
resolve_emulator_serial() {
    detect_adb
    local -a running
    mapfile -t running < <(list_emulator_serials)

    if [[ "${#running[@]}" -eq 0 ]]; then
        boot_emulator_or_die
        mapfile -t running < <(list_emulator_serials)
        if [[ "${#running[@]}" -eq 0 ]]; then
            die "Boot reported success but no emulator serial visible."
        fi
        RESOLVED_SERIAL="${running[0]}"
        return
    fi

    local -a labels=()
    local s
    for s in "${running[@]}"; do
        labels+=("$(serial_label "$s")")
    done
    local start_new_label="Start a new AVD"
    labels+=("$start_new_label")

    pick_from_list "Pick emulator:" "${labels[@]}"
    local choice="$PICKED"

    if [[ "$choice" == "$start_new_label" ]]; then
        local -a before=("${running[@]}")
        boot_emulator_or_die
        local -a after
        mapfile -t after < <(list_emulator_serials)
        local new=""
        local a p found
        for a in "${after[@]}"; do
            found=0
            for p in "${before[@]}"; do
                [[ "$p" == "$a" ]] && { found=1; break; }
            done
            if [[ "$found" -eq 0 ]]; then
                new="$a"
                break
            fi
        done
        [[ -z "$new" ]] && die "Could not identify newly booted emulator serial."
        RESOLVED_SERIAL="$new"
        return
    fi

    # Chosen label has the form "emulator-5554 — <metadata>"; strip to serial.
    RESOLVED_SERIAL="${choice%% *}"
}

# Resolves the watch serial to target. Prefers the cached pairing when
# it's actually attached, falls back to a menu on ambiguity.
resolve_watch_serial() {
    detect_adb
    local -a attached
    mapfile -t attached < <(list_watch_serials)
    local cached
    cached="$(read_cached_watch_serial)"

    if [[ -n "$cached" ]]; then
        local s
        for s in "${attached[@]}"; do
            if [[ "$s" == "$cached" ]]; then
                RESOLVED_SERIAL="$cached"
                return
            fi
        done
    fi

    if [[ "${#attached[@]}" -eq 0 ]]; then
        if [[ -n "$cached" ]]; then
            die "Cached watch serial '$cached' is not attached. Try './run.sh connect $cached' or './run.sh connect <ip:port>' for a fresh pairing."
        fi
        die "No watch attached. Run './run.sh connect <ip:port>' first."
    fi

    if [[ "${#attached[@]}" -eq 1 ]]; then
        RESOLVED_SERIAL="${attached[0]}"
        return
    fi

    local -a labels=()
    local s
    for s in "${attached[@]}"; do
        labels+=("$(serial_label "$s")")
    done
    pick_from_list "Multiple watches attached — pick one:" "${labels[@]}"
    RESOLVED_SERIAL="${PICKED%% *}"
}

apply_target() {
    case "$DOCTATE_TARGET" in
        watch)
            if [[ -z "$ADB_SERIAL" ]]; then
                resolve_watch_serial
                ADB_SERIAL="$RESOLVED_SERIAL"
            fi
            local server="${DOCTATE_DEV_SERVER:-$DEFAULT_DEV_SERVER}"
            GRADLE_PROPS+=("-Pdoctate.serverUrl=$server")
            info "Target: watch (serial=$ADB_SERIAL, server=$server)"
            ;;
        emulator)
            if [[ -z "$ADB_SERIAL" ]]; then
                resolve_emulator_serial
                ADB_SERIAL="$RESOLVED_SERIAL"
            fi
            info "Target: emulator (serial=$ADB_SERIAL, server defaults to 10.0.2.2:3000)"
            ;;
        "")
            die "No target selected. Use './run.sh watch …' or './run.sh emulator …' (or set DOCTATE_TARGET)."
            ;;
        *)
            die "Unknown target: '$DOCTATE_TARGET' (expected: watch | emulator)"
            ;;
    esac

    if [[ -n "${DOCTATE_API_KEY:-}" ]]; then
        GRADLE_PROPS+=("-Pdoctate.apiKey=$DOCTATE_API_KEY")
    fi
    if [[ -n "$ADB_SERIAL" ]]; then
        GRADLE_PROPS+=("-Pandroid.injected.device.serial=$ADB_SERIAL")
        # AGP's `android.injected.device.serial` silently falls back to "all
        # attached devices" when the serial doesn't match exactly (notably
        # with mDNS-TLS serials like adb-<hwid>-<suffix>._adb-tls-connect._tcp).
        # ANDROID_SERIAL works one level deeper on every spawned adb binary,
        # so the filter holds even when AGP's own matcher doesn't.
        export ANDROID_SERIAL="$ADB_SERIAL"
    fi
}

ensure_device() {
    detect_adb
    # apply_target must have resolved a serial before any device-bound command runs.
    if [[ -z "$ADB_SERIAL" ]]; then
        die "Internal error: ADB_SERIAL not set. This is a bug in run.sh dispatch."
    fi
    if ! "$ADB" devices | awk 'NR>1 && $2=="device" {print $1}' | grep -qx "$ADB_SERIAL"; then
        die "Pinned device '$ADB_SERIAL' is no longer attached. Check './run.sh devices'."
    fi
}

boot_emulator_or_die() {
    local emulator_bin="$HOME/Android/Sdk/emulator/emulator"
    if [[ ! -x "$emulator_bin" ]]; then
        die "No device attached and emulator not found at $emulator_bin."
    fi

    local -a avds
    mapfile -t avds < <("$emulator_bin" -list-avds 2>/dev/null)
    if [[ "${#avds[@]}" -eq 0 ]]; then
        die "No device attached and no AVDs configured. Create one in Android Studio."
    fi

    local chosen=""

    # Env-var override: skip the menu (CI / scripted runs).
    if [[ -n "${WEAR_AVD:-}" ]]; then
        local a
        for a in "${avds[@]}"; do
            if [[ "$a" == "$WEAR_AVD" ]]; then
                chosen="$WEAR_AVD"
                break
            fi
        done
        if [[ -z "$chosen" ]]; then
            die "WEAR_AVD='$WEAR_AVD' not in available AVDs: ${avds[*]}"
        fi
    elif [[ -t 0 ]]; then
        info "No device attached. Available AVDs:"
        local i
        for i in "${!avds[@]}"; do
            printf "  %d) %s\n" "$((i+1))" "${avds[$i]}"
        done
        printf "  0) abort\n"
        local choice
        read -r -p "Start which? " choice
        if [[ ! "$choice" =~ ^[0-9]+$ ]]; then
            die "Invalid choice: '$choice' (expected a number)."
        fi
        if [[ "$choice" -eq 0 ]]; then
            die "Aborted by user."
        fi
        if [[ "$choice" -lt 1 || "$choice" -gt "${#avds[@]}" ]]; then
            die "Out of range: $choice (have ${#avds[@]} AVDs)."
        fi
        chosen="${avds[$((choice-1))]}"
    else
        die "No device attached. Set WEAR_AVD=<name> or run interactively. Available: ${avds[*]}"
    fi

    start_emulator_and_wait "$emulator_bin" "$chosen"
}

start_emulator_and_wait() {
    local emulator_bin="$1"
    local avd="$2"

    # -no-snapshot-load forces a cold boot. Quick Boot's RAM snapshot freezes
    # the wallclock from the last session, and Wear OS in-AVD NTP often does
    # not catch up — case timestamps end up days in the past. Cold boot
    # picks up the host RTC at startup, no in-app correction needed.
    info "Starting AVD '$avd' (cold boot, background)..."
    nohup "$emulator_bin" -avd "$avd" -no-snapshot-load >/dev/null 2>&1 &
    disown

    info "Waiting for ADB to see the device..."
    adb_cmd wait-for-device

    info "Waiting for boot to complete (up to 180s)..."
    local boot=""
    local elapsed=0
    local timeout=180
    until [[ "$boot" == "1" ]]; do
        if [[ "$elapsed" -ge "$timeout" ]]; then
            die "Boot timeout after ${timeout}s (AVD '$avd')."
        fi
        sleep 2
        elapsed=$((elapsed + 2))
        boot=$(adb_cmd shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' || true)
    done
    ok "AVD '$avd' is up (boot took ${elapsed}s)."
}

# --- Subcommands -----------------------------------------------------------
cmd_build() {
    detect_java
    info "Building debug APK..."
    (cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:assembleDebug)
    ok "Built."
}

cmd_install() {
    detect_java
    ensure_device
    info "Installing on device..."
    (cd "$SCRIPT_DIR" && ./gradlew "${GRADLE_PROPS[@]}" :app:installDebug)
    ok "Installed."
}

cmd_start() {
    ensure_device
    info "Starting $PACKAGE/$ACTIVITY..."
    adb_cmd shell am start -n "$PACKAGE/$ACTIVITY" >/dev/null
    ok "Started."
}

cmd_stop() {
    ensure_device
    info "Stopping $PACKAGE..."
    adb_cmd shell am force-stop "$PACKAGE"
    ok "Stopped."
}

cmd_logcat() {
    ensure_device
    local pid
    pid=$(adb_cmd shell pidof "$PACKAGE" | tr -d '\r' | awk '{print $1}')
    if [[ -z "$pid" ]]; then
        die "App $PACKAGE is not running. Use './run.sh start' first."
    fi
    info "Logcat for $PACKAGE (PID $pid). Ctrl-C to exit."
    exec "$ADB" ${ADB_SERIAL:+-s "$ADB_SERIAL"} logcat --pid="$pid"
}

cmd_shot() {
    ensure_device
    local out="${1:-/tmp/wear_screen.png}"
    info "Screenshot -> $out"
    adb_cmd exec-out screencap -p > "$out"
    ok "Saved ($(wc -c < "$out") bytes)."
}

cmd_devices() {
    detect_adb
    info "Attached devices:"
    "$ADB" devices -l | sed '1d;/^$/d' | sed 's/^/  /'
    if [[ -n "$ADB_SERIAL" ]]; then
        info "Pinned by current target: $ADB_SERIAL"
    fi
    local cached
    cached="$(read_cached_watch_serial)"
    if [[ -n "$cached" ]]; then
        info "Cached watch serial (.watch_serial): $cached"
    fi
}

cmd_connect() {
    detect_adb
    local target="${1:-}"
    if [[ -z "$target" ]]; then
        target="$(read_cached_watch_serial)"
        if [[ -z "$target" ]]; then
            die "Usage: ./run.sh connect <ip:port> (no cached serial in .watch_serial yet)."
        fi
        info "Reusing cached watch serial: $target"
    fi
    info "Connecting to $target..."
    "$ADB" connect "$target"
    # adb connect prints success or failure; verify it's actually 'device'.
    if "$ADB" devices | awk 'NR>1 {print $1, $2}' | grep -qx "$target device"; then
        printf '%s\n' "$target" > "$WATCH_SERIAL_FILE"
        ok "Connected and cached as watch serial."
    else
        die "Connect did not yield a 'device' state for $target. Check WiFi-Debug pairing."
    fi
}

cmd_clean() {
    detect_java
    info "Cleaning..."
    (cd "$SCRIPT_DIR" && ./gradlew clean)
    ok "Clean."
}

cmd_test() {
    detect_java
    info "Running JVM unit tests..."
    (cd "$SCRIPT_DIR" && ./gradlew :app:testDebugUnitTest)
    ok "Unit tests passed."
    if [[ "${SKIP_INSTRUMENTED:-0}" == "1" ]]; then
        info "SKIP_INSTRUMENTED=1 set; skipping connectedAndroidTest."
        return
    fi
    ensure_device
    info "Running instrumented tests..."
    (cd "$SCRIPT_DIR" && ./gradlew :app:connectedDebugAndroidTest)
    ok "Instrumented tests passed."
}

cmd_help() {
    # Print the leading comment block (lines 2..first non-comment line).
    awk 'NR==1 { next } /^[^[:space:]#]/ { exit } { print }' "$0" \
        | sed 's/^# \{0,1\}//'
}

# --- Dispatch --------------------------------------------------------------
# Required target prefix (watch | emulator) before the subcommand. The legacy
# "(none)" fallback is gone: every device-bound subcommand must know which
# device it's talking to, otherwise we'd quietly pick "the first" — which is
# exactly what the user asked to stop doing.
if [[ "${1:-}" == "watch" || "${1:-}" == "emulator" ]]; then
    DOCTATE_TARGET="$1"
    shift
fi

# Subcommands that are device- and target-agnostic bypass apply_target so
# they don't trigger a resolver menu as a side-effect. Ordering matters:
# this short-circuit must run before apply_target.
case "${1:-}" in
    help|-h|--help) cmd_help; exit 0 ;;
    devices)        cmd_devices; exit 0 ;;
    connect)        shift; cmd_connect "$@"; exit 0 ;;
    clean)          cmd_clean; exit 0 ;;
esac

apply_target

case "${1:-}" in
    "")                cmd_install; cmd_start ;;
    build)             cmd_build ;;
    install)           cmd_install ;;
    start)             cmd_start ;;
    stop)              cmd_stop ;;
    logcat)            cmd_logcat ;;
    shot)              shift; cmd_shot "$@" ;;
    test)              cmd_test ;;
    *)                 die "Unknown command: $1 (try './run.sh help')" ;;
esac
