#!/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"
  exit 1
}

check_prereqs() {
  local requirements="base64 basename cp diff echo grep head mkdir podman tr read rm sed 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
}

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_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
    echo "Discovery Installed."
    exit 0
    ;;
  upgrade)
    upgrade
    upgrade_status=$?
    if [ $upgrade_status -eq 0 ]; then
      echo "Upgrade complete. You may now start Discovery."
    fi
    exit $upgrade_status
    ;;
  uninstall)
    uninstall
    ;;
  create-server-password)
    create-server-password
    exit $?
    ;;
  create-app-secret)
    create-app-secret
    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
        env_unit="${env_unit//${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

  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 "Error: 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." >&2
  fi
  return $exit_status
}

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

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

uninstall() {
  stop_containers

  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

  echo "Discovery Uninstalled."
  exit 0
}

check_prereqs
set_default_vars
main "$@"
