#!/bin/bash
set -Eeuo pipefail

if [ "${1:0:1}" = '-' ]; then
  set -- mongod "$@"
fi

originalArgOne="$1"

# allow the container to be started with `--user`
# all mongo* commands should be dropped to the correct user
if [[ "$originalArgOne" == mongo* ]] && [ "$(id -u)" = '0' ]; then
  if [ "$originalArgOne" = 'mongod' ]; then
    find /data/configdb /data/db \! -user mongodb -exec chown mongodb '{}' +
  fi

  # make sure we can write to stdout and stderr as "mongodb"
  # (for our "initdb" code later; see "--logpath" below)
  chown --dereference mongodb "/proc/$$/fd/1" "/proc/$$/fd/2" || :
  # ignore errors thanks to https://github.com/docker-library/mongo/issues/149

  exec gosu mongodb "$BASH_SOURCE" "$@"
fi

dpkgArch="$(dpkg --print-architecture)"
case "$dpkgArch" in
amd64) # https://github.com/docker-library/mongo/issues/485#issuecomment-891991814
  if ! grep -qE '^flags.* avx( .*|$)' /proc/cpuinfo; then
    {
      echo
      echo 'WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!'
      echo '  see https://jira.mongodb.org/browse/SERVER-54407'
      echo '  see also https://www.mongodb.com/community/forums/t/mongodb-5-0-cpu-intel-g4650-compatibility/116610/2'
      echo '  see also https://github.com/docker-library/mongo/issues/485#issuecomment-891991814'
      echo
    } >&2
  fi
  ;;

arm64) # https://github.com/docker-library/mongo/issues/485#issuecomment-970864306
  # https://en.wikichip.org/wiki/arm/armv8#ARMv8_Extensions_and_Processor_Features
  # http://javathunderx.blogspot.com/2018/11/cheat-sheet-for-cpuinfo-features-on.html
  if ! grep -qE '^Features.* (fphp|dcpop|sha3|sm3|sm4|asimddp|sha512|sve)( .*|$)' /proc/cpuinfo; then
    {
      echo
      echo 'WARNING: MongoDB 5.0+ requires ARMv8.2-A or higher, and your current system does not appear to implement any of the common features for that!'
      echo '  see https://jira.mongodb.org/browse/SERVER-55178'
      echo '  see also https://en.wikichip.org/wiki/arm/armv8#ARMv8_Extensions_and_Processor_Features'
      echo '  see also https://github.com/docker-library/mongo/issues/485#issuecomment-970864306'
      echo
    } >&2
  fi
  ;;
esac

# you should use numactl to start your mongod instances, including the config servers, mongos instances, and any clients.
# https://docs.mongodb.com/manual/administration/production-notes/#configuring-numa-on-linux
if [[ "$originalArgOne" == mongo* ]]; then
  numa='numactl --interleave=all'
  if $numa true &>/dev/null; then
    set -- $numa "$@"
  fi
fi

# usage: file_env VAR [DEFAULT]
#    ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
#  "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
  local var="$1"
  local fileVar="${var}_FILE"
  local def="${2:-}"
  if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
    echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
    exit 1
  fi
  local val="$def"
  if [ "${!var:-}" ]; then
    val="${!var}"
  elif [ "${!fileVar:-}" ]; then
    val="$(<"${!fileVar}")"
  fi
  export "$var"="$val"
  unset "$fileVar"
}

# see https://github.com/docker-library/mongo/issues/147 (mongod is picky about duplicated arguments)
_mongod_hack_have_arg() {
  local checkArg="$1"
  shift
  local arg
  for arg; do
    case "$arg" in
    "$checkArg" | "$checkArg"=*)
      return 0
      ;;
    esac
  done
  return 1
}
# _mongod_hack_get_arg_val '--some-arg' "$@"
_mongod_hack_get_arg_val() {
  local checkArg="$1"
  shift
  while [ "$#" -gt 0 ]; do
    local arg="$1"
    shift
    case "$arg" in
    "$checkArg")
      echo "$1"
      return 0
      ;;
    "$checkArg"=*)
      echo "${arg#$checkArg=}"
      return 0
      ;;
    esac
  done
  return 1
}
declare -a mongodHackedArgs
# _mongod_hack_ensure_arg '--some-arg' "$@"
# set -- "${mongodHackedArgs[@]}"
_mongod_hack_ensure_arg() {
  local ensureArg="$1"
  shift
  mongodHackedArgs=("$@")
  if ! _mongod_hack_have_arg "$ensureArg" "$@"; then
    mongodHackedArgs+=("$ensureArg")
  fi
}
# _mongod_hack_ensure_no_arg '--some-unwanted-arg' "$@"
# set -- "${mongodHackedArgs[@]}"
_mongod_hack_ensure_no_arg() {
  local ensureNoArg="$1"
  shift
  mongodHackedArgs=()
  while [ "$#" -gt 0 ]; do
    local arg="$1"
    shift
    if [ "$arg" = "$ensureNoArg" ]; then
      continue
    fi
    mongodHackedArgs+=("$arg")
  done
}
# _mongod_hack_ensure_no_arg '--some-unwanted-arg' "$@"
# set -- "${mongodHackedArgs[@]}"
_mongod_hack_ensure_no_arg_val() {
  local ensureNoArg="$1"
  shift
  mongodHackedArgs=()
  while [ "$#" -gt 0 ]; do
    local arg="$1"
    shift
    case "$arg" in
    "$ensureNoArg")
      shift # also skip the value
      continue
      ;;
    "$ensureNoArg"=*)
      # value is already included
      continue
      ;;
    esac
    mongodHackedArgs+=("$arg")
  done
}
# _mongod_hack_ensure_arg_val '--some-arg' 'some-val' "$@"
# set -- "${mongodHackedArgs[@]}"
_mongod_hack_ensure_arg_val() {
  local ensureArg="$1"
  shift
  local ensureVal="$1"
  shift
  _mongod_hack_ensure_no_arg_val "$ensureArg" "$@"
  mongodHackedArgs+=("$ensureArg" "$ensureVal")
}

# _js_escape 'some "string" value'
_js_escape() {
  jq --null-input --arg 'str' "$1" '$str'
}

: "${TMPDIR:=/tmp}"
jsonConfigFile="$TMPDIR/docker-entrypoint-config.json"
tempConfigFile="$TMPDIR/docker-entrypoint-temp-config.json"
_parse_config() {
  if [ -s "$tempConfigFile" ]; then
    return 0
  fi

  local configPath
  if configPath="$(_mongod_hack_get_arg_val --config "$@")" && [ -s "$configPath" ]; then
    # if --config is specified, parse it into a JSON file so we can remove a few problematic keys (especially SSL-related keys)
    # see https://docs.mongodb.com/manual/reference/configuration-options/
    if grep -vEm1 '^[[:space:]]*(#|$)' "$configPath" | grep -qE '^[[:space:]]*[^=:]+[[:space:]]*='; then
      # if the first non-comment/non-blank line of the config file looks like "foo = ...", this is probably the 2.4 and older "ini-style config format"
      # mongod tries to parse config as yaml and then falls back to ini-style parsing
      # https://github.com/mongodb/mongo/blob/r6.0.3/src/mongo/util/options_parser/options_parser.cpp#L1883-L1894
      echo >&2
      echo >&2 "WARNING: it appears that '$configPath' is in the older INI-style format (replaced by YAML in MongoDB 2.6)"
      echo >&2 '  This script does not parse the older INI-style format, and thus will ignore it.'
      echo >&2
      return 1
    fi
    if [ "$mongoShell" = 'mongo' ]; then
      "$mongoShell" --norc --nodb --quiet --eval "load('/js-yaml.js'); printjson(jsyaml.load(cat($(_js_escape "$configPath"))))" >"$jsonConfigFile"
    else
      # https://www.mongodb.com/docs/manual/reference/method/js-native/#std-label-native-in-mongosh
      "$mongoShell" --norc --nodb --quiet --eval "load('/js-yaml.js'); JSON.stringify(jsyaml.load(fs.readFileSync($(_js_escape "$configPath"), 'utf8')))" >"$jsonConfigFile"
    fi
    if [ "$(head -c1 "$jsonConfigFile")" != '{' ] || [ "$(tail -c2 "$jsonConfigFile")" != '}' ]; then
      # if the file doesn't start with "{" and end with "}", it's *probably* an error ("uncaught exception: YAMLException: foo" for example), so we should print it out
      echo >&2 'error: unexpected "js-yaml.js" output while parsing config:'
      cat >&2 "$jsonConfigFile"
      exit 1
    fi
    jq 'del(.systemLog, .processManagement, .net, .security, .replication)' "$jsonConfigFile" >"$tempConfigFile"
    return 0
  fi

  return 1
}
dbPath=
_dbPath() {
  if [ -n "$dbPath" ]; then
    echo "$dbPath"
    return
  fi

  if ! dbPath="$(_mongod_hack_get_arg_val --dbpath "$@")"; then
    if _parse_config "$@"; then
      dbPath="$(jq -r '.storage.dbPath // empty' "$jsonConfigFile")"
    fi
  fi

  if [ -z "$dbPath" ]; then
    if _mongod_hack_have_arg --configsvr "$@" || {
      _parse_config "$@" &&
        clusterRole="$(jq -r '.sharding.clusterRole // empty' "$jsonConfigFile")" &&
        [ "$clusterRole" = 'configsvr' ]
    }; then
      # if running as config server, then the default dbpath is /data/configdb
      # https://docs.mongodb.com/manual/reference/program/mongod/#cmdoption-mongod-configsvr
      dbPath=/data/configdb
    fi
  fi

  : "${dbPath:=/data/db}"

  echo "$dbPath"
}

if [ "$originalArgOne" = 'mongod' ]; then
  file_env 'MONGO_INITDB_ROOT_USERNAME'
  file_env 'MONGO_INITDB_ROOT_PASSWORD'

  mongoShell='mongo'
  if ! command -v "$mongoShell" >/dev/null; then
    mongoShell='mongosh'
  fi

  # pre-check a few factors to see if it's even worth bothering with initdb
  shouldPerformInitdb=
  if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then
    # if we have a username/password, let's set "--auth"
    _mongod_hack_ensure_arg '--auth' "$@"
    set -- "${mongodHackedArgs[@]}"
    shouldPerformInitdb='true'
  elif [ "$MONGO_INITDB_ROOT_USERNAME" ] || [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then
    cat >&2 <<-'EOF'

			error: missing 'MONGO_INITDB_ROOT_USERNAME' or 'MONGO_INITDB_ROOT_PASSWORD'
			       both must be specified for a user to be created

		EOF
    exit 1
  fi

  if [ -z "$shouldPerformInitdb" ]; then
    # if we've got any /docker-entrypoint-initdb.d/* files to parse later, we should initdb
    for f in /docker-entrypoint-initdb.d/*; do
      case "$f" in
      *.sh | *.js) # this should match the set of files we check for below
        shouldPerformInitdb="$f"
        break
        ;;
      esac
    done
  fi

  # check for a few known paths (to determine whether we've already initialized and should thus skip our initdb scripts)
  if [ -n "$shouldPerformInitdb" ]; then
    dbPath="$(_dbPath "$@")"
    for path in \
      "$dbPath/WiredTiger" \
      "$dbPath/journal" \
      "$dbPath/local.0" \
      "$dbPath/storage.bson"; do
      if [ -e "$path" ]; then
        shouldPerformInitdb=
        break
      fi
    done
  fi

  if [ -n "$shouldPerformInitdb" ]; then
    mongodHackedArgs=("$@")
    if _parse_config "$@"; then
      _mongod_hack_ensure_arg_val --config "$tempConfigFile" "${mongodHackedArgs[@]}"
    fi
    _mongod_hack_ensure_arg_val --bind_ip 127.0.0.1 "${mongodHackedArgs[@]}"
    _mongod_hack_ensure_arg_val --port 27017 "${mongodHackedArgs[@]}"
    _mongod_hack_ensure_no_arg --bind_ip_all "${mongodHackedArgs[@]}"

    # remove "--auth" and "--replSet" for our initial startup (see https://docs.mongodb.com/manual/tutorial/enable-authentication/#start-mongodb-without-access-control)
    # https://github.com/docker-library/mongo/issues/211
    _mongod_hack_ensure_no_arg --auth "${mongodHackedArgs[@]}"
    # "keyFile implies security.authorization"
    # https://docs.mongodb.com/manual/reference/configuration-options/#mongodb-setting-security.keyFile
    _mongod_hack_ensure_no_arg_val --keyFile "${mongodHackedArgs[@]}"
    if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then
      _mongod_hack_ensure_no_arg_val --replSet "${mongodHackedArgs[@]}"
    fi

    # "BadValue: need sslPEMKeyFile when SSL is enabled" vs "BadValue: need to enable SSL via the sslMode flag when using SSL configuration parameters"
    tlsMode='disabled'
    if _mongod_hack_have_arg '--tlsCertificateKeyFile' "$@"; then
      tlsMode='allowTLS'
    fi
    _mongod_hack_ensure_arg_val --tlsMode "$tlsMode" "${mongodHackedArgs[@]}"

    if stat "/proc/$$/fd/1" >/dev/null && [ -w "/proc/$$/fd/1" ]; then
      # https://github.com/mongodb/mongo/blob/38c0eb538d0fd390c6cb9ce9ae9894153f6e8ef5/src/mongo/db/initialize_server_global_state.cpp#L237-L251
      # https://github.com/docker-library/mongo/issues/164#issuecomment-293965668
      _mongod_hack_ensure_arg_val --logpath "/proc/$$/fd/1" "${mongodHackedArgs[@]}"
    else
      initdbLogPath="$(_dbPath "$@")/docker-initdb.log"
      echo >&2 "warning: initdb logs cannot write to '/proc/$$/fd/1', so they are in '$initdbLogPath' instead"
      _mongod_hack_ensure_arg_val --logpath "$initdbLogPath" "${mongodHackedArgs[@]}"
    fi
    _mongod_hack_ensure_arg --logappend "${mongodHackedArgs[@]}"

    pidfile="$TMPDIR/docker-entrypoint-temp-mongod.pid"
    rm -f "$pidfile"
    _mongod_hack_ensure_arg_val --pidfilepath "$pidfile" "${mongodHackedArgs[@]}"

    "${mongodHackedArgs[@]}" --fork

    mongo=("$mongoShell" --host 127.0.0.1 --port 27017 --quiet)

    # check to see that our "mongod" actually did start up (catches "--help", "--version", MongoDB 3.2 being silly, slow prealloc, etc)
    # https://jira.mongodb.org/browse/SERVER-16292
    tries=30
    while true; do
      if ! { [ -s "$pidfile" ] && ps "$(<"$pidfile")" &>/dev/null; }; then
        # bail ASAP if "mongod" isn't even running
        echo >&2
        echo >&2 "error: $originalArgOne does not appear to have stayed running -- perhaps it had an error?"
        echo >&2
        exit 1
      fi
      if "${mongo[@]}" 'admin' --eval 'quit(0)' &>/dev/null; then
        # success!
        break
      fi
      ((tries--))
      if [ "$tries" -le 0 ]; then
        echo >&2
        echo >&2 "error: $originalArgOne does not appear to have accepted connections quickly enough -- perhaps it had an error?"
        echo >&2
        exit 1
      fi
      sleep 1
    done

    if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then
      rootAuthDatabase='admin'

      "${mongo[@]}" "$rootAuthDatabase" <<-EOJS
				db.createUser({
					user: $(_js_escape "$MONGO_INITDB_ROOT_USERNAME"),
					pwd: $(_js_escape "$MONGO_INITDB_ROOT_PASSWORD"),
					roles: [ { role: 'root', db: $(_js_escape "$rootAuthDatabase") } ]
				})
			EOJS
    fi

    export MONGO_INITDB_DATABASE="${MONGO_INITDB_DATABASE:-test}"

    echo
    for f in /docker-entrypoint-initdb.d/*; do
      case "$f" in
      *.sh)
        echo "$0: running $f"
        . "$f"
        ;;
      *.js)
        echo "$0: running $f"
        "${mongo[@]}" "$MONGO_INITDB_DATABASE" "$f"
        echo
        ;;
      *) echo "$0: ignoring $f" ;;
      esac
      echo
    done

    "${mongodHackedArgs[@]}" --shutdown
    rm -f "$pidfile"

    echo
    echo 'MongoDB init process complete; ready for start up.'
    echo
  fi

  # MongoDB 3.6+ defaults to localhost-only binding
  haveBindIp=
  if _mongod_hack_have_arg --bind_ip "$@" || _mongod_hack_have_arg --bind_ip_all "$@"; then
    haveBindIp=1
  elif _parse_config "$@" && jq --exit-status '.net.bindIp // .net.bindIpAll' "$jsonConfigFile" >/dev/null; then
    haveBindIp=1
  fi
  if [ -z "$haveBindIp" ]; then
    # so if no "--bind_ip" is specified, let's add "--bind_ip_all"
    set -- "$@" --bind_ip_all
  fi

  unset "${!MONGO_INITDB_@}"
fi

rm -f "$jsonConfigFile" "$tempConfigFile"

exec "$@"