#!/bin/bash
# Purpose: *.patchtt functionality for ngcpcfg config/templates
################################################################################

set -e
set -u

# support testsuite
FUNCTIONS="${FUNCTIONS:-/usr/share/ngcp-ngcpcfg/functions/}"
HELPER="${HELPER:-/usr/share/ngcp-ngcpcfg/helper/}"
SCRIPTS="${SCRIPTS:-/usr/share/ngcp-ngcpcfg/scripts/}"

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

cd "${NGCPCTL_MAIN}"

## functions {{{
patch_help() {
  export TIME_FORMAT=''
  log_info "'ngcpcfg patch' walks through all templates searching for '*.patchtt.tt2' files"
  log_info "and generates '*.customtt.tt2' files based on the original template."
  log_info "The option '--from-customtt' simplifies migration from customtt to patchtt."
  log_info ""
  log_info "Sample:"
  log_info "  ngcpcfg patch [--help | --from-customtt [<customtt file(s)>] | <patchtt file(s)>]"
  log_info ""
  log_info "Run 'man ngcpcfg' for more information."
}

patch_search() {
  local name="$1"
  shift # remove the first 'patchtt/customtt'
  local files=("$@")
  local fileslist
  fileslist=$(mktemp -t ngcpcfg-patch-search.XXXXXXXXXX)

  if [ -z "${NGCP_IS_MULTI_SITE:-}" ] && [ -e /etc/default/ngcp-roles ]; then
    # shellcheck disable=SC1091
    . /etc/default/ngcp-roles
  fi
  # Only defined on PRO/Carrier
  : "${NGCP_IS_MULTI_SITE:=no}"

  local regexp=""
  local hosts=()
  if [ -n "${HOST_FILE:-}" ]; then
    hosts+=("${HOST_FILE}")
  fi
  if [ -n "${PAIR_FILE:-}" ]; then
    hosts+=("${PAIR_FILE}")
  fi
  if [ -n "${NODE_FILE:-}" ]; then
    hosts+=("${NODE_FILE}")
  fi
  for val in "${hosts[@]}"; do
    regexp="${regexp}|\\${val}"
  done
  local awk_regexp=".*${name}\.tt2$"
  if [ -n "${regexp}" ]; then
    awk_regexp=".*${name}\.tt2(${regexp#|})?$"
  fi
  log_debug "Searching for ${name} files (requested '${files[*]}') awk_regexp:${awk_regexp}"

  declare -a template_base_dirs
  # For single-site mode
  for dir in ${CONFIG_POOL}; do
    template_base_dirs+=("${TEMPLATE_POOL_BASE%/}/${dir#/}")
  done

  if [ "${NGCP_IS_MULTI_SITE}" = "yes" ]; then
    log_debug "Operating in multi-site mode"
    for site_dir in "${SITES_DIR}"/*; do
      for dir in ${CONFIG_POOL}; do
        template_base_dirs+=("${site_dir}/templates/${dir#/}")
      done
    done
  fi

  for dir in "${template_base_dirs[@]}"; do
    [ -n "${dir}" ] || log_error "${dir} doesn't exist"
    log_debug "Iterate over all files in ${dir}"
    while read -r file ; do
      if [ "${#files[@]}" = "0" ] ; then
        log_debug "NO arguments provided via cmdline"
        log_debug "Found ${name} '${file}'"
        echo "${file}" >> "${fileslist}"
      else
        # arguments (file list/pattern) provided via cmdline
        for arg in "${files[@]}"; do
          if echo "${file}" | grep -q -- "${arg}" ; then
            log_debug "Processing ${name} '${file}' as requested"
            echo "${file}" >> "${fileslist}"
          fi
        done
      fi
    done < <(find "${dir}" -regextype awk -iregex "${awk_regexp}")
  done

  # output patch list, make sure we provide the file names just once
  sort -u "${fileslist}"

  if [ -n "${DEBUG:-}" ] ; then
    # send to stderr since stdout is used from outside
    log_debug "Not removing temporary fileslist files since we are in debug mode:" >&2
    log_debug "  fileslist = ${fileslist}" >&2
  else
    rm -f "${fileslist}"
  fi
}

patch_validate_patch() {
  local patch="$1"
  log_debug "Validating patch: '${patch}'"

  if [ ! -f "${patch}" ] ; then
    log_error "Missing patch file '${patch}'"
    bad_files+=("${patch}")
    return 1
  fi

  local template="${patch%%.patchtt*}.tt2"

  if [ -f "${template}" ] ; then
    log_debug "Found template for the patch: '${template}'"
  else
    log_error "Missing template '${template}'"
    log_error "       for patch '${patch}'"
    bad_files+=("${patch}")
    return 1
  fi

  local customtt="${patch//.patchtt/.customtt}"
  if [ -f "${customtt}" ] ; then
    log_debug "Overwriting customtt '${customtt}'"
  else
    log_debug "Not found customtt for the patch: '${customtt}'"
  fi
}

patch_apply() {
  local patch="$1"
  local apply="${2:-false}"
  local template="${patch%%.patchtt*}.tt2"
  local customtt="${patch//.patchtt/.customtt}"
  local patch_output
  patch_output=$(mktemp -t ngcpcfg-patch-out.XXXXXXXXXX)

  local patch_opts=()
  patch_opts+=("--input=${patch}")
  patch_opts+=("--prefix=/dev/null")  # do not produce .orig backup files
  patch_opts+=("--reject-file=-")     # do not produce .rej file
  patch_opts+=(-F0 -N)              # disable fuzzy logic to be as safe as possible
  if "${apply}" ; then
    patch_opts+=("--output=${customtt}")
  else
    patch_opts+=(--dry-run)
    patch_opts+=("--output=/dev/null")
  fi

  log_debug "Generating customtt '${customtt}' from '${patch}' (apply=${apply})"

  log_debug "Executing: patch ${patch_opts[*]} ${template}"
  if patch "${patch_opts[@]}" "${template}" >"${patch_output}" 2>&1 ; then
    if "${apply}" ; then
      log_info "Successfully created '${customtt}'"
    else
      log_debug "Patch '${patch}' can be applied"
    fi
    good_files+=("${patch}")
  else
    log_error "The patch '${patch}' cannot be applied:"
    cat "${patch_output}" >&2
    bad_files+=("${patch}")
  fi

  if [[ -f "${template}" && -f "${customtt}" ]] ; then
    chmod --reference="${template}" "${customtt}"
    chown --reference="${template}" "${customtt}"
  fi
  rm -f "${patch_output}"
}

patch_validate_customtt() {
  local file="$1"
  log_debug "Validating customtt: '${file}'"

  if [ ! -f "${file}" ] ; then
    log_error "Missing customtt file '${file}'"
    bad_files+=("${file}")
    return 1
  fi

  local template="${file%%.customtt*}.tt2"

  if [ -f "${template}" ] ; then
    log_debug "Found template for the customtt: '${template}'"
  else
    log_error "Missing template for customtt '${customtt}'"
    bad_files+=("${customtt}")
    return 1
  fi

  local patchtt="${file//.customtt/.patchtt}"
  if [ -f "${patchtt}" ] ; then
    log_debug "Overwriting patchtt '${patchtt}'"
  else
    log_debug "Not found patchtt for the customtt: '${patchtt}'"
  fi
}

patch_import_customtt() {
  local customtt_file="$1"
  local apply="${2:-false}"
  local template="${customtt_file%%.customtt*}.tt2"
  local patchtt="${customtt_file//.customtt/.patchtt}"

  local tmp_patchtt
  tmp_patchtt=$(mktemp -t ngcpcfg-patch-import.XXXXXXXXXX)
  local diff_output
  diff_output=$(mktemp -t ngcpcfg-patch-diff.XXXXXXXXXX)


  # diff exit status is 0 if inputs are the same, 1 if different, 2 if trouble.
  log_debug "Generating temporary patchtt '${tmp_patchtt}' from template '${template}' and customtt '${customtt_file}'"
  case "$(diff -u "${template}" "${customtt_file}" > "${tmp_patchtt}" 2>"${diff_output}" && echo $? || echo $?)" in
    0)
      log_error "No difference between customtt '${customtt_file}' and template '${template}' (patchtt is not necessary here, remove or change customtt file)"
      bad_files+=("${customtt_file}")
      ;;
    1)
      log_debug "Successfully processed diff for customtt '${customtt_file}'"
      good_files+=("${customtt_file}")
      ;;
    2)
      log_error "Diff failed between template '${template}' and customtt file '${customtt_file}':"
      cat "${diff_output}" >&2
      bad_files+=("${customtt_file}")
      ;;
    *)
      log_error "We should not be here. Aborting (better safe then sorry)."
      exit 1
      ;;
  esac

  if "${apply}" ; then
    log_info "Creating patchtt file '${patchtt}'"
    log_debug "Removing first 2 lines (filename + date) from 'diff -u' output (due to time changes on '--from-customtt')"
    tail -n +3 "${tmp_patchtt}" > "${patchtt}"
  fi

  rm -f "${tmp_patchtt}"
  rm -f "${diff_output}"
}

patch_import() {
  mapfile -t files < <(patch_search "customtt" "$@")

  log_debug "Validating customtt files"
  for customtt in "${files[@]}" ; do
    log_info "Validating customtt '${customtt}'"
    if patch_validate_customtt "${customtt}" ; then
      patch_import_customtt "${customtt}" "false"
    fi
  done

  if [ "${#bad_files[@]}" != "0" ] ; then
    log_debug "Aborted here due to failed patch validation above"
    return
  fi

  log_debug "Validating patchtt files from customtt"
  for customtt in "${files[@]}" ; do
    patch_import_customtt "${customtt}" "true"
  done
}

patch_patch() {
  mapfile -t files < <(patch_search "patchtt" "$@")

  for patch in "${files[@]}" ; do
    log_info "Validating patch '${patch}'"
    if patch_validate_patch "${patch}" ; then
      patch_apply "${patch}" "false"
    fi
  done

  if [ "${#bad_files[@]}" != "0" ] ; then
    log_debug "Aborted here due to failed patch validation above"
    return
  fi

  for patch in "${files[@]}" ; do
    log_info "Applying patch '${patch}'"
    patch_apply "${patch}" "true"
  done
}

patch_footer() {
  local label="$1"

  if [ "${#bad_files[@]}" = "0" ] ; then
    if [ "${#good_files[@]}" = "0" ] ; then
      log_info "No ${label} files found, nothing to patch."
    else
      log_info "Requested ${label} operation has finished successfully."
    fi
  else
    log_error "Some operations above finished with an error for the file(s):"
    mapfile -t bad_files_unique < <(echo "${bad_files[@]}" | tr ' ' '\n' | sort -u)
    printf '\t%s\n' "${bad_files_unique[@]}"
    RC=1
  fi
}
## }}}

declare -a bad_files=()
declare -a good_files=()

patch_type=patchtt

while [ -n "${1:-}" ] ; do
  case "${1:-}" in
    --help)
      patch_help
      exit 0
      ;;
    --from-customtt)
      patch_type=customtt
      shift
      ;;
    *)
      break
      ;;
  esac
done

if [ "${patch_type}" = 'patchtt' ]; then
  patch_patch "$@"
else
  patch_import "$@"
fi
patch_footer "${patch_type}"

exit "${RC:-0}"

## END OF FILE #################################################################
