#!/usr/bin/sh
# Per-user bootstrap for the timbre transcription daemon (timbred).
#
# The package ships timbred's Python sources under /usr/lib/timbre, but their ML
# dependencies (faster-whisper, onnx-asr, onnxruntime, optional CUDA wheels) are
# pip-only — none exist as distro packages — so they cannot live in the .deb/.rpm.
# This shim provisions a PER-USER virtualenv the first time the daemon runs for a
# user (and rebuilds it when the shipped package changes), then execs the real
# timbred from it.
#
# No root, no `sudo timbre-setup` for this: the venv lives in the calling user's
# own data dir and model weights download into that user's cache, so each user
# gets an isolated, unprivileged install the first time their `systemctl --user`
# unit starts. Device access (uinput + keyboard) needs no setup either: a shipped
# udev rule (data/70-timbre.rules) grants it to the seat user via logind uaccess.
set -eu

CORE=/usr/lib/timbre/core
DAEMON=/usr/lib/timbre/daemon

DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
VENV="$DATA_HOME/timbre/venv"
PY="$VENV/bin/python"
STAMP="$VENV/.build-stamp"

if [ ! -f "$DAEMON/pyproject.toml" ] || [ ! -f "$CORE/pyproject.toml" ]; then
    echo "timbred: shipped sources missing under /usr/lib/timbre — reinstall the package." >&2
    exit 1
fi

# GPU extra when an NVIDIA GPU is present (cuDNN/cuBLAS + onnxruntime-gpu);
# CPU-only otherwise. The choice is part of the build signature below, so moving
# the venv between a GPU and a non-GPU machine rebuilds it.
EXTRA=""
GPU="cpu"
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null 2>&1; then
    EXTRA="[gpu]"
    GPU="gpu"
fi

# Build signature: the shipped sources are installed NON-editable (copied into
# the venv), because /usr/lib/timbre is root-owned and read-only to this user —
# an editable install would try to write *.egg-info there. So the venv must be
# rebuilt whenever any shipped file changes; the stamp is a hash of both trees
# plus the GPU flag. Build/VCS/cache artifacts (build/, dist/, *.egg-info/,
# __pycache__, *.pyc) are pruned: `pip install` drops them INTO the source tree
# when it is writable, and counting them would change the signature after every
# build and force a needless rebuild on the next start. `sort` makes it
# order-independent.
SIG="$( { find "$CORE" "$DAEMON" \
            \( -name build -o -name dist -o -name '*.egg-info' \
               -o -name '*.dist-info' -o -name __pycache__ -o -name .git \) -prune \
            -o -type f ! -name '*.pyc' -exec sha256sum {} + ; \
          echo "$GPU"; } | sort | sha256sum | cut -d' ' -f1 )"

# Fast path: a venv built from the current sources already exists — just run it.
if [ -x "$VENV/bin/timbred" ] && [ "$(cat "$STAMP" 2>/dev/null || true)" = "$SIG" ]; then
    exec "$VENV/bin/timbred" "$@"
fi

mkdir -p "$DATA_HOME/timbre"

# Pick the interpreter to build the venv with. NOT a blind `python3`: the ML
# stack (av, torch, onnxruntime, ctranslate2) ships wheels only for released
# CPython lines, and a brand-new system python (e.g. 3.14) has none yet under our
# pins — pip then compiles them from source and fails (missing ffmpeg/BLAS dev
# headers), looping forever. So choose the NEWEST interpreter in the supported
# range [3.11, 3.14) that can actually create a venv (ensurepip present), and
# print it. Empty output → none found. Update the range when the ML wheels catch
# up to a new CPython.
pick_python() {
    for c in python3.13 python3.12 python3.11; do
        p="$(command -v "$c" 2>/dev/null)" || continue
        "$p" -c 'import ensurepip' >/dev/null 2>&1 || continue
        printf '%s\n' "$p"
        return 0
    done
    # Then a bare `python3`, but only if it is itself in range and has
    # ensurepip (so we never silently fall onto an unsupported system python).
    if p="$(command -v python3 2>/dev/null)" && "$p" - <<'PYEOF' >/dev/null 2>&1; then
import sys, ensurepip  # noqa: F401
raise SystemExit(0 if (3, 11) <= sys.version_info < (3, 14) else 1)
PYEOF
        printf '%s\n' "$p"
        return 0
    fi
    # Last resort: a uv-managed 3.13, downloaded per-user into ~/.local/share/uv
    # (Arch ships only the newest python, and the arch package depends on uv).
    # From / with VIRTUAL_ENV unset, so uv can't pick up a stray venv instead.
    command -v uv >/dev/null 2>&1 || return 1
    (cd / && env -u VIRTUAL_ENV uv python install --quiet 3.13 >&2 \
        && env -u VIRTUAL_ENV uv python find --managed-python --no-project 3.13)
}

# Each step is guarded with `|| return 1` on purpose: build() is called from an
# `if`/`!` context, where `set -e` is suppressed inside the function, so a bare
# failing command would NOT abort. The stamp is written only after the entry
# point is confirmed present, so a partial/broken venv never looks "built".
build() {
    PYBIN="$(pick_python)"
    if [ -z "$PYBIN" ]; then
        echo "timbred: no supported Python found. Need 3.11, 3.12 or 3.13 with venv support" >&2
        echo "         (your default python3 is too new for the ML wheels). Install one, e.g." >&2
        echo "         Debian/Ubuntu: sudo apt install python3.12-venv  |  Fedora: sudo dnf install python3.12" >&2
        return 1
    fi
    echo "timbred: provisioning per-user environment at $VENV ($GPU, $("$PYBIN" -V 2>&1))…" >&2
    rm -rf "$VENV"
    "$PYBIN" -m venv "$VENV" || return 1
    "$PY" -m pip install --quiet --upgrade pip || return 1
    # /usr/lib/timbre is root-owned and read-only to this user, but pip builds a
    # local source directory IN PLACE — setuptools writes *.egg-info into it,
    # which fails with EACCES against a read-only tree. So stage writable copies
    # in a temp dir and install from those (chmod u+w: cp may carry the source's
    # read-only mode). timbre-core is a local path dep of timbred and not on
    # PyPI, so install it first; then timbred itself (+[gpu]).
    STAGE="$(mktemp -d)" || return 1
    cp -r "$CORE" "$STAGE/core" && cp -r "$DAEMON" "$STAGE/daemon" \
        && chmod -R u+w "$STAGE" || { rm -rf "$STAGE"; return 1; }
    if ! "$PY" -m pip install --quiet "$STAGE/core" \
        || ! "$PY" -m pip install --quiet "$STAGE/daemon$EXTRA"; then
        rm -rf "$STAGE"
        return 1
    fi
    rm -rf "$STAGE"
    if [ "$GPU" = "gpu" ]; then
        # onnx-asr pulls the CPU onnxruntime; [gpu] adds onnxruntime-gpu. The two
        # are mutually exclusive — with both present the CPU build shadows the GPU
        # providers (Parakeet silently runs on CPU). Keep only the GPU wheel: it
        # ships BOTH the CUDA and CPU providers.
        "$PY" -m pip uninstall -y onnxruntime onnxruntime-gpu >/dev/null 2>&1 || true
        "$PY" -m pip install --quiet onnxruntime-gpu || return 1
    fi
    [ -x "$VENV/bin/timbred" ] || return 1
    printf '%s' "$SIG" > "$STAMP"
    echo "timbred: environment ready." >&2
}

run_build() {
    if ! build; then
        rm -rf "$VENV"
        echo "timbred: environment build failed (offline?). Retries on next start." >&2
        exit 1
    fi
}

# Serialize the build so two concurrent starts don't collide on the same venv.
# The waiter re-checks the stamp after acquiring the lock and skips the rebuild.
if command -v flock >/dev/null 2>&1; then
    exec 9>"$DATA_HOME/timbre/.build.lock"
    flock 9
    if [ -x "$VENV/bin/timbred" ] && [ "$(cat "$STAMP" 2>/dev/null || true)" = "$SIG" ]; then
        exec "$VENV/bin/timbred" "$@"
    fi
    run_build
else
    run_build
fi

exec "$VENV/bin/timbred" "$@"
