#!/usr/bin/env bash
# AutoSeg Viewer + Agfa Enterprise Imaging (ambiente demo.agfa.net)
#
#   bash agfa.sh teste    SECRET   testa login e DICOMweb do Agfa, como o viewer faz
#   bash agfa.sh instalar SECRET   instala/atualiza o viewer ligado ao Agfa
#   bash agfa.sh sondar   SECRET   diagnóstico de 401/403: papéis do token e variações da busca
#   bash agfa.sh relatorio SECRET [StudyInstanceUID]
#                                  testa TUDO o que o viewer usa do DICOMweb, passo a passo,
#                                  com URLs, erros, diagnóstico e causas; salva em /root
#   bash agfa.sh buscar   SECRET [filtros]   pesquisa estudos no PACS (MOSTRA dados de paciente)
#       filtros: nome=SILVA  id=12345  data=20260101-20261112  dias=7  modalidade=CT
#                acesso=A123  limite=25  pagina=2  estudo=StudyInstanceUID (lista as séries)
#
# O SECRET é o client secret do Keycloak do Agfa (32 caracteres), NÃO o token
# (eyJ...). Nada aqui grava o secret em disco; o instalador o guarda num Secret
# do Kubernetes. A saída mostra só códigos HTTP, contagens e UIDs, sem nomes,
# exceto em "buscar", que mostra os pacientes na tela e não salva nada.
set -u

ACAO=${1:-}
SEC=$(printf '%s' "${2:-}" | tr -d '[:space:]')
[ $# -ge 2 ] && shift 2 || shift $#

CID=${AGFA_CLIENT_ID:-xeroticket}
H=${AGFA_HOST:-ahei01.demo.agfa.net}
if [ -n "${AGFA_IP:-}" ]; then IP=$AGFA_IP; IP_ORIGEM="definido em AGFA_IP"
else
  IP=$(getent ahostsv4 "$H" 2>/dev/null | awk 'NR==1{print $1}')
  if [ -n "$IP" ]; then IP_ORIGEM="DNS da máquina"
  else IP=192.168.93.5; IP_ORIGEM="fixo: a máquina não resolve o nome; IP do arquivo hosts da estação Windows"; fi
fi
WADOURI=${AGFA_WADO_URI:-}
VM=${VIEWER_IP:-192.168.93.202}
VER=${VIEWER_VERSION:-1.4.6}
TURL=${AGFA_TOKEN_URL:-https://$H:1243/auth/realms/EI/protocol/openid-connect/token}
BASE=${AGFA_DICOMWEB:-https://$H/dicom-web}
CAF=${AGFA_CA:-/root/ca-agfa.pem}
PKG=https://pkg.rtmedical.ai/api/packages/apps/generic/viewer

die() { echo "ERRO: $*" >&2; exit 1; }
uso() { sed -n '4,12p' "$0" | sed 's/^# //'; exit 1; }

case $ACAO in teste|instalar|sondar|relatorio|buscar) ;; *) uso ;; esac
[ -n "$SEC" ] || die "falta o secret. Ex.: bash agfa.sh $ACAO 9mne...Comh"
case $SEC in eyJ*|*eyJhbGci*) die "isso é (ou contém) o TOKEN, não o secret. Use só o client secret de 32 caracteres." ;; esac
[ ${#SEC} -eq 32 ] || echo "aviso: o secret tem ${#SEC} caracteres (o do Keycloak costuma ter 32)."

# Certificados do Agfa (porta 443 e 1243), usados como "CA" do PACS.
garantir_ca() {
  if grep -q 'BEGIN CERT' "$CAF" 2>/dev/null; then return 0; fi
  echo "criando $CAF com os certificados do $H ..."
  : > "$CAF"
  for p in 443 1243; do
    echo | openssl s_client -connect "$IP:$p" -servername "$H" 2>/dev/null | openssl x509 >> "$CAF" 2>/dev/null
  done
  grep -q 'BEGIN CERT' "$CAF" || die "não consegui ler o certificado do $H ($IP)"
}

TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
TLS=(--cacert "$CAF")
# O nome do Agfa não tem DNS: resolve $H -> $IP em cada porta usada pelas URLs.
porta() { local u=${1#*://}; u=${u%%/*}
  case $u in *:*) echo "${u##*:}" ;; *) case $1 in https:*) echo 443 ;; *) echo 80 ;; esac ;; esac; }
RES=(); for p in $(printf '%s\n' "$(porta "$TURL")" "$(porta "$BASE")" | sort -u); do RES+=(--resolve "$H:$p:$IP"); done
c() { curl -sS -m 30 "${RES[@]}" "${TLS[@]}" "$@"; }
tok() { c -X POST "$TURL" --data-urlencode grant_type=client_credentials \
          --data-urlencode "client_id=$CID" --data-urlencode "client_secret=$SEC"; }

PY=$(command -v python3 || command -v /usr/libexec/platform-python || true)
# pick TAG MODALIDADES -> "<quantidade> <valor da TAG no 1º item, preferindo as modalidades>"
pick() {
  [ -n "$PY" ] || { echo "? -"; return; }
  "$PY" -c '
import json,sys
tag,pref=sys.argv[1],sys.argv[2].split(",")
try: d=json.load(open(sys.argv[3]))
except Exception: print("0 -"); sys.exit()
if not isinstance(d,list): print("0 -"); sys.exit()
def v(x,t):
    try: return x[t]["Value"][0]
    except Exception: return ""
best=[x for x in d if v(x,"00080060") in pref] or d
print(len(d), (v(best[0],tag) or "-") if best else "-")
' "$1" "$2" "$TMP/r.json"
}
# q ROTULO URL -> GET com token; mostra HTTP e, se não for 200, o começo da resposta
q() {
  local code
  code=$(c -H "Authorization: Bearer $TOK" -H 'Accept: application/dicom+json' \
           -o "$TMP/r.json" -w '%{http_code}' "$2")
  echo "$1: HTTP $code, $({ wc -c < "$TMP/r.json"; } 2>/dev/null || echo 0) bytes"
  if [ "$code" != 200 ]; then head -c 300 "$TMP/r.json" 2>/dev/null; echo; return 1; fi
}

obter_token() {
  echo "== 1. token ($CID)"
  local j rc; j=$(tok 2>&1); rc=$?
  case $rc in 35|51|53|54|58|59|60|64|66|77|80|82|83|90|91)
    echo "o certificado do Agfa NÃO confere com $CAF (curl $rc): $(printf '%s' "$j" | head -c 200)"
    echo "repetindo sem verificar o certificado, só para ver se o resto funciona..."
    TLS=(-k); j=$(tok 2>&1) ;;
  esac
  TOK=$(printf '%s' "$j" | sed -n 's/.*"access_token" *: *"\([^"]*\)".*/\1/p')
  if [ -z "$TOK" ]; then echo "token: FALHOU"; printf '%s\n' "$j" | head -c 300; echo; exit 1; fi
  echo "token: ok (${#TOK} caracteres), TLS: ${TLS[*]}"
}

teste() {
  garantir_ca
  obter_token
  echo "== 2. busca de estudos (QIDO)"
  q estudos "$BASE/studies?limit=10" || exit 1
  local n st se sop
  read -r n st <<< "$(pick 0020000D x)"; echo "estudos: $n, primeiro: $st"
  [ "$st" != "-" ] || { echo "nenhum estudo retornado"; exit 1; }
  echo "== 3. séries"
  q series "$BASE/studies/$st/series" || exit 1
  read -r n se <<< "$(pick 0020000E CT,MR)"; echo "séries: $n, escolhida: $se"
  echo "== 4. metadados (WADO-RS)"
  q metadados "$BASE/studies/$st/series/$se/metadata"
  echo "== 5. imagem (WADO-RS)"
  q instancias "$BASE/studies/$st/series/$se/instances" || exit 1
  read -r n sop <<< "$(pick 00080018 x)"; echo "imagens na série: $n"
  c -H "Authorization: Bearer $TOK" -H 'Accept: multipart/related; type="application/dicom"' \
    -o "$TMP/i.bin" -w 'imagem: HTTP %{http_code}, %{size_download} bytes\n' \
    "$BASE/studies/$st/series/$se/instances/$sop"
  echo "== link do estudo no viewer"
  echo "https://$VM/viewer/launch?url=$BASE/studies/$st"
}

# ---------------------------------------------------------------------------
# buscar: pesquisa no PACS (QIDO-RS), como a futura tela "Pesquisar no PACS".
# É a única ação que mostra dados de paciente; nada é gravado em disco.

# qido URL [args do curl] -> GET com token; mostra a URL exata e o HTTP; QC = código
qido() {
  local u=$1 out; shift
  out=$(c -G -H "Authorization: Bearer $TOK" -H 'Accept: application/dicom+json' \
          -o "$TMP/r.json" -w '%{http_code} %{url_effective}' "$@" "$u" 2>"$TMP/err")
  QC=${out%% *}
  echo "GET ${out#* }"
  echo "    HTTP $QC, $({ wc -c < "$TMP/r.json"; } 2>/dev/null || echo 0) bytes"
  [ "$QC" = 200 ] || [ "$QC" = 204 ]
}

# Explica por que a pesquisa não voltou 200 e sai.
qido_falhou() {
  echo
  case $QC in
    403) echo "RECUSADO (403): o Agfa reconheceu o token da conta $CID, mas ela não tem"
         echo "permissão de pesquisa no DICOMweb. Isso é liberado pelo administrador do Agfa."
         echo "Detalhes para mandar ao Agfa: bash agfa.sh relatorio SECRET" ;;
    401) echo "RECUSADO (401): o Agfa não aceitou o token. Confira o secret e o client $CID." ;;
    400) echo "O Agfa não entendeu a pesquisa (400). Tente um filtro por vez para achar qual ele não aceita." ;;
    000) echo "Sem resposta do Agfa: $(head -c 200 "$TMP/err")"
         echo "Confira a rede até $H ($IP) e o certificado em $CAF." ;;
    *)   echo "Resposta inesperada ($QC)." ;;
  esac
  if [ "$QC" != 000 ] && [ -s "$TMP/r.json" ]; then
    echo "começo da resposta:"; head -c 300 "$TMP/r.json" | tr -s '\r\n' '  '; echo
  fi
  exit 1
}

# mostrar estudos|series ARQUIVO [StudyUID] -> tabela legível + link do viewer
mostrar() {
  [ -n "$PY" ] || { echo "(sem python3: resposta crua)"; head -c 3000 "$TMP/r.json"; echo; return 0; }
  "$PY" -c '
import json,sys,urllib.parse
modo,arq,vm,base,uid=sys.argv[1:6]
try: d=json.load(open(arq))
except Exception: d=[]
if not isinstance(d,list): d=[]
def v(x,t):
    try: vals=x[t]["Value"]
    except Exception: return ""
    out=[]
    for a in vals:
        if isinstance(a,dict): a=a.get("Alphabetic") or a.get("Ideographic") or ""
        out.append(str(a))
    return "/".join(out)
def dt(s): return s[6:8]+"/"+s[4:6]+"/"+s[0:4] if len(s)>=8 and s[:8].isdigit() else (s or "-")
def nome(s): return ", ".join(p for p in s.split("^")[:2] if p).strip() or "(sem nome)"
def num(s):
    try: return int(float(s))
    except Exception: return 0
abrir="https://%s/viewer/launch?url=%s" % (vm, urllib.parse.quote(base+"/studies/", safe=""))
if modo=="estudos":
    if not d: sys.exit(3)
    d.sort(key=lambda x: v(x,"00080020")+v(x,"00080030"), reverse=True)
    print("%d estudo(s), do mais recente para o mais antigo:" % len(d))
    for n,x in enumerate(d,1):
        se,im=v(x,"00201206"),v(x,"00201208")
        info=[ "ID "+(v(x,"00100020") or "-"), "nasc. "+dt(v(x,"00100030")) ]
        if v(x,"00080050"): info.append("acesso "+v(x,"00080050"))
        if v(x,"00081030"): info.append(v(x,"00081030"))
        if se or im: info.append("%s séries, %s imagens" % (se or "?", im or "?"))
        print()
        print("%3d. %s  %-16s  %s" % (n, dt(v(x,"00080020")), v(x,"00080061") or "-", nome(v(x,"00100010"))))
        print("     " + " | ".join(info))
        print("     UID   " + (v(x,"0020000D") or "-"))
        if v(x,"0020000D"): print("     abrir " + abrir + v(x,"0020000D"))
else:
    if not d: sys.exit(3)
    d.sort(key=lambda x: num(v(x,"00200011")))
    print("%d série(s):" % len(d))
    for x in d:
        print()
        print("  %-9s nº %-5s %6s imagens  %s" % (v(x,"00080060") or "-", v(x,"00200011") or "-",
              v(x,"00201209") or "?", v(x,"0008103E")))
        print("            UID " + (v(x,"0020000E") or "-"))
    print()
    print("abrir o estudo no viewer:")
    print(abrir + uid)
' "$1" "$TMP/r.json" "$VM" "$BASE" "${2:-}"
}

# DD/MM/AAAA -> AAAAMMDD (AAAAMMDD passa direto)
data_dicom() { case $1 in ??/??/????) echo "${1:6:4}${1:3:2}${1:0:2}" ;; *) echo "$1" ;; esac; }

buscar() {
  local a k v estudo='' limite=25 pagina=1 f=() d1 d2 rc
  for a in "$@"; do
    k=${a%%=*}; v=${a#*=}
    if [ "$k" = "$a" ] || [ -z "$v" ]; then
      die "filtro inválido: '$a'. Use nome=valor, ex.: nome=SILVA  id=12345  dias=7"
    fi
    case $k in
      nome)
        case $v in *[*?]*) ;; *) v="$v*" ;; esac
        f+=(--data-urlencode "PatientName=$v") ;;
      id)
        f+=(--data-urlencode "PatientID=$v") ;;
      data)
        if [[ $v == *-* ]]; then d1=$(data_dicom "${v%%-*}"); d2=$(data_dicom "${v#*-}"); v="$d1-$d2"
        else v=$(data_dicom "$v"); fi
        [[ $v =~ ^([0-9]{8})?(-([0-9]{8})?)?$ ]] && [ "$v" != - ] \
          || die "data inválida: '$a'. Ex.: data=20261112  data=01/11/2026-12/11/2026  data=20260101-"
        f+=(--data-urlencode "StudyDate=$v") ;;
      dias)
        [[ $v =~ ^[0-9]+$ ]] || die "dias tem de ser um número, ex.: dias=7"
        f+=(--data-urlencode "StudyDate=$(date -d "-$v days" +%Y%m%d)-") ;;
      modalidade)
        f+=(--data-urlencode "ModalitiesInStudy=${v^^}") ;;
      acesso)
        f+=(--data-urlencode "AccessionNumber=$v") ;;
      limite)
        [[ $v =~ ^[1-9][0-9]*$ ]] || die "limite tem de ser um número, ex.: limite=50"; limite=$v ;;
      pagina)
        [[ $v =~ ^[1-9][0-9]*$ ]] || die "pagina tem de ser um número, ex.: pagina=2"; pagina=$v ;;
      estudo)
        [[ $v =~ ^[0-9.]+$ ]] || die "estudo= espera o StudyInstanceUID (só números e pontos)"; estudo=$v ;;
      *)
        die "filtro desconhecido: '$k'. Use nome, id, data, dias, modalidade, acesso, limite, pagina ou estudo" ;;
    esac
  done

  garantir_ca
  obter_token
  echo "ATENÇÃO: o resultado tem dados de pacientes. Não encaminhe esta tela."
  echo

  if [ -n "$estudo" ]; then
    echo "== 2. séries do estudo (QIDO-RS)"
    if ! qido "$BASE/studies/$estudo/series" --data-urlencode includefield=0008103E \
           --data-urlencode includefield=00200011 --data-urlencode includefield=00201209; then
      [ "$QC" = 400 ] || qido_falhou
      echo "    o Agfa não aceitou os campos extras (includefield); repetindo sem eles"
      qido "$BASE/studies/$estudo/series" || qido_falhou
    fi
    echo
    mostrar series "$estudo"; rc=$?
    [ "$rc" != 3 ] || echo "nenhuma série encontrada para esse StudyInstanceUID"
    return 0
  fi

  echo "== 2. pesquisa de estudos (QIDO-RS)"
  [ ${#f[@]} -gt 0 ] || echo "(sem filtros: mostra os primeiros $limite que o Agfa devolver; use dias=7 para os recentes)"
  local pag=(--data-urlencode "limit=$limite")
  [ "$pagina" = 1 ] || pag+=(--data-urlencode "offset=$(( (pagina - 1) * limite ))")
  if ! qido "$BASE/studies" "${f[@]}" "${pag[@]}" --data-urlencode includefield=00081030 \
         --data-urlencode includefield=00201206 --data-urlencode includefield=00201208 \
         --data-urlencode includefield=00100030; then
    [ "$QC" = 400 ] || qido_falhou
    echo "    o Agfa não aceitou os campos extras (includefield); repetindo sem eles"
    qido "$BASE/studies" "${f[@]}" "${pag[@]}" || qido_falhou
  fi
  echo
  mostrar estudos; rc=$?
  if [ "$rc" = 3 ]; then
    echo "nenhum estudo encontrado com esses filtros."
    echo "dicas:"
    echo "  - o nome no DICOM costuma ser SOBRENOME^NOME: tente só o sobrenome (nome=SILVA)"
    echo "  - curinga no começo, se o Agfa aceitar: nome='*SILVA*'"
    echo "  - os mais recentes, sem outros filtros: dias=7"
  else
    echo
    echo "séries de um estudo:  bash agfa.sh buscar SECRET estudo=<UID>"
    echo "próxima página:       acrescente pagina=$((pagina + 1)) aos mesmos filtros"
  fi
}

# Sem dados de paciente: só códigos HTTP, tamanhos e o conteúdo do token.
sondar() {
  garantir_ca
  RES+=(--resolve "$H:8443:$IP")
  obter_token
  echo "== 2. o que o token diz (para o administrador do Agfa)"
  [ -n "$PY" ] && "$PY" -c '
import base64,json,sys,datetime
p=sys.argv[1].split(".")[1]; d=json.loads(base64.urlsafe_b64decode(p+"="*(-len(p)%4)))
for k in ("iss","azp","aud","preferred_username","scope"): print(f"  {k}: {d.get(k)}")
print("  papéis do realm:", ", ".join(d.get("realm_access",{}).get("roles",[])) or "-")
for c,v in (d.get("resource_access") or {}).items(): print(f"  papéis do client {c}:", ", ".join(v.get("roles",[])))
print("  validade:", (d.get("exp",0)-d.get("iat",0))//60, "min")
' "$TOK"
  echo "== 3. variações da busca (código HTTP, tamanho)"
  local u a code
  s() { code=$(c "${@:3}" -o "$TMP/s.out" -w '%{http_code} %{size_download}B %{content_type}' "$2"); printf '  %-44s %s\n' "$1" "$code"; }
  A="Authorization: Bearer $TOK"
  s "sem token (esperado 302)"            "$BASE/studies?limit=1"
  s "token, sem filtro"                   "$BASE/studies?limit=1" -H "$A" -H 'Accept: application/dicom+json'
  s "token, filtro de data"               "$BASE/studies?limit=1&StudyDate=19000101-20991231" -H "$A" -H 'Accept: application/dicom+json'
  s "token, filtro de modalidade CT"      "$BASE/studies?limit=1&ModalitiesInStudy=CT" -H "$A" -H 'Accept: application/dicom+json'
  s "token, PatientID=*"                  "$BASE/studies?limit=1&PatientID=*" -H "$A" -H 'Accept: application/dicom+json'
  s "token, Accept application/json"      "$BASE/studies?limit=1" -H "$A" -H 'Accept: application/json'
  s "token, Accept */*"                   "$BASE/studies?limit=1" -H "$A" -H 'Accept: */*'
  s "token, nível paciente /patients"     "$BASE/patients?limit=1" -H "$A" -H 'Accept: application/dicom+json'
  s "token, porta 8443"                   "https://$H:8443/dicom-web/studies?limit=1" -H "$A" -H 'Accept: application/dicom+json'
  s "token, raiz /dicom-web/"             "$BASE/" -H "$A"
  echo "== 4. resposta do Agfa ao pedido sem filtro"
  c -H "$A" -H 'Accept: application/dicom+json' -D "$TMP/h.txt" -o "$TMP/s.out" "$BASE/studies?limit=1" >/dev/null 2>&1
  grep -i -E '^(HTTP|www-authenticate|server|x-)' "$TMP/h.txt" | sed 's/^/  /'
  if ! grep -q '^HTTP[^ ]* 200' "$TMP/h.txt"; then head -c 300 "$TMP/s.out" | sed 's/^/  /'; echo; fi
}

# ─────────────────────────────────────────────────────────────────────────────
# relatorio: testa, passo a passo, tudo o que o AutoSeg Viewer usa do DICOMweb
# (as mesmas URLs e cabeçalhos do backend: remote-source.service.ts) e imprime
# cada URL tentada, o resultado, o diagnóstico e as causas possíveis.
# Sem dados de paciente: só códigos HTTP, tempos, contagens, modalidades e UIDs.
# ─────────────────────────────────────────────────────────────────────────────
R_OK=(); R_FALHA=(); R_AVISO=(); R_NT=(); PRIMEIRA_FALHA=""
DIAG=()        # linhas do diagnóstico final
CAUSAS=()      # causas possíveis, em ordem
PEDIDOS=()     # o que pedir ao Agfa

st() {  # st N "título" STATUS
  local n=$1 t=$2 s=$3 pad
  pad=$(( 56 - ${#t} )); [ $pad -lt 3 ] && pad=3
  printf '\n[%s] %s %s %s\n' "$n" "$t" "$(printf '%*s' "$pad" '' | tr ' ' '.')" "$s"
  printf '%s' "$BUF"; BUF=""
  case $s in
    OK*) R_OK+=("$n") ;;
    FALHA*) R_FALHA+=("$n"); [ -n "$PRIMEIRA_FALHA" ] || PRIMEIRA_FALHA=$n ;;
    AVISO*) R_AVISO+=("$n") ;;
    *) R_NT+=("$n") ;;
  esac
}
# Pedidos feitos antes do st() ficam em BUF e saem logo abaixo do título do passo.
BUF=""
o() { if [ -n "$BUF" ]; then BUF+="$1"$'\n'; else printf '%s\n' "$1"; fi; }
i() { o "    $*"; }

# http MÉTODO URL ACCEPT TOKEN|- [args extras do curl]  ->  HC HT HS HCT CURL_RC; corpo em $TMP/b
http() {
  local m=$1 u=$2 a=$3 t=$4 out
  shift 4
  local args=(-X "$m" -D "$TMP/h" -o "$TMP/b" -w '%{http_code} %{time_total} %{size_download} %{content_type}')
  [ -n "$a" ] && args+=(-H "Accept: $a")
  [ "$t" != "-" ] && args+=(-H "Authorization: Bearer $t")
  : > "$TMP/h"; : > "$TMP/b"
  out=$(c "${args[@]}" "$@" "$u" 2>"$TMP/e"); CURL_RC=$?
  read -r HC HT HS HCT <<< "$out"
  HC=${HC:-000}
  BUF+="    $m $u"$'\n'
  [ -n "$a" ] && BUF+="        Accept: $a"$'\n'
  [ "$t" != "-" ] && BUF+="        Authorization: Bearer <token ${#t} caracteres>"$'\n'
  if [ "$HC" = 000 ]; then
    BUF+="        -> SEM RESPOSTA (curl $CURL_RC): $(tr '\n' ' ' < "$TMP/e" | head -c 220)"$'\n'
  else
    BUF+="        -> HTTP $HC em ${HT}s, $HS bytes, ${HCT:--}"$'\n'
  fi
}
corpo() { o "        resposta: $(tr '\r\n' '  ' < "$TMP/b" | head -c 220)"; }
cabecalhos() {
  local l
  while IFS= read -r l; do
    case ${l,,} in set-cookie:*) l="Set-Cookie: ${l#*: }"; l="${l%%=*}=<oculto>" ;; esac
    o "        | $l"
  done < <(sed 's/\r$//' "$TMP/h" | grep -v '^$' | head -20)
}

escrever_py() {
cat > "$TMP/j.py" <<'PY'
import json, sys, base64, datetime
cmd = sys.argv[1]
def load():
    try:
        d = json.load(open(sys.argv[2]))
        return d if isinstance(d, list) else None
    except Exception:
        return None
def v(x, t, all_=False):
    try:
        vals = x[t]["Value"]
        return vals if all_ else vals[0]
    except Exception:
        return [] if all_ else ""
if cmd == "count":
    d = load(); print("-" if d is None else len(d))
elif cmd == "study":          # escolhe estudo: prefere CT/MR + RT
    d = load() or []
    def score(x):
        m = set(v(x, "00080061", True)); s = 0
        if m & {"CT", "MR"}: s += 2
        if m & {"RTSTRUCT", "RTPLAN", "RTDOSE"}: s += 1
        return s
    best = sorted(d, key=score, reverse=True)
    if best: print(v(best[0], "0020000D") or "-", ",".join(v(best[0], "00080061", True)) or "?")
    else: print("- -")
elif cmd == "series":         # lista modalidades e UIDs: MOD UID NUM
    for x in load() or []:
        print(v(x, "00080060") or "?", v(x, "0020000E") or "-", v(x, "00201209") or "?")
elif cmd == "first":          # primeiro valor da TAG
    d = load() or []; print((v(d[0], sys.argv[3]) if d else "") or "-")
elif cmd == "claims":
    p = sys.argv[2].split(".")[1]
    d = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))
    ts = lambda k: datetime.datetime.fromtimestamp(d.get(k, 0), datetime.timezone.utc).strftime("%H:%M:%S UTC")
    print("emissor (iss) ......:", d.get("iss"))
    print("client (azp) .......:", d.get("azp"))
    print("destinatário (aud) .:", d.get("aud"))
    print("usuário ............:", d.get("preferred_username"))
    print("IP visto pelo login :", d.get("clientAddress", "-"))
    print("papéis do realm ....:", ", ".join(d.get("realm_access", {}).get("roles", [])) or "-")
    for c, r in (d.get("resource_access") or {}).items():
        print(f"papéis do client {c}:", ", ".join(r.get("roles", [])))
    print("escopo .............:", d.get("scope"))
    print("validade ...........:", ts("iat"), "->", ts("exp"), " id (jti):", d.get("jti"))
elif cmd == "short":
    p = sys.argv[2].split(".")[1]
    d = json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4)))
    print(f"aud={d.get('aud')}, user={d.get('preferred_username')}, realm roles={', '.join(d.get('realm_access', {}).get('roles', [])) or '-'}")
elif cmd == "iss":
    p = sys.argv[2].split(".")[1]
    print(json.loads(base64.urlsafe_b64decode(p + "=" * (-len(p) % 4))).get("iss", ""))
elif cmd == "ts":             # sintaxe de transferência do primeiro Part-10 na resposta
    b = open(sys.argv[2], "rb").read(); k = b.find(b"DICM")
    names = {"1.2.840.10008.1.2": "Implicit VR LE", "1.2.840.10008.1.2.1": "Explicit VR LE",
             "1.2.840.10008.1.2.2": "Explicit VR BE", "1.2.840.10008.1.2.4.50": "JPEG Baseline",
             "1.2.840.10008.1.2.4.57": "JPEG Lossless", "1.2.840.10008.1.2.4.70": "JPEG Lossless SV1",
             "1.2.840.10008.1.2.4.80": "JPEG-LS Lossless", "1.2.840.10008.1.2.4.81": "JPEG-LS Near-lossless",
             "1.2.840.10008.1.2.4.90": "JPEG 2000 Lossless", "1.2.840.10008.1.2.4.91": "JPEG 2000",
             "1.2.840.10008.1.2.5": "RLE Lossless", "1.2.840.10008.1.2.1.99": "Deflated"}
    if k < 0: print("não é DICOM Part-10 (sem 'DICM')"); sys.exit()
    p = k + 4; ts = sop = ""
    while p + 8 <= len(b):
        g = int.from_bytes(b[p:p+2], "little"); e = int.from_bytes(b[p+2:p+4], "little")
        if g != 2: break
        vr = b[p+4:p+6]
        if vr in (b"OB", b"OW", b"UN", b"SQ", b"UT", b"UC", b"UR"):
            n = int.from_bytes(b[p+8:p+12], "little"); val = b[p+12:p+12+n]; p += 12 + n
        else:
            n = int.from_bytes(b[p+6:p+8], "little"); val = b[p+8:p+8+n]; p += 8 + n
        s = val.rstrip(b"\x00 ").decode("ascii", "replace")
        if e == 0x10: ts = s
        if e == 0x02: sop = s
    print(f"{ts} ({names.get(ts, 'desconhecida')}); SOP Class {sop}")
PY
}

relatorio_corpo() {
  local STUDY=${1:-${AGFA_STUDY:-}} STUDY_ORIGEM="" SE="" SOP="" SERIES_LIST="" ok p ms t0 tok_ok=0
  escrever_py
  local agora; agora=$(date '+%d/%m/%Y %H:%M:%S %Z'); local utc; utc=$(date -u '+%H:%M:%S UTC')
  local vmip; vmip=$(hostname -I 2>/dev/null | awk '{print $1}')
  echo "=========================================================================="
  echo " RELATÓRIO DICOMweb — AutoSeg Viewer -> Agfa Enterprise Imaging"
  echo " $agora ($utc) · máquina $(hostname) (${vmip:-?}) · script agfa.sh 1.1.0"
  echo "=========================================================================="
  echo "CONFIGURAÇÃO TESTADA"
  i "servidor Agfa ....: $H -> $IP ($IP_ORIGEM)"
  i "login (token) ....: $TURL"
  i "DICOMweb .........: $BASE   (QIDO-RS e WADO-RS na mesma base)"
  i "WADO-URI .........: ${WADOURI:-(não configurado; opcional)}"
  i "client ...........: $CID (secret ${SEC:0:4}...${SEC: -4}, ${#SEC} caracteres; não aparece no relatório)"
  i "certificados .....: $CAF ($(grep -c 'BEGIN CERT' "$CAF" 2>/dev/null || echo 0) no arquivo)"
  i "estudo de teste ..: ${STUDY:-(o primeiro que a busca devolver)}"

  # [1] DNS ------------------------------------------------------------------
  local dns ns
  dns=$(getent ahostsv4 "$H" 2>/dev/null | awk 'NR==1{print $1}')
  ns=$(awk '/^nameserver/{printf "%s ", $2}' /etc/resolv.conf 2>/dev/null)
  if [ -z "$dns" ]; then
    st 1 "DNS: nome $H" "AVISO"
    i "a máquina NÃO resolve o nome (servidores DNS: ${ns:-?})."
    i "o teste usa o IP fixo $IP com o nome no pedido (curl --resolve), que é o que o"
    i "viewer faz com a opção --pacs-host. Isso funciona, desde que $IP seja mesmo o $H."
  elif [ "$dns" = "$IP" ]; then
    st 1 "DNS: nome $H" "OK"; i "resolve para $dns (servidores DNS: ${ns:-?})"
  else
    st 1 "DNS: nome $H" "AVISO"; i "o DNS dá $dns, mas o teste usa $IP ($IP_ORIGEM)"
  fi

  # [2] Rede -----------------------------------------------------------------
  ok=1
  st_rede=""
  for p in $(printf '%s\n' "$(porta "$TURL")" "$(porta "$BASE")" | sort -u); do
    t0=$(date +%s%N)
    if timeout 5 bash -c "echo > /dev/tcp/$IP/$p" 2>/dev/null; then
      ms=$(( ($(date +%s%N) - t0) / 1000000 )); st_rede+="porta $p aberta (${ms} ms); "
    else ok=0; st_rede+="porta $p SEM RESPOSTA; "; fi
  done
  if [ $ok = 1 ]; then st 2 "Rede até $IP" "OK"; else st 2 "Rede até $IP" "FALHA"; fi
  i "${st_rede%; }"
  if [ $ok = 0 ]; then
    DIAG+=("A máquina $(hostname) não alcança o servidor do Agfa em $IP.")
    CAUSAS+=("firewall entre ${vmip:-a VM} e $IP" "IP errado para $H (confirmar com o Agfa)" "serviço do Agfa parado")
    PEDIDOS+=("liberar ${vmip:-a VM} -> $IP nas portas $(porta "$TURL") e $(porta "$BASE")")
  fi

  # [3] Certificado ----------------------------------------------------------
  local cert subj iss fim host_ok="?" bp; bp=$(porta "$BASE")
  cert=$(echo | timeout 10 openssl s_client -connect "$IP:$bp" -servername "$H" 2>/dev/null | openssl x509 2>/dev/null)
  if [ -n "$cert" ]; then
    subj=$(printf '%s\n' "$cert" | openssl x509 -noout -subject 2>/dev/null | sed 's/^subject= *//')
    iss=$(printf '%s\n' "$cert" | openssl x509 -noout -issuer 2>/dev/null | sed 's/^issuer= *//')
    fim=$(printf '%s\n' "$cert" | openssl x509 -noout -enddate 2>/dev/null | sed 's/^notAfter=//')
    if printf '%s\n' "$cert" | openssl x509 -noout -checkhost "$H" 2>/dev/null | grep -q 'does match'; then host_ok=sim; else host_ok=não; fi
  fi
  http GET "$BASE/studies?limit=1" "application/dicom+json" -
  case $CURL_RC in
    35|51|53|54|58|59|60|64|66|77|80|82|83|90|91)
      st 3 "Certificado HTTPS do Agfa" "FALHA"
      i "o certificado não confere com $CAF (curl $CURL_RC): $(tr '\n' ' ' < "$TMP/e" | head -c 160)"
      DIAG+=("O certificado HTTPS do Agfa não é aceito com $CAF; o resto do teste seguiu sem verificar o certificado (-k).")
      CAUSAS+=("arquivo $CAF desatualizado ou sem a CA que emitiu o certificado do Agfa")
      PEDIDOS+=("enviar o certificado da CA (.pem) que emitiu o certificado de $H")
      TLS=(-k) ;;
    *) if [ -n "$cert" ]; then st 3 "Certificado HTTPS do Agfa" "OK"; else st 3 "Certificado HTTPS do Agfa" "AVISO"; fi ;;
  esac
  i "dono ......: ${subj:-?}"
  i "emissor ...: ${iss:-?}"
  i "válido até : ${fim:-?}"
  i "vale para o nome $H: $host_ok"

  # [4] Token ----------------------------------------------------------------
  http POST "$TURL" "" - --data-urlencode grant_type=client_credentials \
       --data-urlencode "client_id=$CID" --data-urlencode "client_secret=$SEC"
  i "    corpo: grant_type=client_credentials&client_id=$CID&client_secret=<oculto>"
  TOK=$(sed -n 's/.*"access_token" *: *"\([^"]*\)".*/\1/p' "$TMP/b")
  if [ -n "$TOK" ]; then
    tok_ok=1; st 4 "Login no Keycloak do Agfa (token)" "OK"
    if [ -n "$PY" ]; then "$PY" "$TMP/j.py" claims "$TOK" | sed 's/^/    /'; fi
  else
    st 4 "Login no Keycloak do Agfa (token)" "FALHA"; corpo
    local err; err=$(sed -n 's/.*"error" *: *"\([^"]*\)".*/\1/p' "$TMP/b")
    DIAG+=("O Keycloak do Agfa recusou o login do client $CID (HTTP $HC${err:+, $err}).")
    case $err in
      invalid_client) CAUSAS+=("secret errado ou client $CID inexistente/desativado no realm EI") ;;
      unauthorized_client) CAUSAS+=("o client $CID não tem 'Service Accounts' (client credentials) habilitado") ;;
      *) CAUSAS+=("URL do token errada (HTTP $HC)" "client sem client credentials") ;;
    esac
    PEDIDOS+=("confirmar client id, secret e 'Service Accounts Enabled' do client $CID")
  fi

  # [5] e [6] Proteção do DICOMweb --------------------------------------------
  local loc loc_host="" loc_cli=""
  http GET "$BASE/studies?limit=1" "application/dicom+json" -
  loc=$(sed -n 's/^[Ll]ocation: *//p' "$TMP/h" | tr -d '\r' | head -1)
  case $HC in
    302|303|307) loc_host=$(printf '%s' "$loc" | sed -E 's#^https?://([^:/]+).*#\1#')
                 loc_cli=$(printf '%s' "$loc" | sed -n 's/.*[?&]client_id=\([^&]*\).*/\1/p')
                 st 5 "DICOMweb sem login (deve ser recusado)" "OK"
                 i "redireciona para o login: $loc_host (client_id=${loc_cli:-?})" ;;
    401|403)     st 5 "DICOMweb sem login (deve ser recusado)" "OK"; i "recusa sem login (HTTP $HC)" ;;
    200)         st 5 "DICOMweb sem login (deve ser recusado)" "AVISO"; i "responde SEM login: o token nem é necessário" ;;
    404)         st 5 "DICOMweb sem login (deve ser recusado)" "FALHA"; corpo
                 DIAG+=("O caminho $BASE não existe (404).")
                 CAUSAS+=("URL do DICOMweb errada"); PEDIDOS+=("confirmar a URL base do DICOMweb (QIDO-RS e WADO-RS)") ;;
    *)           st 5 "DICOMweb sem login (deve ser recusado)" "AVISO"; corpo ;;
  esac
  http GET "$BASE/studies?limit=1" "application/dicom+json" "token-invalido-de-teste"
  case $HC in
    401|403) st 6 "DICOMweb com token falso (deve ser recusado)" "OK"; i "o DICOMweb valida o token (HTTP $HC)" ;;
    302|303) st 6 "DICOMweb com token falso (deve ser recusado)" "FALHA"
             i "com token o DICOMweb ainda manda para a tela de login: este endereço só aceita navegador"
             DIAG+=("$BASE não aceita token Bearer (redireciona para login mesmo com token).")
             CAUSAS+=("endpoint de navegador, não de sistemas")
             PEDIDOS+=("indicar a URL do DICOMweb para integração entre sistemas (que aceite Bearer token)") ;;
    *)       st 6 "DICOMweb com token falso (deve ser recusado)" "AVISO"; corpo ;;
  esac

  if [ $tok_ok = 0 ]; then
    for p in 7 8 9 10 11 12 13 14; do st "$p" "(depende do login)" "NÃO TESTADO"; done
    resumo; return
  fi

  # [7] Busca de estudos (QIDO-RS) ----------------------------------------------
  http GET "$BASE/studies?limit=10&includefield=00080061" "application/dicom+json" "$TOK"
  local qido_code=$HC
  if [ "$HC" = 200 ] || [ "$HC" = 204 ]; then
    local n; n=$("$PY" "$TMP/j.py" count "$TMP/b" 2>/dev/null || echo -)
    st 7 "Busca de estudos (QIDO-RS /studies)" "OK"; i "estudos devolvidos: $n"
    if [ -z "$STUDY" ]; then
      read -r STUDY mods <<< "$("$PY" "$TMP/j.py" study "$TMP/b")"
      [ "$STUDY" = "-" ] && STUDY=""
      [ -n "$STUDY" ] && { STUDY_ORIGEM="escolhido da busca"; i "estudo escolhido: $STUDY (modalidades: $mods)"; }
    fi
  else
    st 7 "Busca de estudos (QIDO-RS /studies)" "FALHA"; corpo
    i "cabeçalhos da resposta:"; cabecalhos
    i "outras formas da mesma busca (para descartar o formato do pedido):"
    local u
    for u in "$BASE/studies?limit=1" \
             "$BASE/studies?limit=1&StudyDate=19000101-20991231" \
             "$BASE/studies?limit=1&ModalitiesInStudy=CT" \
             "$BASE/studies?limit=1&PatientID=*" \
             "$BASE/patients?limit=1" \
             "$BASE/" ; do
      c -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $TOK" -H 'Accept: application/dicom+json' "$u" > "$TMP/x" 2>/dev/null
      printf '        HTTP %s  GET %s\n' "$(cat "$TMP/x")" "$u"
    done
    if [ "$(porta "$BASE")" = 443 ]; then
      RES+=(--resolve "$H:8443:$IP")
      u="https://$H:8443${BASE#https://$H}/studies?limit=1"
      c -o /dev/null -w '%{http_code}' -m 10 -H "Authorization: Bearer $TOK" -H 'Accept: application/dicom+json' "$u" > "$TMP/x" 2>/dev/null
      printf '        HTTP %s  GET %s\n' "$(cat "$TMP/x")" "$u"
    fi
    local tiss tiss_host; tiss=$("$PY" "$TMP/j.py" iss "$TOK" 2>/dev/null); tiss_host=$(printf '%s' "$tiss" | sed -E 's#^https?://([^:/]+).*#\1#')
    if [ "$HC" = 403 ]; then
      DIAG+=("O login funciona, mas o DICOMweb RECUSA (403 Forbidden) a conta service-account-$CID em todas as formas de busca.")
      CAUSAS+=("falta permissão: o token traz só os papéis padrão do realm EI; nenhum papel que libere o DICOMweb (QIDO-RS/WADO-RS)")
      CAUSAS+=("audience: o token vem com aud=account; o DICOMweb${loc_cli:+ (client $loc_cli)} pode exigir que o token seja destinado a ele")
      CAUSAS+=("usuário no EI: a conta service-account-$CID pode precisar existir no Enterprise Imaging com acesso aos estudos")
      CAUSAS+=("restrição por IP: o pedido sai de ${vmip:-esta máquina}; o EI pode aceitar só IPs ou sistemas cadastrados")
      if [ "$host_ok" = sim ] && [ "$loc_host" = "$H" ] && [ "$tiss_host" = "$H" ]; then
        DIAG+=("DNS/nome DESCARTADO: o certificado, o redirecionamento de login e o emissor do token apontam todos para $H; estamos falando com o servidor certo.")
      else
        CAUSAS+=("nome/DNS: certificado vale p/ $H=$host_ok; login redireciona p/ ${loc_host:-?}; emissor do token=${tiss_host:-?}")
      fi
      PEDIDOS+=("liberar LEITURA no DICOMweb (QIDO-RS e WADO-RS) para o client $CID, ou criar um client só para o AutoSeg")
      PEDIDOS+=("se necessário, incluir ${loc_cli:-server-app} como audience do token desse client")
      PEDIDOS+=("se houver restrição por IP, liberar ${vmip:-a VM do viewer}")
    elif [ "$HC" = 401 ]; then
      DIAG+=("O DICOMweb NÃO aceita o token do client $CID (401), embora aceite o formato Bearer.")
      CAUSAS+=("audience: o DICOMweb exige token destinado a ${loc_cli:-server-app} (o token vem com aud=account)")
      CAUSAS+=("emissor/nome: o token foi emitido por $tiss; o DICOMweb pode esperar outro nome de servidor (DNS)")
      PEDIDOS+=("incluir ${loc_cli:-server-app} como audience do token do client $CID e confirmar o emissor esperado")
    fi
  fi

  # Sem estudo não dá para seguir com séries/imagens
  if [ -z "$STUDY" ]; then
    for p in 8 9 10 11 12; do
      st "$p" "$(nome_passo "$p")" "NÃO TESTADO"
    done
    i "(precisa de um StudyInstanceUID: a busca não devolveu estudos ou foi recusada. Para testar mesmo assim,"
    i " pegue o UID de um estudo no XERO e rode: bash agfa.sh relatorio SECRET <StudyInstanceUID>)"
  else
    [ -n "$STUDY_ORIGEM" ] || STUDY_ORIGEM="informado"
    # [8] Séries -------------------------------------------------------------
    http GET "$BASE/studies/$STUDY/series?includefield=0008103E&includefield=00200011&includefield=00201209" "application/dicom+json" "$TOK"
    if [ "$HC" != 200 ]; then
      local hc1=$HC; corpo
      http GET "$BASE/studies/$STUDY/series" "application/dicom+json" "$TOK"
      [ "$HC" = 200 ] && i "(sem includefield funcionou; o viewer usa esse plano B)"
      [ "$HC" != 200 ] && corpo
    fi
    if [ "$HC" = 200 ]; then
      SERIES_LIST=$("$PY" "$TMP/j.py" series "$TMP/b")
      st 8 "Séries do estudo (QIDO-RS /series)" "OK"
      i "séries: $(printf '%s\n' "$SERIES_LIST" | grep -c .) — modalidades: $(printf '%s\n' "$SERIES_LIST" | awk '{print $1}' | sort | uniq -c | awk '{printf "%s×%s ", $1, $2}')"
      SE=$(printf '%s\n' "$SERIES_LIST" | awk '$1=="CT"||$1=="MR"{print $2; exit}')
      [ -n "$SE" ] || SE=$(printf '%s\n' "$SERIES_LIST" | awk 'NR==1{print $2}')
      i "série de imagens escolhida: $SE"
    else
      st 8 "Séries do estudo (QIDO-RS /series)" "FALHA"
      DIAG+=("A lista de séries do estudo falhou (HTTP $HC).")
    fi
    # [9] Instâncias -----------------------------------------------------------
    if [ -n "$SE" ]; then
      http GET "$BASE/studies/$STUDY/series/$SE/instances" "application/dicom+json" "$TOK"
      if [ "$HC" = 200 ]; then
        SOP=$("$PY" "$TMP/j.py" first "$TMP/b" 00080018)
        st 9 "Imagens da série (QIDO-RS /instances)" "OK"; i "imagens: $("$PY" "$TMP/j.py" count "$TMP/b"), primeira: $SOP"
      else st 9 "Imagens da série (QIDO-RS /instances)" "FALHA"; corpo; DIAG+=("A lista de imagens da série falhou (HTTP $HC)."); fi
    else st 9 "Imagens da série (QIDO-RS /instances)" "NÃO TESTADO"; i "depende da lista de séries (passo 8)"; fi
    # [10] Metadados -----------------------------------------------------------
    if [ -n "$SE" ]; then
      http GET "$BASE/studies/$STUDY/series/$SE/metadata" "application/dicom+json" "$TOK"
      if [ "$HC" != 200 ]; then corpo; http GET "$BASE/studies/$STUDY/series/$SE/metadata" 'multipart/related; type="application/dicom+json"' "$TOK"; fi
      if [ "$HC" = 200 ]; then st 10 "Metadados da série (WADO-RS /metadata)" "OK"
      else st 10 "Metadados da série (WADO-RS /metadata)" "FALHA"; corpo; DIAG+=("Os metadados da série (WADO-RS) falharam (HTTP $HC)."); fi
    else st 10 "Metadados da série (WADO-RS /metadata)" "NÃO TESTADO"; i "depende da lista de séries (passo 8)"; fi
    # [11] Imagem ---------------------------------------------------------------
    if [ -n "$SOP" ] && [ "$SOP" != "-" ]; then
      http GET "$BASE/studies/$STUDY/series/$SE/instances/$SOP" 'multipart/related; type="application/dicom"' "$TOK"
      if [ "$HC" != 200 ]; then corpo; http GET "$BASE/studies/$STUDY/series/$SE/instances/$SOP" 'application/dicom' "$TOK"; fi
      if [ "$HC" = 200 ]; then
        st 11 "Download da imagem (WADO-RS /instances/{sop})" "OK"
        i "sintaxe de transferência: $("$PY" "$TMP/j.py" ts "$TMP/b")"
      else
        st 11 "Download da imagem (WADO-RS /instances/{sop})" "FALHA"; corpo
        DIAG+=("O download da imagem (WADO-RS) falhou (HTTP $HC).")
      fi
    else st 11 "Download da imagem (WADO-RS /instances/{sop})" "NÃO TESTADO"; i "depende da lista de imagens (passo 9)"; fi
    # [12] Objetos RT ------------------------------------------------------------
    local rts rtp
    rts=$(printf '%s\n' "$SERIES_LIST" | awk '$1=="RTSTRUCT"{print $2; exit}')
    rtp=$(printf '%s\n' "$SERIES_LIST" | awk '$1=="RTPLAN"||$1=="RTDOSE"{print $1" "$2; exit}')
    if [ -z "$SERIES_LIST" ]; then
      st 12 "Objetos RT (contornos, plano, dose)" "NÃO TESTADO"; i "depende da lista de séries (passo 8)"
    elif [ -z "$rts$rtp" ]; then
      st 12 "Objetos RT (contornos, plano, dose)" "NÃO TESTADO"; i "o estudo testado não tem RTSTRUCT/RTPLAN/RTDOSE"
    else
      ok=1
      if [ -n "$rts" ]; then
        http GET "$BASE/studies/$STUDY/series/$rts/instances" "application/dicom+json" "$TOK"; [ "$HC" = 200 ] || ok=0
      fi
      if [ -n "$rtp" ]; then
        http GET "$BASE/studies/$STUDY/series/${rtp#* }/metadata" "application/dicom+json" "$TOK"
        if [ "$HC" != 200 ]; then corpo; http GET "$BASE/studies/$STUDY/series/${rtp#* }/metadata" 'multipart/related; type="application/dicom+json"' "$TOK"; fi
        [ "$HC" = 200 ] || ok=0
      fi
      if [ $ok = 1 ]; then st 12 "Objetos RT (contornos, plano, dose)" "OK"
      else st 12 "Objetos RT (contornos, plano, dose)" "FALHA"; corpo; DIAG+=("A leitura dos objetos RT falhou."); fi
    fi
  fi

  # [13] Busca com filtros (tela de pesquisa) -----------------------------------
  local fr=() fa=1 q
  for q in "PatientName=A*" "PatientID=*" "StudyDate=$(date -d '-1 year' +%Y%m%d 2>/dev/null || echo 20250101)-$(date +%Y%m%d)" \
           "ModalitiesInStudy=CT" "AccessionNumber=*" "offset=5" "includefield=all"; do
    c -o "$TMP/b" -w '%{http_code}' -H "Authorization: Bearer $TOK" -H 'Accept: application/dicom+json' \
      "$BASE/studies?limit=5&$q" > "$TMP/x" 2>/dev/null
    fr+=("HTTP $(cat "$TMP/x") ($("$PY" "$TMP/j.py" count "$TMP/b" 2>/dev/null || echo -) estudos)  GET $BASE/studies?limit=5&$q")
    case $(cat "$TMP/x") in 200|204) ;; *) fa=0 ;; esac
  done
  if [ $fa = 1 ]; then st 13 "Busca com filtros (para a tela de pesquisa)" "OK"
  elif [ "$qido_code" = 200 ]; then st 13 "Busca com filtros (para a tela de pesquisa)" "AVISO"
  else st 13 "Busca com filtros (para a tela de pesquisa)" "FALHA"; fi
  for q in "${fr[@]}"; do printf '        %s\n' "$q"; done

  # [14] WADO-URI ----------------------------------------------------------------
  if [ -z "$WADOURI" ]; then
    st 14 "WADO-URI (opcional)" "NÃO TESTADO"; i "não configurado (o viewer usa WADO-RS; WADO-URI é só plano B)"
  elif [ -n "$SOP" ] && [ "$SOP" != "-" ]; then
    http GET "$WADOURI?requestType=WADO&studyUID=$STUDY&seriesUID=$SE&objectUID=$SOP&contentType=application/dicom" "application/dicom" "$TOK"
    if [ "$HC" = 200 ]; then st 14 "WADO-URI (opcional)" "OK"; else st 14 "WADO-URI (opcional)" "AVISO"; corpo; fi
  else st 14 "WADO-URI (opcional)" "NÃO TESTADO"; fi

  resumo
}

nome_passo() {
  case $1 in
    8) echo "Séries do estudo (QIDO-RS /series)" ;; 9) echo "Imagens da série (QIDO-RS /instances)" ;;
    10) echo "Metadados da série (WADO-RS /metadata)" ;; 11) echo "Download da imagem (WADO-RS /instances/{sop})" ;;
    12) echo "Objetos RT (contornos, plano, dose)" ;;
  esac
}

resumo() {
  echo
  echo "=========================================================================="
  echo "RESUMO"
  i "OK ..........: ${R_OK[*]:-nenhum}"
  i "AVISO .......: ${R_AVISO[*]:-nenhum}"
  i "FALHA .......: ${R_FALHA[*]:-nenhuma}"
  i "NÃO TESTADO .: ${R_NT[*]:-nenhum}"
  echo
  if [ ${#R_FALHA[@]} -eq 0 ] && [ ${#DIAG[@]} -eq 0 ]; then
    echo "DIAGNÓSTICO: tudo o que o viewer usa do DICOMweb funcionou."
    i "próximo passo: bash agfa.sh instalar SECRET"
  else
    echo "DIAGNÓSTICO"
    local d; for d in "${DIAG[@]}"; do i "- $d"; done
    if [ ${#CAUSAS[@]} -gt 0 ]; then
      echo; echo "CAUSAS POSSÍVEIS (da mais provável para a menos provável)"
      local n=1; for d in "${CAUSAS[@]}"; do i "$n. $d"; n=$((n+1)); done
    fi
    if [ ${#PEDIDOS[@]} -gt 0 ]; then
      echo; echo "O QUE PRECISAMOS DO ADMINISTRADOR DO AGFA"
      for d in "${PEDIDOS[@]}"; do i "- $d"; done
    fi
  fi
  echo
  echo "COMO REPRODUZIR (secret e token omitidos)"
  i "TOKEN=\$(curl -s --resolve $H:$(porta "$TURL"):$IP --cacert $CAF -X POST '$TURL' \\"
  i "   -d grant_type=client_credentials -d client_id=$CID -d client_secret=<SECRET> | sed 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/')"
  i "curl -i --resolve $H:$(porta "$BASE"):$IP --cacert $CAF -H \"Authorization: Bearer \$TOKEN\" \\"
  i "   -H 'Accept: application/dicom+json' '$BASE/studies?limit=1'"
  if [ -n "$PRIMEIRA_FALHA" ]; then
    echo
    echo "ENGLISH SUMMARY (for Agfa support)"
    i "Client-credentials login for client '$CID' (realm EI): $( [ "${TOK:-}" ] && echo 'OK, token issued' || echo 'FAILED')."
    i "First failing step: [$PRIMEIRA_FALHA]. DICOMweb base: $BASE"
    [ -n "${qido_code:-}" ] && i "GET $BASE/studies with that Bearer token -> HTTP ${qido_code}."
    [ -n "${TOK:-}" ] && [ -n "$PY" ] && i "Token claims: $("$PY" "$TMP/j.py" short "$TOK" 2>/dev/null)"
    case ${qido_code:-} in
      403) i "The token is accepted (a fake token gets 401) but every DICOMweb path returns 403."
           i "We need read access (QIDO-RS/WADO-RS) to the EI DICOMweb for this service account"
           i "(or a dedicated client), and the DICOMweb client as token audience if required." ;;
      401) i "DICOMweb rejects this valid token with 401: likely audience/issuer mismatch." ;;
      *)   i "See the step-by-step report above for the failing request and response." ;;
    esac
  fi
}

relatorio() {
  [ -n "$PY" ] || echo "aviso: sem python3; contagens e conteúdo do token não serão mostrados."
  garantir_ca
  local out=/root/agfa-relatorio-$(date +%Y%m%d-%H%M).txt
  [ -w /root ] || out=./agfa-relatorio-$(date +%Y%m%d-%H%M).txt
  relatorio_corpo "$@" 2>&1 | tee "$out"
  echo
  echo "Relatório salvo em: $out"
}

instalar() {
  garantir_ca
  cd "$TMP" || die "sem diretório temporário"
  curl -fsSLo install-viewer.sh "$PKG/$VER/install-viewer.sh" || die "não baixei o instalador $VER"
  curl -fsSLo SHA256SUMS "$PKG/$VER/SHA256SUMS" || die "não baixei o SHA256SUMS"
  sha256sum -c SHA256SUMS || die "SHA-256 do instalador não confere"
  bash install-viewer.sh --yes --ip "$VM" \
    --pacs-host "$H=$IP" \
    --pacs-url "$BASE" \
    --pacs-token-url "$TURL" \
    --pacs-client-id "$CID" \
    --pacs-client-secret "$SEC" \
    --pacs-ca "$CAF" "$@"
}

"$ACAO" "$@"
