#!/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"

# 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"
}

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

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
    mkdir -p "$dir"
    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 "$APP")"
  file="$dir/excludes"
  mkdir -p "$dir"
  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.

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;
}

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 "";
}

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;
}
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 "";
}
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;
}
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
}

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 -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" ]]
}

fn-ga-write-env-file() {
  local envfile="$GOOGLE_AUTH_DATA_ROOT/service.env"
  local domains emails
  domains="$(fn-ga-global-get-list allowed-domains | paste -sd, -)"
  emails="$(fn-ga-global-get-list allowed-emails | paste -sd, -)"
  mkdir -p "$GOOGLE_AUTH_DATA_ROOT"
  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_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"
  fn-ga-image-exists || fn-ga-build-image
  fn-ga-write-env-file
  local port
  port="$(fn-ga-global-get port "$GOOGLE_AUTH_DEFAULT_PORT")"
  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" \
    "$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
}
