#!/usr/bin/env bash
#
# go-live-check.sh — checklist mecânico do go-live (ADR-042 / F6.I3.T3).
#
# Não substitui o e2e/axe (isso é `make check E2E=1`): ele cobre o que depende do **ambiente real**
# e muda por cliente — páginas críticas no ar, arquivos de raiz do SEO, cache/ETag e os cabeçalhos
# de segurança do edge (ADR-020/ADR-022/ADR-027).
#
# Uso:
#   tools/go-live-check.sh                        # usa BASE_URL (ou WEB_PORT do .env)
#   tools/go-live-check.sh --url https://portal.exemplo.gov.br
#   tools/go-live-check.sh --path /diario-oficial --path /licitacoes   # acrescenta páginas
#   tools/go-live-check.sh --json                 # saída JSON (para o kd)
#   tools/go-live-check.sh --require-hsts         # exige HSTS (produção; default só em I3)
#   tools/go-live-check.sh --require-deny         # exige 403 do Apache nos caminhos proibidos
#   tools/go-live-check.sh --require-hygiene      # exige .env 600, sem public/storage e /storage/ negado
#
# Sai != 0 se qualquer item falhar — é isso que entra no registro do go-live.
set -Eeuo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"

env_value() {
    local key="$1" fallback="$2" value
    value="$(grep -E "^${key}=" .env 2>/dev/null | tail -1 | cut -d= -f2- | tr -d "\"' ")"
    printf '%s' "${value:-$fallback}"
}

BASE_URL="${BASE_URL:-http://localhost:$(env_value WEB_PORT 8080)}"
JSON=0
REQUIRE_HSTS=0
REQUIRE_DENY=0
REQUIRE_HYGIENE=0
EXTRA_PATHS=()

while [ $# -gt 0 ]; do
    case "$1" in
        --url) shift; BASE_URL="${1:-}" ;;
        --url=*) BASE_URL="${1#*=}" ;;
        --path) shift; EXTRA_PATHS+=("${1:-}") ;;
        --path=*) EXTRA_PATHS+=("${1#*=}") ;;
        --json) JSON=1 ;;
        --require-hsts) REQUIRE_HSTS=1 ;;
        --require-deny) REQUIRE_DENY=1 ;;
        --require-hygiene) REQUIRE_HYGIENE=1 ;;
        -h | --help) sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *) echo "flag desconhecida: $1" >&2; exit 2 ;;
    esac
    shift
done

# Páginas críticas do MVP (módulos de F4/F5): 200 + metadata + cabeçalhos.
PATHS=(
    '/'
    '/noticias'
    '/dados-abertos'
    '/politica-de-privacidade'
    '/lgpd'
    '/sitemap.xml'
    '/robots.txt'
    '/manifest.webmanifest'
    '/.well-known/security.txt'
)
for extra in "${EXTRA_PATHS[@]:-}"; do
    [ -n "$extra" ] && PATHS+=("$extra")
done

fail=0
results=()

# check <caminho> — 200 + `Content-Type` + cache coerente; devolve a linha do relatório.
check() {
    local path="$1" headers status ctype cache csp nosniff refpol
    # Sem `-f`: um 404/500 tem de aparecer como "HTTP 404" no relatório, não como "sem resposta".
    headers="$(curl -sS -D - -o /dev/null --max-time 20 "${BASE_URL}${path}" 2>/dev/null || true)"

    if [ -z "$headers" ]; then
        results+=("FAIL|${path}|sem resposta")
        fail=1
        return
    fi

    status="$(printf '%s' "$headers" | awk 'NR==1 {print $2}')"
    ctype="$(printf '%s' "$headers" | tr -d '\r' | grep -i '^content-type:' | head -1 | cut -d: -f2- | xargs || true)"
    cache="$(printf '%s' "$headers" | tr -d '\r' | grep -i '^cache-control:' | head -1 | cut -d: -f2- | xargs || true)"
    csp="$(printf '%s' "$headers" | tr -d '\r' | grep -i '^content-security-policy:' | head -1 | cut -d: -f2- | xargs || true)"
    nosniff="$(printf '%s' "$headers" | tr -d '\r' | grep -ic '^x-content-type-options:[[:space:]]*nosniff' || true)"
    refpol="$(printf '%s' "$headers" | tr -d '\r' | grep -ic '^referrer-policy:' || true)"

    local notes=() bad=0

    [ "$status" = "200" ] || { notes+=("HTTP ${status}"); bad=1; }
    [ -n "$ctype" ] || { notes+=("sem Content-Type"); bad=1; }
    [ -n "$csp" ] || { notes+=("sem CSP"); bad=1; }
    [ "$nosniff" -ge 1 ] || { notes+=("sem X-Content-Type-Options"); bad=1; }
    [ "$refpol" -ge 1 ] || { notes+=("sem Referrer-Policy"); bad=1; }

    case "$path" in
        /.well-known/security.txt)
            printf '%s' "$ctype" | grep -qi '^text/plain' \
                || { notes+=("Content-Type não text/plain"); bad=1; }
            ;;
    esac

    # HTML público: cache explícito (ETag/304 é coberto pelo e2e). Arquivos de raiz: `public`.
    case "$path" in
        /sitemap.xml|/robots.txt|/manifest.webmanifest|/.well-known/security.txt)
            printf '%s' "$cache" | grep -qi 'public' || { notes+=("raiz sem Cache-Control public"); bad=1; }
            ;;
        *)
            [ -n "$cache" ] || { notes+=("sem Cache-Control"); bad=1; }
            ;;
    esac

    if [ "$bad" -eq 0 ]; then
        results+=("OK|${path}|200 · ${ctype:-?}")
    else
        results+=("FAIL|${path}|${notes[*]}")
        fail=1
    fi
}

json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }

echo "==> go-live: ${BASE_URL}"
for path in "${PATHS[@]}"; do
    check "$path"
done

# ── Borda: compressão, 304, immutable, cookies, arquivos negados e TRACE ───────────
# Tudo aqui passa no stack local hoje e falha no host se o proxy/edge divergir. O modo
# `--require-hsts`/`--require-deny` é opt-in até I3 (quando o Apache do WHM entra).
add_check() { # <status> <rótulo> <detalhe>
    results+=("$1|$2|$3")
    [ "$1" = OK ] || fail=1
}

http_headers() { curl -sS -D - -o /dev/null --max-time 20 "$@" 2>/dev/null || true; }
http_code() { curl -sS -o /dev/null -w '%{http_code}' --max-time 20 "$@" 2>/dev/null || true; }
# `xargs` remove as aspas do ETag (vira If-None-Match inválido e o 304 nunca acontece).
header_of() { printf '%s' "$1" | tr -d '\r' | grep -i "^$2:" | head -1 | cut -d: -f2- | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' || true; }

h="$(http_headers -H 'Accept-Encoding: gzip' "${BASE_URL}/")"
enc="$(header_of "$h" 'Content-Encoding')"
case "$enc" in
    gzip|br|zstd) add_check OK '#compress' "$enc" ;;
    *) add_check FAIL '#compress' "sem Content-Encoding (${enc:-?})" ;;
esac

etag="$(header_of "$(http_headers "${BASE_URL}/")" 'ETag')"
if [ -z "$etag" ]; then
    add_check FAIL '#304' 'sem ETag'
else
    code="$(http_code -H "If-None-Match: $etag" "${BASE_URL}/")"
    [ "$code" = "304" ] && add_check OK '#304' "$code" || add_check FAIL '#304' "HTTP ${code:-?}"
fi

getag="$(header_of "$(http_headers -H 'Accept-Encoding: gzip' "${BASE_URL}/")" 'ETag')"
if [ -z "$getag" ]; then
    add_check FAIL '#304-gzip' 'sem ETag comprimido'
else
    code="$(http_code -H 'Accept-Encoding: gzip' -H "If-None-Match: $getag" "${BASE_URL}/")"
    [ "$code" = "304" ] && add_check OK '#304-gzip' "$code" || add_check FAIL '#304-gzip' "HTTP ${code:-?}"
fi

html="$(curl -sS --max-time 20 "${BASE_URL}/" 2>/dev/null || true)"
asset="$(printf '%s' "$html" | grep -oE '/build/assets/[A-Za-z0-9._/-]+\.(js|css)' | head -1 || true)"
if [ -z "$asset" ]; then
    add_check FAIL '#immutable' 'asset do Vite não encontrado no HTML'
else
    cc="$(header_of "$(http_headers "${BASE_URL}${asset}")" 'Cache-Control')"
    case "$cc" in
        *immutable*) add_check OK '#immutable' "$asset" ;;
        *) add_check FAIL '#immutable' "$asset sem immutable (${cc:-?})" ;;
    esac
fi

if printf '%s' "$(http_headers "${BASE_URL}/")" | grep -qi '^set-cookie:'; then
    add_check FAIL '#cookie' 'Set-Cookie em HTML público'
else
    add_check OK '#cookie' 'ausente'
fi

for path in '/.env' '/composer.json' '/artisan'; do
    code="$(http_code "${BASE_URL}${path}")"
    if [ "$REQUIRE_DENY" = 1 ]; then
        [ "$code" = "403" ] && add_check OK "#deny ${path}" "$code" \
            || add_check FAIL "#deny ${path}" "HTTP ${code:-?} (esperado 403)"
    else
        case "$code" in
            403|404) add_check OK "#deny ${path}" "$code" ;;
            *) add_check FAIL "#deny ${path}" "HTTP ${code:-?}" ;;
        esac
    fi
done

code="$(http_code -X TRACE "${BASE_URL}/")"
case "$code" in
    403|405) add_check OK '#trace' "$code" ;;
    *) add_check FAIL '#trace' "HTTP ${code:-?}" ;;
esac

if [ "$REQUIRE_HYGIENE" = 1 ]; then
    # I6: o deploy de produção não pode deixar o `.env` legível nem servir `public/storage`.
    if [ -f .env ]; then
        mode="$(stat -c '%a' .env 2>/dev/null || stat -f '%Lp' .env 2>/dev/null || echo '?')"
        [ "$mode" = "600" ] && add_check OK '#env-mode' "$mode" \
            || add_check FAIL '#env-mode' "$mode (esperado 600)"
    else
        add_check FAIL '#env-mode' '.env ausente (rode dentro do deploy/container)'
    fi
    if [ -e public/storage ]; then
        add_check FAIL '#public-storage' 'symlink/docroot de storage presente'
    else
        add_check OK '#public-storage' 'ausente'
    fi
    code="$(http_code "${BASE_URL}/storage/")"
    case "$code" in
        403|404) add_check OK '#storage-url' "$code" ;;
        *) add_check FAIL '#storage-url' "HTTP ${code:-?} (o docroot não pode servir storage)" ;;
    esac
fi

if [ "$REQUIRE_HSTS" = 1 ]; then
    if printf '%s' "$(http_headers "${BASE_URL}/")" | grep -qi '^strict-transport-security:'; then
        add_check OK '#hsts' 'presente'
    else
        add_check FAIL '#hsts' 'ausente'
    fi
fi

if [ "$JSON" = 1 ]; then
    printf '{"event":"go_live_check","base_url":"%s","checks":[' "$(json_escape "$BASE_URL")"
    sep=""
    for line in "${results[@]}"; do
        IFS='|' read -r st path detail <<<"$line"
        printf '%s{"path":"%s","status":"%s","detail":"%s"}' "$sep" "$(json_escape "$path")" "$(json_escape "$st")" "$(json_escape "$detail")"
        sep=","
    done
    printf '],"ok":%s}\n' "$([ "$fail" -eq 0 ] && echo true || echo false)"
else
    printf '%-32s %-6s %s\n' "CAMINHO" "STATUS" "OBSERVAÇÃO"
    printf '%s\n' "-------------------------------------------------------------"
    for line in "${results[@]}"; do
        IFS='|' read -r st path detail <<<"$line"
        printf '%-32s %-6s %s\n' "$path" "$st" "$detail"
    done
fi

if [ "$fail" -ne 0 ]; then
    echo "==> FALHOU — corrija antes de declarar o go-live." >&2
    exit 1
fi

echo "==> OK — páginas críticas no ar, arquivos de raiz e cabeçalhos coerentes."
echo "    (a11y/axe e e2e são do \`make check E2E=1\`; usuários: \`core:user:create\`; 301: \`seo:redirect\`)"
exit 0
