#!/usr/bin/env bash
# Diagnóstico rápido: estado dos containers + saúde do app (Supabase/Redis/WAHA).
#
# CÓDIGO DE SAÍDA: 0 só quando o app se declara `healthy` no /api/v1/health
# (e a sonda conseguiu responder). Qualquer outro desfecho — `degraded`,
# `unhealthy`, app mudo, JSON ilegível — sai 1, depois de imprimir o resto do
# diagnóstico. Até o #4087 ele saía 0 sempre, então nem cron nem monitor
# conseguiam usar isto como alarme.
source "$(dirname "$0")/_common.sh"
enter_project

# Assume reprovado até a sonda provar o contrário (falha fechada).
saude_app_ok=1

step "Containers"
# MEDIDO (#1955): com o resolver do Docker saturado (`dial udp 8.8.4.4:53:
# i/o timeout`), o `docker ps` também travava — e o healthcheck ficava preso em
# "▶ Containers" para sempre, justamente na tela que o dono abre para descobrir
# o que aconteceu. Com prazo ele diz que o Docker não respondeu e o resto do
# diagnóstico continua rodando.
if com_prazo 45 docker compose $(dc_files) ps; then
  :
else
  c_red "⛔ O Docker não respondeu em 45s. Ou o daemon está travado, ou o resolver"
  c_red "   está saturado (journalctl -u docker -n 100 | tail). Enquanto isso, os"
  c_red "   comandos de docker continuam travando: reinicie o Docker com"
  c_red "   systemctl restart docker."
fi

step "Saúde interna do app (/api/v1/health)"
# Roda de dentro da rede do compose (a rota não é exposta publicamente sem TLS).
# Mesmo prazo do `ps` acima, pelo mesmo motivo (#1955): um `docker exec` com o
# resolver saturado travava aqui também, e este era o segundo ponto em que o
# healthcheck ficava sem resposta nenhuma.
#
# #4087: quem responde "saudável ou não" é o status GERAL da rota, e a decisão
# mora num lugar só (relatar_saude_do_app, _common.sh) — a mesma sonda que o
# install.sh e o update.sh usam. Aqui havia um grep pelo status `ok` no JSON
# cru: `ok` é o vocabulário dos CHECKS (ok|degraded|down), não o do status
# geral (healthy|degraded|unhealthy), então `checks.redis.status` casava e a
# linha verde saía com o BANCO FORA.
saida="$(app_health_probe 30)"
if [ -n "$saida" ]; then
  case "$saida" in
    *$'\n'*) printf '%s\n' "${saida#*$'\n'}" ;;   # o corpo, para o dono ver
  esac
  relatar_saude_do_app "$saida" && saude_app_ok=0
else
  c_ylw "⚠ app não respondeu ao /api/v1/health. Logs: docker compose $(dc_files) logs --tail=50 app"
fi

step "Atualização pela tela (agente do host)"
# Achado numa VPS real: o cron do agente é instalado no TOPO do update.sh, de
# propósito (para sobreviver a uma saída antecipada) — e isso cria uma janela em
# que o agente existe e o schema do banco ainda não. Ele então bate 500 a cada 5
# minutos, com o erro indo só para o .update-agent.log e o `>/dev/null` do cron.
# Para o dono, o sintoma é "o botão nunca apareceu": nenhuma pista, nenhum alarme.
# Este bloco é a pista.
if crontab -l 2>/dev/null | grep -q 'hostgator-setup-kit/agent.sh'; then
  c_grn "✓ agente instalado no cron (a cada 5 minutos)"
  log="$PROJECT_DIR/.update-agent.log"
  # Recência pela MTIME do arquivo, não parseando a data de dentro: `date -d` é
  # sintaxe GNU e falha calado no BSD/macOS, devolvendo "sem falhas" para um
  # agente que está falhando agora. `find -mmin` é portátil.
  if [ -s "$log" ] && [ -n "$(find "$log" -mmin -120 2>/dev/null)" ]; then
    c_ylw "⚠ o agente falhou recentemente ao falar com o app:"
    # A última ENTRADA, não a penúltima linha: uma falha de POST ocupa várias
    # linhas (`<data> [agent] POST <url> -> <corpo>`, o código HTTP numa linha
    # própria, e uma linha vazia no meio quando o corpo termina em \n) — o
    # `tail -2 | head -1` de antes caía na vazia. Junta as linhas não vazias
    # desde o último cabeçalho `[agent]`. O log não leva o Bearer (agent.sh, post()).
    # Cortada em 300 caracteres: o corpo pode ser uma página de erro HTML de KBs
    # (proxy na frente do app). O `sub` tira o espaço da frente quando o
    # `tail -n 200` do log_err já levou o cabeçalho embora.
    c_ylw "  $(awk '/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[^ ]* \[agent\] /{e=$0; next} NF{e=e" "$0} END{sub(/^ +/, "", e); if (length(e) > 300) e = substr(e, 1, 300) "..."; print e}' "$log")"
    c_ylw "  Se o botão de atualizar não aparece na tela, é por isto."
    c_ylw "  Quase sempre resolve rodando: bash hostgator-setup-kit/update.sh"
  else
    c_grn "✓ sem falhas recentes do agente"
  fi
else
  c_ylw "⚠ o agente NÃO está no cron — o botão de atualizar não vai aparecer na tela."
  c_ylw "  Ative rodando: bash hostgator-setup-kit/update.sh"
fi

step "E-mails de acesso (confirmar conta e redefinir senha)"
# ── Por que esta seção existe ────────────────────────────────────────────────
# Num Supabase PRÓPRIO, quem renderiza estes dois e-mails é o GoTrue, e o molde
# padrão dele linka para `/auth/v1/verify`, que devolve um `code` PKCE. O
# verificador desse code vive num cookie SameSite=Strict, e clique vindo de
# webmail é navegação cross-site: o cookie não viaja e a sessão nunca fecha.
# Medido em produção em 2026-09-10 — a conta era confirmada e a pessoa entrava
# sem organização e sem menu.
#
# O conserto é apontar `GOTRUE_MAILER_TEMPLATES_*` para a rota do app. Como o
# GoTrue não é serviço deste compose (o kit sobe app, worker, scheduler, waha,
# redis, srh e caddy — o Supabase próprio fica FORA), o kit não tem como
# escrever essa configuração. A exceção é o modo single-server, em que o
# Supabase é do kit: lá o install-single-server.sh e o update.sh gravam as duas
# chaves (gravar_modelos_do_gotrue, _common.sh — #2109). Para o resto, o que o
# kit pode, e é o que faz aqui, é MEDIR o estado e dizer as duas linhas exatas. Silêncio aqui seria o `return` mudo que
# o invariante 6(c) do Sistema Vivo proíbe.
case "${NEXT_PUBLIC_SUPABASE_URL:-}" in
  https://*.supabase.co*)
    c_grn "✓ Supabase na nuvem — os e-mails são configurados pela Management API."
    c_dim "  Quem cuida disso é: bash hostgator-setup-kit/marca-emails.sh"
    ;;
  "")
    c_ylw "⚠ NEXT_PUBLIC_SUPABASE_URL vazia no .env — não dá para saber a topologia."
    ;;
  *)
    c_dim "  Supabase próprio: aqui não existe Management API, a ligação é por env do GoTrue."

    # (a) A rota do app responde? Perguntamos de DENTRO da rede do compose, como
    #     na seção de saúde acima: a rota é pública, mas o host pode estar atrás
    #     de proxy e um erro de TLS aqui seria diagnóstico errado.
    molde="$(dc exec -T app node -e "
fetch('http://127.0.0.1:3000/email-templates/confirmation').then(r=>r.text()).then(t=>{console.log(t);process.exit(0)}).catch(()=>process.exit(1))
" 2>/dev/null || echo '')"
    if printf '%s' "$molde" | grep -q 'token_hash={{ .TokenHash }}'; then
      c_grn "✓ o app serve o molde em /email-templates/confirmation"
    elif [ -n "$molde" ]; then
      c_ylw "⚠ /email-templates/confirmation respondeu, mas sem o token_hash esperado."
      c_ylw "  Esta versão do app é anterior à correção. Rode: bash hostgator-setup-kit/update.sh"
    else
      c_ylw "⚠ o app não serviu /email-templates/confirmation."
      c_ylw "  Ou está fora do ar, ou é uma versão anterior à correção."
    fi

    # (b) O GoTrue desta máquina está apontado para lá? Ele não é nosso, então
    #     lemos o ambiente de QUALQUER contêiner que declare a variável — é
    #     leitura, não escrita, e é o único jeito de responder sem adivinhar.
    apontado=""; dono=""
    # A chave sai de uma VARIÁVEL, não literal dentro do `sed`: o
    # test-validators.sh varre o kit cobrando que todo `GOTRUE_MAILER_TEMPLATES_*=`
    # escrito aqui seja URL http(s), e um `s/^CHAVE=//p` entraria nessa varredura
    # como se `//p` fosse o valor configurado.
    chave=GOTRUE_MAILER_TEMPLATES_CONFIRMATION
    for c in $(docker ps --format '{{.Names}}' 2>/dev/null); do
      v="$(docker inspect --format \
        '{{range .Config.Env}}{{println .}}{{end}}' "$c" 2>/dev/null \
        | sed -n "s/^${chave}=//p" | head -1)"
      [ -n "$v" ] && { apontado="$v"; dono="$c"; break; }
    done
    if [ -z "$apontado" ]; then
      c_ylw "⚠ nenhum GoTrue desta máquina aponta para o molde do app."
      c_ylw "  Os e-mails de acesso vão sair no modelo padrão, e o link dele NÃO fecha"
      c_ylw "  a sessão quando o clique vem do webmail."
      if [ "${SINGLE_SERVER:-0}" = "1" ]; then
        # O Supabase é do kit: quem grava é o update.sh, pela rede privada (#2587).
        c_ylw "  Rode: bash hostgator-setup-kit/update.sh  (ele grava os moldes no auth deste Supabase:"
        c_ylw "    GOTRUE_MAILER_TEMPLATES_CONFIRMATION=${BASE_INTERNA_DO_APP:-http://app:3000}/email-templates/confirmation"
        c_ylw "    GOTRUE_MAILER_TEMPLATES_RECOVERY=${BASE_INTERNA_DO_APP:-http://app:3000}/email-templates/recovery )"
      else
        c_ylw "  Acrescente ao serviço 'auth' do SEU Supabase (não a este compose):"
        c_ylw "    GOTRUE_MAILER_TEMPLATES_CONFIRMATION=${NEXT_PUBLIC_APP_URL:-https://SEU_DOMINIO}/email-templates/confirmation"
        c_ylw "    GOTRUE_MAILER_TEMPLATES_RECOVERY=${NEXT_PUBLIC_APP_URL:-https://SEU_DOMINIO}/email-templates/recovery"
        c_dim "  (se o seu Supabase roda em outra máquina, confira lá — daqui não dá para ver)"
      fi
    else
      # #2587 — no single-server, o padrão ANTIGO do kit (o domínio público) faz
      # o auth buscar o molde pelo IP da própria VPS; atrás de um Traefik externo
      # isso dá timeout. O update.sh migra esse valor para a rede privada.
      if [ "${SINGLE_SERVER:-0}" = "1" ] && [ -n "${NEXT_PUBLIC_APP_URL:-}" ] \
         && [ "$apontado" = "${NEXT_PUBLIC_APP_URL%/}/email-templates/confirmation" ]; then
        c_ylw "⚠ o GoTrue ($dono) busca o molde pelo domínio público, não pela rede privada."
        c_ylw "  Atrás de um proxy externo essa busca pode dar timeout. Rode: bash hostgator-setup-kit/update.sh"
        c_dim "  $apontado"
        apontado=""  # já avisado: não cair no ✓ abaixo, que diria o contrário
      fi
      case "$apontado" in
        "") ;;
        http*/email-templates/*)
          c_grn "✓ o GoTrue ($dono) busca o molde do app"
          c_dim "  $apontado" ;;
        http*)
          c_ylw "⚠ o GoTrue ($dono) busca um molde que não é o do app:"
          c_ylw "  $apontado" ;;
        *)
          c_red "✗ o GoTrue ($dono) está com CAMINHO DE ARQUIVO, não URL:"
          c_red "  $apontado"
          c_red "  O GoTrue cola isso no fim do SITE_URL e busca por HTTP — o cliente"
          c_red "  recebe a tela de login dentro do e-mail. Troque por uma URL http(s)." ;;
      esac
    fi
    ;;
esac

step "TLS do banco (Supabase)"
# ── Por que este passo existe (#829) ──────────────────────────────────────────
# Instalação sem patches permanentes falhou nos diagnósticos com
# `SELF_SIGNED_CERT_IN_CHAIN`: a cadeia do pooler não está na trust store
# padrão, e o erro cru não diz o que fazer. Aqui o teste roda com verificação
# TOTAL (sslmode=verify-full + sslrootcert, montado pelo pg_container) e, quando
# a CA não está declarada, a frase vem com o NOME da variável que falta — é o
# que a issue pede. Nada é desligado para o teste passar.
#
# A chave é OPCIONAL, e o passo trata a ausência dela como opção, não como
# defeito: sem SUPABASE_SSL_ROOT_CERT a linha é informativa (c_dim), sem
# instrução de download. Em amarelo, ela mandaria TODA instalação existente —
# a maioria não exige verificação de certificado — baixar uma CA de que não
# precisa. No single-server o banco é o Postgres desta máquina, e a CA da nuvem
# não se aplica. O amarelo fica para quem DECLAROU a CA e ela não funciona.
if [ "${SINGLE_SERVER:-0}" = "1" ]; then
  c_dim "  não se aplica: no single-server o banco é o Postgres desta máquina, não o pooler da nuvem."
elif [ -z "${SUPABASE_SSL_ROOT_CERT:-}" ]; then
  c_dim "  (opcional) SUPABASE_SSL_ROOT_CERT não declarada no .env — nada a verificar."
  c_dim "  Só faz falta se a sua conexão exige verificação de certificado (veja o README do kit)."
elif tls_dito="$(tls_do_banco 2>&1)"; then
  c_grn "✓ TLS do banco verificado (sslmode=verify-full com a CA de SUPABASE_SSL_ROOT_CERT)"
else
  while IFS= read -r linha_tls; do
    if [ -n "$linha_tls" ]; then c_ylw "$linha_tls"; fi
  done <<< "$tls_dito"
  c_dim "  (a verificação de certificado continua ligada — nada foi desligado para este teste.)"
fi

# Veredito. Só aqui, no fim, para o dono ver o diagnóstico INTEIRO mesmo quando o
# app está fora — e para o cron/monitor ter um sinal de máquina. `degraded` sai 1
# junto com `unhealthy`: na instalação o `degraded` passa (wait_app_healthy, ele
# quer dizer "opcional ainda não configurado"), mas um diagnóstico chamado à mão
# não pode chamar de saudável um app com subsistema fora.
exit "$saude_app_ok"
