#!/usr/bin/env bash
# Shared functions for the google-auth dokku plugin.
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x

GOOGLE_AUTH_PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

export DOKKU_ROOT=${DOKKU_ROOT:-/home/dokku}
export DOKKU_LIB_ROOT=${DOKKU_LIB_ROOT:-/var/lib/dokku}
export PLUGIN_CORE_AVAILABLE_PATH=${PLUGIN_CORE_AVAILABLE_PATH:-$DOKKU_LIB_ROOT/core-plugins/available}

if [[ -f "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" ]]; then
  # shellcheck disable=SC1091
  source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
fi

GOOGLE_AUTH_DATA_ROOT="$DOKKU_LIB_ROOT/data/google-auth"
GOOGLE_AUTH_SERVICE_NAME="dokku-google-auth"
GOOGLE_AUTH_IMAGE="dokku-google-auth:latest"
GOOGLE_AUTH_DEFAULT_PORT="2999"
GOOGLE_AUTH_ROUTE_PREFIX="/_google-auth"
# Where the per-app data directory is bind-mounted inside the service container.
GOOGLE_AUTH_APP_CONFIG_MOUNT="/data/apps"

# Fallbacks so the plugin can be exercised outside a dokku host (tests, dev).
if ! declare -f dokku_log_info1 >/dev/null 2>&1; then
  dokku_log_info1() { echo "-----> $*"; }
  dokku_log_info2() { echo "=====> $*"; }
  dokku_log_verbose() { echo "       $*"; }
  dokku_log_warn() { echo " !     $*" 1>&2; }
  dokku_log_fail() {
    echo " !     $*" 1>&2
    exit 1
  }
  verify_app_name() {
    [[ -n "$1" && -d "$DOKKU_ROOT/$1" ]] || dokku_log_fail "App $1 does not exist"
  }
fi

# --- key/value storage (one file per key under the plugin data dir) ---

fn-ga-global-get() {
  declare KEY="$1" DEFAULT="${2:-}"
  local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
  if [[ -s "$file" ]]; then
    head -n1 "$file"
  else
    printf '%s' "$DEFAULT"
  fi
}

fn-ga-global-set() {
  declare KEY="$1" VALUE="$2"
  mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
  printf '%s\n' "$VALUE" >"$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
  chmod 600 "$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
}

# Multi-value keys store one entry per line.
fn-ga-global-get-list() {
  declare KEY="$1"
  cat "$GOOGLE_AUTH_DATA_ROOT/global/$KEY" 2>/dev/null || true
}

fn-ga-global-set-list() {
  declare KEY="$1"
  shift
  mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
  local file="$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
  : >"$file"
  local entry
  for entry in "$@"; do
    printf '%s\n' "$entry" >>"$file"
  done
  chmod 600 "$file"
}

# --- access lists, scoped to "global" or to one app ---
#
# An app's allow entries replace the global ones for that app; deny entries from
# both scopes are combined. The auth service implements that precedence — these
# helpers only store the entries.

fn-ga-list-file() {
  declare SCOPE="$1" KEY="$2"
  if [[ "$SCOPE" == "global" ]]; then
    echo "$GOOGLE_AUTH_DATA_ROOT/global/$KEY"
  else
    echo "$(fn-ga-app-dir "$SCOPE")/$KEY"
  fi
}

fn-ga-list-get() {
  declare SCOPE="$1" KEY="$2"
  cat "$(fn-ga-list-file "$SCOPE" "$KEY")" 2>/dev/null || true
}

fn-ga-list-count() {
  declare SCOPE="$1" KEY="$2"
  fn-ga-list-get "$SCOPE" "$KEY" | grep -c . || true
}

fn-ga-list-contains() {
  declare SCOPE="$1" KEY="$2" VALUE="$3"
  grep -qxF "$VALUE" "$(fn-ga-list-file "$SCOPE" "$KEY")" 2>/dev/null
}

fn-ga-list-add() {
  declare SCOPE="$1" KEY="$2" VALUE="$3"
  local file
  file="$(fn-ga-list-file "$SCOPE" "$KEY")"
  if [[ "$SCOPE" == "global" ]]; then
    mkdir -p "$GOOGLE_AUTH_DATA_ROOT/global"
    touch "$file"
    chmod 600 "$file"
  else
    fn-ga-app-dir-ensure "$SCOPE" >/dev/null
    touch "$file"
    # Readable through the service's read-only bind mount; see fn-ga-app-dir-ensure.
    chmod 644 "$file"
  fi
  grep -qxF "$VALUE" "$file" || printf '%s\n' "$VALUE" >>"$file"
}

fn-ga-list-remove() {
  declare SCOPE="$1" KEY="$2" VALUE="$3"
  local file tmp
  file="$(fn-ga-list-file "$SCOPE" "$KEY")"
  [[ -f "$file" ]] || return 0
  tmp="$(mktemp)"
  grep -vxF "$VALUE" "$file" >"$tmp" || true
  cat "$tmp" >"$file"
  rm -f "$tmp"
}

# Sets GA_SCOPE from a command's first argument: "global" for --global,
# otherwise a verified app name. Fails (and exits) on anything else.
fn-ga-resolve-scope() {
  declare CMD="$1" ARG="${2:-}"
  case "$ARG" in
    --global)
      GA_SCOPE=global
      ;;
    "")
      dokku_log_fail "usage: dokku $CMD <app>|--global [<entry>...]"
      ;;
    -*)
      dokku_log_fail "unknown flag '$ARG' — pass an app name or --global"
      ;;
    *)
      verify_app_name "$ARG"
      GA_SCOPE="$ARG"
      ;;
  esac
}

# Human-readable scope for log lines: "globally" or "for my-app".
fn-ga-scope-label() {
  declare SCOPE="$1"
  if [[ "$SCOPE" == "global" ]]; then
    echo "globally"
  else
    echo "for $SCOPE"
  fi
}

# The scope as it is typed on the command line, for suggested commands.
fn-ga-scope-arg() {
  declare SCOPE="$1"
  if [[ "$SCOPE" == "global" ]]; then
    echo "--global"
  else
    echo "$SCOPE"
  fi
}

# Advisory mirror of the service's allow-list precedence (an app's entries
# replace the global ones), used only to warn operators. The authority is
# emailAllowedFor in internal/authproxy.
fn-ga-email-effectively-allowed() {
  declare SCOPE="$1" EMAIL="$2"
  local effective="$SCOPE"
  if [[ "$SCOPE" != "global" ]] &&
    [[ "$(fn-ga-list-count "$SCOPE" allowed-domains)" -eq 0 &&
    "$(fn-ga-list-count "$SCOPE" allowed-emails)" -eq 0 ]]; then
    effective=global
  fi
  fn-ga-list-contains "$effective" allowed-emails "$EMAIL" && return 0
  fn-ga-list-contains "$effective" allowed-domains "${EMAIL##*@}"
}

# Makes a list change take effect. Global lists travel in the container's
# environment and need a restart; per-app lists are read live through the bind
# mount, so they only need the mount to actually be there.
fn-ga-apply-list-change() {
  declare SCOPE="$1"
  if [[ "$SCOPE" == "global" ]]; then
    fn-ga-reload-service-config
    return 0
  fi
  # An app's lists only apply if its nginx config tells the service which app a
  # request belongs to. Configs written before that header existed would make
  # the app fall back to the global lists, so refresh it here rather than wait
  # for the next deploy. This is a no-op when the config is already current.
  if fn-google-auth-app-enabled "$SCOPE"; then
    fn-ga-apply "$SCOPE"
  fi
  if ! fn-ga-service-running; then
    dokku_log_verbose "auth service is not running; changes apply when it starts"
    return 0
  fi
  if ! fn-ga-service-reads-app-lists; then
    dokku_log_info1 "recreating the auth service so it can read per-app lists (this rebuilds the image if the plugin was updated)"
    fn-ga-service-start
    return 0
  fi
  dokku_log_verbose "in effect within a few seconds (no restart needed)"
}

# Entries end up in a comma/space separated env var, so they may contain
# neither.
fn-ga-validate-list-entry() {
  declare ENTRY="$1"
  [[ -n "$ENTRY" ]] || return 1
  ! printf '%s' "$ENTRY" | grep -qE '[,[:space:]]'
}

# "signal.org" and "@signal.org" name a domain; "guest@partner.com" names one
# address. Callers strip any leading "@" themselves.
fn-ga-allow-entry-kind() {
  declare ENTRY="$1"
  if [[ "$ENTRY" == @* || "$ENTRY" != *@* ]]; then
    echo domain
  else
    echo email
  fi
}

# The service reads the allow/deny lists from its env file at startup, so
# changing them means recreating the container.
fn-ga-reload-service-config() {
  if fn-ga-service-running; then
    fn-ga-service-start
  else
    dokku_log_verbose "auth service is not running; changes apply when it starts"
  fi
}

fn-ga-app-dir() {
  declare APP="$1"
  echo "$GOOGLE_AUTH_DATA_ROOT/apps/$APP"
}

# Creates an app's state directory with modes the auth service can use. It reads
# per-app lists through a read-only bind mount as an unprivileged uid, so the
# directories must be traversable and the list files readable. Nothing is
# exposed to other users on the host: $GOOGLE_AUTH_DATA_ROOT itself stays 0700,
# and secrets live in global/, which is never mounted.
fn-ga-app-dir-ensure() {
  declare APP="$1"
  local dir
  dir="$(fn-ga-app-dir "$APP")"
  mkdir -p "$dir"
  chmod 711 "$GOOGLE_AUTH_DATA_ROOT/apps" "$dir" 2>/dev/null || true
  printf '%s' "$dir"
}

fn-google-auth-app-enabled() {
  declare APP="$1"
  [[ -f "$(fn-ga-app-dir "$APP")/enabled" ]]
}

fn-ga-app-set-enabled() {
  declare APP="$1" ENABLED="$2"
  local dir
  dir="$(fn-ga-app-dir "$APP")"
  if [[ "$ENABLED" == "true" ]]; then
    dir="$(fn-ga-app-dir-ensure "$APP")"
    touch "$dir/enabled"
  else
    rm -f "$dir/enabled"
  fi
}

fn-ga-excludes() {
  declare APP="$1"
  cat "$(fn-ga-app-dir "$APP")/excludes" 2>/dev/null || true
}

fn-ga-enabled-apps() {
  local dir
  for dir in "$GOOGLE_AUTH_DATA_ROOT/apps"/*/; do
    [[ -d "$dir" ]] || continue
    local app
    app="$(basename "$dir")"
    fn-google-auth-app-enabled "$app" && echo "$app"
  done
  return 0
}

fn-ga-configured() {
  [[ -n "$(fn-ga-global-get client-id)" ]] || return 1
  [[ -n "$(fn-ga-global-get client-secret)" ]] || return 1
  [[ -n "$(fn-ga-global-get auth-host)" ]] || return 1
  [[ -n "$(fn-ga-global-get cookie-secret)" ]] || return 1
  [[ -s "$GOOGLE_AUTH_DATA_ROOT/global/allowed-domains" || -s "$GOOGLE_AUTH_DATA_ROOT/global/allowed-emails" ]] || return 1
}

# --- exclusion patterns ---

# Patterns are either a path prefix ("/api/webhooks") or a regex ("re:^/v[0-9]+/public/").
fn-ga-validate-pattern() {
  declare PATTERN="$1"
  [[ -n "$PATTERN" ]] || return 1
  # Guard against nginx config injection.
  if printf '%s' "$PATTERN" | grep -qE '[;{}"'"'"'[:space:]]'; then
    return 1
  fi
  if [[ "$PATTERN" == re:* ]]; then
    [[ -n "${PATTERN#re:}" ]] || return 1
  else
    [[ "$PATTERN" == /* ]] || return 1
  fi
}

fn-ga-exclude-add() {
  declare APP="$1" PATTERN="$2"
  local dir file
  dir="$(fn-ga-app-dir-ensure "$APP")"
  file="$dir/excludes"
  touch "$file"
  grep -qxF "$PATTERN" "$file" || printf '%s\n' "$PATTERN" >>"$file"
}

fn-ga-exclude-remove() {
  declare APP="$1" PATTERN="$2"
  local file tmp
  file="$(fn-ga-app-dir "$APP")/excludes"
  [[ -f "$file" ]] || return 0
  tmp="$(mktemp)"
  grep -vxF "$PATTERN" "$file" >"$tmp" || true
  cat "$tmp" >"$file"
  rm -f "$tmp"
}

# --- nginx config generation ---

# The app's upstream block is created by dokku's own nginx template; we reuse
# it by name so excluded and protected locations proxy to the same place.
fn-ga-upstream-name() {
  declare APP="$1"
  local nginx_conf="$DOKKU_ROOT/$APP/nginx.conf"
  [[ -f "$nginx_conf" ]] || return 1
  local name
  name="$(awk '$1 == "upstream" {print $2; exit}' "$nginx_conf")"
  [[ -n "$name" ]] || return 1
  printf '%s' "$name"
}

fn-ga-proxy-read-timeout() {
  declare APP="$1"
  local t="" file
  for file in "$DOKKU_LIB_ROOT/config/nginx/$APP/proxy-read-timeout" \
    "$DOKKU_LIB_ROOT/config/nginx/--global/proxy-read-timeout"; do
    if [[ -s "$file" ]]; then
      t="$(head -n1 "$file")"
      break
    fi
  done
  printf '%s' "${t:-60s}"
}

# Mirrors the proxy directives from dokku's default nginx template so
# requests routed through our locations behave like stock dokku routing.
fn-ga-proxy-directives() {
  declare UPSTREAM="$1" TIMEOUT="$2"
  cat <<EOF
    proxy_pass http://${UPSTREAM};
    proxy_http_version 1.1;
    proxy_read_timeout ${TIMEOUT};
    proxy_set_header Upgrade \$http_upgrade;
    proxy_set_header Connection \$http_connection;
    proxy_set_header Host \$http_host;
    proxy_set_header X-Forwarded-For \$remote_addr;
    proxy_set_header X-Forwarded-Port \$server_port;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_set_header X-Request-Start \$msec;
EOF
}

fn-ga-generate-conf() {
  declare APP="$1"
  local upstream timeout port
  upstream="$(fn-ga-upstream-name "$APP")" || return 1
  timeout="$(fn-ga-proxy-read-timeout "$APP")"
  port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"

  cat <<EOF
# Managed by the dokku google-auth plugin — do not edit by hand.
# Regenerated on every deploy and by google-auth:* commands.

# Every location below sets X-Google-Auth-App explicitly, which both tells the
# auth service whose access lists to apply and overwrites any value a client
# tried to send.
location = ${GOOGLE_AUTH_ROUTE_PREFIX}/verify {
    internal;
    proxy_pass http://127.0.0.1:${port};
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header Host \$host;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_set_header X-Forwarded-For \$remote_addr;
    proxy_set_header X-Google-Auth-App "${APP}";
}

location ^~ ${GOOGLE_AUTH_ROUTE_PREFIX}/ {
    proxy_pass http://127.0.0.1:${port};
    proxy_http_version 1.1;
    proxy_set_header Host \$host;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_set_header X-Forwarded-For \$remote_addr;
    proxy_set_header X-Forwarded-Port \$server_port;
    proxy_set_header X-Auth-Request-Redirect "";
    proxy_set_header X-Google-Auth-App "${APP}";
}

location @google_auth_signin {
    rewrite ^ ${GOOGLE_AUTH_ROUTE_PREFIX}/start break;
    proxy_pass http://127.0.0.1:${port};
    proxy_set_header Host \$host;
    proxy_set_header X-Forwarded-Proto \$scheme;
    proxy_set_header X-Forwarded-For \$remote_addr;
    proxy_set_header X-Auth-Request-Redirect \$request_uri;
    proxy_set_header X-Google-Auth-App "${APP}";
}
EOF

  local pattern
  while IFS= read -r pattern; do
    [[ -z "$pattern" ]] && continue
    echo ""
    if [[ "$pattern" == re:* ]]; then
      echo "# google-auth: path excluded from SSO (${pattern})"
      echo "location ~ ${pattern#re:} {"
    else
      echo "# google-auth: path excluded from SSO (${pattern})"
      echo "location ^~ ${pattern} {"
    fi
    fn-ga-proxy-directives "$upstream" "$timeout"
    cat <<'EOF'
    # Strip identity headers so clients cannot spoof them on open paths.
    proxy_set_header X-Forwarded-User "";
    proxy_set_header X-Forwarded-Email "";
    proxy_set_header X-Auth-Request-User "";
    proxy_set_header X-Auth-Request-Email "";
    proxy_set_header X-Auth-Request-Name "";
    proxy_set_header X-Google-Auth-App "";
}
EOF
  done < <(fn-ga-excludes "$APP")

  cat <<EOF

# Everything else requires a Google session.
location ~ ^/ {
    auth_request ${GOOGLE_AUTH_ROUTE_PREFIX}/verify;
    auth_request_set \$google_auth_user \$upstream_http_x_auth_request_user;
    auth_request_set \$google_auth_email \$upstream_http_x_auth_request_email;
    auth_request_set \$google_auth_name \$upstream_http_x_auth_request_name;
    error_page 401 = @google_auth_signin;

$(fn-ga-proxy-directives "$upstream" "$timeout")
    proxy_set_header X-Forwarded-User \$google_auth_user;
    proxy_set_header X-Forwarded-Email \$google_auth_email;
    proxy_set_header X-Auth-Request-User \$google_auth_user;
    proxy_set_header X-Auth-Request-Email \$google_auth_email;
    proxy_set_header X-Auth-Request-Name \$google_auth_name;
    proxy_set_header X-Google-Auth-App "";
}
EOF
}

fn-ga-conf-path() {
  declare APP="$1"
  echo "$DOKKU_ROOT/$APP/nginx.conf.d/google-auth.conf"
}

# Regenerates (or removes) the app's conf file without touching nginx.
# Prints one of: changed, unchanged, skipped.
fn-ga-write-conf() {
  declare APP="$1"
  local conf dir tmp
  conf="$(fn-ga-conf-path "$APP")"
  dir="$(dirname "$conf")"

  if ! fn-google-auth-app-enabled "$APP"; then
    if [[ -f "$conf" ]]; then
      rm -f "$conf"
      echo changed
    else
      echo unchanged
    fi
    return 0
  fi

  tmp="$(mktemp)"
  if ! fn-ga-generate-conf "$APP" >"$tmp" 2>/dev/null; then
    rm -f "$tmp"
    echo skipped
    return 0
  fi
  mkdir -p "$dir"
  if [[ -f "$conf" ]] && cmp -s "$tmp" "$conf"; then
    rm -f "$tmp"
    echo unchanged
    return 0
  fi
  cat "$tmp" >"$conf"
  rm -f "$tmp"
  echo changed
}

# --- nginx validate/reload (via dokku core helpers when available) ---

fn-ga-source-nginx-functions() {
  if [[ -f "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/functions" ]]; then
    # shellcheck disable=SC1091
    source "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/functions" 2>/dev/null || true
  fi
}

fn-ga-nginx-validate() {
  fn-ga-source-nginx-functions
  if declare -f validate_nginx >/dev/null 2>&1; then
    (validate_nginx) >/dev/null 2>&1
    return $?
  fi
  return 0
}

fn-ga-nginx-reload() {
  fn-ga-source-nginx-functions
  if declare -f restart_nginx >/dev/null 2>&1; then
    restart_nginx >/dev/null 2>&1 || dokku_log_warn "nginx reload reported an error; check 'nginx -t'"
  else
    dokku_log_warn "could not reload nginx automatically; run: sudo systemctl reload nginx"
  fi
}

# Write conf for one app, validate nginx, roll back on failure, reload.
fn-ga-apply() {
  declare APP="$1"
  local conf backup="" had_file=false status
  conf="$(fn-ga-conf-path "$APP")"
  if [[ -f "$conf" ]]; then
    backup="$(mktemp)"
    cat "$conf" >"$backup"
    had_file=true
  fi

  status="$(fn-ga-write-conf "$APP")"
  case "$status" in
    skipped)
      dokku_log_warn "$APP has no generated nginx config yet (not deployed?). google-auth config will be added on the next deploy."
      ;;
    changed)
      if ! fn-ga-nginx-validate; then
        if [[ "$had_file" == "true" ]]; then
          cat "$backup" >"$conf"
        else
          rm -f "$conf"
        fi
        [[ -n "$backup" ]] && rm -f "$backup"
        dokku_log_fail "nginx rejected the generated config for $APP; change reverted (check exclude patterns)"
      fi
      fn-ga-nginx-reload
      ;;
  esac
  [[ -n "$backup" ]] && rm -f "$backup"
  return 0
}

# Regenerate confs for every enabled app (e.g. after the service port changes).
fn-ga-apply-all() {
  local app any_changed=false status
  while IFS= read -r app; do
    [[ -z "$app" ]] && continue
    status="$(fn-ga-write-conf "$app")"
    [[ "$status" == "changed" ]] && any_changed=true
  done < <(fn-ga-enabled-apps)
  if [[ "$any_changed" == "true" ]]; then
    if fn-ga-nginx-validate; then
      fn-ga-nginx-reload
    else
      dokku_log_warn "nginx validation failed after regenerating google-auth configs; run 'nginx -t' to inspect"
    fi
  fi
  return 0
}

# --- auth service container management ---

fn-ga-image-exists() {
  docker image inspect "$GOOGLE_AUTH_IMAGE" >/dev/null 2>&1
}

# Fingerprint of everything that ends up in the service image. The image is
# labelled with it so an upgraded plugin rebuilds instead of reusing a binary
# built from older source. Without this the shell half of the plugin upgrades
# the moment the files change — new commands, new nginx config, new env var —
# while the binary enforcing them stays whatever was built first, and the
# mismatch is invisible from the outside.
fn-ga-source-hash() {
  local dir="$GOOGLE_AUTH_PLUGIN_DIR" file
  {
    while IFS= read -r file; do
      printf '%s ' "${file#"$dir/"}"
      sha256sum "$file" | awk '{print $1}'
    done < <(
      {
        printf '%s\n' "$dir/Dockerfile" "$dir/go.mod"
        find "$dir/cmd" "$dir/internal" -type f 2>/dev/null
      } | LC_ALL=C sort
    )
  } | sha256sum | awk '{print $1}'
}

# True when the built image matches the plugin's current source.
fn-ga-image-current() {
  fn-ga-image-exists || return 1
  local labelled
  labelled="$(docker image inspect -f '{{index .Config.Labels "google-auth.source-hash"}}' "$GOOGLE_AUTH_IMAGE" 2>/dev/null)"
  # Images built before this label existed report an empty value or "<no value>",
  # and are stale by definition.
  [[ -n "$labelled" && "$labelled" != "<no value>" ]] || return 1
  [[ "$labelled" == "$(fn-ga-source-hash)" ]]
}

fn-ga-build-image() {
  command -v docker >/dev/null 2>&1 || dokku_log_fail "docker is required to build the google-auth service image"
  dokku_log_info1 "Building $GOOGLE_AUTH_IMAGE (first build downloads the Go toolchain image; this can take a few minutes)"
  docker image build \
    --label "google-auth.source-hash=$(fn-ga-source-hash)" \
    -t "$GOOGLE_AUTH_IMAGE" "$GOOGLE_AUTH_PLUGIN_DIR"
}

fn-ga-service-running() {
  [[ "$(docker container inspect -f '{{.State.Running}}' "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" == "true" ]]
}

# Asks the running service which per-app config directory it is using, and
# prints it. This is the only check that sees the binary rather than the
# container around it; a build from before per-app lists existed answers with a
# bare "ok" and names no directory. Fails if the service is unreachable or too
# old to answer.
fn-ga-service-app-config-dir() {
  command -v curl >/dev/null 2>&1 || return 1
  local port body dir
  port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
  body="$(curl -fsS --max-time 3 "http://127.0.0.1:${port}${GOOGLE_AUTH_ROUTE_PREFIX}/healthz" 2>/dev/null)" || return 1
  dir="$(sed -n 's/.*"app_config_dir":"\([^"]*\)".*/\1/p' <<<"$body")"
  [[ -n "$dir" ]] || return 1
  printf '%s' "$dir"
}

# True when the running service can actually read per-app lists. That takes
# three things, and missing any of them makes the service ignore every per-app
# list and fall back to the global one — silently, and in the permissive
# direction — so all three are checked:
#
#   the bind mount             — a container from an older plugin has none;
#   the env var naming it      — one recreated from a stale service.env has the
#                                mount without the variable;
#   a binary that reads both   — one recreated from a stale *image* has the
#                                mount and the variable and ignores them, which
#                                no amount of docker metadata can reveal. Only
#                                the service's own answer distinguishes it.
fn-ga-service-reads-app-lists() {
  local inspected
  inspected="$(docker container inspect \
    -f '{{range .Mounts}}mount={{println .Destination}}{{end}}{{range .Config.Env}}env={{println .}}{{end}}' \
    "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" || return 1
  grep -qxF "mount=$GOOGLE_AUTH_APP_CONFIG_MOUNT" <<<"$inspected" || return 1
  grep -qxF "env=GOOGLE_AUTH_APP_CONFIG_DIR=$GOOGLE_AUTH_APP_CONFIG_MOUNT" <<<"$inspected" || return 1

  local reported
  if reported="$(fn-ga-service-app-config-dir)"; then
    [[ "$reported" == "$GOOGLE_AUTH_APP_CONFIG_MOUNT" ]]
    return $?
  fi
  # No answer (curl missing, or the service is not reachable on the loopback
  # port): fall back to checking that the image was built from this plugin's
  # current source and that the container is running that image.
  fn-ga-image-current && fn-ga-container-runs-current-image
}

# True when the running container was created from the current service image,
# rather than from an earlier build still tagged over.
fn-ga-container-runs-current-image() {
  local running current
  running="$(docker container inspect -f '{{.Image}}' "$GOOGLE_AUTH_SERVICE_NAME" 2>/dev/null)" || return 1
  current="$(docker image inspect -f '{{.Id}}' "$GOOGLE_AUTH_IMAGE" 2>/dev/null)" || return 1
  [[ -n "$running" && "$running" == "$current" ]]
}

fn-ga-write-env-file() {
  local envfile="$GOOGLE_AUTH_DATA_ROOT/service.env"
  local domains emails denied
  domains="$(fn-ga-global-get-list allowed-domains | paste -sd, -)"
  emails="$(fn-ga-global-get-list allowed-emails | paste -sd, -)"
  denied="$(fn-ga-global-get-list denied-emails | paste -sd, -)"
  mkdir -p "$GOOGLE_AUTH_DATA_ROOT"
  # The umask stays inside this subshell. Leaking it left every directory
  # created later in the same command at 0700 — including an app's config
  # directory during google-auth:enable, which the service then could not
  # traverse to read that app's lists.
  (
    umask 077
    cat >"$envfile" <<EOF
GOOGLE_AUTH_CLIENT_ID=$(fn-ga-global-get client-id)
GOOGLE_AUTH_CLIENT_SECRET=$(fn-ga-global-get client-secret)
GOOGLE_AUTH_COOKIE_SECRET=$(fn-ga-global-get cookie-secret)
GOOGLE_AUTH_AUTH_HOST=$(fn-ga-global-get auth-host)
GOOGLE_AUTH_ALLOWED_DOMAINS=$domains
GOOGLE_AUTH_ALLOWED_EMAILS=$emails
GOOGLE_AUTH_DENIED_EMAILS=$denied
GOOGLE_AUTH_APP_CONFIG_DIR=$GOOGLE_AUTH_APP_CONFIG_MOUNT
GOOGLE_AUTH_COOKIE_NAME=$(fn-ga-global-get cookie-name _google_auth)
GOOGLE_AUTH_SESSION_TTL=$(fn-ga-global-get session-ttl 24h)
GOOGLE_AUTH_ALLOW_INSECURE=$(fn-ga-global-get allow-insecure false)
GOOGLE_AUTH_LISTEN=:2999
EOF
  )
}

fn-ga-service-start() {
  fn-ga-configured || dokku_log_fail "google-auth is not fully configured; run: dokku google-auth:configure"
  # Rebuild whenever the source moved, not merely when the image is missing.
  # Starting the service is how an operator expects to pick up an upgraded
  # plugin, and reusing the old binary there is what makes a new feature look
  # broken rather than absent.
  fn-ga-image-current || fn-ga-build-image
  fn-ga-write-env-file
  local port
  port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
  mkdir -p "$GOOGLE_AUTH_DATA_ROOT/apps"
  chmod 711 "$GOOGLE_AUTH_DATA_ROOT/apps" 2>/dev/null || true
  docker container rm -f "$GOOGLE_AUTH_SERVICE_NAME" >/dev/null 2>&1 || true
  docker container run -d \
    --name "$GOOGLE_AUTH_SERVICE_NAME" \
    --restart=unless-stopped \
    -p "127.0.0.1:${port}:2999" \
    --env-file "$GOOGLE_AUTH_DATA_ROOT/service.env" \
    -v "$GOOGLE_AUTH_DATA_ROOT/apps:${GOOGLE_AUTH_APP_CONFIG_MOUNT}:ro" \
    "$GOOGLE_AUTH_IMAGE" >/dev/null
  dokku_log_info1 "google-auth service running on 127.0.0.1:${port}"
}

fn-ga-service-stop() {
  docker container rm -f "$GOOGLE_AUTH_SERVICE_NAME" >/dev/null 2>&1 || true
}

# Warn when the configured auth host is not served by any enabled app,
# because Google's callback would then land on nothing.
fn-ga-warn-if-auth-host-unrouted() {
  local auth_host app vhost_file
  auth_host="$(fn-ga-global-get auth-host)"
  [[ -n "$auth_host" ]] || return 0
  while IFS= read -r app; do
    [[ -z "$app" ]] && continue
    vhost_file="$DOKKU_ROOT/$app/VHOST"
    [[ -f "$vhost_file" ]] && grep -qxF "$auth_host" "$vhost_file" && return 0
  done < <(fn-ga-enabled-apps)
  dokku_log_warn "auth host '$auth_host' is not a domain of any google-auth-enabled app."
  dokku_log_warn "Google's OAuth callback (https://$auth_host$GOOGLE_AUTH_ROUTE_PREFIX/callback) must route to an enabled app."
  return 0
}
