#!/bin/bash
set -euo pipefail

# grind-stack.sh - serial verify-stack lifecycle for package work [#632]
#
# One test stack at a time, built on labeled resources ONLY (label
# cloudron.grind=1): an ephemeral postgres/redis/mysql matching the
# package manifest's addons, the built app image with the same
# CLOUDRON_* env Cloudron injects, and a strict teardown. Nothing
# outside the label is ever stopped or removed.
#
# Usage:
#   scripts/grind-stack.sh up <pkg-dir>    build image + start verify stack
#   scripts/grind-stack.sh curl [path]     probe the app (default: / )
#   scripts/grind-stack.sh logs            app container logs (tail 50)
#   scripts/grind-stack.sh down            teardown: containers, net, volume
#   scripts/grind-stack.sh sweep <pkg-dir> post-commit prune of the test
#                                          image + dangling build cache
#   scripts/grind-stack.sh preflight       disk + slice sanity check
#
# All containers run under ukrrs-batch.slice (cores 0-5, CPUWeight 25)
# so the desktop/gateway never starves. Serial by design: `up` refuses
# to start a second stack while one is live.

LABEL="cloudron.grind=1"
NET="grind-test"
TESTPORT=18080
SLICE="ukrrs-batch.slice"
APP="grind-app"; PG="grind-pg"; RD="grind-redis"; MY="grind-mysql"

die() { echo "ERROR: $*" >&2; exit 1; }

label_args() { echo "--label $LABEL"; }

stack_running() {
  [ -n "$(docker ps -q --filter label="$LABEL" 2>/dev/null)" ]
}

manifest_field() { jq -r "$1" "$PKG/CloudronManifest.json"; }

cmd_up() {
  PKG="${1:?usage: grind-stack.sh up <pkg-dir>}"
  [ -f "$PKG/CloudronManifest.json" ] || die "no CloudronManifest.json in $PKG"
  APPNAME="$(basename "$PKG" | tr '[:upper:]' '[:lower:]')"
  PORT="$(manifest_field .httpPort)"
  MEM="$(manifest_field .memoryLimit)"
  ADDONS="$(jq -r '.addons | keys[]' "$PKG/CloudronManifest.json" | sort | tr '\n' ' ')"

  stack_running && die "a grind stack is already up - run 'down' first (serial policy)"

  echo "== grind up: $APPNAME (port $PORT, addons: ${ADDONS:-none})"

  # disk guard: the docker root must have room for image + test stack
  ROOT="$(docker info --format '{{.DockerRootDir}}')"
  AVAIL_GB=$(df -BG --output=avail "$(dirname "$ROOT")" | tail -1 | tr -dc '0-9')
  [ "$AVAIL_GB" -ge 20 ] || die "only ${AVAIL_GB}G free on the docker root - sweep first"

  docker build --cgroup-parent "$SLICE" -t "grind-$APPNAME:test" "$PKG"

  docker network create --label "$LABEL" "$NET" 2>/dev/null || true

  case "$ADDONS" in *postgresql*)
    docker run -d --rm --name "$PG" --cgroup-parent "$SLICE" --network "$NET" \
      $(label_args) -e POSTGRES_DB=testdb -e POSTGRES_USER=testdb \
      -e POSTGRES_PASSWORD=testpg postgres:16-alpine >/dev/null ;;
  esac
  case "$ADDONS" in *mysql*)
    docker run -d --rm --name "$MY" --cgroup-parent "$SLICE" --network "$NET" \
      $(label_args) -e MYSQL_DATABASE=testdb -e MYSQL_USER=testdb \
      -e MYSQL_PASSWORD=testmy -e MYSQL_ROOT_PASSWORD=testroot \
      mysql:8.4 >/dev/null ;;
  esac
  case "$ADDONS" in *redis*)
    docker run -d --rm --name "$RD" --cgroup-parent "$SLICE" --network "$NET" \
      $(label_args) redis:7-alpine --requirepass testrd >/dev/null ;;
  esac

  # the same env contract Cloudron injects, pointed at the test services.
  # No --rm on the app: a crashed container must keep its logs for `logs`.
  docker run -d --name "$APP" --cgroup-parent "$SLICE" --network "$NET" \
    $(label_args) -v "grind-$APPNAME-data:/app/data" \
    -p "127.0.0.1:$TESTPORT:$PORT" \
    -e CLOUDRON_POSTGRESQL_HOST="$PG" -e CLOUDRON_POSTGRESQL_PORT=5432 \
    -e CLOUDRON_POSTGRESQL_DATABASE=testdb -e CLOUDRON_POSTGRESQL_USERNAME=testdb \
    -e CLOUDRON_POSTGRESQL_PASSWORD=testpg \
    -e CLOUDRON_MYSQL_HOST="$MY" -e CLOUDRON_MYSQL_PORT=3306 \
    -e CLOUDRON_MYSQL_DATABASE=testdb -e CLOUDRON_MYSQL_USERNAME=testdb \
    -e CLOUDRON_MYSQL_PASSWORD=testmy \
    -e CLOUDRON_REDIS_HOST="$RD" -e CLOUDRON_REDIS_PORT=6379 \
    -e CLOUDRON_REDIS_PASSWORD=testrd -e CLOUDRON_REDIS_URL=redis://:testrd@"$RD":6379 \
    -e CLOUDRON_APP_DOMAIN="$APPNAME.test" \
    -e CLOUDRON_APP_ORIGIN="https://$APPNAME.test" \
    -e CLOUDRON_OIDC_ISSUER=https://sso.test -e CLOUDRON_OIDC_CLIENT_ID=grind \
    -e CLOUDRON_OIDC_CLIENT_SECRET=grind-secret \
    "grind-$APPNAME:test" >/dev/null

  echo "stack up: app on http://127.0.0.1:$TESTPORT (Host: $APPNAME.test)"
  echo "next: curl / logs / down / sweep"
}

cmd_curl() {
  PATH_="${1:-/}"
  docker ps --filter name="$APP" --filter label="$LABEL" --format '{{.Image}}' | grep -q . \
    || die "no grind app running"
  HOST="$(docker inspect "$APP" --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^CLOUDRON_APP_DOMAIN=//p')"
  CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 8 -H "Host: ${HOST:-app.test}" "http://127.0.0.1:$TESTPORT$PATH_")
  echo "GET $PATH_ -> $CODE (with Host: ${HOST:-app.test})"
}

cmd_logs() { docker logs --tail 50 "$APP" 2>&1; }

cmd_down() {
  echo "== grind down: removing labeled stack"
  docker rm -f "$APP" "$PG" "$RD" "$MY" >/dev/null 2>&1 || true
  docker network rm "$NET" >/dev/null 2>&1 || true
  for v in $(docker volume ls --filter name='grind-*-data' -q 2>/dev/null); do
    docker volume rm "$v" >/dev/null 2>&1 || true
  done
  echo "stack down (labeled resources only)"
}

cmd_sweep() {
  PKG="${1:?usage: grind-stack.sh sweep <pkg-dir>}"
  APPNAME="$(basename "$PKG" | tr '[:upper:]' '[:lower:]')"
  echo "== grind sweep: $APPNAME"
  stack_running && die "stack still up - run 'down' before sweep"
  docker image rm "grind-$APPNAME:test" >/dev/null 2>&1 || echo "  (test image already gone)"
  docker builder prune -f >/dev/null && echo "  build cache pruned (dangling only)"
  echo "swept. Base images are kept for reuse; fleets untouched."
}

SYFT_IMAGE="anchore/syft:v1.18.1@sha256:b8c170b8e51bfc4779ec3ef4399942c57290f5ce76a9c3af564c9d00d4946a6b"

cmd_sbom() {
  PKG="${1:?usage: grind-stack.sh sbom <pkg-dir>}"
  [ -f "$PKG/CloudronManifest.json" ] || die "no CloudronManifest.json in $PKG"
  APPNAME="$(basename "$PKG" | tr '[:upper:]' '[:lower:]')"
  IMAGE="grind-$APPNAME:test"
  docker image inspect "$IMAGE" >/dev/null 2>&1 || die "no test image $IMAGE - run 'up' first (sbom runs BEFORE sweep)"
  echo "== grind sbom: $IMAGE -> CycloneDX"
  # syft via pinned container (no host installs); reads the docker daemon
  docker run --rm --cgroup-parent "$SLICE" -v /var/run/docker.sock:/var/run/docker.sock \
    "$SYFT_IMAGE" "$IMAGE" -o cyclonedx-json > "$PKG/sbom.cyclonedx.json" \
    || die "syft failed"
  jq -r '"  components: \(.components | length), tool: \(.metadata.tools[0].name // "?") \(.metadata.tools[0].version // "")"' "$PKG/sbom.cyclonedx.json" 2>/dev/null || true
  echo "wrote $PKG/sbom.cyclonedx.json [#834] - commit it with the package"
}

cmd_preflight() {
  ROOT="$(docker info --format '{{.DockerRootDir}}')"
  echo "docker root: $ROOT"
  df -h "$ROOT" | tail -1 | sed 's/^/  /'
  systemctl cat "$SLICE" >/dev/null 2>&1 && echo "slice $SLICE: present" || echo "WARN: slice $SLICE missing"
  stack_running && echo "stack: UP (serial lock held)" || echo "stack: down"
  docker system df | sed 's/^/  /'
}

case "${1:-preflight}" in
  up)        shift; cmd_up "$@" ;;
  curl)      shift; cmd_curl "$@" ;;
  logs)      cmd_logs ;;
  down)      cmd_down ;;
  sweep)     shift; cmd_sweep "$@" ;;
  preflight) cmd_preflight ;;
  *) echo "usage: $0 up <pkg-dir> | curl [path] | logs | down | sweep <pkg-dir> | preflight"; exit 2 ;;
esac
