#!/usr/bin/env bash

########################################################################
# PUID/PGID support
#
# When the container runs as root (e.g. --user root), the pgadmin user
# is reassigned to the requested UID/GID and all initialization +
# gunicorn run via su-exec as that user.
#
# When the container runs as non-root (default USER 5050, or OpenShift
# random UID), PUID/PGID are ignored and everything runs as the
# current user.
########################################################################

PUID=${PUID:-5050}
PGID=${PGID:-0}

# Validate PUID/PGID are numeric and in acceptable range
if ! echo "$PUID" | grep -qE '^[0-9]+$'; then
    echo "ERROR: PUID must be a numeric value, got '$PUID'"
    exit 1
fi
if ! echo "$PGID" | grep -qE '^[0-9]+$'; then
    echo "ERROR: PGID must be a numeric value, got '$PGID'"
    exit 1
fi
if [ "$PUID" -eq 0 ]; then
    echo "ERROR: PUID=0 (root) is not allowed. Use a non-root UID."
    exit 1
fi

if [ "$(id -u)" = "0" ]; then
    # Ensure a group with the target GID exists
    if ! getent group "$PGID" > /dev/null 2>&1; then
        if ! addgroup -g "$PGID" pggroup; then
            echo "ERROR: Failed to create group with GID=$PGID"
            exit 1
        fi
    fi

    # Reassign the pgadmin user to the desired UID/GID
    if ! usermod -o -u "$PUID" -g "$PGID" pgadmin; then
        echo "ERROR: Failed to set pgadmin user to UID=$PUID GID=$PGID"
        exit 1
    fi

    # Fix ownership of runtime directories BEFORE any initialization
    for dir in /run/pgadmin /var/lib/pgadmin; do
        if [ -d "$dir" ]; then
            chown -R "$PUID:$PGID" "$dir"
        fi
    done

    # Fix ownership of individual files (no -R needed)
    if [ -e /pgadmin4/config_distro.py ]; then
        chown "$PUID:$PGID" /pgadmin4/config_distro.py
    fi

    if [ -d /certs ]; then
        chown -R "$PUID:$PGID" /certs
    fi

    SU_EXEC="su-exec $PUID:$PGID"
    echo "pgAdmin will run as UID=$PUID, GID=$PGID"
else
    SU_EXEC=""

    # Fixup the passwd file, in case we're on OpenShift
    if ! whoami > /dev/null 2>&1; then
        if [ "$(id -u)" -ne 5050 ]; then
            if [ -w /etc/passwd ]; then
                echo "${USER_NAME:-pgadminr}:x:$(id -u):0:${USER_NAME:-pgadminr} user:${HOME}:/sbin/nologin" >> /etc/passwd
            fi
        fi
    fi
fi

# Decide which python interpreter to use for gunicorn.
#
# /venv/bin/python3-cap is a symlink to /usr/local/bin/python3-cap, a copy
# of the system python carrying CAP_NET_BIND_SERVICE. It is needed to bind
# to privileged ports (the default 80/443) as the non-root pgadmin user.
#
# Some platforms refuse to honor file capabilities, in which case execing
# the capped binary either fails outright or silently strips the caps so
# bind() to a port <1024 still returns EPERM:
#
#   - NoNewPrivs=1 (--security-opt=no-new-privileges, OpenShift's
#     allowPrivilegeEscalation: false): the kernel silently strips file
#     capabilities on exec.
#   - CAP_NET_BIND_SERVICE missing from the bounding set (--cap-drop=ALL,
#     OpenShift restricted-v2 SCC): exec of the capped binary returns
#     EPERM.
#
# Detect either condition via /proc/self/status. When restricted, fall
# back to the un-capped venv python and (if the user has not picked a
# port) default PGADMIN_LISTEN_PORT to 8080/8443 so the server can
# actually bind.
PYTHON_BIN=/venv/bin/python3-cap
restricted=0

if grep -q '^NoNewPrivs:[[:space:]]*1' /proc/self/status 2>/dev/null; then
    restricted=1
fi

if [ "$restricted" = "0" ]; then
    cap_bnd=$(awk '/^CapBnd:/ { print $2 }' /proc/self/status 2>/dev/null)
    if [ -n "$cap_bnd" ] && [ "$(( 0x${cap_bnd} & 0x400 ))" -eq 0 ]; then
        restricted=1
    fi
fi

if [ "$restricted" = "1" ] || [ ! -x /venv/bin/python3-cap ]; then
    PYTHON_BIN=/venv/bin/python3
    if [ -z "${PGADMIN_LISTEN_PORT}" ]; then
        if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
            export PGADMIN_LISTEN_PORT=8443
        else
            export PGADMIN_LISTEN_PORT=8080
        fi
        echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to ${PGADMIN_LISTEN_PORT}."
    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, for Docker's secrets feature)
function file_env() {
	local var="$1"
	local fileVar="${var}_FILE"
	local def="${2:-}"
	if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
		printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
		exit 1
	fi
	local val="$def"
	if [ "${!var:-}" ]; then
		val="${!var}"
	elif [ "${!fileVar:-}" ] && [ ! -r "${!fileVar}" ]; then
		printf >&2 'error: %s is set to "%s" but the file does not exist or is not readable\n' \
			"$fileVar" "${!fileVar}"
		exit 1
	elif [ "${!fileVar:-}" ]; then
		val="$(< "${!fileVar}")"
	fi
	export "$var"="$val"
	unset "$fileVar"
}

# Set values for config variables that can be passed using secrets
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI_FILE}" ]; then
  file_env PGADMIN_CONFIG_CONFIG_DATABASE_URI
fi
file_env PGADMIN_DEFAULT_PASSWORD

# TO enable custom path for config_distro, pass config distro path via environment variable.
export CONFIG_DISTRO_FILE_PATH="${PGADMIN_CUSTOM_CONFIG_DISTRO_FILE:-/pgadmin4/config_distro.py}"
# Populate config_distro.py. This has some default config, as well as anything
# provided by the user through the PGADMIN_CONFIG_* environment variables.
# Only update the file on first launch. The empty file is created only in default path during the
# container build so it can have the required ownership.
if [ ! -e "${CONFIG_DISTRO_FILE_PATH}" ] || [ "$(wc -m "${CONFIG_DISTRO_FILE_PATH}" 2>/dev/null | awk '{ print $1 }')" = "0" ]; then
    cat << EOF > "${CONFIG_DISTRO_FILE_PATH}"
CA_FILE = '/etc/ssl/certs/ca-certificates.crt'
LOG_FILE = '/dev/null'
HELP_PATH = '../../docs'
DEFAULT_BINARY_PATHS = {
        'pg': '/usr/local/pgsql-18',
        'pg-18': '/usr/local/pgsql-18',
        'pg-17': '/usr/local/pgsql-17',
        'pg-16': '/usr/local/pgsql-16',
        'pg-15': '/usr/local/pgsql-15',
        'pg-14': '/usr/local/pgsql-14',
        'pg-13': '/usr/local/pgsql-13'
}
EOF

    # This is a bit kludgy, but necessary as the container uses BusyBox/ash as
    # it's shell and not bash which would allow a much cleaner implementation
    for var in $(env | grep "^PGADMIN_CONFIG_" | cut -d "=" -f 1); do
        # Get the raw value
        val=$(eval "echo \"\$$var\"")
        # This normalization step is what makes 'true', 'True'
        case "$(echo "$val" | tr '[:upper:]' '[:lower:]')" in
            true)  val="True" ;;
            false) val="False" ;;
        esac
        echo "${var#PGADMIN_CONFIG_} = $val" >> "${CONFIG_DISTRO_FILE_PATH}"
    done

    # If running as root with custom config distro path, fix ownership
    if [ "$(id -u)" = "0" ] && [ "${CONFIG_DISTRO_FILE_PATH}" != "/pgadmin4/config_distro.py" ]; then
        chown "$PUID:$PGID" "${CONFIG_DISTRO_FILE_PATH}"
    fi
fi

# Check whether the external configuration database exists if it is being used.
#
# The URI is read inside Python via os.environ — the shell no longer
# participates in Python-literal quoting. `ast.literal_eval` unwraps the
# legacy `'url'` form that the config_distro.py generation convention
# requires; raw values pass through unchanged. On any Python failure the
# "False" default below is preserved so first-launch user setup still
# runs (see #9984).
external_config_db_exists="False"
if [ -n "${PGADMIN_CONFIG_CONFIG_DATABASE_URI}" ]; then
    result=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "
import os, ast
from check_external_config_db import check_external_config_db
raw = os.environ['PGADMIN_CONFIG_CONFIG_DATABASE_URI']
try:
    uri = ast.literal_eval(raw)
except (ValueError, SyntaxError):
    uri = raw
print(check_external_config_db(uri))
" 2>/dev/null)
    if [ -n "$result" ]; then
        external_config_db_exists="$result"
    fi
fi

# DRY of the code to load the PGADMIN_SERVER_JSON_FILE
function load_server_json_file() {
    export PGADMIN_SERVER_JSON_FILE="${PGADMIN_SERVER_JSON_FILE:-/pgadmin4/servers.json}"

    EXTRA_ARGS=""

    if [ "${PGADMIN_REPLACE_SERVERS_ON_STARTUP}" = "True" ]; then
        EXTRA_ARGS="--replace"
    fi

    if [ -f "${PGADMIN_SERVER_JSON_FILE}" ]; then
        # When running in Desktop mode, no user is created
        # so we have to import servers anonymously
        if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
            $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" ${EXTRA_ARGS}
        else
            $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py load-servers "${PGADMIN_SERVER_JSON_FILE}" --user "${PGADMIN_DEFAULT_EMAIL}" ${EXTRA_ARGS}
        fi
    fi
}

if [ ! -f /var/lib/pgadmin/pgadmin4.db ] && [ "${external_config_db_exists}" = "False" ]; then
    if [ -z "${PGADMIN_DEFAULT_EMAIL}" ] || { [ -z "${PGADMIN_DEFAULT_PASSWORD}" ] && [ -z "${PGADMIN_DEFAULT_PASSWORD_FILE}" ]; }; then
        echo 'You need to define the PGADMIN_DEFAULT_EMAIL and PGADMIN_DEFAULT_PASSWORD or PGADMIN_DEFAULT_PASSWORD_FILE environment variables.'
        exit 1
    fi

    # Validate PGADMIN_DEFAULT_EMAIL
    CHECK_EMAIL_DELIVERABILITY="False"
    if [ -n "${PGADMIN_CONFIG_CHECK_EMAIL_DELIVERABILITY}" ]; then
        CHECK_EMAIL_DELIVERABILITY=${PGADMIN_CONFIG_CHECK_EMAIL_DELIVERABILITY}
    fi
    ALLOW_SPECIAL_EMAIL_DOMAINS="[]"
    if [ -n "${PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS}" ]; then
        ALLOW_SPECIAL_EMAIL_DOMAINS=${PGADMIN_CONFIG_ALLOW_SPECIAL_EMAIL_DOMAINS}
    fi
    GLOBALLY_DELIVERABLE="True"
    if [ -n "${PGADMIN_CONFIG_GLOBALLY_DELIVERABLE}" ]; then
        GLOBALLY_DELIVERABLE=${PGADMIN_CONFIG_GLOBALLY_DELIVERABLE}
    fi
     email_config="{'CHECK_EMAIL_DELIVERABILITY': ${CHECK_EMAIL_DELIVERABILITY}, 'ALLOW_SPECIAL_EMAIL_DOMAINS': ${ALLOW_SPECIAL_EMAIL_DOMAINS}, 'GLOBALLY_DELIVERABLE': ${GLOBALLY_DELIVERABLE}}"
     echo "email config is ${email_config}"
     is_valid_email=$(cd /pgadmin4/pgadmin/utils && $SU_EXEC /venv/bin/python3 -c "from validation_utils import validate_email; val = validate_email('${PGADMIN_DEFAULT_EMAIL}', ${email_config}); print(val)")
     if echo "${is_valid_email}" | grep "False" > /dev/null; then
         echo "'${PGADMIN_DEFAULT_EMAIL}' does not appear to be a valid email address. Please reset the PGADMIN_DEFAULT_EMAIL environment variable and try again."
         echo "Validation output: ${is_valid_email}"
         exit 1
     fi
    # Switch back to root directory for further process
    cd /pgadmin4

    # Set the default username and password in a
    # backwards compatible way
    export PGADMIN_SETUP_EMAIL="${PGADMIN_DEFAULT_EMAIL}"
    export PGADMIN_SETUP_PASSWORD="${PGADMIN_DEFAULT_PASSWORD}"

    # Initialize DB before starting Gunicorn
    # Importing pgadmin4 (from this script) is enough
    $SU_EXEC /venv/bin/python3 run_pgadmin.py

    export PGADMIN_PREFERENCES_JSON_FILE="${PGADMIN_PREFERENCES_JSON_FILE:-/pgadmin4/preferences.json}"

    # Pre-load any required servers
    load_server_json_file

    # Pre-load any required preferences
    if [ -f "${PGADMIN_PREFERENCES_JSON_FILE}" ]; then
        if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
            DESKTOP_USER=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.DESKTOP_USER)')
            $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${DESKTOP_USER}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
        else
            $SU_EXEC /venv/bin/python3 /pgadmin4/setup.py set-prefs "${PGADMIN_DEFAULT_EMAIL}" --input-file "${PGADMIN_PREFERENCES_JSON_FILE}"
        fi
    fi
    # Copy the pgpass file passed using secrets
    if [ -n "${PGPASS_FILE}" ] && [ -f "${PGPASS_FILE}" ]; then
        if [ "${PGADMIN_CONFIG_SERVER_MODE}" = "False" ]; then
            cp "${PGPASS_FILE}" /var/lib/pgadmin/.pgpass
            chmod 600 /var/lib/pgadmin/.pgpass
            # Fix ownership when running as root
            if [ "$(id -u)" = "0" ]; then
                chown "$PUID:$PGID" /var/lib/pgadmin/.pgpass
            fi
        else
            PGADMIN_USER_CONFIG_DIR=$(echo "${PGADMIN_DEFAULT_EMAIL}" | sed 's/@/_/g')
            mkdir -p "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
            cp "${PGPASS_FILE}" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
            chmod 600 "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}/.pgpass"
            # Fix ownership when running as root
            if [ "$(id -u)" = "0" ]; then
                chown -R "$PUID:$PGID" "/var/lib/pgadmin/storage/${PGADMIN_USER_CONFIG_DIR}"
            fi
        fi
    fi
# If already initialised and PGADMIN_REPLACE_SERVERS_ON_STARTUP is set to true, then load the server json file.
elif [ "${PGADMIN_REPLACE_SERVERS_ON_STARTUP}" = "True" ]; then
    load_server_json_file
fi

# Start Postfix to handle password resets etc.
if [ -z "${PGADMIN_DISABLE_POSTFIX}" ]; then
    sudo /usr/sbin/postfix start
fi

# Get the session timeout from the pgAdmin config. We'll use this (in seconds)
# to define the Gunicorn worker timeout
TIMEOUT=$(cd /pgadmin4 && $SU_EXEC /venv/bin/python3 -c 'import config; print(config.SESSION_EXPIRATION_TIME * 60 * 60 * 24)')

# NOTE: currently pgadmin can run only with 1 worker due to sessions implementation
# Using --threads to have multi-threaded single-process worker

if [ -n "${PGADMIN_ENABLE_SOCK}" ]; then
    BIND_ADDRESS="unix:/run/pgadmin/pgadmin.sock"
else
    if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
        BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-443}"
    else
        BIND_ADDRESS="${PGADMIN_LISTEN_ADDRESS:-[::]}:${PGADMIN_LISTEN_PORT:-80}"
    fi
fi

if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
    exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
else
    exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
fi
