#!/data/data/com.termux/files/usr/bin/bash
# AI Keyboard bridge setup (Phase 5a). Idempotent — every step checks current
# state before acting; re-runs reconcile to the same end-state. Privacy: never
# logs API keys, OAuth tokens, persona content, or model output.
# `set -e` deliberately omitted; use explicit `if !...; then` for retry safety.

set -uo pipefail

# `curl … | bash` consumes stdin via the pipe, so every `read` later in the
# script would fail with "stdin closed before confirmation". Detect that case
# and re-open stdin from /dev/tty so confirm prompts + interactive menus +
# OAuth prompts still work. Falls through silently in non-tty contexts (CI,
# `bash setup.sh < /dev/null`), where --yes / --providers flags are the
# appropriate path.
if [ ! -t 0 ] && [ -r /dev/tty ]; then
    exec </dev/tty
fi

# Pinned versions. The Claude Code pin is load-bearing: ≥ 2.1.113 ships
# glibc-only and breaks on Termux's Bionic libc. See PHASE_REVIEW.md
# "Known accepted corner cases" — Phase 12 rechecks before each release.
CLAUDE_CODE_VERSION="2.1.112"
GEMINI_CLI_VERSION="latest"
# Codex pin: v0.43+ regressed on prctl(PR_SET_DUMPABLE) under Termux+proot
# (openai/codex#6757). v0.42.0 is the documented last-known-working version;
# Phase 12 release-prep re-evaluates against the current latest.
CODEX_VERSION="0.42.0"
NODE_REQUIRED_MAJOR=20

TERMUX_PREFIX="/data/data/com.termux/files/usr"
HOME_BIN="$HOME/bin"
CLAUDE_SNAPSHOT="$HOME/claude-code-pinned"
BRIDGE_DEST="$HOME/ai-keyboard-bridge"
BRIDGE_CHECKOUT_CACHE="$HOME/.cache/aikeyboard-bridge-checkout"
SERVICE_DIR="$TERMUX_PREFIX/var/service/ai-keyboard-bridge"
SERVICE_LOG_DIR="$TERMUX_PREFIX/var/log/sv/ai-keyboard-bridge"
TERMUX_PROPS="$HOME/.termux/termux.properties"
BOOT_HOOK_DIR="$HOME/.termux/boot"

# Fallback git source when no local bridge/ tree is available (e.g. running
# setup.sh standalone via `curl … | bash` in Termux). Overridable via
# --bridge-source-git URL[#REF]. REF defaults to DEFAULT_BRIDGE_GIT_REF.
DEFAULT_BRIDGE_GIT_URL="https://github.com/gestrow/ai-keyboard.git"
DEFAULT_BRIDGE_GIT_REF="main"

SUPPORTED_PROVIDERS=(claude gemini codex)
SELECTED_PROVIDERS=()
BRIDGE_SOURCE=""
BRIDGE_SOURCE_GIT=""
BRIDGE_SOURCE_GIT_REF=""
ASSUME_YES=0
PROVIDERS_VIA_FLAG=0
REAUTH_PROVIDER=""

# Resolve script's own directory for `--bridge-source` auto-detection.
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
DEFAULT_BRIDGE_SOURCE="$(dirname "$SCRIPT_DIR")/bridge"

if [[ -t 1 ]]; then
    C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
    C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_RED=$'\033[31m'; C_BLUE=$'\033[34m'
else
    C_RESET=""; C_BOLD=""; C_DIM=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_BLUE=""
fi

log_step()  { printf '\n%s== %s%s\n' "$C_BOLD$C_BLUE" "$1" "$C_RESET"; }
log_info()  { printf '%s%s\n'        "$C_DIM" "$1$C_RESET"; }
log_ok()    { printf '%s✓ %s%s\n'   "$C_GREEN" "$1" "$C_RESET"; }
log_skip()  { printf '%s↷ %s%s\n'   "$C_DIM" "$1" "$C_RESET"; }
log_warn()  { printf '%s! %s%s\n'   "$C_YELLOW" "$1" "$C_RESET" >&2; }
log_error() { printf '%s✗ %s%s\n'   "$C_RED" "$1" "$C_RESET" >&2; }
die()       { log_error "$1"; exit "${2:-1}"; }

ensure_line_in_file() {
    # Append $line to $file iff a verbatim match isn't already there.
    local file="$1" line="$2"
    if [[ ! -f "$file" ]]; then
        mkdir -p "$(dirname "$file")"
        : > "$file"
    fi
    if ! grep -Fxq -- "$line" "$file" 2>/dev/null; then
        printf '%s\n' "$line" >> "$file"
        return 0
    fi
    return 1
}

provider_selected() {
    local p="$1" s
    for s in "${SELECTED_PROVIDERS[@]:-}"; do
        [[ "$s" == "$p" ]] && return 0
    done
    return 1
}

trim() {
    local s="$1"
    s="${s#"${s%%[![:space:]]*}"}"
    s="${s%"${s##*[![:space:]]}"}"
    printf '%s' "$s"
}

node_major() {
    if ! command -v node >/dev/null 2>&1; then
        return 0
    fi
    node --version 2>/dev/null | sed -nE 's/^v([0-9]+).*/\1/p'
}

# Probe http://127.0.0.1:8787/health using node's built-in fetch (Node ≥ 18).
# Echoes the response body on success, returns non-zero on timeout/failure.
probe_health() {
    local timeout_secs="${1:-10}" start now elapsed body
    start=$(date +%s)
    while true; do
        now=$(date +%s)
        elapsed=$((now - start))
        if (( elapsed >= timeout_secs )); then
            return 1
        fi
        if body=$(node -e '
            const ac = new AbortController();
            const t = setTimeout(() => ac.abort(), 2000);
            fetch("http://127.0.0.1:8787/health", { signal: ac.signal })
                .then(r => r.text())
                .then(t => { process.stdout.write(t); })
                .catch(() => process.exit(1))
                .finally(() => clearTimeout(t));
        ' 2>/dev/null); then
            printf '%s\n' "$body"
            return 0
        fi
        sleep 1
    done
}

usage() {
    cat <<'EOF'
Usage: bash setup.sh [OPTIONS]

  --bridge-source DIR  bridge/ tree to deploy (default: <script_dir>/../bridge)
  --bridge-source-git URL[#REF]
                       clone the bridge from a git URL when no local
                       --bridge-source exists. Default URL:
                       https://github.com/gestrow/ai-keyboard.git
                       Default REF: main. Use this for standalone Termux
                       installs (`curl …/setup.sh | bash`).
  --providers LIST     comma list (claude,gemini,codex); skips interactive menu
  --reauth PROVIDER    only run interactive OAuth for one provider
                       (claude|gemini|codex), skipping pkg install / bridge
                       deploy / service registration. Used by the IME's
                       TermuxOrchestrator (Phase 5b).
  -y, --yes            skip the initial confirm; OAuth prompts still pause
  -h, --help           print this and exit

Idempotent — re-run any time.
EOF
}

parse_args() {
    while (( $# > 0 )); do
        case "$1" in
            --bridge-source)   shift; [[ $# -gt 0 ]] || die "--bridge-source requires a path"; BRIDGE_SOURCE="$1"; shift ;;
            --bridge-source=*) BRIDGE_SOURCE="${1#*=}"; shift ;;
            --bridge-source-git)   shift; [[ $# -gt 0 ]] || die "--bridge-source-git requires URL[#REF]"; BRIDGE_SOURCE_GIT="$1"; shift ;;
            --bridge-source-git=*) BRIDGE_SOURCE_GIT="${1#*=}"; shift ;;
            --providers)       shift; [[ $# -gt 0 ]] || die "--providers requires a comma list"; parse_providers_arg "$1"; shift ;;
            --providers=*)     parse_providers_arg "${1#*=}"; shift ;;
            --reauth)          shift; [[ $# -gt 0 ]] || die "--reauth requires a provider name (claude|gemini|codex)"; REAUTH_PROVIDER="$1"; shift ;;
            --reauth=*)        REAUTH_PROVIDER="${1#*=}"; shift ;;
            -y|--yes)          ASSUME_YES=1; shift ;;
            -h|--help)         usage; exit 0 ;;
            *)                 die "Unknown argument: $1 (try --help)" ;;
        esac
    done
    [[ -n "$BRIDGE_SOURCE" ]] || BRIDGE_SOURCE="$DEFAULT_BRIDGE_SOURCE"
    # Split BRIDGE_SOURCE_GIT into url + ref (url#ref form).
    if [[ -n "$BRIDGE_SOURCE_GIT" ]]; then
        if [[ "$BRIDGE_SOURCE_GIT" == *"#"* ]]; then
            BRIDGE_SOURCE_GIT_REF="${BRIDGE_SOURCE_GIT##*#}"
            BRIDGE_SOURCE_GIT="${BRIDGE_SOURCE_GIT%%#*}"
        else
            BRIDGE_SOURCE_GIT_REF="$DEFAULT_BRIDGE_GIT_REF"
        fi
    fi
}

parse_providers_arg() {
    PROVIDERS_VIA_FLAG=1
    SELECTED_PROVIDERS=()
    local raw="$1" part
    IFS=',' read -ra parts <<< "$raw"
    for part in "${parts[@]}"; do
        part=$(trim "$part")
        case "$part" in
            "") ;;
            claude|gemini|codex) SELECTED_PROVIDERS+=("$part") ;;
            *) die "Unknown provider: $part (supported: claude, gemini, codex)" ;;
        esac
    done
    [[ ${#SELECTED_PROVIDERS[@]} -gt 0 ]] || die "--providers list was empty"
}

require_termux() {
    if [[ ! -d "$TERMUX_PREFIX" ]] || ! command -v pkg >/dev/null 2>&1; then
        die "This script must run inside Termux. \$TERMUX_PREFIX ($TERMUX_PREFIX) not found or 'pkg' missing."
    fi
    log_ok "Running inside Termux ($TERMUX_PREFIX)"
}

print_banner() {
    cat <<EOF

${C_BOLD}AI Keyboard — Termux bridge setup${C_RESET}
Pins: Claude Code v${CLAUDE_CODE_VERSION}, Gemini CLI ${GEMINI_CLI_VERSION},
Codex v${CODEX_VERSION}, Node ≥ ${NODE_REQUIRED_MAJOR}.
Steps: termux.properties, pkg install, DNS resolv.conf shim, provider OAuth,
bridge → ${BRIDGE_DEST}, termux-services unit + optional Termux:Boot hook,
/health probe. Safe to re-run.

EOF
}

confirm_proceed_or_exit() {
    if (( ASSUME_YES == 1 )); then log_skip "Confirmation skipped (--yes)"; return 0; fi
    printf '%sProceed? [y/N] %s' "$C_BOLD" "$C_RESET"
    local reply
    read -r reply || die "stdin closed before confirmation"
    case "$reply" in
        y|Y|yes|YES) ;;
        *) log_info "Aborted by user."; exit 0 ;;
    esac
}

set_allow_external_apps() {
    log_step "1/11 allow-external-apps in $TERMUX_PROPS"
    if ensure_line_in_file "$TERMUX_PROPS" "allow-external-apps = true"; then
        if command -v termux-reload-settings >/dev/null 2>&1; then
            termux-reload-settings || log_warn "termux-reload-settings exited non-zero (continuing)"
        fi
        log_ok "Wrote allow-external-apps = true"
    else
        log_skip "allow-external-apps already set"
    fi
}

install_termux_packages() {
    log_step "2/11 Installing Termux packages"
    local pkgs=(nodejs git termux-api which ripgrep)
    local missing=()
    local p
    for p in "${pkgs[@]}"; do
        if ! dpkg -s "$p" >/dev/null 2>&1; then
            missing+=("$p")
        fi
    done
    if (( ${#missing[@]} == 0 )); then
        log_skip "All required packages already installed (${pkgs[*]})"
    else
        log_info "Installing: ${missing[*]}"
        if ! pkg update -y >/dev/null 2>&1; then
            log_warn "pkg update returned non-zero — continuing with install"
        fi
        if ! pkg install -y "${missing[@]}"; then
            die "pkg install failed for: ${missing[*]} — check connectivity and retry"
        fi
        log_ok "Installed ${missing[*]}"
    fi

    local major
    major=$(node_major)
    if [[ -z "$major" ]]; then
        die "node is missing after pkg install — bailing"
    fi
    if (( major < NODE_REQUIRED_MAJOR )); then
        die "node v$major found, but bridge requires Node ≥ v$NODE_REQUIRED_MAJOR. Run \`pkg upgrade nodejs\` and retry."
    fi
    log_ok "node v$(node --version | sed 's/^v//') ≥ v$NODE_REQUIRED_MAJOR"
}

select_providers() {
    log_step "3/11 Selecting providers"
    if (( PROVIDERS_VIA_FLAG == 1 )); then
        log_skip "Providers set via --providers: ${SELECTED_PROVIDERS[*]}"
        return 0
    fi

    cat <<EOF

  1) Claude + Gemini only
  2) Claude only
  3) Gemini only
  4) Codex only
  5) All three: Claude + Gemini + Codex  ${C_DIM}[recommended]${C_RESET}

EOF
    local choice
    while true; do
        printf '%sChoose 1-5 (default 5): %s' "$C_BOLD" "$C_RESET"
        if ! read -r choice; then
            choice=""
        fi
        case "$choice" in
            1)    SELECTED_PROVIDERS=(claude gemini); break ;;
            2)    SELECTED_PROVIDERS=(claude); break ;;
            3)    SELECTED_PROVIDERS=(gemini); break ;;
            4)    SELECTED_PROVIDERS=(codex); break ;;
            ""|5) SELECTED_PROVIDERS=(claude gemini codex); break ;;
            *)    log_warn "Invalid choice. Enter 1, 2, 3, 4, or 5." ;;
        esac
    done
    log_ok "Will install: ${SELECTED_PROVIDERS[*]}"
}

# Termux ships without /etc/resolv.conf; Codex's Rust DNS resolver fails with
# "Stream disconnected before completion" without one (openai/codex#11809).
# Idempotent: if the file already exists and is non-empty, leave it alone so
# we don't clobber the user's custom DNS config. Termux-wide fix that
# benefits any future Rust-based CLI we ship.
ensure_resolv_conf() {
    log_step "4/11 DNS resolver shim ($TERMUX_PREFIX/etc/resolv.conf)"
    local resolv="$TERMUX_PREFIX/etc/resolv.conf"
    if [[ -f "$resolv" && -s "$resolv" ]]; then
        log_skip "resolv.conf already present at $resolv"
        return 0
    fi
    log_info "Writing nameserver entries (openai/codex#11809 — Termux has none by default)"
    mkdir -p "$TERMUX_PREFIX/etc"
    cat > "$resolv" <<'RESOLV_EOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
RESOLV_EOF
    log_ok "DNS shim written to $resolv"
}

install_claude_code_if_selected() {
    log_step "5/11 Claude Code (v$CLAUDE_CODE_VERSION pinned)"
    if ! provider_selected claude; then
        log_skip "Claude not selected"
        return 0
    fi

    # Disable the autoupdater FIRST — write to BOTH ~/.profile (login-shell
    # source) and ~/.bashrc (RUN_COMMAND subshells). Belt and suspenders.
    local marker="export DISABLE_AUTOUPDATER=1  # ai-keyboard: pin Claude Code (do not remove)"
    local wrote=0
    ensure_line_in_file "$HOME/.profile" "$marker" && wrote=1
    ensure_line_in_file "$HOME/.bashrc"  "$marker" && wrote=1
    if (( wrote == 1 )); then
        log_ok "DISABLE_AUTOUPDATER=1 exported from ~/.profile and ~/.bashrc"
    else
        log_skip "DISABLE_AUTOUPDATER already set in ~/.profile and ~/.bashrc"
    fi
    export DISABLE_AUTOUPDATER=1  # this shell also needs it for the install below

    # Clear any clobbered prior install (idempotent — exits 0 if not installed).
    log_info "Clearing any prior @anthropic-ai/claude-code install"
    npm uninstall -g @anthropic-ai/claude-code >/dev/null 2>&1 || true

    log_info "npm i -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
    if ! npm i -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"; then
        die "npm install failed for @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}. If the version was pulled from the registry, try the next-lower 2.1.x and file an issue."
    fi

    local npm_root
    npm_root=$(npm root -g 2>/dev/null)
    [[ -n "$npm_root" ]] || die "could not resolve \`npm root -g\`"
    local cli_js="$npm_root/@anthropic-ai/claude-code/cli.js"
    [[ -f "$cli_js" ]] || die "Expected $cli_js not found after install — pinned version may have dropped cli.js. See ARCHITECTURE.md Termux compat section."
    log_ok "cli.js present at $cli_js"

    # Snapshot outside npm's reach so a future autoupdate can't clobber it.
    log_info "Snapshotting install to $CLAUDE_SNAPSHOT"
    rm -rf "$CLAUDE_SNAPSHOT"
    cp -r "$npm_root/@anthropic-ai/claude-code" "$CLAUDE_SNAPSHOT" \
        || die "failed to snapshot Claude Code to $CLAUDE_SNAPSHOT"
    [[ -f "$CLAUDE_SNAPSHOT/cli.js" ]] || die "snapshot is missing cli.js"

    # Wrapper at ~/bin/claude pointing at the snapshot (not npm's path).
    mkdir -p "$HOME_BIN"
    cat > "$HOME_BIN/claude" <<'WRAPPER_EOF'
#!/data/data/com.termux/files/usr/bin/bash
# AI Keyboard wrapper for Claude Code v2.1.112. Snapshot at
# ~/claude-code-pinned/ is npm-untouched so the autoupdater can't clobber it.
exec node "$HOME/claude-code-pinned/cli.js" "$@"
WRAPPER_EOF
    chmod +x "$HOME_BIN/claude"
    log_ok "Wrapper at $HOME_BIN/claude"

    # Symlink into $PREFIX/bin (always on PATH; rc-file PATH export isn't
    # reliable across login-shell vs RUN_COMMAND-subshell paths).
    ln -sf "$HOME_BIN/claude" "$TERMUX_PREFIX/bin/claude" \
        || die "failed to symlink $HOME_BIN/claude into $TERMUX_PREFIX/bin"
    log_ok "Symlinked $TERMUX_PREFIX/bin/claude → $HOME_BIN/claude"

    # Best-effort: older versions lack `claude config`; env flag is real defense.
    if claude config set -g autoUpdates false >/dev/null 2>&1; then
        log_ok "claude config: autoUpdates=false"
    else
        log_skip "claude config set autoUpdates=false (unsupported on this version — env flag covers it)"
    fi

    local version_out
    version_out=$(claude --version 2>&1) || die "claude --version failed; wrapper/symlink broken"
    if [[ "$version_out" == *"$CLAUDE_CODE_VERSION"* ]]; then
        log_ok "claude --version: $(printf '%s' "$version_out" | head -1)"
    else
        die "claude --version='$version_out', expected to contain $CLAUDE_CODE_VERSION — investigate wrapper/symlink/snapshot."
    fi
}

install_gemini_cli_if_selected() {
    log_step "6/11 Gemini CLI"
    if ! provider_selected gemini; then
        log_skip "Gemini not selected"
        return 0
    fi
    log_info "npm i -g @google/gemini-cli@${GEMINI_CLI_VERSION}"
    if ! npm i -g "@google/gemini-cli@${GEMINI_CLI_VERSION}"; then
        die "npm install failed for @google/gemini-cli — check connectivity and retry"
    fi
    if ! command -v gemini >/dev/null 2>&1; then
        die "gemini binary not on PATH after install — npm misconfigured?"
    fi
    local v
    v=$(gemini --version 2>&1 | head -1 || true)
    log_ok "gemini installed: ${v:-<no version output>}"
}

install_codex_if_selected() {
    log_step "7/11 Codex CLI (v$CODEX_VERSION pinned)"
    if ! provider_selected codex; then
        log_skip "Codex not selected"
        return 0
    fi
    # Codex ships a thin JS shim + vendored Rust binary in the npm tarball;
    # there is no postinstall script, no autoupdater. Plain npm install is
    # durable (vs Claude Code's snapshot+wrapper dance).
    log_info "npm i -g @openai/codex@${CODEX_VERSION}"
    if ! npm i -g "@openai/codex@${CODEX_VERSION}"; then
        die "npm install failed for @openai/codex@${CODEX_VERSION}. v0.42.0 is the documented last-known-working Termux target; if it's been pulled from npm, file an issue."
    fi
    if ! command -v codex >/dev/null 2>&1; then
        die "codex binary not on PATH after install — npm misconfigured?"
    fi
    # Empirical Termux launch check — fail fast if the binary doesn't even
    # exec on this device. Per openai/codex#6757, some Termux+proot setups
    # crash inside prctl(PR_SET_DUMPABLE, 0); pinning to v0.42.0 sidesteps
    # the known regression, but the long tail of Termux builds may still hit
    # it. A clear early error beats a cryptic /chat failure later.
    if ! codex --version >/dev/null 2>&1; then
        die "codex --version failed. This Termux install may hit the prctl regression (openai/codex#6757). Try Termux outside proot, or pin to an older version (e.g., 0.41.0)."
    fi
    log_ok "codex installed: $(codex --version 2>&1 | head -1)"
}

run_oauth_for_claude() {
    run_one_oauth claude "$HOME/.claude/.credentials.json" \
        "Claude Code launches interactively. Sign in at claude.ai, then /exit (or Ctrl+C) to return."
}

run_oauth_for_gemini() {
    run_one_oauth gemini "$HOME/.gemini/oauth_creds.json" \
        "Gemini CLI launches interactively. Sign in with Google, then /quit (or Ctrl+C) to return."
}

run_oauth_for_codex() {
    # Codex's device-code OAuth prints a URL + one-time code; the user
    # completes auth on any device. Better than Claude Code's loopback flow
    # — no termux-open / browser handoff on the phone is needed.
    run_one_oauth_cmd codex "$HOME/.codex/auth.json" \
        "Codex prints a verification URL and one-time code. Open the URL on any device, paste the code, sign in with ChatGPT, then return here." \
        login
}

run_oauth_flows() {
    log_step "8/11 OAuth flows"
    if provider_selected claude; then
        run_oauth_for_claude
    fi
    if provider_selected gemini; then
        run_oauth_for_gemini
    fi
    if provider_selected codex; then
        run_oauth_for_codex
    fi
}

run_one_oauth() {
    local cli="$1" creds_path="$2" preamble="$3"
    local short="${creds_path/#$HOME/\~}"
    printf '\n%sProvider: %s%s\n%s\n\n' "$C_BOLD" "$cli" "$C_RESET" "$preamble"

    local already=0
    if [[ -f "$creds_path" ]]; then
        already=1
        log_info "Existing credentials at $short — auth already complete."
    fi

    printf '%sEnter to launch %s, s+Enter to skip: %s' "$C_BOLD" "$cli" "$C_RESET"
    local choice
    if ! read -r choice; then choice="s"; fi
    case "$choice" in
        s|S|skip|SKIP)
            if (( already == 1 )); then log_ok "Skipped (already authenticated)"
            else log_warn "Skipped — bridge will report '$cli: not authenticated' until you re-run \`$cli\`."
            fi
            return 0
            ;;
    esac

    # Foreground launch — stdio passes through so the user can do OAuth.
    log_info "Launching: $cli"
    "$cli" || log_warn "$cli exited non-zero (Ctrl+C? — verifying auth state anyway)"

    if [[ -f "$creds_path" ]]; then
        log_ok "$cli authenticated (credentials at $short)"
    else
        log_warn "$cli completed but no credentials file at $short — re-run and pick this provider again."
    fi
}

run_one_oauth_cmd() {
    # Variant for CLIs that require an explicit subcommand to enter the login
    # flow (e.g. `codex login` vs Claude/Gemini's bare `claude` / `gemini`).
    # All other semantics match run_one_oauth.
    local cli="$1" creds_path="$2" preamble="$3" subcommand="$4"
    local short="${creds_path/#$HOME/\~}"
    printf '\n%sProvider: %s%s\n%s\n\n' "$C_BOLD" "$cli" "$C_RESET" "$preamble"

    local already=0
    if [[ -f "$creds_path" ]]; then
        already=1
        log_info "Existing credentials at $short — auth already complete."
    fi

    printf '%sEnter to launch %s %s, s+Enter to skip: %s' "$C_BOLD" "$cli" "$subcommand" "$C_RESET"
    local choice
    if ! read -r choice; then choice="s"; fi
    case "$choice" in
        s|S|skip|SKIP)
            if (( already == 1 )); then log_ok "Skipped (already authenticated)"
            else log_warn "Skipped — bridge will report '$cli: not authenticated' until you re-run \`$cli $subcommand\`."
            fi
            return 0
            ;;
    esac

    log_info "Launching: $cli $subcommand"
    "$cli" "$subcommand" || log_warn "$cli $subcommand exited non-zero (Ctrl+C? — verifying auth state anyway)"

    if [[ -f "$creds_path" ]]; then
        log_ok "$cli authenticated (credentials at $short)"
    else
        log_warn "$cli $subcommand completed but no credentials file at $short — re-run and complete the device-code flow."
    fi
}

deploy_bridge() {
    log_step "9/11 Deploying bridge to $BRIDGE_DEST"

    if [[ ! -d "$BRIDGE_SOURCE" ]]; then
        # No local bridge/ tree — clone from a git URL. Defaults to the public
        # gestrow/ai-keyboard repo; --bridge-source-git overrides.
        local git_url="$BRIDGE_SOURCE_GIT" git_ref="$BRIDGE_SOURCE_GIT_REF"
        if [[ -z "$git_url" ]]; then
            git_url="$DEFAULT_BRIDGE_GIT_URL"
            git_ref="$DEFAULT_BRIDGE_GIT_REF"
        fi
        log_info "No local bridge tree at $BRIDGE_SOURCE"
        log_info "Cloning $git_url (ref: $git_ref) → $BRIDGE_CHECKOUT_CACHE"
        command -v git >/dev/null 2>&1 \
            || die "git not on PATH — the pkg-install step should have provided it. Run \`pkg install git\` and retry."
        rm -rf "$BRIDGE_CHECKOUT_CACHE"
        mkdir -p "$(dirname "$BRIDGE_CHECKOUT_CACHE")"
        # First attempt: shallow clone of the named ref (works for branches + tags).
        if ! git clone --depth 1 --branch "$git_ref" "$git_url" "$BRIDGE_CHECKOUT_CACHE" >/dev/null 2>&1; then
            # Fallback for SHA refs: full clone + checkout.
            rm -rf "$BRIDGE_CHECKOUT_CACHE"
            git clone "$git_url" "$BRIDGE_CHECKOUT_CACHE" >/dev/null 2>&1 \
                || die "git clone of $git_url failed. Check network/URL and retry, or pass --bridge-source DIR with a local tree."
            ( cd "$BRIDGE_CHECKOUT_CACHE" && git checkout "$git_ref" >/dev/null 2>&1 ) \
                || die "git checkout $git_ref failed in cloned repo."
        fi
        BRIDGE_SOURCE="$BRIDGE_CHECKOUT_CACHE/bridge"
        [[ -d "$BRIDGE_SOURCE" ]] \
            || die "Cloned repo does not contain a bridge/ subdirectory at $BRIDGE_SOURCE — wrong --bridge-source-git URL?"
        log_ok "Bridge source cloned to $BRIDGE_SOURCE"
    fi
    if [[ ! -f "$BRIDGE_SOURCE/server.js" || ! -f "$BRIDGE_SOURCE/package.json" ]]; then
        die "$BRIDGE_SOURCE doesn't look like a bridge dir (missing server.js or package.json)"
    fi
    log_info "Source: $BRIDGE_SOURCE"

    # Clean-slate copy: source's node_modules may carry arch-mismatched binaries
    # from the dev box, so we drop them and let `npm install` rebuild for Termux.
    rm -rf "$BRIDGE_DEST"
    mkdir -p "$BRIDGE_DEST"
    cp -r "$BRIDGE_SOURCE/." "$BRIDGE_DEST/" || die "failed to copy bridge into $BRIDGE_DEST"
    rm -rf "$BRIDGE_DEST/node_modules"

    log_info "Installing bridge dependencies (npm install --omit=dev)"
    ( cd "$BRIDGE_DEST" && npm install --omit=dev ) \
        || die "npm install failed in $BRIDGE_DEST — check connectivity and retry"

    # Smoke-test load (server.js's start() only runs when require.main===module).
    if ! node -e 'require(process.argv[1])' "$BRIDGE_DEST/server.js" >/dev/null 2>&1; then
        die "node could not load $BRIDGE_DEST/server.js — check the deploy"
    fi
    log_ok "Bridge deployed and loadable"
}

ensure_runsvdir_running() {
    # termux-services' supervisor (runsvdir) is started by
    # $PREFIX/etc/profile.d/start-services.sh on Termux session start. On a
    # fresh install — or on any session that predates the install —
    # runsvdir isn't running yet, and `sv up` / `sv-enable` are no-ops.
    # Start it ourselves with setsid so it survives our shell exit.
    if pgrep -f "runsvdir.*var/service" >/dev/null 2>&1; then
        return 0
    fi
    log_info "Starting runsvdir (termux-services supervisor)"
    setsid "$TERMUX_PREFIX/bin/runsvdir" -P "$TERMUX_PREFIX/var/service" \
        >/dev/null 2>&1 < /dev/null &
    disown 2>/dev/null || true
    sleep 1
    if pgrep -f "runsvdir.*var/service" >/dev/null 2>&1; then
        log_ok "runsvdir started"
    else
        die "runsvdir failed to start. Try fully closing and reopening Termux, then re-run this script."
    fi
}

register_services() {
    log_step "10/11 Registering termux-services unit"

    if ! command -v sv >/dev/null 2>&1; then
        log_info "termux-services not installed — installing"
        pkg install -y termux-services || die "pkg install termux-services failed"
    fi

    mkdir -p "$SERVICE_DIR" "$SERVICE_DIR/log" "$SERVICE_LOG_DIR"

    # Main run script. HOME + PATH set explicitly: runit starts services with
    # a minimal env, and adapters need HOME for ~/.claude/.credentials.json.
    cat > "$SERVICE_DIR/run" <<RUN_EOF
#!/data/data/com.termux/files/usr/bin/sh
exec 2>&1
export HOME=$HOME
export PATH=$TERMUX_PREFIX/bin:$HOME_BIN:\$PATH
export DISABLE_AUTOUPDATER=1
cd "\$HOME/ai-keyboard-bridge"
exec node server.js
RUN_EOF
    chmod +x "$SERVICE_DIR/run"

    # Log run script — svlogd rotates by size (default 1 MB × 10 files); -tt
    # prefixes lines with TAI64N timestamps. Output → $SERVICE_LOG_DIR/current.
    cat > "$SERVICE_DIR/log/run" <<LOG_EOF
#!/data/data/com.termux/files/usr/bin/sh
exec svlogd -tt $SERVICE_LOG_DIR
LOG_EOF
    chmod +x "$SERVICE_DIR/log/run"

    log_ok "Service registered at $SERVICE_DIR"
    log_info "Logs will be written to $SERVICE_LOG_DIR/current"

    ensure_runsvdir_running

    # runsvdir polls for new services every ~5s; wait for the per-service
    # `runsv` supervisor to attach (it creates supervise/ok) before issuing
    # sv commands, otherwise `sv up` fails with 'unable to open supervise/ok'.
    local waited=0
    while (( waited < 15 )); do
        [[ -e "$SERVICE_DIR/supervise/ok" ]] && break
        sleep 1
        waited=$((waited + 1))
    done
    if [[ ! -e "$SERVICE_DIR/supervise/ok" ]]; then
        log_warn "runsv did not attach to $SERVICE_DIR within ${waited}s — sv commands may fail"
    fi

    # `sv-enable` is idempotent — symlinks the service into the active
    # service directory iff not already linked.
    if ! sv-enable ai-keyboard-bridge >/dev/null 2>&1; then
        log_warn "sv-enable returned non-zero — service may already be enabled (continuing)"
    else
        log_ok "Service enabled (sv-enable ai-keyboard-bridge)"
    fi

    # Termux:Boot autostart — opt-in, only if companion app is installed
    # (it creates ~/.termux/boot/ on first launch). termux-wake-lock keeps
    # the service responsive when the screen is off.
    if [[ -d "$BOOT_HOOK_DIR" ]]; then
        cat > "$BOOT_HOOK_DIR/start-ai-keyboard-bridge" <<'BOOT_EOF'
#!/data/data/com.termux/files/usr/bin/sh
termux-wake-lock
sv up ai-keyboard-bridge
BOOT_EOF
        chmod +x "$BOOT_HOOK_DIR/start-ai-keyboard-bridge"
        log_ok "Termux:Boot autostart hook at $BOOT_HOOK_DIR/start-ai-keyboard-bridge"
    else
        log_info "Termux:Boot not detected — install from F-Droid for boot-time bridge autostart."
    fi
}

start_bridge() {
    log_step "11/11 Starting bridge and probing /health"

    # `sv restart`, not `sv up`: redeploys must bounce the running node
    # process so the new on-disk server.js actually takes effect. On first
    # run runit treats restart-of-down as start.
    if ! sv restart ai-keyboard-bridge >/dev/null 2>&1; then
        log_warn "sv restart returned non-zero — checking status anyway"
    fi
    sleep 2  # let runit spawn the supervised process before /health probe

    log_info "Probing http://127.0.0.1:8787/health (up to 10s)..."
    local body
    if body=$(probe_health 10); then
        log_ok "Bridge is responding"
        printf '%s%s%s\n' "$C_DIM" "$body" "$C_RESET"
        # Sanity-check ok=true. The body is JSON; do a lightweight contains check.
        case "$body" in
            *'"ok":true'*) ;;
            *) log_warn "Body received but \"ok\":true not present — check provider auth state above" ;;
        esac
    else
        log_error "Bridge /health did not respond within 10s"
        local current_log="$SERVICE_LOG_DIR/current"
        if [[ -f "$current_log" ]]; then
            printf '\n%sLast 30 log lines from %s:%s\n' "$C_DIM" "$current_log" "$C_RESET"
            tail -n 30 "$current_log" || true
        else
            log_info "No log file yet at $current_log — service may not have started. Try: sv status ai-keyboard-bridge"
        fi
        die "Bridge failed to start. See logs above; remediate and re-run this script."
    fi
}

print_success() {
    cat <<EOF

${C_BOLD}${C_GREEN}✓ Setup complete.${C_RESET}
  Bridge:   http://127.0.0.1:8787
  Restart:  sv up ai-keyboard-bridge
  Stop:     sv down ai-keyboard-bridge
  Status:   sv status ai-keyboard-bridge
  Logs:     tail -f $SERVICE_LOG_DIR/current
  Re-run:   bash $0   ${C_DIM}# idempotent${C_RESET}

Pick "Termux bridge" as the active backend in AI Keyboard's settings (Phase 6+).

EOF
}

run_reauth_only() {
    # Limited mode used by the IME's TermuxOrchestrator (Phase 5b). Skips
    # pkg install, bridge deploy, service registration, and the success
    # banner — only opens the named CLI for an interactive OAuth flow.
    # Existing in-flight bridge sessions don't need a restart: each /chat
    # spawns a fresh CLI subprocess that re-reads the cred file.
    log_step "Re-auth: $REAUTH_PROVIDER"
    case "$REAUTH_PROVIDER" in
        claude) run_oauth_for_claude ;;
        gemini) run_oauth_for_gemini ;;
        codex)  run_oauth_for_codex ;;
        *) die "Unknown provider: $REAUTH_PROVIDER. Valid: claude, gemini, codex." 2 ;;
    esac
}

main() {
    parse_args "$@"
    require_termux
    if [[ -n "$REAUTH_PROVIDER" ]]; then
        run_reauth_only
        exit 0
    fi
    print_banner
    confirm_proceed_or_exit
    set_allow_external_apps
    install_termux_packages
    select_providers
    ensure_resolv_conf
    install_claude_code_if_selected
    install_gemini_cli_if_selected
    install_codex_if_selected
    run_oauth_flows
    deploy_bridge
    register_services
    start_bridge
    print_success
}

main "$@"
