#!/bin/bash

set -e
set -u
set -o pipefail

# shellcheck disable=SC2155
declare -r ME="$(basename "$0")"
declare -r OPTIONS=("$@")
INVOKED_BY="${INVOKED_BY:-$0 $*}"
FORCE=false

declare -r APPROX_HOME="/var/cache/approx"
if [[ ! -d "${APPROX_HOME}" ]] ; then
  echo "ERROR: missing ${APPROX_HOME}" >&2
  exit 1
fi

declare -r INFO_LABEL="snapshot-info"
declare -r INFO_DIR="${APPROX_HOME}/${INFO_LABEL}"
mkdir -p "${INFO_DIR}"
chown -R root:root "${INFO_DIR}"

declare -r APPROX_SNAPS="/mnt/glusterfs/mgmt-share/approx_snapshots"
mkdir -p "${APPROX_SNAPS}"
chown approx:approx "${APPROX_SNAPS}"

declare CUR_TIME
CUR_TIME="$(TZ=UTC date +"%Y%m%d%H%M%S")"
declare SNAP_NAME="${SNAP_NAME:-${CUR_TIME}}"

#########################################################################
# functions

help() {
  cat <<EOT

${ME} - Approx snapshot helper script for NGCP PRO/Carrier systems

${ME} [<option(s)>] <action(s)>

  Actions:
   -h|--help            - show this help
   -f|--force           - force all the actions (answer 'yes' to all questions)
   -c|--create          - create approx snapshot (apt metadata only)
   -d|--delete <name>   - delete specific approx snapshot
   -e|--export <name>   - export specific approx snapshot
   -i|--import <file>   - delete specific approx snapshot
   -l|--list            - show list of available approx snapshots
   -s|--switch <name>   - switch to new approx snapshot. NOTE: the active cache will be GONE!
   -A|--apt-update      - clean local apt cache and re-fetch it from approx
   -C|--clean           - remove all non-locked snapshots
   -L|--lock <name>     - lock snapshot (to prevent removal)
   -U|--unlock <name>   - unlock snapshot (to be able to remove it)
   -S|--search <name>   - search for particular package in snapshots

EOT
}

main() {
  local _cmdline_opts="help,force,create,list,delete:,export:,import:,switch:,clean,lock:,unlock:,apt-update,search:"

  local _opt_temp
  _opt_temp=$(getopt -n "${ME}" -o "+hfcld:e:i:s:CL:U:AS:" -l "${_cmdline_opts}" -- "${OPTIONS[@]}")

  eval set -- "${_opt_temp}"

  while :; do
    case "$1" in
    --help|-h)
      help
      exit 0
      ;;
    --force|-f)
      FORCE=true
      ;;
    --create|-c)
      snapshot_create
      ;;
    --list|-l)
      snapshot_list
      ;;
    --delete|-d)
      shift; SNAP_NAME="$1"
      snapshot_delete
      ;;
    --export|-e)
      shift; SNAP_NAME="$1"
      snapshot_export
      ;;
    --import|-i)
      shift; SNAP_FILE="$1"
      snapshot_import
      ;;
    --switch|-s)
      shift; SNAP_NAME="$1"
      snapshot_switch
      ;;
    --apt-update|-A)
      snapshot_apt_update
      ;;
    --clean|-C)
      snapshot_clean
      ;;
    --lock|-L)
      shift; SNAP_NAME="$1"
      snapshot_lock
      ;;
    --unlock|-U)
      shift; SNAP_NAME="$1"
      snapshot_unlock
      ;;
    --search|-S)
      shift; PACKAGE_NAME="$1"
      snapshot_search
      ;;
    --)
      shift; break
      ;;
    *)
      echo "Internal getopt error! $1" >&2
      exit 1
      ;;
    esac
    shift
  done
}

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

  while true; do
    echo -n "Should we continue here? (yes/no): "
    read -r answer
    case "${answer,,}" in
      yes)
        echo "Continue as requested."
        break
        ;;
      no)
        echo "Aborting as requested." >&2
        exit 1
        ;;
      *)
        echo "Please answer 'yes' or 'no'."
        ;;
    esac
    unset answer
  done
}

snapshot_create() {
  echo "Creating approx snapshot '${SNAP_NAME}'..."

  local SNAP_HOME="${APPROX_SNAPS}/${SNAP_NAME}"
  mkdir "${SNAP_HOME}"

  pushd "${APPROX_HOME}" >/dev/null 2>&1

  echo "Generating extra snapshot information..."
  ngcp-hostname > "${INFO_DIR}/hostname"
  dpkg -l > "${INFO_DIR}/dpkg.list"
  echo "${CUR_TIME:0:4}-${CUR_TIME:4:2}-${CUR_TIME:6:2} ${CUR_TIME:8:2}:${CUR_TIME:10:2}:${CUR_TIME:12:2}" > \
    "${INFO_DIR}/created"
  echo "$(cat /etc/ngcp_version): ${INVOKED_BY}" > "${INFO_DIR}/details"

  echo "Snapshotting approx data..."
  find . \( -name 'dists' -o -name "${INFO_LABEL}" \) -type d \
    -exec cp -r --parents {} "${SNAP_HOME}" \;

  echo "Snapshotting upgrade files..."
  # These files can be missed so don't fail on it
  # Remove '2>/dev/null || true' in mr15.0
  cp meta-release upgrade-paths-blocked "${SNAP_HOME}" 2>/dev/null || true

  echo "Updating snapshot ownership..."
  chown -R approx:approx "${SNAP_HOME}"
  chown -R root:root "${SNAP_HOME}/${INFO_LABEL}"

  popd >/dev/null 2>&1

  echo "The snapshot was created successfully: '${SNAP_NAME}'."
}

snapshot_list() {
  if [ -z "$(ls -1 "${APPROX_SNAPS}")" ]; then
    echo "No snapshots available."
    return
  fi

  echo "List of locally available approx snapshots:"

  while IFS= read -r snap; do
    local snap_dir="${APPROX_SNAPS}/${snap}/${INFO_LABEL}"
    local snap_info="${snap_dir}/created"
    local approx_info="${INFO_DIR}/created"

    local snap_date
    if [ -r "${snap_info}" ]; then
      snap_date="$(cat "${snap_info}")"
    fi

    local msg_active=" "
    if [ -r "${approx_info}" ] && \
       [ "${snap_date}" == "$(cat "${approx_info}")" ]; then
      msg_active="*"
    fi

    local msg_hostname="UNKNOWN"
    if [ -r "${snap_dir}/hostname" ]; then
      msg_hostname="$(cat "${snap_dir}/hostname")"
    fi

    local msg_locked=""
    if [ -f "${snap_dir}/locked" ]; then
      msg_locked="[LOCKED]"
    fi

    local msg_cmd="No info about invoked command is found"
    if [ -r "${snap_dir}/details" ]; then
      msg_cmd="$(cat "${snap_dir}/details")"
    fi

    echo " ${msg_active} ${snap} | Created at ${snap_date} (UTC) on ${msg_hostname} ${msg_locked} | ${msg_cmd}"
  done < <(find "${APPROX_SNAPS}"/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
}

snapshot_delete() {
  local snapshot="${APPROX_SNAPS}/${SNAP_NAME}"

  if [ ! -d "${snapshot}" ]; then
    echo "ERROR: missing snapshots to remove '${snapshot}'" >&2
    exit 1
  fi

  if [ -f "${snapshot}/${INFO_LABEL}/locked" ]; then
    echo "ERROR: cannot remove locked snapshots '${SNAP_NAME}'" >&2
    exit 1
  fi

  echo "Deleting approx snapshot '${SNAP_NAME:?}'"
  snapshot_delete_it "${SNAP_NAME:?}"
}

snapshot_delete_it() {
  local snap_name="${1:?}"

  rm -rf "${APPROX_SNAPS:?}/${snap_name:?}"
}

snapshot_export() {
  echo "Exporting approx snapshot '${SNAP_NAME}'..."

  local snapshot="${APPROX_SNAPS}/${SNAP_NAME}"
  if [ ! -d "${snapshot}" ]; then
    echo "ERROR: missing snapshots to export '${snapshot}'" >&2
    exit 1
  fi

  local TMPDIR
  TMPDIR="$(mktemp -d -t ngcp-approx-snapshot-export-XXXXXXXXXX)"
  local tmp="${TMPDIR}/ngcp-approx-snapshot-${SNAP_NAME}.tar.gz"
  tar -czf "${tmp}" -C "${APPROX_SNAPS}" "${SNAP_NAME}"
  echo "Successfully exported approx snapshot: ${tmp}"
}

snapshot_import() {
  echo "Importing approx snapshot '${SNAP_FILE}'..."

  if [ ! -r "${SNAP_FILE}" ]; then
    echo "ERROR: missing snapshots file to import '${SNAP_FILE}'" >&2
    exit 1
  fi

  pushd "${APPROX_SNAPS}" >/dev/null 2>&1
  tar -xzf "${SNAP_FILE}"
  echo "Successfully imported approx snapshot!"
  popd >/dev/null 2>&1

  chown -R root:root "${INFO_DIR}"
}

snapshot_switch() {
  echo "Switching to approx snapshot..."

  local snapshot="${APPROX_SNAPS}/${SNAP_NAME}"
  if [ ! -d "${snapshot}" ]; then
    echo "ERROR: missing snapshots to switch on '${snapshot}'" >&2
    exit 1
  fi

  echo "WARNING: switching to the approx snapshot '${SNAP_NAME}',"
  echo "         the active approx cache will be removed!"
  request_confirmation

  : "${APPROX_HOME?ERROR: Missing mandatory environment variable 'APPROX_HOME', cannot continue.}"
  if [ -d "${APPROX_HOME}" ]; then
    echo "Removing the active approx cache '${APPROX_HOME}'..."
    approx_gluster=$(readlink -f "${APPROX_HOME}")
    rm -rf "${APPROX_HOME:?}" "${approx_gluster:?}"
    mkdir "${approx_gluster}"
    ln -s "${approx_gluster}" "${APPROX_HOME}"
  fi

  echo "Switching to the approx snapshot '${SNAP_NAME}'..."
  cp -a "${snapshot}"/* "${APPROX_HOME}/"

  echo "Updating ownership for files from the snapshot..."
  # / is important there due to symlink: /var/cache/approx -> /mnt/glusterfs/mgmt-share/approx
  chown -R approx:approx "${APPROX_HOME}" "${APPROX_HOME}"/
}

snapshot_apt_update() {
  echo "Cleaning old apt metadata..."

  rm -f /var/lib/apt/lists/*InRelease
  rm -f /var/lib/apt/lists/*Packages
  rm -f /var/lib/apt/lists/*Sources
  rm -f /var/lib/apt/lists/*_Index
  rm -f /var/lib/apt/lists/*.diff_Index
  rm -f /var/lib/apt/lists/*_Translation*

  NGCP_SHOW_HINT=false apt update
}

snapshot_clean() {
  if [ -z "$(ls -1 "${APPROX_SNAPS}")" ]; then
    echo "No snapshots available."
    return
  fi

  echo "WARNING: Do you want to remove all non-locked snapshots?"
  request_confirmation

  while IFS= read -r snap; do
    if [ -f "${APPROX_SNAPS}/${snap}/${INFO_LABEL}/locked" ]; then
      echo "Skipping locked snapshots '${snap}'"
    else
      echo "Removing snapshots '${snap}'"
      snapshot_delete_it "${snap}"
    fi
  done < <(find "${APPROX_SNAPS}"/ -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
}

snapshot_lock() {
  local snapshot="${APPROX_SNAPS}/${SNAP_NAME}"
  if [ ! -d "${snapshot}" ]; then
    echo "ERROR: missing snapshots to switch on '${snapshot}'" >&2
    exit 1
  fi

  mkdir -p "${snapshot}/${INFO_LABEL}"
  {
    date
    who
  } > "${snapshot}/${INFO_LABEL}/locked"
  chown -R root:root "${snapshot}/${INFO_LABEL}"

  echo "Locked snapshot '${SNAP_NAME}'"
}

snapshot_unlock() {
  local snapshot="${APPROX_SNAPS}/${SNAP_NAME}"
  if [ ! -d "${snapshot}" ]; then
    echo "ERROR: missing snapshots to switch on '${snapshot}'" >&2
    exit 1
  fi

  rm -f "${snapshot}/${INFO_LABEL}/locked"

  echo "Unlocked snapshot '${SNAP_NAME}'"
}

snapshot_search() {
  local nothing_found=true

  echo "Search results for the package '${PACKAGE_NAME}':"

  # Searching for package in all approx snapshots:
  while IFS= read -r package_gz; do
    local version
    version=$(zcat "${package_gz}" | awk -v RS='' "/Package: ${PACKAGE_NAME}\n/ { print }" | awk '/^Version:/ { print $2; exit }' || true)
    if [ -n "${version}" ]; then
      snap="$(echo "${package_gz}" | grep -Po "${APPROX_SNAPS}/\K[^/]*" || true)"
      snap_repo="$(echo "${package_gz}" | grep -Po 'dists/\K[^/]*' || true)"
      echo "   ${snap} : ${version} (${snap_repo})"
      nothing_found=false
    fi
  done < <(find "${APPROX_SNAPS}" -type f -name 'Packages.*' | sort)

  # Searching for package in current approx cache:
  while IFS= read -r package_gz; do
    local version
    version=$(zcat "${package_gz}" | awk -v RS='' "/Package: ${PACKAGE_NAME}\n/ { print }" | awk '/^Version:/ { print $2; exit }' || true)
    if [ -n "${version}" ]; then
      snap_repo="$(echo "${package_gz}" | grep -Po 'dists/\K[^/]*' || true)"
      echo "    active approx : ${version} (${snap_repo})"
      nothing_found=false
    fi
  done < <(find "${APPROX_HOME}"/ -type f -name 'Packages.*' | sort)

  # Showing current/local apt cache:
  local tmp
  tmp=$(mktemp -t ngcp-approx-snapshots-search-XXXXXXXXXX)
  if apt-cache policy "${PACKAGE_NAME}" | grep "Installed\|Candidate" > "${tmp}"; then
    nothing_found=false

    if [ "$(grep -c "Candidate" "${tmp}")" = "1" ]; then
      echo "    apt candidate : $(grep -Po 'Candidate: \K.*$' "${tmp}")"
    else
      echo "    apt candidate : found multiple matches ($(grep -c "Candidate" "${tmp}") matches)"
    fi

    if [ "$(grep -c "Installed" "${tmp}")" = "1" ]; then
      echo "    apt installed : $(grep -Po 'Installed: \K.*$' "${tmp}")"
    else
      echo "    apt installed : found multiple matches ($(grep -c "Installed" "${tmp}") matches)"
    fi
  fi
  rm -f "${tmp}"

  "${nothing_found:-true}" && echo " Nothing found :-("
}
####################################################################################
# Let the party begin!

main "$@"

exit 0
