#!/bin/bash
# Bash strict mode: http://redsymbol.net/articles/unofficial-bash-strict-mode/
set -euo pipefail

# ======================================================
# ENVIRONMENT VARIABLES USED DIRECTLY BY THIS ENTRYPOINT
# ======================================================

export BASEROW_VERSION="1.24.2"

# Used by docker-entrypoint.sh to start the dev server
# If not configured you'll receive this: CommandError: "0.0.0.0:" is not a valid port number or address:port pair.
BASEROW_BACKEND_PORT="${BASEROW_BACKEND_PORT:-8000}"

# Database environment variables used to check the Postgresql connection.
DATABASE_USER="${DATABASE_USER:-baserow}"
DATABASE_HOST="${DATABASE_HOST:-db}"
DATABASE_PORT="${DATABASE_PORT:-5432}"
DATABASE_NAME="${DATABASE_NAME:-baserow}"
DATABASE_PASSWORD="${DATABASE_PASSWORD:-baserow}"
DATABASE_OPTIONS="${DATABASE_OPTIONS:-}"
# Or you can provide a Postgresql connection url
DATABASE_URL="${DATABASE_URL:-}"

BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS="${BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS:-5}"

# Backend server related variables
MIGRATE_ON_STARTUP=${MIGRATE_ON_STARTUP:-true}
SYNC_TEMPLATES_ON_STARTUP=${SYNC_TEMPLATES_ON_STARTUP:-true}
BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION=${BASEROW_TRIGGER_SYNC_TEMPLATES_AFTER_MIGRATION:-$SYNC_TEMPLATES_ON_STARTUP}
BASEROW_BACKEND_BIND_ADDRESS=${BASEROW_BACKEND_BIND_ADDRESS:-0.0.0.0}
BASEROW_BACKEND_LOG_LEVEL=${BASEROW_BACKEND_LOG_LEVEL:-INFO}
BASEROW_ENABLE_SECURE_PROXY_SSL_HEADER=${BASEROW_ENABLE_SECURE_PROXY_SSL_HEADER:-}

BASEROW_AMOUNT_OF_WORKERS=${BASEROW_AMOUNT_OF_WORKERS:-}
BASEROW_AMOUNT_OF_GUNICORN_WORKERS=${BASEROW_AMOUNT_OF_GUNICORN_WORKERS:-3}

# Celery related variables
BASEROW_RUN_MINIMAL=${BASEROW_RUN_MINIMAL:-}
BASEROW_CELERY_BEAT_STARTUP_DELAY=${BASEROW_CELERY_BEAT_STARTUP_DELAY:-15}
BASEROW_CELERY_BEAT_DEBUG_LEVEL=${BASEROW_CELERY_BEAT_DEBUG_LEVEL:-INFO}


# ======================================================
# HELPER FUNCTIONS
# ======================================================

postgres_ready() {
  if [ -z "$DATABASE_URL" ]; then
DATABASE_NAME=$DATABASE_NAME \
DATABASE_USER=$DATABASE_USER \
DATABASE_HOST=$DATABASE_HOST \
DATABASE_PORT=$DATABASE_PORT \
DATABASE_PASSWORD=$DATABASE_PASSWORD \
DATABASE_OPTIONS=$DATABASE_OPTIONS \
python3 << END
import sys
import psycopg2
import json
import os
DATABASE_NAME=os.getenv('DATABASE_NAME')
DATABASE_USER=os.getenv('DATABASE_USER')
DATABASE_HOST=os.getenv('DATABASE_HOST')
DATABASE_PORT=os.getenv('DATABASE_PORT')
DATABASE_PASSWORD=os.getenv('DATABASE_PASSWORD')
DATABASE_OPTIONS=os.getenv('DATABASE_OPTIONS')
try:
    options = json.loads(DATABASE_OPTIONS or "{}")
    psycopg2.connect(
        dbname=DATABASE_NAME,
        user=DATABASE_USER,
        password=DATABASE_PASSWORD,
        host=DATABASE_HOST,
        port=DATABASE_PORT,
        **options
    )
except Exception as e:
    print(f"Error: Failed to connect to the postgresql database at {DATABASE_HOST}")
    print("Please see the error below for more details:")
    print(e)
    print("Trying again without any DATABASE_OPTIONS:")
    try:
      psycopg2.connect(
          dbname=DATABASE_NAME,
          user=DATABASE_USER,
          password=DATABASE_PASSWORD,
          host=DATABASE_HOST,
          port=DATABASE_PORT,
      )
    except Exception as e:
      print(f"Error: Failed to connect to the postgresql database at {DATABASE_HOST} without the {DATABASE_OPTIONS}")
      print("Please see the error below for more details:")
      print(e)
      sys.exit(-1)
sys.exit(0)
END
else
  echo "Checking the provided DATABASE_URL"
DATABASE_URL=$DATABASE_URL \
python3 << END
import sys
import psycopg2
import os
DATABASE_URL=os.getenv('DATABASE_URL')
try:
    psycopg2.connect(
        DATABASE_URL
    )
except psycopg2.OperationalError as e:
    print(f"Error: Failed to connect to the postgresql database at {DATABASE_URL}")
    print("Please see the error below for more details:")
    print(e)
    sys.exit(-1)
sys.exit(0)
END
  fi
}

wait_for_postgres() {
for i in $( seq 0 "$BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS" )
do
  if ! postgres_ready; then
    echo "Waiting for PostgreSQL to become available attempt " \
         "$i/$BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS ..."
    sleep 2
  else
    echo 'PostgreSQL is available'
    return 0
  fi
done
echo 'PostgreSQL did not become available in time...'
exit 1
}



show_help() {
    echo """
The available Baserow backend related commands, services and healthchecks are shown
below:

ADMIN COMMANDS:
setup           : Runs all setup commands (migrate and update_formulas)
manage          : Manage Baserow and its database
bash            : Start a bash shell with the correct env setup
backup          : Backs up Baserow's database to DATA_DIR/backups by default
restore         : Restores Baserow's database restores from DATA_DIR/backups by default
python          : Run a python command
shell           : Start a Django Python shell
shell           : Start a Django Python shell
wait_for_db     : Waits BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS attempts for the
                  configured db to become available.
install-plugin  : Installs a plugin (append --help for more info).
uninstall-plugin: Un-installs a plugin (append --help for more info).
list-plugins    : Lists currently installed plugins.
help            : Show this message

SERVICE COMMANDS:
gunicorn            : Start Baserow backend django using a prod ready gunicorn server:
                         * Waits for the postgres database to be available first
                           checking BASEROW_POSTGRES_STARTUP_CHECK_ATTEMPTS times (default 5)
                           before exiting.
                         * Automatically migrates the database on startup unless
                           MIGRATE_ON_STARTUP is set to something other than 'true'.
                         * Binds to BASEROW_BACKEND_BIND_ADDRESS which defaults to 0.0.0.0
gunicorn-wsgi       : Same as gunicorn but runs a wsgi server which does not support WS
celery-worker       : Start the celery worker queue which runs important async tasks
celery-exportworker : Start the celery worker queue which runs slower async tasks
celery-beat         : Start the celery beat service used to schedule periodic jobs

HEALTHCHECK COMMANDS (exit with non zero when unhealthy, zero when healthy)
backend-healthcheck             : Checks the gunicorn/django-dev service health
celery-worker-healthcheck       : Checks the celery-worker health
celery-exportworker-healthcheck : Checks the celery-exportworker health

DEV COMMANDS (most will only work in the baserow_backend_dev image):
django-dev      : Start a normal Baserow backend django development server, performs
                  the same checks and setup as the gunicorn command above.
lint-shell      : Run the linting (only available if using dev target)
lint            : Run the linting and exit (only available if using dev target)
test:           : Run the tests (only available if using dev target)
ci-test:        : Run the tests for ci including various reports (dev only)
ci-check-startup: Start up a single gunicorn and timeout after 10 seconds for ci (dev)
watch-py CMD    : Auto reruns the provided CMD whenever python files change
install-plugin  : Installs a baserow plugin.
"""
}

run_setup_commands_if_configured(){
  startup_plugin_setup
  if [ "$MIGRATE_ON_STARTUP" = "true" ] ; then
    if [ "${BASEROW_DISABLE_LOCKED_MIGRATIONS:-}" = "true" ] ; then
      migration_command="migrate"
    else
      migration_command="locked_migrate"
    fi
    echo "python /baserow/backend/src/baserow/manage.py $migration_command"
    OTEL_SERVICE_NAME=backend-migrate python /baserow/backend/src/baserow/manage.py "$migration_command"
  fi
}

start_celery_worker(){
  startup_plugin_setup
  if [[ -n "$BASEROW_RUN_MINIMAL" ]]; then
    EXTRA_CELERY_ARGS=(--without-heartbeat --without-gossip --without-mingle)
  else
    EXTRA_CELERY_ARGS=()
  fi
  if [[ -n "$BASEROW_AMOUNT_OF_WORKERS" ]]; then
    EXTRA_CELERY_ARGS+=(--concurrency "$BASEROW_AMOUNT_OF_WORKERS")
  fi
  exec celery -A baserow worker "${EXTRA_CELERY_ARGS[@]}" -l INFO "$@"
}

# Lets devs attach to this container running the passed command, press ctrl-c and only
# the command will stop. Additionally they will be able to use bash history to
# re-run the containers command after they have done what they want.
attachable_exec(){
    echo "$@"
    exec bash --init-file <(echo "history -s $*; $*")
}

run_backend_server(){
  wait_for_postgres
  run_setup_commands_if_configured

  if [[ -n "$BASEROW_ENABLE_SECURE_PROXY_SSL_HEADER" ]]; then
    EXTRA_GUNICORN_ARGS=(--forwarded-allow-ips='*')
  else
    EXTRA_GUNICORN_ARGS=()
  fi

  if [[ "$1" = "wsgi" ]]; then
    STARTUP_ARGS=(baserow.config.wsgi:application)
  elif [[ "$1" = "asgi" ]]; then
    STARTUP_ARGS=(-k uvicorn.workers.UvicornWorker baserow.config.asgi:application)
  else
    echo -e "\e[31mUnknown run_backend_server argument $1 \e[0m" >&2
    exit 1
  fi
  # Gunicorn args explained in order:
  #
  # 1. See https://docs.gunicorn.org/en/stable/faq.html#blocking-os-fchmod for
  #    why we set worker-tmp-dir to /dev/shm by default.
  # 2. Log to stdout
  # 3. Log requests to stdout
  exec gunicorn --workers="$BASEROW_AMOUNT_OF_GUNICORN_WORKERS" \
    --worker-tmp-dir "${TMPDIR:-/dev/shm}" \
    --log-file=- \
    --access-logfile=- \
    --capture-output \
    "${EXTRA_GUNICORN_ARGS[@]}" \
    -b "${BASEROW_BACKEND_BIND_ADDRESS:-0.0.0.0}":"${BASEROW_BACKEND_PORT}" \
    --log-level="${BASEROW_BACKEND_LOG_LEVEL}" \
    "${STARTUP_ARGS[@]}" \
    "${@:2}"
}

setup_otel_vars(){
  # These key value pairs will be exported on every log/metric/trace by any otel
  # exporters running in subprocesses launched by this script.
  EXTRA_OTEL_RESOURCE_ATTRIBUTES="service.namespace=Baserow,"
  EXTRA_OTEL_RESOURCE_ATTRIBUTES+="service.version=${BASEROW_VERSION},"
  EXTRA_OTEL_RESOURCE_ATTRIBUTES+="deployment.environment=${BASEROW_DEPLOYMENT_ENV:-unknown}"

  if [[ -n "${OTEL_RESOURCE_ATTRIBUTES:-}" ]]; then
    # If the container has been launched with some extra otel attributes, make sure not
    # to override them with our Baserow specific ones.
    OTEL_RESOURCE_ATTRIBUTES="${EXTRA_OTEL_RESOURCE_ATTRIBUTES},${OTEL_RESOURCE_ATTRIBUTES}"
  else
    OTEL_RESOURCE_ATTRIBUTES="$EXTRA_OTEL_RESOURCE_ATTRIBUTES"
  fi
  export OTEL_RESOURCE_ATTRIBUTES
  echo "OTEL_RESOURCE_ATTRIBUTES=$OTEL_RESOURCE_ATTRIBUTES"
}

# ======================================================
# COMMANDS
# ======================================================

if [[ -z "${1:-}" ]]; then
  echo "Must provide arguments to docker-entrypoint.sh"
  show_help
  exit 1
fi

source /baserow/venv/bin/activate
source /baserow/plugins/utils.sh

setup_otel_vars

case "$1" in
    django-dev)
        wait_for_postgres
        run_setup_commands_if_configured
        echo "Running Development Server on 0.0.0.0:${BASEROW_BACKEND_PORT}"
        echo "Press CTRL-p CTRL-q to close this session without stopping the container."
        export OTEL_SERVICE_NAME=backend-dev
        attachable_exec python /baserow/backend/src/baserow/manage.py runserver "${BASEROW_BACKEND_BIND_ADDRESS:-0.0.0.0}:${BASEROW_BACKEND_PORT}"
    ;;
    django-dev-no-attach)
        wait_for_postgres
        run_setup_commands_if_configured
        echo "Running Development Server on 0.0.0.0:${BASEROW_BACKEND_PORT}"
        export OTEL_SERVICE_NAME=backend-dev
        python /baserow/backend/src/baserow/manage.py runserver "${BASEROW_BACKEND_BIND_ADDRESS:-0.0.0.0}:${BASEROW_BACKEND_PORT}"
    ;;
    gunicorn)
      export OTEL_SERVICE_NAME="backend-asgi"
      run_backend_server asgi "${@:2}"
    ;;
    gunicorn-wsgi)
      export OTEL_SERVICE_NAME="backend-wsgi"
      run_backend_server wsgi "${@:2}"
    ;;
    backend-healthcheck)
      echo "Running backend healthcheck..."
      curlf() {
        HTTP_CODE=$(curl --silent -o /dev/null --write-out "%{http_code}" --max-time 10 "$@")
        if [[ ${HTTP_CODE} -lt 200 || ${HTTP_CODE} -gt 299 ]] ; then
          return 22
        fi
        return 0
      }
      curlf "http://localhost:$BASEROW_BACKEND_PORT/api/_health/"
    ;;
    bash)
        exec /bin/bash -c "${@:2}"
    ;;
    manage)
        export OTEL_SERVICE_NAME=backend-manage
        exec python3 /baserow/backend/src/baserow/manage.py "${@:2}"
    ;;
    python)
        exec python3 "${@:2}"
    ;;
    setup)
      echo "python3 /baserow/backend/src/baserow/manage.py migrate"
      OTEL_SERVICE_NAME=backend-migrate DONT_UPDATE_FORMULAS_AFTER_MIGRATION=yes python3 /baserow/backend/src/baserow/manage.py migrate
      echo "python3 /baserow/backend/src/baserow/manage.py update_formulas"
      OTEL_SERVICE_NAME=backend-update-formulas python3 /baserow/backend/src/baserow/manage.py update_formulas
    ;;
    shell)
        export OTEL_SERVICE_NAME=backend-shell
        exec python3 /baserow/backend/src/baserow/manage.py shell
    ;;
    lint-shell)
        attachable_exec make lint-python
    ;;
    lint)
        exec make lint-python
    ;;
    ci-test)
        exec make ci-test-python PYTEST_SPLITS="${PYTEST_SPLITS:-1}" PYTEST_SPLIT_GROUP="${PYTEST_SPLIT_GROUP:-1}" PYTEST_EXTRA_ARGS="${*:2}"
    ;;
    ci-check-startup)
        exec make ci-check-startup-python
    ;;
    celery-worker)
      if [[ -n "${BASEROW_RUN_MINIMAL}" && $BASEROW_AMOUNT_OF_WORKERS == "1" ]]; then
        export OTEL_SERVICE_NAME="celery-worker-combined"
        echo "Starting combined celery and export worker..."
        start_celery_worker -Q celery,export -n default-worker@%h "${@:2}"
      else
        export OTEL_SERVICE_NAME="celery-worker"
        start_celery_worker -Q celery -n default-worker@%h "${@:2}"
      fi
    ;;
    celery-worker-healthcheck)
      echo "Running celery worker healthcheck..."
      exec celery -A baserow inspect ping -d "default-worker@$HOSTNAME" -t 10 "${@:2}"
    ;;
    celery-exportworker)
      if [[ -n "${BASEROW_RUN_MINIMAL}" && $BASEROW_AMOUNT_OF_WORKERS == "1" ]]; then
        echo "Not starting export worker as the other worker will handle both queues " \
             "to reduce memory usage"
        while true; do sleep 2073600; done
      else
        export OTEL_SERVICE_NAME="celery-exportworker"
        start_celery_worker -Q export -n export-worker@%h "${@:2}"
      fi
    ;;
    celery-exportworker-healthcheck)
      echo "Running celery export worker healthcheck..."
      exec celery -A baserow inspect ping -d "export-worker@$HOSTNAME" -t 10 "${@:2}"
    ;;
    celery-beat)
      # Delay the beat startup as there seems to be bug where the other celery workers
      # starting up interfere with or break the lock obtained by it. Without this the
      # second time beat extends its lock it will crash and have to be restarted.
      echo "Sleeping for $BASEROW_CELERY_BEAT_STARTUP_DELAY before starting beat to prevent "\
           "startup errors."
      sleep "$BASEROW_CELERY_BEAT_STARTUP_DELAY"
      export OTEL_SERVICE_NAME="celery-beat"
      exec celery -A baserow beat -l "${BASEROW_CELERY_BEAT_DEBUG_LEVEL}" -S redbeat.RedBeatScheduler "${@:2}"
    ;;
    watch-py)
        # Ensure we watch all possible python source code locations for changes.
        directory_args=''
        for i in $(echo "$PYTHONPATH" | tr ":" "\n")
        do
          directory_args="$directory_args -d=$i"
        done

        attachable_exec watchmedo auto-restart "$directory_args" --pattern="*.py" --recursive -- bash "${BASH_SOURCE[0]} ${*:2}"
    ;;
    backup)
        if [[ -n "$DATABASE_URL" ]]; then
          echo -e "\e[31mThe backup command is currently incompatible with DATABASE_URL, "\
            "please set the DATABASE_{HOST,USER,PASSWORD,NAME,PORT} variables manually"\
            " instead. \e[0m" >&2
          exit 1
        fi
        if [[ -n "${DATA_DIR:-}" ]]; then
          cd "$DATA_DIR"/backups || true
        fi
        export PGPASSWORD=$DATABASE_PASSWORD
        exec python3 /baserow/backend/src/baserow/manage.py backup_baserow \
            -h "$DATABASE_HOST" \
            -d "$DATABASE_NAME" \
            -U "$DATABASE_USER" \
            -p "$DATABASE_PORT" \
            "${@:2}"
    ;;
    restore)
        if [[ -n "$DATABASE_URL" ]]; then
          echo -e "\e[31mThe restore command is currently incompatible with DATABASE_URL, "\
            "please set the DATABASE_{HOST,USER,PASSWORD,NAME,PORT} variables manually"\
            " instead. \e[0m" >&2
          exit 1
        fi
        if [[ -n "${DATA_DIR:-}" ]]; then
          cd "$DATA_DIR"/backups || true
        fi
        export PGPASSWORD=$DATABASE_PASSWORD
        exec python3 /baserow/backend/src/baserow/manage.py restore_baserow \
            -h "$DATABASE_HOST" \
            -d "$DATABASE_NAME" \
            -U "$DATABASE_USER" \
            -p "$DATABASE_PORT" \
            "${@:2}"
    ;;
    wait_for_db)
      wait_for_postgres
    ;;
    install-plugin)
      exec /baserow/plugins/install_plugin.sh --runtime "${@:2}"
    ;;
    uninstall-plugin)
      exec /baserow/plugins/uninstall_plugin.sh "${@:2}"
    ;;
    list-plugins)
      exec /baserow/plugins/list_plugins.sh "${@:2}"
    ;;
    *)
        echo "Command given was $*"
        show_help
        exit 1
    ;;
esac
