#!/usr/bin/env bash
# node-check.sh — projde všechny běžící Docker kontejnery (Coolify server),
# zjistí verzi Node.js a porovná ji s nejnovější verzí dané hlavní řady z nodejs.org.
# Rozliší, jestli kontejneru chybí bezpečnostní vydání, a ohlásí řady po konci podpory.
#
# Použití:
#   ./node-check.sh            # kontrola proti aktuálním verzím z nodejs.org
#   ./node-check.sh --all      # vypíše i kontejnery bez Node.js
#   ./node-check.sh --rebuild  # u zastaralých spustí v Coolify force rebuild (se potvrzením)
#   ./node-check.sh --rebuild --yes   # bez ptaní (cron)
#   MIN_22=22.23.4 ./node-check.sh    # ruční minimální verze řady (např. z security advisory)
#
# --rebuild volá Coolify přímo přes `docker exec coolify php artisan tinker` — žádný token.
# Když tinker selže (interní funkce Coolify se můžou s updatem změnit) a je nastavený
# COOLIFY_TOKEN, zkusí se API (COOLIFY_URL, výchozí http://localhost:8000).
#
# Exit code: 0 = vše aktuální, 1 = něco je zastaralé nebo bez podpory (vhodné pro cron).

set -uo pipefail

SHOW_ALL=0; REBUILD=0; ASSUME_YES=0
for arg in "$@"; do
  case "$arg" in
    --all)     SHOW_ALL=1 ;;
    --rebuild) REBUILD=1 ;;
    --yes|-y)  ASSUME_YES=1 ;;
    *) echo "neznámá volba: $arg (--all, --rebuild, --yes)"; exit 2 ;;
  esac
done

RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'; DIM=$'\e[2m'; BOLD=$'\e[1m'; RESET=$'\e[0m'
[[ -t 1 ]] || { RED=""; GREEN=""; YELLOW=""; DIM=""; BOLD=""; RESET=""; }

command -v docker >/dev/null || { echo "docker nenalezen"; exit 2; }

COOLIFY_URL="${COOLIFY_URL:-http://localhost:8000}"
COOLIFY_TOKEN="${COOLIFY_TOKEN:-}"
TODAY=$(date +%F)

# Seznam všech vydání (jeden řádek = jedno vydání, nejnovější nahoře, s příznakem "security")
# a plán podpory jednotlivých řad. Stahuje se jednou za běh, až když je potřeba.
INDEX=""; SCHEDULE=""; FETCHED=0
fetch_release_data() {
  (( FETCHED )) && return
  FETCHED=1
  INDEX=$(curl -fsS --max-time 15 https://nodejs.org/dist/index.json 2>/dev/null)
  SCHEDULE=$(curl -fsS --max-time 10 https://raw.githubusercontent.com/nodejs/Release/main/schedule.json 2>/dev/null)
}

# Pro řadu $1 a verzi $2 vrátí "nejnovější|počet chybějících bezpečnostních vydání"
release_info() {
  local major="$1" cur="$2" var="MIN_$1"
  # ruční override přes env, např. MIN_22=22.23.4 — cokoli pod ním se bere jako bezpečnostní díra
  if [[ -n "${!var:-}" ]]; then
    version_lt "$cur" "${!var}" && echo "${!var}|1" || echo "${!var}|0"
    return
  fi
  awk -F'"' -v maj="$major" -v cur="$cur" '
    function gt(a, b,   x, y, i) {
      split(a, x, "."); split(b, y, ".")
      for (i = 1; i <= 3; i++) { if (x[i] + 0 > y[i] + 0) return 1; if (x[i] + 0 < y[i] + 0) return 0 }
      return 0
    }
    $4 ~ "^v" maj "\\." {
      v = substr($4, 2)
      if (latest == "") latest = v
      if (gt(v, cur) && /"security":true/) sec++
    }
    END { if (latest != "") print latest "|" sec + 0 }' <<<"$INDEX"
}

# Konec podpory řady $1 (YYYY-MM-DD) z plánu vydání
eol_date() {
  awk -v k="\"v$1\":" '$1 == k { f = 1 } f && /"end"/ { gsub(/[",]/, "", $2); print $2; exit }' <<<"$SCHEDULE"
}

# vrátí 0, pokud $1 < $2
version_lt() {
  [[ "$1" != "$2" ]] && [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" == "$1" ]]
}

# Verze Node.js uvnitř kontejneru. Nejdřív přes shell (jedno volání pro všechny cesty),
# pak napřímo binárka bez shellu — distroless image (gcr.io/distroless/nodejs) shell nemají.
node_version_in() {
  local v
  v=$(docker exec "$1" sh -c '
    for b in node nodejs /usr/local/bin/node /usr/bin/node /nodejs/bin/node /opt/nodejs/bin/node; do
      if command -v "$b" >/dev/null 2>&1; then "$b" -v 2>/dev/null; exit 0; fi
    done' 2>/dev/null | grep -oE '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
  if [[ -z "$v" ]]; then
    v=$(docker exec "$1" /nodejs/bin/node -v 2>/dev/null | grep -oE '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
  fi
  echo "${v#v}"
}

# Coolify přidává do názvů 24znakové UUID a časové razítko — pryč s nimi
strip_uuid() { sed -E 's/(^|-)[a-z0-9]{24}([-_].*)?$//' <<<"$1"; }

# Image bez registry; image postavené Coolify (<uuid>:<commit>) jako "(build)"
short_image() {
  local i="${1##*/}"
  [[ "$i" =~ ^[a-z0-9]{24}(_[a-z0-9-]+)?(:|$) ]] && i="(build)"
  echo "$i"
}

# Zkrátí na $2 znaků (ASCII), ať se nerozjedou sloupce
clip() { local s="$1" w="$2"; (( ${#s} > w )) && s="${s:0:w-2}.."; echo "$s"; }

# Řádky: pořadí \t stav \t název \t verze \t nejnovější \t podpora do \t image \t typ:uuid v Coolify (nebo -)
# pořadí: 0 chybí security, 1 konec podpory, 2 zastaralé, 3 nelze ověřit, 4 OK, 5 bez Node.js
rows=()
n_sec=0; n_eol=0; n_old=0; n_unknown=0; n_ok=0

while IFS='|' read -r id name image; do
  ver=$(node_version_in "$id")
  [[ -z "$ver" ]] && (( ! SHOW_ALL )) && continue

  # čitelný název: Coolify resourceName (+ compose služba), jinak jméno kontejneru;
  # compose projekt je u aplikací i služeb Coolify jejich UUID
  IFS='|' read -r res svc project managed ctype < <(docker inspect -f \
    '{{index .Config.Labels "coolify.resourceName"}}|{{index .Config.Labels "com.docker.compose.service"}}|{{index .Config.Labels "com.docker.compose.project"}}|{{index .Config.Labels "coolify.managed"}}|{{index .Config.Labels "coolify.type"}}' \
    "$id" 2>/dev/null)
  res=$(strip_uuid "${res:-}"); svc=$(strip_uuid "${svc:-}")
  [[ -z "$res" ]] && res="$name"
  [[ -n "$svc" && "$svc" != "$res" ]] && res="$res/$svc"
  uuid="-"
  if [[ "${managed:-}" == "true" && "${project:-}" =~ ^[a-z0-9]{24}$ ]]; then
    [[ "${ctype:-}" == "service" ]] && uuid="service:$project" || uuid="application:$project"
  fi

  app=$(clip "$res" 36)
  img=$(clip "$(short_image "$image")" 32)

  if [[ -z "$ver" ]]; then
    rows+=("5"$'\t'"none"$'\t'"$app"$'\t'"-"$'\t'"-"$'\t'"-"$'\t'"$img"$'\t'"$uuid")
    continue
  fi

  fetch_release_data
  major="${ver%%.*}"
  IFS='|' read -r latest sec < <(release_info "$major" "$ver")
  eol=$(eol_date "$major")

  if [[ -z "${latest:-}" ]]; then
    state="unknown"; order=3; latest="-"; ((n_unknown++))
  elif [[ -n "$eol" && "$eol" < "$TODAY" ]]; then
    state="eol"; order=1; ((n_eol++))
  elif (( sec > 0 )); then
    state="sec"; order=0; ((n_sec++))
  elif version_lt "$ver" "$latest"; then
    state="old"; order=2; ((n_old++))
  else
    state="ok"; order=4; ((n_ok++))
  fi
  rows+=("$order"$'\t'"$state"$'\t'"$app"$'\t'"$ver"$'\t'"$latest"$'\t'"${eol:--}"$'\t'"$img"$'\t'"$uuid")
done < <(docker ps --format '{{.ID}}|{{.Names}}|{{.Image}}')

if (( ${#rows[@]} == 0 )); then
  echo "Žádný běžící kontejner s Node.js."
  exit 0
fi

if (( FETCHED )) && [[ -z "$INDEX" ]]; then
  echo "${YELLOW}nodejs.org/dist/index.json se nepodařilo stáhnout — verze nejde ověřit.${RESET}"
fi

# šířka sloupce s názvem podle nejdelšího názvu
w_app=8
for r in "${rows[@]}"; do
  IFS=$'\t' read -r _ _ app _ _ _ _ _ <<<"$r"
  (( ${#app} > w_app )) && w_app=${#app}
done

# "NEJNOVĚJŠÍ" má 3 dvoubajtové znaky, printf počítá bajty → šířka +3
printf "  ${BOLD}%-*s  %-9s  %-*s  %-14s  %s${RESET}\n" "$w_app" "APLIKACE" "NODE" 13 "NEJNOVĚJŠÍ" "PODPORA DO" "IMAGE"

sorted=$(printf '%s\n' "${rows[@]}" | sort -t$'\t' -k1,1n -k3,3)
while IFS=$'\t' read -r _ state app ver latest eol img _; do
  case "$state" in
    sec)     mark="${RED}✗${RESET}";    color="$RED";    ecolor="" ;;
    eol)     mark="${RED}!${RESET}";    color="$RED";    ecolor="$RED"; eol="EOL $eol" ;;
    old)     mark="${YELLOW}↑${RESET}"; color="$YELLOW"; ecolor="" ;;
    unknown) mark="${YELLOW}?${RESET}"; color="$YELLOW"; ecolor="" ;;
    ok)      mark="${GREEN}✓${RESET}";  color="";        ecolor="" ;;
    none)    mark="${DIM}·${RESET}";    color="$DIM";    ecolor=""; ver="bez Node"; latest=""; eol="" ;;
  esac
  printf "%s %-*s  ${color}%-9s${RESET}  %-10s  ${ecolor}%-14s${RESET}  ${DIM}%s${RESET}\n" \
    "$mark" "$w_app" "$app" "$ver" "$latest" "$eol" "$img"
done <<<"$sorted"

echo
summary="${GREEN}${n_ok} OK${RESET}"
(( n_old ))     && summary="${YELLOW}${n_old} zastaralé${RESET}, $summary"
(( n_eol ))     && summary="${RED}${n_eol} bez podpory${RESET}, $summary"
(( n_sec ))     && summary="${RED}${n_sec} bez bezpečnostní opravy${RESET}, $summary"
(( n_unknown )) && summary="$summary, ${YELLOW}${n_unknown} nelze ověřit${RESET}"
echo "$summary"

(( n_sec || n_eol || n_old || n_unknown )) || exit 0

echo
(( n_sec ))     && echo "${DIM}✗ Od nainstalované verze vyšlo bezpečnostní vydání (security release) — rebuild co nejdřív.${RESET}"
(( n_eol ))     && echo "${DIM}! Řada je po konci podpory, opravy už nevycházejí — rebuild nepomůže, je čas na upgrade na LTS.${RESET}"
(( n_unknown )) && echo "${DIM}? nodejs.org pro tuhle řadu nic nevrátil (síť, nebo neznámá verze).${RESET}"

if (( ! REBUILD )); then
  (( n_sec || n_old )) && echo "${DIM}Redeploy v Coolify s vypnutou cache (force rebuild), nebo spusť s --rebuild. U připnutých tagů (node:22.23.3-alpine) nejdřív zvedni verzi v Dockerfile.${RESET}"
  exit 1
fi

# Force rebuild přímo v Coolify (tinker) — funkce, které volá i jeho API. Vypíše "ok|zpráva" / "fail|zpráva".
rebuild_tinker() {
  local kind="$1" uuid="$2" php out
  if [[ "$kind" == "service" ]]; then
    php='$s = App\Models\Service::where("uuid", "'"$uuid"'")->first();
      if (! $s) { echo "fail|služba v Coolify nenalezena"; return; }
      App\Actions\Service\StartService::run($s, pullLatestImages: true);
      echo "ok|služba se spouští s novými image";'
  else
    php='$a = App\Models\Application::where("uuid", "'"$uuid"'")->first();
      if (! $a) { echo "fail|aplikace v Coolify nenalezena"; return; }
      $r = queue_application_deployment(application: $a, deployment_uuid: new_public_id(), force_rebuild: true);
      echo (($r["status"] ?? "") === "queue_full" ? "fail|" : "ok|").($r["message"] ?? "");'
  fi
  out=$(docker exec coolify php artisan tinker --execute="try { $php } catch (\Throwable \$e) { echo 'fail|'.\$e->getMessage(); }" 2>&1 \
    | grep -E '^(ok|fail)\|' | tail -n1)
  echo "${out:-fail|tinker nevrátil výsledek}"
}

# Záloha přes Coolify API (jen s COOLIFY_TOKEN)
rebuild_api() {
  local uuid="$1" resp code body
  resp=$(curl -sS --max-time 20 -w $'\n%{http_code}' \
    -H "Authorization: Bearer $COOLIFY_TOKEN" -H "Accept: application/json" \
    "$COOLIFY_URL/api/v1/deploy?uuid=$uuid&force=true" 2>&1)
  code="${resp##*$'\n'}"; body="${resp%$'\n'*}"
  if [[ "$code" == 2* ]]; then echo "ok|přes API"
  else echo "fail|API HTTP $code: $(grep -o '"message":"[^"]*"' <<<"$body" | head -n1 | cut -d'"' -f4)"; fi
}

# --rebuild: zastaralé aplikace/služby Coolify (bez security opravy i jen starší), každá jen jednou
declare -A seen
targets=(); skipped=()
while IFS=$'\t' read -r _ state app _ _ _ _ ref; do
  [[ "$state" == "sec" || "$state" == "old" ]] || continue
  if [[ "$ref" == "-" ]]; then skipped+=("$app"); continue; fi
  [[ -n "${seen[$ref]:-}" ]] && continue
  seen[$ref]=1
  targets+=("$ref"$'\t'"${app%%/*}")
done <<<"$sorted"

for s in "${skipped[@]}"; do
  echo "${YELLOW}přeskočeno:${RESET} $s — není to aplikace ani služba Coolify"
done
(( ${#targets[@]} )) || exit 1

echo "${BOLD}Force rebuild:${RESET}"
for t in "${targets[@]}"; do ref="${t%%$'\t'*}"; echo "  ${t#*$'\t'} ${DIM}(${ref%%:*} ${ref#*:})${RESET}"; done

if (( ! ASSUME_YES )); then
  # stdin může být roura (curl | bash), ptáme se přes terminál
  if ! { exec 3</dev/tty; } 2>/dev/null; then
    echo "Bez terminálu se neptám — přidej --yes."; exit 1
  fi
  read -r -u 3 -p "Spustit rebuild u ${#targets[@]} aplikací? [a/N] " answer
  exec 3<&-
  [[ "$answer" =~ ^[aAyY] ]] || { echo "Nic se nespustilo."; exit 1; }
fi

queued=0
for t in "${targets[@]}"; do
  ref="${t%%$'\t'*}"; app="${t#*$'\t'}"
  kind="${ref%%:*}"; uuid="${ref#*:}"
  result=$(rebuild_tinker "$kind" "$uuid")
  if [[ "$result" == fail* && -n "$COOLIFY_TOKEN" ]]; then
    api=$(rebuild_api "$uuid")
    [[ "$api" == ok* ]] && result="$api (tinker: ${result#fail|})" || result="$result; $api"
  fi
  if [[ "$result" == ok* ]]; then
    echo "${GREEN}✓${RESET} $app — ${result#ok|}"; ((queued++))
  else
    echo "${RED}✗${RESET} $app — ${result#fail|}"
  fi
done
(( queued )) && echo "${DIM}Buildy běží v Coolify na pozadí (vidíš je v Deployments). Po dostavění spusť kontrolu znovu.${RESET}"
exit 1
