#!/usr/bin/env bash
# Install Burrow Search (local-search) for the current user on Linux or
# macOS, and keep its search pack up to date.
#
#   curl -fsSL https://PROVIDER/install.sh | bash   # downloads the programs
#   scripts/install.sh                     # from a source checkout (builds with cargo)
#   ./install.sh                           # from an unpacked release tarball (no Rust needed)
#   burrow-update                          # later: fetch the newest pack (installed by this script)
#
# What it does:
#   1. installs local-search, local-search-web and index into ~/.local/bin
#   2. downloads the provider's newest signed index (and, with --images, image
#      pack), checks the key's fingerprint, then verifies and installs it
#   3. runs the web UI on http://localhost:8080 whenever you log in (a
#      systemd user service on Linux, a launchd agent on macOS)
#
# Only steps 2 (and building, which fetches crates) use the network. The
# programs themselves never connect anywhere; the download is the same file
# for everyone, so it tells the provider nothing about your searches.
#
# Options:
#   --from URL          where the provider publishes releases (a directory
#                       listing with index-vNNN.{bin,sha256,sig} and a .pub key)
#   --fingerprint FP    the provider key fingerprint you expect (16 hex digits);
#                       without it you are shown the fingerprint and asked
#   --images            also install the image pack (images-vNNN.bin)
#   --no-pack           don't download a pack
#   --update            only update the pack (skip installing the programs)
#   --no-service        don't set up the web UI service
#   --port N            web UI port (default 8080)
#   --prefix DIR        install programs into DIR/bin (default ~/.local)
#   --insecure          accept a self-signed TLS certificate when downloading
#                       (the pack is still checked against the pinned key)
#   -y, --yes           don't ask before downloading or trusting a pinned key
#   --uninstall         remove the programs and the service (keeps your packs;
#                       add --purge to delete them too)
set -euo pipefail

# The default provider. Its key is pinned, so a pack from anywhere else is
# refused. scripts/setup-server.sh rewrites these for other providers
# (DEFAULT_INSECURE=1 when their server has no domain, so a self-signed
# certificate; safe, since every file is checked against the key).
DEFAULT_URL=https://burrowproject.org/releases/
DEFAULT_FINGERPRINT=42ffb7b06daaec47
DEFAULT_INSECURE=

CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/burrow
CONFIG=$CONFIG_DIR/install.conf
CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/burrow
HOME_DIR=${LOCAL_SEARCH_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}/local-search}
UNIT=burrow-web
LABEL=org.burrowproject.web
AGENT=$HOME/Library/LaunchAgents/$LABEL.plist

URL= FINGERPRINT= INSECURE= IMAGES= PORT= PREFIX=
PACK=1 SERVICE=1 PROGRAMS=1 YES= UNINSTALL= PURGE=
# Settings from an earlier run; command-line options win.
# shellcheck disable=SC1090
[ -f "$CONFIG" ] && . "$CONFIG"
# Installs from before the domain saved the server's IP and its self-signed
# certificate; move them to the domain (same key, so the fingerprint stays).
[ "$URL" = https://46.224.238.41/releases/ ] && URL=https://burrowproject.org/releases/ INSECURE=
SAVED_URL=$URL SAVED_FINGERPRINT=$FINGERPRINT

# Empty when piped into bash.
SELF=${BASH_SOURCE[0]:-}
ARGS=("$@")
usage() { sed -n '2,/^set -euo/p' "${SELF:-/dev/null}" | sed '$d; s/^# \{0,1\}//'; exit "${1:-0}"; }
while [ $# -gt 0 ]; do
  case $1 in
    --from) URL=$2; shift ;;
    --fingerprint) FINGERPRINT=$2; shift ;;
    --images) IMAGES=1 ;;
    --no-images) IMAGES= ;;
    --no-pack) PACK= ;;
    --update) PROGRAMS= SERVICE= ;;
    --no-service) SERVICE= ;;
    --port) PORT=$2; shift ;;
    --prefix) PREFIX=$2; shift ;;
    --insecure) INSECURE=1 ;;
    -y|--yes) YES=1 ;;
    --uninstall) UNINSTALL=1 ;;
    --purge) PURGE=1 ;;
    -h|--help) usage ;;
    *) echo "unknown option: $1" >&2; usage 2 >&2 ;;
  esac
  shift
done
# The saved fingerprint belongs to the saved provider.
[ "$URL" != "$SAVED_URL" ] && [ "$FINGERPRINT" = "$SAVED_FINGERPRINT" ] && FINGERPRINT=
if [ -z "$URL" ]; then URL=$DEFAULT_URL FINGERPRINT=${FINGERPRINT:-$DEFAULT_FINGERPRINT} INSECURE=${INSECURE:-$DEFAULT_INSECURE}; fi
URL=${URL%/}/
PORT=${PORT:-8080}
PREFIX=${PREFIX:-$HOME/.local}
BIN=$PREFIX/bin

step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
ask() { # ask "question" -> true for yes (default yes)
  [ -n "$YES" ] && return 0
  local a
  { : </dev/tty; } 2>/dev/null || die "${2:-$1 (no terminal to ask on; -y answers yes)}"
  read -r -p "$1 [Y/n] " a </dev/tty
  [[ ! $a =~ ^[Nn] ]]
}
has_systemd() { command -v systemctl >/dev/null && systemctl --user show-environment >/dev/null 2>&1; }
is_mac() { [ "$(uname -s)" = Darwin ]; }
sha256_check() { if command -v sha256sum >/dev/null; then sha256sum --quiet -c "$1"; else shasum -a 256 -s -c "$1"; fi; }
human() { awk -v b="$1" 'BEGIN { split("B KB MB GB TB", u); i = 1; while (b >= 1000 && i < 5) { b /= 1000; i++ } printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }'; }

if [ -n "$UNINSTALL" ]; then
  step "Uninstalling"
  if has_systemd && [ -f "$HOME/.config/systemd/user/$UNIT.service" ]; then
    systemctl --user disable --now "$UNIT" 2>/dev/null || true
    rm -f "$HOME/.config/systemd/user/$UNIT.service"
    systemctl --user daemon-reload
  fi
  if is_mac; then
    # By label, not by plist file: that also stops a web UI the app started.
    launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
    rm -f "$AGENT"
  fi
  # Whatever still runs these programs (started by hand, or a service that
  # didn't stop) keeps serving from the deleted files until it is killed.
  pkill -f "$BIN/local-search-web" 2>/dev/null || true
  rm -fv "$BIN"/{local-search,local-search-web,index,burrow-update}
  if [ -n "$PURGE" ]; then rm -rf "$HOME_DIR" "$CONFIG_DIR" "$CACHE"; echo "removed $HOME_DIR"
  else echo "kept your packs in $HOME_DIR (--purge removes them)"; fi
  exit 0
fi

CURL=(curl -fL --proto '=https,http' --retry 3)
[ -n "$INSECURE" ] && CURL+=(--insecure)

HERE=
[ -f "$SELF" ] && HERE=$(cd "$(dirname "$SELF")" && pwd)

# 1. Programs ---------------------------------------------------------------
if [ -n "$PROGRAMS" ]; then
  if [ -z "$HERE" ] || { [ ! -x "$HERE/bin/local-search" ] && [ ! -f "$HERE/../Cargo.toml" ]; }; then
    # Piped from the provider: get the programs from there, then carry on
    # with the install.sh inside the download.
    step "Downloading the programs from $URL"
    LISTING=$("${CURL[@]}" -sS -H 'Accept: application/json' "$URL") || die "can't read $URL"
    if is_mac; then PLATFORM=macos; else PLATFORM=linux-$(uname -m); fi
    TARBALL=$({ grep -oE "burrow-[0-9.]+-$PLATFORM\.tar\.gz" <<<"$LISTING" || true; } | sort -u | tail -1)
    [ -n "$TARBALL" ] || die "$URL has no download for $PLATFORM; build from source instead"
    TMP=$(mktemp -d)
    "${CURL[@]}" -# -o "$TMP/$TARBALL" "$URL$TARBALL"
    "${CURL[@]}" -sS -o "$TMP/$TARBALL.sha256" "$URL$TARBALL.sha256"
    (cd "$TMP" && sha256_check "$TARBALL.sha256") || die "$TARBALL is damaged; try again"
    tar -C "$TMP" -xzf "$TMP/$TARBALL"
    # ${ARGS[@]+...}: macOS's bash 3.2 calls an empty array unbound.
    BURROW_TMP=$TMP exec "$TMP/${TARBALL%.tar.gz}/install.sh" ${ARGS[@]+"${ARGS[@]}"} --from "$URL" \
      ${FINGERPRINT:+--fingerprint "$FINGERPRINT"} ${INSECURE:+--insecure}
  elif [ -x "$HERE/bin/local-search" ]; then
    step "Installing the programs from this download"
    SRC=$HERE/bin
  elif [ -f "$HERE/../Cargo.toml" ]; then
    step "Building the programs (a few minutes the first time)"
    if ! command -v cargo >/dev/null; then
      [ -x "$HOME/.cargo/bin/cargo" ] || {
        ask "Rust is needed to build Burrow and isn't installed. Install it with rustup (https://rustup.rs)?" \
          || die "install Rust from https://rustup.rs and run this again"
        curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
      }
      export PATH=$HOME/.cargo/bin:$PATH
    fi
    # Only the client package: it has no networking dependencies at all.
    (cd "$HERE/.." && cargo build --release --locked -p local-search)
    SRC=$HERE/../target/release
  else
    die "no programs found next to this script, and it isn't in a source checkout"
  fi
  mkdir -p "$BIN"
  for p in local-search local-search-web index; do install -m 755 "$SRC/$p" "$BIN/$p"; done
  # A tarball downloaded in a browser is quarantined, and macOS would refuse to
  # run the unsigned programs from it.
  is_mac && xattr -d com.apple.quarantine "$BIN"/{local-search,local-search-web,index} 2>/dev/null || true
  # burrow-update runs this script again, for the pack only.
  mkdir -p "$CONFIG_DIR"
  [ "$SELF" -ef "$CONFIG_DIR/install.sh" ] || install -m 755 "$SELF" "$CONFIG_DIR/install.sh"
  printf '#!/bin/sh\nexec "%s" --update "$@"\n' "$CONFIG_DIR/install.sh" >"$BIN/burrow-update"
  chmod 755 "$BIN/burrow-update"
  echo "installed local-search, local-search-web, index and burrow-update into $BIN"
fi
INDEX=$BIN/index
[ -x "$INDEX" ] || die "$INDEX not found; run without --update first"

save_config() {
  mkdir -p "$CONFIG_DIR"
  cat >"$CONFIG" <<EOF
# Written by install.sh; used by burrow-update.
URL=$URL
FINGERPRINT=$FINGERPRINT
INSECURE=$INSECURE
IMAGES=$IMAGES
PORT=$PORT
PREFIX=$PREFIX
EOF
}
save_config

# 2. Pack -------------------------------------------------------------------

# Newest version of index-vNNN.bin / images-vNNN.bin in the provider's listing.
latest() { { grep -oE "$1-v[0-9]+\.bin" <<<"$LISTING" || true; } | sed -E 's/.*-v0*([0-9]+)\.bin/\1/' | sort -n | tail -1; }
# Installed version of the index ("index") or the image pack ("images").
installed() {
  { "$INDEX" --home "$HOME_DIR" status 2>/dev/null || true; } | awk -v want="$1" '
    /^Image pack:/ { section = "images"; next }
    /^Store:/ { section = "index"; next }
    section == want && $1 ~ /-v[0-9]+\./ { v = $3; sub(/^v/, "", v); sub(/,$/, "", v); if (v + 0 > max) max = v + 0 }
    END { print max + 0 }'
}

trust_key() {
  local pub fp
  pub=$(grep -oE '[A-Za-z0-9._-]+\.pub' <<<"$LISTING" | sed -n 1p)
  [ -n "$pub" ] || die "no .pub key in $URL"
  "${CURL[@]}" -sS -o "$CACHE/$pub" "$URL$pub"
  # Read the key's fingerprint without trusting it yet.
  fp=$("$INDEX" --home "$CACHE/peek" trust "$CACHE/$pub" | awk '{print $3}'); rm -rf "$CACHE/peek"
  if [ -n "$FINGERPRINT" ]; then
    [ "$fp" = "$FINGERPRINT" ] || die "the key at $URL$pub has fingerprint $fp, not $FINGERPRINT. Not installing."
    [ -f "$HOME_DIR/trusted-keys/$fp.pub" ] && return
    echo "provider key $fp matches the pinned fingerprint"
  elif [ -f "$HOME_DIR/trusted-keys/$fp.pub" ]; then
    FINGERPRINT=$fp; save_config; return
  else
    echo "The provider's key fingerprint is $fp."
    echo "Compare it with the one the provider publishes (website, README, a friend) before going on."
    # Never answered by -y: trusting a key is the one decision that matters.
    YES= ask "Trust this key?" "no terminal to confirm key $fp on; pass --fingerprint $fp once you've checked it" \
      || die "key not trusted"
  fi
  "$INDEX" --home "$HOME_DIR" trust "$CACHE/$pub"
  FINGERPRINT=$fp
  save_config
}

fetch_install() { # fetch_install index|images
  local name=$1 what have want file size msg
  [ "$name" = index ] && what="search index" || what="image pack"
  want=$(latest "$name")
  [ -n "$want" ] || { echo "no $name-vNNN.bin at $URL yet"; return; }
  have=$(installed "$name")
  if [ "$have" -ge "$want" ]; then echo "$what v$have is up to date"; return; fi
  file=$(printf '%s-v%03d.bin' "$name" "$want")
  size=$("${CURL[@]}" -sSI "$URL$file" | tr -d '\r' | awk 'tolower($1) == "content-length:" { n = $2 } END { print n + 0 }')
  if [ "$have" -gt 0 ]; then msg="Update the $what from v$have to v$want ($(human "$size"))?"
  else msg="Download the $what v$want ($(human "$size"))?"; fi
  ask "$msg" || return 0
  for ext in sha256 sig bin; do
    # -C - resumes a download that was interrupted earlier.
    "${CURL[@]}" -C - -# -o "$CACHE/${file%.bin}.$ext" "$URL${file%.bin}.$ext"
  done
  "$INDEX" --home "$HOME_DIR" install "$CACHE/$file"
  rm -f "$CACHE/${file%.bin}".{bin,sha256,sig}
  CHANGED=1
}

CHANGED=
if [ -n "$PACK" ]; then
  step "Getting the newest pack from $URL"
  mkdir -p "$CACHE"
  LISTING=$("${CURL[@]}" -sS -H 'Accept: application/json' "$URL") || die "can't read $URL (a self-signed certificate needs --insecure)"
  trust_key
  fetch_install index
  [ -n "$IMAGES" ] && fetch_install images
fi

# 3. Web UI service ---------------------------------------------------------
if [ -n "$SERVICE" ]; then
  if has_systemd; then
    step "Starting the web UI as a user service ($UNIT)"
    mkdir -p "$HOME/.config/systemd/user"
    cat >"$HOME/.config/systemd/user/$UNIT.service" <<EOF
[Unit]
Description=Burrow Search web UI (localhost only)

[Service]
ExecStart=$BIN/local-search-web --port $PORT --home $HOME_DIR --releases-url $URL
Restart=on-failure

[Install]
WantedBy=default.target
EOF
    systemctl --user daemon-reload
    systemctl --user enable "$UNIT" >/dev/null 2>&1
    systemctl --user restart "$UNIT"
  elif is_mac; then
    step "Starting the web UI at login ($(basename "$AGENT"))"
    mkdir -p "$(dirname "$AGENT")"
    cat >"$AGENT" <<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>org.burrowproject.web</string>
  <key>ProgramArguments</key>
  <array>
    <string>$BIN/local-search-web</string>
    <string>--port</string><string>$PORT</string>
    <string>--home</string><string>$HOME_DIR</string>
    <string>--releases-url</string><string>$URL</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
</dict>
</plist>
EOF
    launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
    launchctl bootstrap "gui/$(id -u)" "$AGENT"
  else
    step "Web UI"
    echo "No systemd user session here; start it yourself with:"
    echo "  $BIN/local-search-web --port $PORT"
  fi
elif [ -n "$CHANGED" ]; then
  # A new image pack is only picked up on start.
  if has_systemd && systemctl --user -q is-active "$UNIT" 2>/dev/null; then
    systemctl --user restart "$UNIT"
  elif is_mac && [ -f "$AGENT" ]; then
    launchctl kickstart -k "gui/$(id -u)/$LABEL" 2>/dev/null || true
  fi
fi

# The download that piping into bash unpacked.
[ -n "${BURROW_TMP:-}" ] && rm -rf "$BURROW_TMP"
step "Done"
[ -n "$PROGRAMS" ] || exit 0
case ":$PATH:" in *":$BIN:"*) ;; *) echo "Add $BIN to your PATH to run the programs by name." ;; esac
echo "Search in your browser:  http://localhost:$PORT"
echo "Or in the terminal:      local-search \"your query\""
echo "Get newer packs later:   burrow-update"
