#!/bin/bash

# Resolve an interpreter for a major.minor Python version, installing one if the
# host does not already have a usable one (issue #612).
#
# Prints the absolute path of the interpreter on stdout. Everything else — every
# diagnostic, every progress line — goes to stderr, so the caller can capture the
# path with a plain command substitution.
#
# Run by deploy.yml before it touches the server's checkout, and safe to run by
# hand on the host to provision an interpreter ahead of a deploy:
#
#     bash scripts/ensure-python.sh 3.12
#
# Why this exists: roundup-server/.python-version chooses the interpreter, and
# the deploy rebuilds the server's virtualenv when it moves. Debian is not a
# reliable source for a *specific* version — bookworm packages 3.11 and nothing
# else, trixie jumps to 3.13 — so "apt-get install python3.12" is not an option
# on the deployed host, and requiring a human to prepare it by hand puts a manual
# step in front of every future version change.
#
# The order below prefers what the host already has and only downloads as a last
# resort. A distro-packaged interpreter is used when there is one; uv's managed
# builds (prebuilt CPython from python-build-standalone) fill the gap when there
# is not.

set -euo pipefail

log() { printf '%s\n' "$*" >&2; }

# Pinned rather than "latest", so the installer that runs is one someone chose
# and a deploy does not silently pick up a new uv. Be clear about what that is
# and is not: it is a reproducibility control, not an integrity one. The URL is
# mutable, and without a digest pinned here there is nothing to check the
# download against. Verifying a per-architecture sha256 published with the
# release would be the stronger control.
#
# It also bounds which Python versions this can install: uv carries its own
# manifest of prebuilt CPythons, so a version released after this uv cannot be
# installed until the pin moves. The install failure below says so.
UV_VERSION="0.8.17"

if [ "$#" -ne 1 ]; then
  log "Usage: $0 <major.minor>    (e.g. $0 3.12)"
  exit 2
fi

VERSION="$1"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
  log "Error: expected a major.minor version such as '3.12', got '$VERSION'."
  exit 2
fi

# A candidate is only usable if it is the right version *and* can build a
# virtualenv. Debian splits the standard library's venv module into a separate
# python3.N-venv package, so a system interpreter can be the right version and
# still be unable to do the one thing the deploy needs it for. Rejecting it here
# means the uv path below is tried instead of the deploy failing later.
# Returns 0 if usable, 1 if it is not this version or not runnable at all, and
# 2 specifically when it is the right version but cannot build a virtualenv —
# the caller uses that to decide whether the Debian hint is the right advice.
usable() {
  local py="$1"
  [ -n "$py" ] && [ -x "$py" ] || return 1
  local got
  got="$("$py" -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null)" || return 1
  [ "$got" = "$VERSION" ] || return 1
  # Actually build one rather than checking that `venv` and `ensurepip` import.
  # Debian's ensurepip loads pip's wheel from a separate python3-pip-whl
  # package, so the import can succeed while `-m venv` still fails with
  # "ensurepip is not available" — which would surface inside the deploy, after
  # the checkout has been reset, instead of here.
  local probe rc=0
  probe="$(mktemp -d)"
  "$py" -m venv "$probe" >/dev/null 2>&1 || rc=2
  rm -rf "$probe"
  return "$rc"
}

# 1. Whatever the host already puts on PATH. Preferred: it is the distro's own
#    build, it needs no download, and it survives anything done to uv.
system="$(command -v "python$VERSION" 2>/dev/null || true)"
if [ -n "$system" ]; then
  rc=0
  usable "$system" || rc=$?
  if [ "$rc" -eq 0 ]; then
    log "Using the interpreter already on PATH at $system"
    printf '%s\n' "$system"
    exit 0
  fi
  if [ "$rc" -eq 2 ]; then
    log "$system is Python $VERSION but cannot create a virtualenv."
    log "On Debian that means the python$VERSION-venv package is missing."
  else
    log "$system is on PATH but is not a usable Python $VERSION."
  fi
  log "Falling back to a uv-managed interpreter."
fi

# 2. uv, if the host has it already.
uv_home="${HOME:-}"
if [ -z "$uv_home" ]; then
  log "Error: HOME is not set, so there is nowhere to install uv or its interpreters."
  exit 1
fi

uv_bin="$(command -v uv 2>/dev/null || true)"
if [ -z "$uv_bin" ] && [ -x "$uv_home/.local/bin/uv" ]; then
  uv_bin="$uv_home/.local/bin/uv"
fi

# 3. Otherwise install it. Nothing here needs root: uv lands in the deploy user's
#    home, and so do the interpreters it manages, so a failed or unwanted install
#    is removed by deleting a directory rather than by unpicking system packages.
if [ -z "$uv_bin" ]; then
  if ! command -v curl >/dev/null 2>&1; then
    log "Error: neither a usable python$VERSION nor uv is present, and curl is missing,"
    log "       so uv cannot be installed. Install curl, or install python$VERSION by hand."
    exit 1
  fi
  log "Installing uv $UV_VERSION into $uv_home/.local/bin..."
  # --proto/--tlsv1.2 so -L cannot be redirected onto plain HTTP.
  if ! curl --proto '=https' --tlsv1.2 -LsSf "https://astral.sh/uv/$UV_VERSION/install.sh" \
    | env UV_UNMANAGED_INSTALL="$uv_home/.local/bin" sh >&2; then
    log "Error: could not install uv. The host needs outbound HTTPS to astral.sh."
    exit 1
  fi
  uv_bin="$uv_home/.local/bin/uv"
  if [ ! -x "$uv_bin" ]; then
    log "Error: the uv installer reported success but $uv_bin is not executable."
    exit 1
  fi
fi
log "Using uv at $uv_bin ($("$uv_bin" --version 2>/dev/null || echo 'version unknown'))"

# An interpreter uv already knows about, managed or otherwise. Checked before
# installing so that a deploy that changes nothing does not reach the network.
found="$("$uv_bin" python find "$VERSION" 2>/dev/null || true)"
if usable "$found"; then
  log "Using the interpreter uv found at $found"
  printf '%s\n' "$found"
  exit 0
fi

# `--managed-python` is what stops the search below
# handing back the same unusable system interpreter that was rejected above. A
# uv the host already had may predate it, in which case the unfiltered form is
# used — `usable` still gates the result, so the worst case is a clearer failure
# rather than a wrong answer.
managed_args=(python find --managed-python "$VERSION")
if ! "$uv_bin" python find --managed-python --help >/dev/null 2>&1; then
  log "This uv has no --managed-python; searching without it."
  managed_args=(python find "$VERSION")
fi

log "Installing a uv-managed Python $VERSION..."
if ! "$uv_bin" python install "$VERSION" >&2; then
  log "Error: uv could not install Python $VERSION. Any of these explains it:"
  log "       - no outbound HTTPS from this host;"
  log "       - no prebuilt CPython for its platform ($(uname -s)/$(uname -m)); 32-bit ARM may have none;"
  log "       - uv $UV_VERSION predates Python $VERSION, since uv carries its own list of"
  log "         available builds. Raising UV_VERSION in this script is the fix for that one."
  exit 1
fi

managed="$("$uv_bin" "${managed_args[@]}" 2>/dev/null || true)"
if usable "$managed"; then
  log "Using the uv-managed interpreter at $managed"
  printf '%s\n' "$managed"
  exit 0
fi

log "Error: uv installed Python $VERSION but no usable interpreter came back."
log "       'uv python list' on the host will show what it has."
exit 1
