#!/bin/bash

set -e
set -u

PROGRAM="$(basename "$0")"
export NGCP_UPGRADE_NAME="${PROGRAM}"
OPTIONS=("$@")

NGCP_UPDATE_SCENARIO_BASE_DIR_DEFAULT="/usr/share/ngcp-upgrade/scenarios/"

PARSED_OPTIONS=0

# Whether to disable step to "check maintenance", we're not supposed to have it
# enabled at the point of running this program, so ignoring step by default
DISABLE_MAINTENANCE_CHECK="${DISABLE_MAINTENANCE_CHECK:-true}"

# Whether to disable step to "check apt sources.list", in the case that we do
# not want to test for reachability of repos
DISABLE_APT_CHECK="${DISABLE_APT_CHECK:-false}"

DISABLED_CHECKS=()

error() {
  echo -e "ERROR: $*" >&2
}

debug() {
  if [[ "${DEBUG:-}" =~ ^('yes'|'true')$ ]] ; then
    echo -e "DEBUG: $*" >&2
  fi
}

die() {
  error "$*"
  exit 1
}

usage() {
  echo "Usage: ${PROGRAM} [OPTIONS] TARGET_VERSION"
  echo "  ex.: ${PROGRAM} mr7.5.2"
}

help() {
  echo "${PROGRAM} - Sipwise NGCP platform upgrade framework - pre-checks"
  echo
  echo "Synopsis:"
  echo "  ${PROGRAM} [OPTION]... TARGET_VERSION"
  echo
  echo "Options:"
  echo "  -h|--help                     - display help message and exit"
  echo "  -d|--debug                    - print debugging output"
  echo
  echo "  --disable-apt-check           - disable apt check (reachable repos),"
  echo "                                  enabled by default"
  echo "  --enable-maintenance-check    - enable maintenance check (config)",
  echo "                                  disabled by default"
}

get_options() {
  local cmdline_opts="help,debug,disable-apt-check,enable-maintenance-check"
  local opts
  opts=$(getopt --name "${PROGRAM}" -o +h --long "${cmdline_opts}" -- "${OPTIONS[@]}")

  eval set -- "${opts}"

  while :; do
    case "${1:-}" in
      --help|-h)
        help
        exit 0
        ;;
      --debug|-d)
        DEBUG=true
        PARSED_OPTIONS=$((PARSED_OPTIONS + 1))
        ;;
      --disable-apt-check)
        DISABLE_APT_CHECK="true"
        PARSED_OPTIONS=$((PARSED_OPTIONS + 1))
        ;;
      --enable-maintenance-check)
        DISABLE_MAINTENANCE_CHECK="false"
        PARSED_OPTIONS=$((PARSED_OPTIONS + 1))
        ;;
      *)
        break
        ;;
    esac

    shift
  done
}

# Get time, using simple measure because it's enough for our purposes (tracing
# time spent)
get_time() {
  cut -d' ' -f 1 /proc/uptime
}

# Get timestamp ready to print
get_timestamp() {
  local time_format="+%F %T"
  local timestamp
  timestamp="$(date "${time_format}")"

  echo "${timestamp}"
}

# Calculate elapsed time, using awk to be able to process floating point numbers
get_elapsed() {
  local start="$1"
  local end="$2"

  awk "BEGIN { print ${end} - ${start} }"
}

check_valid_version_or_abort() {
  local v="$1"

  if [[ ${v} =~ ^trunk$ ]] || [[ ${v} =~ ^mr[[:digit:]]+\.[[:digit:]]+(\.[[:digit:]]+)?$ ]] ; then
    debug "Version check: valid: ${v}"
  else
    die "Version check: invalid: ${v}"
  fi
}

# Get counter of steps, ready to print
#
# Note: Sourcing the scenario file to run the steps makes them to run in a kind
# of subshell so variables are not modified in the top-level shell, and there's
# no straightforward way to communicate which steps are run by modifying
# variables.
#
# So one method to keep track of it is using a file, however inefficient /
# inelegant...
get_upgrade_steps_counter() {
  # depends on global variable
  [[ -n "${STEP_COUNTER_FILE:-}" ]] || die "${FUNCNAME[0]}: Missing \$STEP_COUNTER_FILE"

  STEP_COUNTER=$(cat "${STEP_COUNTER_FILE}")
  STEP_COUNTER=$((STEP_COUNTER+1))
  echo "${STEP_COUNTER}" > "${STEP_COUNTER_FILE}"

  printf "[%02i/%02i]" "${STEP_COUNTER}" "${TOTAL_STEP_COUNT}"
}

# Check valid step name
#
# This function is supposed to be a helper for run_step_check(), to make sure
# that we are running checks that fit within the parameters of the pre-check
# phase, and that we stop if we detect that something might be wrong.
check_valid_step_name() {
  local step="$1"

  # all checks start with */check_ or are */report_check_{start,stop}, or are
  # the first of the "upgrade proper" steps
  if [[ "${step}" =~ .*/check_.* ]] ; then
    return
  elif [[ "${step}" =~ .*/report_check_(start|stop) ]] ; then
    return
  elif [[ "${step}" =~ .*/report_upgrade_start ]] ; then
    die "This step is not a 'check' but the start of the 'proper upgrade', aborting: '${step}'"
  fi

  die "Invalid/unexpected check, aborting: '${step}'"
}

# Function similar to ngcp-upgrade's run_step(), but without extra actions
#
# In particular:
#
# 1) removing the "pause-before-step" functionality, it does not make much sense
#    in the context of checks
#
# 2) not keeping track of which checks were already executed, to avoid leaving
#    "state" on disk, and because the full run of tests is supposed to be very
#    fast and be completed within seconds
run_step_check() {
  local step="$1"

  check_valid_step_name "${step}"

  local start_step_time
  start_step_time="$(get_time)"

  shift # drop step name from "$@"
  echo "$(get_timestamp): $(get_upgrade_steps_counter) Running: ${STEPS_FOLDER}/${step} $*"

  local RC=0
  if ! "${STEPS_FOLDER}/${step}" "$@" ; then
    RC=1
  fi

  # print elapsed time
  local end_step_time
  end_step_time="$(get_time)"
  local step_elapsed
  step_elapsed=$(get_elapsed "${start_step_time}" "${end_step_time}")
  echo "Time taken to run step ${step}: ${step_elapsed}s"

  # empty line to separate step output, for legibility
  echo

  if [[ "${RC}" -ne 0 ]] ; then
    die "the step '${step}' failed, upgrade check aborted!"
  fi
}

# Function to simulate to run a check, and print timestamps and counters and
# everything, but which is actually disabled
run_step_check_disabled() {
  local step="$1"

  echo "$(get_timestamp): $(get_upgrade_steps_counter) IGNORING STEP: ${STEPS_FOLDER}/${step}"

  DISABLED_CHECKS+=("${step}")

  # empty line to separate step output, for legibility
  echo
}


################################################################################
# main
################################################################################

PROGRAM_START_TIME="$(get_time)"

get_options "$@"

# (shellcheck: no, we don't want to do anything with the $i)
# shellcheck disable=SC2034
for i in $(seq 1 ${PARSED_OPTIONS}); do
  shift
done

if [[ $# -ne 1 ]] ; then
  {
    usage
    echo
  } >&2
  die "Needs 1 argument but $# provided, see 'usage' above"
fi

# determine the base dir (where 'scenarios' exist), either by using a
# environment variable or by detecting and defining a suitable
# NGCP_UPDATE_SCENARIO_BASE_DIR for the rest of the script to use
if [[ ! -v NGCP_UPDATE_SCENARIO_BASE_DIR ]] ; then
  if [[ -d "${NGCP_UPDATE_SCENARIO_BASE_DIR_DEFAULT}" ]]; then
    NGCP_UPDATE_SCENARIO_BASE_DIR="${NGCP_UPDATE_SCENARIO_BASE_DIR_DEFAULT}"
  else
    die "Could not find 'scenario' directory, and \$NGCP_UPDATE_SCENARIO_BASE_DIR is not defined"
  fi
fi

# determine current version to upgrade from
if [[ ! -r /etc/ngcp_version ]] ; then
  die "Could not read /etc/ngcp_version file"
fi
CURRENT_VERSION="$(cat /etc/ngcp_version)"
check_valid_version_or_abort "${CURRENT_VERSION}"

# determine version to upgrade to
TARGET_VERSION="$1"
check_valid_version_or_abort "${TARGET_VERSION}"

if [[ "${TARGET_VERSION}" == 'trunk' ]]; then
  CURRENT_VERSION=stable
fi

# use this scenario file
TARGET_VERSION_DIR="$(realpath "${NGCP_UPDATE_SCENARIO_BASE_DIR}/${TARGET_VERSION}")"
if [[ ! -d "${TARGET_VERSION_DIR}" ]] ; then
  die "Directories for version '${TARGET_VERSION}' not found: ${TARGET_VERSION_DIR}"
fi
SCENARIO_FILE="$(realpath "${TARGET_VERSION_DIR}/upgrade-${CURRENT_VERSION}-to-${TARGET_VERSION}")"
if ! [[ -r "${SCENARIO_FILE}" ]] ; then
  die "Scenario file does not exist or cannot be read: ${SCENARIO_FILE}"
fi
if ! grep run_step "${SCENARIO_FILE}" &>/dev/null ; then
  die "Scenario file does not seem to have correct format (e.g. lines with 'run_step'): ${SCENARIO_FILE}"
fi
echo -e "Scenario file to use:\\n  ${SCENARIO_FILE}"

# use temporary file for checks
SCENARIO_FILE_BASENAME="$(basename "${SCENARIO_FILE}")"
WORK_DIR="$(mktemp --tmpdir --dir "${PROGRAM}_XXXXXXXX")"
CHECK_FILE="${WORK_DIR}/${SCENARIO_FILE_BASENAME}-checks-only"
echo -e "Extracting checks to:\\n  ${CHECK_FILE}"
sed -n '0,/^## UPGRADE #\+$/ p' "${SCENARIO_FILE}" > "${CHECK_FILE}"
sed -i 's/^run_step /run_step_check /' "${CHECK_FILE}"
# cosmetic change only, to avoid confusing people with the "UPGRADE" line
sed -i 's/^## UPGRADE #\+$/## END OF PRE-UPGRADE CHECKS ##/' "${CHECK_FILE}"

if [[ "${DISABLE_MAINTENANCE_CHECK}" = "true" ]]; then
  sed -i '/check_maintenance_mode/ s/^run_step_check /run_step_check_disabled /' "${CHECK_FILE}"
fi

if [[ "${DISABLE_APT_CHECK}" = "true" ]]; then
  sed -i '/check_apt_sources_list/ s/^run_step_check /run_step_check_disabled /' "${CHECK_FILE}"
fi

# global/static vars for printing progress
TOTAL_STEP_COUNT=$(grep -c '^run_step_check' "${CHECK_FILE}")
debug "Total number of steps detected: ${TOTAL_STEP_COUNT}"
STEP_COUNTER_FILE="${WORK_DIR}/step-counter"
echo "0" > "${STEP_COUNTER_FILE}"
debug "Using file for step counter: ${STEP_COUNTER_FILE}"

# export for steps to use, they are needed with that name in some of them
CONF_FOLDER="$(realpath "${NGCP_UPDATE_SCENARIO_BASE_DIR}/../conf/")"
export CONF_FOLDER
STEPS_FOLDER="$(realpath "${NGCP_UPDATE_SCENARIO_BASE_DIR}/../steps/")"
export STEPS_FOLDER
export UPGRADE_VERSION="${TARGET_VERSION}"
export OLD_VERSION="${CURRENT_VERSION}"
# this variable needs to be set, some steps require it
export NGCP_UPGRADE="ngcp-upgrade"
export LOGFILE="/dev/null"
export FORCE_UPGRADE="${FORCE_UPGRADE:-no}"

# run
echo
echo "================================================================================"
echo "=> Running ${CHECK_FILE}"
echo "================================================================================"
# shellcheck disable=SC1090
source "${CHECK_FILE}"

# epilogue
PROGRAM_END_TIME="$(get_time)"
PROGRAM_ELAPSED="$(get_elapsed "${PROGRAM_START_TIME}" "${PROGRAM_END_TIME}")"
echo "Time taken to run program: ${PROGRAM_ELAPSED}s"

if [[ "${#DISABLED_CHECKS[@]}" -ne 0 ]]; then
  echo
  echo "NOTE: The following checks have been disabled in this run:"
  for check in "${DISABLED_CHECKS[@]}"; do
    echo " - ${check}"
  done
fi
