#!/usr/bin/env bash

# When a `SIGINT` or `SIGTERM` is received, stop all background jobs and exit
# cleanly.
# https://github.com/hecticjeff/shoreman/blob/master/shoreman.sh
# http://steve-parker.org/sh/trap.shtml
#
__graceful_stop() {
  local _message="Stopping edeliver..."
  hint_message "\n$_message" &&
  __log "$_message" &&
  kill ${background_jobs_pids[*]} 2>&1 &&
  local _message="EDELIVER STOPPED!"
  success_message "\n$_message\n" &&
  __log "$_message" &&
  __log "===========================================================" &&
  exit 0
}

# Ensures args (in this case hosts) are space separated so that we can easily loop over them
# e.g. " ruby-1,ruby-2 " -> "ruby-1 ruby-2"
#
__remote_friendly() {
  echo "$@" | tr ',' ' ' | sed 's/^ //' | sed 's/ $//'
}

# If these values are specified at the runtime, hang onto them
# until we're ready to overwrite everything set from other sources
#
__capture_runtime_configs() {
  # DEPRECATE as of v0.8.0
  RUNTIME_SERVERS="$(__remote_friendly "$SERVER $SERVERS $HOSTS")"
  RUNTIME_HOSTS="$RUNTIME_SERVERS"

  RUNTIME_GIT_PUSH="$GIT_PUSH"
  # DEPRECATE as of v0.8.0
  RUNTIME_REFSPEC="${BRANCH:=$REFSPEC}"
  RUNTIME_REVISION="$REVISION"
  RUNTIME_LOG_FILE="$LOG_FILE"
  RUNTIME_APP="$APP"
  RUNTIME_STRATEGY="$STRATEGY"
}

# Overwrite the following configs if they have been specified at runtime:
#
__apply_runtime_configs() {
  if [ -n "$RUNTIME_HOSTS" ]
  then
    HOSTS="$RUNTIME_HOSTS"
  fi

  if [ -n "$RUNTIME_GIT_PUSH" ]
  then
    GIT_PUSH="$RUNTIME_GIT_PUSH"
  fi

  if [ -n "$RUNTIME_REFSPEC" ]
  then
    REFSPEC="$RUNTIME_REFSPEC"
  fi

  if [ -n "$RUNTIME_REVISION" ]
  then
    REVISION="$RUNTIME_REVISION"
  fi

  if [ -n "$RUNTIME_LOG_FILE" ]
  then
    LOG_FILE="$RUNTIME_LOG_FILE"
  fi

  if [ -n "$RUNTIME_APP" ]
  then
    APP="$RUNTIME_APP"
  fi

  if [ -n "$RUNTIME_STRATEGY" ]
  then
    STRATEGY="$RUNTIME_STRATEGY"
  fi
}

# Loads the .deliver config file if it exists
# This is now optional. All config values can be passed at runtime, or, even
# better you can create a project-specific deploy (or even cap deploy) which is
# just a function calling deliver behind the scenes. Imagine the following:
#
#   deploy() {
#     APP=awesome HOSTS=ruby-1 PORT=5000 deliver
#   }
#
__load_app_config() {
  if [ -f "$APP_CONFIG" ]
  then
    source "$APP_CONFIG"
    if [[ -n "$HOST" ]]; then
        [[ "$HOST" = *"@"* ]] && BUILD_USER=${HOST%%@*} && STAGING_USER=${HOST%%@*} && PRODUCTION_USER=${HOST%%@*} && HOST=${HOST##*@}
        SERVER="$HOST" && STAGING_HOSTS="$HOST" && PRODUCTION_HOSTS="$HOST" && BUILD_HOST="$HOST"
    fi
  fi
}

# Ensures all required configs are set, tries to be helpful by adding some
# sane defaults
#
__default_app_config() {
  source "$BASE_PATH/libexec/app_config"
}

# Builds an index of all available strategies
#
__find_all_strategies() {
  local strategies_path=()

  # There are strategies that came with deliver itself
  if [ -d "$BASE_PATH/strategies" ]; then
    strategies_path+=("$BASE_PATH/strategies")
  fi
  # Per-project strategies
  if [ -d "$ORIGIN_DIR/.deliver/strategies" ]; then
    strategies_path+=("$ORIGIN_DIR/.deliver/strategies")
  fi

  STRATEGIES=$(find "${strategies_path[@]}" -regex '^[a-zA-Z0-9@_/.-]*$' -type f ! -iname 'readme*')
  for strategy in $STRATEGIES
  do
    STRATEGIES_NAME="$STRATEGIES_NAME ${strategy##*/}"
  done
}

# Load the correct strategy
#
__load_strategy() {
  for strategy in $STRATEGIES
  do
    [[ ! $strategy =~ $STRATEGY ]] && continue
    source "$strategy" && strategy_loaded=true && break
  done

  if [ -z "$strategy_loaded" ]
  then
    error_message "\n'$STRATEGY' strategy does not exist"
    __available_strategies
    exit 1
  fi
}

# Appends app user to all hosts so that we can log in with this user
# on specific remote jobs.
#
__remote_hosts() {
  for _host in $HOSTS
  do
    HOSTS_APP_USER="$HOSTS_APP_USER,$APP_USER@$_host"
  done
  HOSTS="$(__remote_friendly $HOSTS)"
  HOSTS_APP_USER="$(__remote_friendly $HOSTS_APP_USER)"
}

__check_config() {
  __display_deprecations

  set -f # disables file globbing
  local _missing_required_configs=()

  if [[ -n "$CHECK" ]]
  then
    echo ""
  fi

  for _required_config in ${REQUIRED_CONFIGS[@]}
  do
    local _required_config_value="$(echo $(eval echo \${$_required_config[@]}))"
    if [ -z "$_required_config_value" ]
    then
      _missing_required_configs+=("$_required_config")
      local _config_text="${txtred}"
      local _check_failed=true
    else
      local _config_text="${txtgrn}"
    fi
    # http://unstableme.blogspot.co.uk/2009/07/text-alignment-with-awk-printf-function.html
    # http://linuxconfig.org/bash-printf-syntax-basics-with-examples
    # http://stackoverflow.com/questions/6345429/how-do-i-print-some-text-in-bash-and-pad-it-with-spaces-to-a-certain-width
    # http://wiki.bash-hackers.org/commands/builtin/printf
    if [[ -n "$CHECK" ]]
    then
      printf "%-30s\t%s\n" "${_config_text}$_required_config" "$_required_config_value${txtrst}"
    fi
  done

  if [[ -n "$CHECK" ]]
  then
    hint_message "-----------------------------------------------------------"
  fi

  for _optional_config in ${OPTIONAL_CONFIGS[@]}
  do
    local _optional_config_value="$(echo $(eval echo \${$_optional_config[@]}))"
    if [ -n "$CHECK" ] && [ -n "$_optional_config_value" ]
    then
      printf "%-40s\t%s\n" "${bldylw}$_optional_config" "$_optional_config_value${txtrst}"
    fi
  done
  set +f # re-enables file globbing

  if [ -n "$_check_failed" ]
  then
    error_message "\nEDELIVER IS MISSING THE FOLLOWING CONFIG(S):\n\n${_missing_required_configs[@]}\n"
    exit 1
  else
    if [ -n "$CHECK" ]
    then
      success_message "\nREADY TO DELIVER!\n"
      exit 0
    fi
  fi
}

# Mode aware:
# * COMPACT - silences jobs
# * TEST - captures all jobs, no local or remote changes
#
__exec() {
  eval "$1 $SILENCE"
}

# Waits until all background jobs finish successfully
# If any of them fail, the entire script fails
#
__monitor_background_jobs() {
  for (( i = 0 ; i < ${#background_jobs_pids[@]} ; i++ ))
  do
    wait ${background_jobs_pids[$i]}
    local _exit_status="$?"
    if [[ "$_exit_status" != 0 ]]
    then
      local _host="${background_jobs_hosts[$i]}"
      local _remote_command="${background_jobs[$i]}"

      error "\nA remote command failed on:

  $_host

Output of the command is shown above if the --verbose option
was used and the commands executed on that remote host are
printed below for debugging purposes:

FAILED with exit status $_exit_status:\n\n${_remote_command}\n"
    fi
  done

  unset background_jobs_pids
  unset background_jobs_hosts
  unset background_jobs
}

# Runs commands in parallel, as background jobs
#
__parallelize() {
  local _job="$1"
  local _hosts="${2:-"$HOSTS_APP_USER"}"

  background_job_pids=()
  background_jobs=()

  for _host in $_hosts
  do
    local _logged_job="$(eval echo $_job)"
    if [[ $MODE = "verbose" ]]
    then
      echo "$_logged_job"
    fi

    ( eval "$_job" ) &
    # WON'T WORK because it captures eval's pid, not the job's pid
    # Ideally, I want to stay away from eval
    background_job_pids+=("$!")
    background_jobs+=("$_logged_job")
  done

  __monitor_background_jobs
}

# Multi-host & mode aware
#
__remote() {
  local _remote_job="$1"
  local _hosts="${2:-"$HOSTS_APP_USER"}"
  # some remote jobs are too confusing to print in the error message,
  # e.g. the aws up/download job that contains the contents from the
  # libexec/aws file, so the remote job to print in the error description
  # can be passed as third parameter
  local _error_info_for_remote_job="${3:-"$_remote_job"}"
  background_jobs_pids=()
  background_jobs_hosts=()
  background_jobs=()

  __log "${_hosts} : $_remote_job"

  for _host in $_hosts
  do
    ssh -o ConnectTimeout="$SSH_TIMEOUT" "$_host" "$_remote_job $SILENCE" &
    background_jobs_pids+=("$!")
    local _background_job="ssh -o ConnectTimeout=$SSH_TIMEOUT $_host $_remote_job $SILENCE"
    background_jobs+=("ssh -o ConnectTimeout=$SSH_TIMEOUT $_host $_error_info_for_remote_job $SILENCE")
    background_jobs_hosts+=("$_host")
  done

  __monitor_background_jobs
}


# execute on single remote host
# not in background
__sync_remote() {
  local _remote_job="$1"
  local _hosts="${2:-"$HOSTS_APP_USER"}"
  __log "${_hosts} : $_remote_job"
  [[ $(wc -w <<< "$_hosts") -gt 1 ]] && error "Sync commands can be executed on one host only, but serveral hosts are configured ($_hosts)."
  local _host="$_hosts"
  if [[ "$VERBOSE" = "true" ]]; then
    ssh -t -A -S none -q -o ConnectTimeout="$SSH_TIMEOUT" "$_host" "$_remote_job" || error "\nA remote command failed on:

  $_host

Output of the command is shown above and the command executed
on that host is printed below for debugging purposes:

FAILED with exit status $?:\n${_remote_job}\n"
  else
    local _output _status
    _output=$(ssh -A -S none -o ConnectTimeout="$SSH_TIMEOUT" "$_host" "$_remote_job" 2>&1)
    _status=$?
    [[ "$_status" -ne 0 ]] && echo "$_output" && error "\nA remote command failed on:

  $_host

Output of the command is shown above and the command executed
on that host is printed below for debugging purposes:

FAILED with exit status $_status:\n${_remote_job}\n"
  fi
}

__sync_local() {
  local _local_job="$1"
  local _hosts="${2:-"$HOSTS_APP_USER"}"
  __log "local : $__local_job"
  [[ $(wc -w <<< "$_hosts") -gt 1 ]] && error "Sync commands can be executed on one host only, but serveral hosts are configured ($_hosts)."
  if [[ "$VERBOSE" = "true" ]]; then
    $_local_job || error "\nFAILED $?:\n${_local_job}\n"
  else
    local _output _status
    _output=$($_local_job 2>&1)
    _status=$?
    [[ "$_status" -ne 0 ]] && echo "$_output" && error "\nFAILED $_status:\n${_local_job}\n"
  fi
}

# monitors the parent pid if edeliver is started by a mix task.
# this is required because edeliver is started from an erlang node
# using "Port.open({:spawn, edeliver_command}, options)" and will
# not detect if the erlang node is stopped using ctrl+c because
# erlang uses its own break handler. it also kills al running
# child pids which are usually ssh connections to build or
# deploy servers.
__monitor_parent_pid() {
  local _edeliver_script_pid="$$"
  local _parent_pid="$(ps -o ppid= -p $_edeliver_script_pid)"
  local _child_pid
  [[ "$_parent_pid" =~ ^[0-9]+$ ]] || return 0
  while kill -0 $_edeliver_script_pid >/dev/null 2>&1; do
    if kill -0 $_parent_pid >/dev/null 2>&1; then
      sleep 0.5
    else
      for _child_pid in $(ps ax -o pid= -o ppid= | grep -e "${_edeliver_script_pid}\$" | grep -oe '^[[:digit:]]\+[^ ]' | sort -rn); do
         [[ "$_child_pid" =~ ^[0-9]+$ ]] && kill -9 $_child_pid >/dev/null 2>&1
      done
      kill -9 $_edeliver_script_pid >/dev/null 2>&1
    fi
  done
}

# Logs all events (including formatting styles) so that people can tail,
# inspect and use for general debugging.
#
__log() {
  local _line="$(date) ::: $@"

  echo -e "$_line" >> $LOG_FILE
}

# Used for pre & post hooks
# Runs if functions are defined
#
__exec_if_defined() {
  local _function="$1"
  shift
  declare -F "$_function" > /dev/null && eval "$_function $@"
}
