#!/usr/bin/env bash
# Fetch secrets from AWS SSM Parameter Store and render .env.production.
#
# Hard rule (2026-05-19, after the CORS_ORIGINS silent-empty incident):
# this script MUST fail loudly on any SSM API failure — throttle, network
# blip, transient permission glitch. The previous ``2>/dev/null || echo ""``
# pattern swallowed a real ``ThrottlingException`` and rendered the file
# with ``CORS_ORIGINS=`` — backend then died inside pydantic_settings
# parsing an empty string as ``list[str]`` JSON. A single empty variable
# can take down deploy; that error class must never be silent.
#
# The "soft-optional" parameters (APP_ENV, AUTH_PASSWORD_LOGIN_ENABLED,
# SUCCESSFACTORS_BASE_URL, CORS_ORIGINS, plus every approval-code) are
# allowed to come back as "missing-in-SSM" but never as "API said no" —
# the helper distinguishes those two cases via the SSM error message
# (``ParameterNotFound``) and only the former produces an empty string.
set -euo pipefail

PROJECT="${PROJECT:-helix}"
ENVIRONMENT="${ENVIRONMENT:-prod}"
REGION="${AWS_REGION:-ap-southeast-1}"
ENV_FILE="${ENV_FILE:-/opt/helix/env/.env.production}"

# Retry knobs. Total wall-clock for 3 attempts at 1s / 2s / 4s ≤ 15s
# even if each AWS call itself takes ~2s — keeps deploy.sh moving when
# SSM is healthy, gives transient throttle / DNS blips room to recover.
RETRY_MAX_ATTEMPTS="${RENDER_ENV_RETRY_ATTEMPTS:-3}"
RETRY_BASE_DELAY="${RENDER_ENV_RETRY_BASE_DELAY:-1}"

echo "Fetching secrets from SSM (/${PROJECT}/${ENVIRONMENT}/*)..."

# get_param <NAME>
#
# Echoes the parameter value to stdout. Three outcomes:
#   1. Success: prints value (may legitimately be empty if SSM stores "").
#   2. ParameterNotFound: prints empty string, exit 0. Caller decides
#      whether this is acceptable via the MISSING[] check below.
#   3. Any other failure (throttle, network, IAM, decryption error):
#      retry up to RETRY_MAX_ATTEMPTS with exponential backoff; if every
#      attempt fails, exit 1 with a clear message. Caller never sees an
#      empty string in this case — the script aborts.
#
# Stderr from the AWS CLI is captured (not redirected to /dev/null) so
# the failure path can pattern-match ``ParameterNotFoundException`` and
# echo the real error to the deploy log on hard fail.
get_param() {
  local name="$1"
  local attempt=1
  local value=""
  local err=""
  local stderr_file rc delay
  stderr_file=$(mktemp)

  while (( attempt <= RETRY_MAX_ATTEMPTS )); do
    set +e
    value=$(aws ssm get-parameter \
      --name "/${PROJECT}/${ENVIRONMENT}/${name}" \
      --with-decryption \
      --query "Parameter.Value" \
      --output text \
      --region "$REGION" 2>"$stderr_file")
    rc=$?
    set -e

    if (( rc == 0 )); then
      rm -f "$stderr_file"
      printf '%s' "$value"
      return 0
    fi

    err=$(<"$stderr_file")

    # ParameterNotFound is a legitimate "this key isn't in SSM" — caller
    # validates via MISSING[] (critical-required) or accepts the empty
    # default in the rendered file (soft-optional). NOT a retry case.
    if [[ "$err" == *"ParameterNotFound"* ]]; then
      rm -f "$stderr_file"
      printf ''
      return 0
    fi

    if (( attempt < RETRY_MAX_ATTEMPTS )); then
      delay=$(( RETRY_BASE_DELAY * (1 << (attempt - 1)) ))
      echo "WARN: SSM get-parameter '${name}' failed (attempt ${attempt}/${RETRY_MAX_ATTEMPTS}): ${err}" >&2
      echo "WARN: retrying in ${delay}s..." >&2
      sleep "$delay"
    else
      echo "ERROR: SSM get-parameter '${name}' failed after ${RETRY_MAX_ATTEMPTS} attempts." >&2
      echo "ERROR: last error: ${err}" >&2
      echo "ERROR: refusing to render .env with a silent-empty value — aborting deploy." >&2
      rm -f "$stderr_file"
      return 1
    fi
    attempt=$(( attempt + 1 ))
  done

  # Loop above always returns; defensive cleanup.
  rm -f "$stderr_file"
  return 1
}

# ── Core ──
APP_ENV=$(get_param "APP_ENV")
AUTH_PASSWORD_LOGIN_ENABLED=$(get_param "AUTH_PASSWORD_LOGIN_ENABLED")
CORS_ORIGINS=$(get_param "CORS_ORIGINS")

# ── Database & Cache ──
DATABASE_URL=$(get_param "DATABASE_URL")
DATABASE_URL_SYNC=$(get_param "DATABASE_URL_SYNC")
REDIS_URL=$(get_param "REDIS_URL")

# ── Auth ──
JWT_SECRET_KEY=$(get_param "JWT_SECRET_KEY")
GOOGLE_CLIENT_ID=$(get_param "GOOGLE_CLIENT_ID")

# ── Feishu ──
FEISHU_APP_ID=$(get_param "FEISHU_APP_ID")
FEISHU_APP_SECRET=$(get_param "FEISHU_APP_SECRET")
FEISHU_EVENT_VERIFICATION_TOKEN=$(get_param "FEISHU_EVENT_VERIFICATION_TOKEN")
FEISHU_EVENT_ENCRYPT_KEY=$(get_param "FEISHU_EVENT_ENCRYPT_KEY")
FEISHU_APPROVAL_LAPTOP_REPLACEMENT_CODE=$(get_param "FEISHU_APPROVAL_LAPTOP_REPLACEMENT_CODE")
FEISHU_APPROVAL_COMPUTER_BORROW_CODE=$(get_param "FEISHU_APPROVAL_COMPUTER_BORROW_CODE")
FEISHU_APPROVAL_ASSET_RETURN_CODE=$(get_param "FEISHU_APPROVAL_ASSET_RETURN_CODE")
FEISHU_APPROVAL_OFFBOARDING_BUYBACK_CODE=$(get_param "FEISHU_APPROVAL_OFFBOARDING_BUYBACK_CODE")
FEISHU_APPROVAL_SOFTWARE_SEAT_CODE=$(get_param "FEISHU_APPROVAL_SOFTWARE_SEAT_CODE")
FEISHU_APPROVAL_DATA_ACCESS_GROUP_GRANT_CODE=$(get_param "FEISHU_APPROVAL_DATA_ACCESS_GROUP_GRANT_CODE")
FEISHU_APPROVAL_DATA_ACCESS_SERVICE_ACCOUNT_GRANT_CODE=$(get_param "FEISHU_APPROVAL_DATA_ACCESS_SERVICE_ACCOUNT_GRANT_CODE")
FEISHU_APPROVAL_DATA_ACCESS_PERSONAL_GRANT_CODE=$(get_param "FEISHU_APPROVAL_DATA_ACCESS_PERSONAL_GRANT_CODE")

# ── CMDB ──
CREDENTIAL_ENCRYPTION_KEY=$(get_param "CREDENTIAL_ENCRYPTION_KEY")
TRUSTED_PROXY_IPS=$(get_param "TRUSTED_PROXY_IPS")

# ── SuccessFactors ──
SUCCESSFACTORS_BASE_URL=$(get_param "SUCCESSFACTORS_BASE_URL")
SUCCESSFACTORS_USERNAME=$(get_param "SUCCESSFACTORS_USERNAME")
SUCCESSFACTORS_PASSWORD=$(get_param "SUCCESSFACTORS_PASSWORD")

# Validate critical secrets are not empty. By now an empty value can
# only mean "ParameterNotFound" (API failures already exited above), so
# this catches the "you forgot to populate SSM" class of bug.
MISSING=()
[[ -z "$DATABASE_URL" ]] && MISSING+=("DATABASE_URL")
[[ -z "$DATABASE_URL_SYNC" ]] && MISSING+=("DATABASE_URL_SYNC")
[[ -z "$REDIS_URL" ]] && MISSING+=("REDIS_URL")
[[ -z "$JWT_SECRET_KEY" ]] && MISSING+=("JWT_SECRET_KEY")
[[ -z "$GOOGLE_CLIENT_ID" ]] && MISSING+=("GOOGLE_CLIENT_ID")
# CREDENTIAL_ENCRYPTION_KEY is required: without it the app can't decrypt
# platform_configs.feishu (app_secret), integrations (Google Service
# Account JSON), or cmdb_provider_accounts (cloud credentials). Silently
# deploying with this empty breaks those features in prod.
[[ -z "$CREDENTIAL_ENCRYPTION_KEY" ]] && MISSING+=("CREDENTIAL_ENCRYPTION_KEY")

if [[ ${#MISSING[@]} -gt 0 ]]; then
  echo "ERROR: Missing critical SSM parameters: ${MISSING[*]}"
  echo "Please set them in SSM Parameter Store: /${PROJECT}/${ENVIRONMENT}/<name>"
  exit 1
fi

cat > "$ENV_FILE" <<EOF
# Auto-generated by render-env.sh — DO NOT EDIT MANUALLY
# Source: AWS SSM Parameter Store /${PROJECT}/${ENVIRONMENT}

APP_ENV=${APP_ENV:-production}
APP_DEBUG=false

DATABASE_URL=${DATABASE_URL}
DATABASE_URL_SYNC=${DATABASE_URL_SYNC}
REDIS_URL=${REDIS_URL}

JWT_SECRET_KEY=${JWT_SECRET_KEY}

GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
AUTH_PASSWORD_LOGIN_ENABLED=${AUTH_PASSWORD_LOGIN_ENABLED:-false}

FEISHU_APP_ID=${FEISHU_APP_ID}
FEISHU_APP_SECRET=${FEISHU_APP_SECRET}
FEISHU_EVENT_VERIFICATION_TOKEN=${FEISHU_EVENT_VERIFICATION_TOKEN}
FEISHU_EVENT_ENCRYPT_KEY=${FEISHU_EVENT_ENCRYPT_KEY}
FEISHU_APPROVAL_LAPTOP_REPLACEMENT_CODE=${FEISHU_APPROVAL_LAPTOP_REPLACEMENT_CODE}
FEISHU_APPROVAL_COMPUTER_BORROW_CODE=${FEISHU_APPROVAL_COMPUTER_BORROW_CODE}
FEISHU_APPROVAL_ASSET_RETURN_CODE=${FEISHU_APPROVAL_ASSET_RETURN_CODE}
FEISHU_APPROVAL_OFFBOARDING_BUYBACK_CODE=${FEISHU_APPROVAL_OFFBOARDING_BUYBACK_CODE}
FEISHU_APPROVAL_SOFTWARE_SEAT_CODE=${FEISHU_APPROVAL_SOFTWARE_SEAT_CODE}
FEISHU_APPROVAL_DATA_ACCESS_GROUP_GRANT_CODE=${FEISHU_APPROVAL_DATA_ACCESS_GROUP_GRANT_CODE}
FEISHU_APPROVAL_DATA_ACCESS_SERVICE_ACCOUNT_GRANT_CODE=${FEISHU_APPROVAL_DATA_ACCESS_SERVICE_ACCOUNT_GRANT_CODE}
FEISHU_APPROVAL_DATA_ACCESS_PERSONAL_GRANT_CODE=${FEISHU_APPROVAL_DATA_ACCESS_PERSONAL_GRANT_CODE}

CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
CORS_ORIGINS=${CORS_ORIGINS}

# Reverse-proxy trust: nginx reaches the backend over the compose
# ``helix`` network (pinned subnet 172.28.0.0/16 in
# docker-compose.prod.yml), so the trusted peer is nginx's container
# IP inside that subnet. Without this the backend ignores
# X-Forwarded-For and every per-IP rate limiter / audit ip_address
# collapses onto the nginx container IP (security review 2026-06-11).
# The backend port is loopback-only, so nothing outside the compose
# network can reach :8000 to abuse this trust. Keep the CIDR in
# lock-step with the compose subnet. SSM override optional.
#
# Written as a COMMA-SEPARATED value (no JSON brackets/quotes): this is
# an env_file, and Docker Compose mangles inner double-quotes in
# env-file values, so a JSON array ``["a","b"]`` reached pydantic as
# ``[a,b]`` and crashed boot with a SettingsError (deploy break
# 2026-06-12). The ``parse_trusted_proxy_ips`` validator splits commas.
TRUSTED_PROXY_IPS=${TRUSTED_PROXY_IPS:-127.0.0.1,::1,172.28.0.0/16}

SUCCESSFACTORS_BASE_URL=${SUCCESSFACTORS_BASE_URL:-https://api15.sapsf.cn/odata/v2}
SUCCESSFACTORS_USERNAME=${SUCCESSFACTORS_USERNAME}
SUCCESSFACTORS_PASSWORD=${SUCCESSFACTORS_PASSWORD}
EOF

chmod 600 "$ENV_FILE"
echo "Rendered ${ENV_FILE}"
