#!/usr/bin/env bash
#
# tools/network-check.sh — a matriz de reachability como asserção permanente (D5).
#
# O isolamento de rede é um **contrato**, não uma configuração: o `web` (o único serviço
# exposto) não pode alcançar o banco, o cache ou o storage; o app alcança os três; e os
# serviços de dados não têm saída para a internet (o `internal: true`).
#
# A verificação tem duas metades:
#   1. **estática** — a topologia declarada no Compose (quem está em qual rede, e se a
#      rede de dados é `internal: true`);
#   2. **runtime** — a matriz medida de verdade (o `web` não abre conexão com o
#      Postgres; o app abre; os dados não têm rota padrão).
#
# A metade runtime é o que pega o desvio real: uma rede que "parece" isolada no arquivo
# e não está isolada no container.
#
# Uso: tools/network-check.sh [--static-only]
set -Eeuo pipefail

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

COMPOSE_FILE="${SECURITY_COMPOSE:-docker-compose.yml}"
PROJECT="$(bash "$ROOT/tools/project-name.sh" 2>/dev/null)"
# shellcheck source=tools/runtime.sh
. "$ROOT/tools/runtime.sh"
NC_FILES=(-f "$COMPOSE_FILE")
if [ "$RUNTIME" = "podman" ] && [ "$COMPOSE_FILE" = "docker-compose.yml" ]; then
    NC_FILES+=(-f docker-compose.podman.yml)
fi
COMPOSE=("${ENGINE[@]}" compose -p "$PROJECT" --env-file .env "${NC_FILES[@]}")

rc=0
fail() { echo "network-check: $*" >&2; rc=1; }

# ── 1. Estática: a topologia declarada ─────────────────────────────────────────────
# web/edge: o edge nunca fala com os dados. Os serviços de dados ficam só na `data`.
declare -A ESPERA_REDE=(
    [web]='edge'
    [php-fpm]='edge data'
    [queue]='edge data'
    [postgres]='data'
    [valkey]='data'
    [storage]='data'
    [mailpit]='edge'
    [jaeger]='edge'
)

for svc in "${!ESPERA_REDE[@]}"; do
    if ! grep -q "^  ${svc}:" "$COMPOSE_FILE"; then
        continue
    fi

    bloco="$(sed -n "/^  ${svc}:/,/^  [a-z][a-z0-9-]*:/p" "$COMPOSE_FILE")"
    redes="$(printf '%s' "$bloco" | grep -E '^[[:space:]]+networks:' | head -1 | sed -E 's/.*\[(.*)\].*/\1/')"

    # Comparação de **conjuntos**: a ordem e a vírgula do YAML não importam.
    obtidas="$(printf '%s' "$redes" | tr ',' '\n' | sed 's/[[:space:]]//g' | sort | tr '\n' ' ')"
    esperadas="$(printf '%s' "${ESPERA_REDE[$svc]}" | tr ' ' '\n' | sed '/^$/d' | sort | tr '\n' ' ')"
    if [ "$obtidas" != "$esperadas" ]; then
        fail "${svc} está nas redes [${obtidas:-<nenhuma>}] e o contrato exige [${esperadas}]"
    fi
done

# A rede de dados é `internal: true`: sem rota padrão, sem saída para a internet.
if ! grep -qE '^  data:' "$COMPOSE_FILE"; then
    fail 'a rede `data` não existe no Compose (o isolamento é obrigatório)'
elif ! sed -n '/^  data:/,/^  [a-z]/p' "$COMPOSE_FILE" | grep -qE '^[[:space:]]+internal:[[:space:]]*true'; then
    fail 'a rede `data` não é `internal: true` (os serviços de dados ganham saída para a internet)'
fi

# `web` não pode estar na rede de dados — nem por multi-homing.
if sed -n "/^  web:/,/^  [a-z][a-z0-9-]*:/p" "$COMPOSE_FILE" | grep -qE '^[[:space:]]+networks:.*\bdata\b'; then
    fail 'o serviço `web` está na rede `data` (o edge não fala com os dados)'
fi

if [ "${1:-}" = '--static-only' ]; then
    [ "$rc" -eq 0 ] && echo "network-check: OK (estático)"
    exit "$rc"
fi

# ── 2. Runtime: a matriz medida ───────────────────────────────────────────────────
ip_de() { # <serviço>
    local id
    id="$("${COMPOSE[@]}" ps -q "$1" 2>/dev/null | head -1)"
    [ -n "$id" ] || return 0
    "${ENGINE[@]}" inspect "$id" --format '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null | awk '{print $1}'
}

# A conexão é testada **pelo IP**: o DNS já não resolve entre redes separadas, e o que
# queremos provar é o isolamento de tráfego, não a resolução de nome.
alcanca() { # <serviço> <ip> <porta>
    case "$1" in
        web)
            "${COMPOSE[@]}" exec -T web sh -c "nc -z -w 2 $2 $3" >/dev/null 2>&1 && echo SIM || echo NAO
            ;;
        php-fpm|queue)
            "${COMPOSE[@]}" exec -T "$1" php -r '
                $f = @fsockopen($argv[1], (int) $argv[2], $e, $s, 2);
                echo $f ? "SIM" : "NAO";
            ' "$2" "$3" 2>/dev/null || echo NAO
            ;;
        postgres)
            "${COMPOSE[@]}" exec -T postgres pg_isready -h "$2" -p "$3" -t 2 >/dev/null 2>&1 && echo SIM || echo NAO
            ;;
        valkey)
            "${COMPOSE[@]}" exec -T valkey sh -c "nc -z -w 2 $2 $3" >/dev/null 2>&1 && echo SIM || echo NAO
            ;;
        storage)
            "${COMPOSE[@]}" exec -T storage sh -c "curl -s -o /dev/null --max-time 2 http://$2:$3/health" >/dev/null 2>&1 && echo SIM || echo NAO
            ;;
        *) echo DESCONHECIDO ;;
    esac
}

tem_saida() { # <serviço> — há rota padrão (egress)?
    "${COMPOSE[@]}" exec -T "$1" sh -c "awk '\$2 == \"00000000\" {found=1} END {exit found ? 0 : 1}' /proc/net/route" >/dev/null 2>&1 \
        && echo SIM || echo NAO
}

PG_IP="$(ip_de postgres)"
VK_IP="$(ip_de valkey)"
ST_IP="$(ip_de storage)"

if [ -z "$PG_IP" ] || [ -z "$VK_IP" ] || [ -z "$ST_IP" ]; then
    fail 'não foi possível resolver o IP de postgres/valkey/storage (o stack está no ar?)'
    exit "$rc"
fi

# serviço | alvo | porta | esperado (SIM/NAO)
matriz=(
    "web|postgres|$PG_IP|5432|NAO"
    "web|valkey|$VK_IP|6379|NAO"
    "web|storage|$ST_IP|9000|NAO"
    "php-fpm|postgres|$PG_IP|5432|SIM"
    "php-fpm|valkey|$VK_IP|6379|SIM"
    "php-fpm|storage|$ST_IP|9000|SIM"
    "queue|postgres|$PG_IP|5432|SIM"
    "queue|valkey|$VK_IP|6379|SIM"
    "queue|storage|$ST_IP|9000|SIM"
)

for linha in "${matriz[@]}"; do
    IFS='|' read -r svc alvo ip porta esperado <<<"$linha"
    obtido="$(alcanca "$svc" "$ip" "$porta")"
    if [ "$obtido" != "$esperado" ]; then
        fail "$svc → $alvo:$porta = $obtido (esperado $esperado)"
    fi
done

# Egress: os serviços de dados não saem para a internet; o app sai (o `composer install`
# do deploy precisa) e o edge sai (é a borda).
for svc in postgres valkey storage; do
    if [ "$(tem_saida "$svc")" = "SIM" ]; then
        fail "$svc tem rota padrão (saída para a internet) — a rede de dados deveria ser internal"
    fi
done
for svc in php-fpm queue web; do
    if [ "$(tem_saida "$svc")" = "NAO" ]; then
        fail "$svc não tem rota padrão — o app (composer install) e o edge precisam de saída"
    fi
done

[ "$rc" -eq 0 ] && echo "network-check: OK"
exit "$rc"
