#!/bin/sh
# install.sh — one-line uploy installer (POSIX sh).
#
#   curl -fsSL https://uploy.app/releases/install.sh | sh
#   # or, for a specific version / offline:
#   sh install.sh --version 0.1.0
#   sh install.sh --manifest /path/install-manifest-linux-x64.json --archive /path/uploy-0.1.0-linux-x64.tar.gz
#   # or, the single-executable (SEA) build:
#   sh install.sh --mode exe
#
# What it does:
#   1. Detects platform (linux/darwin, x64/arm64).
#   2. Requires Node.js (the portable launcher + the verifier need it).
#   3. Online: fetches the platform's signed install manifest + the
#      archive + the verifier (install-verify.mjs, pinned by sha256).
#      Offline (--manifest/--archive): uses local files, still verifies
#      the signature against the pinned public key — no network.
#   4. Verifies sha256(archive) === manifest.sha256 AND the Ed25519
#      signature over the manifest against the pinned release public key.
#   5. Extracts to a staging dir and atomically swaps the previous tree
#      in the install prefix (default /opt/uploy, or ~/.uploy-app when
#      not root) — stale files from the old release never survive.
#   6. Prints the PATH export + the autostart command.
#
# Trust root: the pinned public key below (injected at release time).
# A dev build leaves it empty → the script refuses to install (fail-closed,
# same model as the daemon's embedded key).
set -e

UPLOY_RELEASE_URL="${UPLOY_RELEASE_URL:-https://uploy.app/releases}"

# --- Pinned trust anchors (injected at release build time) ---------------
# The release pipeline (build-portable.mjs) replaces these empty literals
# with the real base64 Ed25519 public key + the sha256 of install-verify.mjs.
# Empty = dev build → refuse to install.
UPLOY_INSTALL_PUBKEY="UhbNgXAB5VySmOR5KYixhiopiF98Apbh+cmQAktBMhE="
UPLOY_VERIFY_SHA256="9697ca4742c8b0f0a5584bfc7e9dfdc304a673cd578f5839916508021cdd62e2"
# -------------------------------------------------------------------------

VERSION=""
MANIFEST=""
ARCHIVE=""
PREFIX=""
MODE="portable"
INSTALL_VERIFY_MJS=""

print_usage() {
  cat <<'EOF'
Usage: sh install.sh [options]

  --mode <portable|exe>  portable (default): the self-contained tarball
                         (needs Node.js on the host). exe: the Node SEA
                         single executable + native sidecar (no Node needed
                         on the host at run time).
  --version <ver>       Install a specific version (default: latest).
  --manifest <path>     Offline: use a local signed manifest file.
  --archive <path>      Offline: use a local archive file.
  --prefix <dir>        Install directory (default: /opt/uploy, or
                        ~/.uploy-app when not running as root).
  --release-url <url>   Release base URL (default: https://uploy.app/releases,
                        or $UPLOY_RELEASE_URL).
  -h, --help            Show this help.

Online mode fetches the manifest + archive + verifier from the release URL
and verifies them. Offline mode (--manifest + --archive) verifies the
signature against the pinned key with NO network.
EOF
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --mode) MODE="$2"; shift 2 ;;
    --version) VERSION="$2"; shift 2 ;;
    --manifest) MANIFEST="$2"; shift 2 ;;
    --archive) ARCHIVE="$2"; shift 2 ;;
    --prefix) PREFIX="$2"; shift 2 ;;
    --release-url) UPLOY_RELEASE_URL="$2"; shift 2 ;;
    -h|--help) print_usage; exit 0 ;;
    *) echo "install.sh: unknown argument: $1" >&2; print_usage >&2; exit 1 ;;
  esac
done

if [ "$MODE" != "portable" ] && [ "$MODE" != "exe" ]; then
  echo "install.sh: --mode must be 'portable' or 'exe' (got '$MODE')." >&2
  exit 1
fi
# SEA manifest name suffix (the release pipeline emits
# install-manifest-sea-<platform>.json for the single-executable build).
MANIFEST_SUFFIX=""
if [ "$MODE" = "exe" ]; then MANIFEST_SUFFIX="-sea"; fi

if [ -z "$UPLOY_INSTALL_PUBKEY" ]; then
  echo "install.sh: this is a dev build — the release public key is not injected." >&2
  echo "install.sh: refusing to install (fail-closed). Build via build-portable.mjs to inject it." >&2
  exit 2
fi

# --- Node is required (the launcher + the verifier both need it) ---------
if ! command -v node >/dev/null 2>&1; then
  echo "install.sh: Node.js is required (>= 18). Install it first:" >&2
  echo "  Debian/Ubuntu:  curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs" >&2
  echo "  macOS:          brew install node" >&2
  echo "  Or download from https://nodejs.org/" >&2
  exit 3
fi
NODE_MAJOR=$(node -e "process.stdout.write(String(process.versions.node.split('.')[0]))" 2>/dev/null || echo 0)
if [ "$NODE_MAJOR" -lt 18 ]; then
  echo "install.sh: Node >= 18 required (found $(node -v))." >&2
  exit 3
fi

# --- Platform detection -------------------------------------------------
os_name=$(uname -s 2>/dev/null || echo unknown)
case "$os_name" in
  Linux) os=linux ;;
  Darwin) os=macos ;;
  *) echo "install.sh: unsupported OS: $os_name" >&2; exit 4 ;;
esac
arch_raw=$(uname -m 2>/dev/null || echo unknown)
case "$arch_raw" in
  x86_64|amd64) arch=x64 ;;
  aarch64|arm64) arch=arm64 ;;
  *) echo "install.sh: unsupported arch: $arch_raw" >&2; exit 4 ;;
esac
PLATFORM="$os-$arch"

# --- Install prefix -----------------------------------------------------
if [ -z "$PREFIX" ]; then
  if [ "$(id -u)" = "0" ]; then PREFIX="/opt/uploy"; else PREFIX="$HOME/.uploy-app"; fi
fi

# --- Temp working dir ---------------------------------------------------
TMPDIR_WORK=$(mktemp -d 2>/dev/null || mktemp -d -t uploy-install)
# K-753 (fixed 0.1.16): the atomic swap needs two more scratch dirs. They
# are cleaned in the same EXIT trap so an aborted install leaves nothing
# behind (K-271 pattern: one trap, all scratch).
STAGE_DIR=""
PARK_DIR=""
cleanup() {
  rm -rf "$TMPDIR_WORK"
  if [ -n "$STAGE_DIR" ]; then rm -rf "$STAGE_DIR"; fi
  # rmdir (NOT rm -rf): a park dir that still HOLDS a tree is the only
  # copy of the old install after a failed backout — preserve it for the
  # operator to recover manually.
  if [ -n "$PARK_DIR" ]; then rmdir "$PARK_DIR" 2>/dev/null || true; fi
}
trap cleanup EXIT

# --- Download helper (curl with wget fallback) --------------------------
fetch() {
  # fetch <url> <out-path>
  url="$1"; out="$2"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$url" -o "$out"
  elif command -v wget >/dev/null 2>&1; then
    wget -q "$url" -O "$out"
  else
    echo "install.sh: need curl or wget to download (or use --manifest/--archive for offline)." >&2
    exit 5
  fi
}

# --- Acquire manifest + archive + verifier ------------------------------
ONLINE=1
if [ -n "$MANIFEST" ] && [ -n "$ARCHIVE" ]; then
  ONLINE=0
  echo "install.sh: offline mode — verifying local files (no network)."
elif [ -n "$MANIFEST" ] || [ -n "$ARCHIVE" ]; then
  echo "install.sh: --manifest and --archive must be given together for offline mode." >&2
  exit 1
fi

if [ "$ONLINE" = "1" ]; then
  if [ -n "$VERSION" ]; then
    MANIFEST_URL="$UPLOY_RELEASE_URL/v$VERSION/install-manifest${MANIFEST_SUFFIX}-$PLATFORM.json"
  else
    MANIFEST_URL="$UPLOY_RELEASE_URL/latest/install-manifest${MANIFEST_SUFFIX}-$PLATFORM.json"
  fi
  MANIFEST="$TMPDIR_WORK/install-manifest.json"
  echo "install.sh: fetching manifest $MANIFEST_URL"
  fetch "$MANIFEST_URL" "$MANIFEST"

  # Extract the archive URL from the signed manifest (node parses JSON).
  ARCHIVE_URL=$(node -e "
    const m = JSON.parse(require('fs').readFileSync('$MANIFEST','utf-8'));
    if (!m.url) process.exit(1);
    process.stdout.write(m.url);
  ")
  if [ -z "$ARCHIVE_URL" ]; then
    echo "install.sh: manifest has no archive url." >&2; exit 6
  fi
  ARCHIVE="$TMPDIR_WORK/$(basename "$ARCHIVE_URL")"
  echo "install.sh: fetching archive $ARCHIVE_URL"
  fetch "$ARCHIVE_URL" "$ARCHIVE"

  # Fetch the verifier + pin it by sha256.
  INSTALL_VERIFY_MJS="$TMPDIR_WORK/install-verify.mjs"
  fetch "$UPLOY_RELEASE_URL/install-verify.mjs" "$INSTALL_VERIFY_MJS"
  ACTUAL_VERIFY_SHA=$(node -e "process.stdout.write(require('crypto').createHash('sha256').update(require('fs').readFileSync('$INSTALL_VERIFY_MJS')).digest('hex'))")
  if [ "$ACTUAL_VERIFY_SHA" != "$UPLOY_VERIFY_SHA256" ]; then
    echo "install.sh: verifier sha256 mismatch — downloaded verifier is not the pinned one." >&2
    echo "install.sh:   expected $UPLOY_VERIFY_SHA256" >&2
    echo "install.sh:   got      $ACTUAL_VERIFY_SHA" >&2
    exit 7
  fi
else
  # Offline: the verifier is bundled alongside the archive (operator got
  # it out-of-band). Fall back to a sibling install-verify.mjs next to the
  # manifest, else refuse.
  cand=$(dirname "$MANIFEST")/install-verify.mjs
  if [ -f "$cand" ]; then INSTALL_VERIFY_MJS="$cand"
  else
    echo "install.sh: offline mode needs install-verify.mjs next to the manifest ($cand)." >&2
    exit 7
  fi
fi

# --- Verify the archive -------------------------------------------------
echo "install.sh: verifying signature + sha256…"
node "$INSTALL_VERIFY_MJS" --manifest "$MANIFEST" --archive "$ARCHIVE" --pubkey "$UPLOY_INSTALL_PUBKEY"

# --- Extract + atomic swap (K-753) --------------------------------------
mkdir -p "$PREFIX"

# K-753 (fixed 0.1.16): the old code extracted the tarball as an IN-PLACE
# overlay onto the existing tree (`tar -xzf -C "$PREFIX"`): files the NEW
# release no longer ships survived as stale corpses in the fixed tree
# (renamed helpers, dead-code-sweep removals, old dist bundles). Now the
# archive is extracted into a staging dir ON THE SAME FILESYSTEM (under
# $PREFIX, so the swap is two rename(2) calls — atomic per entry, no
# cross-device EXDEV case) and each top-level entry the archive delivers
# is swapped wholesale: park the old entry, move the new one in, delete
# the park. portable ships `uploy/`; the SEA build ships the same
# enclosing `uploy/` shape with exe + uploy-native INSIDE (K-755 fixed —
# the old two-top-level-entries layout died at the exe-mode layout check
# with exit 8) — the swap is generic over whatever the archive delivers.

# Best-effort cleanup of stage dirs orphaned by earlier crashed runs (the
# staged tree was never committed; a re-run re-delivers it).
for _orphan in "$PREFIX"/.uploy-stage-*; do
  [ -e "$_orphan" ] || continue
  rm -rf "$_orphan"
done

# Recovery: if an earlier run died between the two renames, the old tree
# sits parked in .uploy-old-*/ while $PREFIX/uploy is missing. Restore the
# newest parked copy so this run swaps a normal install instead of
# layering onto a corpse. (Parks older than a day are swept below.)
if [ ! -e "$PREFIX/uploy" ]; then
  _newest_park=$(ls -1dt "$PREFIX"/.uploy-old-*/ 2>/dev/null | head -n 1)
  if [ -n "$_newest_park" ] && [ -e "${_newest_park%/}/uploy" ]; then
    echo "install.sh: restoring the parked tree from an interrupted install (${_newest_park%/})…"
    mv "${_newest_park%/}/uploy" "$PREFIX/uploy" || {
      echo "install.sh: could not restore the parked tree — inspect ${_newest_park%/} manually." >&2
      exit 9
    }
  fi
fi

# Sweep parks older than a day (a failed backout keeps the only copy of
# the old tree in the park for that long; best-effort, ignore errors).
find "$PREFIX" -maxdepth 1 -name '.uploy-old-*' -mtime +0 -exec rm -rf {} + 2>/dev/null || true

STAGE_DIR="$PREFIX/.uploy-stage-$$"
PARK_DIR="$PREFIX/.uploy-old-$$"
mkdir -p "$STAGE_DIR" "$PARK_DIR"

echo "install.sh: extracting to $STAGE_DIR"
tar -xzf "$ARCHIVE" -C "$STAGE_DIR"

# swap_entry <name>: park the existing $PREFIX/<name> into $PARK_DIR, then
# move the staged $STAGE_DIR/<name> into place. If the move fails, restore
# the parked old entry first (K-271-compensating backout) so the prefix is
# never left with a hole, then abort (exit 9).
swap_entry() {
  _name="$1"
  if [ -e "$PREFIX/$_name" ]; then
    mv "$PREFIX/$_name" "$PARK_DIR/$_name" || {
      echo "install.sh: could not park the existing $_name — aborting before any change." >&2
      exit 9
    }
  fi
  if ! mv "$STAGE_DIR/$_name" "$PREFIX/$_name"; then
    if [ -e "$PARK_DIR/$_name" ]; then
      if mv "$PARK_DIR/$_name" "$PREFIX/$_name"; then
        echo "install.sh: atomic swap of $_name failed; the previous tree was restored." >&2
      else
        echo "install.sh: atomic swap of $_name failed AND the old tree could not be restored — it is preserved in $PARK_DIR/$_name." >&2
      fi
    fi
    exit 9
  fi
}

set -- "$STAGE_DIR"/*
if [ ! -e "$1" ]; then
  echo "install.sh: archive delivered no top-level entries (unexpected layout)." >&2
  exit 8
fi
for _name in "$@"; do
  swap_entry "${_name#"$STAGE_DIR"/}"
done
rm -rf "$STAGE_DIR" "$PARK_DIR"
STAGE_DIR=""
PARK_DIR=""

if [ "$MODE" = "exe" ]; then
  # SEA layout: $PREFIX/uploy/uploy (the executable) + $PREFIX/uploy/uploy-native/
  BIN_DIR="$PREFIX/uploy"
  if [ ! -f "$BIN_DIR/uploy" ]; then
    echo "install.sh: extracted archive but $BIN_DIR/uploy not found (unexpected exe layout)." >&2
    exit 8
  fi
  chmod +x "$BIN_DIR/uploy" 2>/dev/null || true
  if [ ! -d "$BIN_DIR/uploy-native" ]; then
    echo "install.sh: warning — $BIN_DIR/uploy-native not found; native addons (isolated-vm/argon2/keyring) will run degraded." >&2
  fi
else
  BIN_DIR="$PREFIX/uploy/bin"
  if [ ! -f "$BIN_DIR/uploy" ]; then
    echo "install.sh: extracted archive but $BIN_DIR/uploy not found (unexpected layout)." >&2
    exit 8
  fi
  chmod +x "$BIN_DIR/uploy" 2>/dev/null || true
fi

# --- Link the command (K-584) ------------------------------------------
# The old install.sh only PRINTED PATH guidance, so `uploy` was never on
# PATH and the installer's own instructions + the welcome page's claim
# ("linked onto your PATH") were wrong. Try a symlink into a standard bin
# dir first; fall back to printed guidance only when that is impossible.
UPLOY_LINKED=""
if [ -w /usr/local/bin ] || { [ ! -d /usr/local/bin ] && mkdir -p /usr/local/bin 2>/dev/null && [ -w /usr/local/bin ]; }; then
  ln -sf "$BIN_DIR/uploy" /usr/local/bin/uploy 2>/dev/null && UPLOY_LINKED=/usr/local/bin/uploy
elif command -v sudo >/dev/null 2>&1 && sudo -n ln -sf "$BIN_DIR/uploy" /usr/local/bin/uploy 2>/dev/null; then
  UPLOY_LINKED=/usr/local/bin/uploy
fi

# --- PATH guidance -----------------------------------------------------
case ":$PATH:" in
  *":$BIN_DIR:"*) ;;
  *)
    # A working symlink makes the PATH advice moot.
    if [ -z "$UPLOY_LINKED" ]; then
      echo
      echo "install.sh: installed. Add uploy to your PATH:"
      SHELL_NAME=$(basename "$SHELL" 2>/dev/null || echo sh)
      case "$SHELL_NAME" in
        zsh)  echo "  echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc" ;;
        fish) echo "  set -gx PATH $BIN_DIR \$PATH   # or add to ~/.config/fish/config.fish" ;;
        *)    echo "  echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bashrc (or ~/.profile)" ;;
      esac
      echo "  source your shell rc, then run: uploy start"
    fi
    ;;
esac

echo
# --- Daemon lifecycle hint (K-702) --------------------------------------
# A daemon left RUNNING from the previous install keeps serving the OLD
# files after this script replaced them — the silent half-updated state
# observed live on the 0.1.9 rollout (CLI 0.1.9, daemon process still
# 0.1.8, no visible symptom). Probe with the freshly installed CLI and
# print the matching hint. The status verb prints `daemon: running` /
# `daemon: stopped` and exits 0/1; anything else (probe failure) degrades
# to the fresh-install hint — a running daemon then makes `uploy start`
# print "already running", which still tells the operator something.
DAEMON_STATE=$("$BIN_DIR/uploy" daemon status 2>/dev/null || true)
case "$DAEMON_STATE" in
  *daemon:*running*)
    echo "install.sh: a daemon from the PREVIOUS install is still RUNNING."
    echo "install.sh: pick up the new files with:       uploy daemon restart"
    echo "install.sh: (or restart it via your supervisor, e.g. systemctl --user restart uploy)"
    ;;
  *)
    echo "install.sh: start the daemon with:  uploy start"
    ;;
esac
echo "install.sh: survive reboots with:    uploy autostart install"
echo "install.sh: dashboard at:           http://localhost:8765"