#!/bin/bash
#
# Download releases of items using the TRUD API.


PROGRAM_NAME="$(basename "$0")"
readonly PROGRAM_NAME

PROGRAM_BUG_RECIPIENT='information.standards@nhs.net'
readonly PROGRAM_BUG_RECIPIENT

PROGRAM_VERSION_LABEL='1.1.0'
readonly PROGRAM_VERSION_LABEL

PACKAGE_NAME='TRUD tools'
readonly PACKAGE_NAME

VERBOSITY=0


lib::available() {
  hash "$1" 2>/dev/null
}


lib::available_or_error_exit() {
  local dependency
  for dependency in "$@"; do
    lib::available "${dependency}" || \
      lib::error_exit "Missing dependency: $_"
  done
}


lib::check_natural_number() {
  [[ $1 =~ ^[[:digit:]]+$ ]]
}


lib::download_release_file() {
  local -r item_id="$1"
  shift
  local -r release_ordinal="$1"
  shift
  local -r release="$1"
  shift
  local -r file_json_prefix="$1"
  shift
  local file_type="$1"

  [[ -z ${file_type} ]] && \
    file_type="${file_json_prefix}"
  readonly file_type

  local -r file_url=$(jq --raw-output ".${file_json_prefix}FileUrl" <<< "${release}")
  [[ -z ${file_url} ]] && \
    lib::error_exit "Missing ${file_type} file URL for release ${release_ordinal} of item ${item_id}."

  local -r file_name=$(jq --raw-output ".${file_json_prefix}FileName" <<< "${release}")
  [[ -z ${file_name} ]] && \
    lib::error_exit "Missing ${file_type} file name for release ${release_ordinal} of item ${item_id}."

  ((VERBOSITY < 2)) && \
    local -r curl_options='--silent'

  lib::info "Downloading item ${item_id} ${file_type} file: ${file_name}"

  curl --location ${curl_options:+${curl_options}} "${file_url}" --output "${file_name}"
}


lib::error() {
  lib::log error "$@"
}


lib::error_exit() {
  lib::error "$@"
  exit 1
}


lib::get_options() {

  local -r short_options="$(lib::trim "$1")"
  shift

  local -r long_options="$(lib::trim "$1")"
  shift

  local options
  if ! options="$(POSIXLY_CORRECT=1 getopt \
      --name "${PROGRAM_NAME}" \
      --options "${short_options}" \
      --longoptions "${long_options}" \
      -- "$@" \
      )"; then
    exit
  fi
  echo "${options}"
}


lib::info() {
  lib::log info "$@"
}


lib::log() {
  local -r message_log_level_lowercase="${1,,}"
  lib::valid_log_level_or_error_exit "${message_log_level_lowercase}"
  local verbosity_level="${VERBOSITY:-0}"
  lib::check_natural_number "${verbosity_level}" || \
    lib::error_exit "Verbosity level is not a natural number: ${verbosity_level}"
  ((verbosity_level > 3)) && \
    verbosity_level=3
  readonly verbosity_level
  case "${verbosity_level}-${message_log_level_lowercase}" in
    0-error|0-warn) ;;
    1-error|1-warn|1-info) ;;
    2-error|2-warn|2-info|2-debug) ;;
    3-error|3-warn|3-info|3-debug|3-trace) ;;
    *) return ;;
  esac
  shift
  echo "${PROGRAM_NAME}: ${message_log_level_lowercase/warn/warning}: $*" >&2
}


lib::plural() {
  if (($2 == 1)); then
    echo "$1"
  else
    echo "$1s"
  fi
}


lib::populate_id_array() {

  local -n id_array_reference="$1"
  shift
  local -r id_type="$1"
  shift

  id_array_reference=()
  local -A id_associative_array
  local id

  for id in "$@"; do

    id="$(lib::trim "${id}")"
    [[ -z ${id} ]] && \
      lib::error_exit "Blank ${id_type} id."

    lib::check_natural_number "${id}" || \
      lib::error_exit "Not a natural number: ${id_type} id: ${id}"

    [[ ${id_associative_array[${id}]} == seen ]] && \
      lib::error_exit "Duplicate ${id_type} id: ${id}"
    id_associative_array["${id}"]='seen'

    id_array_reference+=("${id}")

  done
}


lib::trim() {
  if ! shopt -q extglob; then
    local -r set_extglob_flag=true
    shopt -s extglob
  fi
  local -r all_arguments="$*"
  local -r no_leading_spaces="${all_arguments##+([[:space:]])}"
  local -r no_trailing_spaces="${no_leading_spaces%%+([[:space:]])}"
  echo "${no_trailing_spaces}"
  [[ ${set_extglob_flag} == true ]] && \
    shopt -u extglob
}


lib::usage_error_exit() {
  echo "Try ${PROGRAM_NAME} --help for more information." >&2
  exit 2
}


lib::usage_error_help_exit() {
  [[ -z $1 ]] && \
    lib::error_exit "Too few arguments to function: ${FUNCNAME[0]}"
  lib::error "$1"
  shift
  if [[ -n $1 ]]; then
    echo "$@" >&2
  else
    echo "Try ${PROGRAM_NAME} --help for more information." >&2
  fi
  exit 2
}


lib::usage_help_exit() {
  [[ -z $1 ]] && \
    lib::error_exit "Too few arguments to function: ${FUNCNAME[0]}"
  echo "$@"
  exit 0
}


lib::valid_log_level_or_error_exit() {
  local -r log_level_lowercase="${1,,}"
  case "${log_level_lowercase}" in
    trace|debug|info|warn|error) ;;
    *) lib::error_exit "Invalid log level: $1"
  esac
}


lib::variable_populated() {
  [[ -n ${!1} ]]
}


lib::variable_populated_or_error_exit() {
  declare -p "$1" &>/dev/null || \
    lib::error_exit "Missing environment variable: $1"
  lib::variable_populated "$1" || \
    lib::error_exit "Empty environment variable: $1"
}


lib::version_exit() {
  local package_name
  lib::variable_populated PACKAGE_NAME && \
    package_name=" (${PACKAGE_NAME}) "
  readonly package_name

  echo "${PROGRAM_NAME}${package_name}${PROGRAM_VERSION_LABEL}"

  exit 0
}


main() {

  local -r usage="\
Usage: ${PROGRAM_NAME} [OPTION]... ITEM-ID...
Download releases of item ITEM-ID using the TRUD API.
The API key must be stored in the environment variable TRUD_API_KEY.

  -l, --latest       download only the latest release
  -c, --checksum     also download the checksum file
  -s, --signature    also download the signature file
  -p, --public-key   also download the public key file
  -v, --verbose      print more information about progress (cumulative)
      --help         display this help and exit
      --version      output version information and exit

Report bugs to ${PROGRAM_BUG_RECIPIENT}"


  # Parse the command line.

  local latest_flag
  local checksum_flag
  local signature_flag
  local public_key_flag

  eval set -- "$(lib::get_options \
    'lcspv' \
    'latest,checksum,signature,public-key,verbose,help,version' \
    "$@")"
  while true; do
    case "$1" in
      -l|--latest)
        shift
        latest_flag='true'
        ;;
      -c|--checksum)
        shift
        checksum_flag='true'
        ;;
      -s|--signature)
        shift
        signature_flag='true'
        ;;
      -p|--public-key)
        shift
        public_key_flag='true'
        ;;
      -v|--verbose)
        shift
        VERBOSITY="$((${VERBOSITY:-0} + 1))"
        ;;
      --help) lib::usage_help_exit "${usage}" ;;
      --version) lib::version_exit ;;
      --) break ;;
      *) lib::usage_error_exit ;;
    esac
  done
  shift

  readonly \
    latest_flag \
    checksum_flag \
    signature_flag \
    public_key_flag


  # Validate the arguments.

  (($# < 1)) && \
    lib::usage_error_help_exit 'Too few arguments.' "${usage}"

  local item_id_array
  lib::populate_id_array item_id_array 'item' "$@"
  readonly item_id_array


  # Validate the environment.

  lib::variable_populated_or_error_exit TRUD_API_KEY

  local -r api_key="${TRUD_API_KEY,,}"

  ((${#api_key} != 40)) && \
    lib::error_exit "API key length (${#api_key}) is not 40 characters: ${TRUD_API_KEY}"

  [[ ! ${api_key} =~ ^[[:xdigit:]]{40}$ ]] && \
    lib::error_exit "API key contains non-hexadecimal characters: ${TRUD_API_KEY}"


  # Check for dependencies.
  lib::available_or_error_exit \
    curl \
    jq \
    tac


  # For each item.
  local item_id
  local response
  local -r fqdn="${TRUD_API_FQDN:-isd.digital.nhs.uk}"
  local api_options
  [[ ${latest_flag} = true ]] && \
    api_options='?latest'
  declare -i release_ordinal
  for item_id in "${item_id_array[@]}"; do

    # Get release information for this item.
    response="$(curl --location --silent "https://${fqdn}/trud/api/v1/keys/${api_key}/items/${item_id}/releases${api_options}")"
    message=$(jq --raw-output '.message' <<< "${response}" 2>/dev/null)
    [[ -z ${message} ]] && \
      lib::error_exit "Missing message in the release list endpoint response."
    [[ ${message} != OK ]] && \
      lib::error_exit "${message}"

    # For each release of this item.
    release_ordinal=0
    while IFS= read -r release; do

      release_ordinal+=1

      # Download the zip file.
      lib::download_release_file "${item_id}" "${release_ordinal}" "${release}" 'archive'

      # Download the checksum file if requested.
      [[ ${checksum_flag} = true ]] && \
        lib::download_release_file "${item_id}" "${release_ordinal}" "${release}" 'checksum'

      # Download the signature file if requested.
      [[ ${signature_flag} = true ]] && \
        lib::download_release_file "${item_id}" "${release_ordinal}" "${release}" 'signature'

      # Download the public key file if requested.
      [[ ${public_key_flag} = true ]] && \
        lib::download_release_file "${item_id}" "${release_ordinal}" "${release}" 'publicKey' 'public key'

    done < <(jq --compact-output '.releases[]' <<< "${response}" | tac)

    ((release_ordinal == 0)) && \
      lib::error_exit "No releases found for item ${item_id}."

    lib::info "Downloaded ${release_ordinal} $(lib::plural 'release' ${release_ordinal}) of item ${item_id}."

  done
}


main "$@"
