#!/usr/bin/env bash
#
# AIMS VM agent — installer for a Linux VM (systemd, system-wide).
#
# Hand this script to the VM owner. Running it (as root) checks and installs
# every prerequisite it needs, downloads the agent package zip from the static
# package server (PKG_BASE below), extracts it, sets up a venv + dependencies,
# writes config.ini from the flags passed, then installs and starts a
# system-wide systemd service that starts on boot and restarts on crash (no
# login session needed).
#
#   sudo ./install.sh --url https://aims.example.com --asset-key <ASSET_KEY> [--interval 60]
#
# Required: --url, --asset-key.  Run as root (the service is system-wide).
#
# Re-running is safe: it repairs a half-finished install and upgrades in place,
# keeping the existing config.
#
set -euo pipefail

# Base URL of the STATIC package server hosting aims-vm-agent.zip. This is a plain
# file server, independent of the AIMS app server (which is the --url you pass).
# `packaging/build.sh --pkg-base <URL>` stamps the real value in here; you can also
# override with --pkg-base or the AIMS_PKG_BASE env var.
PKG_BASE="${AIMS_PKG_BASE:-https://aims-assets.astratinvest.com}"

APP_DIR=/opt/aims-vm-agent
CONF_DIR=/etc/aims-vm-agent
SERVICE=aims-vm-agent
MIN_PY_MINOR=9                                   # need Python 3.9+

# ---- defaults (override with flags) ----
URL=""; ASSET_KEY=""; INTERVAL=60; VERIFY_TLS=true
INTERNET_HOST=1.1.1.1; INTERNET_PORT=53; LAN_HOST=""
AIMS_TIMEOUT=10; HEALTH_TIMEOUT=5; ENFORCE_SHUTDOWN_GUARD=true
ZIP_URL=""   # full download URL of the agent zip; defaults to $PKG_BASE/aims-vm-agent.zip

usage() {
  cat <<'USAGE'
AIMS VM agent installer (Linux, systemd — run as root)

Required:
  --url <URL>                Base URL of the AIMS server (no trailing path)
  --asset-key <KEY>          This VM's asset key (mint on AIMS: mint_authkey --asset)

Optional:
  --interval <sec>           Fallback report interval (default 60)
  --verify-tls <true|false>  Verify the AIMS TLS cert (default true)
  --internet-check-host <h>  TCP host for the internet check (default 1.1.1.1)
  --internet-check-port <p>  Port for the internet check (default 53)
  --lan-check-host <h>       Optional gateway/host to probe for LAN
  --aims-timeout <sec>       AIMS request timeout (default 10)
  --health-timeout <sec>     Service health-check timeout (default 5)
  --enforce-shutdown-guard <true|false>
                             Block OS shutdown/reboot unless the VM is in
                             maintenance mode (default true; false opts this host
                             out entirely)
  --zip-url <URL>            Full download URL of aims-vm-agent.zip (the download
                             target). Overrides --pkg-base; use for a direct link.
  --pkg-base <URL>           Base URL of the package server (zip fetched from
                             <base>/aims-vm-agent.zip if --zip-url is not given)
USAGE
}

while [ $# -gt 0 ]; do
  case "$1" in
    --url) URL="$2"; shift 2;;
    --asset-key) ASSET_KEY="$2"; shift 2;;
    --interval) INTERVAL="$2"; shift 2;;
    --verify-tls) VERIFY_TLS="$2"; shift 2;;
    --internet-check-host) INTERNET_HOST="$2"; shift 2;;
    --internet-check-port) INTERNET_PORT="$2"; shift 2;;
    --lan-check-host) LAN_HOST="$2"; shift 2;;
    --aims-timeout) AIMS_TIMEOUT="$2"; shift 2;;
    --health-timeout) HEALTH_TIMEOUT="$2"; shift 2;;
    --enforce-shutdown-guard) ENFORCE_SHUTDOWN_GUARD="$2"; shift 2;;
    --zip-url) ZIP_URL="$2"; shift 2;;
    --pkg-base) PKG_BASE="$2"; shift 2;;
    -h|--help) usage; exit 0;;
    *) echo "Unknown option: $1" >&2; usage; exit 1;;
  esac
done

die() { echo "ERROR: $*" >&2; exit 1; }
say() { echo "-> $*"; }

[ "$(id -u)" -eq 0 ] || die "run as root (sudo ./install.sh ...)."
[ -n "$URL" ]        || die "--url is required."
[ -n "$ASSET_KEY" ]  || die "--asset-key is required."
command -v systemctl >/dev/null 2>&1 || die "systemd not found — this installer targets systemd hosts."

# =====================================================================
# 1. Preflight — check every prerequisite and install what is missing.
#    Nothing below this block may assume a tool is present.
# =====================================================================
PKG_MGR=""
for m in apt-get dnf yum zypper pacman apk; do
  command -v "$m" >/dev/null 2>&1 && { PKG_MGR="$m"; break; }
done
APT_UPDATED=false

# pkg_install <pkg>... — best-effort install; returns non-zero if it failed so
# callers can fall back rather than abort the whole run.
pkg_install() {
  [ -n "$PKG_MGR" ] || return 1
  case "$PKG_MGR" in
    apt-get)
      $APT_UPDATED || { DEBIAN_FRONTEND=noninteractive apt-get update -qq || true; APT_UPDATED=true; }
      DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "$@" >/dev/null 2>&1;;
    dnf)    dnf install -y -q "$@" >/dev/null 2>&1;;
    yum)    yum install -y -q "$@" >/dev/null 2>&1;;
    zypper) zypper --non-interactive install -y "$@" >/dev/null 2>&1;;
    pacman) pacman -Sy --noconfirm "$@" >/dev/null 2>&1;;
    apk)    apk add --quiet "$@" >/dev/null 2>&1;;
  esac
}

# ---- 1a. python3, new enough ----
if ! command -v python3 >/dev/null 2>&1; then
  say "python3 not found; installing…"
  pkg_install python3 || die "could not install Python 3 automatically — install it manually and re-run."
  command -v python3 >/dev/null 2>&1 || die "Python 3 still not on PATH after install."
fi
PY_MINOR="$(python3 -c 'import sys; print(sys.version_info.minor)')"
[ "$(python3 -c 'import sys; print(sys.version_info.major)')" -eq 3 ] \
  && [ "$PY_MINOR" -ge "$MIN_PY_MINOR" ] \
  || die "Python 3.$MIN_PY_MINOR+ required, found $(python3 -V 2>&1)."
say "Python $(python3 -V 2>&1 | cut -d' ' -f2) OK"

# ---- 1b. venv + ensurepip (Debian/Ubuntu ship these split out) ----
# Probe by actually building a throwaway venv — the only reliable test, since
# the module can import yet fail at the ensurepip step.
venv_probe() {
  local t; t="$(mktemp -d)"
  local ok=1
  python3 -m venv "$t" >/dev/null 2>&1 && [ -x "$t/bin/pip" ] && ok=0
  rm -rf "$t"
  return $ok
}
if ! venv_probe; then
  say "Python venv support missing; installing…"
  # Try the version-specific package first (python3.13-venv), then the generic.
  pkg_install "python3.$PY_MINOR-venv" || pkg_install python3-venv || pkg_install python3-pip || true
  venv_probe || die "Python venv support is still unavailable.
    Install it manually and re-run, e.g.:
      Debian/Ubuntu:  sudo apt install python3.$PY_MINOR-venv
      RHEL/Fedora:    sudo dnf install python3-pip"
fi
say "Python venv support OK"

# ---- 1c. a working downloader ----
# The snap build of curl is sandboxed and fails writing outside its confinement
# ("curl: (23) client returned ERROR on write"), so prefer a real system curl,
# then wget, then Python's urllib as a last resort.
DL_TOOL=""
if [ -x /usr/bin/curl ]; then DL_TOOL="/usr/bin/curl"
elif command -v wget >/dev/null 2>&1; then DL_TOOL="wget"
else
  CURL_PATH="$(command -v curl 2>/dev/null || true)"
  case "$CURL_PATH" in
    /snap/*|"") say "no system curl found; installing…"
                pkg_install curl && [ -x /usr/bin/curl ] && DL_TOOL="/usr/bin/curl" || DL_TOOL="python3";;
    *)          DL_TOOL="$CURL_PATH";;
  esac
fi
[ -n "$DL_TOOL" ] || DL_TOOL="python3"

download() {   # download <url> <dest>
  case "$DL_TOOL" in
    wget)    wget -q -O "$2" "$1";;
    python3) python3 - "$1" "$2" <<'PY'
import sys, urllib.request
urllib.request.urlretrieve(sys.argv[1], sys.argv[2])
PY
             ;;
    *)       "$DL_TOOL" -fsSL "$1" -o "$2";;
  esac
}
say "Downloader: $DL_TOOL"

# ---- 1d. unzip is optional (Python's zipfile is the fallback) ----

# =====================================================================
# 2. Download the package zip
# =====================================================================
FETCH_URL="${ZIP_URL:-$PKG_BASE/aims-vm-agent.zip}"
case "$FETCH_URL" in
  *REPLACE-WITH-YOUR-PACKAGE-HOST*)
    die "this install.sh has no package server baked in.
    Pass --pkg-base http://<package-host>:<port> (or --zip-url <full URL>), or ask
    your AIMS admin for the stamped copy of install.sh.";;
esac
DL_DIR="$(mktemp -d)"; trap 'rm -rf "$DL_DIR"' EXIT
ZIP="$DL_DIR/aims-vm-agent.zip"
say "Downloading agent package from $FETCH_URL"
download "$FETCH_URL" "$ZIP" || die "download failed from $FETCH_URL
    Check the package server is reachable from this VM:  $DL_TOOL $FETCH_URL"
[ -s "$ZIP" ] || die "downloaded file is empty — is $FETCH_URL correct?"
python3 -c 'import sys, zipfile; sys.exit(0 if zipfile.is_zipfile(sys.argv[1]) else 1)' "$ZIP" \
  || die "downloaded file is not a zip (the server likely returned an error page).
    Try opening $FETCH_URL in a browser to see what it serves."

# =====================================================================
# 3. Unpack + venv + deps
# =====================================================================
say "Installing to $APP_DIR"
systemctl stop "$SERVICE" 2>/dev/null || true
mkdir -p "$APP_DIR" "$CONF_DIR"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP" "$DL_DIR"' EXIT
if command -v unzip >/dev/null 2>&1; then
  unzip -q "$ZIP" -d "$TMP"
else
  python3 -m zipfile -e "$ZIP" "$TMP"      # no 'unzip' needed — Python does it
fi
[ -d "$TMP/aims_vm_agent" ] || die "package zip has no aims_vm_agent/ directory — bad or truncated download."
rm -rf "$APP_DIR/aims_vm_agent"
cp -r "$TMP/aims_vm_agent" "$APP_DIR/"
cp "$TMP/requirements.txt" "$APP_DIR/"

# A venv left behind by an interrupted or previously-failed run has the directory
# but no usable pip. Test the interpreter, not the directory, and rebuild if broken.
if [ ! -x "$APP_DIR/venv/bin/python3" ] || [ ! -x "$APP_DIR/venv/bin/pip" ]; then
  [ -e "$APP_DIR/venv" ] && { say "Existing venv is incomplete; rebuilding"; rm -rf "$APP_DIR/venv"; }
  say "Creating virtualenv"
  python3 -m venv "$APP_DIR/venv" || { rm -rf "$APP_DIR/venv"; die "failed to create the virtualenv at $APP_DIR/venv."; }
fi
say "Installing Python dependencies"
"$APP_DIR/venv/bin/pip" install --quiet --upgrade pip || true   # nice-to-have, not fatal
"$APP_DIR/venv/bin/pip" install --quiet -r "$APP_DIR/requirements.txt" \
  || die "dependency install failed — check this VM can reach PyPI (or configure an internal index)."
"$APP_DIR/venv/bin/python3" -c 'import requests, psutil' \
  || die "dependencies installed but are not importable — try: sudo rm -rf $APP_DIR/venv && re-run this script."

# =====================================================================
# 4. config.ini from the flags
# =====================================================================
say "Writing $CONF_DIR/config.ini"
cat > "$CONF_DIR/config.ini" <<EOF
[aims]
url = $URL
asset_key = $ASSET_KEY
interval = $INTERVAL
verify_tls = $VERIFY_TLS
aims_timeout = $AIMS_TIMEOUT
health_timeout = $HEALTH_TIMEOUT
internet_check_host = $INTERNET_HOST
internet_check_port = $INTERNET_PORT
lan_check_host = $LAN_HOST
enforce_shutdown_guard = $ENFORCE_SHUTDOWN_GUARD
EOF
chmod 600 "$CONF_DIR/config.ini"

# =====================================================================
# 5. systemd system service -> enable + start (boot + restart-on-crash)
# =====================================================================
say "Installing systemd service '$SERVICE'"
mkdir -p /etc/systemd/system                       # may be absent on a minimal image
cat > "/etc/systemd/system/$SERVICE.service" <<EOF
[Unit]
Description=AIMS VM Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=$APP_DIR
ExecStart=$APP_DIR/venv/bin/python -m aims_vm_agent --config $CONF_DIR/config.ini
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable "$SERVICE" >/dev/null 2>&1 || true
systemctl restart "$SERVICE"

# ---- 6. verify it actually stayed up ----
sleep 3
if systemctl is-active --quiet "$SERVICE"; then
  echo
  echo "✓ AIMS VM agent installed and started (runs on boot, restarts on crash)."
else
  echo
  echo "✗ The service was installed but is not running. Recent logs:" >&2
  journalctl -u "$SERVICE" -n 20 --no-pager >&2 || true
  echo >&2
  echo "  Common causes: wrong --url, wrong/expired --asset-key, or AIMS unreachable from this VM." >&2
fi
echo "  status: systemctl status $SERVICE"
echo "  logs:   journalctl -u $SERVICE -f"
echo "  config: $CONF_DIR/config.ini"
