#!/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 the install prefix (default /opt/uploy, or
#      ~/.uploy-app when not root).
#   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="840ef2928c92ed4f5509bb8845979fb3950c8c77e5ec84b319013052c708ab7f"
# -------------------------------------------------------------------------

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)
trap 'rm -rf "$TMPDIR_WORK"' 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 ------------------------------------------------------------
mkdir -p "$PREFIX"
echo "install.sh: extracting to $PREFIX"
# Archive contains a top-level uploy/ dir (portable: bin/ + server/ + web/ +
# node_modules; exe: the single executable + uploy-native/ sidecar).
tar -xzf "$ARCHIVE" -C "$PREFIX"

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"