#!/bin/sh
# Installs PiDashboard (the board PiDash connects to) with Docker Compose:
#
#   curl -fsSL https://pidash.io/install/setup | sh
#
# It does the steps of https://pidash.io/docs/install.html for you: downloads
# the Compose project, picks the newest version, makes the folders and the
# secrets, sets your user id, asks for the basics of config.yaml and starts
# the board. Run it again at any time: it keeps an existing config.yaml,
# .env and secrets, and only downloads the install files anew.
#
# Settings, all optional, as environment variables (put them after `| `,
# e.g. `| PIDASH_DIR=/srv/pidash sh`):
#   PIDASH_DIR      the project folder (default: ~/pidash)
#   PIDASH_VERSION  the image version (default: the newest on Docker Hub)
set -eu

BASE=https://pidash.io/install
IMAGE=graxo/pidash-server

say() { printf '%s\n' "$*"; }
step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
die() { printf 'pidash: %s\n' "$*" >&2; exit 1; }

# Questions come from the terminal, also when the script itself comes from a pipe.
tty_ok() { [ -r /dev/tty ] && [ -w /dev/tty ] && (: </dev/tty) 2>/dev/null; }

# ask VAR "Question" [default]
ask() {
  _q=$2; _d=${3:-}
  if [ -n "$_d" ]; then printf '%s [%s]: ' "$_q" "$_d" >/dev/tty; else printf '%s: ' "$_q" >/dev/tty; fi
  IFS= read -r _a </dev/tty || _a=
  [ -n "$_a" ] || _a=$_d
  eval "$1=\$_a"
}

# ask_secret VAR "Question": not echoed.
ask_secret() {
  printf '%s: ' "$2" >/dev/tty
  stty -echo </dev/tty 2>/dev/null || true
  IFS= read -r _a </dev/tty || _a=
  stty echo </dev/tty 2>/dev/null || true
  printf '\n' >/dev/tty
  eval "$1=\$_a"
}

# A value for a double-quoted YAML string and a sed replacement; check_value
# first, since a die in $(...) only leaves the subshell.
yaml_value() { printf '"%s"' "$1" | sed 's/[|&]/\\&/g'; }
check_value() {
  case $1 in *'"'*|*'\'*) die "a value may not contain \" or \\: $1" ;; esac
}

newest_version() {
  curl -fsSL "https://hub.docker.com/v2/repositories/$IMAGE/tags?page_size=100" 2>/dev/null |
    grep -o '"name":"[0-9]*\.[0-9]*\.[0-9]*"' | cut -d'"' -f4 | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1
}

default_timezone() {
  if [ -r /etc/timezone ]; then head -n 1 /etc/timezone
  elif command -v timedatectl >/dev/null 2>&1; then timedatectl show -p Timezone --value 2>/dev/null
  fi
}

# Writes config.yaml from the example with the answers, and comments out
# every connection except Alertmanager (and Prometheus when given): those
# need secrets you add later, one at a time.
write_config() {
  for v in "$public_url" "$timezone" "$issuer" "$client_id" "$users_group" "$am_url" "$prom_url"; do
    check_value "$v"
  done
  keep=alertmanager
  [ -n "$prom_url" ] && keep="$keep prometheus"
  sed -e "s|^public_url: .*|public_url: $(yaml_value "$public_url")|" \
      -e "s|^timezone: .*|timezone: $(yaml_value "$timezone")|" \
      -e "s|^  issuer: .*|  issuer: $(yaml_value "$issuer")|" \
      -e "s|^  client_id: .*|  client_id: $(yaml_value "$client_id")|" \
      -e "s|^    users: .*|    users: $(yaml_value "$users_group")|" \
      -e "s|^    operators: .*|    # operators: alertboard-operators|" \
      -e "s|^      url: http://alertmanager:9093|      url: $(yaml_value "$am_url")|" \
      -e "s|^    url: http://prometheus:9090|    url: $(yaml_value "${prom_url:-http://prometheus:9090}")|" \
      config.yaml.example |
  awk -v keep=" $keep " '
    /^connections:/ { inconn = 1; print; next }
    inconn && /^[^ #]/ { inconn = 0 }
    inconn && /^  [a-z_]+:/ { k = $1; sub(":", "", k); drop = index(keep, " " k " ") == 0 }
    inconn && drop && /^  [^#]/ { print "  # " substr($0, 3); next }
    { print }' > config.yaml.new
  mv config.yaml.new config.yaml
  chmod 600 config.yaml
}

main() {
  dir=${PIDASH_DIR:-$HOME/pidash}
  interactive=false
  tty_ok && interactive=true

  step "Checking what is needed"
  for c in curl openssl docker; do
    command -v "$c" >/dev/null 2>&1 || die "$c is not installed. Docker: https://docs.docker.com/engine/install/"
  done
  docker compose version >/dev/null 2>&1 || die "the Docker Compose plugin is missing: https://docs.docker.com/compose/install/linux/"
  docker info >/dev/null 2>&1 ||
    die "can't talk to Docker as $(id -un). Add yourself to the docker group (sudo usermod -aG docker $(id -un)), log out and in, and run this again."
  say "curl, openssl, Docker and Compose: OK"

  step "Getting the files into $dir"
  mkdir -p "$dir"
  cd "$dir"
  # The site serves no .sh files (its web server refuses the extension), so
  # the backup script comes as db-backup and is saved under the name
  # compose.yaml mounts.
  for f in compose.yaml config.yaml.example db-backup:db-backup.sh; do
    src=${f%%:*}; dst=${f#*:}
    curl -fsSL -o "$dst.new" "$BASE/$src" || die "could not download $BASE/$src"
    mv "$dst.new" "$dst"
  done
  chmod 755 db-backup.sh
  say "compose.yaml, config.yaml.example, db-backup.sh"

  if [ -f .env ] && grep -q '^PIDASH_VERSION=' .env; then
    version=$(sed -n 's/^PIDASH_VERSION=//p' .env | tail -n 1)
    say "Keeping version $version from .env"
  else
    version=${PIDASH_VERSION:-$(newest_version)}
    [ -n "$version" ] || die "could not find the newest version on Docker Hub; run again with PIDASH_VERSION=<version> (https://hub.docker.com/r/$IMAGE/tags)"
    printf 'PIDASH_VERSION=%s\n' "$version" >> .env
    say "Version $version, in .env"
  fi

  step "Making the folders and the secrets"
  mkdir -p secrets data/mariadb data/backup data/alertboard
  chmod 700 secrets data
  for s in db_password:24 db_root_password:24 session_key:32; do
    f=secrets/${s%%:*}
    [ -s "$f" ] || (umask 077; openssl rand -hex "${s##*:}" > "$f")
  done
  chmod 600 secrets/*
  say "secrets/db_password, secrets/db_root_password, secrets/session_key"

  uid=$(id -u); gid=$(id -g)
  if [ "$uid" != 1000 ] || [ "$gid" != 1000 ]; then
    sed "s|user: \"1000:1000\"|user: \"$uid:$gid\"|" compose.yaml > compose.yaml.new && mv compose.yaml.new compose.yaml
    say "Your user id is $uid:$gid; set it in compose.yaml"
  fi

  if [ -f config.yaml ]; then
    step "Keeping your config.yaml"
    configured=true
  elif ! $interactive; then
    step "Writing config.yaml"
    cp config.yaml.example config.yaml
    chmod 600 config.yaml
    configured=false
    say "No terminal to ask questions on: copied the example. Fill it in (https://pidash.io/docs/install.html#config)."
  else
    step "The basics of config.yaml"
    say "Press Enter to take the value in [brackets]. You can change all of it later in $dir/config.yaml."
    say ""
    while :; do
      ask public_url "The HTTPS address you will open the board on (e.g. https://board.example.com)"
      public_url=${public_url%/}
      case $public_url in https://?*) break ;; esac
      say "It must start with https:// (the login cookie only works over HTTPS; a name on your LAN or VPN is fine)."
    done
    ask timezone "Time zone" "$(default_timezone)"
    [ -n "$timezone" ] || timezone=Europe/Amsterdam
    say ""
    say "In your login provider (Authentik, Keycloak, ...), make a confidential OIDC client with redirect URI"
    say "    $public_url/auth/callback"
    say "and a group for the people who may log in. Steps: https://pidash.io/docs/connections.html#oidc"
    say ""
    ask issuer "Its issuer URL (e.g. https://login.example.com/application/o/alertboard/)"
    ask client_id "Its client ID" "alertboard"
    ask_secret client_secret "Its client secret (not shown; Enter to add it later)"
    ask users_group "The group that may log in" "alertboard-users"
    say ""
    ask am_url "Your Alertmanager, as this machine reaches it" "http://alertmanager:9093"
    ask prom_url "Your Prometheus, for history and metrics (Enter for none)"
    write_config
    if [ -n "$client_secret" ]; then
      (umask 077; printf '%s\n' "$client_secret" > secrets/oidc_client_secret)
      chmod 600 secrets/oidc_client_secret
      configured=true
    else
      configured=false
    fi
    say "Wrote config.yaml"
  fi

  if [ ! -s secrets/oidc_client_secret ]; then
    configured=false
    missing_secret=true
  else
    missing_secret=false
  fi

  start=false
  if $configured && $interactive; then
    ask yn "Start the board now? [Y/n]" ""
    case $yn in [nN]*) ;; *) start=true ;; esac
  fi

  if $start; then
    step "Starting the board (the first time pulls the images)"
    docker compose up -d </dev/null
    say "Waiting for it to become healthy..."
    i=0
    while [ $i -lt 60 ]; do
      st=$(docker inspect -f '{{.State.Health.Status}}' alertboard 2>/dev/null || true)
      [ "$st" = healthy ] && break
      i=$((i + 1)); sleep 2
    done
    if [ "$st" = healthy ]; then
      say "Running. Check it on this machine: http://127.0.0.1:8080"
    else
      say "Not healthy yet. See why with: cd $dir && docker compose logs alertboard"
    fi
  fi

  step "Next"
  n=1
  if $missing_secret; then
    say "$n. Put the OIDC client secret in place:"
    say "     printf '%s\\n' '<the client secret>' > $dir/secrets/oidc_client_secret && chmod 600 $dir/secrets/oidc_client_secret"
    n=$((n + 1))
  fi
  if ! $start; then
    say "$n. Check $dir/config.yaml, then start it:  cd $dir && docker compose up -d"
    n=$((n + 1))
  fi
  say "$n. Point your TLS reverse proxy at port 8081 of this machine and serve it on your public_url."
  say "   Internet-facing or LAN/VPN only, both work: https://pidash.io/docs/install.html#proxy"
  n=$((n + 1))
  say "$n. Install the app and sign in: https://pidash.io/docs/app.html"
}

main "$@"
