#!/bin/bash

# ---------------------------------------------------------------------------------------------------------------------
# Installs Docker and LeTTo (version 1.3) on
#   - Ubuntu 22.04 LTS, 24.04 LTS, 26.04 LTS
#   - Debian 12 (bookworm), Debian 13 (trixie)
#
# The script makes the complete installation of a LeTTo server:
#   - asks for all settings of the server (passwords, server information, DNS name, email address, certificate)
#   - installs Docker and creates the user letto
#   - creates the common .env file of all containers in $lettobase/docker/compose/letto with all keys and passwords
#   - starts MySQL, Mongo and Redis and creates the database users
#   - starts the proxy, checks if the server is reachable and creates the Let's Encrypt certificate
#   - downloads the compose files of all services and the configuration files of the proxy
#   - starts the setup service and then all services one after the other
#   - checks the installation and shows how to connect to the server
# After the installation the setup service needs no initial configuration (SETUP_INITIALIZED=true).
#
# All settings below which are empty are asked on the console. All inputs are saved in this script, therefore
# the script can be started again if the installation was cancelled or something did not work, and the
# inputs need not be entered again. The state of the installation is stored in $lettobase/docker/compose/letto/install-state.
#
# Usage: install-letto-ubuntu-docker.sh [-p<password>] [-m<mysql-password>] [-t<tag>]
#   -p<password>        password for the user 'admin' of the setup service (and the new linux user 'letto')
#   -m<mysql-password>  root password of the databases MySQL, Mongo and Redis
#   -t<tag>             tag of the LeTTo docker images, default stable-1.3
#                         stable-1.3  stable standard version
#                         daily-1.3   current version for schools which want the newest features
#                         beta-1.3    version for development tests
#                       MySQL, Mongo and Redis also get this tag, only an update keeps their version
# ---------------------------------------------------------------------------------------------------------------------

# password for the user 'admin' in the web-configuration-site
# if user letto is previously not installed the password is used for the new user letto
password=

# root password of the databases MySQL, Mongo and Redis
mysqlpassword=

# password of the database user 'letto' which is used by the LeTTo services
lettopassword=

# password of the database user 'lettolti' of the LTI service
ltipassword=

# information about the operator of the server, e.g. name and address of the school
serverinfo=

# DNS name under which the server is reachable from the internet, e.g. letto.myschool.at
dnsname=

# email address of the server administrator, it is also used for the Let's Encrypt certificate
email=

# https certificate of the server
#   letsencrypt  the certificate is created by Let's Encrypt, the server must be reachable from the internet
#   own          own PEM certificate in $lettobase/docker/cert/cert.crt and cert.key
#   selfsigned   self-signed certificate, only for tests
certificate=

# tag of the LeTTo docker images (stable-1.3, daily-1.3 or beta-1.3)
lettotag=stable-1.3

# docker images of the databases - empty: the newest version which runs on this server is used
#   MySQL: lettohub/letto-mysql:9 (9.7), :8.4.8, :8.4.0-OldCPU (old CPUs), :8.0
#   Mongo: lettohub/letto-mongo:9, :8, :<lettotag> (4.4)
#   Redis: lettohub/letto-redis:8, :7.4
mysqlimage=
mongoimage=
redisimage=

# Base directory for Server-Installation
lettobase=/opt/letto

# Download of the compose files and scripts for version 1.3
downloadurl=https://build.letto.at/download/install-1.3

# Port of the setup service
setupport=3096

# ---------------------------------------------------------------------------------------------------------------------

composeletto=$lettobase/docker/compose/letto
proxydir=$lettobase/docker/proxy
certdir=$lettobase/docker/cert
statefile=$composeletto/install-state
certdoc=https://doc.letto.at/wiki/Https-Zertifikat/
scriptfile=$(readlink -f "$0" 2>/dev/null)

# services which are installed, they are started in this order after the setup service
services="login question plugin pluginsourcecode export main edit app htmlpreview download"
# configuration files of the proxy for the setup service, phpmyadmin and the services
proxyconfs="config phpmyadmin login question plugin pluginsourcecode export main edit app download"

# true if the inputs are saved in the script when it ends
saveonexit=false
# number of problems found during the installation
problems=0

fail() {
  echo
  echo "error: $1"
  echo "installation cancelled!"
  exit 1
}

warn() {
  echo "warning: $1"
  problems=$((problems+1))
}

# true if the script can ask on the console
interactive() {
  [ -t 0 ]
}

# asks a yes/no question, $2 is the default answer (y or n) which is also used without console
askyesno() {
  local answer
  if ! interactive ; then
    [ "$2" = y ]
    return
  fi
  if [ "$2" = y ] ; then echo -n "$1 [Y/n] " ; else echo -n "$1 [y/N] " ; fi
  read -r answer
  if [ -z "$answer" ] ; then answer=$2 ; fi
  [[ "$answer" =~ ^[yYjJ] ]]
}

# asks if the installation should be continued after a problem, otherwise it is cancelled
askcontinue() {
  askyesno "Continue the installation anyway?" n || fail "$1"
}

# checks a password for characters which cannot be stored in the .env file
# returns the error message or nothing if the password is valid
checkpassword() {
  if [[ "$1" =~ [[:space:]\'\"\\\`] ]] ; then
    echo "the password must not contain spaces, quotes, backslashes or backticks"
  fi
}

checkserverinfo() {
  if [[ "$1" =~ [\'\"\\\`\$#] ]] ; then
    echo "the server information must not contain quotes, backslashes, backticks, \$ or #"
  fi
}

checkdnsname() {
  if ! [[ "${1,,}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$ ]] ; then
    echo "'$1' is not a valid DNS name"
  fi
}

checkemail() {
  if ! [[ "$1" =~ ^[^@[:space:]\'\"\\\`\$#]+@[^@[:space:]\'\"\\\`\$#]+\.[^@[:space:]\'\"\\\`\$#]+$ ]] ; then
    echo "'$1' is not a valid email address"
  fi
}

# asks for a value if the variable is empty and checks it
#   $1 name of the variable, $2 prompt, $3 check function, $4 default value (optional)
askvalue() {
  local var=$1 prompt=$2 check=$3 default=$4 value error
  value=${!var}
  if [ -n "$value" ] ; then
    error=$($check "$value")
    if [ -n "$error" ] ; then fail "$error" ; fi
    return
  fi
  interactive || fail "$prompt is not set - please enter it in the settings at the beginning of $scriptfile"
  while true ; do
    if [ -n "$default" ] ; then echo -n "$prompt [$default]: " ; else echo -n "$prompt: " ; fi
    read -r value
    if [ -z "$value" ] ; then value=$default ; fi
    if [ -z "$value" ] ; then
      echo "please enter a value - Ctrl-C to cancel the installation"
      continue
    fi
    error=$($check "$value")
    if [ -z "$error" ] ; then break ; fi
    echo "$error"
  done
  printf -v "$var" '%s' "$value"
}

# asks for a password if the variable is empty, the admin password can be used instead
#   $1 name of the variable, $2 description of the password
askpassword() {
  local var=$1 info=$2 value value2 error
  value=${!var}
  if [ -z "$value" ] ; then
    if askyesno "Use the admin password also as $info?" y ; then
      value=$password
    else
      while true ; do
        echo -n "$info: "
        read -rs value
        echo
        error=$(checkpassword "$value")
        if [ -z "$value" ] ; then
          echo "please enter a password - Ctrl-C to cancel the installation"
        elif [ -n "$error" ] ; then
          echo "$error"
        else
          echo -n "repeat $info: "
          read -rs value2
          echo
          if [ "$value" = "$value2" ] ; then break ; fi
          echo "the passwords are not equal"
        fi
      done
    fi
  fi
  error=$(checkpassword "$value")
  if [ -n "$error" ] ; then fail "$info: $error" ; fi
  printf -v "$var" '%s' "$value"
}

# saves all settings in this script, so that they need not be entered again at the next start
# the script is written to a new file, the running bash keeps reading the old one
savesettings() {
  local tmp line name saved=" "
  if [ ! -f "$scriptfile" ] || [ ! -w "$scriptfile" ] ; then
    echo "warning: cannot save the settings in $0"
    return 1
  fi
  tmp=$(mktemp "$scriptfile.XXXXXX") || return 1
  while IFS= read -r line || [ -n "$line" ] ; do
    name=${line%%=*}
    if [[ "$line" =~ ^(password|mysqlpassword|lettopassword|ltipassword|serverinfo|dnsname|email|certificate|lettotag|mysqlimage|mongoimage|redisimage)= ]] \
       && [[ "$saved" != *" $name "* ]] ; then
      # no value may contain a single quote
      printf "%s='%s'\n" "$name" "${!name}" >>"$tmp"
      saved="$saved$name "
    else
      printf '%s\n' "$line" >>"$tmp"
    fi
  done <"$scriptfile"
  chmod 700 "$tmp" && mv -f "$tmp" "$scriptfile" || { rm -f "$tmp" ; return 1 ; }
}

# at the end of the script all inputs are saved in the script
onexit() {
  if [ "$saveonexit" = true ] ; then
    interactive && stty echo 2>/dev/null
    echo
    if savesettings ; then
      if [ "$(cat "$statefile" 2>/dev/null)" != finished ] ; then
        echo "All inputs are saved in $scriptfile, they are used at the next start of the script."
      fi
      echo "The script contains passwords - please delete it after the installation or remove the passwords."
    fi
  fi
}
trap onexit EXIT

# value of a variable in the .env file
envget() {
  [ -f "$composeletto/.env" ] || return
  sed -n "s/^$1=//p" "$composeletto/.env" | tail -1 | sed "s/^'\(.*\)'$/\1/"
}

# value for the .env file - values with $ are quoted like the setup service does it
envvalue() {
  if [[ "$1" == *'$'* ]] ; then echo "'$1'" ; else echo "$1" ; fi
}

# sets a variable in the .env file
setenv() {
  if grep -q "^$1=" "$composeletto/.env" ; then
    sed -i "s|^$1=.*|$1=$2|" "$composeletto/.env"
  else
    echo "$1=$2" >>"$composeletto/.env"
  fi
}

# random string with letters and digits like ENCRYPT.generateKeyAz09 of the setup service
randomkey() {
  tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$1"
}

# creates the directory for the own certificate with a README
createcertdir() {
  mkdir -p "$certdir" || fail "cannot create $certdir"
  chmod 750 "$certdir"
  cat >"$certdir/README.md" <<EOF
# own https certificate of the LeTTo server

Copy the PEM certificate of the server into this directory:

    $certdir/cert.crt   certificate including the intermediate certificates (full chain)
    $certdir/cert.key   private key of the certificate (without password)

The certificate must be valid for the DNS name of the server ($dnsname).
The proxy letto-proxy uses the certificate if CERT_EXTERN=1 is set in $composeletto/.env.
After changing the certificate the proxy must be restarted: docker restart letto-proxy

See also $certdoc
EOF
}

# checks the own certificate, returns an error message or nothing if the certificate is valid
checkowncert() {
  local crt=$certdir/cert.crt key=$certdir/cert.key
  if [ ! -s "$crt" ] || [ ! -s "$key" ] ; then
    echo "the own certificate is missing - copy the PEM files cert.crt and cert.key into $certdir (see $certdir/README.md)"
    return
  fi
  # without openssl only the existence can be checked, it is installed later with the linux packages
  command -v openssl >/dev/null || return
  if ! openssl x509 -in "$crt" -noout 2>/dev/null ; then
    echo "$crt is not a valid PEM certificate"
  elif ! openssl pkey -in "$key" -noout -passin pass: 2>/dev/null ; then
    echo "$key is not a valid PEM key without password"
  elif [ "$(openssl x509 -in "$crt" -noout -pubkey)" != "$(openssl pkey -in "$key" -pubout -passin pass:)" ] ; then
    echo "the key $key does not belong to the certificate $crt"
  fi
}

# warnings for an own certificate which can be used but may cause problems
warnowncert() {
  local crt=$certdir/cert.crt
  command -v openssl >/dev/null || return
  if ! openssl x509 -in "$crt" -noout -checkend 0 >/dev/null 2>&1 ; then
    warn "the certificate $crt is expired"
  elif ! openssl x509 -in "$crt" -noout -checkend 2592000 >/dev/null 2>&1 ; then
    warn "the certificate $crt expires within 30 days"
  fi
  if ! openssl x509 -in "$crt" -noout -checkhost "$dnsname" 2>/dev/null | grep -q "does match" ; then
    warn "the certificate $crt is not valid for $dnsname"
  fi
}

# downloads a file from the download server or cancels the installation
download() {
  curl -fsSL "$downloadurl/$1" -o "$2" || fail "cannot download $downloadurl/$1"
}

# sets the tag of all LeTTo images in a compose file (like settag.sh and the setup service)
settag() {
  sed -E -i "s#(lettohub/[A-Za-z0-9_.-]+:)[A-Za-z0-9_.-]+#\1$lettotag#g" "$1" || fail "cannot set tag $lettotag in $1"
}

# state of a container: healthy, unhealthy or starting for containers with healthcheck,
# otherwise running, exited, ... or missing if the container does not exist
containerstate() {
  local state
  state=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$1" 2>/dev/null)
  echo "${state:-missing}"
}

# starts a compose file in the compose directory and waits until the container is running
startcompose() {
  local ymlfile=$1
  local container=$2
  echo "start $container"
  docker compose -f "$composeletto/$ymlfile" up -d --pull always --quiet-pull || fail "cannot start $container - please check 'docker compose -f $ymlfile up -d' in $composeletto"
  for i in $(seq 1 60) ; do
    if [ "$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)" = true ] ; then
      return 0
    fi
    sleep 2
  done
  fail "$container is not running - please check 'docker logs $container'"
}

# waits until a command succeeds
#   $1 description, $2 timeout in seconds, the rest is the command
waitfor() {
  local info=$1 timeout=$2 t=0
  shift 2
  echo -n "wait for $info "
  until "$@" >/dev/null 2>&1 ; do
    if [ $t -ge "$timeout" ] ; then
      echo " timeout after $t seconds"
      return 1
    fi
    echo -n .
    sleep 3
    t=$((t+3))
  done
  echo " ok after $t seconds"
}

mysqlready() {
  docker exec -e MYSQL_PWD="$mysqlpassword" letto-mysql mysql -h127.0.0.1 -uroot -e "SELECT 1"
}

mongoready() {
  # MongoDB 6 and newer have only mongosh, MongoDB 4.4 only mongo
  local shell=mongosh
  docker exec letto-mongo sh -c 'command -v mongosh' >/dev/null 2>&1 || shell=mongo
  [ "$(docker exec letto-mongo $shell --quiet -u root -p "$mysqlpassword" --authenticationDatabase admin \
        --eval "db.adminCommand('ping').ok" 2>/dev/null | tr -d '[:space:]')" = 1 ]
}

# image of a database in an existing compose file, e.g. lettohub/letto-mysql:9
composeimage() {
  [ -f "$composeletto/$1" ] && grep -o "lettohub/$2:[A-Za-z0-9_.-]*" "$composeletto/$1" | head -1
}

# chooses the newest image of a database which runs on this server: the server binary of the image is
# started with --version like the diagnostic of the setup service does it. Test images which were not
# present before are removed again if they do not run.
#   $1 name of the variable, $2 name of the database, $3 server binary, the rest are the images from new to old
choosedbimage() {
  local var=$1 info=$2 binary=$3 image existed
  shift 3
  if [ -n "${!var}" ] ; then
    echo "$info: ${!var}"
    return
  fi
  for image in "$@" ; do
    existed=false
    docker image inspect "$image" >/dev/null 2>&1 && existed=true
    if docker run --rm --pull=always --entrypoint "$binary" "$image" --version >/dev/null 2>&1 ; then
      echo "$info: $image runs on this server"
      printf -v "$var" '%s' "$image"
      return
    fi
    echo "$info: $image does not run on this server"
    if [ $existed = false ] ; then docker rmi "$image" >/dev/null 2>&1 ; fi
  done
  fail "no version of $info runs on this server"
}

redisready() {
  [ "$(docker exec -e REDISCLI_AUTH="$mysqlpassword" letto-redis redis-cli ping 2>/dev/null | tr -d '[:space:]')" = PONG ]
}

proxyready() {
  curl -fsSk --max-time 5 -o /dev/null https://127.0.0.1/check.txt
}

setupready() {
  [ "$(curl -fsSk --max-time 5 "https://127.0.0.1:$setupport/config/ping")" = pong ]
}

# runs the healthcheck of the image at once, docker runs it only in its interval (up to 90 seconds)
runhealthcheck() {
  local test=()
  mapfile -t test < <(docker inspect -f '{{range .Config.Healthcheck.Test}}{{println .}}{{end}}' "$1" 2>/dev/null)
  case "${test[0]}" in
    CMD-SHELL) timeout 20 docker exec "$1" sh -c "${test[1]}" ;;
    CMD)       timeout 20 docker exec "$1" "${test[@]:1}" ;;
    *)         return 1 ;;
  esac
}

# a service is ready when its healthcheck is ok, a container without healthcheck when it runs for 10 seconds
serviceready() {
  local started
  case "$(containerstate "$1")" in
    healthy)  return 0 ;;
    starting) runhealthcheck "$1" ;;
    running)
      # container without healthcheck - ready when it runs for 10 seconds
      started=$(date -d "$(docker inspect -f '{{.State.StartedAt}}' "$1")" +%s) || return 1
      [ $(($(date +%s)-started)) -ge 10 ]
      ;;
    *) return 1 ;;
  esac
}

# checks if the server reaches itself over its DNS name like Let's Encrypt does it:
# a file in the webroot of certbot is loaded with http (port 80), the proxy redirects to https (port 443)
checkreachable() {
  local token dir=/data/letsencrypt/.well-known/acme-challenge answer
  token=letto-install-$(randomkey 32)
  docker exec letto-proxy sh -c "mkdir -p $dir && echo $token >$dir/$token" || return 1
  for i in $(seq 1 5) ; do
    answer=$(curl -fsSL -k --max-time 10 "http://$dnsname/.well-known/acme-challenge/$token" 2>/dev/null)
    [ "$answer" = "$token" ] && break
    sleep 2
  done
  docker exec letto-proxy rm -f "$dir/$token"
  [ "$answer" = "$token" ]
}

# checks if the server reaches itself with https and a valid certificate, asks to continue on problems
checkhttps() {
  local error
  echo "check https://$dnsname"
  if [ "$certificate" = selfsigned ] || { [ "$certificate" = letsencrypt ] && [ "$(envget CERT_CREATED)" != true ] ; } ; then
    if curl -fsSk --max-time 10 -o /dev/null "https://$dnsname/check.txt" ; then
      echo "the server is reachable as https://$dnsname with a self-signed certificate - browsers show a security warning"
      return
    fi
    warn "the server cannot reach itself as https://$dnsname - please check the DNS entry and the firewall"
    askcontinue "the server is not reachable as https://$dnsname"
    return
  fi
  if error=$(curl -fsS --max-time 10 -o /dev/null "https://$dnsname/check.txt" 2>&1) ; then
    echo "the server is reachable as https://$dnsname with a valid certificate"
  elif curl -fsSk --max-time 10 -o /dev/null "https://$dnsname/check.txt" ; then
    warn "the server is reachable as https://$dnsname but the certificate is not valid: $error"
    askcontinue "the certificate of https://$dnsname is not valid"
  else
    warn "the server cannot reach itself as https://$dnsname - please check the DNS entry and the firewall"
    askcontinue "the server is not reachable as https://$dnsname"
  fi
}

echo Install LeTTo 1.3
echo

# check if root-rights are available
if [ "$(id -u)" != 0 ] ; then
  echo No root rights! Please start setup as root!
  exit 1
fi

# check linux distribution
if [ ! -r /etc/os-release ] ; then
  fail "cannot detect linux distribution - /etc/os-release not found"
fi
. /etc/os-release
case "$ID" in
  ubuntu)
    arch=ubuntu
    codename=${UBUNTU_CODENAME:-$VERSION_CODENAME}
    case "$VERSION_ID" in
      22.04|24.04|26.04) ;;
      *) echo "warning: Ubuntu $VERSION_ID is not tested - supported are Ubuntu 22.04, 24.04 and 26.04 LTS" ;;
    esac
    ;;
  debian)
    arch=debian
    codename=$VERSION_CODENAME
    case "$VERSION_ID" in
      12|13) ;;
      *) echo "warning: Debian $VERSION_ID is not tested - supported are Debian 12 and 13" ;;
    esac
    ;;
  *)
    fail "please run script only on Ubuntu or Debian or adapt script to your linux-distribution"
    ;;
esac
if [ -z "$codename" ] ; then
  fail "cannot detect the codename of $PRETTY_NAME"
fi
echo "Linux distribution: $PRETTY_NAME"

# an installation of this script which was not finished is continued, other installations are not overwritten
rerun=false
if [ -e "$composeletto/.env" ] ; then
  case "$(cat "$statefile" 2>/dev/null)" in
    running)
      rerun=true
      echo "The previous installation was not finished - it is continued."
      ;;
    finished)
      fail "LeTTo is already installed in $composeletto - please use the setup service for updates"
      ;;
    *)
      fail "LeTTo is already installed in $composeletto - please use the setup service for updates"
      ;;
  esac
fi
if [ -e "$lettobase/docker/compose/setup/docker-compose.yml" ] ; then
  fail "LeTTo 1.2 is installed in $lettobase/docker/compose/setup - please use update-letto-1.3.sh for the update to 1.3"
fi

# commandline-parameters
for i in "$@" ; do
  # password
  if [[ $i == "-p"* ]] ; then
    password=${i:2}
  fi
  # mysql password
  if [[ $i == "-m"* ]] ; then
    mysqlpassword=${i:2}
  fi
  # tag of the docker images
  if [[ $i == "-t"* ]] ; then
    lettotag=${i:2}
  fi
done
if ! [[ "$lettotag" =~ ^[A-Za-z0-9_.-]+$ ]] ; then
  fail "invalid docker image tag '$lettotag'"
fi

# a continued installation uses the values of the .env which are not set in the script
if [ $rerun = true ] ; then
  envmysql=$(envget MYSQL_ROOT_PASSWORD)
  if [ -n "$envmysql" ] && [ -n "$mysqlpassword" ] && [ "$envmysql" != "$mysqlpassword" ] ; then
    echo "The databases are already created with the root password of $composeletto/.env - it is used."
  fi
  [ -n "$envmysql" ] && mysqlpassword=$envmysql
  [ -z "$lettopassword" ] && lettopassword=$(envget MYSQL_LETTO_PASSWORD)
  [ -z "$ltipassword" ]   && ltipassword=$(envget MYSQL_LTI_PASSWORD)
  [ -z "$serverinfo" ]    && serverinfo=$(envget SERVER_INFO)
  [ -z "$dnsname" ]       && dnsname=$(envget SERVER_NAME)
  [ -z "$email" ]         && email=$(envget EMAIL)
fi

# ------------------------------------------ settings of the server ---------------------------------------------------
echo
echo "Settings of the LeTTo server - Ctrl-C to cancel the installation"
echo
saveonexit=true

# password for user admin in the setup
if [ -z "$password" ] ; then
  interactive || fail "the admin password is not set - please use -p<password> or enter it in $scriptfile"
  if getent passwd letto >/dev/null ; then
    echo "Set password for the user 'admin' in the web-configuration-site - No Passwort to cancel installation."
  else
    echo "Set password for the linux-user 'letto' and the user 'admin' in the web-configuration-site - No Passwort to cancel installation."
  fi
  echo -n "password: "
  read -rs password
  echo
  if [ -z "$password" ] ; then
    fail "no password entered"
  fi
  echo -n "repeat password: "
  read -rs password2
  echo
  if [ "$password" != "$password2" ] ; then
    password=
    fail "the passwords are not equal"
  fi
fi
error=$(checkpassword "$password")
if [ -n "$error" ] ; then fail "admin password: $error" ; fi

# passwords of the databases
askpassword mysqlpassword "root password for MySQL, Mongo and Redis"
askpassword lettopassword "password of the database user 'letto'"
askpassword ltipassword   "password of the database user 'lettolti' of the LTI service"

# information about the server
askvalue serverinfo "information about the operator of the server (e.g. name and address of the school)" checkserverinfo
dnsname=${dnsname,,}
askvalue dnsname "DNS name of the server (e.g. letto.myschool.at)" checkdnsname "$(hostname -f 2>/dev/null | tr '[:upper:]' '[:lower:]')"
dnsname=${dnsname,,}
askvalue email "email address of the server administrator" checkemail

# https certificate
if [ -z "$certificate" ] && [ $rerun = true ] && [ "$(envget CERT_EXTERN)" = 1 ] ; then
  certificate=own
fi
if [ -z "$certificate" ] ; then
  interactive || fail "the certificate is not set - please enter letsencrypt, own or selfsigned in $scriptfile"
  echo
  echo "https certificate of the server:"
  echo "  1) Let's Encrypt - the certificate is created automatically,"
  echo "     $dnsname must be reachable from the internet on port 80 and 443"
  echo "  2) own certificate - PEM files cert.crt and cert.key in $certdir"
  echo "  3) self-signed certificate - only for tests, browsers show a security warning"
  while [ -z "$certificate" ] ; do
    echo -n "certificate [1]: "
    read -r answer
    case "$answer" in
      ""|1) certificate=letsencrypt ;;
      2)    certificate=own ;;
      3)    certificate=selfsigned ;;
      *)    echo "please enter 1, 2 or 3" ;;
    esac
  done
fi
case "$certificate" in
  letsencrypt)
    if [ "$dnsname" = localhost ] || [[ "$dnsname" != *.* ]] ; then
      fail "Let's Encrypt needs a public DNS name, not '$dnsname'"
    fi
    ;;
  own|selfsigned) ;;
  *) fail "invalid certificate '$certificate' - valid are letsencrypt, own or selfsigned" ;;
esac

# summary of all settings
samepw() {
  if [ "$1" = "$password" ] ; then echo "same as admin password" ; else echo "own password" ; fi
}
echo
echo "Settings of the installation:"
echo "  server information     : $serverinfo"
echo "  DNS name               : $dnsname"
echo "  email of the admin     : $email"
echo "  certificate            : $certificate"
echo "  docker image tag       : $lettotag"
echo "  MySQL/Mongo/Redis root : $(samepw "$mysqlpassword")"
echo "  database user letto    : $(samepw "$lettopassword")"
echo "  database user lettolti : $(samepw "$ltipassword")"
echo "  installation directory : $lettobase"
echo
if ! askyesno "Are all settings correct and should the installation be started?" y ; then
  echo
  echo "The installation is not started. Change the settings at the beginning of $scriptfile"
  echo "(empty values are asked again) and start the script again."
  exit 1
fi

# the own certificate must be installed before the installation starts
if [ "$certificate" = own ] ; then
  createcertdir
  error=$(checkowncert)
  if [ -n "$error" ] ; then
    fail "$error
Install the certificate and start the script again."
  fi
fi

# ------------------------------------------ disk space --------------------------------------------------------------
# the docker images of LeTTo need about 30 GB, a school more than 10 GB - the images are stored in /var/lib
echo
diskdir=/var/lib
if docker images 2>/dev/null | grep -q '^lettohub/' ; then
  # a continued installation has already loaded the images
  minspace=10
else
  minspace=40
fi
# Ubuntu uses only half of the disk for the root filesystem when it is installed with LVM
lvdevice=$(findmnt -n -o SOURCE --target $diskdir 2>/dev/null)
if command -v lvs >/dev/null && LC_ALL=C lvs "$lvdevice" >/dev/null 2>&1 ; then
  vg=$(LC_ALL=C lvs --noheadings -o vg_name "$lvdevice" | tr -d ' ')
  vgfree=$(LC_ALL=C vgs --noheadings --units g --nosuffix -o vg_free "$vg" | tr -d ' ' | cut -d. -f1)
  if [ "${vgfree:-0}" -ge 1 ] ; then
    echo "The volume group $vg has $vgfree GB of unused space which is not used by the filesystem of $diskdir."
    if askyesno "Extend the filesystem $lvdevice by $vgfree GB?" y ; then
      lvextend -r -l +100%FREE "$lvdevice" || fail "cannot extend $lvdevice"
    fi
  fi
fi
diskfree=$(df -BG --output=avail $diskdir | tail -1 | tr -dc '0-9')
echo "free disk space in $diskdir: $diskfree GB"
if [ "$diskfree" -lt $minspace ] ; then
  warn "only $diskfree GB are free in $diskdir, LeTTo needs at least $minspace GB - please enlarge the disk"
  askcontinue "not enough disk space"
fi

# ------------------------------------------ linux and docker --------------------------------------------------------
echo

export DEBIAN_FRONTEND=noninteractive
aptopts=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold)

# update all
apt-get update && \
apt-get "${aptopts[@]}" upgrade && \
apt-get "${aptopts[@]}" dist-upgrade && \
apt-get -y autoremove || fail "system update failed"
echo system is up to date

# install packages - apache2-utils for htpasswd (bcrypt hash of the admin password)
apt-get "${aptopts[@]}" install ca-certificates curl wget gnupg openssl adduser iproute2 iptables apache2-utils \
    zip unzip less nano net-tools gawk htop iputils-ping dos2unix || fail "cannot install linux-packages"
echo needed linux-packages installed

# check the own certificate again, now openssl is available
if [ "$certificate" = own ] ; then
  error=$(checkowncert)
  if [ -n "$error" ] ; then
    fail "$error
Install a valid certificate and start the script again."
  fi
  warnowncert
fi

# check if user letto is present and create if not
if getent passwd letto >/dev/null ; then
  echo user letto exists
else
  useradd -r -m -d /home/letto -s /bin/bash -U letto || fail "cannot create user letto"
  echo "letto:$password" | chpasswd || fail "cannot set password of user letto"
  echo user letto correctly installed
fi

if getent group letto >/dev/null ; then
  echo group letto exists
else
  groupadd letto || fail "cannot create group letto"
fi
if ! id -nG letto | grep -qw letto ; then
  usermod -aG letto letto
fi

# uid from user letto
lettouid=$(id -u letto)

# check and install docker
if docker compose version >/dev/null 2>&1 ; then
  echo docker and docker compose are already installed
else
  echo install docker from https://download.docker.com/linux/$arch
  # remove unofficial docker packages of the distribution
  for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc ; do
    apt-get -y remove $pkg >/dev/null 2>&1
  done
  install -m 0755 -d /etc/apt/keyrings && \
  curl -fsSL https://download.docker.com/linux/$arch/gpg -o /etc/apt/keyrings/docker.asc && \
  chmod a+r /etc/apt/keyrings/docker.asc || fail "cannot download docker gpg-key"
  # old repository definition of previous install scripts
  rm -f /etc/apt/sources.list.d/docker.list /usr/share/keyrings/docker-archive-keyring.gpg
  cat >/etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$arch
Suites: $codename
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
  apt-get update && \
  apt-get "${aptopts[@]}" install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
    || fail "cannot install docker - is $PRETTY_NAME supported by docker?"
fi

systemctl enable --now containerd docker >/dev/null 2>&1

# wait until docker is running
echo -n "wait for docker "
for i in $(seq 1 60) ; do
  docker info >/dev/null 2>&1 && break
  echo -n .
  sleep 2
done
echo
docker info >/dev/null 2>&1 || fail "docker is not running - please check 'systemctl status docker'"
docker compose version >/dev/null 2>&1 || fail "please install the docker compose plugin"
echo "$(docker --version) and $(docker compose version) are running"

# add user letto to docker group
if id -nG letto | grep -qw docker ; then
  echo letto is in group docker
else
  usermod -aG docker letto
fi

# ------------------------------------------ database versions -----------------------------------------------------
# a continued installation keeps the versions of the existing databases, a downgrade would destroy the data
echo
echo "choose the versions of the databases"
[ -z "$mysqlimage" ] && mysqlimage=$(composeimage docker-compose-mysql.yml letto-mysql)
[ -z "$mongoimage" ] && mongoimage=$(composeimage docker-compose-mongo.yml letto-mongo)
[ -z "$redisimage" ] && redisimage=$(composeimage docker-compose-redis.yml letto-redis)
choosedbimage mysqlimage MySQL mysqld \
  lettohub/letto-mysql:9 lettohub/letto-mysql:8.4.8 lettohub/letto-mysql:8.4.0-OldCPU lettohub/letto-mysql:8.0
choosedbimage mongoimage Mongo mongod \
  lettohub/letto-mongo:9 lettohub/letto-mongo:8 "lettohub/letto-mongo:$lettotag"
choosedbimage redisimage Redis redis-server \
  lettohub/letto-redis:8 lettohub/letto-redis:7.4

# ------------------------------------------ configuration ----------------------------------------------------------
# the setup service of a continued installation must not write the .env while it is changed
if [ $rerun = true ] ; then
  docker stop letto-setup >/dev/null 2>&1
fi

# create paths - the same as the setup service needs, the paths of the services are created after the download
echo create paths
for path in compose/letto proxy public cert \
            storage/database storage/backup storage/database-backup storage/database-dump \
            storage/images storage/plugins storage/photos storage/projekte storage/print storage/export \
            storage/log/letto storage/log/data storage/log/setup ; do
  mkdir -p "$lettobase/docker/$path"
done
echo running >"$statefile"
if [ ! -e "$certdir/README.md" ] ; then
  createcertdir
fi

# create standard startpage in public
if [ ! -e $lettobase/docker/public/index.html ] ; then
  cat >$lettobase/docker/public/index.html <<EOF
<!DOCTYPE html>
<html lang="de">
<head>
   <meta charset="UTF-8">
   <title>LeTTo-Server</title>
</head>
<body>
   <a href="https://letto.at"><img src="/img/LeTTo_Logo_animated_10.gif"/></a><br>
   <h1>LeTTo Server - running !</h1>
   <a href="/config">Setup</a><br>
</body>
</html>
EOF
fi

# compose files of the databases, the proxy and the setup service
echo download compose files and scripts from $downloadurl
for yml in docker-compose-mysql.yml docker-compose-mongo.yml docker-compose-redis.yml docker-compose-proxy.yml docker-compose-setup.yml ; do
  download yml/$yml $composeletto/$yml
  settag $composeletto/$yml
done
# versions of the databases
sed -E -i "s#lettohub/letto-mysql:[A-Za-z0-9_.-]+#$mysqlimage#" $composeletto/docker-compose-mysql.yml
sed -E -i "s#lettohub/letto-mongo:[A-Za-z0-9_.-]+#$mongoimage#" $composeletto/docker-compose-mongo.yml
sed -E -i "s#lettohub/letto-redis:[A-Za-z0-9_.-]+#$redisimage#" $composeletto/docker-compose-redis.yml

# scripts in the compose directory - the same as update-letto-1.3.sh loads
for script in settag.sh replacetag.sh replaceschool.sh replacequestion.sh startall.sh stopall.sh ; do
  download scripts/debian-ubuntu/$script $composeletto/$script
done
chmod 755 $composeletto/*.sh

# keys and passwords of the services - a continued installation keeps the values of the .env
echo create keys and passwords
jwtsecret=$(envget JWT_SECRET)
[ -z "$jwtsecret" ] && jwtsecret=$(randomkey 80 | base64 -w0)
serversecret=$(envget SERVER_SECRET)
[ -z "$serversecret" ] && serversecret=$(randomkey 80 | base64 -w0)
privatekey=$(envget LETTO_LOCAL_PRIVATE_KEY)
publickey=$(envget LETTO_LOCAL_PUBLIC_KEY)
if [ -z "$privatekey" ] || [ -z "$publickey" ] ; then
  # RSA key pair like ENCRYPT.generateKeyPair: 1024 bit, PKCS#8 and X.509 as base64 of DER in one line
  pem=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:1024 2>/dev/null) || fail "cannot create the RSA keys"
  privatekey=$(printf '%s\n' "$pem" | openssl pkey -outform DER | base64 -w0)
  publickey=$(printf '%s\n' "$pem" | openssl pkey -pubout -outform DER | base64 -w0)
fi
for v in SERVICE_USER_PASSWORD SERVICE_GAST_PASSWORD SERVICE_ADMIN_PASSWORD ; do
  value=$(envget $v)
  [ -z "$value" ] && value=$(randomkey 30)
  printf -v "$v" '%s' "$value"
done
# the setup service checks the admin password with the bcrypt hash, the plain password is not stored
adminhash=$(htpasswd -nbBC 10 "" "$password" | tr -d ':\n' | sed 's/^\$2y\$/$2a$/')
[ -n "$adminhash" ] || fail "cannot create the hash of the admin password"
certcreated=$(envget CERT_CREATED)
[ -z "$certcreated" ] && certcreated=false
if [ "$certificate" = own ] ; then certextern=1 ; else certextern=0 ; fi

# common .env of all LeTTo containers - the setup service finds all values and needs no initial configuration
cat >$composeletto/.env <<EOF
SETUP_INITIALIZED=false
ADMIN_PASSWORD=
ADMIN_PASSWORD_ENCRYPTED='$adminhash'
LETTO_UID=$lettouid
RUN_AS_ROOT=true
LETTO_PATH=$lettobase
DOCKER_BASE=$lettobase/docker
SETUP_COMPOSE=$composeletto
LETTO_COMPOSE=$composeletto
PROXY_PATH=$proxydir
HOST_BS=LINUX
PATH_SEPERATOR=/
PORT_SETUP=$setupport
DEBUG=false
STABLE=false
SERVER_NAME=$dnsname
SERVER_INFO=$serverinfo
EMAIL=$email
DOMAIN_ALTERNATIV=
REDIRECT=app
SELF_SIGNED=1
CERT_EXTERN=$certextern
CERT_CREATED=$certcreated
MYSQL_ROOT_PASSWORD=$(envvalue "$mysqlpassword")
MONGO_ROOT_PASSWORD=$(envvalue "$mysqlpassword")
REDIS_PASSWORD=$(envvalue "$mysqlpassword")
MYSQL_LETTO_PASSWORD=$(envvalue "$lettopassword")
MYSQL_LTI_HOST=letto-mysql.nw-letto:3306
MYSQL_LTI_DATABASE=lettolti
MYSQL_LTI_USER=lettolti
MYSQL_LTI_PASSWORD=$(envvalue "$ltipassword")
SERVICE_USER_PASSWORD=$SERVICE_USER_PASSWORD
SERVICE_GAST_PASSWORD=$SERVICE_GAST_PASSWORD
SERVICE_ADMIN_PASSWORD=$SERVICE_ADMIN_PASSWORD
JWT_SECRET=$jwtsecret
SERVER_SECRET=$serversecret
LETTO_LOCAL_PRIVATE_KEY=$privatekey
LETTO_LOCAL_PUBLIC_KEY=$publickey
LETTO_RESTKEY=$(envget LETTO_RESTKEY)
LETTO_LICENSE_SERVER=https://license.letto.at
LETTO_SCHULEN=
EOF

echo set rights for user letto
chmod 755    -R $lettobase/docker
chown letto:letto -R $lettobase/docker
chmod 640    $composeletto/.env
chmod 750    $certdir
if [ -e "$certdir/cert.key" ] ; then chmod 640 "$certdir/cert.key" ; fi

# create docker networks
for nw in nw-letto nw-proxy ; do
  docker network inspect $nw >/dev/null 2>&1 || docker network create $nw >/dev/null || fail "cannot create docker network $nw"
done

# create docker volumes
for vol in certs certs-data lettomysql ; do
  docker volume inspect $vol >/dev/null 2>&1 || docker volume create $vol >/dev/null || fail "cannot create docker volume $vol"
done

# ------------------------------------------ databases ---------------------------------------------------------------
echo
cd $composeletto || fail "cannot change to $composeletto"
startcompose docker-compose-mysql.yml letto-mysql
startcompose docker-compose-mongo.yml letto-mongo
startcompose docker-compose-redis.yml letto-redis
waitfor "MySQL" 300 mysqlready || fail "MySQL is not ready - please check 'docker logs letto-mysql'"
waitfor "Mongo" 120 mongoready || fail "Mongo is not ready - please check 'docker logs letto-mongo'"
waitfor "Redis" 60 redisready  || fail "Redis is not ready - please check 'docker logs letto-redis'"
# the databases must also run after their initialization
sleep 5
mysqlready && mongoready && redisready || fail "the databases do not run stable - please check 'docker logs letto-mysql'"
echo the databases are running

# database users: lettolti for the LTI service, letto for the databases of the schools
echo create database users
docker exec -i -e MYSQL_PWD="$mysqlpassword" letto-mysql mysql -h127.0.0.1 -uroot <<EOF || fail "cannot create the database users"
CREATE USER IF NOT EXISTS 'lettolti'@'%' IDENTIFIED WITH caching_sha2_password BY '$ltipassword';
ALTER USER 'lettolti'@'%' IDENTIFIED WITH caching_sha2_password BY '$ltipassword';
CREATE DATABASE IF NOT EXISTS lettolti;
GRANT ALL PRIVILEGES ON lettolti.* TO 'lettolti'@'%';
CREATE USER IF NOT EXISTS 'letto'@'%' IDENTIFIED WITH caching_sha2_password BY '$lettopassword';
ALTER USER 'letto'@'%' IDENTIFIED WITH caching_sha2_password BY '$lettopassword'
  REQUIRE NONE WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
GRANT USAGE ON *.* TO 'letto'@'%';
GRANT ALL PRIVILEGES ON letto.* TO 'letto'@'%';
GRANT ALL PRIVILEGES ON \`letto_%\`.* TO 'letto'@'%';
FLUSH PRIVILEGES;
EOF

# ------------------------------------------ proxy, reachability and certificate -------------------------------------
echo
startcompose docker-compose-proxy.yml letto-proxy
waitfor "proxy" 60 proxyready || fail "the proxy is not ready - please check 'docker logs letto-proxy'"

reachable=false
if [ "$dnsname" != localhost ] ; then
  echo "check if the server is reachable as http://$dnsname"
  if ! getent hosts "$dnsname" >/dev/null ; then
    warn "the DNS name $dnsname cannot be resolved - please check the DNS entry of the server"
  elif checkreachable ; then
    reachable=true
    echo "the server is reachable as http://$dnsname"
  else
    warn "the server cannot reach itself as http://$dnsname - please check the DNS entry and the firewall,"
    echo "         port 80 and 443 must be open for the server."
  fi
  if [ $reachable = false ] ; then
    if [ "$certificate" = letsencrypt ] ; then
      echo "         Without this the Let's Encrypt certificate cannot be created now, it can be created later in"
      echo "         the setup service with 'CREATE lets encrypt CERTIFICATE'."
    fi
    askcontinue "the server is not reachable as $dnsname"
  fi
fi

if [ "$certificate" = letsencrypt ] && [ $reachable = true ] ; then
  if docker exec letto-proxy test -s "/etc/letsencrypt/live/$dnsname/fullchain.pem" ; then
    echo "the Let's Encrypt certificate for $dnsname already exists"
    setenv CERT_CREATED true
  else
    echo "create Let's Encrypt certificate for $dnsname"
    if docker run -i --rm -v certs:/etc/letsencrypt -v certs-data:/data/letsencrypt certbot/certbot certonly \
         --webroot --webroot-path=/data/letsencrypt --non-interactive --agree-tos -m "$email" -d "$dnsname" ; then
      setenv CERT_CREATED true
      echo "Let's Encrypt certificate created"
    else
      warn "the Let's Encrypt certificate cannot be created - the server uses a self-signed certificate,"
      echo "         please create it later in the setup service with 'CREATE lets encrypt CERTIFICATE'."
      askcontinue "the Let's Encrypt certificate cannot be created"
    fi
  fi
fi

# ------------------------------------------ services ----------------------------------------------------------------
echo
echo "prepare the services: $services"
for s in $services ; do
  download yml/docker-service-$s.yml $composeletto/docker-service-$s.yml
  settag $composeletto/docker-service-$s.yml
done
# configuration files of the proxy, the proxy itself creates https.conf
for c in $proxyconfs ; do
  download proxy/$c.conf $proxydir/$c.conf
done
# directories of the volumes are created for the user letto, otherwise docker creates them for root
for yml in "$composeletto"/docker-*.yml ; do
  docker compose -f "$yml" config 2>/dev/null | sed -n 's/^ *source: \(\/.*\)$/\1/p'
done | sort -u | while read -r dir ; do
  if [ ! -e "$dir" ] && [[ "$dir" == "$lettobase"/* ]] ; then
    mkdir -p "$dir" && chown letto:letto "$dir"
  fi
done
chown letto:letto -R $composeletto $proxydir

# restart the proxy with the configuration of the services and the certificate
echo restart proxy
docker restart letto-proxy >/dev/null || fail "cannot restart letto-proxy"
waitfor "proxy" 60 proxyready || fail "the proxy is not ready after the restart - please check 'docker logs letto-proxy'"
if [ "$dnsname" != localhost ] ; then
  checkhttps
fi

# setup service - it finds a complete configuration
echo
setenv SETUP_INITIALIZED true
startcompose docker-compose-setup.yml letto-setup
waitfor "setup service" 300 setupready || fail "the setup service is not ready - please check 'docker logs letto-setup'"

# all services one after the other, a service is started when the previous one is ready
for s in $services ; do
  echo
  startcompose docker-service-$s.yml letto-$s
  waitfor "letto-$s" 300 serviceready letto-$s || warn "letto-$s is not ready yet - please check 'docker logs letto-$s'"
  # a short break so that not too many containers start at the same time
  sleep 5
done

# ------------------------------------------ check of the installation ----------------------------------------------
echo
echo "check of the installation"
for c in letto-mysql phpmyadmin letto-mongo letto-redis letto-proxy letto-setup $(for s in $services ; do echo letto-$s ; done) ; do
  # services with a long healthcheck interval need some time until they are healthy
  t=0
  while [ "$(containerstate $c)" = starting ] && [ $t -lt 180 ] ; do
    sleep 5
    t=$((t+5))
  done
  state=$(containerstate $c)
  case "$state" in
    healthy|running) printf "  %-24s %s\n" "$c" "$state" ;;
    *)               printf "  %-24s %s\n" "$c" "$state - please check 'docker logs $c'" ; problems=$((problems+1)) ;;
  esac
done

if [ "$certificate" = letsencrypt ] && [ "$(envget CERT_CREATED)" = true ] || [ "$certificate" = own ] ; then
  curlopt=-fsS
else
  curlopt=-fsSk
fi
for url in "https://$dnsname/config/ping" "https://$dnsname/login/" "https://$dnsname/app/" ; do
  if [ "$dnsname" = localhost ] ; then url=${url/localhost/127.0.0.1} ; fi
  if curl $curlopt --max-time 15 -o /dev/null "$url" 2>/dev/null ; then
    printf "  %-40s ok\n" "$url"
  else
    printf "  %-40s not reachable\n" "$url"
    problems=$((problems+1))
  fi
done

echo finished >"$statefile"

# detect ip address
myip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<NF;i++) if ($i=="src") {print $(i+1); exit}}')

echo
if [ $problems -eq 0 ] ; then
  echo "LeTTo is installed and running."
else
  echo "LeTTo is installed, but there were $problems problems - please check the warnings above."
fi
echo
echo "Databases: $mysqlimage, $mongoimage, $redisimage"
echo
echo "Connect to the server:"
echo "  LeTTo           https://$dnsname/"
echo "  setup service   https://$dnsname/config  - login as 'admin' with the admin password"
echo "                  https://${myip:-<server>}:$setupport/config  - only in the local network, do not open port $setupport to the internet!"
echo
echo "Next steps in the setup service: enter the license key of the server and create the school."
echo
echo finished!
