#!/bin/bash

set -e
set -E
set -u
set -o pipefail

PROGNAME=${0##*/}

debug() {
  local debug="${DEBUG:-false}"
  local message="$*"

  if [[ "${debug}" == "true" ]] ; then
    echo "DEBUG: ${message}" >&2
  fi
}

error() {
  local message="$*"
  echo "ERROR: ${message}" >&2
}

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

usage() {
  echo "Usage: ${PROGNAME} TARGET_VERSION"
  echo "  e.g. ${PROGNAME} mr6.5.5"
}

GLOBAL_TMPDIR="$(mktemp -d)"
cleanup() {
  exit_status=$?
  trap '' EXIT HUP INT TERM
  rm -fr "${GLOBAL_TMPDIR:?}"
  exit ${exit_status}
}
trap cleanup EXIT HUP INT TERM

ensure_global_tmpdir() {
  if [[ ! -d "${GLOBAL_TMPDIR}" || ! -w "${GLOBAL_TMPDIR}" ]] ; then
    die "Cannot use \$GLOBAL_TMPDIR: ${GLOBAL_TMPDIR}"
  fi
}

create_tmpfile() {
  local tmpfile=""
  tmpfile="$(mktemp --tmpdir="${GLOBAL_TMPDIR}")"
  if [[ ! -f "${tmpfile}" || ! -w "${tmpfile}" ]] ; then
    die "Not a regular file or cannot write to '${tmpfile}'"
  fi

  echo "${tmpfile}"
}

dump_file() {
  local file="$1"
  if [[ ! -r "${file}" ]] ; then
    die "Cannot read from file ${file}"
  fi

  echo "----"
  cat "${file}"
  echo "----"
}

abort_if_max_tries() {
  local try="$1"
  local max_tries="$2"

  if [[ "${try}" -ge "${max_tries}" ]] ; then
    # try to flush previous output to stdout
    sync

    die "max retries ${max_tries} reached, aborting"
  fi
}

apt_get_with_options() {
  # sanity check
  ensure_global_tmpdir

  echo apt-get \
      -o Dir::Cache="${GLOBAL_TMPDIR}/cachedir" \
      -o Dir::Etc="${GLOBAL_TMPDIR}/etc/apt/" \
      -o Dir::Etc::PreferencesParts="${GLOBAL_TMPDIR}/etc/apt/preferences.d/" \
      -o Dir::Etc::TrustedParts="${GLOBAL_TMPDIR}/etc/apt/trusted.gpg.d/" \
      -o Dir::State="${GLOBAL_TMPDIR}/statedir"
}

apt_update() {
  # sanity check
  ensure_global_tmpdir

  local tmpfile=""
  tmpfile="$(create_tmpfile)"

  local max_tries=3
  local regex1="^E: "
  local regex2="Some index files failed to download"
  local rc=0
  for try in $(seq 1 "${max_tries}") ; do
    rc=0
    rm -rf "${GLOBAL_TMPDIR:?}"/{cachedir,statedir,etc/apt/trusted.gpg.d}
    mkdir -p "${GLOBAL_TMPDIR}"/{cachedir,statedir,etc/apt/trusted.gpg.d}
    mkdir -p "${GLOBAL_TMPDIR}"/etc/apt/preferences.d/ # to avoid spurious warnings
    cp -a /etc/apt/trusted.gpg.d/* "${GLOBAL_TMPDIR}"/etc/apt/trusted.gpg.d/

    # write preferences for pinning to allow downgrades if necessary
    cat <<EOF > "${GLOBAL_TMPDIR}"/etc/apt/preferences.d/0000-pin-to-downgrade
Package: ${UPGRADE_PACKAGE_NAME}
Pin: release n=${UPGRADE_DEBIAN_CODENAME}, o=Sipwise, version ${UPGRADE_VERSION##mr}.*
Pin-Priority: 1100
EOF

    # allow apt to change to the unpriviledged user
    if [[ ${EUID} == 0 ]] ; then
      chown _apt "${GLOBAL_TMPDIR}" "${GLOBAL_TMPDIR}"/{cachedir,statedir}
    fi

    $(apt_get_with_options) update |& tee "${tmpfile}" |& sed 's/^/  /' || rc=$?

    if [[ "${rc}" -ne 0 ]] || grep -qE "${regex1}|${regex2}" "${tmpfile}" ; then
      echo " - WARNING: The repositories cannot update available packages with all configured repos (try #${try}):"
      dump_file "${tmpfile}"

      abort_if_max_tries "${try}" "${max_tries}"
      continue
    else
      echo
      echo "  - OK, repositories for the configured versions are reachable."
      echo
      break
    fi
  done
}

apt_install() {
  # sanity check
  ensure_global_tmpdir

  local tmpfile=""
  tmpfile="$(create_tmpfile)"

  local max_tries=3
  local regex1="^E: "
  local regex2="failed"
  local rc=0
  for try in $(seq 1 "${max_tries}") ; do
    rc=0

    ($(apt_get_with_options) install -t "${CURRENT_DEBIAN_RELEASE}" --allow-downgrades --assume-yes "${UPGRADE_PACKAGE_NAME}" "${INSTALLER_PACKAGE_NAME}" |& tee "${tmpfile}" |& sed 's/^/  /') || rc=$?

    if [[ "${rc}" -ne 0 ]] || grep -qE "${regex1}|${regex2}" "${tmpfile}" ; then
      echo " - WARNING: The packages '${UPGRADE_PACKAGE_NAME} ${INSTALLER_PACKAGE_NAME}' could not be installed (try #${try}):"
      dump_file "${tmpfile}"

      abort_if_max_tries "${try}" "${max_tries}"
      continue
    else
      echo
      echo "  - OK, '${UPGRADE_PACKAGE_NAME} ${INSTALLER_PACKAGE_NAME}' packages installed:"
      dpkg-query --showformat "  \${Package} version \${Version}, status: \${db:Status-Status} \${db:Status-Eflag}\n" -W "${UPGRADE_PACKAGE_NAME}" "${INSTALLER_PACKAGE_NAME}"
      break
    fi
  done
}

write_config() {
  # use clean ${GLOBAL_TMPDIR}/etc/apt/sources.list and .../sources.list.d/* files
  debug "Creating apt repository configuration"

  debug "Using base Debian system '${UPGRADE_DEBIAN_CODENAME}' as guessed for upgrade"

  local ngcp_type=""
  UPGRADE_PACKAGE_NAME="ngcp-upgrade-"
  INSTALLER_PACKAGE_NAME="ngcp-installer-"
  case "${NGCP_EDITION}" in
    spce)
      ngcp_type="spce"
      UPGRADE_PACKAGE_NAME+="ce"
      INSTALLER_PACKAGE_NAME+="ce"
      ;;
    sppro|carrier)
      ngcp_type="sppro"
      UPGRADE_PACKAGE_NAME+="pro"
      INSTALLER_PACKAGE_NAME+="pro"
      ;;
    *)
      die "Could not detect or understand NGCP release type: '${NGCP_EDITION}'"
      ;;
  esac
  debug "Detected edition for repos: ${ngcp_type}"
  debug "Detected ngcp-upgrade package name: ${UPGRADE_PACKAGE_NAME}"
  debug "Detected ngcp-installer package name: ${INSTALLER_PACKAGE_NAME}"

  if [[ "${NGCP_EDITION}" == "spce" ]] ; then
    local REPOS_BASE_URL="https://deb.sipwise.com"
  else
    if [[ -z "${NGCP_MGMT_NODE}" ]] ; then
      die "Missing management node name, cannot continue"
    fi

    if [[ -z "${APPROX_RO_PORT}" || ! "${APPROX_RO_PORT}" =~ ^[0-9]+$ ]] ; then
      die "Missing or invalid APPROX_RO_PORT='${APPROX_RO_PORT}', cannot continue"
    fi

    local REPOS_BASE_URL="http://${NGCP_MGMT_NODE}:${APPROX_RO_PORT}"
  fi

  # create default URL, special case for trunk
  local REPOS_DEFAULT="${REPOS_BASE_URL}/${ngcp_type}/${UPGRADE_VERSION}/ ${UPGRADE_DEBIAN_CODENAME} main"
  if [[ "${UPGRADE_VERSION}" == "trunk" ]] ; then
    REPOS_DEFAULT="${REPOS_BASE_URL}/autobuild/ release-${UPGRADE_VERSION}-${UPGRADE_DEBIAN_CODENAME} main"
  fi

  # support overriding via env variable, and use as global variable
  REPOS="${REPOS:-${REPOS_DEFAULT}}"
  debug "Using URL for repositories: ${REPOS}"

  # exported as global variable for other functions to use
  SOURCES_DIR_PATH="${GLOBAL_TMPDIR}/etc/apt/sources.list.d"

  mkdir -p "${SOURCES_DIR_PATH}"

  debug "Creating ${SOURCES_DIR_PATH}/sipwise.list"
  cat > "${SOURCES_DIR_PATH}"/sipwise.list <<EOF
deb [arch=amd64] ${REPOS}
EOF

  debug "Creating ${SOURCES_DIR_PATH}/debian.list"
  cat > "${SOURCES_DIR_PATH}"/debian.list <<EOF
deb ${REPOS_BASE_URL}/debian/ ${CURRENT_DEBIAN_RELEASE} main contrib non-free
deb ${REPOS_BASE_URL}/debian-security/ ${CURRENT_DEBIAN_RELEASE}-security main contrib non-free
deb ${REPOS_BASE_URL}/debian/ ${CURRENT_DEBIAN_RELEASE}-updates main contrib non-free
EOF

  if "${ENABLE_UPGRADE_PPA:-false}"; then
    local source_list_ppa='/etc/apt/sources.list.d/sipwise_ppa.list'
    if [[ -r "${source_list_ppa}" ]]; then
      cp "${source_list_ppa}" "${SOURCES_DIR_PATH}"/
    fi

    local preference_ppa='/etc/apt/preferences.d/00_sipwise_ppa'
    if [[ -r "${preference_ppa}" ]]; then
      mkdir -p "${GLOBAL_TMPDIR}/etc/apt/preferences.d"
      cp "${preference_ppa}" "${GLOBAL_TMPDIR}/etc/apt/preferences.d"/
    fi
  fi
}

check_valid_release() {
  local release="$1"
  if [[ "${release}" =~ ^trunk$ || "${release}" =~ ^mr([0-9]){1,2}(\.([0-9]){1,2}){1,2}$ ]] ; then
    return 0
  else
    return 1
  fi
}

assert_expected_vars() {
  for expected_var in "$@"; do
    if [[ ! -v "${expected_var}" ]] ; then
      die "missing config var: ${expected_var}"
    fi
  done
}

validate_package_version() {
  local package_name="$1"

  local ngcp_edition=""
  case "${NGCP_EDITION}" in
    sppro|carrier)
      # we don't have https://deb.sipwise.com/carrier/,
      # but need to handle it like a PRO system
      ngcp_edition="sppro"
      ;;
    spce)
      ngcp_edition="${NGCP_EDITION}"
      ;;
    *)
      die "Could not detect or understand NGCP release type: '${NGCP_EDITION}'"
      ;;
  esac

  local package_url="https://deb.sipwise.com"
  # shellcheck disable=SC2153
  if [[ "${NGCP_TYPE}" != "spce" ]]; then
    package_url="${NGCP_MGMT_NODE}:${APPROX_RO_PORT}"
  fi

  for try in {1..3}; do
    local packages_file
    packages_file="${package_url}/${ngcp_edition}/${CURRENT_VERSION}/dists/${CURRENT_DEBIAN_RELEASE}/main/binary-amd64/Packages.gz"
    wget -tries=0 -q -O "${GLOBAL_TMPDIR}"/Packages.gz "${packages_file}" && break
    echo "Trying to download '${packages_file}' failed, try #${try}"
    sleep 3s
  done

  if [[ ! -s "${GLOBAL_TMPDIR}"/Packages.gz ]] ; then
    echo "ERROR: URL '${packages_file}' could not be downloaded" >&2
    echo "NOTE: make sure that execution of wget ${packages_file}' works" >&2
    echo "NOTE: ensure that it's not an IPv4 or IPv6 specific problem by checking via wget -4 … / wget -6 …" >&2
    echo "NOTE: set 'precedence ::ffff:0:0/96  100' in /etc/gai.conf to give IPv4 precedence over IPv6 for destination resolution" >&2
    die "Failed to download repository information file, giving up."
  fi

  # sed: display paragraphs matching the "Package: ..." string, then grab string "^Version: " and display the actual version via awk
  # sort -u to avoid duplicates in repositories (e.g. in the past we had ngcp-installer-pro AND ngcp-installer-pro-ha-v3 debs)
  local available_version installed_version
  available_version=$(zcat "${GLOBAL_TMPDIR}"/Packages.gz | sed "/./{H;\$!d;};x;/Package: ${package_name}/b;d" | awk '/^Version: / {print $2}' | sort -u)
  installed_version=$(dpkg-query --showformat '${Version}\n' -W "${package_name}" 2>/dev/null)
  if [[ "${available_version}" != "${installed_version}" ]] ; then
    error "Installed package version '${installed_version}' of '${package_name}' doesn't seem to match latest available '${available_version}'"
    die "Exiting to avoid upgrade problems due to outdated package versions."
  fi
}

request_confirmation() {
  if [[ "${FORCE_UPGRADE:-}" = "true" ]] ; then
    echo "Forcing as requested via environment variable FORCE_UPGRADE."
    return
  fi

  while true; do
    echo -n "Should the upgrade continue? (yes/no): "
    read -r answer
    case "${answer,,}" in
      yes)
        echo "Continue as requested."
        break
        ;;
      no)
        die "Aborted as requested."
        ;;
      *)
        echo "Please answer 'yes' or 'no'."
        ;;
    esac
    unset answer
  done
}

################################################################################
# main execution
################################################################################

if [[ $# -ne 1 ]] ; then
  error "Wrong number of arguments: $*"
  usage >&2
  exit 1
fi

CONF_FILE="/etc/ngcp-upgrade/prepare_upgrade.conf"

if [[ ! -f "${CONF_FILE}" ]] ; then
  die "missing config file: ${CONF_FILE}"
fi
# shellcheck disable=SC1090
source "${CONF_FILE}"

assert_expected_vars NGCP_TYPE
if [[ "${NGCP_TYPE}" != "spce" ]] ; then
  assert_expected_vars NGCP_MGMT_NODE APPROX_RO_PORT
fi

UPGRADE_VERSION="$1"
if ! check_valid_release "${UPGRADE_VERSION}" ; then
  error "invalid version to upgrade: ${UPGRADE_VERSION}"
  usage >&2
  exit 1
fi
echo "Provided target version to upgrade: ${UPGRADE_VERSION}"

CURRENT_VERSION="$(cat /etc/ngcp_version)"
echo "Detected current version: ${CURRENT_VERSION}"

NGCP_EDITION="${NGCP_TYPE}"
echo "Detected current edition: ${NGCP_EDITION}"

CURRENT_DEBIAN_RELEASE="$(lsb_release -c -s)"
echo "Current Debian release: ${CURRENT_DEBIAN_RELEASE}"

META_RELEASE_FILE="meta-release"
META_RELEASE_URL="https://deb.sipwise.com"
if [[ "${NGCP_TYPE}" != "spce" ]]; then
  META_RELEASE_URL="${NGCP_MGMT_NODE}:${APPROX_RO_PORT}"
fi

for try in {1..3}; do
  rm -f "${META_RELEASE_FILE:?}"
  wget --tries=0 -q --directory-prefix="${GLOBAL_TMPDIR}" "${META_RELEASE_URL}/${META_RELEASE_FILE}" && break
  echo "Trying to download '${META_RELEASE_URL}' failed, try #${try}"
  sleep 3s
done

if [[ ! -s "${GLOBAL_TMPDIR}"/"${META_RELEASE_FILE}" ]] ; then
  echo "ERROR: URL '${META_RELEASE_URL}' could not be downloaded, or file '${GLOBAL_TMPDIR}/${META_RELEASE_FILE} is not valid" >&2
  echo "NOTE: make sure that execution of wget ${META_RELEASE_URL}' works" >&2
  echo "NOTE: ensure that it's not an IPv4 or IPv6 specific problem by checking via wget -4 … / wget -6 …" >&2
  echo "NOTE: set 'precedence ::ffff:0:0/96  100' in /etc/gai.conf to give IPv4 precedence over IPv6 for destination resolution" >&2
  die "Failed to download release information file, giving up."
fi

UPGRADE_DEBIAN_CODENAME=$(sed -n "/^\[${UPGRADE_VERSION}\]$/,/^$/ p" "${GLOBAL_TMPDIR}"/"${META_RELEASE_FILE}" | awk '/^Dist = / {print $3}')
if [[ "${UPGRADE_DEBIAN_CODENAME}" == "_invalid_" ]] || ! [[ "${UPGRADE_DEBIAN_CODENAME}" =~ ^[a-z]+$ ]] ; then
  die "invalid base distribution for upgrade target: ${UPGRADE_DEBIAN_CODENAME}"
else
  echo "Detected base distribution for upgrade target: ${UPGRADE_DEBIAN_CODENAME}"
fi

# write basic config based on values above
echo "Configuring repositories for the version to upgrade: '${UPGRADE_VERSION}' based on '${UPGRADE_DEBIAN_CODENAME}'"
write_config
tail -n +0 "${GLOBAL_TMPDIR}"/etc/apt/sources.list.d/* | sed 's/^/  /'
echo

# check repos
echo "Checking repo accessibility"
apt_update

if ! "${ENABLE_UPGRADE_PPA:-false}"; then
  if aptitude search '?and(?installed,~U)' -F '%p' | grep -q '.' ; then
    error "Outstanding package updates have been identified."
    error "Ensure all packages are recent before continuing with upgrade."
    error "The following packages are not up2date:"
    for package in $(aptitude search '?and(?installed,~U)' -F '%p') ; do
      apt-cache policy "${package}"
    done
    request_confirmation
  fi
fi

# package install
echo "Installing package '${UPGRADE_PACKAGE_NAME}' and '${INSTALLER_PACKAGE_NAME}'..."
apt_install
