#!/usr/bin/env bash
# Runs ON the deploy host, inside /opt/gowalk-backends/<app>. Brings up the
# compose stack, then (when a domain is given) wires an nginx vhost + LE cert
# and health-checks the public URL. Idempotent; safe to re-run.
set -euo pipefail

APP="$1"           # slug — compose project + dir name
DOMAIN="${2:-}"    # api.<app>.gowalk.com, or empty to skip nginx/cert
HEALTH="${3:-/health}"
CERT_EMAIL="${4:-admin@gowalk.com}"
DIR="/opt/gowalk-backends/$APP"
source "$(dirname "${BASH_SOURCE[0]}")/deploy_evidence.sh"
BACKEND_DEPLOY_PHASE=remote_setup
cd "$DIR"
export COMPOSE_FILE="${5:-docker-compose.yml}"
case "$COMPOSE_FILE" in
  compose.yaml|compose.yml|docker-compose.yaml|docker-compose.yml) ;;
  *) echo "::error::unsupported backend compose filename"; exit 1;;
esac
test -f "$COMPOSE_FILE" || { echo "::error::selected backend compose file is missing"; exit 1; }
source "$DIR/select_certbot_account.sh"
source "$DIR/ensure_certificate.sh"

log() { echo "[deploy $APP] $*"; }
log "host $(python3 --version)"

# Refuse a missing ingress token or an older unprotected workflow before Compose changes.
EDGE_CONFIG="/opt/gowalk-backend-ingress/$APP/.backend-edge.json"
EDGE_VHOST="/etc/nginx/conf.d/backend-$APP-edge.conf"
EDGE_MODE=legacy
if [ -f "$EDGE_CONFIG" ] || [ -e "$EDGE_VHOST" ]; then
  python3 "$DIR/edge_vhost.py" validate "$EDGE_CONFIG" "$APP" "$DOMAIN"
  EDGE_MODE="$(python3 "$DIR/edge_inputs.py" mode "$EDGE_CONFIG")"
  python3 "$DIR/edge_inputs.py" runtime "$EDGE_CONFIG"
fi

# ── secrets: generate a per-app .env once, then preserve it (rsync excludes it)
BACKEND_DEPLOY_PHASE=initialize_environment
if [ ! -f .env ]; then
  log "first deploy — generating .env (POSTGRES_PASSWORD)"
  {
    echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
    echo "APP_NAME=$APP"
  } > .env
  chmod 600 .env
fi

COMPOSE_ENV=(--env-file .env)
if [ -s .runtime.env ]; then
  chmod 600 .runtime.env
  COMPOSE_ENV+=(--env-file .runtime.env)
fi
EDGE_PUBLIC="$(dirname "$EDGE_CONFIG")/.backend-public.env"
if [ -s "$EDGE_PUBLIC" ]; then
  COMPOSE_ENV+=(--env-file "$EDGE_PUBLIC")
fi

# ── bring the stack up
BACKEND_DEPLOY_PHASE=compose_up
python3 "$DIR/network_prepare.py" --app "$APP" "${COMPOSE_ENV[@]}"
log "docker compose up -d --build"
docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" \
  up -d --build --remove-orphans

# ── discover the host port the service published on 127.0.0.1.
# The text `ps` PORTS column reliably shows `127.0.0.1:<port>-><target>/tcp`.
detect_port() {
  # Capture ONLY the published port after 127.0.0.1: (not the IP's own digits).
  docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" ps 2>/dev/null \
    | sed -nE 's/.*127\.0\.0\.1:([0-9]+)->.*/\1/p' | head -1
}
BACKEND_DEPLOY_PHASE=discover_port
PORT="$(detect_port || true)"
[ -n "${PORT:-}" ] || { log "ERROR: no 127.0.0.1:<port> publish found in compose ps"; \
  docker compose --project-name "$APP" ps; exit 1; }
log "service published on 127.0.0.1:$PORT"

# ── local health-check first (fast failure, no DNS/cert dependency).
# Generous: a first-ever deploy cold-inits Postgres and pulls base images.
BACKEND_DEPLOY_PHASE=local_health
HEALTHY=""
for i in $(seq 1 90); do
  if curl -fsS "http://127.0.0.1:$PORT$HEALTH" >/dev/null 2>&1; then
    log "container healthy on :$PORT$HEALTH (after ${i}x2s)"; HEALTHY=1; break
  fi
  [ $((i % 15)) -eq 0 ] && log "waiting for health… (${i}x2s)"
  sleep 2
done
if [ -z "$HEALTHY" ]; then
  log "ERROR: container never became healthy on :$PORT$HEALTH"
  docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" ps
  docker compose "${COMPOSE_ENV[@]}" --project-name "$APP" logs --tail 60
  exit 1
fi

# ── container-only deploy (no domain): stop here
if [ -z "$DOMAIN" ]; then
  log "no domain given — container deployed, skipping nginx/cert"
  exit 0
fi

# ── nginx vhost → proxy the domain to the published port
BACKEND_DEPLOY_PHASE=configure_vhost
VHOST="/etc/nginx/conf.d/backend-$APP.conf"
CERT_VHOST="$VHOST"
if [ "${EDGE_MODE:-legacy}" = edge ]; then
  CERT_VHOST="$EDGE_VHOST"
  python3 "$DIR/edge_vhost.py" configure "$EDGE_CONFIG" "$APP" "$DOMAIN" "$PORT"
else
  # Tests and reloads nginx itself after a change, restoring the previous file on failure, so a change nginx
  # refuses never stays on disk to fail the host's next test.
  VHOST_STATE="$(python3 "$DIR/configure_vhost.py" "$VHOST" "$DOMAIN" "$PORT")"
  if [ "$VHOST_STATE" = changed ]; then
    log "updated nginx upstream for $DOMAIN → 127.0.0.1:$PORT"
  fi
fi

# ── Let's Encrypt cert: failed inventory is never absence; only lock contention retries.
# The domain's own vhost tells whether `certbot install` would change anything.
BACKEND_DEPLOY_PHASE=certificate
if ensure_certificate "$DOMAIN" "$CERT_EMAIL" "$CERT_VHOST"; then
  log "certificate ready for $DOMAIN"
else
  status=$?
  log "ERROR: certbot failed for $DOMAIN (exit=$status)"
  exit "$status"
fi

# ── public health-check. This validates DNS, the certificate, nginx routing,
# and the application together; a green deploy must mean the public API works.
BACKEND_DEPLOY_PHASE=public_health
if [ "$EDGE_MODE" != legacy ]; then
  python3 "$DIR/edge_health.py" "$EDGE_CONFIG" "$APP" "$DOMAIN" "$HEALTH"
  log "done"
  exit 0
fi
PUBLIC_HEALTHY=""
for i in $(seq 1 15); do
  CURL_EXIT=0
  HTTP_CODE="$(curl --disable --silent --output /dev/null --write-out '%{http_code}' \
    --proto '=https' --connect-timeout 5 --max-time 10 "https://$DOMAIN$HEALTH")" || CURL_EXIT=$?
  if [ "$CURL_EXIT" -eq 0 ] && [[ "$HTTP_CODE" =~ ^2[0-9]{2}$ ]]; then
    log "public https://$DOMAIN$HEALTH healthy (after ${i}x2s)"
    PUBLIC_HEALTHY=1
    break
  fi
  log "public health not ready (curl=$CURL_EXIT, http=$HTTP_CODE, attempt=$i/15)"
  sleep 2
done
if [ -z "$PUBLIC_HEALTHY" ]; then
  log "ERROR: public https://$DOMAIN$HEALTH failed TLS or health validation"
  exit 1
fi
log "done"
