#!/bin/sh
# cloudron-memlimits.sh - right-size app memory limits [#685]
# Reads CLOUDRON_URL + CLOUDRON_API_TOKEN from ~/.creds/cloudron.env
# Usage: cloudron-memlimits.sh [canary|apply|plan]
#   plan   (default) show current vs proposed, change nothing
#   canary apply axiosheartstudios.com only, then verify
#   apply  apply all proposals
set -eu

ENV_FILE="$HOME/.creds/cloudron.env"
[ -f "$ENV_FILE" ] || { echo "missing $ENV_FILE"; exit 1; }
. "$ENV_FILE"
: "${CLOUDRON_URL:?}" "${CLOUDRON_API_TOKEN:?}"

MODE="${1:-plan}"

# fqdn:new-limit-MB  (from #685 census 2026-09-02, steady-state based)
PROPOSALS="
axiosheartstudios.com:512
community.turnsys.com:4096
nextcloud.knownelement.com:2048
bookmarks.knownelement.com:2048
# photos floor: Immich package manifest declares 3758096384B (3584MB) as
# the minimum user-settable limit; the census's 3072 was unsatisfiable
photos.knownelement.com:3584
share.knownelement.com:512
hfnfc.net:512
notes.knownelement.com:1024
office-onlyoffice.knownelement.com:1536
paperless.knownelement.com:2048
file-transfer.knownelement.com:512
digital-assets-manager.knownelement.com:512
pwvault.turnsys.com:512
finance.turnsys.com:512
finance.reachableceo.com:512
cmdb.knownelement.com:512
"

api() {
  curl -s -m 30 -H "Authorization: Bearer $CLOUDRON_API_TOKEN" "$@"
}

# fetch to a temp file with one retry: the 1MB apps list occasionally
# stalls mid-transfer, and a truncated body would otherwise fail jq later
fetch_apps() {
  local attempt out
  for attempt in 1 2; do
    out=$(api -o "$APPS_JSON" -w '%{http_code} %{size_download}' \
      "$CLOUDRON_URL/api/v1/apps?per_page=200") || true
    [ "${out%% *}" = "200" ] && [ "${out##* }" -gt 1000 ] && return 0
    echo "fetch attempt $attempt failed ($out), retrying" >&2
    sleep 2
  done
  return 1
}
APPS_JSON="/tmp/cloudron-apps.$$.json"
trap 'rm -f "$APPS_JSON"' EXIT
fetch_apps || { echo "API auth/list failed"; exit 1; }
jq -e '.apps' "$APPS_JSON" >/dev/null 2>&1 || { echo "API auth/list failed (bad JSON)"; exit 1; }

reclaimed=0
for line in $PROPOSALS; do
  fqdn=${line%%:*}; newmb=${line##*:}
  # API returns bare hosts in .fqdn (no scheme); subdomain apps carry
  # location:null, so matching fqdn alone is sufficient
  match=$(jq -r --arg f "$fqdn" \
    '.apps[] | select(.fqdn==$f) | "\(.id) \(.memoryLimit)"' "$APPS_JSON" | head -1) 
  [ -z "$match" ] && { echo "SKIP $fqdn (not an app)"; continue; }
  appid=${match%% *}; curmb=$(( ${match##* } / 1048576 ))
  [ "$curmb" -le "$newmb" ] && { echo "SKIP $fqdn (already ${curmb}MB <= ${newmb}MB)"; continue; }

  case "$MODE" in
    plan)
      echo "PLAN $fqdn: ${curmb}MB -> ${newmb}MB (reclaim $((curmb - newmb))MB)"
      ;;
    canary)
      [ "$fqdn" != "axiosheartstudios.com" ] && continue
      echo "CANARY $fqdn: ${curmb}MB -> ${newmb}MB"
      api -X POST "$CLOUDRON_URL/api/v1/apps/$appid/configure/memory_limit" -H 'Content-Type: application/json' \
        -d "{\"memoryLimit\": $((newmb * 1048576))}" | jq -r '.status // .message // .'
      ;;
    apply)
      echo "APPLY $fqdn: ${curmb}MB -> ${newmb}MB"
      api -X POST "$CLOUDRON_URL/api/v1/apps/$appid/configure/memory_limit" -H 'Content-Type: application/json' \
        -d "{\"memoryLimit\": $((newmb * 1048576))}" | jq -r '.status // .message // .'
      sleep 8
      ;;
  esac
  reclaimed=$((reclaimed + curmb - newmb))
done

echo "---"
echo "mode=$MODE total-limit-reclaim=${reclaimed}MB"
