#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

set -x
set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

true "INFO: Currently running script: ${BASH_SOURCE[0]} $*"

MYDIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" && pwd )"

## BEFORE the 'cd' below: the resolver's cwd branch is captured when this library
## is sourced, and cd-ing first would offer it /usr/bin instead of the tree.
##
## This script may run as the installed '/usr/bin' copy, where a '$MYDIR'-relative
## path cannot locate the tree, so resolve it explicitly. Hardcoding
## "$HOME/derivative-maker" made any build outside that path die HERE -- in the
## release phase, after the whole build, on bash's bare "No such file or directory".
# shellcheck source=../libexec/developer-meta-files/source-tree-lib.bsh
source "${MYDIR}/../libexec/developer-meta-files/source-tree-lib.bsh"
derivative_maker_source_tree_resolve "${MYDIR}" || exit 1

cd -- "${MYDIR}"

## 'helper-scripts' resolves via HELPER_SCRIPTS_PATH (set by help-steps/pre for a
## from-source build); this source runs BEFORE 'pre' below, so fall back to the
## resolved tree when the variable is not in the environment.
##
## TODO: Can't we just import strings.bsh after importing help-steps/pre?
# shellcheck source=../../../helper-scripts/usr/libexec/helper-scripts/strings.bsh
source "${HELPER_SCRIPTS_PATH:-${derivative_maker_source_code_dir}/packages/kicksecure/helper-scripts}/usr/libexec/helper-scripts/strings.bsh"
source "${derivative_maker_source_code_dir}/help-steps/pre"
source "${derivative_maker_source_code_dir}/help-steps/variables"

## Build upload_file_list (reset) for ONE image file: the image plus its
## signatures, torrent and checksums. Called once per (file, location) pair from
## main so each image is uploaded to its own destination.
show_files() {
   local file_name_item="$1"
   upload_file_list=()

   if [ -z "${file_name_item}" ]; then
      return 0
   fi

   test -f "${file_name_item}"

   upload_file_list+=("${file_name_item}")
   upload_file_list+=("${file_name_item}.asc")

   if printf '%s\n' "${file_name_item}.sig" | grep dist-installer ; then
      upload_file_list+=("${file_name_item}.sig")
   else
      if test -f "${file_name_item}.sig" ; then
         upload_file_list+=("${file_name_item}.sig")
      fi
   fi

   upload_file_list+=("${file_name_item}.torrent")
   upload_file_list+=("${file_name_item}.torrent.asc")
   upload_file_list+=("${file_name_item}.torrent.sig")
   upload_file_list+=("${file_name_item}.sha512sums")
   upload_file_list+=("${file_name_item}.sha512sums.asc")
   upload_file_list+=("${file_name_item}.sha512sums.sig")

   true "File list:"
   printf '%s\n' "${upload_file_list[@]}"

   local upload_file_item
   for upload_file_item in "${upload_file_list[@]}"; do
      test -f "${upload_file_item}"
   done
}

## Derive the bare hostname from an rsync target ('<user>@<host>:<path>').
## Named and standalone so the derivation is testable on its own; passing the
## whole target to a resolver yields NXDOMAIN for every entry.
dns_probe_host_from_target() {
   local upload_target probe_host

   upload_target="$1"
   probe_host="${upload_target##*@}"
   probe_host="${probe_host%%:*}"
   printf '%s\n' "${probe_host}"
}

upload_files() {
   if [ "${dist_build_installer_dist}" = "true" ]; then
      dist_server_with_upload_location_item_with_version_maybe="${dist_server_with_upload_location_item}/"
   else
      dist_server_with_upload_location_item_with_version_maybe="${dist_server_with_upload_location_item}/${dist_build_version}/"
   fi
   true "dist_server_with_upload_location_item_with_version_maybe: ${dist_server_with_upload_location_item_with_version_maybe}"

   if [ ! "${build_upload_noninteractive:-}" = "true" ]; then
      true "${cyan}INFO: Press any enter to continue.${reset}"
      read -r temp
   fi

   rsync_dry_run_maybe="--dry-run"

   attempts_counter="0"
   attempts_max="100"

   while true; do
      attempts_counter="$(( attempts_counter + 1 ))"
      ## Linear backoff: each retry waits 5 s longer than the
      ## previous one (5 s, 10 s, 15 s, ..., capping at 5 s * 100 =
      ## 500 s ~= 8 min on the final retry, ~7 h cumulative over
      ## attempts_max=100). Computed from attempts_counter rather
      ## than mutating sleep_seconds, so the value cannot leak
      ## (and grow forever) across calls to upload_files.
      sleep_seconds="$(( attempts_counter * 5 ))"

      if [ "${rsync_dry_run_maybe}" = "--dry-run" ]; then
         true "${cyan}INFO: --dry-run only...${reset}"
      else
         true "${cyan}INFO: uploading...${reset}"
      fi

      ## Test if DNS is functional.
      ## Sometimes 'rsync' fails to resolve DNS but running 'nslookup' beforehand fixes it.
      ## Overrule with '|| true' as the failure would be handled below by 'rsync'.
      ## Use a dedicated loop variable so the current pair's
      ## dist_server_with_upload_location_item (the rsync target) is not clobbered.
      ##
      ## The list holds rsync targets ('<user>@<host>:<path>'), NOT hostnames, so
      ## the whole string must not be handed to nslookup: it then resolves
      ## nothing and reports NXDOMAIN every time, which the '|| true' hid -- the
      ## warm-up never actually warmed anything. Strip the user and the path, and
      ## probe each distinct host once.
      local dns_probe_item dns_probe_host
      local -a dns_probed_host_list
      dns_probed_host_list=()
      for dns_probe_item in ${dist_server_with_upload_location_list}; do
         dns_probe_host="$(dns_probe_host_from_target "${dns_probe_item}")"
         if [ -z "${dns_probe_host}" ]; then
            continue
         fi
         if printf '%s\n' "${dns_probed_host_list[@]}" \
            | grep --line-regexp --fixed-strings -- "${dns_probe_host}" >/dev/null 2>&1; then
            continue
         fi
         dns_probed_host_list+=( "${dns_probe_host}" )
         nslookup "${dns_probe_host}" \
            || printf '%s\n' "${0##*/}: WARNING: DNS probe failed for host '${dns_probe_host}'." >&2
      done

      rsync_exit_code="0"
      ## rsync_dry_run_maybe must stay UNQUOTED. It is "--dry-run" on the first pass and is
      ## set to "" for the second, real pass; quoted, the empty value would be handed to
      ## rsync as an EMPTY ARGUMENT rather than vanishing, and rsync reads that as an empty
      ## source path and fails the upload.
      # shellcheck disable=SC2248
      ${rsync_cmd} \
         ${rsync_dry_run_maybe} \
         ${rsync_opts} \
         "${upload_file_list[@]}" \
         "${dist_server_with_upload_location_item_with_version_maybe}" \
         || { rsync_exit_code="$?" ; true; };

      if [ "${rsync_exit_code}" = "0" ]; then
         if [ "${rsync_dry_run_maybe}" = "--dry-run" ]; then
            if [ ! "${build_upload_noninteractive:-}" = "true" ]; then
               true "${cyan}INFO: Press any enter to continue.${reset}"
               read -r temp
            fi
            rsync_dry_run_maybe=""
            continue
         fi
         break
      fi

      if [ "${attempts_counter}" -ge "${attempts_max}" ]; then
         error "${bold}${red}ERROR: Upload failed.${reset}"
         ## Allow to ignore the error such as after fixing the internet connection.
         continue
      fi

      sleep "${sleep_seconds}" || true

   done
}

main() {
   if [ ! "${dist_build_image_upload_supported}" = "true" ]; then
      if [ "${CI:-}" = "true" ]; then
         true "upload support for this --target is not yet implemented."
         exit 0
      else
         error "upload support for this --target is not yet implemented."
      fi
   fi

   if [ "${dist_build_version}" = "" ]; then
      error "Variable dist_build_version is empty."
   fi

   if [ "${dist_server_with_upload_location_list}" = "" ]; then
      error "Variable dist_server_with_upload_location_list is empty."
   fi

   ## Upload each image to its OWN destination via the parallel arrays
   ## (dist_build_files_to_upload[i] -> dist_build_upload_locations[i]), so a
   ## multi-target build sends each image to its own directory. installer-dist
   ## repeats the file once per server.
   local index
   for index in "${!dist_build_files_to_upload[@]}"; do
      dist_server_with_upload_location_item="${dist_build_upload_locations[${index}]}"
      show_files "${dist_build_files_to_upload[${index}]}"
      ## show_files() returns early with an EMPTY upload_file_list when the image path is
      ## empty (an unset binary_image_*_file still appends an entry, keeping the parallel
      ## arrays the same length). rsync invoked with a destination but no source files just
      ## LISTS the remote directory and exits 0, so the build would read a silent no-op as a
      ## successful upload.
      if [ "${#upload_file_list[@]}" -eq 0 ]; then
         error "${bold}${red}ERROR: no files to upload for index ${index}; the image path is empty.${reset}"
      fi
      ## An empty destination makes the rsync target "/${dist_build_version}/" -- an absolute
      ## LOCAL path. rsync would then copy the images into the build host's filesystem root
      ## instead of uploading them, and still exit 0.
      if [ -z "${dist_server_with_upload_location_item}" ]; then
         error "${bold}${red}ERROR: empty upload destination for index ${index}.${reset}"
      fi
      upload_files
   done

   true
}

main
