#!/bin/bash
# Purpose: detect modified files in config tree and execute
#          any defined service modifications
################################################################################

set -e
set -E
set -u

# support testsuite

FUNCTIONS="${FUNCTIONS:-/usr/share/ngcp-ngcpcfg/functions/}"
OUTPUT_DIRECTORY="${OUTPUT_DIRECTORY:-}"

CLEANUP_FILES=()
INSTANCES=()

# load modules

if [[ ! -r "${FUNCTIONS}"/main ]]; then
  printf "Error: %s/main could not be read. Exiting.\n" "${FUNCTIONS}">&2
  exit 1
fi

# shellcheck source=./functions/main
. "${FUNCTIONS}"/main

# functions

instances_info() {
  local target
  local dest
  local instance
  local RUNNER="nice -n 19 ionice -c 3"

  log_debug "${RUNNER} ${HELPER}/instances-info"
  while IFS=":" read -r -a info; do
    target=${info[0]}
    dest=${info[1]}
    instance=${info[2]}
    INSTANCES+=("${target}:${instance}:${dest}")
  done< <(${RUNNER} "${HELPER}/instances-info")
}

exec_wrapper() {
  local service_file=$1
  local instance_name="${2:-}"
  local msg=""
  if [ -n "${instance_name}" ]; then
    msg="[${instance_name}]"
  fi

  if ${DRYRUN} ; then
    log_info "TEST MODE: Would execute action for ${service_file}${msg}"
    return 0
  fi
  log_info "Executing action for ${service_file}${msg}"
  if [[ -x "${service_file}" ]]; then
    log_debug "${service_file}${msg}"
    if ! INSTANCE_NAME="${instance_name}" "${service_file}" ; then
      log_warn "INSTANCE_NAME=${instance_name} ${service_file} returned with error code, continuing anyway."
    fi
  elif [[ -r "${service_file}" ]]; then
    log_debug "INSTANCE_NAME=${instance_name} bash ${service_file}"
    if ! INSTANCE_NAME="${instance_name}" bash "${service_file}" ; then
      log_warn "INSTANCE_NAME=${instance_name} ${service_file} returned with error code, continuing anyway."
    fi
  else
    log_error "Error: ${service_file} could not be read."
    exit 1
  fi
}

# get rid of "./" and "//" in file names
normalize_files() {
  NORMALIZED_FILES="$(mktemp -t ngcpcfg-services-norm.XXXXXXXXXX)"
  log_debug "NORMALIZED_FILES = ${NORMALIZED_FILES}"

  CLEANUP_FILES+=("${NORMALIZED_FILES}")

  while read -r line ; do
    # shellcheck disable=SC2001
    echo "${line}" | sed -e 's_\./_/_g ; s_//_/_g' >> "${NORMALIZED_FILES}"
  done < "${TMPFILE}"
}

# restart sysctl services before the rest (see TT#58703)
# restart monit services before the rest (see MT#9971)
# restart HA just after monit (see MT#17163)
# restart other services in alphabetical order
sort_service_list() {
  SORTED_LIST="$(mktemp -t ngcpcfg-services-sorted.XXXXXXXXXX)"
  log_debug "SORTED_LIST = ${SORTED_LIST}"

  CLEANUP_FILES+=("${SORTED_LIST}")

  for dir in sysctl.d monit corosync pacemaker ha.d; do
    grep "${SERVICES_POOL_BASE}"/etc/${dir}/'.*services' \
      "${NORMALIZED_FILES}" >> "${SORTED_LIST}" || true
  done

  # Sort loads the entire contents before outputting, and we are appending
  # instead of truncating, so this should be safe.
  # shellcheck disable=SC2094
  sort "${NORMALIZED_FILES}" "${SORTED_LIST}" | uniq -u \
    >> "${SORTED_LIST}" || true
}

services_start_queue()
{
  log_info "Clearing out enqueued services actions"
  ngcp-service queue-clear
  log_info "Starting enqueued services actions mode"
  ngcp-service queue-start
}

execute() {
  local line
  local info

  while read -r line ; do
    while IFS=':' read -ra info; do
      exec_wrapper "${info[0]}" "${info[1]:-}"
    done <<< "${line}"
  done < "${SORTED_LIST}"
}

services_flush_enqueued()
{
  log_info "Executing enqueued services actions"
  ngcp-service --queue=default,policy-rc.d queue-show
  ngcp-service --queue=default,policy-rc.d queue-run
}

services_sync_state() {
  log_info "Synchronizing current with expected services state"
  ngcp-service sync-state
}

systemd_daemon_reload_preset() {
  if ${DRYRUN}; then
    log_debug "TEST MODE: not touching systemd"
    return
  fi

  if [ "${NGCP_TESTSUITE:-false}" = "true" ]; then
    log_debug "NGCP_TESTSUITE is set, not touching systemd"
    return
  fi

  if [[ -d "/run/systemd/system" ]]; then
    log_info "Reloading systemd daemon and preset all services"

    log_debug "systemd needs daemon-reload so the unit files loaded are in sync"
    log_debug "Running: systemctl daemon-reload 2>&1 || true"
    systemctl daemon-reload 2>&1 || true

    log_debug "Removing any broken systemd service symlink"
    find -L /etc/systemd/system -type l -print0 \
      | xargs -0 -r rm

    log_debug "systemd needs preset-all to enable/disable services (to start them on boot)"
    log_debug "Running: rm -rf /etc/systemd/system/*.wants/ || true"
    rm -rf /etc/systemd/system/*.wants/ || true
    log_debug "Running: systemctl preset-all 2>&1 || true"
    systemctl preset-all 2>&1 || true
  fi
}

is_absolute_path() {
  local dir="$1"

  if [[ ! "${dir}" =~ ^/ ]]; then
    log_error "${dir} is not an absolute path"
    return 1
  fi

  return 0
}

is_non_git_folder() {
  local dir="$1"

  if [[ ! -d "${dir}/.git" ]]; then
    log_info "${dir} has no support of .services"
    return 1
  fi

  return 0
}

generate_list_to_process() {
  local dir="$1"

  if ${FORCE_ALL_SERVICES} ; then
    log_debug "calling function find_all_services()"
    find_all_services "${dir}"
  else
    log_debug "calling function find_all_changed_services()"
    find_all_changed_services "${dir}"
  fi
}

find_instance() {
  local service_file
  local info
  local target

  # get rid of "./" and "//"
  # shellcheck disable=SC2001
  service_file=$(echo "$1" | sed -e 's_\./_/_g ; s_//_/_g')

  for instance in "${INSTANCES[@]}"; do
    IFS=":" read -ra info <<< "${instance}"
    # get rid of "./" and "//"
    target=$(echo "${TEMPLATE_POOL_BASE}${info[0]}" | sed -e 's_\./_/_g ; s_//_/_g')
    if [[ "${service_file}" =~ ^${target} ]] ; then
      log_debug "[${info[1]}] Storing ${file} in '${TMPFILE}'"
      echo "${file}:${info[1]}" >> "${TMPFILE}"
    fi
  done
}

find_all_services() {
  local dir="$1"

  while read -r file ; do
    if [[ ! -r "${file}" ]]; then
      log_warn "Cannot read file '${file}'"
    fi
    log_debug "Storing ${file} in '${TMPFILE}'"
    echo "${file}" >> "${TMPFILE}"
    find_instance "${file}"
  done < <(find "${SERVICES_POOL_BASE}/${dir}" -name '*.services' | sort -u)
}

find_changed_instance_services() {
  local file
  local dir
  local info
  local dest
  local target
  local target_file

  # get rid of "./" and "//"
  # shellcheck disable=SC2001
  file=$(echo "$1"| sed -e 's_\./_/_g ; s_//_/_g')
  # shellcheck disable=SC2001
  dir=$(echo "$2"| sed -e 's_\./_/_g ; s_//_/_g')

  for instance in "${INSTANCES[@]}"; do
    IFS=":" read -ra info <<< "${instance}"
    target=${info[0]#${dir}/}
    dest=${info[2]#${dir}/}
    if [[ "${file}" =~ ^${dest} ]] ; then
      target_file=${file/${dest}/${target}}
      if [[ -r "${SERVICES_POOL_BASE}/${dir}/${target_file}".services ]]; then
        log_debug "[${info[1]}] Storing ${SERVICES_POOL_BASE}/${dir}/${target_file}.services in '${TMPFILE}'"
        echo "${SERVICES_POOL_BASE}/${dir}/${target_file}.services:${info[1]}"
      elif [[ -r "${SERVICES_POOL_BASE}/${dir}/$(dirname "${target_file}")"/ngcpcfg.services ]]; then
        log_debug "[${info[1]}] Storing ${SERVICES_POOL_BASE}/${dir}/$(dirname "${target_file}")/ngcpcfg.services in '${TMPFILE}'"
        echo "${SERVICES_POOL_BASE}/${dir}/$(dirname "${target_file}")/ngcpcfg.services:${info[1]}"
      fi
    fi
  done
}

find_all_changed_services() {
  local dir="$1"

  log_debug "${FUNCNAME[0]}(): Working in ${OUTPUT_DIRECTORY}${dir}"

  if ! [ -d "${OUTPUT_DIRECTORY}${dir}" ]; then
    log_debug "${OUTPUT_DIRECTORY}${dir} doesn't exist, skip"
    return
  fi

  pushd "${OUTPUT_DIRECTORY}${dir}" >/dev/null

  local changed_files=()
  if ! ${DRYRUN}; then
    mapfile -t changed_files <<< "$(git status -uall --porcelain | sed 's/^...//')"
  else
    local all_files=()
    mapfile -t all_files <<< "$(find . -type f)"

    for file in "${all_files[@]}"; do
      if ! [ -f "${DRYRUN_DIRECTORY}${dir}/${file}" ] || ! diff -q "${file}" "${DRYRUN_DIRECTORY}${dir}/${file}"  >/dev/null; then
        changed_files+=("${file}")
      fi
    done
  fi

  for file in "${changed_files[@]}"; do
    if ! [[ -r "${file}" ]]; then
      continue
    elif [[ -r "${SERVICES_POOL_BASE}/${dir}/${file}".services ]]; then
      log_debug "Storing ${SERVICES_POOL_BASE}/${dir}/${file}.services in '${TMPFILE}'"
      echo "${SERVICES_POOL_BASE}/${dir}/${file}".services
    elif [[ -r "${SERVICES_POOL_BASE}/${dir}/$(dirname "${file}")"/ngcpcfg.services ]]; then
      log_debug "Storing ${SERVICES_POOL_BASE}/${dir}/$(dirname "$file")/ngcpcfg.services in '${TMPFILE}'"
      echo "${SERVICES_POOL_BASE}/${dir}/$(dirname "$file")/ngcpcfg.services"
    else
      find_changed_instance_services "${file}" "${dir}"
    fi
  done | sort -u >> "${TMPFILE}"

  popd >/dev/null
}

# main script

RUNNING_FILE="${RUN_DIR}/ngcpcfg-services.running"
DRYRUN='false'
FORCE_ALL_SERVICES='false'

while [ -n "${1:-}" ]; do
  case "$1" in
    test|--dry-run)
      DRYRUN='true'
      shift
      ;;
    --force-all-services)
      FORCE_ALL_SERVICES='true'
      shift
      ;;
    *)
      break
      ;;
  esac
done

log_debug "DRYRUN = ${DRYRUN}"
log_debug "FORCE_ALL_SERVICES = ${FORCE_ALL_SERVICES}"

if ! ${DRYRUN}; then
  log_debug "creating current execution filesystem trail"
  trap 'rm -f "${RUNNING_FILE}"' EXIT ERR
  echo $$ >"${RUNNING_FILE}"
fi

log_debug "systemd_daemon_reload_preset function"
systemd_daemon_reload_preset

TMPFILE="$(mktemp -t ngcpcfg-services-tmp.XXXXXXXXXX)"
log_debug "TMPFILE = ${TMPFILE}"
log_debug "OUTPUT_DIRECTORY=${OUTPUT_DIRECTORY}"

CLEANUP_FILES+=("${TMPFILE}")


if [ -n "${TEMPLATE_INSTANCES:-}" ] && [ -f "${TEMPLATE_INSTANCES}" ] ; then
  instances_info
fi

for dir in ${CONFIG_POOL} ; do
  is_absolute_path "${OUTPUT_DIRECTORY}${dir}" || continue
  if ! ${DRYRUN}; then
    is_non_git_folder "${OUTPUT_DIRECTORY}${dir}" || continue
  else
    is_non_git_folder "${DRYRUN_DIRECTORY}${dir}" || continue
  fi

  generate_list_to_process "${dir}"
done

if [[ -s "${TMPFILE}" ]]; then
  log_debug "normalize_files function"
  normalize_files

  log_debug "sort_service_list function"
  sort_service_list

  if ! ${DRYRUN}; then
    services_start_queue
  fi

  log_debug "execute function"
  execute
else
  log_debug "No services file(s) reported - no explicit service state changed."
fi

if ! ${DRYRUN}; then
  # Always flush the queues, to handle actions from package upgrades.
  services_flush_enqueued

  log_debug "services_sync_state function"
  services_sync_state
fi

if [[ -n "${DEBUG:-}" ]]; then
  log_debug "Not removing temporary files"
else
  rm -f "${CLEANUP_FILES[@]}"
fi
## END OF FILE #################################################################
