#!/usr/bin/env bash
#
# Alovia Shield daemon installer.
#
# Downloads the alovia-shield binary, verifies its SHA-256 against a signed
# checksum file, installs to /usr/local/bin, writes a systemd unit (Linux) or
# launchd plist (macOS), prompts for the tunnel token and origin URL, and
# enables the service.
#
# Usage:
#   curl -sSL https://get.aloviaai.com | sudo bash
#
# Non-interactive install (CI, config-management):
#   SHIELD_TUNNEL_TOKEN=tkn_... SHIELD_ORIGIN_URL=http://localhost:8080 \
#     curl -sSL https://get.aloviaai.com | sudo bash
#
# Release-infra note: the download host below (get.aloviaai.com/download/...) does
# not yet exist. Bringing it up is Phase 3 ops work — see RELEASE.md.

set -euo pipefail

# ---- constants -----------------------------------------------------------

DOWNLOAD_BASE="https://get.aloviaai.com/download"
CHECKSUMS_URL="${DOWNLOAD_BASE}/checksums.txt"
INSTALL_BIN="/usr/local/bin/alovia-shield"
UNINSTALL_URL="https://get.aloviaai.com/uninstall"

# Per-OS paths set in detect_platform.
ENV_FILE=""
SERVICE_UNIT=""       # systemd unit path OR launchd plist path
LOG_DIR=""

# ---- logging -------------------------------------------------------------

log()  { printf "\033[1;36m[alovia-shield]\033[0m %s\n" "$*"; }
warn() { printf "\033[1;33m[alovia-shield]\033[0m %s\n" "$*" >&2; }
die()  { printf "\033[1;31m[alovia-shield]\033[0m %s\n" "$*" >&2; exit 1; }

# ---- preflight -----------------------------------------------------------

require_root() {
  if [ "$(id -u)" -ne 0 ]; then
    die "installer must run as root (try: curl -sSL https://get.aloviaai.com | sudo bash)"
  fi
}

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

detect_platform() {
  local os_raw arch_raw os arch
  os_raw="$(uname -s)"
  arch_raw="$(uname -m)"

  case "$os_raw" in
    Linux)  os="linux"  ;;
    Darwin) os="darwin" ;;
    *) die "unsupported OS: ${os_raw} (only linux and darwin are supported)" ;;
  esac

  case "$arch_raw" in
    x86_64|amd64)
      arch="amd64"
      [ "$os" = "darwin" ] && die "unsupported: darwin on x86_64 (Intel Macs not supported; Apple Silicon only)"
      ;;
    aarch64|arm64)
      arch="arm64"
      ;;
    *)
      die "unsupported architecture: ${arch_raw}"
      ;;
  esac

  PLATFORM="${os}-${arch}"
  OS="$os"

  case "$OS" in
    linux)
      ENV_FILE="/etc/alovia-shield.env"
      SERVICE_UNIT="/etc/systemd/system/alovia-shield.service"
      LOG_DIR=""  # journald handles logs
      SHA_CMD="sha256sum"
      ;;
    darwin)
      ENV_FILE="/usr/local/etc/alovia-shield.env"
      SERVICE_UNIT="/Library/LaunchDaemons/com.alovia.shield.plist"
      LOG_DIR="/usr/local/var/log"
      SHA_CMD="shasum -a 256"
      ;;
  esac

  log "detected platform: ${PLATFORM}"
}

# ---- download + verify ---------------------------------------------------

download_binary() {
  local gz_name="alovia-shield-${PLATFORM}.gz"
  local url="${DOWNLOAD_BASE}/${gz_name}"
  local tmp_gz tmp_bin tmp_sums expected got

  tmp_gz="$(mktemp)"
  tmp_bin="$(mktemp)"
  tmp_sums="$(mktemp)"
  trap 'rm -f "$tmp_gz" "$tmp_bin" "$tmp_sums"' EXIT

  log "fetching checksums: ${CHECKSUMS_URL}"
  if ! curl -fsSL "$CHECKSUMS_URL" -o "$tmp_sums"; then
    die "could not fetch ${CHECKSUMS_URL} — release host not yet live? (see RELEASE.md)"
  fi

  # checksums.txt line format: <sha256>  <filename>
  expected="$(awk -v name="$gz_name" '$2 == name {print $1}' "$tmp_sums" || true)"
  [ -n "$expected" ] || die "no checksum entry for ${gz_name} in checksums.txt"

  log "downloading ${url}"
  if ! curl -fsSL "$url" -o "$tmp_gz"; then
    die "download failed — release host not yet live? (see RELEASE.md)"
  fi

  got="$($SHA_CMD "$tmp_gz" | awk '{print $1}')"
  if [ "$got" != "$expected" ]; then
    die "checksum mismatch for ${gz_name}: expected ${expected}, got ${got}"
  fi
  log "checksum OK"

  gunzip -c "$tmp_gz" > "$tmp_bin"
  install -m 0755 "$tmp_bin" "$INSTALL_BIN"

  rm -f "$tmp_gz" "$tmp_bin" "$tmp_sums"
  trap - EXIT

  log "installed ${INSTALL_BIN}"
}

# ---- token + origin prompt ----------------------------------------------

collect_config() {
  local token origin have_tty=1

  # If an env file already exists with a token, reuse it on subsequent
  # idempotent runs. Operator can always edit it directly.
  if [ -f "$ENV_FILE" ] && grep -q '^SHIELD_TUNNEL_TOKEN=' "$ENV_FILE"; then
    log "existing config at ${ENV_FILE} — keeping as-is"
    TOKEN="$(awk -F= '/^SHIELD_TUNNEL_TOKEN=/{sub(/^SHIELD_TUNNEL_TOKEN=/,""); print; exit}' "$ENV_FILE")"
    ORIGIN="$(awk -F= '/^SHIELD_ORIGIN=/{sub(/^SHIELD_ORIGIN=/,""); print; exit}' "$ENV_FILE")"
    ORIGIN="${ORIGIN:-http://localhost:8080}"
    return
  fi

  # Non-interactive path: stdin is not a tty (e.g. `curl | bash`).
  if [ ! -t 0 ]; then
    have_tty=0
  fi

  token="${SHIELD_TUNNEL_TOKEN:-}"
  origin="${SHIELD_ORIGIN_URL:-${SHIELD_ORIGIN:-}}"

  if [ -z "$token" ]; then
    if [ "$have_tty" -eq 0 ]; then
      die "SHIELD_TUNNEL_TOKEN is required when stdin is not a tty (set it in the environment)"
    fi
    printf "Paste your SHIELD_TUNNEL_TOKEN (dashboard -> Settings -> Tunnel): "
    # read from the controlling terminal explicitly so piping through bash
    # still lets us prompt interactively.
    IFS= read -r token </dev/tty
    [ -n "$token" ] || die "token is required"
  fi

  if [ -z "$origin" ]; then
    if [ "$have_tty" -eq 1 ]; then
      printf "Local origin URL [Enter to accept http://localhost:8080]: "
      IFS= read -r origin </dev/tty
    fi
    origin="${origin:-http://localhost:8080}"
  fi

  TOKEN="$token"
  ORIGIN="$origin"
}

write_env_file() {
  umask 077
  mkdir -p "$(dirname "$ENV_FILE")"
  cat > "$ENV_FILE" <<EOF
# Written by install.sh. chmod 600. Contains a credential.
SHIELD_TUNNEL_TOKEN=${TOKEN}
SHIELD_ORIGIN=${ORIGIN}
EOF
  chmod 0600 "$ENV_FILE"
  log "wrote ${ENV_FILE}"
}

# ---- linux: user + systemd ----------------------------------------------

ensure_service_user_linux() {
  if ! id -u alovia-shield >/dev/null 2>&1; then
    log "creating system user: alovia-shield"
    useradd --system --shell /usr/sbin/nologin --home-dir /nonexistent --no-create-home alovia-shield
  fi
  chown root:alovia-shield "$ENV_FILE"
  chmod 0640 "$ENV_FILE"
}

install_systemd_unit() {
  local src_unit
  src_unit="$(dirname "$0")/systemd/alovia-shield.service"

  # When run via `curl | bash` the script's directory is not a real path.
  # Prefer the shipped unit if present, otherwise embed a copy inline so
  # the one-liner installer still works.
  if [ -f "$src_unit" ]; then
    install -m 0644 "$src_unit" "$SERVICE_UNIT"
  else
    cat > "$SERVICE_UNIT" <<'EOF'
[Unit]
Description=Alovia Shield tunnel daemon
Documentation=https://get.aloviaai.com
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=alovia-shield
Group=alovia-shield
EnvironmentFile=/etc/alovia-shield.env
ExecStart=/usr/local/bin/alovia-shield
Restart=on-failure
RestartSec=10s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
EOF
  fi
  log "wrote ${SERVICE_UNIT}"

  systemctl daemon-reload
  systemctl enable alovia-shield.service >/dev/null
  # `restart` (not `start`) so a re-run picks up a rotated token cleanly.
  systemctl restart alovia-shield.service
}

# ---- macOS: launchd plist -----------------------------------------------

ensure_log_dir_darwin() {
  mkdir -p "$LOG_DIR"
  chmod 0755 "$LOG_DIR"
}

install_launchd_plist() {
  local src_plist
  src_plist="$(dirname "$0")/launchd/com.alovia.shield.plist"

  # launchd has no EnvironmentFile equivalent — inline the token + origin
  # into the plist. Rotation requires re-running the installer. We still
  # write the env file for parity with linux and for future tooling.
  local tmp_plist
  tmp_plist="$(mktemp)"
  if [ -f "$src_plist" ]; then
    cp "$src_plist" "$tmp_plist"
  else
    cat > "$tmp_plist" <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.alovia.shield</string>
    <key>ProgramArguments</key>
    <array><string>/usr/local/bin/alovia-shield</string></array>
    <key>EnvironmentVariables</key>
    <dict>
        <key>SHIELD_TUNNEL_TOKEN</key><string>TOKEN_PLACEHOLDER</string>
        <key>SHIELD_ORIGIN</key><string>ORIGIN_PLACEHOLDER</string>
    </dict>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key><true/>
    <key>StandardOutPath</key><string>/usr/local/var/log/alovia-shield.log</string>
    <key>StandardErrorPath</key><string>/usr/local/var/log/alovia-shield.log</string>
    <key>ProcessType</key><string>Background</string>
</dict>
</plist>
EOF
  fi

  # Safe substitution: write via python when available (handles XML escaping
  # properly), fall back to sed with a delimiter unlikely to appear in a token.
  if command -v python3 >/dev/null 2>&1; then
    python3 - "$tmp_plist" "$TOKEN" "$ORIGIN" <<'PY'
import sys, xml.sax.saxutils as x
path, token, origin = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path, "r") as f: data = f.read()
data = data.replace("TOKEN_PLACEHOLDER", x.escape(token))
data = data.replace("ORIGIN_PLACEHOLDER", x.escape(origin))
with open(path, "w") as f: f.write(data)
PY
  else
    # Token/origin are unlikely to contain the pipe delimiter; if they do,
    # the sed line will fail noisily rather than produce bad XML.
    sed -i.bak \
      -e "s|TOKEN_PLACEHOLDER|${TOKEN}|g" \
      -e "s|ORIGIN_PLACEHOLDER|${ORIGIN}|g" \
      "$tmp_plist"
    rm -f "${tmp_plist}.bak"
  fi

  install -m 0644 "$tmp_plist" "$SERVICE_UNIT"
  rm -f "$tmp_plist"
  chown root:wheel "$SERVICE_UNIT" || true
  log "wrote ${SERVICE_UNIT}"

  # Idempotent (re)load. `bootout` is tolerant of "not loaded".
  launchctl bootout system "$SERVICE_UNIT" 2>/dev/null || true
  launchctl bootstrap system "$SERVICE_UNIT"
  launchctl enable "system/com.alovia.shield"
  launchctl kickstart -k system/com.alovia.shield
}

# ---- main ----------------------------------------------------------------

main() {
  require_root
  require_cmd curl
  require_cmd awk
  require_cmd gunzip
  detect_platform
  require_cmd "${SHA_CMD%% *}"

  download_binary
  collect_config
  write_env_file

  case "$OS" in
    linux)
      ensure_service_user_linux
      install_systemd_unit
      log "running — check status: systemctl status alovia-shield"
      log "view logs:              journalctl -u alovia-shield -f"
      ;;
    darwin)
      ensure_log_dir_darwin
      install_launchd_plist
      log "running — check status: sudo launchctl print system/com.alovia.shield | head"
      log "view logs:              log stream --predicate 'process == \"alovia-shield\"' --info"
      log "                    or: tail -f ${LOG_DIR}/alovia-shield.log"
      ;;
  esac

  log "installed. uninstall: curl -sSL ${UNINSTALL_URL} | sudo bash"
}

main "$@"
