#!/bin/bash
#
# wds -- wize deployment services
#
# Well, it more of a stupid name that is easy to type.
#

#
# Following two arrays contain names of git dirs to be deployed.
# Associative arrays would have been more
# clean but for that we have to wait till Bash 4.
#
declare -a app_list
declare -a repo_list
declare -a configvar_list


# default config variable for all dev dynos. TODO : add prod option
configvar_list+=(NPM_CONFIG_PRODUCTION=false)

OPT_VERBOSE=0
OPT_PLUGIN=1
OPT_ORG=
OPT_INFO=
OPT_MAINTENANCE=
OPT_DB=1
OPT_BUILDLOG=
WDS_CONFIG_FILE=wds.conf
WDS_CONFIG_PATH="/etc/${WDS_CONFIG_FILE}"
WDS_TOP_DIR=
WDS_COMMAND=
# this global helps in exiting on signal
WDS_LOCKED_MODULE=

trap clean_exit HUP INT QUIT PIPE TERM

debug() {
    if [[ $OPT_VERBOSE > 0 ]]; then
        echo "DEBUG: $*"
    fi
}

#
# Messages that get printed on practically every use should not go here. But
# the fact that a particular command (add, commit, etc) succeeded is an
# exception.
#
info() {
        echo "== [$WDS_TOP_DIR] $*"
}

warn() {
    echo "WARNING: $*"
}

error() {
    echo "ERROR: $*"
}

die() {
    echo "FATAL: $*"
    exit 1
}

complain_error() {
    local err=0
    local msg

    err=$1; shift; msg="$*"
    (( $err != 0 )) && error $msg

    return $err
}

clean_exit() {
    #unlock_cache
    exit
}

jsofy(){
local arr=();
local k

  while [[ $# -gt 0 ]]
  do
      arr=("${arr[@]}" $1 $2 )
      shift
      shift
  done

  vars=(${arr[@]})
  len=${#arr[@]}

  printf "{\n"
  for (( k=0; k<len; k+=2 ))
  do
      printf "\"${vars[k]}\": \"${vars[k+1]}\""
      if [ $k -lt $((len-2)) ] ; then
          printf ",\n"
      fi
  done
  printf "\n}\n"
}


run_silent() {
    local err=0
    local cwd="$(pwd)"

    debug "PWD=$cwd ; COMMAND=$@"
    $@ > /dev/null 2>&1
    err=$?
    return $err
}

check_dir_exists() {
    if [[ -d "$1" ]]; then
        return 0
    fi

    return 1
}

check_git_dir_exists() {
    check_dir_exists "$1" && return 0
    error "Code directory does not exist: $1"
    return 1
}

check_gitcmd_present(){
    local err=0
    run_silent "git --version" || err=1
    return $err
}

check_herokucli_present(){
    local err=0
    run_silent "heroku --version" || err=1

    return $err
}

check_heroku_login(){
    local err=0
    run_silent "heroku auth:token" || err=1

    return $err
}

set_heroku_config_var(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    cd "${m}"
    for ((k=0; k < ${#configvar_list[@]}; k++)); do
        configvar=${configvar_list[$k]}
        run_silent "heroku config:set ${configvar}"  || err=1
    done
    cd "${cwd}"

    return $err
}

add_heroku_config_var(){
    local configvar=$1
    configvar_list+=(${configvar})
}

check_git_init() {
    local m=$1
    local tag=$2
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    run_silent "git rev-parse --is-inside-work-tree" || err=1
    #git rev-parse --is-bare-repository > /dev/null || err=1
    complain_error $err "Dir is not a git working repo ${m}"
    cd "${cwd}"

    return $err
}

get_gits_ready(){
    git_map_init "${@}"
    local git_repo
    local err=0
    local i=0

    for ((i=0; i < ${#repo_list[@]}; i++)); do
        git_repo=${repo_list[$i]}
        check_git_dir_exists "${git_repo}" || return 1
        if [[ -n "$OPT_INFO" || -n "$OPT_MAINTENANCE" ]]; then
            debug "Skipped git operation for ${git_repo}"
        else
          check_git_init "${git_repo}" || do_git_init "${git_repo}"
          do_git_add "${git_repo}" && (  check_git_sync "${git_repo}" || do_git_autocommit "${git_repo}" )
        fi
        app_list+=("${git_repo}")   # TODO option to select only re-synced app for deployment
        debug "Added app dir=${git_repo}"
    done

    return $err
}

deploy_app(){
    local git_repo
    local err=0
    local server_url=

    for ((i=0; i < ${#app_list[@]}; i++)); do
        heroku_app=${app_list[$i]}

        if [[ -n "$OPT_MAINTENANCE" ]] && [[ ${OPT_MAINTENANCE} =~ "stop" ]]; then
            stop_heroku_app "${heroku_app}" || err=1
            return_heroku_info "${heroku_app}" || err=1
            continue
        elif [[ -n "$OPT_MAINTENANCE" ]] && [[ ${OPT_MAINTENANCE} =~ "start" ]]; then
            start_heroku_app "${heroku_app}" || err=1
            return_heroku_info "${heroku_app}" || err=1
            continue
        fi

        if [[ -n "$OPT_INFO" ]]; then
            return_heroku_info "${heroku_app}" || err=1
            continue
        fi

        is_heroku_app "${heroku_app}" || create_heroku_app "${heroku_app}"
        set_heroku_config_var "${heroku_app}"

        if is_repo_server "${heroku_app}" ;then
          if [[ ${OPT_DB} != 0 ]]; then
            check_pg_attached "${heroku_app}" || attach_pg "${heroku_app}"
          else
            info "Skipping DB operations"
          fi
        server_url=$(get_heroku_app_url "${heroku_app}")
        add_heroku_config_var "SERVERURI=${server_url}"
        fi

        start_heroku_app  "${heroku_app}" || err=1
        heroku_push "${heroku_app}" || err=128
        complain_error $err "Error in heroku deployment."
        return_heroku_info "${heroku_app}" || err=2
    done

    return $err
}
return_heroku_info(){
    local m=$1
    local err=0
    local str
    local cwd="$(pwd)"
    local status
    declare -a output
    cd "${m}"
    status="$(status_heroku_app ${m})"
    str="$(heroku apps:info)"
    output+=(TARGET_APP "$(echo ${m}|tr -d '[:space:]')" )
    output+=(STATUS ${status})
    output+=(WEB_URL $(echo "${str}"|sed -e '/Web URL/!d' -e 's/\(.*\)\s*\(http.*\)/\2/'))
    output+=(APP_NAME $(echo "${str}"| sed -e '/Git URL/!d' -e 's/\(.*\)\/\(.*\)/\2/' -e 's/\(.*\)\.\(.*\)/\1/'))
    jsofy ${output[@]}
    cd "${cwd}"

    return $err
}

do_git_init() {
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    run_silent "git init" ||  die "Couldn't init git module ${m}"
    cd "${cwd}"

    return $err
}

do_git_add(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    run_silent "git add -A" || die "Cannot add files in git repo ${m}"
    cd "${cwd}"

    return $err
}

check_git_sync(){
    local m=$1
    local tag=$2
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    ( git diff --exit-code && git diff --cached --exit-code )>/dev/null || err=1
    complain_error $err "Git repo not synced ${m}"
    cd "${cwd}"

    return $err
}

do_git_autocommit(){
    local m=$1
    local tag=$2
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    GIT_STATUS_MESG=$(git -c color.status=false status | sed -n -e '1,/Changes to be committed:/ d' \
        -e '1,1 d' \
        -e '/^Untracked files:/,$ d' \
        -e 's/^\s*//' \
        -e '/./p')
    git commit -m "${GIT_STATUS_MESG}" || err=1
    complain_error $err "Git auto commit failed ${m}"
    cd "${cwd}"

    return $err
}

is_repo_server(){
    local m=$1
    local err=0
    cd "${m}"
    #[[ -e server.js ]] && grep -q postgresql server.js || echo client
    if [[ -e server.js ]];then
    debug "git repo ${m} is a server"
    else
    err=1
    fi

    return $err
}

check_pg_attached(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    heroku pg:info|grep -q Version || err=1
    cd "${cwd}"

    return $err
}

attach_pg(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    run_silent "heroku addons:create heroku-postgresql:hobby-dev" || err=1
    complain_error $err "Unable to add postgres addon in ${m}"
    cd "${cwd}"

    return $err
}

is_heroku_app(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    run_silent "git remote show heroku" || err=1
    cd "${cwd}"

    return $err
}

get_heroku_app_url(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    output=$(heroku apps:info|sed -e '/Web URL/!d' -e 's/\(.*\)\s*\(http.*\)/\2/')
    echo ${output}
    cd "${cwd}"

    return $err
}

get_heroku_app_id(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    output=$(heroku apps:info| sed -e '/Git URL/!d' -e 's/\(.*\)\/\(.*\)/\2/' -e 's/\(.*\)\.\(.*\)/\1/')
    echo ${output}
    cd "${cwd}"

    return $err
}

heroku_push(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    if [[ -n "${OPT_BUILDLOG}" ]];then
    git push heroku master || err=1
    else
    run_silent "git push heroku master" || err=1
    fi
    cd "${cwd}"

    return $err
}

create_heroku_app(){
    local m=$1
    local err=0
    local cwd="$(pwd)"
    local output
    cd "${m}"
    if [[ -n ${OPT_ORG} ]];then
    run_silent "heroku create -o ${OPT_ORG} " || err=1
    else
    run_silent "heroku create" || err=1
    fi
    cd "${cwd}"

    return $err
}

start_heroku_app(){
  local m=$1
  local err=0
  local cwd="$(pwd)"
  local output
  cd "${m}"
  run_silent "heroku maintenance:off" || err=1
  cd "${cwd}"

  return $err
}

status_heroku_app(){
  local m=$1
  local err=0
  local cwd="$(pwd)"
  local output
  cd "${m}"
  mstatus=$(heroku maintenance)
  if [[ $mstatus =~ "off" ]]; then
    echo "Running"
  else
    echo "Stopped"
  fi
  cd "${cwd}"

  return $err
}
stop_heroku_app(){
  local m=$1
  local err=0
  local cwd="$(pwd)"
  local output
  cd "${m}"
  run_silent "heroku maintenance:on" || err=1
  cd "${cwd}"

  return $err
}

#
# Ignoring suffix is a convenience to skip the slash (/) that is automatically
# provided by Bash in the command-line.
#
array_linear_search_ignoring_suffix() {
    local s=$1
    local needle=$2
    local hay

    (( $# > 2 )) || return 1

    shift 2
    for hay; do
        [[ ${hay%$s} == ${needle%$s} ]] && return 0
    done

    return 1
}

in_array() {
    local needle=$1
    local hay

    shift
    for hay; do
        [[ $hay == $needle ]] && return 0
    done

    return 1
}

wds_top_dir_set() {
    local d

    d=$(pwd -P .)
    WDS_TOP_DIR=${d##*/}
}

#
# Determine if the current word takes a parameter.
#
# A single function that knows all the arguments under any command (status,
# log, etc) that expects a parameter. This helps skip to the module names.
#
module_map_init_arg_with_param() {
    [[ $1 == "--tag" ]]
}

git_map_init() {
    local err=0
    local i=0
    local line

    # consume command options
    while [[ $1 =~ ^--.* || $1 =~ ^-.* ]]; do
        # one extra shift for arguments with parameters
        #module_map_init_arg_with_param "$1" && shift
        shift
    done

    while [[ $# > 0 ]] ; do
      repo_list[$i]=$1
      debug "Added git dir=$1"
      ((i++))
      shift
    done

    return $err
}

usage() {
    local prog="${0##*/}"
    cat << BIGBANG
usage: $prog [OPTIONS] TARGET [GIT_REPOS]...

Perform deployment tasks for Wize submodules.
  -h|--help         Display this help and exit
  -e|--env          Env variables to be put in target apps
  -o|--org          Organisation under which app be created
  -v|--verbose      Print debug messages
  --logs            Print build logs too
  --no-db           disable DB creation with App
  --stop            Put the app in maintenance mode
  --start           Put the app again in running mode
  --info            Print info about app related to repo

Possible deploy targets are:
  heroku       Visit https://dashboard.heroku.com/apps for dashboard
  azure        Coming Soon.

BIGBANG
    exit
}

# In Git, return upstream branch for a given local branch.
git_upstream() {
    local b=$1

    git for-each-ref --format='%(refname:short) %(upstream:short)' \
            refs/heads/$b \
        | awk '{print $2}'
}


is_detached_head() {
    local branch

    branch=$(git branch | grep '*')

    if [[ $branch == "* (no branch)" ]]; then
        return 0
    fi

    return 1
}

# Check that there are no uncommitted changes.
is_clean_work_dir() {
    local module=$1

    if [[ -n "$(git status -s --untracked-files=no)" ]]; then
        warn "You have uncommitted changes in $module."
        return 1
    fi

    return 0
}

save_changes() {
    if git stash &> /dev/null; then
        debug "Saved local changes."
    else
        error "Could not save local changes."
        return 1
    fi
}

undo_save_changes() {
    if git stash pop --quiet; then
        debug "Undoing saved changes."
    else
        warn "Could not undo saved changes."
    fi
}



cache_disable() {
    FEATURE_CACHE_ENABLED=0
}

cache_enabled() {
    local ret_val=1

    if [[ $FEATURE_CACHE_ENABLED == 1 ]]; then
        ret_val=0
    fi

    return $ret_val
}

read_config() {
    local path=$WDS_CONFIG_PATH
    local err=0

    if [[ -r $path ]]; then
        . $path
    else
        die "Could not read config file: $path"
    fi

    return $err
}

check_target_deploy(){
  local err=0
	if [[  "$WDS_COMMAND" == "heroku" ]]; then
		check_herokucli_present || die "Heroku cli not present. do npm install -g heroku-cli"
		check_heroku_login || die "Heroku login auth token not found"
	fi

	return $err
}

while [[ $# -gt 0 ]];do
    key="$1"
    case $key in
        -v|--verbose)
        OPT_VERBOSE=1
        debug "Debugging on"
        ;;

        heroku|azure)
        debug "Deployment target is $key"
        WDS_COMMAND=$key
        ;;

        -e|--env)
        debug "Adding env var ${2}"
        configvar_list+=("${2}")
        shift
        ;;

        -o|--org)
        debug "Adding Organisation var ${2}"
        OPT_ORG="${2}"
        shift
        ;;

        -h|--help)
        usage
        ;;

        --no-db)
        debug "DB actions will be skipped in server repo"
        OPT_DB=0
        ;;

        --stop)
        debug "Will stop the target app"
        OPT_MAINTENANCE="stop"
        ;;

        --start)
        debug "Will start the target app"
        OPT_MAINTENANCE="start"
        ;;

        --info)
        debug "Will show info only"
        OPT_INFO=1
        ;;

        --logs)
        debug "Will show build logs"
        OPT_BUILDLOG=1
        ;;
    *)
        debug "Input DIR $1"
        break
        ;;
    esac
    shift
done


if [[ -z "$WDS_COMMAND" ]]; then
    die "No command given. Try \"${0##*/} --help\"."
fi

check_gitcmd_present || die "Git binary is not installed on system"
check_target_deploy || die "Target deploy $WDS_COMMAND is not setup"
get_gits_ready "$@" || die "Failed to setup git"
deploy_app "$@" || die "Failed to deploy apps"
