#!/usr/bin/bash
#
# Discovery Installer
#

if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
  echo "This script is intended to be run, not sourced." >&2
  return 1
fi

set -e

if [ "$(id -u)" -eq 0 ]; then
  echo "$0 must not run as root." 1>&2
  exit 1
fi

usage() {
  echo "Usage: $0 [-v|--verbose] [--override-conf-dir value] <command>"
  echo "Options:"
  echo "  -v, --verbose                 Enable verbose output mode"
  echo "  --override-conf-dir value     Path of a directory containing config overrides"
  echo "Commands:"
  echo "  install                       Install Discovery"
  echo "  upgrade                       Upgrade Discovery"
  echo "  uninstall                     Uninstall Discovery"
  echo "  create-server-password        Create a Discovery server password"
  echo "  create-app-secret             Create a Discovery application secret"
  echo "  create-db-password            Create a DB password"
  echo "  create-redis-password         Create a Redis password"
  echo "  check                         Check Discovery setup and configurations"
  exit 1
}

check_prereqs() {
  local requirements="awk base64 basename cp diff echo grep head mkdir podman tr read rm sed sort systemctl"
  for requirement in $requirements; do
    if ! command -v "$requirement" >/dev/null 2>&1; then
      echo "Error: $requirement is not installed or is not in the PATH." >&2
      exit 1
    fi
  done

  if [[ "$(podman info --format "{{.Host.CgroupsVersion}}")" != "v2" ]]; then
    cat <<EOFDOC >&2
Error: system is not configured to use cgroups v2.

To enable cgroups v2 (a.k.a. cgroup2fs), you may need to
update your kernel arguments and reboot. Consider running
these commands before trying to use discovery-installer:

$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"
$ sudo reboot

EOFDOC
    exit 1
  fi
}

set_default_vars() {
  VERBOSE="n"
  OVERRIDE_CONF_DIR="" # Optional

  INSTALLER_BIN_DIR=$(dirname "$(realpath "$0")")
  if [[ $INSTALLER_BIN_DIR == "/usr/bin" ]]; then
    # bin and base dirs are different when installed by RPM...
    INSTALLER_BASE_DIR="/usr/share/discovery-installer"
  else
    # ...versus when running from source.
    INSTALLER_BASE_DIR=$(dirname "$INSTALLER_BIN_DIR")
  fi
  INSTALLER_BIN_DIR="${INSTALLER_BASE_DIR}/bin"
  INSTALLER_CONFIG_DIR="${INSTALLER_BASE_DIR}/config"
  INSTALLER_ENV_DIR="${INSTALLER_BASE_DIR}/env"

  # All external programs discovery-installer needs are in these bin paths.
  # Do not allow a user's custom PATH environment to interfere.
  export PATH="${INSTALLER_BIN_DIR}:/usr/bin:/bin:/usr/sbin:/sbin"

  QUIPUCORDS_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/discovery"
  QUIPUCORDS_ENV_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/discovery/env"
  QUIPUCORDS_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/discovery"
  SYSTEMD_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/containers/systemd"

  if [ -z "${XDG_RUNTIME_DIR}" ]; then
    export XDG_RUNTIME_DIR=/run/user/$UID
  fi
}

main() {
  set_default_vars

  while [[ $# -gt 0 ]]; do
    case "$1" in
    -v | --verbose)
      VERBOSE="y"
      shift
      ;;
    --override-conf-dir)
      if [[ -n $2 && $2 != -* ]]; then
        OVERRIDE_CONF_DIR=$2
        shift 2
      else
        echo "Error: --override-conf-dir requires a value." 1>&2
        usage
      fi
      if [ ! -d "${OVERRIDE_CONF_DIR}" ]; then
        echo "Override configuration directory '${OVERRIDE_CONF_DIR}' does not exist." 1>&2
        usage
      fi
      ;;
    -h | --help)
      usage
      ;;
    -*)
      echo "Error: Invalid option '$1'" 1>&2
      usage
      ;;
    *)
      break
      ;;
    esac
  done

  if [[ $# -ne 1 ]]; then
    echo "Error: Exactly one command is required." 1>&2
    usage
  fi

  COMMAND="$1"
  shift

  case "$COMMAND" in
  install)
    install
    cat <<EOFDOC
Install complete.

You may now start Discovery by running the following command:

    systemctl --user start discovery-app

EOFDOC
    exit 0
    ;;
  upgrade)
    upgrade
    upgrade_status=$?
    if [ $upgrade_status -eq 0 ]; then
      cat <<EOFDOC
Upgrade complete.

You may now start Discovery by running the following command:

    systemctl --user start discovery-app

EOFDOC
    fi
    exit $upgrade_status
    ;;
  uninstall)
    uninstall
    ;;
  check)
    check
    exit $?
    ;;
  create-server-password)
    create-server-password
    exit $?
    ;;
  create-app-secret)
    create-app-secret
    exit $?
    ;;
  create-db-password)
    create-db-password
    exit $?
    ;;
  create-redis-password)
    create-redis-password
    exit $?
    ;;
  *)
    echo "Error: Invalid command '$COMMAND'" 1>&2
    usage
    ;;
  esac
}

debug_msg() {
  if [ "${VERBOSE}" == "y" ]; then
    echo "$@"
  fi
}

get_section() {
  # get_section expects two arguments:
  # 1) file path
  # 2) config section name

  config_file="${1}"
  section_name="${2}"
  <"${config_file}" sed "0,/^\[${section_name}\]/d" | sed '/^[\b]*$/,$d'
}

override_unit_section() {
  # override_unit_section expects two arguments:
  # 1) string containing config lines
  # 2) string containing lines to override

  export config_section="${1}"
  export override_section="${2}"
  if [ -z "${override_section}" ]; then
    echo "${config_section}"
    return
  fi
  updated_config="${config_section}"
  updated_config=$(
    echo "${override_section}" | cut -f1 -d= | sort -u |
      (
        while read -r var_name; do
          NL=$'\n'
          # number of var_name entries in the config section we want to update
          num_var_names=$(echo "${updated_config}" | grep -c "^${var_name}=" >/dev/null)
          var_lines=$(
            echo "${override_section}" | grep "^${var_name}=.*" |
              (
                var_lines=""
                while read -r line; do
                  if [ -z "${var_lines}" ]; then
                    var_lines="${line}"
                  else
                    var_lines="${var_lines}${NL}${line}"
                  fi
                done
                echo "${var_lines}"
              )
          )
          if [ "${num_var_names}" == "0" ]; then # just append the overrides at the end
            for line in ${var_lines}; do
              updated_config="${updated_config}${NL}${line}"
            done
          else # replace block
            # prefix is the config section that precedes the var_name entries
            prefix=$(echo "${updated_config}" | sed "/^${var_name}=.*/,$ d")
            # suffix is the config section that succeeds the var_name entries
            suffix=$(echo "${updated_config}" | sed "1,/^${var_name}=.*/d")
            suffix=$(echo "${suffix}" | sed "/^${var_name}=.*/d")
            updated_config="${prefix}"
            for line in ${var_lines}; do
              updated_config="${updated_config}${NL}${line}"
            done
            updated_config="${updated_config}${NL}${suffix}"
          fi
        done
        echo "${updated_config}"
      )
  )
  echo "${updated_config}"
}

copy_container() {
  # copy_container expects two arguments:
  # 1) file path
  # 2) target directory path

  container_config="${1}"
  target_dir="${2}"
  container_file=$(basename "${container_config}")
  debug_msg "Copying ${container_config} ${target_dir} ..."
  if [ -n "${OVERRIDE_CONF_DIR}" ] && [ -s "${OVERRIDE_CONF_DIR}/${container_file}" ]; then
    debug_msg "Overriding ${container_file}:"
    container_override="${OVERRIDE_CONF_DIR}/${container_file}"

    config_unit=$(get_section "${container_config}" "Unit")
    override_unit=$(get_section "${container_override}" "Unit")
    updated_unit=$(override_unit_section "${config_unit}" "${override_unit}")

    config_container=$(get_section "${container_config}" "Container")
    override_container=$(get_section "${container_override}" "Container")
    updated_container=$(override_unit_section "${config_container}" "${override_container}")

    config_service=$(get_section "${container_config}" "Service")
    override_service=$(get_section "${container_override}" "Service")
    updated_service=$(override_unit_section "${config_service}" "${override_service}")

    config_install=$(get_section "${container_config}" "Install")
    override_install=$(get_section "${container_override}" "Install")
    updated_install=$(override_unit_section "${config_install}" "${override_install}")

    NL=$'\n'
    updated_config="[Unit]${NL}${updated_unit}${NL}"
    updated_config="${updated_config}${NL}[Container]${NL}${updated_container}${NL}"
    updated_config="${updated_config}${NL}[Service]${NL}${updated_service}${NL}"
    updated_config="${updated_config}${NL}[Install]${NL}${updated_install}"
    echo "${updated_config}" >"${target_dir}/${container_file}"
    if [ "${VERBOSE}" = "y" ]; then
      diff --suppress-common-line -y "${container_config}" "${target_dir}/${container_file}" || true
      echo
    fi
  else
    cp "${container_config}" "${target_dir}/${container_file}"
  fi
}

copy_env() {
  # copy_env expects two arguments:
  # 1) file path
  # 2) target directory path

  env_config="${1}"
  target_dir="${2}"
  env_file=$(basename "${env_config}")
  env_unit="$(cat "${env_config}")"
  debug_msg "Copying ${env_config} ${target_dir} ..."
  if [ -n "${OVERRIDE_CONF_DIR}" ] && [ -s "${OVERRIDE_CONF_DIR}/${env_file}" ]; then
    debug_msg "Overriding ${env_file}:"
    while IFS=$'\n' read -r line; do
      env_var=${line%=*}
      env_val=${line#*=}
      if echo "${env_unit}" | grep "^${env_var}=" >/dev/null; then
        # shellcheck disable=SC2001
        env_unit="$(echo "${env_unit}" | sed -e "s/^${env_var}=.\+/${env_var}=${env_val}/")"
      else
        NL=$'\n'
        env_unit="${env_unit}${NL}${env_var}=${env_val}"
      fi
    done <"${OVERRIDE_CONF_DIR}/${env_file}"
    echo "${env_unit}" >"${target_dir}/${env_file}"

    if [ "${VERBOSE}" = "y" ]; then
      diff --suppress-common-line -y "${env_config}" "${target_dir}/${env_file}" || true
      echo
    fi
  else
    echo "${env_unit}" >"${target_dir}/${env_file}"
  fi
}

install() {
  if ! podman secret exists discovery-server-password; then
    create-server-password || exit 1
  fi

  if ! podman secret exists discovery-django-secret-key; then
    create-app-secret -y || exit 1
  fi

  if ! podman secret exists discovery-db-password; then
    create-db-password -y || exit 1
  fi

  if ! podman secret exists discovery-redis-password; then
    create-redis-password -y || exit 1
  fi

  mkdir -p "${QUIPUCORDS_DATA_DIR}/data"
  mkdir -p "${QUIPUCORDS_DATA_DIR}/db"
  mkdir -p "${QUIPUCORDS_DATA_DIR}/log"
  mkdir -p "${QUIPUCORDS_DATA_DIR}/sshkeys"
  mkdir -p "${QUIPUCORDS_DATA_DIR}/certs"

  systemctl --user reset-failed

  echo "Installing Discovery configuration files ..."
  mkdir -p "${QUIPUCORDS_ENV_DIR}"
  mkdir -p "${SYSTEMD_CONFIG_DIR}"

  cp "${INSTALLER_CONFIG_DIR}"/*.network "${SYSTEMD_CONFIG_DIR}"
  for container_file in "${INSTALLER_CONFIG_DIR}"/*.container; do
    copy_container "${container_file}" "${SYSTEMD_CONFIG_DIR}"
  done
  for env_file in "${INSTALLER_ENV_DIR}"/*.env; do
    copy_env "${env_file}" "${QUIPUCORDS_ENV_DIR}"
  done

  echo "Generate the Discovery services ..."
  systemctl --user daemon-reload
}

pull_latest_images() {
  local exit_status=0
  for config_container in "${INSTALLER_CONFIG_DIR}"/*.container; do
    local container_basename
    container_basename=$(basename "$config_container")
    local systemd_container="${SYSTEMD_CONFIG_DIR}/${container_basename}"
    local image
    image=$(grep -o '^Image=.*' "$systemd_container" | tail -n 1 | sed 's/^Image=//')
    echo "Pulling the latest version of '$image'"
    if ! podman pull -q "$image" &>/dev/null; then
      echo "Warning: Could not pull '$image'." >&2
      exit_status=1
    fi
  done
  if [ $exit_status -ne 0 ]; then
    echo "At least one image failed to pull. Check network connectivity and try again, or manually import the images before starting Discovery. If you are installing in a disconnected environment and you have already pulled the latest images, you may ignore these warnings." >&2
  fi
  return $exit_status
}

remove_container_images() {
  echo "Removing Discovery container images ..."
  local exit_status=0
  local unique_images
  unique_images=$(
    grep -h '^Image=.*' "${INSTALLER_CONFIG_DIR}"/*.container |
      awk -F= '!seen[$2]++' |
      sed 's/^Image=//g' |
      sort -u
  )
  if [[ -z "$unique_images" || "$unique_images" =~ ^[[:space:]]*$ ]]; then
    # no images found, nothing to do
    return
  fi

  while IFS= read -r image; do
    echo "Removing container image '$image'"
    if ! podman rmi "$image" &>/dev/null; then
      echo "Error: Could not remove '$image'." >&2
      exit_status=1
    fi
  done <<<"$unique_images"
  if [ $exit_status -ne 0 ]; then
    echo "At least one image failed to be removed." >&2
  fi
}

upgrade() {
  echo "Upgrading Discovery ..."
  stop_containers
  install
  pull_latest_images
  return $?
}

check_directory_status() {
  [[ ! -d "${1}" ]] && return 3                  # Missing
  [[ ! -O "${1}" ]] && return 2                  # Not owned by you
  [[ -r "${1}" ]] && [[ -w "${1}" ]] && return 0 # OK
  return 1                                       # Bad permissions
}

check_file_status() {
  [[ ! -f "${1}" ]] && return 3                  # Missing
  [[ ! -O "${1}" ]] && return 2                  # Not owned by you
  [[ -r "${1}" ]] && [[ -w "${1}" ]] && return 0 # OK
  return 1                                       # Bad permissions
}

print_path_status() {
  local owner perms perms_owner
  if [ "${1}" -eq 0 ]; then
    perms_owner=$(stat -c '%A %U' "${2}")
    echo "${2}: OK: ${perms_owner}"
  elif [ "${1}" -eq 1 ]; then
    perms=$(stat -c '%a %A' "${2}")
    echo "${2}: ERROR: Incorrect permission(s): ${perms}"
  elif [ "${1}" -eq 2 ]; then
    owner=$(stat -c '%u %U' "${2}")
    echo "${2}: ERROR: Not owned by you (incorrect ownership): ${owner}"
  elif [ "${1}" -eq 3 ]; then
    echo "${2}: ERROR: Missing"
  else
    echo "${2}: ERROR: Unknown status"
  fi
}

check_directory_and_print_status() {
  check_directory_status "${1}"
  local exit_status=$?
  print_path_status ${exit_status} "${1}"
  return $exit_status
}

check_file_and_print_status() {
  check_file_status "${1}"
  local exit_status=$?
  print_path_status ${exit_status} "${1}"
  return $exit_status
}

check() {
  echo "Checking Discovery setup and configurations..."
  local result=0
  check_directory_and_print_status "${QUIPUCORDS_DATA_DIR}" || result=$((result + 1))
  for name in certs data db log sshkeys; do
    local subdirectory="${QUIPUCORDS_DATA_DIR}/$name"
    check_directory_and_print_status "$subdirectory" || result=$((result + 1))
  done
  check_file_and_print_status "${QUIPUCORDS_DATA_DIR}/certs/server.key" || result=$((result + 1))
  check_file_and_print_status "${QUIPUCORDS_DATA_DIR}/certs/server.crt" || result=$((result + 1))
  check_file_and_print_status "${QUIPUCORDS_DATA_DIR}/data/secret.txt" || result=$((result + 1))
  check_directory_and_print_status "${QUIPUCORDS_DATA_DIR}/db/userdata" || result=$((result + 1))
  check_directory_and_print_status "${QUIPUCORDS_CONFIG_DIR}" || result=$((result + 1))
  check_directory_and_print_status "${QUIPUCORDS_ENV_DIR}" || result=$((result + 1))
  for name in ansible app celery-worker db redis server; do
    local env_file="$QUIPUCORDS_ENV_DIR/env-${name}.env"
    check_file_and_print_status "${env_file}" || result=$((result + 1))
  done
  check_directory_and_print_status "${SYSTEMD_CONFIG_DIR}" || result=$((result + 1))
  for name in app celery-worker db redis server; do
    local container_file="${SYSTEMD_CONFIG_DIR}/discovery-${name}.container"
    check_file_and_print_status "${container_file}" || result=$((result + 1))
  done
  local network_file="${SYSTEMD_CONFIG_DIR}/discovery.network"
  check_file_and_print_status "${network_file}" || result=$((result + 1))
  return $result
}

stop_containers() {
  echo "Stopping Discovery ..."
  systemctl --user stop discovery-app || true
  systemctl --user stop discovery-network || true
}

uninstall() {
  stop_containers
  remove_container_images

  echo "Removing Discovery Services ..."
  rm -f "${XDG_RUNTIME_DIR}"/systemd/generator/discovery-*.service
  rm -f "${SYSTEMD_CONFIG_DIR}"/discovery*.network
  rm -f "${SYSTEMD_CONFIG_DIR}"/discovery*.container
  rm -f "${QUIPUCORDS_ENV_DIR}"/*.env

  systemctl --user daemon-reload
  systemctl --user reset-failed

  echo "Removing Discovery Data ..."
  rm -rf "${QUIPUCORDS_DATA_DIR}/data"
  rm -rf "${QUIPUCORDS_DATA_DIR}/log"
  rm -rf "${QUIPUCORDS_DATA_DIR}/sshkeys"
  rm -rf "${QUIPUCORDS_DATA_DIR}/certs"
  podman secret rm discovery-server-password &>/dev/null
  podman secret rm discovery-django-secret-key &>/dev/null
  podman secret rm discovery-db-password &>/dev/null
  podman secret rm discovery-redis-password &>/dev/null

  echo "Discovery Uninstalled."
  exit 0
}

check_prereqs
set_default_vars
main "$@"
