#!/usr/bin/env bash
# webtarpit installer — PEP 668 venv, optional systemd/cron, optional config onboarding.
# One-liner (prompts on /dev/tty so curl | bash still works):
#   curl -fsSL https://tacticaldataconcepts.com/tools/webtarpit/install.sh | bash
# Non-interactive:
#   curl -fsSL https://tacticaldataconcepts.com/tools/webtarpit/install.sh | sudo bash -s -- --yes --service systemd --no-onboard --no-auto-update
set -euo pipefail

# curl | bash has no on-disk $0. Copy ourselves so we can re-exec under sudo.
if [ -z "${WEBTARPIT_INSTALL_SELF:-}" ]; then
  _src=${BASH_SOURCE[0]:-}
  if [ -n "$_src" ] && [ -f "$_src" ] && [ -s "$_src" ] \
      && [ "$_src" != "/dev/stdin" ] && [ "$_src" != "/dev/fd/0" ]; then
    export WEBTARPIT_INSTALL_SELF="$(cd "$(dirname "$_src")" && pwd)/$(basename "$_src")"
  else
    export WEBTARPIT_INSTALL_SELF="$(mktemp /tmp/webtarpit-install.XXXXXX.sh)"
    cat >"$WEBTARPIT_INSTALL_SELF"
    chmod 0700 "$WEBTARPIT_INSTALL_SELF"
    exec bash "$WEBTARPIT_INSTALL_SELF" "$@"
  fi
fi

CHANNEL_DEFAULT="https://tacticaldataconcepts.com/tools/webtarpit/version.json"
BASE_DEFAULT="https://tacticaldataconcepts.com/tools/webtarpit"
VERSION_FALLBACK="0.7.1"
MIN_PY_MAJOR=3
MIN_PY_MINOR=10

BASE="${WEBTARPIT_INSTALL_BASE:-$BASE_DEFAULT}"
CHANNEL="${WEBTARPIT_CHANNEL:-$CHANNEL_DEFAULT}"
PREFIX=""
BINDIR=""
RUN_USER=""
CREATE_USER=""
WANT_SERVICE=""   # systemd | user-systemd | cron | none
WANT_ONBOARD=""   # 1 | 0
WANT_AUTOUPDATE=""  # 1 | 0
ASSUME_YES=0
TARBALL_OVERRIDE=""
IS_ROOT=0
TTY=""
INSTALL_TMP=""
INSTALL_LOG=""

cleanup() { rm -rf "${INSTALL_TMP:-}"; }
trap cleanup EXIT

# Root cannot append to another uid's file in sticky /tmp when
# fs.protected_regular=1 (Joe's hang: Permission denied on the joe-owned log).
choose_log() {
  local uid cand
  uid=$(id -u)
  if [ -n "${PREFIX:-}" ] && [ -d "${PREFIX:-}" ] && [ -w "$PREFIX" ]; then
    cand="$PREFIX/install.log"
  else
    cand="/tmp/webtarpit-install.${uid}.log"
  fi
  if ! : >>"$cand" 2>/dev/null; then
    rm -f "$cand" 2>/dev/null || true
    cand="/tmp/webtarpit-install.${uid}.$$.log"
    : >>"$cand" 2>/dev/null || cand="/dev/null"
  fi
  INSTALL_LOG=$cand
}

log() {
  printf 'webtarpit-install: %s\n' "$*" >&2
  [ -n "$INSTALL_LOG" ] || return 0
  printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$INSTALL_LOG" 2>/dev/null || true
}
die() {
  printf 'webtarpit-install: error: %s\n' "$*" >&2
  [ -n "$INSTALL_LOG" ] && printf '%s ERROR %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$INSTALL_LOG" 2>/dev/null || true
  exit 1
}
have() { command -v "$1" >/dev/null 2>&1; }

usage() {
  cat <<'EOF'
webtarpit installer

  curl -fsSL https://tacticaldataconcepts.com/tools/webtarpit/install.sh | bash
  # Installing under /opt or enabling a system unit will ask for sudo.
  curl -fsSL https://tacticaldataconcepts.com/tools/webtarpit/install.sh | sudo bash -s -- --yes

Options:
  --prefix DIR          Install root (root default: /opt/webtarpit,
                        user default: ~/.local/share/webtarpit)
  --bindir DIR          Directory for the webtarpit symlink
  --user NAME           Service/run-as user (root default: sudo invoker,
                        not a new webtarpit account)
  --create-user         Create --user if missing (root only)
  --no-create-user      Do not create a system user
  --service KIND        systemd | user-systemd | cron | none
  --no-service          Same as --service none
  --onboard             Walk through generating webtarpit.yaml
  --no-onboard          Install only; write sample yaml, do not prompt
  --auto-update         Set auto_update: true in generated config
  --no-auto-update      Leave auto-update off (recommended default)
  --tarball PATH|URL    Use this sdist instead of version.json
  --channel URL         version.json URL
  --yes, -y             Non-interactive; accept remaining defaults
  -h, --help            Show this help

Operate webtarpit only on hosts you own or are authorized to run.
EOF
}

is_root() { [ "$(id -u)" -eq 0 ]; }

# True if we can mkdir -p $1 without extra privileges.
dir_is_writable() {
  local target=$1 probe
  [ -n "$target" ] || return 1
  case "$target" in
    ~*) target="${target/#\~/$HOME}" ;;
  esac
  if [ -d "$target" ]; then
    [ -w "$target" ]
    return
  fi
  probe=$target
  while [ ! -e "$probe" ]; do
    probe=$(dirname "$probe")
    [ "$probe" = "." ] && probe=/
    [ "$probe" = "/" ] && break
  done
  [ -w "$probe" ]
}

# Re-run this same script with sudo, keeping answers already collected.
reexec_sudo() {
  local why=$1
  [ "$IS_ROOT" -eq 1 ] && return 0
  log "$why"
  have sudo || die "$why — install sudo or re-run as root"
  if [ "$ASSUME_YES" -eq 0 ] && [ -n "$TTY" ]; then
    ask_yn _sudo_now "Use sudo now? (you may be prompted for your password)" "y"
    if [ "$_sudo_now" != "y" ]; then
      die "aborted. Re-run as root, or: sudo bash $WEBTARPIT_INSTALL_SELF --prefix ${PREFIX:-/opt/webtarpit}"
    fi
  else
    log "re-running with sudo"
  fi
  local extra=()
  [ -n "$PREFIX" ] && extra+=(--prefix "$PREFIX")
  [ -n "$BINDIR" ] && extra+=(--bindir "$BINDIR")
  [ -n "$RUN_USER" ] && extra+=(--user "$RUN_USER")
  if [ -n "$CREATE_USER" ]; then
    if [ "$CREATE_USER" = "1" ]; then extra+=(--create-user); else extra+=(--no-create-user); fi
  fi
  [ -n "$WANT_SERVICE" ] && extra+=(--service "$WANT_SERVICE")
  if [ -n "$WANT_ONBOARD" ]; then
    if [ "$WANT_ONBOARD" = "1" ]; then extra+=(--onboard); else extra+=(--no-onboard); fi
  fi
  if [ -n "$WANT_AUTOUPDATE" ]; then
    if [ "$WANT_AUTOUPDATE" = "1" ]; then extra+=(--auto-update); else extra+=(--no-auto-update); fi
  fi
  [ "$ASSUME_YES" -eq 1 ] && extra+=(--yes)
  [ -n "$TARBALL_OVERRIDE" ] && extra+=(--tarball "$TARBALL_OVERRIDE")
  extra+=(--channel "$CHANNEL")
  # Do not preserve INSTALL_LOG: the joe-owned /tmp file is unwritable by
  # root under fs.protected_regular. Let root open its own log.
  exec sudo --preserve-env=WEBTARPIT_INSTALL_SELF,WEBTARPIT_INSTALL_BASE,WEBTARPIT_CHANNEL \
    -- bash "$WEBTARPIT_INSTALL_SELF" "${extra[@]}"
}

pick_tty() {
  if [ -r /dev/tty ] && [ -w /dev/tty ]; then
    TTY=/dev/tty
  elif [ -t 0 ]; then
    TTY=/dev/stdin
  else
    TTY=""
  fi
}

# Never wrap this in $() — after `sudo` allocates a pty, a subshell that
# reads /dev/tty gets SIGTTIN and stops (T+). That is the hang Joe hit.
# First arg is the variable name to set.
# Use nameref, not `printf -v name`: printf -v from a nested function does
# not write the caller's `local` (Joe typed "y" at sudo and still aborted).
ask() {
  local -n __ask_dest=$1
  local prompt=$2 default=${3-} __ans
  if [ "$ASSUME_YES" -eq 1 ] || [ -z "$TTY" ]; then
    __ask_dest=$default
    return 0
  fi
  if [ -n "$default" ]; then
    printf '%s [%s]: ' "$prompt" "$default" >"$TTY"
  else
    printf '%s: ' "$prompt" >"$TTY"
  fi
  IFS= read -r __ans <"$TTY" || true
  if [ -z "$__ans" ]; then
    __ask_dest=$default
  else
    __ask_dest=$__ans
  fi
}

ask_yn() {
  local -n __yn_dest=$1
  local prompt=$2 default=${3:-n} __raw
  ask __raw "$prompt (y/n)" "$default"
  case "$__raw" in
    y|Y|yes|YES) __yn_dest=y ;;
    *) __yn_dest=n ;;
  esac
}

parse_args() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --prefix) [ $# -ge 2 ] || die "--prefix needs a value"; PREFIX=$2; shift 2 ;;
      --bindir) [ $# -ge 2 ] || die "--bindir needs a value"; BINDIR=$2; shift 2 ;;
      --user) [ $# -ge 2 ] || die "--user needs a value"; RUN_USER=$2; shift 2 ;;
      --create-user) CREATE_USER=1; shift ;;
      --no-create-user) CREATE_USER=0; shift ;;
      --service)
        [ $# -ge 2 ] || die "--service needs systemd|user-systemd|cron|none"
        WANT_SERVICE=$2
        shift 2
        ;;
      --no-service) WANT_SERVICE=none; shift ;;
      --onboard) WANT_ONBOARD=1; shift ;;
      --no-onboard) WANT_ONBOARD=0; shift ;;
      --auto-update) WANT_AUTOUPDATE=1; shift ;;
      --no-auto-update) WANT_AUTOUPDATE=0; shift ;;
      --tarball) [ $# -ge 2 ] || die "--tarball needs a path or URL"; TARBALL_OVERRIDE=$2; shift 2 ;;
      --channel) [ $# -ge 2 ] || die "--channel needs a URL"; CHANNEL=$2; shift 2 ;;
      --yes|-y) ASSUME_YES=1; shift ;;
      -h|--help) usage; exit 0 ;;
      *) die "unknown argument: $1" ;;
    esac
  done
}

python_ok() {
  "$1" -c 'import sys; raise SystemExit(0 if sys.version_info>=(3,10) else 1)' 2>/dev/null
}

find_python() {
  local c
  for c in python3.13 python3.12 python3.11 python3.10 python3; do
    if have "$c" && python_ok "$c"; then
      printf '%s\n' "$c"
      return 0
    fi
  done
  return 1
}

invoking_user() {
  if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
    printf '%s\n' "$SUDO_USER"
  else
    printf '%s\n' "$(id -un)"
  fi
}

primary_group() {
  id -gn "$1" 2>/dev/null || printf '%s\n' "$1"
}

# useradd/adduser can block forever on /etc/passwd.lock (another installer
# stopped with Ctrl-Z still holds lckpwdf). Never wait unbounded.
run_timed() {
  local secs=$1 pid i
  shift
  setsid "$@" </dev/null >/dev/null 2>>"$INSTALL_LOG" &
  pid=$!
  i=0
  while [ "$i" -lt "$secs" ]; do
    if ! kill -0 "$pid" 2>/dev/null; then
      wait "$pid"
      return $?
    fi
    sleep 1
    i=$((i + 1))
  done
  log "timed out after ${secs}s: $*"
  kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
  sleep 1
  kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
  wait "$pid" 2>/dev/null || true
  return 124
}

kill_stopped_installers() {
  local pids
  pids=$(ps -eo pid,stat,args 2>/dev/null | awk -v me="$$" '
    $3 ~ /webtarpit-install/ && $2 ~ /T/ && $1 != me { print $1 }
  ')
  if [ -n "${pids:-}" ]; then
    log "killing stopped (Ctrl-Z) installer pid(s): $pids (they hold passwd lock)"
    # shellcheck disable=SC2086
    kill -9 $pids 2>/dev/null || true
  fi
}

warn_passwd_lock() {
  local n
  n=$(ps -eo stat,args 2>/dev/null | awk '/webtarpit-install/ && $1 ~ /T/ {c++} END {print c+0}')
  if [ "${n:-0}" -gt 0 ]; then
    log "warning: $n stopped (Ctrl-Z) webtarpit-install process(es) — they can hold the passwd lock"
    log "kill them first: sudo kill -9 \$(ps -eo pid,stat,args | awk '/webtarpit-install/ && \$2 ~ /T/ {print \$1}')"
  fi
}

ensure_user() {
  local name=$1
  if id -u "$name" >/dev/null 2>&1; then
    log "run-as user $name already exists"
    return 0
  fi
  [ "$IS_ROOT" -eq 1 ] || die "user $name does not exist (re-run as root to create it)"
  if [ "${CREATE_USER:-0}" != "1" ]; then
    log "user $name missing; not creating (use --create-user to opt in)"
    return 1
  fi
  kill_stopped_installers
  warn_passwd_lock
  log "creating system user $name (no home, 3s timeout — skip if this host locks passwd)"
  export DEBIAN_FRONTEND=noninteractive
  local nologin=/usr/sbin/nologin
  [ -x "$nologin" ] || nologin=/bin/false
  if have useradd; then
    if run_timed 3 useradd -r -M -N -s "$nologin" -d "$PREFIX" -c webtarpit "$name"; then
      log "created system user $name"
      return 0
    fi
    log "useradd failed or timed out"
  elif have adduser; then
    if run_timed 3 adduser --system --no-create-home --ingroup "$(invoking_user)" --home "$PREFIX" --shell "$nologin" --gecos webtarpit "$name"; then
      log "created system user $name"
      return 0
    fi
    log "adduser failed or timed out"
  else
    log "need useradd or adduser to create $name"
  fi
  return 1
}

yaml_quote() {
  # Double-quoted YAML scalar; keep keys with # : and spaces intact.
  local s=$1
  s=${s//\\/\\\\}
  s=${s//\"/\\\"}
  printf '"%s"' "$s"
}

write_yaml() {
  local dest=$1
  local bind=$2 port=$3 data=$4 mode=$5 topic=$6 base=$7 model=$8 auto=$9 key=${10-}
  {
    printf 'bind: %s\n' "$(yaml_quote "$bind")"
    printf 'port: %s\n' "$port"
    printf 'data: %s\n' "$(yaml_quote "$data")"
    printf 'mode: %s\n' "$mode"
    printf 'topic: %s\n' "$(yaml_quote "$topic")"
    printf 'openai_base: %s\n' "$(yaml_quote "$base")"
    printf 'model: %s\n' "$(yaml_quote "$model")"
    if [ -n "$key" ]; then
      printf 'api_key: %s\n' "$(yaml_quote "$key")"
    fi
    cat <<EOF
timeout: 60
weave: true
auto_update: ${auto}
update_interval: 21600
device_cookie: true
device_etag: true
max_gens_per_min: 30
max_pages_per_tenant: 200
delay_min_ms: 40
delay_max_ms: 350
weave_sample: 2
weave_recent: 12
EOF
  } >"$dest"
  if [ -n "$key" ]; then
    chmod 600 "$dest" 2>/dev/null || true
  fi
}

install_unit_systemd() {
  local unit=/etc/systemd/system/webtarpit.service
  cat >"$unit" <<EOF
[Unit]
Description=webtarpit scanner honeypot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=${RUN_USER}
Group=$(primary_group "$RUN_USER")
WorkingDirectory=${PREFIX}
EnvironmentFile=-${PREFIX}/webtarpit.env
ExecStart=${PREFIX}/venv/bin/webtarpit --config ${PREFIX}/webtarpit.yaml serve
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable --now webtarpit.service
  log "systemd unit enabled: webtarpit.service"
}

install_unit_user() {
  local dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
  mkdir -p "$dir"
  cat >"$dir/webtarpit.service" <<EOF
[Unit]
Description=webtarpit scanner honeypot
After=network-online.target

[Service]
Type=simple
WorkingDirectory=${PREFIX}
EnvironmentFile=-${PREFIX}/webtarpit.env
ExecStart=${PREFIX}/venv/bin/webtarpit --config ${PREFIX}/webtarpit.yaml serve
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
  if have systemctl; then
    systemctl --user daemon-reload || true
    systemctl --user enable --now webtarpit.service || log "could not start user unit (loginctl enable-linger?)"
  fi
}

install_cron() {
  local line="@reboot ${PREFIX}/venv/bin/webtarpit --config ${PREFIX}/webtarpit.yaml serve"
  local tmp
  tmp=$(mktemp)
  {
    if [ "$IS_ROOT" -eq 1 ]; then
      crontab -u "$RUN_USER" -l 2>/dev/null || true
    else
      crontab -l 2>/dev/null || true
    fi
  } | grep -v webtarpit >"$tmp" || true
  printf '%s\n' "$line" >>"$tmp"
  if [ "$IS_ROOT" -eq 1 ]; then
    crontab -u "$RUN_USER" "$tmp"
  else
    crontab "$tmp"
  fi
  rm -f "$tmp"
  log "installed @reboot cron for $RUN_USER"
}

fetch_channel() {
  local py=$1
  "$py" - "$CHANNEL" <<'PY'
import json, ssl, sys, urllib.request
url = sys.argv[1]
ctx = ssl.create_default_context()
req = urllib.request.Request(url, headers={"User-Agent": "webtarpit-install/0.7.1"})
with urllib.request.urlopen(req, context=ctx, timeout=30) as r:
    data = json.load(r)
for k in ("version", "tarball_url", "sha256"):
    if k not in data:
        raise SystemExit(f"channel missing {k}")
print(data["version"])
print(data["tarball_url"])
print(data["sha256"])
PY
}

sha256_file() {
  local f=$1
  if have sha256sum; then
    sha256sum "$f" | awk '{print $1}'
  elif have shasum; then
    shasum -a 256 "$f" | awk '{print $1}'
  else
    python3 -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$f"
  fi
}

download() {
  local url=$1 dest=$2
  if have curl; then
    curl -fsSL -o "$dest" "$url"
  elif have wget; then
    wget -qO "$dest" "$url"
  else
    die "need curl or wget"
  fi
}

main() {
  parse_args "$@"
  pick_tty
  if is_root; then IS_ROOT=1; else IS_ROOT=0; fi
  choose_log
  log "start pid=$$ uid=$(id -un) tty=$TTY log=$INSTALL_LOG args=$*"
  kill_stopped_installers

  PY=$(find_python) || die "need Python 3.10+ (python3)"
  log "python: $PY ($($PY -c 'import sys; print(".".join(map(str, sys.version_info[:3])))'))"

  if [ -z "$PREFIX" ]; then
    if [ "$IS_ROOT" -eq 1 ]; then
      PREFIX=/opt/webtarpit
    else
      PREFIX="${HOME}/.local/share/webtarpit"
    fi
    ask PREFIX "Install directory" "$PREFIX"
  fi
  case "$PREFIX" in ~*) PREFIX="${PREFIX/#\~/$HOME}" ;; esac
  if ! dir_is_writable "$PREFIX"; then
    reexec_sudo "cannot write install directory $PREFIX as $(id -un)"
  fi
  mkdir -p "$PREFIX"
  PREFIX=$(cd "$PREFIX" && pwd)
  if [ -w "$PREFIX" ]; then
    local copied="$PREFIX/install.log"
    if [ "$INSTALL_LOG" != "$copied" ]; then
      cat "$INSTALL_LOG" >>"$copied" 2>/dev/null || true
      INSTALL_LOG=$copied
      log "continuing log at $INSTALL_LOG"
    fi
  fi

  if [ -z "$BINDIR" ]; then
    if [ "$IS_ROOT" -eq 1 ]; then
      BINDIR=/usr/local/bin
    else
      BINDIR="${HOME}/.local/bin"
    fi
  fi
  case "$BINDIR" in ~*) BINDIR="${BINDIR/#\~/$HOME}" ;; esac
  if ! dir_is_writable "$BINDIR"; then
    reexec_sudo "cannot write $BINDIR as $(id -un)"
  fi
  mkdir -p "$BINDIR"

  if [ -z "$RUN_USER" ]; then
    # Never prompt. Typing a missing name (webtarpit) used to call useradd
    # and hang on a passwd lock. Default is the sudo invoker.
    if [ "$IS_ROOT" -eq 1 ]; then
      RUN_USER=$(invoking_user)
    else
      RUN_USER=$(id -un)
    fi
  fi
  log "run-as user: $RUN_USER (override with --user NAME)"

  if [ -z "$WANT_SERVICE" ]; then
    local svc_def=none
    if [ "$IS_ROOT" -eq 1 ] && have systemctl && [ -d /run/systemd/system ]; then
      svc_def=systemd
    elif have systemctl; then
      svc_def=user-systemd
    elif have crontab; then
      svc_def=cron
    fi
    ask WANT_SERVICE "Autorun (systemd / user-systemd / cron / none)" "$svc_def"
  fi
  case "$WANT_SERVICE" in
    systemd|user-systemd|cron|none) ;;
    *) die "unknown --service $WANT_SERVICE" ;;
  esac
  if [ "$WANT_SERVICE" = systemd ] && [ "$IS_ROOT" -eq 0 ]; then
    reexec_sudo "a systemd system unit needs root"
  fi

  if [ -z "$WANT_ONBOARD" ]; then
    ask_yn _onb "Walk through config onboarding now?" "y"
    if [ "$_onb" = "y" ]; then
      WANT_ONBOARD=1
    else
      WANT_ONBOARD=0
    fi
  fi

  if [ -z "$WANT_AUTOUPDATE" ]; then
    ask_yn _au "Enable auto-update from TDC.com? (off is safer)" "n"
    if [ "$_au" = "y" ]; then
      WANT_AUTOUPDATE=1
    else
      WANT_AUTOUPDATE=0
    fi
  fi

  if [ "$IS_ROOT" -eq 1 ]; then
    if id -u "$RUN_USER" >/dev/null 2>&1; then
      log "run-as user $RUN_USER already exists"
    else
      # Never useradd unless --create-user. Interactive "create webtarpit"
      # hung this host (passwd lock from a Ctrl-Z'd installer).
      if [ "${CREATE_USER:-0}" = "1" ]; then
        if ! ensure_user "$RUN_USER"; then
          log "falling back to $(invoking_user)"
          RUN_USER=$(invoking_user)
        fi
      else
        log "user $RUN_USER does not exist; skipping useradd (passwd lock). using $(invoking_user)"
        log "to force a dedicated account: --create-user (3s timeout, then fallback)"
        RUN_USER=$(invoking_user)
      fi
    fi
  fi
  log "resolved run-as user: $RUN_USER"

  local version tarball sha
  INSTALL_TMP=$(mktemp -d)

  if [ -n "$TARBALL_OVERRIDE" ]; then
    if [ -f "$TARBALL_OVERRIDE" ]; then
      cp "$TARBALL_OVERRIDE" "$INSTALL_TMP/src.tar.gz"
    else
      download "$TARBALL_OVERRIDE" "$INSTALL_TMP/src.tar.gz"
    fi
    version=$VERSION_FALLBACK
  else
    log "channel $CHANNEL"
    local meta
    meta=$(fetch_channel "$PY") || die "could not read $CHANNEL"
    version=$(printf '%s\n' "$meta" | sed -n '1p')
    tarball=$(printf '%s\n' "$meta" | sed -n '2p')
    sha=$(printf '%s\n' "$meta" | sed -n '3p')
    log "release $version"
    download "$tarball" "$INSTALL_TMP/src.tar.gz"
    local got
    got=$(sha256_file "$INSTALL_TMP/src.tar.gz")
    if [ -n "$sha" ] && [ "$got" != "$sha" ]; then
      die "SHA-256 mismatch (got $got want $sha)"
    fi
  fi

  "$PY" -m venv "$PREFIX/venv"
  "$PREFIX/venv/bin/pip" install --upgrade pip >/dev/null
  "$PREFIX/venv/bin/pip" install --force-reinstall "$INSTALL_TMP/src.tar.gz"
  ln -sfn "$PREFIX/venv/bin/webtarpit" "$BINDIR/webtarpit"

  local cfg="$PREFIX/webtarpit.yaml"
  local data="$PREFIX/data"
  mkdir -p "$data"

  if [ "$WANT_ONBOARD" = "1" ]; then
    local bind port mode topic base model auto_s
    ask bind "Listen address" "0.0.0.0"
    ask port "Listen port" "8088"
    ask mode "Mode (shared / per-ip / per-device)" "per-device"
    ask topic "Site topic" "defunct regional electronics distributor, est. 1998"
    ask base "OpenAI-compatible API base (LiteLLM e.g. http://127.0.0.1:4000)" "http://127.0.0.1:11434"
    ask model "Model id" "llama3.2"
    local apikey=""
    if [ "$ASSUME_YES" -eq 1 ] || [ -z "$TTY" ]; then
      apikey=""
    else
      printf 'API key for LiteLLM/OpenAI-compat (blank = none) []: ' >"$TTY"
      IFS= read -r apikey <"$TTY" || true
    fi
    if [ "$WANT_AUTOUPDATE" = "1" ]; then auto_s=true; else auto_s=false; fi
    write_yaml "$cfg" "$bind" "$port" "$data" "$mode" "$topic" "$base" "$model" "$auto_s" "$apikey"
    if [ -n "$apikey" ]; then
      log "wrote $cfg (api_key set, not logged)"
    else
      log "wrote $cfg (no api_key; set api_key, api_key_file, or WEBTARPIT_API_KEY / LITELLM_API_KEY)"
    fi
  else
    "$PREFIX/venv/bin/webtarpit" sample-config --out "$cfg" --force
    if [ "$WANT_AUTOUPDATE" = "1" ]; then
      "$PY" - "$cfg" <<'PY'
from pathlib import Path
import sys
p = Path(sys.argv[1])
t = p.read_text(encoding="utf-8")
t = t.replace("auto_update: false", "auto_update: true", 1)
p.write_text(t, encoding="utf-8")
PY
    fi
    log "wrote sample $cfg (edit before serving, or: webtarpit --config $cfg serve)"
  fi

  if [ "$IS_ROOT" -eq 1 ]; then
    chown -R "$RUN_USER:$(primary_group "$RUN_USER")" "$PREFIX" || true
  fi

  case "$WANT_SERVICE" in
    systemd)
      [ "$IS_ROOT" -eq 1 ] || die "systemd system unit needs root"
      install_unit_systemd
      ;;
    user-systemd) install_unit_user ;;
    cron) install_cron ;;
    none) log "no autorun; start with: $BINDIR/webtarpit --config $cfg serve" ;;
  esac

  cat <<EOF

webtarpit ${version} installed.

  binary : $BINDIR/webtarpit
  prefix : $PREFIX
  config : $cfg
  data   : $data
  user   : $RUN_USER
  autorun: $WANT_SERVICE
  auto-update: $([ "$WANT_AUTOUPDATE" = "1" ] && echo on || echo off)

If $BINDIR is not on PATH, either add it or run:
  $PREFIX/venv/bin/webtarpit --config $cfg serve

Report:
  $BINDIR/webtarpit --config $cfg report

Operate only on hosts you own or are authorized to run.
EOF
}

main "$@"
